diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 2b10348505..e0be883ca7 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -13,11 +13,11 @@ jobs: if: ${{ !github.event.pull_request.draft }} steps: - name: Checkout - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 + uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '17' distribution: 'adopt' - name: Build run: ./gradlew clean build -x test -x ktlintMainSourceSetCheck @@ -30,11 +30,11 @@ jobs: if: ${{ !github.event.pull_request.draft }} steps: - name: Checkout - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 + uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '17' distribution: 'adopt' - name: Lint run: ./gradlew ktlintCheck @@ -43,22 +43,29 @@ jobs: name: Lint JavaScript runs-on: macos-latest if: ${{ !github.event.pull_request.draft }} - defaults: - run: - working-directory: readium/navigator env: - scripts: ${{ 'src/main/assets/_scripts' }} + scripts: ${{ 'readium/navigator/src/main/assets/_scripts' }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + package_json_file: readium/navigator/src/main/assets/_scripts/package.json + run_install: false + - name: Setup cache + uses: actions/setup-node@v3 + with: + node-version: 20 + cache: 'pnpm' + cache-dependency-path: readium/navigator/src/main/assets/_scripts/pnpm-lock.yaml - name: Install dependencies - run: yarn --cwd "$scripts" install --frozen-lockfile + run: pnpm --dir "$scripts" install --frozen-lockfile - name: Lint - run: yarn --cwd "$scripts" run lint + run: pnpm --dir "$scripts" run lint - name: Check formatting - run: yarn --cwd "$scripts" run checkformat - # FIXME: This suddenly stopped working even though the toolchain versions seem identical. - # - name: Check if bundled scripts are up-to-date - # run: | - # make scripts - # git diff --exit-code --name-only src/main/assets/readium/scripts/*.js + run: pnpm --dir "$scripts" run checkformat + - name: Check if bundled scripts are up-to-date + run: | + make scripts + git diff --exit-code --name-only src/main/assets/readium/scripts/*.js diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 54cc479ab6..4abba3507f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -30,7 +30,7 @@ jobs: - uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 distribution: 'adopt' - name: Get current Readium version diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 32bd05c089..1b8a9556a8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,14 +12,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: develop - - name: Set up JDK 11 - uses: actions/setup-java@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v3 with: distribution: adopt - java-version: 11 + java-version: 17 # Builds the release artifacts of the library - name: Release build diff --git a/.gitignore b/.gitignore index 3070fa2ac9..d46e9173c1 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ captures/ .idea/libraries .idea/jarRepositories.xml .idea/misc.xml +.idea/migrations.xml # Android Studio 3 in .gitignore file. .idea/caches .idea/modules.xml @@ -82,4 +83,5 @@ lint/reports/ docs/readium docs/index.md docs/package-list -site/ \ No newline at end of file +site/ +androidTestResultsUserPreferences.xml diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index e1eea1d6b9..8d81632f83 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="KotlinJpsPluginSettings"> - <option name="version" value="1.7.20" /> + <option name="version" value="1.9.22" /> </component> </project> \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b8faa114d..9a35bded3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,68 @@ All notable changes to this project will be documented in this file. Take a look **Warning:** Features marked as *experimental* may change or be removed in a future release without notice. Use with caution. -<!-- ## [Unreleased] --> +## [Unreleased] + +:warning: Please consult [the migration guide](docs/migration-guide.md#300-alpha1) to assist you in handling the breaking changes in this latest major release. + +### Added + +#### Shared + +* A new `Format` type was introduced to augment `MediaType` with more precise information about the format specifications of an `Asset`. +* The `DownloadManager` interface handles HTTP downloads. Components like the `LcpService` rely on it for downloading publications. Readium v3 ships with two implementations: + * `ForegroundDownloadManager` uses an `HttpClient` to download files while the app is running. + * `AndroidDownloadManager` is built upon [Android's `DownloadManager`](https://developer.android.com/reference/android/app/DownloadManager) to manage HTTP downloads, even when the application is closed. It allows for resuming downloads after losing connection. +* The default `ZipArchiveOpener` now supports streaming ZIP archives, which enables opening a packaged publication (e.g. EPUB or LCP protected audiobook): + * served by a remote HTTP server, + * accessed through an Android `ContentProvider`, such as the shared storage. + +#### Navigator + +* Support for keyboard events in the EPUB, PDF and image navigators. See `VisualNavigator.addInputListener()`. + +#### LCP + +* You can now stream an LCP protected publication using its LCP License Document. This is useful for example to read a large audiobook without downloading it on the device first. +* The hash of protected publications is now verified upon download. + +### Changed + +* :warning: To avoid conflicts when merging your app resources, all resources declared in the Readium toolkit now have the prefix `readium_`. This means that you must rename any layouts or strings you have overridden. Some resources were removed from the toolkit. Please consult [the migration guide](docs/migration-guide.md#300-alpha1). +* Most APIs now return an `Error` instance instead of an `Exception` in case of failure, as these objects are not thrown by the toolkit but returned as values + +#### Shared + +* :warning: To improve the interoperability with other Readium toolkits (in particular the Readium Web Toolkits, which only work in a streaming context) **Readium v3 now generates and expects valid URLs** for `Locator` and `Link`'s `href`. **You must migrate the HREFs or Locators stored in your database**, please consult [the migration guide](docs/migration-guide.md#300-alpha1). +* `Link.href` and `Locator.href` are now respectively `Href` and `Url` objects. If you still need the string value, you can call `toString()` +* `MediaType` no longer has static helpers for sniffing it from a file or URL. Instead, you can use an `AssetRetriever` to retrieve the format of a file. + +#### Navigator + +* Version 3 includes a new component called `DirectionalNavigationAdapter` that replaces `EdgeTapNavigation`. This helper enables users to navigate between pages using arrow and space keys on their keyboard or by tapping the edge of the screen. +* The `onTap` and `onDrag` events of `VisualNavigator.Listener` have been deprecated. You can now use multiple implementations of `InputListener` with `VisualNavigator.addInputListener()`. + +#### Streamer + +* The `Streamer` object has been deprecated in favor of components with smaller responsibilities: `AssetRetriever` and `PublicationOpener`. + +#### LCP + +* `LcpService.acquirePublication()` is deprecated in favor of `LcpService.publicationRetriever()`, which provides greater flexibility thanks to the `DownloadManager`. +* The way the host view of a `LcpDialogAuthentication` is retrieved was changed to support Android configuration changes. + +### Deprecated + +* Both the Fuel and Kovenant libraries have been completely removed from the toolkit. With that, several deprecated functions have also been removed. + +#### Shared + +* The `putPublication` and `getPublication` helpers in `Intent` are deprecated. Now, it is the application's responsibility to pass `Publication` objects between activities and reopen them when necessary. + +#### Navigator + +* EPUB external links are no longer handled by the navigator. You need to open the link in your own Web View or Chrome Custom Tab. + ## [2.4.0] @@ -48,10 +109,35 @@ All notable changes to this project will be documented in this file. Take a look ### Changed +* Readium resources are now prefixed with `readium_`. Take care of updating any overridden resource by following [the migration guide](docs/migration-guide.md#300). +* `Link` and `Locator`'s `href` are normalized as valid URLs to improve interoperability with the Readium Web toolkits. + * **You MUST migrate your database if you were persisting HREFs and Locators**. Take a look at [the migration guide](docs/migration-guide.md) for guidance. + +#### Shared + +* `Publication.localizedTitle` is nullable, as we cannot guarantee that all publication sources offer a title. +* The `MediaType` sniffing helpers are deprecated in favor of `MediaTypeRetriever` (for media type and file extension hints and raw content) and `AssetRetriever` (for URLs). + #### Navigator * `EpubNavigatorFragment.firstVisibleElementLocator()` now returns the first *block* element that is visible on the screen, even if it starts on previous pages. * This is used to make sure the user will not miss any context when restoring a TTS session in the middle of a resource. +* The `VisualNavigator`'s drag and tap listener events are moved to a new `addInputListener()` API. +* The new `DirectionalNavigationAdapter` component replaces `EdgeTapNavigation`, helping you turn pages with the arrow and space keyboard keys, or taps on the edge of the screen. + +### Deprecated + +#### Shared + +* `DefaultHttClient.additionalHeaders` is deprecated. Set all the headers when creating a new `HttpRequest`, or modify outgoing requests in `DefaultHttpClient.Callback.onStartRequest()`. + +#### Navigator + +* All the navigator `Activity` are deprecated in favor of the `Fragment` variants. + +#### Streamer + +* The `Fetcher` interface was deprecated in favor of the `Container` one in `readium-shared`. ### Fixed @@ -63,7 +149,6 @@ All notable changes to this project will be documented in this file. Take a look * Fixed issue with the TTS starting from the beginning of the chapter instead of the current position. - ## [2.3.0] ### Added diff --git a/Makefile b/Makefile index 8e42a5b89f..b03c46f2b2 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,11 @@ format: .PHONY: scripts scripts: - yarn --cwd "$(SCRIPTS_PATH)" install --frozen-lockfile - yarn --cwd "$(SCRIPTS_PATH)" run format - yarn --cwd "$(SCRIPTS_PATH)" run lint - yarn --cwd "$(SCRIPTS_PATH)" run bundle + @which corepack >/dev/null 2>&1 || (echo "ERROR: corepack is required, please install it first\nhttps://pnpm.io/installation#using-corepack"; exit 1) + + cd $(SCRIPTS_PATH); \ + corepack install; \ + pnpm install --frozen-lockfile; \ + pnpm run format; \ + pnpm run lint; \ + pnpm run bundle diff --git a/README.md b/README.md index 4a8acdfea4..558dbc5681 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,12 @@ A [Test App](test-app) demonstrates how to integrate the Readium Kotlin toolkit ## Minimum Requirements -| Readium | Android min SDK | Android compile SDK | Kotlin compiler | Gradle | -|---------|-----------------|---------------------|-----------------|--------| -| latest | 21 | 33 | 1.7.10 | 6.9.3 | +| Readium | Android min SDK | Android compile SDK | Kotlin compiler (✻) | Gradle (✻) | +|---------|-----------------|---------------------|---------------------|------------| +| 3.0.0 | 21 | 34 | 1.9.22 | 8.2.0 | +| 2.3.0 | 21 | 33 | 1.7.10 | 6.9.3 | + +✻ Only required if you integrate Readium as a submodule instead of using Maven Central. ## Setting Up Readium diff --git a/build.gradle.kts b/build.gradle.kts index f9774a5d8a..ef236c3079 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,7 @@ plugins { id("io.github.gradle-nexus.publish-plugin") apply true id("org.jetbrains.dokka") apply true id("org.jetbrains.kotlin.android") apply false + id("com.google.devtools.ksp") apply false id("org.jlleitschuh.gradle.ktlint") apply true } diff --git a/docs/guides/index.md b/docs/guides/index.md new file mode 100644 index 0000000000..01bee09211 --- /dev/null +++ b/docs/guides/index.md @@ -0,0 +1,9 @@ +# User guides + +* [Opening a publication](open-publication.md) +* [Extracting the content of a publication](content.md) +* [Supporting PDF documents](pdf.md) +* [Configuring the Navigator](navigator-preferences.md) +* [Font families in the EPUB navigator](epub-fonts.md) +* [Media Navigator](media-navigator.md) +* [Text-to-speech](tts.md) \ No newline at end of file diff --git a/docs/guides/media-navigator.md b/docs/guides/media-navigator.md new file mode 100644 index 0000000000..4ee2f3fe19 --- /dev/null +++ b/docs/guides/media-navigator.md @@ -0,0 +1,214 @@ +# Media Navigator + +A `MediaNavigator` implementation can play media-based reading orders, such as audiobooks, text-to-speech rendition, and Media overlays. It enables you to reuse your UI, media controls, and logic related to media playback. + +## Controlling the playback + +A media navigator provides the API you need to pause or resume playback. + +```kotlin +navigator.pause() +check(!navigator.playback.value.playWhenReady) + +navigator.play() +check(navigator.playback.value.playWhenReady) +``` + +## Observing the playback changes + +You can observe the changes in the playback with the `navigator.playback` flow property. + +`playWhenReady` indicates whether the media is playing or will start playing once the required conditions are met (e.g. buffering). You will typically use this to change the icon of a play/pause button. + +The `state` property gives more information about the status of the playback: + +* `Ready` when the media is ready to be played if `playWhenReady` is true. +* `Ended` after reaching the end of the reading order items. +* `Buffering` if the navigator cannot play because the buffer is starved. +* `Error` occurs when an error preventing the playback happened. + +By combining the two, you can determine if the media is really playing: `playWhenReady && state == Ready`. + +Finally, you can use the `index` property to know which `navigator.readingOrder` item is set to be played. + +```kotlin +navigator.playback + .onEach { playback -> + playPauseButton.toggle(playback.playWhenReady) + + val playingItem = navigator.readingOrder.items[playback.index] + + if (playback.state is MediaNavigator.State.Failure) { + // Alert + } + } + .launchIn(scope) +``` + +`MediaNavigator` implementations may provide additional playback properties. + +## Specializations of `MediaNavigator` + +### Audio Navigator + +The `AudioNavigator` interface is a specialized version of `MediaNavigator` for publications based on pre-recorded audio resources, such as audiobooks. It provides additional time-based APIs and properties. + +```kotlin +audioNavigator.playback + .onEach { playback -> + print("At duration ${playback.offset} in the resource, buffered ${playback.buffered}") + } + .launchIn(scope) + +// Jump to a particular duration offset in the resource item at index 4. +audioNavigator.seek(index = 4, offset = 5.seconds) +``` + +### Text-aware Media Navigator + +`TextAwareMediaNavigator` specializes `MediaNavigator` for media-based resources that are synchronized with text utterances, such as sentences. It offers additional APIs and properties to determine which utterances are playing. This interface is helpful for a text-to-speech or a Media overlays navigator. + +```kotlin +textAwareNavigator.playback + .onEach { playback -> + print("Playing the range ${playback.range} in text ${playback.utterance}") + } + .launchIn(scope) + +// Get additional context by observing the location instead of the playback. +textAwareNavigator.location + .onEach { location -> + // Highlight the portion of text being played. + visualNavigator.applyDecorations( + listOf(Decoration( + locator = location.utteranceLocator, + style = Decoration.Style.Highlight(tint = Color.RED) + )), + "highlight" + ) + } + .launchIn(scope) + +// Skip the current utterance. +if (textAwareNavigator.hasNextUtterance()) { + textAwareNavigator.goToNextUtterance() +} +``` + +## Background playback and media notification + +The Readium Kotlin toolkit provides implementations of `MediaNavigator` powered by Jetpack media3. This allows for continuous playback in the background and displaying Media-style notifications with playback controls. + +To accomplish this, you must create your own `MediaSessionService`. Get acquainted with [the concept behind media3](https://developer.android.com/guide/topics/media/media3) first. + +### Configuration + +Add the following [Jetpack media3](https://developer.android.com/jetpack/androidx/releases/media3) dependencies to your `build.gradle`, after checking for the latest version. + +```groovy +dependencies { + implementation "androidx.media3:media3-common:1.0.2" + implementation "androidx.media3:media3-session:1.0.2" + implementation "androidx.media3:media3-exoplayer:1.0.2" +} +``` + +### Add the `MediaSessionService` + +Create a new implementation of `MediaSessionService` in your application. For an example, take a look at `MediaService` in the Test App. You can access the media3 `Player` from the navigator with `navigator.asMedia3Player()`. + +Don't forget to declare this new service in your `AndroidManifest.xml`. + +```xml +<manifest ...> + + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <!-- If targeting Android SDK 34, you will need this permission --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> + + <application ...> + ... + + <!-- Update android:name to match your service package --> + <service android:name=".reader.MediaService" + android:enabled="true" + android:exported="true" + android:foregroundServiceType="mediaPlayback" + tools:ignore="ExportedSerddvice" + > + + <intent-filter> + <action android:name="androidx.media3.session.MediaSessionService"/> + <action android:name="androidx.media2.session.MediaSessionService"/> + <action android:name="android.media.session.MediaSessionService" /> + </intent-filter> + </service> + </application> +</manifest> +``` + +### Customizing the notification metadata + +By default, the navigators will use the publication's metadata to display playback information in the Media-style notification. If you want to customize this, for example by retrieving metadata from your database, you can provide a custom `MediaMetadataFactory` implementation when creating the navigator. + +Here's an example for the `AndroidTtsNavigator`. + +```kotlin +val navigatorFactory = AndroidTtsNavigatorFactory( + application, publication, + metadataProvider = { pub -> + DatabaseMediaMetadataFactory( + context = application, + scope = application, + bookId = bookId, + trackCount = pub.readingOrder.size + ) + } +) + +/** + * Factory of media3 metadata for the local publication with given [bookId]. + */ +class DatabaseMediaMetadataFactory( + private val context: Context, + scope: CoroutineScope, + private val bookId: Int, + private val trackCount: Int +) : MediaMetadataFactory { + + private class Metadata( + val title: String, + val author: String, + val cover: ByteArray + ) + + private val metadata: Deferred<Metadata?> = scope.async { + Database.getInstance(context).bookDao().get(bookId)?.let { book -> + Metadata( + title = book.title, + author = book.author, + // Byte arrays will go cross processes and should be kept small + cover = book.cover.scaleToFit(400, 400).toPng() + ) + } + } + + override suspend fun publicationMetadata(): MediaMetadata = + builder()?.build() ?: MediaMetadata.EMPTY + + override suspend fun resourceMetadata(index: Int): MediaMetadata = + builder()?.setTrackNumber(index)?.build() ?: MediaMetadata.EMPTY + + private suspend fun builder(): MediaMetadata.Builder? { + val metadata = metadata.await() ?: return null + + return MediaMetadata.Builder() + .setTitle(metadata.title) + .setTotalTrackCount(trackCount) + .setArtist(metadata.artist) + // We can't yet directly use a `content://` or `file://` URI with `setArtworkUri`. + // See https://github.com/androidx/media/issues/271 + .setArtworkData(metadata.cover, PICTURE_TYPE_FRONT_COVER) } + } +} +``` diff --git a/docs/guides/open-publication.md b/docs/guides/open-publication.md new file mode 100644 index 0000000000..6a2a1daa9a --- /dev/null +++ b/docs/guides/open-publication.md @@ -0,0 +1,86 @@ +# Opening a publication + +:warning: The APIs described here may still undergo changes before the stable 3.0 release. + +To open a publication with Readium, you need to instantiate a couple of components: an `AssetRetriever` and a `PublicationOpener`. + +## `AssetRetriever` + +The `AssetRetriever` grants access to the content of an asset located at a given URL, such as a publication package, manifest, or LCP license. + +### Constructing an `AssetRetriever` + +You can create an instance of `AssetRetriever` with: + +* A `ContentResolver` to support data access through the `content` URL scheme. +* An `HttpClient` to enable the toolkit to perform HTTP requests and support the `http` and `https` URL schemes. You can use `DefaultHttpClient` which provides callbacks for handling authentication when needed. + +```kotlin +val httpClient = DefaultHttpClient() +val assetRetriever = AssetRetriever(context.contentResolver, httpClient) +``` + +### Retrieving an `Asset` + +With your fresh instance of `AssetRetriever`, you can open an `Asset` from an `AbsoluteUrl`. + +```kotlin +// From a `File` +val url = File("...").toUrl() +// or from a content:// `Uri` +val url = contentUri.toAbsoluteUrl() +// or from a raw URL string +val url = AbsoluteUrl("https://domain/book.epub") + +val asset = assetRetriever.retrieve(url) + .getOrElse { /* Failed to retrieve the Asset */ } +``` + +The `AssetRetriever` will sniff the media type of the asset, which you can store in your bookshelf database to speed up the process next time you retrieve the `Asset`. This will improve performance, especially with HTTP URL schemes. + +```kotlin +val mediaType = asset.format.mediaType + +// Speed up the retrieval with a known media type. +val asset = assetRetriever.retrieve(url, mediaType) +``` + +## `PublicationOpener` + +`PublicationOpener` builds a `Publication` object from an `Asset` using: + +* A `PublicationParser` to parse the asset structure and publication metadata. + * The `DefaultPublicationParser` handles all the formats supported by Readium out of the box. +* An optional list of `ContentProtection` to decrypt DRM-protected publications. + * If you support Readium LCP, you can get one from the `LcpService`. + +```kotlin +val contentProtections = listOf(lcpService.contentProtection(authentication)) + +val publicationParser = DefaultPublicationParser(context, httpClient, assetRetriever, pdfFactory) + +val publicationOpener = PublicationOpener(publicationParser, contentProtections) +``` + +### Opening a `Publication` + +Now that you have a `PublicationOpener` ready, you can use it to create a `Publication` from an `Asset` that was previously obtained using the `AssetRetriever`. + +The `allowUserInteraction` parameter is useful when supporting Readium LCP. When enabled and using a `LcpDialogAuthentication`, the toolkit will prompt the user if the passphrase is missing. + +```kotlin +val publication = publicationOpener.open(asset, allowUserInteraction = true) + .getOrElse { /* Failed to access or parse the publication */ } +``` + +## Supporting additional formats or URL schemes + +`DefaultPublicationParser` accepts additional parsers. You also have the option to use your own parser list by using `CompositePublicationParser` or create your own `PublicationParser` for a fully customized parsing resolution strategy. + +The `AssetRetriever` offers an additional constructor that provides greater extensibility options, using: + +* `ResourceFactory` which handles the URL schemes through which you can access content. +* `ArchiveOpener` which determines the types of archives (ZIP, RAR, etc.) that can be opened by the `AssetRetriever`. +* `FormatSniffer` which identifies the file formats that `AssetRetriever` can recognize. + +You can use either the default implementations or implement your own for each of these components using the composite pattern. The toolkit's `CompositeResourceFactory`, `CompositeArchiveOpener`, and `CompositeFormatSniffer` provide a simple resolution strategy. diff --git a/docs/guides/tts.md b/docs/guides/tts.md index 5a15ead559..1271332f22 100644 --- a/docs/guides/tts.md +++ b/docs/guides/tts.md @@ -1,180 +1,161 @@ # Text-to-speech -:warning: The API described in this guide will be changed in the next version of the Kotlin toolkit to support background TTS playback and media notifications. It is recommended that you wait before integrating it in your app. - -Text-to-speech can be used to read aloud a publication using a synthetic voice. The Readium toolkit ships with a TTS implementation based on the native [Android TTS engine](https://developer.android.com/reference/android/speech/tts/TextToSpeech), but it is opened for extension if you want to use a different TTS engine. +Text-to-speech can read aloud a publication using a synthetic voice. The Readium toolkit includes an implementation based on the [Android TTS engine](https://developer.android.com/reference/android/speech/tts/TextToSpeech), but it can be extended to use a different TTS engine. ## Glossary -* **engine** – a TTS engine takes an utterance and transforms it into audio using a synthetic voice -* **rate** - speech speed of a synthetic voice -* **tokenizer** - algorithm splitting the publication text content into individual utterances, usually by sentences * **utterance** - a single piece of text played by a TTS engine, such as a sentence -* **voice** – a synthetic voice is used by a TTS engine to speak a text using rules pertaining to the voice's language and region - -## Reading a publication aloud - -To read a publication, you need to create an instance of `PublicationSpeechSynthesizer`. It orchestrates the rendition of a publication by iterating through its content, splitting it into individual utterances using a `ContentTokenizer`, then using a `TtsEngine` to read them aloud. Not all publications can be read using TTS, therefore the constructor returns a nullable object. You can also check whether a publication can be played beforehand using `PublicationSpeechSynthesizer.canSpeak(publication)`. +* **tokenizer** - algorithm splitting the publication text content into individual utterances, usually by sentences +* **engine** – a TTS engine takes an utterance and transforms it into audio using a synthetic voice +* **voice** – a synthetic voice is used by a TTS engine to speak a text in a way suitable for the language and region -```kotlin -val synthesizer = PublicationSpeechSynthesizer( - publication = publication, - config = PublicationSpeechSynthesizer.Configuration( - rateMultiplier = 1.25 - ), - listener = object : PublicationSpeechSynthesizer.Listener { ... } -) -``` +## Getting started -Then, begin the playback from a given starting `Locator`. When missing, the playback will start from the beginning of the publication. +:warning: Apps targeting Android 11 that use the native text-to-speech must declare `INTENT_ACTION_TTS_SERVICE` in the queries elements of their manifest. -```kotlin -synthesizer.start() +```xml +<queries> + <intent> + <action android:name="android.intent.action.TTS_SERVICE" /> + </intent> +</queries> ``` -You should now hear the TTS engine speak the utterances from the beginning. `PublicationSpeechSynthesizer` provides the APIs necessary to control the playback from the app: - -* `stop()` - stops the playback ; requires start to be called again -* `pause()` - interrupts the playback temporarily -* `resume()` - resumes the playback where it was paused -* `pauseOrResume()` - toggles the pause -* `previous()` - skips to the previous utterance -* `next()` - skips to the next utterance - -Look at `TtsControls` in the Test App for an example of a view calling these APIs. - -:warning: Once you are done with the synthesizer, you should call `close()` to release held resources. - -## Observing the playback state +The text-to-speech feature is implemented as a standalone `Navigator`, which can render any publication with a [Content Service](content.md), such as an EPUB. This means you don't need an `EpubNavigatorFragment` open to read the publication; you can use the TTS navigator in the background. -The `PublicationSpeechSynthesizer` should be the single source of truth to represent the playback state in your user interface. You can observe the `synthesizer.state` property to keep your user interface synchronized with the playback. The possible states are: +To get a new instance of `TtsNavigator`, first create an `AndroidTtsNavigatorFactory` to use the default Android TTS engine. -* `Stopped` when idle and waiting for a call to `start()`. -* `Paused(utterance: Utterance)` when interrupted while playing `utterance`. -* `Playing(utterance: Utterance, range: Locator?)` when speaking `utterance`. This state is updated repeatedly while the utterance is spoken, updating the `range` property with the portion of utterance being played (usually the current word). +```kotlin +val factory = AndroidTtsNavigatorFactory(application, publication) + ?: throw Exception("This publication cannot be played with the TTS navigator") -When pairing the `PublicationSpeechSynthesizer` with a `Navigator`, you can use the `utterance.locator` and `range` properties to highlight spoken utterances and turn pages automatically. +val navigator = factory.createNavigator() +navigator.play() +``` -## Configuring the TTS +`TtsNavigator` implements `MediaNavigator`, so you can use all the APIs available for media-based playback. Check out the [dedicated user guide](media-navigator.md) to learn how to control `TtsNavigator` and observe playback notifications. -The `PublicationSpeechSynthesizer` offers some options to configure the TTS engine. Note that the support of each configuration option depends on the TTS engine used. +## Configuring the Android TTS navigator -Update the configuration by setting it directly. The configuration is not applied right away but for the next utterance. +The `AndroidTtsNavigator` implements [`Configurable`](navigator-preferences.md) and provides various settings to customize the text-to-speech experience. ```kotlin -synthesizer.setConfig(synthesizer.config.copy( - defaultLanguage = Language(Locale.FRENCH) +navigator.submitPreferences(AndroidTtsPreferences( + language = Language("fr"), + pitch = 0.8f, + speed = 1.5f )) ``` -To keep your settings user interface up to date when the configuration changes, observe the `PublicationSpeechSynthesizer.config` property. Look at `TtsControls` in the Test App for an example of a TTS settings screen. +A `PreferencesEditor` is available to help you construct your user interface and modify the preferences. -### Default language +```kotlin +val factory = AndroidTtsNavigatorFactory(application, publication) + ?: throw Exception("This publication cannot be played with the TTS navigator") -The language used by the synthesizer is important, as it determines which TTS voices are used and the rules to tokenize the publication text content. +val navigator = factory.createNavigator() -By default, `PublicationSpeechSynthesizer` will use any language explicitly set on a text element (e.g. with `lang="fr"` in HTML) and fall back on the global language declared in the publication manifest. You can override the fallback language with `Configuration.defaultLanguage` which is useful when the publication language is incorrect or missing. +val editor = factory.createPreferencesEditor(preferences) +editor.pitch.increment() +navigator.submitPreferences(editor.preferences) +``` -### Speech rate +### Language preference -The `rateMultiplier` configuration sets the speech speed as a multiplier, 1.0 being the normal speed. The available range depends on the TTS engine and can be queried with `synthesizer.rateMultiplierRange`. +The language set in the preferences determines the default voice used and how the publication text content is tokenized – i.e. split in utterances. -```kotlin -PublicationSpeechSynthesizer.Configuration( - rateMultiplier = multiplier.coerceIn(synthesizer.rateMultiplierRange) -) -``` +By default, the TTS navigator uses any language explicitly set on a text element (e.g. `lang="fr"` in HTML) and, if none is set, it falls back on the language declared in the publication manifest. Providing an explicit language preference is useful when the publication language is incorrect or missing. -### Voice +### Voices preference -The `voice` setting can be used to change the synthetic voice used by the engine. To get the available list, use `synthesizer.availableVoices`. Note that the available voices can change during runtime, observe `availableVoices` to keep your interface up to date. +The Android TTS engine supports multiple voices. To allow users to choose their preferred voice for each language, they are stored as a dictionary `Map<Language, AndroidTtsEngine.Voice.Id?>` in `AndroidTtsPreferences`. -To restore a user-selected voice, persist the unique voice identifier returned by `voice.id`. +Use the `voices` property of the `AndroidTtsNavigator` instance to get the full list of available voices. -Users do not expect to see all available voices at all time, as they depend on the selected language. You can group the voices by their language and filter them by the selected language using the following snippet. +Users don't expect to see all available voices at once, as they depend on the selected language. To get an `EnumPreference<AndroidTtsEngine.Voice.Id?>` based on the current `language` preference, you can use the following snippet. ```kotlin -// Supported voices grouped by their language. -val voicesByLanguage: Flow<Map<Language, List<Voice>>> = - synthesizer.availableVoices - .map { voices -> voices.groupBy { it.language } } - -// Supported voices for the language selected in the configuration. -val voicesForSelectedLanguage: Flow<List<Voice>> = - combine( - synthesizer.config.map { it.defaultLanguage }, - voicesByLanguage, - ) { language, voices -> - language - ?.let { voices[it] } - ?.sortedBy { it.name ?: it.id } - ?: emptyList() +// We remove the region to show all the voices for a given language, no matter the region (e.g. Canada, France). +val currentLanguage = editor.language.effectiveValue?.removeRegion() + +val voice: EnumPreference<AndroidTtsEngine.Voice.Id?> = editor.voices + .map( + from = { voices -> + currentLanguage?.let { voices[it] } + }, + to = { voice -> + currentLanguage + ?.let { editor.voices.value.orEmpty().update(it, voice) } + ?: editor.voices.value.orEmpty() + } + ) + .withSupportedValues( + navigator.voices + .filter { it.language.removeRegion() == currentLanguage } + .map { it.id } + ) + +fun <K, V> Map<K, V>.update(key: K, value: V?): Map<K, V> = + buildMap { + putAll(this@update) + if (value == null) { + remove(key) + } else { + put(key, value) + } } ``` -## Installing missing voice data +#### Installing missing voice data :point_up: This only applies if you use the default `AndroidTtsEngine`. -Sometimes the device does not have access to all the data required by a selected voice, in which case the user needs to download it manually. You can catch the `TtsEngine.Exception.LanguageSupportIncomplete` error and call `synthesizer.engine.requestInstallMissingVoice()` to start the system voice download activity. +If the device lacks the data necessary for the chosen voice, the user needs to manually download it. To do so, call the `AndroidTtsEngine.requestInstallVoice()` helper when the `AndroidTtsEngine.Error.LanguageMissingData` error occurs. This will launch the system voice download activity. ```kotlin -val synthesizer = PublicationSpeechSynthesizer(context, publication) - -synthesizer.listener = object : PublicationSpeechSynthesizer.Listener { - override fun onUtteranceError( utterance: PublicationSpeechSynthesizer.Utterance, error: PublicationSpeechSynthesizer.Exception) { - handle(error) - } - - override fun onError(error: PublicationSpeechSynthesizer.Exception) { - handle(error) - } - - private fun handle(error: PublicationSpeechSynthesizer.Exception) { - when (error) { - is PublicationSpeechSynthesizer.Exception.Engine -> - when (val err = error.error) { - is TtsEngine.Exception.LanguageSupportIncomplete -> { - synthesizer.engine.requestInstallMissingVoice(context) - } - - else -> { - ... - } - } +navigator.playback + .onEach { playback -> + (playback?.state as? TtsNavigator.State.Failure.EngineError<*>) + ?.let { it.error as? AndroidTtsEngine.Error.LanguageMissingData } + ?.let { error -> + Timber.e("Missing data for language ${error.language}") + AndroidTtsEngine.requestInstallVoice(context) } - } } + .launchIn(viewModelScope) ``` -## Synchronizing the TTS with a Navigator +## Synchronizing the TTS navigator with a visual navigator -While `PublicationSpeechSynthesizer` is completely independent from `Navigator` and can be used to play a publication in the background, most apps prefer to render the publication while it is being read aloud. The `Locator` core model is used as a means to synchronize the synthesizer with the navigator. +`TtsNavigator` is a standalone navigator that can be used to play a publication in the background. However, most apps prefer to display the publication while it is being read aloud. To do this, you can open the publication with a visual navigator (e.g. `EpubNavigatorFragment`) alongside the `TtsNavigator`. Then, synchronize the progression between the two navigators and use the Decorator API to highlight the spoken utterances. + +For concrete examples, take a look at `TtsViewModel` in the Test App. ### Starting the TTS from the visible page -`PublicationSpeechSynthesizer.start()` takes a starting `Locator` for parameter. You can use it to begin the playback from the currently visible page in a `VisualNavigator` using `firstVisibleElementLocator()`. +To start the TTS from the currently visible page, you can use the `VisualNavigator.firstVisibleElementLocator()` API to feed the initial locator of the `TtsNavigator`. ```kotlin -val start = (navigator as? VisualNavigator)?.firstVisibleElementLocator() -synthesizer.start(fromLocator = start) +val ttsNavigator = ttsNavigatorFactory.createNavigator( + initialLocator = (navigator as? VisualNavigator)?.firstVisibleElementLocator() +) ``` ### Highlighting the currently spoken utterance -If you want to highlight or underline the current utterance on the page, you can apply a `Decoration` on the utterance locator with a `DecorableNavigator`. +To highlight the current utterance on the page, you can apply a `Decoration` on the utterance locator if the visual navigator implements `DecorableNavigator`. ```kotlin -val navigator: DecorableNavigator +val visualNavigator: DecorableNavigator -synthesizer.state - .map { (it as? State.Playing)?.utterance } +ttsNavigator.location + .map { it.utteranceLocator } .distinctUntilChanged() - .onEach { utterance -> + .onEach { locator -> navigator.applyDecorations(listOf( Decoration( id = "tts-utterance", - locator = utterance.locator, + locator = locator, style = Decoration.Style.Highlight(tint = Color.RED) ) ), group = "tts") @@ -184,47 +165,48 @@ synthesizer.state ### Turning pages automatically -You can use the same technique as described above to automatically synchronize the `Navigator` with the played utterance, using `navigator.go(utterance.locator)`. +To keep the visual navigator in sync with the utterance being played, observe the navigator's current `location` as described above and use `navigator.go(location.utteranceLocator)`. + +However, this won't turn pages in the middle of an utterance, which can be irritating when speaking a lengthy sentence that spans two pages. To tackle this issue, you can use `location.tokenLocator` when available. It is updated constantly while you speak each word of an utterance. + +Jumping to the token locator for every word can significantly reduce performance. To address this, it is recommended to use [`throttleLatest`](https://github.com/Kotlin/kotlinx.coroutines/issues/1107#issuecomment-1083076517). -However, this will not turn pages mid-utterance, which can be annoying when speaking a long sentence spanning two pages. To address this, you can go to the `State.Playing.range` locator instead, which is updated regularly while speaking each word of an utterance. Note that jumping to the `range` locator for every word can severely impact performances. To alleviate this, you can throttle the flow using [`throttleLatest`](https://github.com/Kotlin/kotlinx.coroutines/issues/1107#issuecomment-1083076517). ```kotlin -synthesizer.state - .filterIsInstance<State.Playing>() - .map { it.range ?: it.utterance.locator } +ttsNavigator.location .throttleLatest(1.seconds) + .map { it.tokenLocator ?: it.utteranceLocator } + .distinctUntilChanged() .onEach { locator -> navigator.go(locator, animated = false) } .launchIn(scope) ``` -## Using a custom utterance tokenizer +## Advanced customizations + +### Utterance tokenizer -By default, the `PublicationSpeechSynthesizer` will split the publication text into sentences to create the utterances. You can customize this for finer or coarser utterances using a different tokenizer. +By default, the `TtsNavigator` splits the publication text into sentences, but you can supply your own tokenizer to customize how the text is divided. -For example, this will speak the content word-by-word: +For example, this will speak the content word by word: ```kotlin -val synthesizer = PublicationSpeechSynthesizer(context, publication, +val navigatorFactory = TtsNavigatorFactory( + application, publication, tokenizerFactory = { language -> - TextContentTokenizer( - defaultLanguage = language, - unit = TextUnit.Word - ) + DefaultTextContentTokenizer(unit = TextUnit.Word, language = language) } ) ``` -For completely custom tokenizing or to improve the existing tokenizers, you can implement your own `ContentTokenizer`. +### Custom TTS engine -## Using a custom TTS engine - -`PublicationSpeechSynthesizer` can be used with any TTS engine, provided they implement the `TtsEngine` interface. Take a look at `AndroidTtsEngine` for an example implementation. +`TtsNavigator` is compatible with any TTS engine if you provide an adapter implementing the `TtsEngine` interface. For an example, take a look at `AndroidTtsEngine`. ```kotlin -val synthesizer = PublicationSpeechSynthesizer(publication, - engineFactory = { listener -> MyCustomEngine(listener) } +val navigatorFactory = TtsNavigatorFactory( + application, publication, + engineProvider = MyEngineProvider() ) ``` - diff --git a/docs/migration-guide.md b/docs/migration-guide.md index 5522689c2b..7347af208d 100644 --- a/docs/migration-guide.md +++ b/docs/migration-guide.md @@ -2,7 +2,379 @@ All migration steps necessary in reading apps to upgrade to major versions of the Kotlin Readium toolkit will be documented in this file. -<!-- ## Unreleased --> +## 3.0.0-alpha.1 + +First of all, upgrade to version 2.4.0 and resolve any deprecation notices. This will help you avoid troubles, as the APIs that were deprecated in version 2.x have been removed in version 3.0. + +### Minimum requirements + +If you integrate Readium 3.0 as a submodule, it requires Kotlin 1.9.22 and Gradle 8.2.0. You should start by updating these dependencies in your application. + +#### Targeting Android SDK 34 + +The modules now target Android SDK 34. If your app also targets it, you will need the `FOREGROUND_SERVICE_MEDIA_PLAYBACK` permission in your `AndroidManifest.xml` file to use TTS and audiobook playback. + +### `Publication` + +#### Opening a `Publication` + +The `Streamer` object has been deprecated in favor of components with smaller responsibilities: + +* `AssetRetriever` grants access to the content of an asset located at a given URL, such as a publication package, manifest, or LCP license +* `PublicationOpener` uses a publication parser and a set of content protections to create a `Publication` object from an `Asset`. + +[See the user guide for a detailed explanation on how to use these new APIs](guides/open-publication.md). + +#### Sharing `Publication` across Android activities + +The `putPublication` and `getPublication` helpers in `Intent` are deprecated. Now, it is the application's responsibility to pass `Publication` objects between activities and reopen them when necessary. + +You can take a look at the [`ReaderRepository` in the Test App](https://github.com/readium/kotlin-toolkit/blob/09e338b0f3acc8d59282280bded6c4bf93de6281/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt#L46) for inspiration. + +Alternatively, you can copy [the deprecated helpers](https://github.com/readium/kotlin-toolkit/blob/7649378a0a6b924abedf8b72372c3808fe9b992f/readium/shared/src/main/java/org/readium/r2/shared/extensions/Intent.kt#L26) and add them to your codebase. However, please note that this approach is discouraged because it will not handle configuration changes smoothly. + +### `MediaType` + +#### Sniffing a `MediaType` + +`MediaType` no longer has static helpers for sniffing it from a file or URL. Instead, you can use an `AssetRetriever` to retrieve the format of a file. + +```kotlin +val httpClient = DefaultHttpClient() +val assetRetriever = AssetRetriever(context.contentResolver, httpClient) + +val mediaType = assetRetriever.sniffFormat(File(...)) + .getOrElse { /* Failed to access the asset or recognize its format */ } + .mediaType +``` + +### HREFs + +#### `Link.href` and `Locator.href` are not strings anymore + +`Link.href` and `Locator.href` are now respectively `Href` and `Url` objects. If you still need the string value, you can call `toString()`, but you may find the `Url` objects more useful in practice. + +Use `link.url()` to get a `Url` from a `Link` object. + +#### Migration of HREFs and Locators (bookmarks, annotations, etc.) + +:warning: This requires a database migration in your application, if you were persisting `Locator` objects. + +In Readium v2.x, a `Link` or `Locator`'s `href` could be either: + +* a valid absolute URL for a streamed publication, e.g. `https://domain.com/isbn/dir/my%20chapter.html`, +* a percent-decoded path for a local archive such as an EPUB, e.g. `/dir/my chapter.html`. + * Note that it was relative to the root of the archive (`/`). + +To improve the interoperability with other Readium toolkits (in particular the Readium Web Toolkits, which only work in a streaming context) **Readium v3 now generates and expects valid URLs** for `Locator` and `Link`'s `href`. + +* `https://domain.com/isbn/dir/my%20chapter.html` is left unchanged, as it was already a valid URL. +* `/dir/my chapter.html` becomes the relative URL path `dir/my%20chapter.html` + * We dropped the `/` prefix to avoid issues when resolving to a base URL. + * Special characters are percent-encoded. + +**You must migrate the HREFs or Locators stored in your database** when upgrading to Readium 3. To assist you, two helpers are provided: `Url.fromLegacyHref()` and `Locator.fromLegacyJSON()`. + +Here's an example of a Jetpack Room migration that can serve as inspiration: + +```kotlin +val MIGRATION_HREF = object : Migration(1, 2) { + + override fun migrate(db: SupportSQLiteDatabase) { + val normalizedHrefs: Map<Long, String> = buildMap { + db.query("SELECT id, href FROM bookmarks").use { cursor -> + while (cursor.moveToNext()) { + val id = cursor.getLong(0) + val href = cursor.getString(1) + + val normalizedHref = Url.fromLegacyHref(href)?.toString() + if (normalizedHref != null) { + put(id, normalizedHref) + } + } + } + } + + val stmt = db.compileStatement("UPDATE bookmarks SET href = ? WHERE id = ?") + for ((id, href) in normalizedHrefs) { + stmt.bindString(1, href) + stmt.bindLong(2, id) + stmt.executeUpdateDelete() + } + } +} +``` + +### Error management + +Most APIs now return an `Error` instance instead of an `Exception` in case of failure, as these objects are not thrown by the toolkit but returned as values. + +It is recommended to handle `Error` objects using a `when` statement. However, if you still need an `Exception`, you may wrap an `Error` with `ErrorException`, for example: + +```kotlin +assetRetriever.sniffFormat(...) + .getOrElse { throw ErrorException(it) } +``` +`UserException` is also deprecated. The application now needs to provide localized error messages for toolkit errors. + +### Navigator + +#### Click on external links in the EPUB navigator + +Clicking on external links is no longer managed by the EPUB navigator. To open the link yourself, override `HyperlinkNavigator.Listener.onExternalLinkActivated`, for example: + +```kotlin +override fun onExternalLinkActivated(url: AbsoluteUrl) { + if (!url.isHttp) return + val context = requireActivity() + val uri = url.toUri() + try { + CustomTabsIntent.Builder() + .build() + .launchUrl(context, uri) + } catch (e: ActivityNotFoundException) { + context.startActivity(Intent(Intent.ACTION_VIEW, uri)) + } +} +``` + +##### Edge tap and keyboard navigation + +Version 3 includes a new component called `DirectionalNavigationAdapter` that replaces `EdgeTapNavigation`. This helper enables users to navigate between pages using arrow and space keys on their keyboard or by tapping the edge of the screen. + +As it implements `InputListener`, you can attach it to any `OverflowableNavigator`. + +```kotlin +navigator.addInputListener( + DirectionalNavigationAdapter( + navigator, + animatedTransition = true + ) +) +``` + +The `DirectionalNavigationAdapter` provides plenty of customization options. Please refer to its API for more details. + +#### Tap and drag events + +The `onTap` and `onDrag` events of `VisualNavigator.Listener` have been deprecated. You can now use multiple implementations of `InputListener`. The order is important when events are consumed. + +```kotlin +navigator.addInputListener(DirectionalNavigationAdapter(navigator)) + +navigator.addInputListener(object : InputListener { + override fun onTap(event: TapEvent): Boolean { + toggleUi() + return true + } +}) +``` + +### LCP + +#### Creating an `LcpService` + +The `LcpService` now requires an instance of `AssetRetriever` and `DownloadManager` during construction. To get the same behavior as before, you can use a `ForegroundDownloadManager`. If you want to support downloads in the background instead, take a look at `AndroidDownloadManager`. + +```kotlin +val lcpService = LcpService( + context, + assetRetriever = assetRetriever, + downloadManager = ForegroundDownloadManager( + httpClient = httpClient, + downloadsDirectory = File(context.cacheDir, "lcp") + ) +) +``` + +#### Downloading an LCP protected publication from a license + +`LcpService.acquirePublication()` is deprecated in favor of `LcpService.publicationRetriever()`, which provides greater flexibility thanks to the `DownloadManager`. + +```kotlin +// 1. Open an `Asset` from a `File`. +val asset = assetRetriever.retrieve(file) + .getOrElse { /* Failed to open the file or sniff its format */ } + +// 2. Verify that it is an LCP License Document. +if (asset is ResourceAsset && asset.format.conformsTo(LcpLicenseSpecification)) { + // 3. Parse the LCP License Document from its JSON representation. + val license = lcplAsset.resource.read() + .getOrElse { /* Failed to read the content of the LCPL asset */ } + .let { LicenseDocument.fromBytes(it) } + .getOrElse { /* Failed to parse a valid LCP License Document from the the raw bytes */ } + + // 4. Download the publication using the `LcpPublicationRetriever`. + // The returned `requestId` can be used to cancel an on-going download, or to resume a download + // with `LcpPublicationRetriever.register()`, if it was downloaded in the background. + val requestId = lcpService.publicationRetriever() + .retrieve(license, listener = object : LcpPublicationRetriever.Listener { + override fun onAcquisitionCompleted( + requestId: LcpPublicationRetriever.RequestId, + acquiredPublication: LcpService.AcquiredPublication + ) { + } + + override fun onAcquisitionProgressed( + requestId: LcpPublicationRetriever.RequestId, + downloaded: Long, + expected: Long? + ) { + // Report progress. + } + + override fun onAcquisitionFailed( + requestId: LcpPublicationRetriever.RequestId, + error: LcpError + ) { + // Report error. + } + + override fun onAcquisitionCancelled(requestId: LcpPublicationRetriever.RequestId) { + // Handle cancellation. + } + }) +} +``` + +If you are using a `ForegroundDownloadManager` and **not supporting background downloads**, you can use this helper to have a similar API as Readium 2.x with coroutines. + +```kotlin +suspend fun LcpService.acquirePublication( + lcplAsset: ResourceAsset, + onProgress: (Double) -> Unit +): Try<LcpService.AcquiredPublication, Error> { + require(lcplAsset.format.conformsTo(LcpLicenseSpecification)) + + val license = lcplAsset.resource.read() + .flatMap { LicenseDocument.fromBytes(it) } + .getOrElse { return Try.failure(it) } + + return suspendCancellableCoroutine { cont -> + publicationRetriever().retrieve(license, object : LcpPublicationRetriever.Listener { + override fun onAcquisitionCompleted( + requestId: LcpPublicationRetriever.RequestId, + acquiredPublication: LcpService.AcquiredPublication + ) { + cont.resume(Try.success(acquiredPublication)) + } + + override fun onAcquisitionProgressed( + requestId: LcpPublicationRetriever.RequestId, + downloaded: Long, + expected: Long? + ) { + expected ?: return + onProgress(downloaded.toDouble() / expected.toDouble()) + } + + override fun onAcquisitionFailed( + requestId: LcpPublicationRetriever.RequestId, + error: LcpError + ) { + cont.resume(Try.failure(error)) + } + + override fun onAcquisitionCancelled(requestId: LcpPublicationRetriever.RequestId) { + cont.cancel() + } + }) + } +} +``` + +#### `LcpDialogAuthentication` updated to support configuration changes + +The way the host view of a `LcpDialogAuthentication` is retrieved was changed to support Android configuration changes. You no longer need to pass an activity, fragment or view as `sender` parameter. + +Instead, call on your instance of `LcpDialogAuthentication`: +* `onParentViewAttachedToWindow` every time you have a view attached to a window available as anchor +* `onParentViewDetachedFromWindow` every time it gets detached + +You can monitor these events by setting a `View.OnAttachStateChangeListener` on your view. [See the Test App for an example](https://github.com/readium/kotlin-toolkit/blob/01d6c7936accea2d6b953d435e669260676e8c99/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt#L68). + +### Removal of Fuel and Kovenant + +Both the Fuel and Kovenant libraries have been completely removed from the toolkit. With that, several deprecated functions have also been removed. + +### Resources + +To avoid conflicts when merging your app resources, all resources declared in the Readium toolkit now have the prefix `readium_`. This means that you must rename any layouts or strings you have overridden. Some resources were removed from the toolkit. + +#### Deleted resources + +If you referenced these resources, you need to remove them from your application or copy them to your own resources. + +##### Deleted colors + +| Name | +|-----------------------------| +| `colorPrimary` | +| `colorPrimaryDark` | +| `colorAccent` | +| `colorAccentPrefs` | +| `snackbar_background_color` | +| `snackbar_text_color` | + +##### Deleted strings + +| Name | +|----------------------------| +| `end_of_chapter` | +| `end_of_chapter_indicator` | +| `zero` | +| `epub_navigator_tag` | +| `image_navigator_tag` | +| `snackbar_text_color` | + +All the localized error messages are also removed. + +#### Renamed resources + +If you used the resources listed below, you must rename the references to reflect the new names. You can use a global search to help you find the references in your project. + +##### Renamed layouts + +| Deprecated | New | +|-----------------------------|-----------------------------------------------| +| `activity_r2_viewpager` | `readium_navigator_viewpager` | +| `fragment_fxllayout_double` | `readium_navigator_fragment_fxllayout_double` | +| `fragment_fxllayout_single` | `readium_navigator_fragment_fxllayout_single` | +| `popup_footnote` | `readium_navigator_popup_footnote` | +| `r2_lcp_auth_dialog` | `readium_lcp_auth_dialog` | +| `viewpager_fragment_cbz` | `readium_navigator_viewpager_fragment_cbz` | +| `viewpager_fragment_epub` | `readium_navigator_viewpager_fragment_epub` | + +##### Renamed dimensions + +| Deprecated | New | +|--------------------------------------|-------------------------------------------| +| `r2_navigator_epub_vertical_padding` | `readium_navigator_epub_vertical_padding` | + +##### Renamed strings + +| Deprecated | New | +|---------------------------------------------|--------------------------------------------------| +| `r2_lcp_dialog_cancel` | `readium_lcp_dialog_cancel` | +| `r2_lcp_dialog_continue` | `readium_lcp_dialog_continue` | +| `r2_lcp_dialog_forgotPassphrase` | `readium_lcp_dialog_forgotPassphrase` | +| `r2_lcp_dialog_help` | `readium_lcp_dialog_help` | +| `r2_lcp_dialog_prompt` | `readium_lcp_dialog_prompt` | +| `r2_lcp_dialog_reason_invalidPassphrase` | `readium_lcp_dialog_reason_invalidPassphrase` | +| `r2_lcp_dialog_reason_passphraseNotFound` | `readium_lcp_dialog_reason_passphraseNotFound` | +| `r2_lcp_dialog_support_mail` | `readium_lcp_dialog_support_mail` | +| `r2_lcp_dialog_support_phone` | `readium_lcp_dialog_support_phone` | +| `r2_lcp_dialog_support_web` | `readium_lcp_dialog_support_web` | +| `r2_media_notification_channel_description` | `readium_media_notification_channel_description` | +| `r2_media_notification_channel_name` | `readium_media_notification_channel_name` | + +##### Renamed drawables + +| Deprecated | New | +|-----------------------------------------|----------------------------------------------| +| `r2_media_notification_fastforward.xml` | `readium_media_notification_fastforward.xml` | +| `r2_media_notification_rewind.xml` | `readium_media_notification_rewind.xml` | + ## 2.4.0 diff --git a/gradle.properties b/gradle.properties index c064741c76..cac7c68c14 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,5 +19,3 @@ android.useAndroidX=true android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official - -android.disableAutomaticComponentCreation=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a32be7e185..ecc21d3bb4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,62 +1,68 @@ [versions] -androidx-activity = "1.6.1" -androidx-appcompat = "1.5.1" -androidx-browser = "1.4.0" +accompanist = "0.32.0" + +androidx-activity = "1.8.2" +androidx-appcompat = "1.6.1" +androidx-browser = "1.7.0" androidx-cardview = "1.0.0" -androidx-compose-compiler = "1.3.2" -androidx-compose-animation = "1.3.0-beta03" -androidx-compose-foundation = "1.3.0-beta03" -androidx-compose-material = "1.3.0-beta03" -androidx-compose-material3 = "1.0.0-beta03" -androidx-compose-runtime = "1.3.0-beta03" -androidx-compose-theme-adapter = "1.1.19" -androidx-compose-ui = "1.3.0-beta03" +# Make sure to align with the Kotlin version +# https://developer.android.com/jetpack/androidx/releases/compose-kotlin +androidx-compose-compiler = "1.5.8" +androidx-compose-animation = "1.5.4" +androidx-compose-foundation = "1.5.4" +androidx-compose-material = "1.5.4" +androidx-compose-material3 = "1.1.2" +androidx-compose-runtime = "1.5.4" +androidx-compose-ui = "1.5.4" androidx-constraintlayout = "2.1.4" -androidx-core = "1.9.0" +androidx-core = "1.12.0" androidx-datastore = "1.0.0" -androidx-expresso-core = "3.4.0" -androidx-ext-junit = "1.1.3" -androidx-fragment-ktx = "1.5.4" +androidx-expresso-core = "3.5.1" +androidx-ext-junit = "1.1.5" +androidx-fragment-ktx = "1.6.2" androidx-legacy = "1.0.0" -androidx-lifecycle = "2.5.1" +androidx-lifecycle = "2.7.0" androidx-lifecycle-extensions = "2.2.0" -androidx-media = "1.6.0" -androidx-media2 = "1.2.1" -androidx-media3 = "1.0.0-rc01" -androidx-navigation = "2.5.2" -androidx-paging = "3.1.1" -androidx-recyclerview = "1.2.1" -androidx-room = "2.4.3" +androidx-media = "1.7.0" +androidx-media2 = "1.3.0" +androidx-media3 = "1.2.0" +androidx-navigation = "2.7.6" +androidx-paging = "3.2.1" +androidx-recyclerview = "1.3.2" +androidx-room = "2.6.1" androidx-viewpager2 = "1.0.0" -androidx-webkit = "1.5.0" +androidx-webkit = "1.9.0" -assertj = "3.23.1" +assertj = "3.25.1" -dokka = "1.7.20" +dokka = "1.9.10" -google-exoplayer = "2.18.1" -google-material = "1.7.0" +google-exoplayer = "2.19.1" +google-material = "1.11.0" -joda-time = "2.12.1" -jsoup = "1.15.3" +joda-time = "2.12.6" +jsoup = "1.17.2" junit = "4.13.2" -kotlin = "1.7.20" -kotlinx-coroutines = "1.6.4" -kotlinx-coroutines-test = "1.6.4" -kotlinx-serialization-json = "1.4.1" +kotlin = "1.9.22" +kotlinx-coroutines = "1.7.3" +kotlinx-coroutines-test = "1.7.3" +kotlinx-serialization-json = "1.6.2" pdfium = "1.8.2" pdf-viewer = "2.8.2" -picasso = "2.71828" +#noinspection GradleDependency +picasso = "2.8" pspdfkit = "8.4.1" -robolectric = "4.9" +robolectric = "4.11.1" timber = "5.0.1" [libraries] +accompanist-themeadapter-material = { group = "com.google.accompanist", name = "accompanist-themeadapter-material", version.ref = "accompanist" } + androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "androidx-activity" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidx-browser" } @@ -67,7 +73,6 @@ androidx-compose-foundation = { group = "androidx.compose.foundation", name = "f androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "androidx-compose-material" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" } androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "androidx-compose-material" } -androidx-compose-theme-adapter = { group ="com.google.android.material", name = "compose-theme-adapter", version.ref = "androidx-compose-theme-adapter" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidx-compose-ui" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidx-compose-ui" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } @@ -119,7 +124,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" } kotlin-gradle = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-junit = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit", version.ref = "kotlin" } kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" } -kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } @@ -137,7 +142,7 @@ timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "tim [bundles] -compose = ["androidx-compose-activity", "androidx-compose-animation", "androidx-compose-foundation", "androidx-compose-material", "androidx-compose-material3", "androidx-compose-material-icons", "androidx-compose-theme-adapter", "androidx-compose-ui", "androidx-compose-ui-tooling"] +compose = ["androidx-compose-activity", "androidx-compose-animation", "androidx-compose-foundation", "androidx-compose-material", "androidx-compose-material3", "androidx-compose-material-icons", "androidx-compose-ui", "androidx-compose-ui-tooling"] coroutines = ["kotlinx-coroutines-core", "kotlinx-coroutines-android"] exoplayer = ["google-exoplayer-core", "google-exoplayer-ui", "google-exoplayer-mediasession", "google-exoplayer-workmanager", "google-exoplayer-extension-media2"] lifecycle = ["androidx-lifecycle-common", "androidx-lifecycle-extensions", "androidx-lifecycle-livedata", "androidx-lifecycle-runtime", "androidx-lifecycle-viewmodel", "androidx-lifecycle-vmsavedstate", "androidx-lifecycle-viewmodel-compose"] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa991fceae..15de90249f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/mkdocs.yml b/mkdocs.yml index aa372bf708..5756d0a2fa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,7 +10,7 @@ repo_name: kotlin-toolkit repo_url: https://github.com/readium/kotlin-toolkit # Copyright (shown at the footer) -copyright: 'Copyright © 2022 Readium Foundation' +copyright: 'Copyright © 2023 Readium Foundation' # Material theme theme: diff --git a/readium/adapters/exoplayer/audio/build.gradle.kts b/readium/adapters/exoplayer/audio/build.gradle.kts new file mode 100644 index 0000000000..93fd839bb3 --- /dev/null +++ b/readium/adapters/exoplayer/audio/build.gradle.kts @@ -0,0 +1,66 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +plugins { + id("com.android.library") + kotlin("android") + kotlin("plugin.parcelize") + kotlin("plugin.serialization") +} + +android { + resourcePrefix = "readium_" + + compileSdk = 34 + + defaultConfig { + minSdk = 21 + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + freeCompilerArgs = freeCompilerArgs + listOf( + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=org.readium.r2.shared.InternalReadiumApi" + ) + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android.txt")) + } + } + buildFeatures { + viewBinding = true + } + namespace = "org.readium.adapter.exoplayer.audio" +} + +kotlin { + explicitApi() +} + +rootProject.ext["publish.artifactId"] = "readium-navigator-exoplayer-audio" +apply(from = "$rootDir/scripts/publish-module.gradle") + +dependencies { + api(project(":readium:readium-shared")) + api(project(":readium:navigators:media:readium-navigator-media-audio")) + + implementation(libs.androidx.media3.common) + implementation(libs.androidx.media3.exoplayer) + implementation(libs.timber) + implementation(libs.bundles.coroutines) + implementation(libs.kotlinx.serialization.json) + + // Tests + testImplementation(libs.junit) +} diff --git a/readium/adapters/pdfium/pdfium-document/src/main/AndroidManifest.xml b/readium/adapters/exoplayer/audio/src/main/AndroidManifest.xml similarity index 100% rename from readium/adapters/pdfium/pdfium-document/src/main/AndroidManifest.xml rename to readium/adapters/exoplayer/audio/src/main/AndroidManifest.xml diff --git a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoAudiobookPlayer.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoAudiobookPlayer.kt new file mode 100644 index 0000000000..e76564f9f1 --- /dev/null +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoAudiobookPlayer.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.adapter.exoplayer.audio + +import androidx.media3.common.ForwardingPlayer +import androidx.media3.exoplayer.ExoPlaybackException +import androidx.media3.exoplayer.ExoPlayer +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.ExperimentalTime +import timber.log.Timber + +/** + * A wrapper around ExoPlayer to customize some behaviours. + */ +@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +internal class ExoAudiobookPlayer( + private val player: ExoPlayer, + private val itemDurations: List<Duration>?, + private val seekForwardIncrement: Duration, + private val seekBackwardIncrement: Duration +) : ForwardingPlayer(player) { + + fun seekBy(offset: Duration) { + itemDurations + ?.let { smartSeekBy(offset, it) } + ?: dumbSeekBy(offset) + } + + override fun seekForward() { + seekBy(seekForwardIncrement) + } + + override fun seekBack() { + seekBy(-seekBackwardIncrement) + } + + override fun getPlayerError(): ExoPlaybackException? { + return player.playerError + } + + @OptIn(ExperimentalTime::class) + private fun smartSeekBy( + offset: Duration, + durations: List<Duration> + ) { + val (newIndex, newPosition) = + SmartSeeker.dispatchSeek( + offset, + player.currentPosition.milliseconds, + player.currentMediaItemIndex, + durations + ) + Timber.v("Smart seeking by $offset resolved to item $newIndex position $newPosition") + player.seekTo(newIndex, newPosition.inWholeMilliseconds) + } + + private fun dumbSeekBy(offset: Duration) { + val newIndex = player.currentMediaItemIndex + val newPosition = player.currentPosition + offset.inWholeMilliseconds + player.seekTo(newIndex, newPosition) + } +} diff --git a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerAliases.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerAliases.kt new file mode 100644 index 0000000000..d095ad6729 --- /dev/null +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerAliases.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.adapter.exoplayer.audio + +import org.readium.navigator.media.audio.AudioNavigator +import org.readium.navigator.media.audio.AudioNavigatorFactory +import org.readium.r2.shared.ExperimentalReadiumApi + +@OptIn(ExperimentalReadiumApi::class) +public typealias ExoPlayerNavigatorFactory = AudioNavigatorFactory<ExoPlayerSettings, ExoPlayerPreferences, ExoPlayerPreferencesEditor> + +@OptIn(ExperimentalReadiumApi::class) +public typealias ExoPlayerNavigator = AudioNavigator<ExoPlayerSettings, ExoPlayerPreferences> diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerDataSource.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt similarity index 74% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerDataSource.kt rename to readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt index c3411199fd..401395caf7 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerDataSource.kt +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.exoplayer +package org.readium.adapter.exoplayer.audio import android.net.Uri import androidx.media3.common.C.LENGTH_UNSET @@ -15,21 +15,32 @@ import androidx.media3.datasource.DataSpec import androidx.media3.datasource.TransferListener import java.io.IOException import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.buffered import org.readium.r2.shared.publication.Publication - -sealed class ExoPlayerDataSourceException(message: String, cause: Throwable?) : IOException(message, cause) { +import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.buffered +import org.readium.r2.shared.util.toUrl + +internal sealed class ExoPlayerDataSourceException(message: String, cause: Throwable?) : IOException( + message, + cause +) { class NotOpened(message: String) : ExoPlayerDataSourceException(message, null) class NotFound(message: String) : ExoPlayerDataSourceException(message, null) - class ReadFailed(uri: Uri, offset: Int, readLength: Int, cause: Throwable) : ExoPlayerDataSourceException("Failed to read $readLength bytes of URI $uri at offset $offset.", cause) + class ReadFailed(uri: Uri, offset: Int, readLength: Int, cause: Throwable) : ExoPlayerDataSourceException( + "Failed to read $readLength bytes of URI $uri at offset $offset.", + cause + ) } /** * An ExoPlayer's [DataSource] which retrieves resources from a [Publication]. */ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) -internal class ExoPlayerDataSource internal constructor(private val publication: Publication) : BaseDataSource(/* isNetwork = */ true) { +internal class ExoPlayerDataSource internal constructor( + private val publication: Publication +) : BaseDataSource(/* isNetwork = */ true) { class Factory( private val publication: Publication, @@ -47,23 +58,25 @@ internal class ExoPlayerDataSource internal constructor(private val publication: private data class OpenedResource( val resource: Resource, val uri: Uri, - var position: Long, + var position: Long ) private var openedResource: OpenedResource? = null override fun open(dataSpec: DataSpec): Long { - val link = publication.linkWithHref(dataSpec.uri.toString()) - ?: throw ExoPlayerDataSourceException.NotFound("Can't find a [Link] for URI: ${dataSpec.uri}. Make sure you only request resources declared in the manifest.") - - val resource = publication.get(link) + val resource = dataSpec.uri.toUrl() + ?.let { publication.linkWithHref(it) } + ?.let { publication.get(it) } // Significantly improves performances, in particular with deflated ZIP entries. - .buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) + ?.buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) + ?: throw ExoPlayerDataSourceException.NotFound( + "Can't find a [Link] for URI: ${dataSpec.uri}. Make sure you only request resources declared in the manifest." + ) openedResource = OpenedResource( resource = resource, uri = dataSpec.uri, - position = dataSpec.position, + position = dataSpec.position ) val bytesToRead = @@ -96,12 +109,15 @@ internal class ExoPlayerDataSource internal constructor(private val publication: return 0 } - val openedResource = openedResource ?: throw ExoPlayerDataSourceException.NotOpened("No opened resource to read from. Did you call open()?") + val openedResource = openedResource ?: throw ExoPlayerDataSourceException.NotOpened( + "No opened resource to read from. Did you call open()?" + ) try { val data = runBlocking { openedResource.resource .read(range = openedResource.position until (openedResource.position + length)) + .mapFailure { ReadException(it) } .getOrThrow() } @@ -141,8 +157,9 @@ internal class ExoPlayerDataSource internal constructor(private val publication: if (e !is InterruptedException) { throw e } + } finally { + openedResource = null } } - openedResource = null } } diff --git a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDefaults.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDefaults.kt new file mode 100644 index 0000000000..81bba2f128 --- /dev/null +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDefaults.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.adapter.exoplayer.audio + +import org.readium.r2.shared.ExperimentalReadiumApi + +/** + * Default values for the ExoPlayer engine. + * + * These values will be used as a last resort by [ExoPlayerSettingsResolver] + * when no user preference takes precedence. + * + * @see ExoPlayerPreferences + */ +@ExperimentalReadiumApi +public data class ExoPlayerDefaults( + val pitch: Double? = null, + val speed: Double? = null +) { + init { + require(pitch == null || pitch > 0) + require(speed == null || speed > 0) + } +} diff --git a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerEngine.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerEngine.kt new file mode 100644 index 0000000000..d7dfcda84b --- /dev/null +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerEngine.kt @@ -0,0 +1,257 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.adapter.exoplayer.audio + +import android.app.Application +import androidx.media3.common.* +import androidx.media3.datasource.DataSource +import androidx.media3.exoplayer.ExoPlaybackException +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.readium.navigator.media.audio.AudioEngine +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.toUri +import org.readium.r2.shared.util.units.Hz +import org.readium.r2.shared.util.units.hz + +/** + * An [AudioEngine] based on Media3 ExoPlayer. + */ +@ExperimentalReadiumApi +@OptIn(ExperimentalCoroutinesApi::class) +@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +public class ExoPlayerEngine private constructor( + private val exoPlayer: ExoAudiobookPlayer, + private val settingsResolver: SettingsResolver, + private val configuration: Configuration, + initialPreferences: ExoPlayerPreferences +) : AudioEngine<ExoPlayerSettings, ExoPlayerPreferences> { + + public companion object { + + public suspend operator fun invoke( + application: Application, + settingsResolver: SettingsResolver, + dataSourceFactory: DataSource.Factory, + playlist: Playlist, + configuration: Configuration, + initialIndex: Int, + initialPosition: Duration, + initialPreferences: ExoPlayerPreferences + ): ExoPlayerEngine { + val exoPlayer = ExoPlayer.Builder(application) + .setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory)) + .setAudioAttributes( + AudioAttributes.Builder() + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) + .setUsage(C.USAGE_MEDIA) + .build(), + true + ) + .setHandleAudioBecomingNoisy(true) + .setSeekBackIncrementMs(configuration.seekBackwardIncrement.inWholeMilliseconds) + .setSeekForwardIncrementMs(configuration.seekForwardIncrement.inWholeMilliseconds) + .build() + + exoPlayer.setMediaItems( + playlist.items.map { item -> + MediaItem.Builder() + .setUri(item.url.toUri()) + .setMediaMetadata(item.mediaMetadata) + .build() + } + ) + + val durations: List<Duration>? = + playlist.items.mapNotNull { it.duration } + .takeIf { it.size == playlist.items.size } + + exoPlayer.playlistMetadata = playlist.mediaMetadata + + exoPlayer.seekTo(initialIndex, initialPosition.inWholeMilliseconds) + + prepareExoPlayer(exoPlayer) + + val customizedPlayer = + ExoAudiobookPlayer( + exoPlayer, + durations, + configuration.seekForwardIncrement, + configuration.seekBackwardIncrement + ) + + return ExoPlayerEngine( + customizedPlayer, + settingsResolver, + configuration, + initialPreferences + ) + } + + private suspend fun prepareExoPlayer(player: ExoPlayer) { + lateinit var listener: Player.Listener + suspendCancellableCoroutine { continuation -> + listener = object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + when (playbackState) { + Player.STATE_READY -> continuation.resume(Unit) {} + Player.STATE_IDLE -> if (player.playerError != null) { + continuation.resume(Unit) {} + } + else -> {} + } + } + } + continuation.invokeOnCancellation { player.removeListener(listener) } + player.addListener(listener) + player.prepare() + } + player.removeListener(listener) + } + } + + public data class Configuration( + val positionRefreshRate: Hz = 2.0.hz, + val seekBackwardIncrement: Duration = 15.seconds, + val seekForwardIncrement: Duration = 30.seconds + ) + + public data class Playlist( + val mediaMetadata: MediaMetadata, + val duration: Duration?, + val items: List<Item> + ) { + public data class Item( + val url: Url, + val mediaMetadata: MediaMetadata, + val duration: Duration? + ) + } + + public fun interface SettingsResolver { + + /** + * Computes a set of engine settings from the engine preferences. + */ + public fun settings(preferences: ExoPlayerPreferences): ExoPlayerSettings + } + + private inner class Listener : Player.Listener { + + override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { + submitPreferences( + ExoPlayerPreferences( + pitch = playbackParameters.pitch.toDouble(), + speed = playbackParameters.speed.toDouble() + ) + ) + } + + override fun onEvents(player: Player, events: Player.Events) { + _playback.value = exoPlayer.playback + } + } + + public data class Error(val error: ExoPlaybackException) : AudioEngine.Error + + private val coroutineScope: CoroutineScope = + MainScope() + + init { + exoPlayer.addListener(Listener()) + } + + private val _settings: MutableStateFlow<ExoPlayerSettings> = + MutableStateFlow(settingsResolver.settings(initialPreferences)) + + private val _playback: MutableStateFlow<AudioEngine.Playback> = + MutableStateFlow(exoPlayer.playback) + + init { + coroutineScope.launch { + val positionRefreshDelay = (1.0 / configuration.positionRefreshRate.value).seconds + while (isActive) { + delay(positionRefreshDelay) + _playback.value = exoPlayer.playback + } + } + + submitPreferences(initialPreferences) + } + + override val playback: StateFlow<AudioEngine.Playback> + get() = _playback.asStateFlow() + + override val settings: StateFlow<ExoPlayerSettings> + get() = _settings.asStateFlow() + + override fun play() { + exoPlayer.play() + } + + override fun pause() { + exoPlayer.pause() + } + + override fun skipTo(index: Int, offset: Duration) { + exoPlayer.seekTo(index, offset.inWholeMilliseconds) + } + + override fun skip(duration: Duration) { + exoPlayer.seekBy(duration) + } + + override fun skipForward() { + exoPlayer.seekForward() + } + + override fun skipBackward() { + exoPlayer.seekBack() + } + + override fun close() { + coroutineScope.cancel() + exoPlayer.release() + } + + override fun asPlayer(): Player { + return exoPlayer + } + + override fun submitPreferences(preferences: ExoPlayerPreferences) { + val newSettings = settingsResolver.settings(preferences) + exoPlayer.playbackParameters = PlaybackParameters( + newSettings.speed.toFloat(), + newSettings.pitch.toFloat() + ) + } + + private val ExoAudiobookPlayer.playback: AudioEngine.Playback get() = + AudioEngine.Playback( + state = engineState, + playWhenReady = playWhenReady, + index = currentMediaItemIndex, + offset = currentPosition.milliseconds, + buffered = bufferedPosition.milliseconds + ) + + private val ExoAudiobookPlayer.engineState: AudioEngine.State get() = + when (this.playbackState) { + Player.STATE_READY -> AudioEngine.State.Ready + Player.STATE_BUFFERING -> AudioEngine.State.Buffering + Player.STATE_ENDED -> AudioEngine.State.Ended + else -> AudioEngine.State.Failure(Error(playerError!!)) + } +} diff --git a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerEngineProvider.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerEngineProvider.kt new file mode 100644 index 0000000000..63355d0aa1 --- /dev/null +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerEngineProvider.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.adapter.exoplayer.audio + +import android.app.Application +import androidx.media3.datasource.DataSource +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import org.readium.navigator.media.audio.AudioEngineProvider +import org.readium.navigator.media.common.DefaultMediaMetadataProvider +import org.readium.navigator.media.common.MediaMetadataProvider +import org.readium.r2.navigator.extensions.time +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.indexOfFirstWithHref +import org.readium.r2.shared.util.Try + +/** + * Main component to use the audio navigator with the ExoPlayer adapter. + * + * Provide [ExoPlayerDefaults] to customize the default values that will be used by + * the navigator for some preferences. + */ +@ExperimentalReadiumApi +@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +public class ExoPlayerEngineProvider( + private val application: Application, + private val metadataProvider: MediaMetadataProvider = DefaultMediaMetadataProvider(), + private val defaults: ExoPlayerDefaults = ExoPlayerDefaults(), + private val configuration: ExoPlayerEngine.Configuration = ExoPlayerEngine.Configuration() +) : AudioEngineProvider<ExoPlayerSettings, ExoPlayerPreferences, ExoPlayerPreferencesEditor> { + + override suspend fun createEngine( + publication: Publication, + initialLocator: Locator, + initialPreferences: ExoPlayerPreferences + ): Try<ExoPlayerEngine, Nothing> { + val metadataFactory = metadataProvider.createMetadataFactory(publication) + val settingsResolver = ExoPlayerSettingsResolver(defaults) + val dataSourceFactory: DataSource.Factory = ExoPlayerDataSource.Factory(publication) + val initialIndex = publication.readingOrder.indexOfFirstWithHref(initialLocator.href) ?: 0 + val initialPosition = initialLocator.locations.time ?: Duration.ZERO + val playlist = ExoPlayerEngine.Playlist( + mediaMetadata = metadataFactory.publicationMetadata(), + duration = publication.metadata.duration?.seconds, + items = publication.readingOrder.mapIndexed { index, link -> + ExoPlayerEngine.Playlist.Item( + url = link.url(), + mediaMetadata = metadataFactory.resourceMetadata(index), + duration = link.duration?.seconds + ) + } + ) + + val engine = ExoPlayerEngine( + application = application, + settingsResolver = settingsResolver, + playlist = playlist, + dataSourceFactory = dataSourceFactory, + configuration = configuration, + initialIndex = initialIndex, + initialPosition = initialPosition, + initialPreferences = initialPreferences + ) + + return Try.success(engine) + } + + override fun createPreferenceEditor( + publication: Publication, + initialPreferences: ExoPlayerPreferences + ): ExoPlayerPreferencesEditor = + ExoPlayerPreferencesEditor( + initialPreferences, + publication.metadata, + defaults + ) + + override fun createEmptyPreferences(): ExoPlayerPreferences = + ExoPlayerPreferences() +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerPreferences.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerPreferences.kt similarity index 59% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerPreferences.kt rename to readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerPreferences.kt index c77c551475..cac4a12758 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerPreferences.kt +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerPreferences.kt @@ -4,19 +4,27 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.exoplayer +package org.readium.adapter.exoplayer.audio import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.shared.ExperimentalReadiumApi +/** + * Preferences for the the ExoPlayer engine. + * + * @param pitch Playback pitch rate. + * @param speed Playback speed rate. + */ @ExperimentalReadiumApi @kotlinx.serialization.Serializable -data class ExoPlayerPreferences( - val rateMultiplier: Double? = null, +public data class ExoPlayerPreferences( + val pitch: Double? = null, + val speed: Double? = null ) : Configurable.Preferences<ExoPlayerPreferences> { override fun plus(other: ExoPlayerPreferences): ExoPlayerPreferences = ExoPlayerPreferences( - rateMultiplier = other.rateMultiplier ?: rateMultiplier, + pitch = other.pitch ?: pitch, + speed = other.speed ?: speed ) } diff --git a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerPreferencesEditor.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerPreferencesEditor.kt new file mode 100644 index 0000000000..c17da9f058 --- /dev/null +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerPreferencesEditor.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.adapter.exoplayer.audio + +import org.readium.r2.navigator.extensions.format +import org.readium.r2.navigator.preferences.DoubleIncrement +import org.readium.r2.navigator.preferences.PreferencesEditor +import org.readium.r2.navigator.preferences.RangePreference +import org.readium.r2.navigator.preferences.RangePreferenceDelegate +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Metadata + +/** + * Editor for a set of [ExoPlayerPreferences]. + * + * Use [ExoPlayerPreferencesEditor] to assist you in building a preferences user interface or modifying + * existing preferences. It includes rules for adjusting preferences, such as the supported values + * or ranges. + */ +@ExperimentalReadiumApi +public class ExoPlayerPreferencesEditor( + initialPreferences: ExoPlayerPreferences, + @Suppress("UNUSED_PARAMETER") publicationMetadata: Metadata, + defaults: ExoPlayerDefaults +) : PreferencesEditor<ExoPlayerPreferences> { + + private data class State( + val preferences: ExoPlayerPreferences, + val settings: ExoPlayerSettings + ) + + private val settingsResolver: ExoPlayerSettingsResolver = + ExoPlayerSettingsResolver(defaults) + + private var state: State = + initialPreferences.toState() + + override val preferences: ExoPlayerPreferences + get() = state.preferences + + override fun clear() { + updateValues { ExoPlayerPreferences() } + } + + public val pitch: RangePreference<Double> = + RangePreferenceDelegate( + getValue = { preferences.pitch }, + getEffectiveValue = { state.settings.pitch }, + getIsEffective = { true }, + updateValue = { value -> updateValues { it.copy(pitch = value) } }, + supportedRange = 0.1..Double.MAX_VALUE, + progressionStrategy = DoubleIncrement(0.1), + valueFormatter = { "${it.format(2)}x" } + ) + + public val speed: RangePreference<Double> = + RangePreferenceDelegate( + getValue = { preferences.speed }, + getEffectiveValue = { state.settings.speed }, + getIsEffective = { true }, + updateValue = { value -> updateValues { it.copy(speed = value) } }, + supportedRange = 0.1..Double.MAX_VALUE, + progressionStrategy = DoubleIncrement(0.1), + valueFormatter = { "${it.format(2)}x" } + ) + + private fun updateValues(updater: (ExoPlayerPreferences) -> ExoPlayerPreferences) { + val newPreferences = updater(preferences) + state = newPreferences.toState() + } + + private fun ExoPlayerPreferences.toState() = + State( + preferences = this, + settings = settingsResolver.settings(this) + ) +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerPreferencesSerializer.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerPreferencesSerializer.kt similarity index 84% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerPreferencesSerializer.kt rename to readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerPreferencesSerializer.kt index aada34c11e..4dae0d4aed 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerPreferencesSerializer.kt +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerPreferencesSerializer.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.exoplayer +package org.readium.adapter.exoplayer.audio import kotlinx.serialization.json.Json import org.readium.r2.navigator.preferences.PreferencesSerializer @@ -14,7 +14,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi * JSON serializer of [ExoPlayerPreferences]. */ @ExperimentalReadiumApi -class ExoPlayerPreferencesSerializer : PreferencesSerializer<ExoPlayerPreferences> { +public class ExoPlayerPreferencesSerializer : PreferencesSerializer<ExoPlayerPreferences> { override fun serialize(preferences: ExoPlayerPreferences): String = Json.encodeToString(ExoPlayerPreferences.serializer(), preferences) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerSettings.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerSettings.kt similarity index 62% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerSettings.kt rename to readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerSettings.kt index 58bc54b1e5..67b2e013f3 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerSettings.kt +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerSettings.kt @@ -4,12 +4,18 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.exoplayer +package org.readium.adapter.exoplayer.audio import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.shared.ExperimentalReadiumApi +/** + * Settings values of the ExoPlayer engine. + * + * @see ExoPlayerPreferences + */ @ExperimentalReadiumApi -data class ExoPlayerSettings( - val rateMultiplier: Double +public data class ExoPlayerSettings( + val pitch: Double, + val speed: Double ) : Configurable.Settings diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerSettingsResolver.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerSettingsResolver.kt similarity index 51% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerSettingsResolver.kt rename to readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerSettingsResolver.kt index b940630a3e..8a7719394a 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerSettingsResolver.kt +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerSettingsResolver.kt @@ -4,20 +4,19 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.exoplayer +package org.readium.adapter.exoplayer.audio import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.publication.Metadata @ExperimentalReadiumApi internal class ExoPlayerSettingsResolver( - private val metadata: Metadata, -) { - - fun settings(preferences: ExoPlayerPreferences): ExoPlayerSettings { + private val defaults: ExoPlayerDefaults +) : ExoPlayerEngine.SettingsResolver { + override fun settings(preferences: ExoPlayerPreferences): ExoPlayerSettings { return ExoPlayerSettings( - rateMultiplier = preferences.rateMultiplier ?: 1.0, + pitch = preferences.pitch ?: defaults.pitch ?: 1.0, + speed = preferences.speed ?: defaults.speed ?: 1.0 ) } } diff --git a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/SmartSeeker.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/SmartSeeker.kt new file mode 100644 index 0000000000..68e42c62a2 --- /dev/null +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/SmartSeeker.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.adapter.exoplayer.audio + +import kotlin.time.Duration +import kotlin.time.ExperimentalTime + +/** + * Computes relative seeks across playlist items. + */ +@ExperimentalTime +internal object SmartSeeker { + + data class Result(val index: Int, val position: Duration) + + fun dispatchSeek( + offset: Duration, + currentPosition: Duration, + currentIndex: Int, + playlist: List<Duration> + ): Result { + val currentDuration = playlist[currentIndex] + val dummyNewPosition = currentPosition + offset + + return when { + offset == Duration.ZERO -> { + Result(currentIndex, currentPosition) + } + currentDuration > dummyNewPosition && dummyNewPosition > Duration.ZERO -> { + Result(currentIndex, dummyNewPosition) + } + offset.isPositive() && currentIndex == playlist.size - 1 -> { + Result(currentIndex, playlist[currentIndex]) + } + offset.isNegative() && currentIndex == 0 -> { + Result(0, Duration.ZERO) + } + offset.isPositive() -> { + var toDispatch = offset - (currentDuration - currentPosition) + var index = currentIndex + 1 + while (toDispatch > playlist[index] && index + 1 < playlist.size) { + toDispatch -= playlist[index] + index += 1 + } + Result(index, toDispatch.coerceAtMost(playlist[index])) + } + else -> { + var toDispatch = offset + currentPosition + var index = currentIndex - 1 + while (-toDispatch > playlist[index] && index > 0) { + toDispatch += playlist[index] + index -= 1 + } + Result(index, (playlist[index] + toDispatch).coerceAtLeast(Duration.ZERO)) + } + } + } +} diff --git a/readium/adapters/exoplayer/audio/src/test/java/org/readium/adapter/exoplayer/audio/SmartSeekerTest.kt b/readium/adapters/exoplayer/audio/src/test/java/org/readium/adapter/exoplayer/audio/SmartSeekerTest.kt new file mode 100644 index 0000000000..f410850119 --- /dev/null +++ b/readium/adapters/exoplayer/audio/src/test/java/org/readium/adapter/exoplayer/audio/SmartSeekerTest.kt @@ -0,0 +1,114 @@ +package org.readium.adapter.exoplayer.audio + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.ExperimentalTime +import org.junit.Assert.assertEquals +import org.junit.Test + +@OptIn(ExperimentalTime::class) +class SmartSeekerTest { + + private val playlist: List<Duration> = listOf( + 10, + 20, + 15, + 800, + 10, + 230, + 20, + 10 + ).map { it.seconds } + + private val forwardOffset = 50.seconds + + private val backwardOffset = (-50).seconds + + @Test + fun `seek forward within current item`() { + val result = SmartSeeker.dispatchSeek( + offset = forwardOffset, + currentPosition = 200.seconds, + currentIndex = 3, + playlist + ) + assertEquals(SmartSeeker.Result(3, 250.seconds), result) + } + + @Test + fun `seek backward within current item`() { + val result = SmartSeeker.dispatchSeek( + offset = backwardOffset, + currentPosition = 200.seconds, + currentIndex = 3, + playlist + ) + assertEquals(SmartSeeker.Result(3, 150.seconds), result) + } + + @Test + fun `seek forward across items`() { + val result = SmartSeeker.dispatchSeek( + offset = forwardOffset, + currentPosition = 780.seconds, + currentIndex = 3, + playlist + ) + assertEquals(SmartSeeker.Result(5, 20.seconds), result) + } + + @Test + fun `seek backward across items`() { + val result = SmartSeeker.dispatchSeek( + offset = backwardOffset, + currentPosition = 10.seconds, + currentIndex = 3, + playlist + ) + assertEquals(SmartSeeker.Result(0, 5.seconds), result) + } + + @Test + fun `positive offset too big within last item`() { + val result = SmartSeeker.dispatchSeek( + offset = forwardOffset, + currentPosition = 5.seconds, + currentIndex = 7, + playlist + ) + assertEquals(SmartSeeker.Result(7, 10.seconds), result) + } + + @Test + fun `positive offset too big across items`() { + val result = SmartSeeker.dispatchSeek( + offset = forwardOffset, + currentPosition = 220.seconds, + currentIndex = 6, + playlist + ) + assertEquals(SmartSeeker.Result(7, 10.seconds), result) + } + + @Test + fun `negative offset too small within first item`() { + val result = SmartSeeker.dispatchSeek( + offset = backwardOffset, + currentPosition = 5.seconds, + currentIndex = 0, + playlist + ) + assertEquals(SmartSeeker.Result(0, 0.seconds), result) + } + + @Test + fun `negative offset too small across items`() { + val result = SmartSeeker.dispatchSeek( + offset = backwardOffset, + currentPosition = 10.seconds, + currentIndex = 2, + playlist + ) + assertEquals(SmartSeeker.Result(0, 0.seconds), result) + } +} diff --git a/readium/adapters/exoplayer/build.gradle.kts b/readium/adapters/exoplayer/build.gradle.kts new file mode 100644 index 0000000000..c36ab5a491 --- /dev/null +++ b/readium/adapters/exoplayer/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +plugins { + id("com.android.library") + kotlin("android") + kotlin("plugin.parcelize") +} + +android { + resourcePrefix = "readium_" + + compileSdk = 34 + + defaultConfig { + minSdk = 21 + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + freeCompilerArgs = freeCompilerArgs + listOf( + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=org.readium.r2.shared.InternalReadiumApi" + ) + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android.txt")) + } + } + namespace = "org.readium.adapter.exoplayer" +} + +kotlin { + explicitApi() +} + +rootProject.ext["publish.artifactId"] = "readium-adapter-exoplayer" +apply(from = "$rootDir/scripts/publish-module.gradle") + +dependencies { + api(project(":readium:adapters:exoplayer:readium-adapter-exoplayer-audio")) +} diff --git a/readium/adapters/pspdfkit/pspdfkit-document/src/main/AndroidManifest.xml b/readium/adapters/exoplayer/src/main/AndroidManifest.xml similarity index 100% rename from readium/adapters/pspdfkit/pspdfkit-document/src/main/AndroidManifest.xml rename to readium/adapters/exoplayer/src/main/AndroidManifest.xml diff --git a/readium/adapters/pdfium/build.gradle.kts b/readium/adapters/pdfium/build.gradle.kts index 8021c8dee7..de6e0c5a4e 100644 --- a/readium/adapters/pdfium/build.gradle.kts +++ b/readium/adapters/pdfium/build.gradle.kts @@ -13,19 +13,19 @@ plugins { android { resourcePrefix = "readium_" - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs = freeCompilerArgs + listOf( "-opt-in=kotlin.RequiresOptIn", "-opt-in=org.readium.r2.shared.InternalReadiumApi" @@ -37,7 +37,11 @@ android { proguardFiles(getDefaultProguardFile("proguard-android.txt")) } } - namespace = "org.readium.adapters.pdfium" + namespace = "org.readium.adapter.pdfium" +} + +kotlin { + explicitApi() } rootProject.ext["publish.artifactId"] = "readium-adapter-pdfium" diff --git a/readium/adapters/pdfium/pdfium-document/build.gradle.kts b/readium/adapters/pdfium/document/build.gradle.kts similarity index 82% rename from readium/adapters/pdfium/pdfium-document/build.gradle.kts rename to readium/adapters/pdfium/document/build.gradle.kts index 6113dec3c1..c18cdbde30 100644 --- a/readium/adapters/pdfium/pdfium-document/build.gradle.kts +++ b/readium/adapters/pdfium/document/build.gradle.kts @@ -13,19 +13,19 @@ plugins { android { resourcePrefix = "readium_" - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs = freeCompilerArgs + listOf( "-opt-in=kotlin.RequiresOptIn", "-opt-in=org.readium.r2.shared.InternalReadiumApi" @@ -40,7 +40,11 @@ android { buildFeatures { viewBinding = true } - namespace = "org.readium.adapters.pdfium.document" + namespace = "org.readium.adapter.pdfium.document" +} + +kotlin { + explicitApi() } rootProject.ext["publish.artifactId"] = "readium-adapter-pdfium-document" diff --git a/readium/adapters/pdfium/document/src/main/AndroidManifest.xml b/readium/adapters/pdfium/document/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2d10029868 --- /dev/null +++ b/readium/adapters/pdfium/document/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest /> + diff --git a/readium/adapters/pdfium/pdfium-document/src/main/java/org/readium/adapters/pdfium/document/PdfiumDocument.kt b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt similarity index 72% rename from readium/adapters/pdfium/pdfium-document/src/main/java/org/readium/adapters/pdfium/document/PdfiumDocument.kt rename to readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt index d654bf1024..6a388c6454 100644 --- a/readium/adapters/pdfium/pdfium-document/src/main/java/org/readium/adapters/pdfium/document/PdfiumDocument.kt +++ b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pdfium.document +package org.readium.adapter.pdfium.document import android.content.Context import android.graphics.Bitmap @@ -15,19 +15,22 @@ import java.io.File import kotlin.reflect.KClass import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.readium.r2.shared.PdfSupport +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.extensions.md5 import org.readium.r2.shared.extensions.tryOrNull -import org.readium.r2.shared.fetcher.Resource +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ReadTry +import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use import timber.log.Timber -@OptIn(PdfSupport::class) -class PdfiumDocument( - val core: PdfiumCore, - val document: _PdfiumDocument, +public class PdfiumDocument( + @InternalReadiumApi public val core: PdfiumCore, + @InternalReadiumApi public val document: _PdfiumDocument, override val identifier: String?, override val pageCount: Int ) : PdfDocument { @@ -68,10 +71,9 @@ class PdfiumDocument( override suspend fun close() {} - companion object + public companion object } -@OptIn(PdfSupport::class) private fun _PdfiumDocument.Bookmark.toOutlineNode(): PdfDocument.OutlineNode = PdfDocument.OutlineNode( title = title, @@ -79,36 +81,48 @@ private fun _PdfiumDocument.Bookmark.toOutlineNode(): PdfDocument.OutlineNode = children = children.map { it.toOutlineNode() } ) -@OptIn(PdfSupport::class) -class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory<PdfiumDocument> { +public class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory<PdfiumDocument> { override val documentType: KClass<PdfiumDocument> = PdfiumDocument::class private val core by lazy { PdfiumCore(context.applicationContext) } - override suspend fun open(file: File, password: String?): PdfiumDocument = - core.fromFile(file, password) - - override suspend fun open(resource: Resource, password: String?): PdfiumDocument { + override suspend fun open(resource: Resource, password: String?): ReadTry<PdfiumDocument> { // First try to open the resource as a file on the FS for performance improvement, as // PDFium requires the whole PDF document to be loaded in memory when using raw bytes. return resource.openAsFile(password) ?: resource.openBytes(password) } - private suspend fun Resource.openAsFile(password: String?): PdfiumDocument? = - file?.let { - tryOrNull { open(it, password) } + private suspend fun Resource.openAsFile(password: String?): ReadTry<PdfiumDocument>? = + tryOrNull { + sourceUrl?.toFile()?.let { file -> + withContext(Dispatchers.IO) { + Try.success(core.fromFile(file, password)) + } + } } - private suspend fun Resource.openBytes(password: String?): PdfiumDocument = + private suspend fun Resource.openBytes(password: String?): ReadTry<PdfiumDocument> = use { - core.fromBytes(read().getOrThrow(), password) + it.read() + .flatMap { bytes -> + try { + Try.success( + core.fromBytes(bytes, password) + ) + } catch (e: Exception) { + Try.failure(ReadError.Decoding(e)) + } + } } private fun PdfiumCore.fromFile(file: File, password: String?): PdfiumDocument = fromDocument( - newDocument(ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY), password), + newDocument( + ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY), + password + ), identifier = file.md5() ) diff --git a/readium/adapters/pdfium/document/src/main/java/org/readium/adapters/pdfium/document/Deprecated.kt b/readium/adapters/pdfium/document/src/main/java/org/readium/adapters/pdfium/document/Deprecated.kt new file mode 100644 index 0000000000..fbbe24936d --- /dev/null +++ b/readium/adapters/pdfium/document/src/main/java/org/readium/adapters/pdfium/document/Deprecated.kt @@ -0,0 +1,15 @@ +package org.readium.adapters.pdfium.document + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pdfium.document.PdfiumDocument"), + level = DeprecationLevel.ERROR +) +public typealias PdfiumDocument = org.readium.adapter.pdfium.document.PdfiumDocument + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pdfium.document.PdfiumDocumentFactory"), + level = DeprecationLevel.ERROR +) +public typealias PdfiumDocumentFactory = org.readium.adapter.pdfium.document.PdfiumDocumentFactory diff --git a/readium/adapters/pdfium/pdfium-navigator/build.gradle.kts b/readium/adapters/pdfium/navigator/build.gradle.kts similarity index 84% rename from readium/adapters/pdfium/pdfium-navigator/build.gradle.kts rename to readium/adapters/pdfium/navigator/build.gradle.kts index 31ce3a0f4d..7afc1d3620 100644 --- a/readium/adapters/pdfium/pdfium-navigator/build.gradle.kts +++ b/readium/adapters/pdfium/navigator/build.gradle.kts @@ -14,19 +14,19 @@ plugins { android { resourcePrefix = "readium_" - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs = freeCompilerArgs + listOf( "-opt-in=kotlin.RequiresOptIn", "-opt-in=org.readium.r2.shared.InternalReadiumApi" @@ -41,7 +41,11 @@ android { buildFeatures { viewBinding = true } - namespace = "org.readium.adapters.pdfium.navigator" + namespace = "org.readium.adapter.pdfium.navigator" +} + +kotlin { + explicitApi() } rootProject.ext["publish.artifactId"] = "readium-adapter-pdfium-navigator" diff --git a/readium/adapters/pdfium/pdfium-navigator/src/main/AndroidManifest.xml b/readium/adapters/pdfium/navigator/src/main/AndroidManifest.xml similarity index 100% rename from readium/adapters/pdfium/pdfium-navigator/src/main/AndroidManifest.xml rename to readium/adapters/pdfium/navigator/src/main/AndroidManifest.xml diff --git a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumDefaults.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDefaults.kt similarity index 80% rename from readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumDefaults.kt rename to readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDefaults.kt index 0515064caa..024509cf3b 100644 --- a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumDefaults.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDefaults.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pdfium.navigator +package org.readium.adapter.pdfium.navigator import org.readium.r2.navigator.preferences.ReadingProgression import org.readium.r2.shared.ExperimentalReadiumApi @@ -17,7 +17,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi * @see PdfiumPreferences */ @ExperimentalReadiumApi -data class PdfiumDefaults( +public data class PdfiumDefaults( val pageSpacing: Double? = null, - val readingProgression: ReadingProgression? = null, + val readingProgression: ReadingProgression? = null ) diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt new file mode 100644 index 0000000000..21911e3ad9 --- /dev/null +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt @@ -0,0 +1,197 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.adapter.pdfium.navigator + +import android.graphics.PointF +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import com.github.barteksc.pdfviewer.PDFView +import kotlin.math.roundToInt +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.readium.adapter.pdfium.document.PdfiumDocumentFactory +import org.readium.r2.navigator.pdf.PdfDocumentFragment +import org.readium.r2.navigator.preferences.Axis +import org.readium.r2.navigator.preferences.Fit +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.LocalizedString +import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.publication.Metadata +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.SingleJob +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.toDebugDescription +import timber.log.Timber + +@ExperimentalReadiumApi +public class PdfiumDocumentFragment internal constructor( + private val publication: Publication, + private val href: Url, + private val initialPageIndex: Int, + initialSettings: PdfiumSettings, + private val listener: Listener? +) : PdfDocumentFragment<PdfiumSettings>() { + + // Dummy constructor to address https://github.com/readium/kotlin-toolkit/issues/395 + public constructor() : this( + publication = Publication( + manifest = Manifest( + metadata = Metadata( + identifier = "readium:dummy", + localizedTitle = LocalizedString("") + ) + ) + ), + href = Url("publication.pdf")!!, + initialPageIndex = 0, + initialSettings = PdfiumSettings( + fit = Fit.WIDTH, + pageSpacing = 0.0, + readingProgression = ReadingProgression.LTR, + scrollAxis = Axis.VERTICAL + ), + listener = null + ) + + internal interface Listener { + fun onResourceLoadFailed(href: Url, error: ReadError) + fun onConfigurePdfView(configurator: PDFView.Configurator) + fun onTap(point: PointF): Boolean + } + + private lateinit var pdfView: PDFView + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = + PDFView(inflater.context, null) + .also { pdfView = it } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + resetJob = SingleJob(viewLifecycleOwner.lifecycleScope) + reset(pageIndex = initialPageIndex) + } + + private lateinit var resetJob: SingleJob + + private fun reset(pageIndex: Int = _pageIndex.value) { + if (view == null) return + val context = context?.applicationContext ?: return + + resetJob.launch { + val resource = requireNotNull(publication.get(href)) + val document = PdfiumDocumentFactory(context) + // PDFium crashes when reusing the same PdfDocument, so we must not cache it. +// .cachedIn(publication) + .open(resource, null) + .getOrElse { error -> + Timber.e(error.toDebugDescription()) + listener?.onResourceLoadFailed(href, error) + return@launch + } + + pageCount = document.pageCount + val page = convertPageIndexToView(pageIndex) + + pdfView.recycle() + pdfView + .fromSource { _, _, _ -> document.document } + .apply { + if (isPagesOrderReversed) { + // AndroidPdfViewer doesn't support RTL. A workaround is to provide + // the explicit page list in the right order. + pages(*((pageCount - 1) downTo 0).toList().toIntArray()) + } + } + .swipeHorizontal(settings.scrollAxis == Axis.HORIZONTAL) + .spacing(settings.pageSpacing.roundToInt()) + // Customization of [PDFView] is done before setting the listeners, + // to avoid overriding them in reading apps, which would break the + // navigator. + .apply { listener?.onConfigurePdfView(this) } + .defaultPage(page) + .onRender { _, _, _ -> + if (settings.fit == Fit.WIDTH) { + pdfView.fitToWidth() + // Using `fitToWidth` often breaks the use of `defaultPage`, so we + // need to jump manually to the target page. + pdfView.jumpTo(page, false) + } + } + .onPageChange { index, _ -> + _pageIndex.value = convertPageIndexFromView(index) + } + .onTap { event -> + listener?.onTap(PointF(event.x, event.y)) ?: false + } + .load() + } + } + + private var pageCount = 0 + + private val _pageIndex = MutableStateFlow(initialPageIndex) + override val pageIndex: StateFlow<Int> = _pageIndex.asStateFlow() + + override fun goToPageIndex(index: Int, animated: Boolean): Boolean { + if (!isValidPageIndex(index)) { + return false + } + pdfView.jumpTo(convertPageIndexToView(index), animated) + return true + } + + private fun isValidPageIndex(pageIndex: Int): Boolean { + val validRange = 0 until pageCount + return validRange.contains(pageIndex) + } + + private fun convertPageIndexToView(page: Int): Int { + var index = (page - 1).coerceAtLeast(0) + if (isPagesOrderReversed) { + index = (pageCount - 1) - index + } + return index + } + + private fun convertPageIndexFromView(index: Int): Int { + var page = index + 1 + if (isPagesOrderReversed) { + page = (pageCount + 1) - page + } + return page + } + + /** + * Indicates whether the order of the [PDFView] pages is reversed to take into account + * right-to-left reading progressions. + */ + private val isPagesOrderReversed: Boolean get() = + settings.scrollAxis == Axis.HORIZONTAL && settings.readingProgression == ReadingProgression.RTL + + private var settings: PdfiumSettings = initialSettings + + override fun applySettings(settings: PdfiumSettings) { + if (this.settings == settings) { + return + } + + this.settings = settings + reset() + } +} diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt new file mode 100644 index 0000000000..5d8fe18863 --- /dev/null +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.adapter.pdfium.navigator + +import android.graphics.PointF +import com.github.barteksc.pdfviewer.PDFView +import org.readium.r2.navigator.OverflowableNavigator +import org.readium.r2.navigator.SimpleOverflow +import org.readium.r2.navigator.input.TapEvent +import org.readium.r2.navigator.pdf.PdfDocumentFragmentInput +import org.readium.r2.navigator.pdf.PdfEngineProvider +import org.readium.r2.navigator.util.SingleFragmentFactory +import org.readium.r2.navigator.util.createFragmentFactory +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Metadata +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError + +/** + * Main component to use the PDF navigator with the PDFium adapter. + * + * Provide [PdfiumDefaults] to customize the default values that will be used by + * the navigator for some preferences. + */ +@ExperimentalReadiumApi +public class PdfiumEngineProvider( + private val defaults: PdfiumDefaults = PdfiumDefaults(), + private val listener: Listener? = null +) : PdfEngineProvider<PdfiumSettings, PdfiumPreferences, PdfiumPreferencesEditor> { + + public interface Listener : PdfEngineProvider.Listener { + + /** Called when configuring [PDFView]. */ + public fun onConfigurePdfView(configurator: PDFView.Configurator) {} + } + + override fun createDocumentFragmentFactory( + input: PdfDocumentFragmentInput<PdfiumSettings> + ): SingleFragmentFactory<PdfiumDocumentFragment> = + createFragmentFactory { + PdfiumDocumentFragment( + publication = input.publication, + href = input.href, + initialPageIndex = input.pageIndex, + initialSettings = input.settings, + listener = object : PdfiumDocumentFragment.Listener { + override fun onResourceLoadFailed(href: Url, error: ReadError) { + input.navigatorListener?.onResourceLoadFailed(href, error) + } + + override fun onConfigurePdfView(configurator: PDFView.Configurator) { + listener?.onConfigurePdfView(configurator) + } + + override fun onTap(point: PointF): Boolean = + input.inputListener?.onTap(TapEvent(point)) ?: false + } + ) + } + + override fun computeSettings(metadata: Metadata, preferences: PdfiumPreferences): PdfiumSettings { + val settingsPolicy = PdfiumSettingsResolver(metadata, defaults) + return settingsPolicy.settings(preferences) + } + + override fun computeOverflow(settings: PdfiumSettings): OverflowableNavigator.Overflow = + SimpleOverflow( + readingProgression = settings.readingProgression, + scroll = true, + axis = settings.scrollAxis + ) + + override fun createPreferenceEditor( + publication: Publication, + initialPreferences: PdfiumPreferences + ): PdfiumPreferencesEditor = + PdfiumPreferencesEditor( + initialPreferences, + publication.metadata, + defaults + ) + + override fun createEmptyPreferences(): PdfiumPreferences = + PdfiumPreferences() +} diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumNavigator.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumNavigator.kt new file mode 100644 index 0000000000..2086be435c --- /dev/null +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumNavigator.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.adapter.pdfium.navigator + +import org.readium.r2.navigator.pdf.PdfNavigatorFactory +import org.readium.r2.navigator.pdf.PdfNavigatorFragment +import org.readium.r2.shared.ExperimentalReadiumApi + +@ExperimentalReadiumApi +public typealias PdfiumNavigatorFragment = PdfNavigatorFragment<PdfiumSettings, PdfiumPreferences> + +@ExperimentalReadiumApi +public typealias PdfiumNavigatorFactory = PdfNavigatorFactory<PdfiumSettings, PdfiumPreferences, PdfiumPreferencesEditor> diff --git a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumPreferences.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumPreferences.kt similarity index 85% rename from readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumPreferences.kt rename to readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumPreferences.kt index 951d56c58f..076ab3c43d 100644 --- a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumPreferences.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumPreferences.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pdfium.navigator +package org.readium.adapter.pdfium.navigator import kotlinx.serialization.Serializable import org.readium.r2.navigator.preferences.Axis @@ -23,11 +23,11 @@ import org.readium.r2.shared.ExperimentalReadiumApi */ @ExperimentalReadiumApi @Serializable -data class PdfiumPreferences( +public data class PdfiumPreferences( val fit: Fit? = null, val pageSpacing: Double? = null, val readingProgression: ReadingProgression? = null, - val scrollAxis: Axis? = null, + val scrollAxis: Axis? = null ) : Configurable.Preferences<PdfiumPreferences> { init { @@ -35,11 +35,11 @@ data class PdfiumPreferences( require(pageSpacing == null || pageSpacing >= 0) } - override operator fun plus(other: PdfiumPreferences) = + override operator fun plus(other: PdfiumPreferences): PdfiumPreferences = PdfiumPreferences( fit = other.fit ?: fit, pageSpacing = other.pageSpacing ?: pageSpacing, readingProgression = other.readingProgression ?: readingProgression, - scrollAxis = other.scrollAxis ?: scrollAxis, + scrollAxis = other.scrollAxis ?: scrollAxis ) } diff --git a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumPreferencesEditor.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumPreferencesEditor.kt similarity index 86% rename from readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumPreferencesEditor.kt rename to readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumPreferencesEditor.kt index c4d0b2b768..fefd7eeedd 100644 --- a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumPreferencesEditor.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumPreferencesEditor.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pdfium.navigator +package org.readium.adapter.pdfium.navigator import org.readium.r2.navigator.extensions.format import org.readium.r2.navigator.preferences.* @@ -19,10 +19,10 @@ import org.readium.r2.shared.publication.Metadata * or ranges. */ @ExperimentalReadiumApi -class PdfiumPreferencesEditor internal constructor( +public class PdfiumPreferencesEditor internal constructor( initialPreferences: PdfiumPreferences, publicationMetadata: Metadata, - defaults: PdfiumDefaults, + defaults: PdfiumDefaults ) : PreferencesEditor<PdfiumPreferences> { private data class State( @@ -49,19 +49,19 @@ class PdfiumPreferencesEditor internal constructor( /** * Indicates how pages should be laid out within the viewport. */ - val fit: EnumPreference<Fit> = + public val fit: EnumPreference<Fit> = EnumPreferenceDelegate( getValue = { preferences.fit }, getEffectiveValue = { state.settings.fit }, getIsEffective = { true }, updateValue = { value -> updateValues { it.copy(fit = value) } }, - supportedValues = listOf(Fit.CONTAIN, Fit.WIDTH), + supportedValues = listOf(Fit.CONTAIN, Fit.WIDTH) ) /** * Space between pages in dp. */ - val pageSpacing: RangePreference<Double> = + public val pageSpacing: RangePreference<Double> = RangePreferenceDelegate( getValue = { preferences.pageSpacing }, getEffectiveValue = { state.settings.pageSpacing }, @@ -69,31 +69,31 @@ class PdfiumPreferencesEditor internal constructor( updateValue = { value -> updateValues { it.copy(pageSpacing = value) } }, supportedRange = 0.0..50.0, progressionStrategy = DoubleIncrement(5.0), - valueFormatter = { "${it.format(1)} dp" }, + valueFormatter = { "${it.format(1)} dp" } ) /** * Direction of the horizontal progression across pages. */ - val readingProgression: EnumPreference<ReadingProgression> = + public val readingProgression: EnumPreference<ReadingProgression> = EnumPreferenceDelegate( getValue = { preferences.readingProgression }, getEffectiveValue = { state.settings.readingProgression }, getIsEffective = { true }, updateValue = { value -> updateValues { it.copy(readingProgression = value) } }, - supportedValues = listOf(ReadingProgression.LTR, ReadingProgression.RTL), + supportedValues = listOf(ReadingProgression.LTR, ReadingProgression.RTL) ) /** * Indicates the axis along which pages should be laid out in scroll mode. */ - val scrollAxis: EnumPreference<Axis> = + public val scrollAxis: EnumPreference<Axis> = EnumPreferenceDelegate( getValue = { preferences.scrollAxis }, getEffectiveValue = { state.settings.scrollAxis }, getIsEffective = { true }, updateValue = { value -> updateValues { it.copy(scrollAxis = value) } }, - supportedValues = listOf(Axis.VERTICAL, Axis.HORIZONTAL), + supportedValues = listOf(Axis.VERTICAL, Axis.HORIZONTAL) ) private fun updateValues(updater: (PdfiumPreferences) -> PdfiumPreferences) { diff --git a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumPreferencesFilters.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumPreferencesFilters.kt similarity index 69% rename from readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumPreferencesFilters.kt rename to readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumPreferencesFilters.kt index 75a1dc5f93..e1898d4967 100644 --- a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumPreferencesFilters.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumPreferencesFilters.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pdfium.navigator +package org.readium.adapter.pdfium.navigator import org.readium.r2.navigator.preferences.PreferencesFilter import org.readium.r2.shared.ExperimentalReadiumApi @@ -13,11 +13,11 @@ import org.readium.r2.shared.ExperimentalReadiumApi * Suggested filter to keep only shared [PdfiumPreferences]. */ @ExperimentalReadiumApi -object PdfiumSharedPreferencesFilter : PreferencesFilter<PdfiumPreferences> { +public object PdfiumSharedPreferencesFilter : PreferencesFilter<PdfiumPreferences> { override fun filter(preferences: PdfiumPreferences): PdfiumPreferences = preferences.copy( - readingProgression = null, + readingProgression = null ) } @@ -25,10 +25,10 @@ object PdfiumSharedPreferencesFilter : PreferencesFilter<PdfiumPreferences> { * Suggested filter to keep only publication-specific [PdfiumPreferences]. */ @ExperimentalReadiumApi -object PdfiumPublicationPreferencesFilter : PreferencesFilter<PdfiumPreferences> { +public object PdfiumPublicationPreferencesFilter : PreferencesFilter<PdfiumPreferences> { override fun filter(preferences: PdfiumPreferences): PdfiumPreferences = PdfiumPreferences( - readingProgression = preferences.readingProgression, + readingProgression = preferences.readingProgression ) } diff --git a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumPreferencesSerializer.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumPreferencesSerializer.kt similarity index 84% rename from readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumPreferencesSerializer.kt rename to readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumPreferencesSerializer.kt index 8353a636d4..f944732e17 100644 --- a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumPreferencesSerializer.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumPreferencesSerializer.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pdfium.navigator +package org.readium.adapter.pdfium.navigator import kotlinx.serialization.json.Json import org.readium.r2.navigator.preferences.PreferencesSerializer @@ -14,7 +14,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi * JSON serializer of [PdfiumPreferences]. */ @ExperimentalReadiumApi -class PdfiumPreferencesSerializer : PreferencesSerializer<PdfiumPreferences> { +public class PdfiumPreferencesSerializer : PreferencesSerializer<PdfiumPreferences> { override fun serialize(preferences: PdfiumPreferences): String = Json.encodeToString(PdfiumPreferences.serializer(), preferences) diff --git a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumSettings.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumSettings.kt similarity index 83% rename from readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumSettings.kt rename to readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumSettings.kt index dc83987411..0c8a56af5d 100644 --- a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumSettings.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumSettings.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pdfium.navigator +package org.readium.adapter.pdfium.navigator import org.readium.r2.navigator.preferences.* import org.readium.r2.shared.ExperimentalReadiumApi @@ -15,9 +15,9 @@ import org.readium.r2.shared.ExperimentalReadiumApi * @see PdfiumPreferences */ @ExperimentalReadiumApi -data class PdfiumSettings( +public data class PdfiumSettings( val fit: Fit, val pageSpacing: Double, val readingProgression: ReadingProgression, - val scrollAxis: Axis, + val scrollAxis: Axis ) : Configurable.Settings diff --git a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumSettingsResolver.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumSettingsResolver.kt similarity index 95% rename from readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumSettingsResolver.kt rename to readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumSettingsResolver.kt index cc821d6a0c..366f21289f 100644 --- a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumSettingsResolver.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumSettingsResolver.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pdfium.navigator +package org.readium.adapter.pdfium.navigator import org.readium.r2.navigator.preferences.Axis import org.readium.r2.navigator.preferences.Fit @@ -48,7 +48,7 @@ internal class PdfiumSettingsResolver( fit = fit, pageSpacing = pageSpacing, readingProgression = readingProgression, - scrollAxis = scrollAxis, + scrollAxis = scrollAxis ) } } diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapters/pdfium/navigator/Deprecated.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapters/pdfium/navigator/Deprecated.kt new file mode 100644 index 0000000000..4c2109f269 --- /dev/null +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapters/pdfium/navigator/Deprecated.kt @@ -0,0 +1,82 @@ +@file:OptIn(ExperimentalReadiumApi::class) + +package org.readium.adapters.pdfium.navigator + +import org.readium.r2.shared.ExperimentalReadiumApi + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pdfium.navigator.PdfiumEngineProvider"), + level = DeprecationLevel.ERROR +) +public typealias PdfiumEngineProvider = org.readium.adapter.pdfium.navigator.PdfiumEngineProvider + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pdfium.navigator.PdfiumDefaults"), + level = DeprecationLevel.ERROR +) +public typealias PdfiumDefaults = org.readium.adapter.pdfium.navigator.PdfiumDefaults + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pdfium.navigator.PdfiumPreferences"), + level = DeprecationLevel.ERROR +) +public typealias PdfiumPreferences = org.readium.adapter.pdfium.navigator.PdfiumPreferences + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pdfium.navigator.PdfiumPreferencesEditor"), + level = DeprecationLevel.ERROR +) +public typealias PdfiumPreferencesEditor = org.readium.adapter.pdfium.navigator.PdfiumPreferencesEditor + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pdfium.navigator.PdfiumSettings"), + level = DeprecationLevel.ERROR +) +public typealias PdfiumSettings = org.readium.adapter.pdfium.navigator.PdfiumSettings + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pdfium.navigator.PdfiumDocumentFragment"), + level = DeprecationLevel.ERROR +) +public typealias PdfiumDocumentFragment = org.readium.adapter.pdfium.navigator.PdfiumDocumentFragment + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pdfium.navigator.PdfiumNavigatorFactory"), + level = DeprecationLevel.ERROR +) +public typealias PdfiumNavigatorFactory = org.readium.adapter.pdfium.navigator.PdfiumNavigatorFactory + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pdfium.navigator.PdfiumNavigatorFragment"), + level = DeprecationLevel.ERROR +) +public typealias PdfiumNavigatorFragment = org.readium.adapter.pdfium.navigator.PdfiumNavigatorFragment + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pdfium.navigator.PdfiumPreferencesSerializer"), + level = DeprecationLevel.ERROR +) +public typealias PdfiumPreferencesSerializer = org.readium.adapter.pdfium.navigator.PdfiumPreferencesSerializer + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pdfium.navigator.PdfiumPublicationPreferencesFilter"), + level = DeprecationLevel.ERROR +) +public typealias PdfiumPublicationPreferencesFilter = org.readium.adapter.pdfium.navigator.PdfiumPublicationPreferencesFilter + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pdfium.navigator.PdfiumSharedPreferencesFilter"), + level = DeprecationLevel.ERROR +) +public typealias PdfiumSharedPreferencesFilter = org.readium.adapter.pdfium.navigator.PdfiumSharedPreferencesFilter diff --git a/readium/adapters/pdfium/pdfium-navigator/src/main/res/layout/readium_pspdfkit_fragment.xml b/readium/adapters/pdfium/navigator/src/main/res/layout/readium_pspdfkit_fragment.xml similarity index 100% rename from readium/adapters/pdfium/pdfium-navigator/src/main/res/layout/readium_pspdfkit_fragment.xml rename to readium/adapters/pdfium/navigator/src/main/res/layout/readium_pspdfkit_fragment.xml diff --git a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumDocumentFragment.kt b/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumDocumentFragment.kt deleted file mode 100644 index d8248eccfc..0000000000 --- a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumDocumentFragment.kt +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.adapters.pdfium.navigator - -import android.graphics.PointF -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.lifecycle.lifecycleScope -import com.github.barteksc.pdfviewer.PDFView -import kotlin.math.roundToInt -import kotlinx.coroutines.launch -import org.readium.adapters.pdfium.document.PdfiumDocumentFactory -import org.readium.r2.navigator.pdf.PdfDocumentFragment -import org.readium.r2.navigator.preferences.Axis -import org.readium.r2.navigator.preferences.Fit -import org.readium.r2.navigator.preferences.ReadingProgression -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.LocalizedString -import org.readium.r2.shared.publication.Manifest -import org.readium.r2.shared.publication.Metadata -import org.readium.r2.shared.publication.Publication -import timber.log.Timber - -@ExperimentalReadiumApi -class PdfiumDocumentFragment internal constructor( - private val publication: Publication, - private val link: Link, - private val initialPageIndex: Int, - settings: PdfiumSettings, - private val appListener: Listener?, - private val navigatorListener: PdfDocumentFragment.Listener? -) : PdfDocumentFragment<PdfiumSettings>() { - - // Dummy constructor to address https://github.com/readium/kotlin-toolkit/issues/395 - constructor() : this( - publication = Publication( - manifest = Manifest( - metadata = Metadata( - identifier = "readium:dummy", - localizedTitle = LocalizedString("") - ) - ) - ), - link = Link(href = "publication.pdf", type = "application/pdf"), - initialPageIndex = 0, - settings = PdfiumSettings( - fit = Fit.WIDTH, - pageSpacing = 0.0, - readingProgression = ReadingProgression.LTR, - scrollAxis = Axis.VERTICAL - ), - appListener = null, - navigatorListener = null - ) - - interface Listener { - /** Called when configuring [PDFView]. */ - fun onConfigurePdfView(configurator: PDFView.Configurator) {} - } - - override var settings: PdfiumSettings = settings - set(value) { - if (field == value) return - - val page = pageIndex - field = value - reloadDocumentAtPage(page) - } - - private lateinit var pdfView: PDFView - - private var isReloading: Boolean = false - private var hasToReload: Int? = null - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View = - PDFView(inflater.context, null) - .also { pdfView = it } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - reloadDocumentAtPage(pageIndex) - } - - private fun reloadDocumentAtPage(pageIndex: Int) { - if (isReloading) { - hasToReload = pageIndex - return - } - - isReloading = true - - val context = context?.applicationContext ?: return - - viewLifecycleOwner.lifecycleScope.launch { - - try { - val document = PdfiumDocumentFactory(context) - // PDFium crashes when reusing the same PdfDocument, so we must not cache it. -// .cachedIn(publication) - .open(publication.get(link), null) - - pageCount = document.pageCount - val page = convertPageIndexToView(pageIndex) - - pdfView.recycle() - pdfView - .fromSource { _, _, _ -> document.document } - .apply { - if (isPagesOrderReversed) { - // AndroidPdfViewer doesn't support RTL. A workaround is to provide - // the explicit page list in the right order. - pages(*((pageCount - 1) downTo 0).toList().toIntArray()) - } - } - .swipeHorizontal(settings.scrollAxis == Axis.HORIZONTAL) - .spacing(settings.pageSpacing.roundToInt()) - // Customization of [PDFView] is done before setting the listeners, - // to avoid overriding them in reading apps, which would break the - // navigator. - .apply { appListener?.onConfigurePdfView(this) } - .defaultPage(page) - .onRender { _, _, _ -> - if (settings.fit == Fit.WIDTH) { - pdfView.fitToWidth() - // Using `fitToWidth` often breaks the use of `defaultPage`, so we - // need to jump manually to the target page. - pdfView.jumpTo(page, false) - } - } - .onLoad { - val hasToReloadNow = hasToReload - if (hasToReloadNow != null) { - reloadDocumentAtPage(pageIndex) - } else { - isReloading = false - } - } - .onPageChange { index, _ -> - navigatorListener?.onPageChanged(convertPageIndexFromView(index)) - } - .onTap { event -> - navigatorListener?.onTap(PointF(event.x, event.y)) - ?: false - } - .load() - } catch (e: Exception) { - val error = Resource.Exception.wrap(e) - Timber.e(error) - navigatorListener?.onResourceLoadFailed(link, error) - } - } - } - - override val pageIndex: Int get() = viewPageIndex ?: initialPageIndex - - private val viewPageIndex: Int? get() = - if (pdfView.isRecycled) null - else convertPageIndexFromView(pdfView.currentPage) - - override fun goToPageIndex(index: Int, animated: Boolean): Boolean { - if (!isValidPageIndex(index)) { - return false - } - pdfView.jumpTo(convertPageIndexToView(index), animated) - return true - } - - private var pageCount = 0 - - private fun isValidPageIndex(pageIndex: Int): Boolean { - val validRange = 0 until pageCount - return validRange.contains(pageIndex) - } - - private fun convertPageIndexToView(page: Int): Int { - var index = (page - 1).coerceAtLeast(0) - if (isPagesOrderReversed) { - index = (pageCount - 1) - index - } - return index - } - - private fun convertPageIndexFromView(index: Int): Int { - var page = index + 1 - if (isPagesOrderReversed) { - page = (pageCount + 1) - page - } - return page - } - - /** - * Indicates whether the order of the [PDFView] pages is reversed to take into account - * right-to-left reading progressions. - */ - private val isPagesOrderReversed: Boolean get() = - settings.scrollAxis == Axis.HORIZONTAL && - settings.readingProgression == ReadingProgression.RTL -} diff --git a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumEngineProvider.kt b/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumEngineProvider.kt deleted file mode 100644 index 9f2d1c5e1d..0000000000 --- a/readium/adapters/pdfium/pdfium-navigator/src/main/java/org/readium/adapters/pdfium/navigator/PdfiumEngineProvider.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.adapters.pdfium.navigator - -import org.readium.r2.navigator.SimplePresentation -import org.readium.r2.navigator.VisualNavigator -import org.readium.r2.navigator.pdf.PdfDocumentFragmentInput -import org.readium.r2.navigator.pdf.PdfEngineProvider -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.publication.Metadata -import org.readium.r2.shared.publication.Publication - -/** - * Main component to use the PDF navigator with the PDFium adapter. - * - * Provide [PdfiumDefaults] to customize the default values that will be used by - * the navigator for some preferences. - */ -@ExperimentalReadiumApi -class PdfiumEngineProvider( - private val listener: PdfiumDocumentFragment.Listener? = null, - private val defaults: PdfiumDefaults = PdfiumDefaults() -) : PdfEngineProvider<PdfiumSettings, PdfiumPreferences, PdfiumPreferencesEditor> { - - override suspend fun createDocumentFragment(input: PdfDocumentFragmentInput<PdfiumSettings>) = - PdfiumDocumentFragment( - publication = input.publication, - link = input.link, - initialPageIndex = input.initialPageIndex, - settings = input.settings, - appListener = listener, - navigatorListener = input.listener - ) - - override fun computeSettings(metadata: Metadata, preferences: PdfiumPreferences): PdfiumSettings { - val settingsPolicy = PdfiumSettingsResolver(metadata, defaults) - return settingsPolicy.settings(preferences) - } - - override fun computePresentation(settings: PdfiumSettings): VisualNavigator.Presentation = - SimplePresentation( - readingProgression = settings.readingProgression, - scroll = true, - axis = settings.scrollAxis - ) - - override fun createPreferenceEditor( - publication: Publication, - initialPreferences: PdfiumPreferences - ): PdfiumPreferencesEditor = - PdfiumPreferencesEditor( - initialPreferences, - publication.metadata, - defaults - ) - - override fun createEmptyPreferences(): PdfiumPreferences = - PdfiumPreferences() -} diff --git a/readium/adapters/pspdfkit/build.gradle.kts b/readium/adapters/pspdfkit/build.gradle.kts index e09623883c..30ba3d3e8b 100644 --- a/readium/adapters/pspdfkit/build.gradle.kts +++ b/readium/adapters/pspdfkit/build.gradle.kts @@ -13,19 +13,19 @@ plugins { android { resourcePrefix = "readium_" - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs = freeCompilerArgs + listOf( "-opt-in=kotlin.RequiresOptIn", "-opt-in=org.readium.r2.shared.InternalReadiumApi" @@ -37,7 +37,11 @@ android { proguardFiles(getDefaultProguardFile("proguard-android.txt")) } } - namespace = "org.readium.adapters.pspdfkit" + namespace = "org.readium.adapter.pspdfkit" +} + +kotlin { + explicitApi() } rootProject.ext["publish.artifactId"] = "readium-adapter-pspdfkit" diff --git a/readium/adapters/pspdfkit/pspdfkit-document/build.gradle.kts b/readium/adapters/pspdfkit/document/build.gradle.kts similarity index 82% rename from readium/adapters/pspdfkit/pspdfkit-document/build.gradle.kts rename to readium/adapters/pspdfkit/document/build.gradle.kts index a8ab144c29..84322e6426 100644 --- a/readium/adapters/pspdfkit/pspdfkit-document/build.gradle.kts +++ b/readium/adapters/pspdfkit/document/build.gradle.kts @@ -13,19 +13,19 @@ plugins { android { resourcePrefix = "readium_" - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs = freeCompilerArgs + listOf( "-opt-in=kotlin.RequiresOptIn", "-opt-in=org.readium.r2.shared.InternalReadiumApi" @@ -40,7 +40,11 @@ android { buildFeatures { viewBinding = true } - namespace = "org.readium.adapters.pspdfkit.document" + namespace = "org.readium.adapter.pspdfkit.document" +} + +kotlin { + explicitApi() } rootProject.ext["publish.artifactId"] = "readium-adapter-pspdfkit-document" diff --git a/readium/adapters/pspdfkit/document/src/main/AndroidManifest.xml b/readium/adapters/pspdfkit/document/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2d10029868 --- /dev/null +++ b/readium/adapters/pspdfkit/document/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest /> + diff --git a/readium/adapters/pspdfkit/pspdfkit-document/src/main/java/org/readium/adapters/pspdfkit/document/PsPdfKitDocument.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt similarity index 69% rename from readium/adapters/pspdfkit/pspdfkit-document/src/main/java/org/readium/adapters/pspdfkit/document/PsPdfKitDocument.kt rename to readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt index 2e231dcf9e..102a79b539 100644 --- a/readium/adapters/pspdfkit/pspdfkit-document/src/main/java/org/readium/adapters/pspdfkit/document/PsPdfKitDocument.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt @@ -4,46 +4,56 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pspdfkit.document +package org.readium.adapter.pspdfkit.document import android.content.Context import android.graphics.Bitmap -import androidx.core.net.toUri import com.pspdfkit.annotations.actions.GoToAction import com.pspdfkit.document.DocumentSource import com.pspdfkit.document.OutlineElement import com.pspdfkit.document.PageBinding import com.pspdfkit.document.PdfDocument as _PsPdfKitDocument import com.pspdfkit.document.PdfDocumentLoader -import java.io.File +import com.pspdfkit.exceptions.InvalidPasswordException +import com.pspdfkit.exceptions.InvalidSignatureException +import java.io.IOException import kotlin.reflect.KClass import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.readium.r2.shared.fetcher.Resource import org.readium.r2.shared.publication.ReadingProgression +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory +import org.readium.r2.shared.util.resource.Resource import timber.log.Timber -class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory<PsPdfKitDocument> { +public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory<PsPdfKitDocument> { private val context = context.applicationContext override val documentType: KClass<PsPdfKitDocument> = PsPdfKitDocument::class - override suspend fun open(file: File, password: String?): PsPdfKitDocument = - open(context, DocumentSource(file.toUri(), password)) - - override suspend fun open(resource: Resource, password: String?): PsPdfKitDocument = - open(context, DocumentSource(ResourceDataProvider(resource), password)) - - private suspend fun open(context: Context, documentSource: DocumentSource): PsPdfKitDocument = + override suspend fun open(resource: Resource, password: String?): ReadTry<PsPdfKitDocument> = withContext(Dispatchers.IO) { - PsPdfKitDocument(PdfDocumentLoader.openDocument(context, documentSource)) + val dataProvider = ResourceDataProvider(resource) + val documentSource = DocumentSource(dataProvider, password) + try { + val innerDocument = PdfDocumentLoader.openDocument(context, documentSource) + Try.success(PsPdfKitDocument(innerDocument)) + } catch (e: InvalidPasswordException) { + Try.failure(ReadError.Decoding(ThrowableError(e))) + } catch (e: InvalidSignatureException) { + Try.failure(ReadError.Decoding(ThrowableError(e))) + } catch (e: IOException) { + Try.failure(dataProvider.error!!) + } } } -class PsPdfKitDocument( - val document: _PsPdfKitDocument +public class PsPdfKitDocument( + public val document: _PsPdfKitDocument ) : PdfDocument { // FIXME: Doesn't seem to be exposed by PSPDFKit. diff --git a/readium/adapters/pspdfkit/pspdfkit-document/src/main/java/org/readium/adapters/pspdfkit/document/ResourceDataProvider.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/ResourceDataProvider.kt similarity index 64% rename from readium/adapters/pspdfkit/pspdfkit-document/src/main/java/org/readium/adapters/pspdfkit/document/ResourceDataProvider.kt rename to readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/ResourceDataProvider.kt index fcef729023..65e881f429 100644 --- a/readium/adapters/pspdfkit/pspdfkit-document/src/main/java/org/readium/adapters/pspdfkit/document/ResourceDataProvider.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/ResourceDataProvider.kt @@ -4,32 +4,39 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pspdfkit.document +package org.readium.adapter.pspdfkit.document import com.pspdfkit.document.providers.DataProvider -import java.util.* +import java.util.UUID import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.synchronized +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.isLazyInitialized +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.synchronized +import org.readium.r2.shared.util.toDebugDescription import timber.log.Timber -class ResourceDataProvider( +internal class ResourceDataProvider( resource: Resource, - private val onResourceError: (Resource.Exception) -> Unit = { Timber.e(it) } + private val onResourceError: (ReadError) -> Unit = { Timber.e(it.toDebugDescription()) } ) : DataProvider { + var error: ReadError? = null + private val resource = // PSPDFKit accesses the resource from multiple threads. resource.synchronized() - private val length: Long = runBlocking { - resource.length() - .getOrElse { - onResourceError(it) - DataProvider.FILE_SIZE_UNKNOWN.toLong() - } + private val length by lazy { + runBlocking { + resource.length() + .getOrElse { + error = it + onResourceError(it) + DataProvider.FILE_SIZE_UNKNOWN.toLong() + } + } } override fun getSize(): Long = length @@ -47,6 +54,7 @@ class ResourceDataProvider( val range = offset until (offset + size) resource.read(range) .getOrElse { + error = it onResourceError(it) DataProvider.NO_DATA_AVAILABLE } @@ -54,6 +62,7 @@ class ResourceDataProvider( override fun release() { if (::resource.isLazyInitialized) { + error = null runBlocking { resource.close() } } } diff --git a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapters/pspdfkit/document/Deprecated.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapters/pspdfkit/document/Deprecated.kt new file mode 100644 index 0000000000..d66a1b220b --- /dev/null +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapters/pspdfkit/document/Deprecated.kt @@ -0,0 +1,15 @@ +package org.readium.adapters.pspdfkit.document + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pspdfkit.document.PsPdfKitDocument"), + level = DeprecationLevel.ERROR +) +public typealias PsPdfKitDocument = org.readium.adapter.pspdfkit.document.PsPdfKitDocument + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pspdfkit.document.PsPdfKitDocumentFactory"), + level = DeprecationLevel.ERROR +) +public typealias PsPdfKitDocumentFactory = org.readium.adapter.pspdfkit.document.PsPdfKitDocumentFactory diff --git a/readium/adapters/pspdfkit/pspdfkit-navigator/build.gradle.kts b/readium/adapters/pspdfkit/navigator/build.gradle.kts similarity index 84% rename from readium/adapters/pspdfkit/pspdfkit-navigator/build.gradle.kts rename to readium/adapters/pspdfkit/navigator/build.gradle.kts index 0b9a53a6e3..98e163027d 100644 --- a/readium/adapters/pspdfkit/pspdfkit-navigator/build.gradle.kts +++ b/readium/adapters/pspdfkit/navigator/build.gradle.kts @@ -14,19 +14,19 @@ plugins { android { resourcePrefix = "readium_" - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs = freeCompilerArgs + listOf( "-opt-in=kotlin.RequiresOptIn", "-opt-in=org.readium.r2.shared.InternalReadiumApi" @@ -41,7 +41,11 @@ android { buildFeatures { viewBinding = true } - namespace = "org.readium.adapters.pspdfkit.navigator" + namespace = "org.readium.adapter.pspdfkit.navigator" +} + +kotlin { + explicitApi() } rootProject.ext["publish.artifactId"] = "readium-adapter-pspdfkit-navigator" diff --git a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/AndroidManifest.xml b/readium/adapters/pspdfkit/navigator/src/main/AndroidManifest.xml similarity index 100% rename from readium/adapters/pspdfkit/pspdfkit-navigator/src/main/AndroidManifest.xml rename to readium/adapters/pspdfkit/navigator/src/main/AndroidManifest.xml diff --git a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitDefaults.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDefaults.kt similarity index 86% rename from readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitDefaults.kt rename to readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDefaults.kt index e29906b6b7..2f1cc0730c 100644 --- a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitDefaults.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDefaults.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pspdfkit.navigator +package org.readium.adapter.pspdfkit.navigator import org.readium.r2.navigator.preferences.ReadingProgression import org.readium.r2.navigator.preferences.Spread @@ -18,10 +18,10 @@ import org.readium.r2.shared.ExperimentalReadiumApi * @see PsPdfKitPreferences */ @ExperimentalReadiumApi -data class PsPdfKitDefaults( +public data class PsPdfKitDefaults( val offsetFirstPage: Boolean? = null, val pageSpacing: Double? = null, val readingProgression: ReadingProgression? = null, val scroll: Boolean? = null, - val spread: Spread? = null, + val spread: Spread? = null ) diff --git a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitDocumentFragment.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt similarity index 51% rename from readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitDocumentFragment.kt rename to readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt index f302faec1d..a2e0532f94 100644 --- a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitDocumentFragment.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pspdfkit.navigator +package org.readium.adapter.pspdfkit.navigator import android.graphics.PointF import android.os.Bundle @@ -14,6 +14,10 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.commitNow +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModel +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewModelScope import com.pspdfkit.annotations.Annotation import com.pspdfkit.annotations.LinkAnnotation import com.pspdfkit.annotations.SoundAnnotation @@ -31,36 +35,107 @@ import com.pspdfkit.listeners.OnPreparePopupToolbarListener import com.pspdfkit.ui.PdfFragment import com.pspdfkit.ui.toolbar.popup.PdfTextSelectionPopupToolbar import kotlin.math.roundToInt -import org.readium.adapters.pspdfkit.document.PsPdfKitDocument +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.readium.adapter.pspdfkit.document.PsPdfKitDocument +import org.readium.adapter.pspdfkit.document.PsPdfKitDocumentFactory import org.readium.r2.navigator.pdf.PdfDocumentFragment import org.readium.r2.navigator.preferences.Axis import org.readium.r2.navigator.preferences.Fit import org.readium.r2.navigator.preferences.ReadingProgression import org.readium.r2.navigator.preferences.Spread +import org.readium.r2.navigator.util.createViewModelFactory import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.isProtected +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ReadTry +import org.readium.r2.shared.util.pdf.cachedIn +import timber.log.Timber @ExperimentalReadiumApi -internal class PsPdfKitDocumentFragment( +public class PsPdfKitDocumentFragment internal constructor( private val publication: Publication, - private val document: PsPdfKitDocument, - private val initialPageIndex: Int, - settings: PsPdfKitSettings, + private val href: Url, + initialPageIndex: Int, + initialSettings: PsPdfKitSettings, private val listener: Listener? ) : PdfDocumentFragment<PsPdfKitSettings>() { - override var settings: PsPdfKitSettings = settings - set(value) { - if (field == value) return + internal interface Listener { + fun onResourceLoadFailed(href: Url, error: ReadError) + fun onConfigurePdfView(builder: PdfConfiguration.Builder): PdfConfiguration.Builder + fun onTap(point: PointF): Boolean + } + + private companion object { + private const val pdfFragmentTag = "com.pspdfkit.ui.PdfFragment" + } + private var pdfFragment: PdfFragment? = null + set(value) { field = value - reloadDocumentAtPage(pageIndex) + value?.apply { + setOnPreparePopupToolbarListener(psPdfKitListener) + addDocumentListener(psPdfKitListener) + } } - private lateinit var pdfFragment: PdfFragment private val psPdfKitListener = PsPdfKitListener() + private class DocumentViewModel( + document: suspend () -> ReadTry<PsPdfKitDocument> + ) : ViewModel() { + + private val _document: Deferred<ReadTry<PsPdfKitDocument>> = + viewModelScope.async { document() } + + suspend fun loadDocument(): ReadTry<PsPdfKitDocument> = + _document.await() + + @OptIn(ExperimentalCoroutinesApi::class) + val document: PsPdfKitDocument? get() = + _document.run { + if (isCompleted) { + getCompleted().getOrNull() + } else { + null + } + } + } + + private val viewModel: DocumentViewModel by viewModels { + createViewModelFactory { + DocumentViewModel( + document = { + val resource = requireNotNull(publication.get(href)) + PsPdfKitDocumentFactory(requireContext()) + .cachedIn(publication) + .open(resource, null) + } + ) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Restores the PdfFragment after a configuration change. + pdfFragment = (childFragmentManager.findFragmentByTag(pdfFragmentTag) as? PdfFragment) + ?.apply { + val document = checkNotNull(viewModel.document) { + "Should have a document when restoring the PdfFragment." + } + setCustomPdfSources(document.document.documentSources) + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -73,60 +148,47 @@ internal class PsPdfKitDocumentFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - reloadDocumentAtPage(initialPageIndex) - } - - private fun reloadDocumentAtPage(pageIndex: Int) { - pdfFragment = createPdfFragment() - childFragmentManager.commitNow { - replace(R.id.readium_pspdfkit_fragment, pdfFragment, "com.pspdfkit.ui.PdfFragment") + if (pdfFragment == null) { + viewLifecycleOwner.lifecycleScope.launch { + viewModel.loadDocument() + .onFailure { error -> + listener?.onResourceLoadFailed(href, error) + } + .onSuccess { resetPdfFragment() } + } } } - private fun createPdfFragment(): PdfFragment { - document.document.pageBinding = settings.readingProgression.pageBinding - val config = configForSettings(settings) + /** + * Recreates the [PdfFragment] with the current settings. + */ + private fun resetPdfFragment() { + if (isStateSaved || view == null) return + val doc = viewModel.document ?: return - val newFragment = if (::pdfFragment.isInitialized) { - PdfFragment.newInstance(pdfFragment, config) - } else { - PdfFragment.newInstance(document.document, config) - } + doc.document.pageBinding = settings.readingProgression.pageBinding - newFragment.apply { - setOnPreparePopupToolbarListener(psPdfKitListener) - addDocumentListener(psPdfKitListener) - } + val fragment = PdfFragment.newInstance(doc.document, configForSettings(settings)) + .also { pdfFragment = it } - return newFragment + childFragmentManager.commitNow { + replace(R.id.readium_pspdfkit_fragment, fragment, pdfFragmentTag) + } } private fun configForSettings(settings: PsPdfKitSettings): PdfConfiguration { - val config = PdfConfiguration.Builder() + var config = PdfConfiguration.Builder() .animateScrollOnEdgeTaps(false) .annotationReplyFeatures(AnnotationReplyFeatures.READ_ONLY) .automaticallyGenerateLinks(true) .autosaveEnabled(false) -// .backgroundColor(Color.TRANSPARENT) .disableAnnotationEditing() .disableAnnotationRotation() .disableAutoSelectNextFormElement() .disableFormEditing() .enableMagnifier(true) .excludedAnnotationTypes(emptyList()) - .fitMode(settings.fit.fitMode) - .layoutMode(settings.spread.pageLayout) -// .loadingProgressDrawable(null) -// .maxZoomScale() - .firstPageAlwaysSingle(settings.offsetFirstPage) - .pagePadding(settings.pageSpacing.roundToInt()) - .restoreLastViewedPage(false) - .scrollDirection( - if (!settings.scroll) PageScrollDirection.HORIZONTAL - else settings.scrollAxis.scrollDirection - ) - .scrollMode(settings.scroll.scrollMode) .scrollOnEdgeTapEnabled(false) .scrollOnEdgeTapMargin(50) .scrollbarsEnabled(true) @@ -138,41 +200,70 @@ internal class PsPdfKitDocumentFragment( .videoPlaybackEnabled(true) .zoomOutBounce(true) + // Customization point for integrators. + listener?.let { + config = it.onConfigurePdfView(config) + } + + // Settings-specific configuration + config = config + .fitMode(settings.fit.fitMode) + .layoutMode(settings.spread.pageLayout) + .firstPageAlwaysSingle(settings.offsetFirstPage) + .pagePadding(settings.pageSpacing.roundToInt()) + .restoreLastViewedPage(false) + .scrollDirection( + if (!settings.scroll) { + PageScrollDirection.HORIZONTAL + } else { + settings.scrollAxis.scrollDirection + } + ) + .scrollMode(settings.scroll.scrollMode) + if (publication.isProtected) { - config.disableCopyPaste() + config = config.disableCopyPaste() } return config.build() } - override var pageIndex: Int = initialPageIndex - private set + private val _pageIndex = MutableStateFlow(initialPageIndex) + override val pageIndex: StateFlow<Int> = _pageIndex.asStateFlow() override fun goToPageIndex(index: Int, animated: Boolean): Boolean { + val fragment = pdfFragment ?: return false if (!isValidPageIndex(index)) { return false } - pageIndex = index - pdfFragment.setPageIndex(index, animated) + fragment.setPageIndex(index, animated) return true } private fun isValidPageIndex(pageIndex: Int): Boolean { - val validRange = 0 until pdfFragment.pageCount + val validRange = 0 until (pdfFragment?.pageCount ?: 0) return validRange.contains(pageIndex) } + private var settings: PsPdfKitSettings = initialSettings + + override fun applySettings(settings: PsPdfKitSettings) { + if (this.settings == settings) { + return + } + + this.settings = settings + resetPdfFragment() + } + private inner class PsPdfKitListener : DocumentListener, OnPreparePopupToolbarListener { override fun onPageChanged(document: PdfDocument, pageIndex: Int) { - this@PsPdfKitDocumentFragment.pageIndex = pageIndex - listener?.onPageChanged(pageIndex) + _pageIndex.value = pageIndex } override fun onDocumentClick(): Boolean { - val listener = listener ?: return false - val center = view?.run { PointF(width.toFloat() / 2, height.toFloat() / 2) } - return center?.let { listener.onTap(it) } ?: false + return center?.let { listener?.onTap(it) } ?: false } override fun onPageClick( @@ -185,17 +276,24 @@ internal class PsPdfKitDocumentFragment( if ( pagePosition == null || clickedAnnotation is LinkAnnotation || clickedAnnotation is SoundAnnotation - ) return false + ) { + return false + } - pdfFragment.viewProjection.toViewPoint(pagePosition, pageIndex) + checkNotNull(pdfFragment).viewProjection.toViewPoint(pagePosition, pageIndex) return listener?.onTap(pagePosition) ?: false } - private val allowedTextSelectionItems = listOf( - R.id.pspdf__text_selection_toolbar_item_share, - R.id.pspdf__text_selection_toolbar_item_copy, - R.id.pspdf__text_selection_toolbar_item_speak - ) + private val allowedTextSelectionItems: List<Int> by lazy { + buildList { + add(com.pspdfkit.R.id.pspdf__text_selection_toolbar_item_speak) + + if (!publication.isProtected) { + add(com.pspdfkit.R.id.pspdf__text_selection_toolbar_item_share) + add(com.pspdfkit.R.id.pspdf__text_selection_toolbar_item_copy) + } + } + } override fun onPrepareTextSelectionPopupToolbar(toolbar: PdfTextSelectionPopupToolbar) { // Makes sure only the menu items in `allowedTextSelectionItems` will be visible. @@ -204,9 +302,15 @@ internal class PsPdfKitDocumentFragment( } override fun onDocumentLoaded(document: PdfDocument) { - super.onDocumentLoaded(document) + val index = pageIndex.value + if (index < 0 || index >= document.pageCount) { + Timber.w( + "Tried to restore page index $index, but the document has ${document.pageCount} pages" + ) + return + } - pdfFragment.setPageIndex(pageIndex, false) + checkNotNull(pdfFragment).setPageIndex(index, false) } } diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt new file mode 100644 index 0000000000..580748f6f3 --- /dev/null +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.adapter.pspdfkit.navigator + +import android.graphics.PointF +import com.pspdfkit.configuration.PdfConfiguration +import org.readium.r2.navigator.OverflowableNavigator +import org.readium.r2.navigator.SimpleOverflow +import org.readium.r2.navigator.input.TapEvent +import org.readium.r2.navigator.pdf.PdfDocumentFragmentInput +import org.readium.r2.navigator.pdf.PdfEngineProvider +import org.readium.r2.navigator.preferences.Axis +import org.readium.r2.navigator.util.SingleFragmentFactory +import org.readium.r2.navigator.util.createFragmentFactory +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Metadata +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError + +/** + * Main component to use the PDF navigator with PSPDFKit. + * + * Provide [PsPdfKitDefaults] to customize the default values that will be used by + * the navigator for some preferences. + */ +@ExperimentalReadiumApi +public class PsPdfKitEngineProvider( + private val defaults: PsPdfKitDefaults = PsPdfKitDefaults(), + private val listener: Listener? = null +) : PdfEngineProvider<PsPdfKitSettings, PsPdfKitPreferences, PsPdfKitPreferencesEditor> { + + public interface Listener : PdfEngineProvider.Listener { + + /** Called when configuring a new PDF fragment. */ + public fun onConfigurePdfView(builder: PdfConfiguration.Builder): PdfConfiguration.Builder = builder + } + + override fun createDocumentFragmentFactory( + input: PdfDocumentFragmentInput<PsPdfKitSettings> + ): SingleFragmentFactory<PsPdfKitDocumentFragment> = + createFragmentFactory { + PsPdfKitDocumentFragment( + publication = input.publication, + href = input.href, + initialPageIndex = input.pageIndex, + initialSettings = input.settings, + listener = object : PsPdfKitDocumentFragment.Listener { + override fun onResourceLoadFailed(href: Url, error: ReadError) { + input.navigatorListener?.onResourceLoadFailed(href, error) + } + + override fun onConfigurePdfView(builder: PdfConfiguration.Builder): PdfConfiguration.Builder = + listener?.onConfigurePdfView(builder) ?: builder + + override fun onTap(point: PointF): Boolean = + input.inputListener?.onTap(TapEvent(point)) ?: false + } + ) + } + + override fun computeSettings(metadata: Metadata, preferences: PsPdfKitPreferences): PsPdfKitSettings { + val settingsPolicy = PsPdfKitSettingsResolver(metadata, defaults) + return settingsPolicy.settings(preferences) + } + + override fun computeOverflow(settings: PsPdfKitSettings): OverflowableNavigator.Overflow = + SimpleOverflow( + readingProgression = settings.readingProgression, + scroll = settings.scroll, + axis = if (settings.scroll) settings.scrollAxis else Axis.HORIZONTAL + ) + + override fun createPreferenceEditor( + publication: Publication, + initialPreferences: PsPdfKitPreferences + ): PsPdfKitPreferencesEditor = + PsPdfKitPreferencesEditor( + initialPreferences = initialPreferences, + publicationMetadata = publication.metadata, + defaults = defaults + ) + + override fun createEmptyPreferences(): PsPdfKitPreferences = + PsPdfKitPreferences() +} diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitNavigator.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitNavigator.kt new file mode 100644 index 0000000000..fe5b6760ba --- /dev/null +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitNavigator.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.adapter.pspdfkit.navigator + +import org.readium.r2.navigator.pdf.PdfNavigatorFactory +import org.readium.r2.navigator.pdf.PdfNavigatorFragment +import org.readium.r2.shared.ExperimentalReadiumApi + +@ExperimentalReadiumApi +public typealias PsPdfKitNavigatorFragment = PdfNavigatorFragment<PsPdfKitSettings, PsPdfKitPreferences> + +@ExperimentalReadiumApi +public typealias PsPdfKitNavigatorFactory = PdfNavigatorFactory<PsPdfKitSettings, PsPdfKitPreferences, PsPdfKitPreferencesEditor> diff --git a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitPreferences.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitPreferences.kt similarity index 88% rename from readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitPreferences.kt rename to readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitPreferences.kt index 329adfe66b..bf7b19440a 100644 --- a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitPreferences.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitPreferences.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pspdfkit.navigator +package org.readium.adapter.pspdfkit.navigator import kotlinx.serialization.Serializable import org.readium.r2.navigator.preferences.* @@ -23,14 +23,14 @@ import org.readium.r2.shared.ExperimentalReadiumApi */ @ExperimentalReadiumApi @Serializable -data class PsPdfKitPreferences( +public data class PsPdfKitPreferences( val fit: Fit? = null, val offsetFirstPage: Boolean? = null, val pageSpacing: Double? = null, val readingProgression: ReadingProgression? = null, val scroll: Boolean? = null, val scrollAxis: Axis? = null, - val spread: Spread? = null, + val spread: Spread? = null ) : Configurable.Preferences<PsPdfKitPreferences> { init { @@ -38,7 +38,7 @@ data class PsPdfKitPreferences( require(pageSpacing == null || pageSpacing >= 0) } - override operator fun plus(other: PsPdfKitPreferences) = + override operator fun plus(other: PsPdfKitPreferences): PsPdfKitPreferences = PsPdfKitPreferences( fit = other.fit ?: fit, offsetFirstPage = other.offsetFirstPage ?: offsetFirstPage, @@ -46,6 +46,6 @@ data class PsPdfKitPreferences( readingProgression = other.readingProgression ?: readingProgression, scroll = other.scroll ?: scroll, scrollAxis = other.scrollAxis ?: scrollAxis, - spread = other.spread ?: spread, + spread = other.spread ?: spread ) } diff --git a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitPreferencesEditor.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitPreferencesEditor.kt similarity index 88% rename from readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitPreferencesEditor.kt rename to readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitPreferencesEditor.kt index 5b01eca390..3b1e5842a5 100644 --- a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitPreferencesEditor.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitPreferencesEditor.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pspdfkit.navigator +package org.readium.adapter.pspdfkit.navigator import org.readium.r2.navigator.extensions.format import org.readium.r2.navigator.preferences.Axis @@ -30,10 +30,10 @@ import org.readium.r2.shared.publication.Metadata * or ranges. */ @ExperimentalReadiumApi -class PsPdfKitPreferencesEditor internal constructor( +public class PsPdfKitPreferencesEditor internal constructor( initialPreferences: PsPdfKitPreferences, publicationMetadata: Metadata, - defaults: PsPdfKitDefaults, + defaults: PsPdfKitDefaults ) : PreferencesEditor<PsPdfKitPreferences> { private data class State( @@ -60,13 +60,13 @@ class PsPdfKitPreferencesEditor internal constructor( /** * Indicates how pages should be laid out within the viewport. */ - val fit: EnumPreference<Fit> = + public val fit: EnumPreference<Fit> = EnumPreferenceDelegate( getValue = { preferences.fit }, getEffectiveValue = { state.settings.fit }, getIsEffective = { true }, updateValue = { value -> updateValues { it.copy(fit = value) } }, - supportedValues = listOf(Fit.CONTAIN, Fit.WIDTH), + supportedValues = listOf(Fit.CONTAIN, Fit.WIDTH) ) /** @@ -76,18 +76,18 @@ class PsPdfKitPreferencesEditor internal constructor( * - [scroll] is off * - [spread] are not disabled */ - val offsetFirstPage: Preference<Boolean> = + public val offsetFirstPage: Preference<Boolean> = PreferenceDelegate( getValue = { preferences.offsetFirstPage }, getEffectiveValue = { state.settings.offsetFirstPage }, getIsEffective = { !state.settings.scroll && state.settings.spread != Spread.NEVER }, - updateValue = { value -> updateValues { it.copy(offsetFirstPage = value) } }, + updateValue = { value -> updateValues { it.copy(offsetFirstPage = value) } } ) /** * Space between pages in dp. */ - val pageSpacing: RangePreference<Double> = + public val pageSpacing: RangePreference<Double> = RangePreferenceDelegate( getValue = { preferences.pageSpacing }, getEffectiveValue = { state.settings.pageSpacing }, @@ -95,30 +95,30 @@ class PsPdfKitPreferencesEditor internal constructor( updateValue = { value -> updateValues { it.copy(pageSpacing = value) } }, supportedRange = 0.0..50.0, progressionStrategy = DoubleIncrement(5.0), - valueFormatter = { "${it.format(1)} dp" }, + valueFormatter = { "${it.format(1)} dp" } ) /** * Direction of the horizontal progression across pages. */ - val readingProgression: EnumPreference<ReadingProgression> = + public val readingProgression: EnumPreference<ReadingProgression> = EnumPreferenceDelegate( getValue = { preferences.readingProgression }, getEffectiveValue = { state.settings.readingProgression }, getIsEffective = { true }, updateValue = { value -> updateValues { it.copy(readingProgression = value) } }, - supportedValues = listOf(ReadingProgression.LTR, ReadingProgression.RTL), + supportedValues = listOf(ReadingProgression.LTR, ReadingProgression.RTL) ) /** * Indicates if pages should be handled using scrolling instead of pagination. */ - val scroll: Preference<Boolean> = + public val scroll: Preference<Boolean> = PreferenceDelegate( getValue = { preferences.scroll }, getEffectiveValue = { state.settings.scroll }, getIsEffective = { true }, - updateValue = { value -> updateValues { it.copy(scroll = value) } }, + updateValue = { value -> updateValues { it.copy(scroll = value) } } ) /** @@ -126,13 +126,13 @@ class PsPdfKitPreferencesEditor internal constructor( * * Only effective when [scroll] is on. */ - val scrollAxis: EnumPreference<Axis> = + public val scrollAxis: EnumPreference<Axis> = EnumPreferenceDelegate( getValue = { preferences.scrollAxis }, getEffectiveValue = { state.settings.scrollAxis }, getIsEffective = { state.settings.scroll }, updateValue = { value -> updateValues { it.copy(scrollAxis = value) } }, - supportedValues = listOf(Axis.VERTICAL, Axis.HORIZONTAL), + supportedValues = listOf(Axis.VERTICAL, Axis.HORIZONTAL) ) /** @@ -140,13 +140,13 @@ class PsPdfKitPreferencesEditor internal constructor( * * Only effective when [scroll] is off. */ - val spread: EnumPreference<Spread> = + public val spread: EnumPreference<Spread> = EnumPreferenceDelegate( getValue = { preferences.spread }, getEffectiveValue = { state.settings.spread }, getIsEffective = { !state.settings.scroll }, updateValue = { value -> updateValues { it.copy(spread = value) } }, - supportedValues = listOf(Spread.AUTO, Spread.NEVER, Spread.ALWAYS), + supportedValues = listOf(Spread.AUTO, Spread.NEVER, Spread.ALWAYS) ) private fun updateValues(updater: (PsPdfKitPreferences) -> PsPdfKitPreferences) { diff --git a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitPreferencesFilters.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitPreferencesFilters.kt similarity index 76% rename from readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitPreferencesFilters.kt rename to readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitPreferencesFilters.kt index 28d01056e4..368b3e23b9 100644 --- a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitPreferencesFilters.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitPreferencesFilters.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pspdfkit.navigator +package org.readium.adapter.pspdfkit.navigator import org.readium.r2.navigator.preferences.PreferencesFilter import org.readium.r2.shared.ExperimentalReadiumApi @@ -13,13 +13,13 @@ import org.readium.r2.shared.ExperimentalReadiumApi * Suggested filter to keep only shared [PsPdfKitPreferences]. */ @ExperimentalReadiumApi -object PsPdfKitSharedPreferencesFilter : PreferencesFilter<PsPdfKitPreferences> { +public object PsPdfKitSharedPreferencesFilter : PreferencesFilter<PsPdfKitPreferences> { override fun filter(preferences: PsPdfKitPreferences): PsPdfKitPreferences = preferences.copy( readingProgression = null, offsetFirstPage = null, - spread = null, + spread = null ) } @@ -27,12 +27,12 @@ object PsPdfKitSharedPreferencesFilter : PreferencesFilter<PsPdfKitPreferences> * Suggested filter to keep only publication-specific [PsPdfKitPreferences]. */ @ExperimentalReadiumApi -object PsPdfKitPublicationPreferencesFilter : PreferencesFilter<PsPdfKitPreferences> { +public object PsPdfKitPublicationPreferencesFilter : PreferencesFilter<PsPdfKitPreferences> { override fun filter(preferences: PsPdfKitPreferences): PsPdfKitPreferences = PsPdfKitPreferences( readingProgression = preferences.readingProgression, offsetFirstPage = preferences.offsetFirstPage, - spread = preferences.spread, + spread = preferences.spread ) } diff --git a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitPreferencesSerializer.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitPreferencesSerializer.kt similarity index 84% rename from readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitPreferencesSerializer.kt rename to readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitPreferencesSerializer.kt index b461c1a9d9..7348fc655f 100644 --- a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitPreferencesSerializer.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitPreferencesSerializer.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pspdfkit.navigator +package org.readium.adapter.pspdfkit.navigator import kotlinx.serialization.json.Json import org.readium.r2.navigator.preferences.PreferencesSerializer @@ -14,7 +14,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi * JSON serializer of [PsPdfKitPreferences]. */ @ExperimentalReadiumApi -class PsPdfKitPreferencesSerializer : PreferencesSerializer<PsPdfKitPreferences> { +public class PsPdfKitPreferencesSerializer : PreferencesSerializer<PsPdfKitPreferences> { override fun serialize(preferences: PsPdfKitPreferences): String = Json.encodeToString(PsPdfKitPreferences.serializer(), preferences) diff --git a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitSettings.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitSettings.kt similarity index 85% rename from readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitSettings.kt rename to readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitSettings.kt index 7f90046e12..e43867feab 100644 --- a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitSettings.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitSettings.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pspdfkit.navigator +package org.readium.adapter.pspdfkit.navigator import org.readium.r2.navigator.preferences.* import org.readium.r2.shared.ExperimentalReadiumApi @@ -15,12 +15,12 @@ import org.readium.r2.shared.ExperimentalReadiumApi * @see PsPdfKitPreferences */ @ExperimentalReadiumApi -data class PsPdfKitSettings( +public data class PsPdfKitSettings( val fit: Fit, val offsetFirstPage: Boolean, val pageSpacing: Double, val readingProgression: ReadingProgression, val scroll: Boolean, val scrollAxis: Axis, - val spread: Spread, + val spread: Spread ) : Configurable.Settings diff --git a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitSettingsResolver.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitSettingsResolver.kt similarity index 95% rename from readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitSettingsResolver.kt rename to readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitSettingsResolver.kt index a56213138b..cfa7e01102 100644 --- a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitSettingsResolver.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitSettingsResolver.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.adapters.pspdfkit.navigator +package org.readium.adapter.pspdfkit.navigator import org.readium.r2.navigator.preferences.Axis import org.readium.r2.navigator.preferences.Fit @@ -17,7 +17,7 @@ import org.readium.r2.shared.publication.ReadingProgression as PublicationReadin @ExperimentalReadiumApi internal class PsPdfKitSettingsResolver( private val metadata: Metadata, - private val defaults: PsPdfKitDefaults, + private val defaults: PsPdfKitDefaults ) { fun settings(preferences: PsPdfKitPreferences): PsPdfKitSettings { val readingProgression: ReadingProgression = @@ -66,7 +66,7 @@ internal class PsPdfKitSettingsResolver( readingProgression = readingProgression, scroll = scroll, scrollAxis = scrollAxis, - spread = spread, + spread = spread ) } } diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/Deprecated.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/Deprecated.kt new file mode 100644 index 0000000000..bbdd9efb49 --- /dev/null +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/Deprecated.kt @@ -0,0 +1,82 @@ +@file:OptIn(ExperimentalReadiumApi::class) + +package org.readium.adapters.pspdfkit.navigator + +import org.readium.r2.shared.ExperimentalReadiumApi + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pspdfkit.navigator.PsPdfKitEngineProvider"), + level = DeprecationLevel.ERROR +) +public typealias PsPdfKitEngineProvider = org.readium.adapter.pspdfkit.navigator.PsPdfKitEngineProvider + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pspdfkit.navigator.PsPdfKitDefaults"), + level = DeprecationLevel.ERROR +) +public typealias PsPdfKitDefaults = org.readium.adapter.pspdfkit.navigator.PsPdfKitDefaults + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pspdfkit.navigator.PsPdfKitPreferences"), + level = DeprecationLevel.ERROR +) +public typealias PsPdfKitPreferences = org.readium.adapter.pspdfkit.navigator.PsPdfKitPreferences + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pspdfkit.navigator.PsPdfKitPreferencesEditor"), + level = DeprecationLevel.ERROR +) +public typealias PsPdfKitPreferencesEditor = org.readium.adapter.pspdfkit.navigator.PsPdfKitPreferencesEditor + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pspdfkit.navigator.PsPdfKitSettings"), + level = DeprecationLevel.ERROR +) +public typealias PsPdfKitSettings = org.readium.adapter.pspdfkit.navigator.PsPdfKitSettings + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pspdfkit.navigator.PsPdfKitDocumentFragment"), + level = DeprecationLevel.ERROR +) +public typealias PsPdfKitDocumentFragment = org.readium.adapter.pspdfkit.navigator.PsPdfKitDocumentFragment + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pspdfkit.navigator.PsPdfKitNavigatorFactory"), + level = DeprecationLevel.ERROR +) +public typealias PsPdfKitNavigatorFactory = org.readium.adapter.pspdfkit.navigator.PsPdfKitNavigatorFactory + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pspdfkit.navigator.PsPdfKitNavigatorFragment"), + level = DeprecationLevel.ERROR +) +public typealias PsPdfKitNavigatorFragment = org.readium.adapter.pspdfkit.navigator.PsPdfKitNavigatorFragment + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pspdfkit.navigator.PsPdfKitPreferencesSerializer"), + level = DeprecationLevel.ERROR +) +public typealias PsPdfKitPreferencesSerializer = org.readium.adapter.pspdfkit.navigator.PsPdfKitPreferencesSerializer + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pspdfkit.navigator.PsPdfKitPublicationPreferencesFilter"), + level = DeprecationLevel.ERROR +) +public typealias PsPdfKitPublicationPreferencesFilter = org.readium.adapter.pspdfkit.navigator.PsPdfKitPublicationPreferencesFilter + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.adapter.pspdfkit.navigator.PsPdfKitSharedPreferencesFilter"), + level = DeprecationLevel.ERROR +) +public typealias PsPdfKitSharedPreferencesFilter = org.readium.adapter.pspdfkit.navigator.PsPdfKitSharedPreferencesFilter diff --git a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/res/values/ids.xml b/readium/adapters/pspdfkit/navigator/src/main/res/values/ids.xml similarity index 100% rename from readium/adapters/pspdfkit/pspdfkit-navigator/src/main/res/values/ids.xml rename to readium/adapters/pspdfkit/navigator/src/main/res/values/ids.xml diff --git a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitEngineProvider.kt b/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitEngineProvider.kt deleted file mode 100644 index 5fbc8d8f5c..0000000000 --- a/readium/adapters/pspdfkit/pspdfkit-navigator/src/main/java/org/readium/adapters/pspdfkit/navigator/PsPdfKitEngineProvider.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.adapters.pspdfkit.navigator - -import android.content.Context -import org.readium.adapters.pspdfkit.document.PsPdfKitDocumentFactory -import org.readium.r2.navigator.SimplePresentation -import org.readium.r2.navigator.VisualNavigator -import org.readium.r2.navigator.pdf.PdfDocumentFragment -import org.readium.r2.navigator.pdf.PdfDocumentFragmentInput -import org.readium.r2.navigator.pdf.PdfEngineProvider -import org.readium.r2.navigator.preferences.Axis -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.publication.Metadata -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.pdf.cachedIn - -/** - * Main component to use the PDF navigator with PSPDFKit. - * - * Provide [PsPdfKitDefaults] to customize the default values that will be used by - * the navigator for some preferences. - */ -@ExperimentalReadiumApi -class PsPdfKitEngineProvider( - private val context: Context, - private val defaults: PsPdfKitDefaults = PsPdfKitDefaults() -) : PdfEngineProvider<PsPdfKitSettings, PsPdfKitPreferences, PsPdfKitPreferencesEditor> { - - override suspend fun createDocumentFragment( - input: PdfDocumentFragmentInput<PsPdfKitSettings> - ): PdfDocumentFragment<PsPdfKitSettings> { - - val publication = input.publication - val document = PsPdfKitDocumentFactory(context) - .cachedIn(publication) - .open(publication.get(input.link), null) - - return PsPdfKitDocumentFragment( - publication = publication, - document = document, - initialPageIndex = input.initialPageIndex, - settings = input.settings, - listener = input.listener - ) - } - - override fun computeSettings(metadata: Metadata, preferences: PsPdfKitPreferences): PsPdfKitSettings { - val settingsPolicy = PsPdfKitSettingsResolver(metadata, defaults) - return settingsPolicy.settings(preferences) - } - - override fun computePresentation(settings: PsPdfKitSettings): VisualNavigator.Presentation = - SimplePresentation( - readingProgression = settings.readingProgression, - scroll = settings.scroll, - axis = if (settings.scroll) settings.scrollAxis else Axis.HORIZONTAL - ) - - override fun createPreferenceEditor( - publication: Publication, - initialPreferences: PsPdfKitPreferences - ): PsPdfKitPreferencesEditor = - PsPdfKitPreferencesEditor( - initialPreferences = initialPreferences, - publicationMetadata = publication.metadata, - defaults = defaults - ) - - override fun createEmptyPreferences(): PsPdfKitPreferences = - PsPdfKitPreferences() -} diff --git a/readium/lcp/build.gradle.kts b/readium/lcp/build.gradle.kts index 2d4fd94f66..57da92b017 100644 --- a/readium/lcp/build.gradle.kts +++ b/readium/lcp/build.gradle.kts @@ -8,23 +8,24 @@ plugins { id("com.android.library") kotlin("android") kotlin("plugin.parcelize") - kotlin("kapt") + id("com.google.devtools.ksp") } android { + resourcePrefix = "readium_" - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_17.toString() allWarningsAsErrors = true freeCompilerArgs = freeCompilerArgs + listOf( "-opt-in=kotlin.RequiresOptIn", @@ -37,9 +38,16 @@ android { proguardFiles(getDefaultProguardFile("proguard-android.txt")) } } + buildFeatures { + buildConfig = true + } namespace = "org.readium.r2.lcp" } +kotlin { + explicitApi() +} + rootProject.ext["publish.artifactId"] = "readium-lcp" apply(from = "$rootDir/scripts/publish-module.gradle") @@ -59,15 +67,14 @@ dependencies { exclude(module = "support-v4") } implementation(libs.joda.time) - implementation("org.zeroturnaround:zt-zip:1.15") implementation(libs.androidx.browser) implementation(libs.bundles.room) - kapt(libs.androidx.room.compiler) - kapt("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.5.0") + ksp(libs.androidx.room.compiler) // Tests testImplementation(libs.junit) + testImplementation(libs.kotlin.junit) androidTestImplementation(libs.androidx.ext.junit) androidTestImplementation(libs.androidx.expresso.core) diff --git a/readium/lcp/src/main/AndroidManifest.xml b/readium/lcp/src/main/AndroidManifest.xml index d0c18d3e0e..05d00db7b9 100644 --- a/readium/lcp/src/main/AndroidManifest.xml +++ b/readium/lcp/src/main/AndroidManifest.xml @@ -1,11 +1,8 @@ <!-- - ~ Module: r2-lcp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpAuthenticating.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpAuthenticating.kt index 96a9ebc4a7..97e1507da3 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpAuthenticating.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpAuthenticating.kt @@ -13,7 +13,7 @@ import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.lcp.license.model.components.Link import org.readium.r2.lcp.license.model.components.lcp.User -interface LcpAuthenticating { +public interface LcpAuthenticating { /** * Retrieves the passphrase used to decrypt the given license. @@ -41,17 +41,14 @@ interface LcpAuthenticating { * @param license Information to show to the user about the license being opened. * @param reason Reason why the passphrase is requested. It should be used to prompt the user. * @param allowUserInteraction Indicates whether the user can be prompted for their passphrase. - * @param sender Free object that can be used by reading apps to give some UX context when - * presenting dialogs. */ - suspend fun retrievePassphrase( + public suspend fun retrievePassphrase( license: AuthenticatedLicense, reason: AuthenticationReason, - allowUserInteraction: Boolean, - sender: Any? = null + allowUserInteraction: Boolean ): String? - enum class AuthenticationReason { + public enum class AuthenticationReason { /** No matching passphrase was found. */ PassphraseNotFound, @@ -59,13 +56,13 @@ interface LcpAuthenticating { /** The provided passphrase was invalid. */ InvalidPassphrase; - companion object + public companion object } /** * @param document License Document being opened. */ - data class AuthenticatedLicense(val document: LicenseDocument) { + public data class AuthenticatedLicense(val document: LicenseDocument) { /** * A hint to be displayed to the User to help them remember the User Passphrase. @@ -78,13 +75,13 @@ interface LcpAuthenticating { * about the User Passphrase. */ val hintLink: Link? - get() = document.link(LicenseDocument.Rel.hint) + get() = document.link(LicenseDocument.Rel.Hint) /** * Support resources for the user (either a website, an email or a telephone number). */ val supportLinks: List<Link> - get() = document.links(LicenseDocument.Rel.support) + get() = document.links(LicenseDocument.Rel.Support) /** * URI of the license provider. @@ -95,27 +92,47 @@ interface LcpAuthenticating { /** * Informations about the user owning the license. */ - val user: User? + val user: User get() = document.user } } -@Deprecated("Renamed to `LcpAuthenticating`", replaceWith = ReplaceWith("LcpAuthenticating")) -typealias LCPAuthenticating = LcpAuthenticating +@Deprecated( + "Renamed to `LcpAuthenticating`", + replaceWith = ReplaceWith("LcpAuthenticating"), + level = DeprecationLevel.ERROR +) +public typealias LCPAuthenticating = LcpAuthenticating @Deprecated("Not used anymore", level = DeprecationLevel.ERROR) -interface LCPAuthenticationDelegate - -@Deprecated("Renamed to `LcpAuthenticating.AuthenticationReason`", replaceWith = ReplaceWith("LcpAuthenticating.AuthenticationReason")) -typealias LCPAuthenticationReason = LcpAuthenticating.AuthenticationReason - -@Deprecated("Renamed to `LcpAuthenticating.AuthenticatedLicense`", replaceWith = ReplaceWith("LcpAuthenticating.AuthenticatedLicense")) -typealias LCPAuthenticatedLicense = LcpAuthenticating.AuthenticatedLicense - -@Deprecated("Renamed to `PassphraseNotFound`", replaceWith = ReplaceWith("PassphraseNotFound")) -val LcpAuthenticating.AuthenticationReason.Companion.passphraseNotFound get() = - LcpAuthenticating.AuthenticationReason.PassphraseNotFound - -@Deprecated("Renamed to `InvalidPassphrase`", replaceWith = ReplaceWith("InvalidPassphrase")) -val LcpAuthenticating.AuthenticationReason.Companion.invalidPassphrase get() = - LcpAuthenticating.AuthenticationReason.InvalidPassphrase +public interface LCPAuthenticationDelegate + +@Deprecated( + "Renamed to `LcpAuthenticating.AuthenticationReason`", + replaceWith = ReplaceWith("LcpAuthenticating.AuthenticationReason"), + level = DeprecationLevel.ERROR +) +public typealias LCPAuthenticationReason = LcpAuthenticating.AuthenticationReason + +@Deprecated( + "Renamed to `LcpAuthenticating.AuthenticatedLicense`", + replaceWith = ReplaceWith("LcpAuthenticating.AuthenticatedLicense"), + level = DeprecationLevel.ERROR +) +public typealias LCPAuthenticatedLicense = LcpAuthenticating.AuthenticatedLicense + +@Deprecated( + "Renamed to `PassphraseNotFound`", + replaceWith = ReplaceWith("PassphraseNotFound"), + level = DeprecationLevel.ERROR +) +public val LcpAuthenticating.AuthenticationReason.Companion.passphraseNotFound: LcpAuthenticating.AuthenticationReason + get() = LcpAuthenticating.AuthenticationReason.PassphraseNotFound + +@Deprecated( + "Renamed to `InvalidPassphrase`", + replaceWith = ReplaceWith("InvalidPassphrase"), + level = DeprecationLevel.ERROR +) +public val LcpAuthenticating.AuthenticationReason.Companion.invalidPassphrase: LcpAuthenticating.AuthenticationReason + get() = LcpAuthenticating.AuthenticationReason.InvalidPassphrase diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index 5bf5ff7595..6d038707d8 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -7,47 +7,102 @@ package org.readium.r2.lcp import org.readium.r2.lcp.auth.LcpPassphraseAuthentication -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.fetcher.TransformingFetcher -import org.readium.r2.shared.publication.ContentProtection -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.asset.FileAsset -import org.readium.r2.shared.publication.asset.PublicationAsset +import org.readium.r2.lcp.license.model.LicenseDocument +import org.readium.r2.shared.publication.encryption.Encryption +import org.readium.r2.shared.publication.encryption.encryption +import org.readium.r2.shared.publication.epub.EpubEncryptionParser +import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.publication.services.contentProtectionServiceFactory +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.decodeRwpm +import org.readium.r2.shared.util.data.decodeXml +import org.readium.r2.shared.util.data.readDecodeOrElse +import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.format.EpubSpecification +import org.readium.r2.shared.util.format.LcpLicenseSpecification +import org.readium.r2.shared.util.format.LcpSpecification +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.TransformingContainer internal class LcpContentProtection( private val lcpService: LcpService, - private val authentication: LcpAuthenticating + private val authentication: LcpAuthenticating, + private val assetRetriever: AssetRetriever ) : ContentProtection { override suspend fun open( - asset: PublicationAsset, - fetcher: Fetcher, + asset: Asset, credentials: String?, - allowUserInteraction: Boolean, - sender: Any? - ): Try<ContentProtection.ProtectedAsset, Publication.OpeningException>? { - if (asset !is FileAsset) { - return null + allowUserInteraction: Boolean + ): Try<ContentProtection.OpenResult, ContentProtection.OpenError> = + when (asset) { + is ContainerAsset -> openPublication(asset, credentials, allowUserInteraction) + is ResourceAsset -> openLicense(asset, credentials, allowUserInteraction) } - if (!lcpService.isLcpProtected(asset.file)) { - return null + private suspend fun openPublication( + asset: ContainerAsset, + credentials: String?, + allowUserInteraction: Boolean + ): Try<ContentProtection.OpenResult, ContentProtection.OpenError> { + if ( + !asset.format.conformsTo(LcpSpecification) + ) { + return Try.failure(ContentProtection.OpenError.AssetNotSupported()) } - val authentication = credentials?.let { LcpPassphraseAuthentication(it, fallback = this.authentication) } + val license = retrieveLicense(asset, credentials, allowUserInteraction) + return createResultAsset(asset, license) + } + + private suspend fun retrieveLicense( + asset: Asset, + credentials: String?, + allowUserInteraction: Boolean + ): Try<LcpLicense, LcpError> { + val authentication = credentials + ?.let { LcpPassphraseAuthentication(it, fallback = this.authentication) } ?: this.authentication - val license = lcpService - .retrieveLicense(asset.file, authentication, allowUserInteraction, sender) + return lcpService.retrieveLicense(asset, authentication, allowUserInteraction) + } + private suspend fun createResultAsset( + asset: ContainerAsset, + license: Try<LcpLicense, LcpError> + ): Try<ContentProtection.OpenResult, ContentProtection.OpenError> { val serviceFactory = LcpContentProtectionService - .createFactory(license?.getOrNull(), license?.exceptionOrNull()) + .createFactory(license.getOrNull(), license.failureOrNull()) + + val encryptionData = + when { + asset.format.conformsTo(EpubSpecification) -> parseEncryptionDataEpub( + asset.container + ) + else -> parseEncryptionDataRpf(asset.container) + } + .getOrElse { return Try.failure(ContentProtection.OpenError.Reading(it)) } + + val decryptor = LcpDecryptor(license.getOrNull(), encryptionData) - val protectedFile = ContentProtection.ProtectedAsset( - asset = asset, - fetcher = TransformingFetcher(fetcher, LcpDecryptor(license?.getOrNull())::transform), + val container = TransformingContainer(asset.container, decryptor::transform) + + val protectedFile = ContentProtection.OpenResult( + asset = ContainerAsset( + format = asset.format, + container = container + ), onCreatePublication = { servicesBuilder.contentProtectionServiceFactory = serviceFactory } @@ -55,4 +110,121 @@ internal class LcpContentProtection( return Try.success(protectedFile) } + + private suspend fun parseEncryptionDataEpub(container: Container<Resource>): Try<Map<Url, Encryption>, ReadError> { + val encryptionResource = container[Url("META-INF/encryption.xml")!!] + ?: return Try.failure(ReadError.Decoding("Missing encryption.xml")) + + val encryptionDocument = encryptionResource + .readDecodeOrElse( + decode = { it.decodeXml() }, + recover = { return Try.failure(it) } + ) + + return Try.success(EpubEncryptionParser.parse(encryptionDocument)) + } + + private suspend fun parseEncryptionDataRpf(container: Container<Resource>): Try<Map<Url, Encryption>, ReadError> { + val manifestResource = container[Url("manifest.json")!!] + ?: return Try.failure(ReadError.Decoding("Missing manifest")) + + val manifest = manifestResource + .readDecodeOrElse( + decode = { it.decodeRwpm() }, + recover = { return Try.failure(it) } + ) + + val encryptionData = manifest + .let { (it.readingOrder + it.resources) } + .mapNotNull { link -> link.properties.encryption?.let { link.url() to it } } + .toMap() + + return Try.success(encryptionData) + } + + private suspend fun openLicense( + licenseAsset: ResourceAsset, + credentials: String?, + allowUserInteraction: Boolean + ): Try<ContentProtection.OpenResult, ContentProtection.OpenError> { + if (!licenseAsset.format.conformsTo(LcpLicenseSpecification)) { + return Try.failure(ContentProtection.OpenError.AssetNotSupported()) + } + + val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction) + + val licenseDoc = license.getOrNull()?.license + ?: licenseAsset.resource.read() + .map { + try { + LicenseDocument(it) + } catch (e: Exception) { + return Try.failure( + ContentProtection.OpenError.Reading( + ReadError.Decoding( + DebugError( + "Failed to read the LCP license document", + cause = ThrowableError(e) + ) + ) + ) + ) + } + } + .getOrElse { + return Try.failure( + ContentProtection.OpenError.Reading(it) + ) + } + + val link = licenseDoc.publicationLink + val url = (link.url() as? AbsoluteUrl) + ?: return Try.failure( + ContentProtection.OpenError.Reading( + ReadError.Decoding( + DebugError( + "The LCP license document does not contain a valid link to the publication" + ) + ) + ) + ) + + val asset = + if (link.mediaType != null) { + assetRetriever.retrieve( + url, + mediaType = link.mediaType + ) + .map { it as ContainerAsset } + .mapFailure { it.wrap() } + } else { + assetRetriever.retrieve(url) + .mapFailure { it.wrap() } + .flatMap { + if (it is ContainerAsset) { + Try.success((it)) + } else { + Try.failure( + ContentProtection.OpenError.AssetNotSupported( + DebugError( + "LCP license points to an unsupported publication." + ) + ) + ) + } + } + } + + return asset.flatMap { createResultAsset(it, license) } + } + + private fun AssetRetriever.RetrieveUrlError.wrap(): ContentProtection.OpenError = + when (this) { + is AssetRetriever.RetrieveUrlError.FormatNotSupported -> + ContentProtection.OpenError.AssetNotSupported(this) + is AssetRetriever.RetrieveUrlError.Reading -> + ContentProtection.OpenError.Reading(cause) + is AssetRetriever.RetrieveUrlError.SchemeNotSupported -> + ContentProtection.OpenError.AssetNotSupported(this) + } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtectionService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtectionService.kt index 71eedd8890..91891d0850 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtectionService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtectionService.kt @@ -9,11 +9,14 @@ package org.readium.r2.lcp -import org.readium.r2.shared.publication.ContentProtection import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.publication.services.ContentProtectionService -class LcpContentProtectionService(val license: LcpLicense?, override val error: LcpException?) : ContentProtectionService { +public class LcpContentProtectionService( + public val license: LcpLicense?, + override val error: LcpError? +) : ContentProtectionService { override val isRestricted: Boolean = license == null @@ -22,11 +25,17 @@ class LcpContentProtectionService(val license: LcpLicense?, override val error: override val rights: ContentProtectionService.UserRights = license ?: ContentProtectionService.UserRights.AllRestricted - override val scheme = ContentProtection.Scheme.Lcp + override val scheme: ContentProtection.Scheme = ContentProtection.Scheme.Lcp - companion object { + override fun close() { + license?.close() + } + + public companion object { - fun createFactory(license: LcpLicense?, error: LcpException?): (Publication.Service.Context) -> LcpContentProtectionService = + public fun createFactory(license: LcpLicense?, error: LcpError?): ( + Publication.Service.Context + ) -> LcpContentProtectionService = { LcpContentProtectionService(license, error) } } } @@ -34,5 +43,5 @@ class LcpContentProtectionService(val license: LcpLicense?, override val error: /** * Returns the [LcpLicense] if the [Publication] is protected by LCP and the license is opened. */ -val Publication.lcpLicense: LcpLicense? +public val Publication.lcpLicense: LcpLicense? get() = findService(LcpContentProtectionService::class)?.license diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 6aff0c21cd..5f15df16fc 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -9,33 +9,53 @@ package org.readium.r2.lcp -import java.io.IOException import org.readium.r2.shared.extensions.coerceFirstNonNegative import org.readium.r2.shared.extensions.inflate import org.readium.r2.shared.extensions.requireLengthFitInt -import org.readium.r2.shared.fetcher.* -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.encryption.encryption +import org.readium.r2.shared.publication.encryption.Encryption +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.resource.FailureResource +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.TransformingResource +import org.readium.r2.shared.util.resource.flatMap /** * Decrypts a resource protected with LCP. */ -internal class LcpDecryptor(val license: LcpLicense?) { - - fun transform(resource: Resource): Resource = LazyResource { - // Checks if the resource is encrypted and whether the encryption schemes of the resource - // and the DRM license are the same. - val link = resource.link() - val encryption = link.properties.encryption - if (encryption == null || encryption.scheme != "http://readium.org/2014/01/lcp") - return@LazyResource resource - - when { - license == null -> FailureResource(link, Resource.Exception.Forbidden()) - link.isDeflated || !link.isCbcEncrypted -> FullLcpResource(resource, license) - else -> CbcLcpResource(resource, license) +internal class LcpDecryptor( + val license: LcpLicense?, + val encryptionData: Map<Url, Encryption> +) { + + fun transform(url: Url, resource: Resource): Resource { + return resource.flatMap { + val encryption = encryptionData[url] + + // Checks if the resource is encrypted and whether the encryption schemes of the resource + // and the DRM license are the same. + if (encryption == null || encryption.scheme != "http://readium.org/2014/01/lcp") { + return@flatMap resource + } + + when { + license == null -> + FailureResource( + ReadError.Decoding( + DebugError( + "Cannot decipher content because the publication is locked." + ) + ) + ) + encryption.isDeflated || !encryption.isCbcEncrypted -> + FullLcpResource(resource, encryption, license) + else -> + CbcLcpResource(resource, encryption, license) + } } } @@ -47,15 +67,15 @@ internal class LcpDecryptor(val license: LcpLicense?) { */ private class FullLcpResource( resource: Resource, + private val encryption: Encryption, private val license: LcpLicense ) : TransformingResource(resource) { - override suspend fun transform(data: ResourceTry<ByteArray>): ResourceTry<ByteArray> = - license.decryptFully(data, resource.link().isDeflated) + override suspend fun transform(data: Try<ByteArray, ReadError>): Try<ByteArray, ReadError> = + license.decryptFully(data, encryption.isDeflated) - override suspend fun length(): ResourceTry<Long> = - resource.link().properties.encryption?.originalLength - ?.let { Try.success(it) } + override suspend fun length(): Try<Long, ReadError> = + encryption.originalLength?.let { Try.success(it) } ?: super.length() } @@ -66,85 +86,158 @@ internal class LcpDecryptor(val license: LcpLicense?) { */ private class CbcLcpResource( private val resource: Resource, + private val encryption: Encryption, private val license: LcpLicense - ) : Resource { - - lateinit var _length: ResourceTry<Long> - - override suspend fun link(): Link = resource.link() + ) : Resource by resource { + + private class Cache( + var startIndex: Int? = null, + val data: ByteArray = ByteArray(3 * AES_BLOCK_SIZE) + ) + + private lateinit var _length: Try<Long, ReadError> + + /* + * Decryption needs to look around the data strictly matching the content to decipher. + * That means that in case of contiguous read requests, data fetched from the underlying + * resource are not contiguous. Every request to the underlying resource starts slightly + * before the end of the previous one. This is an issue with remote publications because + * you have to make a new HTTP request every time instead of reusing the previous one. + * To alleviate this, we cache the three last bytes read in each call and reuse them + * in the next call if possible. + */ + private val _cache: Cache = Cache() /** Plain text size. */ - override suspend fun length(): ResourceTry<Long> { - if (::_length.isInitialized) + override suspend fun length(): Try<Long, ReadError> { + if (::_length.isInitialized) { return _length + } - _length = resource.length().flatMapCatching { length -> - if (length < 2 * AES_BLOCK_SIZE) { - throw Exception("Invalid CBC-encrypted stream") - } + _length = encryption.originalLength?.let { Try.success(it) } + ?: lengthFromPadding() - val readOffset = length - (2 * AES_BLOCK_SIZE) - resource.read(readOffset..length) - .mapCatching { bytes -> - val decryptedBytes = license.decrypt(bytes) - .getOrElse { throw Exception("Can't decrypt trailing size of CBC-encrypted stream", it) } - check(decryptedBytes.size == AES_BLOCK_SIZE) - - return@mapCatching length - - AES_BLOCK_SIZE - // Minus IV - decryptedBytes.last().toInt() // Minus padding size - } + return _length + } + + private suspend fun lengthFromPadding(): Try<Long, ReadError> { + val length = resource.length() + .getOrElse { return Try.failure(it) } + + if (length < 2 * AES_BLOCK_SIZE) { + return Try.failure( + ReadError.Decoding( + DebugError("Invalid CBC-encrypted stream.") + ) + ) } - return _length + val readOffset = length - (2 * AES_BLOCK_SIZE) + val bytes = resource.read(readOffset..length) + .getOrElse { return Try.failure(it) } + + val decryptedBytes = license.decrypt(bytes) + .getOrElse { + return Try.failure( + ReadError.Decoding( + DebugError("Can't decrypt trailing size of CBC-encrypted stream") + ) + ) + } + + check(decryptedBytes.size == AES_BLOCK_SIZE) + + val adjustedLength = length - + AES_BLOCK_SIZE - // Minus IV + decryptedBytes.last().toInt() // Minus padding size + + return Try.success(adjustedLength) } - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> { - if (range == null) + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> { + if (range == null) { return license.decryptFully(resource.read(), isDeflated = false) + } @Suppress("NAME_SHADOWING") val range = range .coerceFirstNonNegative() .requireLengthFitInt() - if (range.isEmpty()) + if (range.isEmpty()) { return Try.success(ByteArray(0)) + } - return resource.length().flatMapCatching { encryptedLength -> + val encryptedLength = resource.length() + .getOrElse { return Try.failure(it) } - // encrypted data is shifted by AES_BLOCK_SIZE because of IV and - // the previous block must be provided to perform XOR on intermediate blocks - val encryptedStart = range.first.floorMultipleOf(AES_BLOCK_SIZE.toLong()) - val encryptedEndExclusive = (range.last + 1).ceilMultipleOf(AES_BLOCK_SIZE.toLong()) + AES_BLOCK_SIZE + // encrypted data is shifted by AES_BLOCK_SIZE because of IV and + // the previous block must be provided to perform XOR on intermediate blocks + val encryptedStart = range.first.floorMultipleOf(AES_BLOCK_SIZE.toLong()) + val encryptedEndExclusive = (range.last + 1).ceilMultipleOf(AES_BLOCK_SIZE.toLong()) + AES_BLOCK_SIZE - resource.read(encryptedStart until encryptedEndExclusive).mapCatching { encryptedData -> - val bytes = license.decrypt(encryptedData) - .getOrElse { throw IOException("Can't decrypt the content at: ${link().href}", it) } + val encryptedData = getEncryptedData(encryptedStart until encryptedEndExclusive) + .getOrElse { return Try.failure(it) } - // exclude the bytes added to match a multiple of AES_BLOCK_SIZE - val sliceStart = (range.first - encryptedStart).toInt() + if (encryptedData.size >= _cache.data.size) { + // cache the three last encrypted blocks that have been read for future use + val cacheStart = encryptedData.size - _cache.data.size + _cache.startIndex = (encryptedEndExclusive - _cache.data.size).toInt() + encryptedData.copyInto(_cache.data, 0, cacheStart) + } - // was the last block read to provide the desired range - val lastBlockRead = encryptedLength - encryptedEndExclusive <= AES_BLOCK_SIZE + val bytes = license.decrypt(encryptedData) + .getOrElse { + return Try.failure( + ReadError.Decoding( + DebugError( + "Can't decrypt the content for resource with key: ${resource.sourceUrl}", + it + ) + ) + ) + } - val rangeLength = - if (lastBlockRead) - // use decrypted length to ensure range.last doesn't exceed decrypted length - 1 - range.last.coerceAtMost(length().getOrThrow() - 1) - range.first + 1 - else - // the last block won't be read, so there's no need to compute length - range.last - range.first + 1 + // exclude the bytes added to match a multiple of AES_BLOCK_SIZE + val sliceStart = (range.first - encryptedStart).toInt() + + // was the last block read to provide the desired range + val lastBlockRead = encryptedLength - encryptedEndExclusive <= AES_BLOCK_SIZE + + val rangeLength = + if (lastBlockRead) { + // use decrypted length to ensure range.last doesn't exceed decrypted length - 1 + val decryptedLength = length() + .getOrElse { return Try.failure(it) } + range.last.coerceAtMost(decryptedLength - 1) - range.first + 1 + } else { + // the last block won't be read, so there's no need to compute length + range.last - range.first + 1 + } - // keep only enough bytes to fit the length corrected request in order to never include padding - val sliceEnd = sliceStart + rangeLength.toInt() + // keep only enough bytes to fit the length corrected request in order to never include padding + val sliceEnd = sliceStart + rangeLength.toInt() - bytes.sliceArray(sliceStart until sliceEnd) - } - } + return Try.success(bytes.sliceArray(sliceStart until sliceEnd)) } - override suspend fun close() = resource.close() + private suspend fun getEncryptedData(range: LongRange): Try<ByteArray, ReadError> { + val cacheStartIndex = _cache.startIndex + ?.takeIf { cacheStart -> + val cacheEnd = cacheStart + _cache.data.size + range.first in cacheStart until cacheEnd && cacheEnd <= range.last + 1 + } ?: return resource.read(range) + + val bytes = ByteArray(range.last.toInt() - range.first.toInt() + 1) + val offsetInCache = (range.first - cacheStartIndex).toInt() + val fromCacheLength = _cache.data.size - offsetInCache + + return resource.read(range.first + fromCacheLength..range.last).map { + _cache.data.copyInto(bytes, 0, offsetInCache) + it.copyInto(bytes, fromCacheLength) + bytes + } + } companion object { private const val AES_BLOCK_SIZE = 16 // bytes @@ -152,14 +245,24 @@ internal class LcpDecryptor(val license: LcpLicense?) { } } -private suspend fun LcpLicense.decryptFully(data: ResourceTry<ByteArray>, isDeflated: Boolean): ResourceTry<ByteArray> = - data.mapCatching { encryptedData -> +private suspend fun LcpLicense.decryptFully( + data: Try<ByteArray, ReadError>, + isDeflated: Boolean +): Try<ByteArray, ReadError> = + data.flatMap { encryptedData -> // Decrypts the resource. var bytes = decrypt(encryptedData) - .getOrElse { throw Exception("Failed to decrypt the resource", it) } + .getOrElse { + return Try.failure( + ReadError.Decoding( + DebugError("Failed to decrypt the resource", it) + ) + ) + } - if (bytes.isEmpty()) + if (bytes.isEmpty()) { throw IllegalStateException("Lcp.nativeDecrypt returned an empty ByteArray") + } // Removes the padding. val padding = bytes.last().toInt() @@ -170,14 +273,14 @@ private suspend fun LcpLicense.decryptFully(data: ResourceTry<ByteArray>, isDefl bytes = bytes.inflate(nowrap = true) } - bytes + Try.success(bytes) } -private val Link.isDeflated: Boolean get() = - properties.encryption?.compression?.lowercase(java.util.Locale.ROOT) == "deflate" +private val Encryption.isDeflated: Boolean get() = + compression?.lowercase(java.util.Locale.ROOT) == "deflate" -private val Link.isCbcEncrypted: Boolean get() = - properties.encryption?.algorithm == "http://www.w3.org/2001/04/xmlenc#aes256-cbc" +private val Encryption.isCbcEncrypted: Boolean get() = + algorithm == "http://www.w3.org/2001/04/xmlenc#aes256-cbc" private fun Long.ceilMultipleOf(divisor: Long) = divisor * (this / divisor + if (this % divisor == 0L) 0 else 1) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt index 299d4681ae..bc38a882df 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt @@ -7,19 +7,23 @@ * LICENSE file present in the project repository where this source code is maintained. */ +@file:Suppress("unused") + package org.readium.r2.lcp import kotlin.math.ceil import org.readium.r2.shared.extensions.coerceIn -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.mapCatching import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use import timber.log.Timber -suspend fun Publication.checkDecryption() { - +internal suspend fun Publication.checkDecryption() { checkResourcesAreReadableInOneBlock(this) checkLengthComputationIsCorrect(this) @@ -29,29 +33,32 @@ suspend fun Publication.checkDecryption() { checkExceedingRangesAreAllowed(this) } -private suspend fun checkResourcesAreReadableInOneBlock(publication: Publication) { +internal suspend fun checkResourcesAreReadableInOneBlock(publication: Publication) { Timber.d("checking resources are readable in one block") (publication.readingOrder + publication.resources) .forEach { link -> Timber.d("attempting to read ${link.href} in one block") - publication.get(link).use { resource -> + publication.get(link)!!.use { resource -> val bytes = resource.read() check(bytes.isSuccess) { "failed to read ${link.href} in one block" } } } } -private suspend fun checkLengthComputationIsCorrect(publication: Publication) { +internal suspend fun checkLengthComputationIsCorrect(publication: Publication) { Timber.d("checking length computation is correct") (publication.readingOrder + publication.resources) .forEach { link -> - val trueLength = publication.get(link).use { it.read().getOrThrow().size.toLong() } - publication.get(link).use { resource -> + val trueLength = publication.get(link)!!.use { it.read().checkSuccess().size.toLong() } + publication.get(link)!!.use { resource -> resource.length() .onFailure { - throw IllegalStateException("failed to compute length of ${link.href}", it) + throw IllegalStateException( + "failed to compute length of ${link.href}", + ErrorException(it) + ) }.onSuccess { check(it == trueLength) { "computed length of ${link.href} seems to be wrong" } } @@ -59,46 +66,53 @@ private suspend fun checkLengthComputationIsCorrect(publication: Publication) { } } -private suspend fun checkAllResourcesAreReadableByChunks(publication: Publication) { +internal suspend fun checkAllResourcesAreReadableByChunks(publication: Publication) { Timber.d("checking all resources are readable by chunks") (publication.readingOrder + publication.resources) .forEach { link -> Timber.d("attempting to read ${link.href} by chunks ") - val groundTruth = publication.get(link).use { it.read() }.getOrThrow() + val groundTruth = publication.get(link)!!.use { it.read() }.checkSuccess() for (chunkSize in listOf(4096L, 2050L)) { publication.get(link).use { resource -> - resource.readByChunks(chunkSize, groundTruth).onFailure { - throw IllegalStateException("failed to read ${link.href} by chunks of size $chunkSize", it) + resource!!.readByChunks(chunkSize, groundTruth).onFailure { + throw IllegalStateException( + "failed to read ${link.href} by chunks of size $chunkSize", + it + ) } } } } } -private suspend fun checkExceedingRangesAreAllowed(publication: Publication) { +internal suspend fun checkExceedingRangesAreAllowed(publication: Publication) { Timber.d("checking exceeding ranges are allowed") (publication.readingOrder + publication.resources) .forEach { link -> publication.get(link).use { resource -> - val length = resource.length().getOrThrow() - val fullTruth = resource.read().getOrThrow() + val length = resource!!.length().checkSuccess() + val fullTruth = resource.read().checkSuccess() for ( - range in listOf( - 0 until length + 100, - 0 until length + 2048, - length - 500 until length + 200, - length until length + 5028, - length + 200 until length + 500 - ) + range in listOf( + 0 until length + 100, + 0 until length + 2048, + length - 500 until length + 200, + length until length + 5028, + length + 200 until length + 500 + ) ) { resource.read(range) .onFailure { - throw IllegalStateException("unable to decrypt range $range from ${link.href}") + throw IllegalStateException( + "unable to decrypt range $range from ${link.href}" + ) }.onSuccess { val coercedRange = range.coerceIn(0L until fullTruth.size) - val truth = fullTruth.sliceArray(coercedRange.first.toInt()..coercedRange.last.toInt()) + val truth = fullTruth.sliceArray( + coercedRange.first.toInt()..coercedRange.last.toInt() + ) check(it.contentEquals(truth)) { Timber.d("decrypted length: ${it.size}") Timber.d("expected length: ${truth.size}") @@ -110,12 +124,16 @@ private suspend fun checkExceedingRangesAreAllowed(publication: Publication) { } } -private suspend fun Resource.readByChunks( +internal suspend fun Resource.readByChunks( chunkSize: Long, groundTruth: ByteArray, shuffle: Boolean = true ) = - length().mapCatching { length -> + try { + val length = length() + .mapFailure { ErrorException(it) } + .getOrThrow() + val blockNb = ceil(length / chunkSize.toDouble()).toInt() val blocks = (0 until blockNb) .map { Pair(it, it * chunkSize until kotlin.math.min(length, (it + 1) * chunkSize)) } @@ -131,14 +149,22 @@ private suspend fun Resource.readByChunks( blocks.forEach { Timber.d("block index ${it.first}: ${it.second}") val decryptedBytes = read(it.second).getOrElse { error -> - throw IllegalStateException("unable to decrypt chunk ${it.second} from ${link().href}", error) + throw IllegalStateException( + "unable to decrypt chunk ${it.second} from $sourceUrl", + ErrorException(error) + ) } check(decryptedBytes.isNotEmpty()) { "empty decrypted bytearray" } check(decryptedBytes.contentEquals(groundTruth.sliceArray(it.second.map(Long::toInt)))) { Timber.d("decrypted length: ${decryptedBytes.size}") - Timber.d("expected length: ${groundTruth.sliceArray(it.second.map(Long::toInt)).size}") - "decrypted chunk ${it.first}: ${it.second} seems to be wrong in ${link().href}" + Timber.d( + "expected length: ${groundTruth.sliceArray(it.second.map(Long::toInt)).size}" + ) + "decrypted chunk ${it.first}: ${it.second} seems to be wrong in $sourceUrl" } Pair(it.first, decryptedBytes) } + Try.success(Unit) + } catch (e: Exception) { + Try.failure(e) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDownloadsRepository.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDownloadsRepository.kt new file mode 100644 index 0000000000..f9c05ae2e3 --- /dev/null +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDownloadsRepository.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.lcp + +import android.content.Context +import java.io.File +import java.util.LinkedList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.json.JSONObject +import org.readium.r2.shared.util.CoroutineQueue + +internal class LcpDownloadsRepository( + context: Context +) { + private val coroutineScope: CoroutineScope = + MainScope() + + private val queue: CoroutineQueue = + CoroutineQueue() + + private val storageDir: Deferred<File> = + coroutineScope.async { + withContext(Dispatchers.IO) { + File(context.noBackupFilesDir, LcpDownloadsRepository::class.qualifiedName!!) + .also { if (!it.exists()) it.mkdirs() } + } + } + + private val storageFile: Deferred<File> = + coroutineScope.async { + withContext(Dispatchers.IO) { + File(storageDir.await(), "licenses.json") + .also { if (!it.exists()) { it.writeText("{}", Charsets.UTF_8) } } + } + } + + private val snapshot: Deferred<MutableMap<String, JSONObject>> = + coroutineScope.async { + readSnapshot().toMutableMap() + } + + fun addDownload(id: String, license: JSONObject) { + coroutineScope.launch { + val snapshotCompleted = snapshot.await() + snapshotCompleted[id] = license + writeSnapshot(snapshotCompleted) + } + } + + fun removeDownload(id: String) { + queue.launch { + val snapshotCompleted = snapshot.await() + snapshotCompleted.remove(id) + writeSnapshot(snapshotCompleted) + } + } + + suspend fun retrieveLicense(id: String): JSONObject? = + queue.await { + snapshot.await()[id] + } + + private suspend fun readSnapshot(): Map<String, JSONObject> { + return withContext(Dispatchers.IO) { + storageFile.await().readText(Charsets.UTF_8).toData().toMutableMap() + } + } + + private suspend fun writeSnapshot(snapshot: Map<String, JSONObject>) { + val storageFileCompleted = storageFile.await() + withContext(Dispatchers.IO) { + storageFileCompleted.writeText(snapshot.toJson(), Charsets.UTF_8) + } + } + + private fun Map<String, JSONObject>.toJson(): String { + val jsonObject = JSONObject() + for ((id, license) in this.entries) { + jsonObject.put(id, license) + } + return jsonObject.toString() + } + + private fun String.toData(): Map<String, JSONObject> { + val jsonObject = JSONObject(this) + val names = jsonObject.keys().iterator().toList() + return names.associateWith { jsonObject.getJSONObject(it) } + } + + private fun <T> Iterator<T>.toList(): List<T> = + LinkedList<T>().apply { + while (hasNext()) + this += next() + }.toMutableList() +} diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt new file mode 100644 index 0000000000..85d2ee611d --- /dev/null +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt @@ -0,0 +1,262 @@ +/* + * Copyright 2020 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.lcp + +import java.net.SocketTimeoutException +import java.util.* +import org.readium.r2.lcp.service.NetworkException +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.Url + +public sealed class LcpError( + override val message: String, + override val cause: Error? = null +) : Error { + + /** The interaction is not available with this License. */ + public object LicenseInteractionNotAvailable : + LcpError("This interaction is not available.") + + /** This License's profile is not supported by liblcp. */ + public object LicenseProfileNotSupported : + LcpError( + "This License has a profile identifier that this app cannot handle, the publication cannot be processed" + ) + + /** Failed to retrieve the Certificate Revocation List. */ + public object CrlFetching : + LcpError("Can't retrieve the Certificate Revocation List") + + /** A network request failed with the given exception. */ + public class Network(override val cause: Error?) : + LcpError("NetworkError", cause = cause) { + + internal constructor(throwable: Throwable) : this(ThrowableError(throwable)) + } + + /** + * An unexpected LCP exception occurred. Please post an issue on r2-lcp-kotlin with the error + * message and how to reproduce it. + */ + public class Runtime(message: String) : + LcpError("Unexpected LCP error", DebugError(message)) + + /** An unknown low-level exception was reported. */ + public class Unknown(override val cause: Error?) : + LcpError("Unknown LCP error") { + + internal constructor(throwable: Throwable) : this(ThrowableError(throwable)) + } + + /** + * Errors while checking the status of the License, using the Status Document. + * + * The app should notify the user and stop there. The message to the user must be clear about + * the status of the license: don't display "expired" if the status is "revoked". The date and + * time corresponding to the new status should be displayed (e.g. "The license expired on 01 + * January 2018"). + */ + public sealed class LicenseStatus( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + public class Cancelled(public val date: Date) : + LicenseStatus("This license was cancelled on $date") + + public class Returned(public val date: Date) : + LicenseStatus("This license has been returned on $date") + + public class NotStarted(public val start: Date) : + LicenseStatus("This license starts on $start") + + public class Expired(public val end: Date) : + LicenseStatus("This license expired on $end") + + /** + * If the license has been revoked, the user message should display the number of devices which + * registered to the server. This count can be calculated from the number of "register" events + * in the status document. If no event is logged in the status document, no such message should + * appear (certainly not "The license was registered by 0 devices"). + */ + public class Revoked(public val date: Date, public val devicesCount: Int) : + LicenseStatus( + "This license was revoked by its provider on $date. It was registered by $devicesCount device(s)." + ) + } + + /** + * Errors while renewing a loan. + */ + public sealed class Renew( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + /** Your publication could not be renewed properly. */ + public object RenewFailed : + Renew("Publication could not be renewed properly") + + /** Incorrect renewal period, your publication could not be renewed. */ + public class InvalidRenewalPeriod(public val maxRenewDate: Date?) : + Renew("Incorrect renewal period, your publication could not be renewed") + + /** An unexpected error has occurred on the licensing server. */ + public object UnexpectedServerError : + Renew("An unexpected error has occurred on the server") + } + + /** + * Errors while returning a loan. + */ + public sealed class Return( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + /** Your publication could not be returned properly. */ + public object ReturnFailed : + Return("Publication could not be returned properly") + + /** Your publication has already been returned before or is expired. */ + + public object AlreadyReturnedOrExpired : + Return("Publication has already been returned before or is expired") + + /** An unexpected error has occurred on the licensing server. */ + public object UnexpectedServerError : + Return("An unexpected error has occurred on the server") + } + + /** + * Errors while parsing the License or Status JSON Documents. + */ + public sealed class Parsing( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + /** The JSON is malformed and can't be parsed. */ + public object MalformedJSON : + Parsing("The JSON is malformed and can't be parsed") + + /** The JSON is not representing a valid License Document. */ + public object LicenseDocument : + Parsing("The JSON is not representing a valid License Document") + + /** The JSON is not representing a valid Status Document. */ + public object StatusDocument : + Parsing("The JSON is not representing a valid Status Document") + + /** Invalid Link. */ + public object Link : + Parsing("The JSON is not representing a valid document") + + /** Invalid Encryption. */ + public object Encryption : + Parsing("The JSON is not representing a valid document") + + /** Invalid License Document Signature. */ + public object Signature : + Parsing("The JSON is not representing a valid document") + + /** Invalid URL for link with [rel]. */ + public class Url(public val rel: String) : + Parsing("The JSON is not representing a valid document") + } + + /** + * Errors while reading or writing a LCP container (LCPL, EPUB, LCPDF, etc.) + */ + public sealed class Container( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + /** Can't access the container, it's format is wrong. */ + public object OpenFailed : + Container("Can't open the license container") + + /** The file at given relative path is not found in the Container. */ + public class FileNotFound(public val url: Url) : + Container("License not found in container") + + /** Can't read the file at given relative path in the Container. */ + public class ReadFailed(public val url: Url?) : + Container("Can't read license from container") + + /** Can't write the file at given relative path in the Container. */ + public class WriteFailed(public val url: Url?) : + Container("Can't write license in container") + } + + /** + * An error occurred while checking the integrity of the License, it can't be retrieved. + */ + public sealed class LicenseIntegrity( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + public object CertificateRevoked : + LicenseIntegrity("Certificate has been revoked in the CRL") + + public object InvalidCertificateSignature : + LicenseIntegrity("Certificate has not been signed by CA") + + public object InvalidLicenseSignatureDate : + LicenseIntegrity("License has been issued by an expired certificate") + + public object InvalidLicenseSignature : + LicenseIntegrity("License signature does not match") + + public object InvalidUserKeyCheck : + LicenseIntegrity("User key check invalid") + } + + public sealed class Decryption( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + public object ContentKeyDecryptError : + Decryption("Unable to decrypt encrypted content key from user key") + + public object ContentDecryptError : Decryption( + "Unable to decrypt encrypted content from content key" + ) + } + + public companion object { + + internal fun wrap(e: Exception): LcpError = when (e) { + is LcpException -> e.error + is NetworkException -> Network(e) + is SocketTimeoutException -> Network(e) + else -> Unknown(e) + } + } +} + +internal class LcpException(val error: LcpError) : Exception(error.message, ErrorException(error)) + +@Deprecated( + "Renamed to `LcpException`", + replaceWith = ReplaceWith("LcpException"), + level = DeprecationLevel.ERROR +) +public typealias LCPError = LcpError + +@Deprecated( + "Use `getUserMessage()` instead", + replaceWith = ReplaceWith("getUserMessage(context)"), + level = DeprecationLevel.ERROR +) +public val LcpError.errorDescription: String get() = message diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpException.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpException.kt deleted file mode 100644 index 0f35992c47..0000000000 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpException.kt +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2020 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.lcp - -import androidx.annotation.PluralsRes -import androidx.annotation.StringRes -import java.net.SocketTimeoutException -import java.util.* -import org.readium.r2.shared.UserException - -sealed class LcpException( - userMessageId: Int, - vararg args: Any, - quantity: Int? = null, - cause: Throwable? = null -) : UserException(userMessageId, quantity, *args, cause = cause) { - constructor(@StringRes userMessageId: Int, vararg args: Any, cause: Throwable? = null) : this(userMessageId, *args, quantity = null, cause = cause) - constructor( - @PluralsRes userMessageId: Int, - quantity: Int, - vararg args: Any, - cause: Throwable? = null - ) : this(userMessageId, *args, quantity = quantity, cause = cause) - - /** The interaction is not available with this License. */ - object LicenseInteractionNotAvailable : LcpException(R.string.r2_lcp_exception_license_interaction_not_available) - - /** This License's profile is not supported by liblcp. */ - object LicenseProfileNotSupported : LcpException(R.string.r2_lcp_exception_license_profile_not_supported) - - /** Failed to retrieve the Certificate Revocation List. */ - object CrlFetching : LcpException(R.string.r2_lcp_exception_crl_fetching) - - /** A network request failed with the given exception. */ - class Network(override val cause: Throwable?) : LcpException(R.string.r2_lcp_exception_network, cause = cause) - - /** - * An unexpected LCP exception occurred. Please post an issue on r2-lcp-kotlin with the error - * message and how to reproduce it. - */ - class Runtime(override val message: String) : LcpException(R.string.r2_lcp_exception_runtime) - - /** An unknown low-level exception was reported. */ - class Unknown(override val cause: Throwable?) : LcpException(R.string.r2_lcp_exception_unknown) - - /** - * Errors while checking the status of the License, using the Status Document. - * - * The app should notify the user and stop there. The message to the user must be clear about - * the status of the license: don't display "expired" if the status is "revoked". The date and - * time corresponding to the new status should be displayed (e.g. "The license expired on 01 - * January 2018"). - */ - sealed class LicenseStatus(userMessageId: Int, vararg args: Any, quantity: Int? = null) : LcpException(userMessageId, *args, quantity = quantity) { - constructor(@StringRes userMessageId: Int, vararg args: Any) : this(userMessageId, *args, quantity = null) - constructor(@PluralsRes userMessageId: Int, quantity: Int, vararg args: Any) : this(userMessageId, *args, quantity = quantity) - - class Cancelled(val date: Date) : LicenseStatus(R.string.r2_lcp_exception_license_status_cancelled, date) - - class Returned(val date: Date) : LicenseStatus(R.string.r2_lcp_exception_license_status_returned, date) - - class NotStarted(val start: Date) : LicenseStatus(R.string.r2_lcp_exception_license_status_not_started, start) - - class Expired(val end: Date) : LicenseStatus(R.string.r2_lcp_exception_license_status_expired, end) - - /** - * If the license has been revoked, the user message should display the number of devices which - * registered to the server. This count can be calculated from the number of "register" events - * in the status document. If no event is logged in the status document, no such message should - * appear (certainly not "The license was registered by 0 devices"). - */ - class Revoked(val date: Date, val devicesCount: Int) : - LicenseStatus(R.plurals.r2_lcp_exception_license_status_revoked, devicesCount, date, devicesCount) - } - - /** - * Errors while renewing a loan. - */ - sealed class Renew(@StringRes userMessageId: Int) : LcpException(userMessageId) { - - /** Your publication could not be renewed properly. */ - object RenewFailed : Renew(R.string.r2_lcp_exception_renew_renew_failed) - - /** Incorrect renewal period, your publication could not be renewed. */ - class InvalidRenewalPeriod(val maxRenewDate: Date?) : Renew(R.string.r2_lcp_exception_renew_invalid_renewal_period) - - /** An unexpected error has occurred on the licensing server. */ - object UnexpectedServerError : Renew(R.string.r2_lcp_exception_renew_unexpected_server_error) - } - - /** - * Errors while returning a loan. - */ - sealed class Return(@StringRes userMessageId: Int) : LcpException(userMessageId) { - - /** Your publication could not be returned properly. */ - object ReturnFailed : Return(R.string.r2_lcp_exception_return_return_failed) - - /** Your publication has already been returned before or is expired. */ - - object AlreadyReturnedOrExpired : Return(R.string.r2_lcp_exception_return_already_returned_or_expired) - - /** An unexpected error has occurred on the licensing server. */ - object UnexpectedServerError : Return(R.string.r2_lcp_exception_return_unexpected_server_error) - } - - /** - * Errors while parsing the License or Status JSON Documents. - */ - sealed class Parsing(@StringRes userMessageId: Int = R.string.r2_lcp_exception_parsing) : LcpException(userMessageId) { - - /** The JSON is malformed and can't be parsed. */ - object MalformedJSON : Parsing(R.string.r2_lcp_exception_parsing_malformed_json) - - /** The JSON is not representing a valid License Document. */ - object LicenseDocument : Parsing(R.string.r2_lcp_exception_parsing_license_document) - - /** The JSON is not representing a valid Status Document. */ - object StatusDocument : Parsing(R.string.r2_lcp_exception_parsing_status_document) - - /** Invalid Link. */ - object Link : Parsing() - - /** Invalid Encryption. */ - object Encryption : Parsing() - - /** Invalid License Document Signature. */ - object Signature : Parsing() - - /** Invalid URL for link with [rel]. */ - class Url(val rel: String) : Parsing() - } - - /** - * Errors while reading or writing a LCP container (LCPL, EPUB, LCPDF, etc.) - */ - sealed class Container(@StringRes userMessageId: Int) : LcpException(userMessageId) { - - /** Can't access the container, it's format is wrong. */ - object OpenFailed : Container(R.string.r2_lcp_exception_container_open_failed) - - /** The file at given relative path is not found in the Container. */ - class FileNotFound(val path: String) : Container(R.string.r2_lcp_exception_container_file_not_found) - - /** Can't read the file at given relative path in the Container. */ - class ReadFailed(val path: String) : Container(R.string.r2_lcp_exception_container_read_failed) - - /** Can't write the file at given relative path in the Container. */ - class WriteFailed(val path: String) : Container(R.string.r2_lcp_exception_container_write_failed) - } - - /** - * An error occurred while checking the integrity of the License, it can't be retrieved. - */ - sealed class LicenseIntegrity(@StringRes userMessageId: Int) : LcpException(userMessageId) { - - object CertificateRevoked : LicenseIntegrity(R.string.r2_lcp_exception_license_integrity_certificate_revoked) - - object InvalidCertificateSignature : LicenseIntegrity(R.string.r2_lcp_exception_license_integrity_invalid_certificate_signature) - - object InvalidLicenseSignatureDate : LicenseIntegrity(R.string.r2_lcp_exception_license_integrity_invalid_license_signature_date) - - object InvalidLicenseSignature : LicenseIntegrity(R.string.r2_lcp_exception_license_integrity_invalid_license_signature) - - object InvalidUserKeyCheck : LicenseIntegrity(R.string.r2_lcp_exception_license_integrity_invalid_user_key_check) - } - - sealed class Decryption(@StringRes userMessageId: Int) : LcpException(userMessageId) { - - object ContentKeyDecryptError : Decryption(R.string.r2_lcp_exception_decryption_content_key_decrypt_error) - - object ContentDecryptError : Decryption(R.string.r2_lcp_exception_decryption_content_decrypt_error) - } - - companion object { - - internal fun wrap(e: Exception?): LcpException = when (e) { - is LcpException -> e - is SocketTimeoutException -> Network(e) - else -> Unknown(e) - } - } -} - -@Deprecated("Renamed to `LcpException`", replaceWith = ReplaceWith("LcpException")) -typealias LCPError = LcpException - -@Deprecated("Use `getUserMessage()` instead", replaceWith = ReplaceWith("getUserMessage(context)")) -val LcpException.errorDescription: String? get() = message diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt index e8ca3acc08..1ac13e10a1 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt @@ -10,52 +10,56 @@ import java.net.URL import java.util.* import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.joda.time.DateTime import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.lcp.license.model.StatusDocument import org.readium.r2.shared.publication.services.ContentProtectionService +import org.readium.r2.shared.util.Closeable import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.toDebugDescription import timber.log.Timber /** * Opened license, used to decipher a protected publication and manage its license. */ -interface LcpLicense : ContentProtectionService.UserRights { +public interface LcpLicense : ContentProtectionService.UserRights, Closeable { /** * License Document information. * https://readium.org/lcp-specs/releases/lcp/latest.html */ - val license: LicenseDocument + public val license: LicenseDocument /** * License Status Document information. * https://readium.org/lcp-specs/releases/lsd/latest.html */ - val status: StatusDocument? + public val status: StatusDocument? /** * Number of remaining characters allowed to be copied by the user. If null, there's no limit. */ - val charactersToCopyLeft: Int? + public val charactersToCopyLeft: StateFlow<Int?> /** * Number of pages allowed to be printed by the user. If null, there's no limit. */ - val pagesToPrintLeft: Int? + public val pagesToPrintLeft: StateFlow<Int?> /** * Can the user renew the loaned publication? */ - val canRenewLoan: Boolean + public val canRenewLoan: Boolean /** * The maximum potential date to renew to. * If null, then the renew date might not be customizable. */ - val maxRenewDate: Date? + public val maxRenewDate: Date? /** * Renews the loan by starting a renew LSD interaction. @@ -63,22 +67,22 @@ interface LcpLicense : ContentProtectionService.UserRights { * @param prefersWebPage Indicates whether the loan should be renewed through a web page if * available, instead of programmatically. */ - suspend fun renewLoan(listener: RenewListener, prefersWebPage: Boolean = false): Try<Date?, LcpException> + public suspend fun renewLoan(listener: RenewListener, prefersWebPage: Boolean = false): Try<Date?, LcpError> /** * Can the user return the loaned publication? */ - val canReturnPublication: Boolean + public val canReturnPublication: Boolean /** * Returns the publication to its provider. */ - suspend fun returnPublication(): Try<Unit, LcpException> + public suspend fun returnPublication(): Try<Unit, LcpError> /** * Decrypts the given [data] encrypted with the license's content key. */ - suspend fun decrypt(data: ByteArray): Try<ByteArray, LcpException> + public suspend fun decrypt(data: ByteArray): Try<ByteArray, LcpError> /** * UX delegate for the loan renew LSD interaction. @@ -86,7 +90,7 @@ interface LcpLicense : ContentProtectionService.UserRights { * If your application fits Material Design guidelines, take a look at [MaterialRenewListener] * for a default implementation. */ - interface RenewListener { + public interface RenewListener { /** * Called when the renew interaction allows to customize the end date programmatically. @@ -94,7 +98,7 @@ interface LcpLicense : ContentProtectionService.UserRights { * * The returned date can't exceed [maximumDate]. */ - suspend fun preferredEndDate(maximumDate: Date?): Date? + public suspend fun preferredEndDate(maximumDate: Date?): Date? /** * Called when the renew interaction uses an HTML web page. @@ -102,40 +106,70 @@ interface LcpLicense : ContentProtectionService.UserRights { * You should present the URL in a Chrome Custom Tab and terminate the function when the * web page is dismissed by the user. */ - suspend fun openWebPage(url: URL) + public suspend fun openWebPage(url: Url) } - @Deprecated("Use `license.encryption.profile` instead", ReplaceWith("license.encryption.profile")) - val encryptionProfile: String? get() = + @Deprecated( + "Use `license.encryption.profile` instead", + ReplaceWith("license.encryption.profile"), + level = DeprecationLevel.ERROR + ) + public val encryptionProfile: String? get() = license.encryption.profile - @Deprecated("Use `decrypt()` with coroutines instead", ReplaceWith("decrypt(data)")) - fun decipher(data: ByteArray): ByteArray? = + @Deprecated( + "Use `decrypt()` with coroutines instead", + ReplaceWith("decrypt(data)"), + level = DeprecationLevel.ERROR + ) + public fun decipher(data: ByteArray): ByteArray? = runBlocking { decrypt(data) } - .onFailure { Timber.e(it) } + .onFailure { Timber.e(it.toDebugDescription()) } .getOrNull() - @Deprecated("Use `renewLoan` with `RenewListener` instead", ReplaceWith("renewLoan(LcpLicense.RenewListener)"), level = DeprecationLevel.ERROR) - suspend fun renewLoan(end: DateTime?, urlPresenter: suspend (URL) -> Unit): Try<Unit, LcpException> = Try.success(Unit) - - @Deprecated("Use `renewLoan` with `RenewListener` instead", ReplaceWith("renewLoan(LcpLicense.RenewListener)"), level = DeprecationLevel.ERROR) - fun renewLoan( + @Deprecated( + "Use `renewLoan` with `RenewListener` instead", + ReplaceWith("renewLoan(LcpLicense.RenewListener)"), + level = DeprecationLevel.ERROR + ) + public suspend fun renewLoan(end: DateTime?, urlPresenter: suspend (URL) -> Unit): Try<Unit, LcpError> = Try.success( + Unit + ) + + @Deprecated( + "Use `renewLoan` with `RenewListener` instead", + ReplaceWith("renewLoan(LcpLicense.RenewListener)"), + level = DeprecationLevel.ERROR + ) + public fun renewLoan( end: DateTime?, present: (URL, dismissed: () -> Unit) -> Unit, - completion: (LcpException?) -> Unit + completion: (LcpError?) -> Unit ) {} - @Deprecated("Use `returnPublication()` with coroutines instead", ReplaceWith("returnPublication")) + @Deprecated( + "Use `returnPublication()` with coroutines instead", + ReplaceWith("returnPublication"), + level = DeprecationLevel.ERROR + ) @DelicateCoroutinesApi - fun returnPublication(completion: (LcpException?) -> Unit) { + public fun returnPublication(completion: (LcpError?) -> Unit) { GlobalScope.launch { - completion(returnPublication().exceptionOrNull()) + completion(returnPublication().failureOrNull()) } } } -@Deprecated("Renamed to `LcpService`", replaceWith = ReplaceWith("LcpService")) -typealias LCPService = LcpService - -@Deprecated("Renamed to `LcpLicense`", replaceWith = ReplaceWith("LcpLicense")) -typealias LCPLicense = LcpLicense +@Deprecated( + "Renamed to `LcpService`", + replaceWith = ReplaceWith("LcpService"), + level = DeprecationLevel.ERROR +) +public typealias LCPService = LcpService + +@Deprecated( + "Renamed to `LcpLicense`", + replaceWith = ReplaceWith("LcpLicense"), + level = DeprecationLevel.ERROR +) +public typealias LCPLicense = LcpLicense diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt new file mode 100644 index 0000000000..b16f261e85 --- /dev/null +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt @@ -0,0 +1,332 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.lcp + +import android.content.Context +import java.io.File +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import org.readium.r2.lcp.license.container.createLicenseContainer +import org.readium.r2.lcp.license.model.LicenseDocument +import org.readium.r2.lcp.util.sha256 +import org.readium.r2.shared.extensions.tryOrLog +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.FileExtension +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.format.EpubSpecification +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatHints +import org.readium.r2.shared.util.format.FormatSpecification +import org.readium.r2.shared.util.format.LcpSpecification +import org.readium.r2.shared.util.format.ZipSpecification +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.mediatype.MediaType + +/** + * Utility to acquire a protected publication from an LCP License Document. + */ +public class LcpPublicationRetriever( + context: Context, + private val downloadManager: DownloadManager, + private val assetRetriever: AssetRetriever +) { + + @JvmInline + public value class RequestId(public val value: String) + + public interface Listener { + + /** + * Called when the publication has been successfully acquired. + */ + public fun onAcquisitionCompleted( + requestId: RequestId, + acquiredPublication: LcpService.AcquiredPublication + ) + + /** + * The acquisition with ID [requestId] has downloaded [downloaded] out of [expected] bytes. + */ + public fun onAcquisitionProgressed( + requestId: RequestId, + downloaded: Long, + expected: Long? + ) + + /** + * The acquisition with ID [requestId] has failed with the given [error]. + */ + public fun onAcquisitionFailed( + requestId: RequestId, + error: LcpError + ) + + /** + * The acquisition with ID [requestId] has been cancelled. + */ + public fun onAcquisitionCancelled( + requestId: RequestId + ) + } + + /** + * Submits a new request to acquire the publication protected with the given [license]. + * + * The given [listener] will automatically be registered. + * + * Returns the ID of the acquisition request, which can be used to cancel it. + */ + public fun retrieve( + license: LicenseDocument, + listener: Listener + ): RequestId { + val requestId = fetchPublication(license) + addListener(requestId, listener) + return requestId + } + + /** + * Registers a listener for the acquisition with the given [requestId]. + * + * If the [downloadManager] provided during construction supports background downloading, this + * should typically be used when you create a new instance after the app restarted. + */ + public fun register( + requestId: RequestId, + listener: Listener + ) { + addListener( + requestId, + listener, + onFirstListenerAdded = { + downloadManager.register( + DownloadManager.RequestId(requestId.value), + downloadListener + ) + } + ) + } + + /** + * Cancels the acquisition with the given [requestId]. + */ + public fun cancel(requestId: RequestId) { + downloadManager.cancel(DownloadManager.RequestId(requestId.value)) + downloadsRepository.removeDownload(requestId.value) + } + + /** + * Releases any in-memory resource associated with this [LcpPublicationRetriever]. + * + * If the pending acquisitions cannot continue in the background, they will be cancelled. + */ + public fun close() { + downloadManager.close() + } + + private val coroutineScope: CoroutineScope = + MainScope() + + private val downloadsRepository: LcpDownloadsRepository = + LcpDownloadsRepository(context) + + private val downloadListener: DownloadManager.Listener = + DownloadListener() + + private val listeners: MutableMap<RequestId, MutableList<Listener>> = + mutableMapOf() + + private fun addListener( + requestId: RequestId, + listener: Listener, + onFirstListenerAdded: () -> Unit = {} + ) { + listeners + .getOrPut(requestId) { + onFirstListenerAdded() + mutableListOf() + } + .add(listener) + } + + private fun fetchPublication( + license: LicenseDocument + ): RequestId { + val url = license.publicationLink.url() as AbsoluteUrl + + val requestId = downloadManager.submit( + request = DownloadManager.Request( + url = url, + headers = emptyMap() + ), + listener = downloadListener + ) + + downloadsRepository.addDownload(requestId.value, license.json) + return RequestId(requestId.value) + } + + private inner class DownloadListener : DownloadManager.Listener { + + @OptIn(ExperimentalEncodingApi::class, ExperimentalStdlibApi::class) + override fun onDownloadCompleted( + requestId: DownloadManager.RequestId, + download: DownloadManager.Download + ) { + coroutineScope.launch { + val lcpRequestId = RequestId(requestId.value) + val listenersForId = checkNotNull(listeners.remove(lcpRequestId)) + + fun failWithError(error: LcpError) { + listenersForId.forEach { + it.onAcquisitionFailed(lcpRequestId, error) + } + tryOrLog { download.file.delete() } + } + + val license = downloadsRepository.retrieveLicense(requestId.value) + ?.let { LicenseDocument(it) } + .also { downloadsRepository.removeDownload(requestId.value) } + ?: run { + failWithError( + LcpError.wrap( + Exception("Couldn't retrieve license from local storage.") + ) + ) + return@launch + } + + license.publicationLink.hash + ?.takeIf { download.file.checkSha256(it) == false } + ?.run { + failWithError( + LcpError.Network( + Exception("Digest mismatch: download looks corrupted.") + ) + ) + return@launch + } + + val format = + assetRetriever.sniffFormat( + download.file, + FormatHints( + mediaTypes = listOfNotNull( + license.publicationLink.mediaType, + download.mediaType + ) + ) + ).getOrElse { + when (it) { + is AssetRetriever.RetrieveError.Reading -> { + failWithError(LcpError.wrap(ErrorException(it))) + return@launch + } + is AssetRetriever.RetrieveError.FormatNotSupported -> { + Format( + specification = FormatSpecification( + ZipSpecification, + EpubSpecification, + LcpSpecification + ), + mediaType = MediaType.EPUB, + fileExtension = FileExtension("epub") + ) + } + } + } + + try { + // Saves the License Document into the downloaded publication + val container = createLicenseContainer(download.file, format.specification) + container.write(license) + } catch (e: Exception) { + failWithError(LcpError.wrap(e)) + return@launch + } + + val acquiredPublication = LcpService.AcquiredPublication( + localFile = download.file, + suggestedFilename = "${license.id}.${format.fileExtension}", + format, + licenseDocument = license + ) + + listenersForId.forEach { + it.onAcquisitionCompleted(lcpRequestId, acquiredPublication) + } + } + } + + override fun onDownloadProgressed( + requestId: DownloadManager.RequestId, + downloaded: Long, + expected: Long? + ) { + val lcpRequestId = RequestId(requestId.value) + val listenersForId = checkNotNull(listeners[lcpRequestId]) + + listenersForId.forEach { + it.onAcquisitionProgressed( + lcpRequestId, + downloaded, + expected + ) + } + } + + override fun onDownloadFailed( + requestId: DownloadManager.RequestId, + error: DownloadManager.DownloadError + ) { + val lcpRequestId = RequestId(requestId.value) + val listenersForId = checkNotNull(listeners[lcpRequestId]) + + downloadsRepository.removeDownload(requestId.value) + + listenersForId.forEach { + it.onAcquisitionFailed( + lcpRequestId, + LcpError.Network(ErrorException(error)) + ) + } + + listeners.remove(lcpRequestId) + } + + override fun onDownloadCancelled(requestId: DownloadManager.RequestId) { + val lcpRequestId = RequestId(requestId.value) + val listenersForId = checkNotNull(listeners[lcpRequestId]) + listenersForId.forEach { + it.onAcquisitionCancelled(lcpRequestId) + } + listeners.remove(lcpRequestId) + } + } + + /** + * Checks that the sha256 sum of file content matches the expected one. + * Returns null if we can't decide. + */ + @OptIn(ExperimentalEncodingApi::class, ExperimentalStdlibApi::class) + private fun File.checkSha256(expected: String): Boolean? { + val actual = sha256() ?: return null + + // Supports hexadecimal encoding for compatibility. + // See https://github.com/readium/lcp-specs/issues/52 + return when (expected.length) { + 44 -> Base64.encode(actual) == expected + 64 -> actual.toHexString() == expected + else -> null + } + } +} diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt index 1c099f16b7..23222a2d27 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt @@ -11,8 +11,11 @@ package org.readium.r2.lcp import android.content.Context import java.io.File -import kotlinx.coroutines.* +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.readium.r2.lcp.auth.LcpDialogAuthentication +import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.lcp.persistence.LcpDatabase import org.readium.r2.lcp.service.CRLService import org.readium.r2.lcp.service.DeviceRepository @@ -23,18 +26,28 @@ import org.readium.r2.lcp.service.LicensesService import org.readium.r2.lcp.service.NetworkService import org.readium.r2.lcp.service.PassphrasesRepository import org.readium.r2.lcp.service.PassphrasesService -import org.readium.r2.shared.publication.ContentProtection +import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.format.Format /** * Service used to acquire and open publications protected with LCP. */ -interface LcpService { +public interface LcpService { /** - * Returns if the publication is protected by LCP. + * Returns if the file is a LCP license document or a publication protected by LCP. */ - suspend fun isLcpProtected(file: File): Boolean + @Deprecated( + "Use an AssetSniffer and check the conformance of the returned format to LcpSpecification", + level = DeprecationLevel.ERROR + ) + public suspend fun isLcpProtected(file: File): Boolean { + throw NotImplementedError() + } /** * Acquires a protected publication from a standalone LCPL's bytes. @@ -43,7 +56,14 @@ interface LcpService { * * @param onProgress Callback to follow the acquisition progress from 0.0 to 1.0. */ - suspend fun acquirePublication(lcpl: ByteArray, onProgress: (Double) -> Unit = {}): Try<AcquiredPublication, LcpException> + @Deprecated( + "Use a LcpPublicationRetriever instead.", + ReplaceWith("publicationRetriever()"), + level = DeprecationLevel.ERROR + ) + public suspend fun acquirePublication(lcpl: ByteArray, onProgress: (Double) -> Unit = {}): Try<AcquiredPublication, LcpError> { + throw NotImplementedError() + } /** * Acquires a protected publication from a standalone LCPL file. @@ -52,12 +72,15 @@ interface LcpService { * * @param onProgress Callback to follow the acquisition progress from 0.0 to 1.0. */ - suspend fun acquirePublication(lcpl: File, onProgress: (Double) -> Unit = {}): Try<AcquiredPublication, LcpException> = withContext(Dispatchers.IO) { - try { - acquirePublication(lcpl.readBytes(), onProgress) - } catch (e: Exception) { - Try.failure(LcpException.wrap(e)) - } + @Deprecated( + "Use a LcpPublicationRetriever instead.", + ReplaceWith("publicationRetriever()"), + level = DeprecationLevel.ERROR + ) + public suspend fun acquirePublication(lcpl: File, onProgress: (Double) -> Unit = {}): Try<AcquiredPublication, LcpError> = withContext( + Dispatchers.IO + ) { + throw NotImplementedError() } /** @@ -71,12 +94,44 @@ interface LcpService { * @param sender Free object that can be used by reading apps to give some UX context when * presenting dialogs with [LcpAuthenticating]. */ - suspend fun retrieveLicense( + @Deprecated( + "Use the overload taking an asset instead.", + level = DeprecationLevel.ERROR + ) + public suspend fun retrieveLicense( file: File, authentication: LcpAuthenticating = LcpDialogAuthentication(), allowUserInteraction: Boolean, sender: Any? = null - ): Try<LcpLicense, LcpException>? + ): Try<LcpLicense, LcpError>? { + throw NotImplementedError() + } + + /** + * Opens the LCP license of a protected publication, to access its DRM metadata and decipher + * its content. If the updated license cannot be stored into the [Asset], you'll get + * an exception if the license points to a LSD server that cannot be reached, + * for instance because no Internet gateway is available. + * + * Updated licenses can currently be stored only into [Asset]s whose source property points to + * a URL with scheme _file_ or _content_. + * + * @param authentication Used to retrieve the user passphrase if it is not already known. + * The request will be cancelled if no passphrase is found in the LCP passphrase storage + * and the provided [authentication]. + * @param allowUserInteraction Indicates whether the user can be prompted for their passphrase. + */ + public suspend fun retrieveLicense( + asset: Asset, + authentication: LcpAuthenticating, + allowUserInteraction: Boolean + ): Try<LcpLicense, LcpError> + + /** + * Creates an [LcpPublicationRetriever] instance which can be used to acquire a protected + * publication from an LCP License Document. + */ + public fun publicationRetriever(): LcpPublicationRetriever /** * Creates a [ContentProtection] instance which can be used with a Streamer to unlock @@ -86,8 +141,9 @@ interface LcpService { * LCP license. The default implementation [LcpDialogAuthentication] presents a dialog to the * user to enter their passphrase. */ - fun contentProtection(authentication: LcpAuthenticating = LcpDialogAuthentication()): ContentProtection = - LcpContentProtection(this, authentication) + public fun contentProtection( + authentication: LcpAuthenticating + ): ContentProtection /** * Information about an acquired publication protected with LCP. @@ -97,75 +153,110 @@ interface LcpService { * @param suggestedFilename Filename that should be used for the publication when importing it in * the user library. */ - data class AcquiredPublication( + public data class AcquiredPublication( val localFile: File, - val suggestedFilename: String + val suggestedFilename: String, + val format: Format, + val licenseDocument: LicenseDocument ) { - @Deprecated("Use `localFile` instead", replaceWith = ReplaceWith("localFile")) + @Deprecated( + "Use `localFile` instead", + replaceWith = ReplaceWith("localFile"), + level = DeprecationLevel.ERROR + ) val localURL: String get() = localFile.path } - companion object { + public companion object { /** * LCP service factory. */ - operator fun invoke(context: Context): LcpService? { - if (!LcpClient.isAvailable()) + public operator fun invoke( + context: Context, + assetRetriever: AssetRetriever, + downloadManager: DownloadManager + ): LcpService? { + if (!LcpClient.isAvailable()) { return null + } val db = LcpDatabase.getDatabase(context).lcpDao() val deviceRepository = DeviceRepository(db) val passphraseRepository = PassphrasesRepository(db) val licenseRepository = LicensesRepository(db) val network = NetworkService() - val device = DeviceService(repository = deviceRepository, network = network, context = context) + val device = DeviceService( + repository = deviceRepository, + network = network, + context = context + ) val crl = CRLService(network = network, context = context) val passphrases = PassphrasesService(repository = passphraseRepository) - return LicensesService(licenses = licenseRepository, crl = crl, device = device, network = network, passphrases = passphrases, context = context) + return LicensesService( + licenses = licenseRepository, + crl = crl, + device = device, + network = network, + passphrases = passphrases, + context = context, + assetRetriever = assetRetriever, + downloadManager = downloadManager + ) } - @Deprecated("Use `LcpService()` instead", ReplaceWith("LcpService(context)"), level = DeprecationLevel.ERROR) - fun create(context: Context): LcpService? = invoke(context) + @Suppress("UNUSED_PARAMETER") + @Deprecated( + "Use `LcpService()` instead", + ReplaceWith("LcpService(context, AssetRetriever(), MediaTypeRetriever())"), + level = DeprecationLevel.ERROR + ) + public fun create(context: Context): LcpService = throw NotImplementedError() } - @Deprecated("Use `acquirePublication()` with coroutines instead", ReplaceWith("acquirePublication(lcpl)")) + @Deprecated( + "Use a LcpPublicationRetriever instead.", + ReplaceWith("publicationRetriever()"), + level = DeprecationLevel.ERROR + ) @DelicateCoroutinesApi - fun importPublication( + public fun importPublication( lcpl: ByteArray, authentication: LcpAuthenticating?, - completion: (AcquiredPublication?, LcpException?) -> Unit + completion: (AcquiredPublication?, LcpError?) -> Unit ) { - GlobalScope.launch { - acquirePublication(lcpl) - .onSuccess { completion(it, null) } - .onFailure { completion(null, it) } - } + throw NotImplementedError() } - @Deprecated("Use `retrieveLicense()` with coroutines instead", ReplaceWith("retrieveLicense(File(publication), authentication, allowUserInteraction = true)")) + @Deprecated( + "Use `retrieveLicense()` with coroutines instead", + ReplaceWith( + "retrieveLicense(File(publication), authentication, allowUserInteraction = true)" + ), + level = DeprecationLevel.ERROR + ) @DelicateCoroutinesApi - fun retrieveLicense( + public fun retrieveLicense( publication: String, authentication: LcpAuthenticating?, - completion: (LcpLicense?, LcpException?) -> Unit + completion: (LcpLicense?, LcpError?) -> Unit ) { - GlobalScope.launch { - val result = retrieveLicense(File(publication), authentication ?: LcpDialogAuthentication(), allowUserInteraction = true) - if (result == null) { - completion(null, null) - } else { - result - .onSuccess { completion(it, null) } - .onFailure { completion(null, it) } - } - } + throw NotImplementedError() } } -@Deprecated("Renamed to `LcpService()`", replaceWith = ReplaceWith("LcpService(context)")) -fun R2MakeLCPService(context: Context): LcpService = - LcpService(context) ?: throw Exception("liblcp is missing on the classpath") +@Suppress("UNUSED_PARAMETER") +@Deprecated( + "Renamed to `LcpService()`", + replaceWith = ReplaceWith("LcpService(context)"), + level = DeprecationLevel.ERROR +) +public fun R2MakeLCPService(context: Context): LcpService = + throw NotImplementedError() -@Deprecated("Renamed to `LcpService.AcquiredPublication`", replaceWith = ReplaceWith("LcpService.AcquiredPublication")) -typealias LCPImportedPublication = LcpService.AcquiredPublication +@Deprecated( + "Renamed to `LcpService.AcquiredPublication`", + replaceWith = ReplaceWith("LcpService.AcquiredPublication"), + level = DeprecationLevel.ERROR +) +public typealias LCPImportedPublication = LcpService.AcquiredPublication diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/MaterialRenewListener.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/MaterialRenewListener.kt index 5661a01aaa..6fe2f85d77 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/MaterialRenewListener.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/MaterialRenewListener.kt @@ -12,12 +12,12 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.browser.customtabs.CustomTabsIntent import androidx.fragment.app.FragmentManager import com.google.android.material.datepicker.* -import java.net.URL import java.util.* import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.suspendCancellableCoroutine +import org.readium.r2.shared.util.Url /** * A default implementation of the [LcpLicense.RenewListener] using Chrome Custom Tabs for @@ -31,7 +31,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine * @param caller Activity or Fragment used to register the ActivityResultLauncher. * @param fragmentManager FragmentManager used to present the date picker. */ -class MaterialRenewListener( +public class MaterialRenewListener( private val license: LcpLicense, private val caller: ActivityResultCaller, private val fragmentManager: FragmentManager @@ -73,19 +73,23 @@ class MaterialRenewListener( .show(fragmentManager, "MaterialRenewListener.DatePicker") } - override suspend fun openWebPage(url: URL) = suspendCoroutine<Unit> { cont -> - webPageContinuation = cont + override suspend fun openWebPage(url: Url) { + suspendCoroutine { cont -> + webPageContinuation = cont - webPageLauncher.launch( - CustomTabsIntent.Builder().build().intent.apply { - data = Uri.parse(url.toString()) - } - ) + webPageLauncher.launch( + CustomTabsIntent.Builder().build().intent.apply { + data = Uri.parse(url.toString()) + } + ) + } } private var webPageContinuation: Continuation<Unit>? = null - private val webPageLauncher = caller.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + private val webPageLauncher = caller.registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { webPageContinuation?.resume(Unit) } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/auth/LcpDialogAuthentication.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/auth/LcpDialogAuthentication.kt index 83b81ab99f..02e513bce3 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/auth/LcpDialogAuthentication.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/auth/LcpDialogAuthentication.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 Readium Foundation. All rights reserved. + * Copyright 2023 Readium Foundation. All rights reserved. * Use of this source code is governed by the BSD-style license * available in the top-level LICENSE file of the project. */ @@ -7,68 +7,118 @@ package org.readium.r2.lcp.auth import android.annotation.SuppressLint -import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri +import android.text.Editable import android.view.Gravity import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.Button import android.widget.ListPopupWindow import android.widget.PopupWindow import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible -import androidx.fragment.app.Fragment +import androidx.core.widget.addTextChangedListener import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import java.util.* +import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withContext import org.readium.r2.lcp.LcpAuthenticating import org.readium.r2.lcp.R import org.readium.r2.lcp.license.model.components.Link import org.readium.r2.shared.extensions.tryOr import org.readium.r2.shared.extensions.tryOrNull -import timber.log.Timber +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.toUri /** * An [LcpAuthenticating] implementation presenting a dialog to the user. * - * For this authentication to trigger, you must provide a [sender] parameter of type [Activity], - * [Fragment] or [View] to `Streamer::open()` or `LcpService::retrieveLicense()`. It will be used as - * the host view for the dialog. + * This authentication requires a view to anchor on. To use it, you'll need to call + * [onParentViewAttachedToWindow] every time it gets attached to a window and + * [onParentViewDetachedFromWindow] when it gets detached. You can typically achieve this with + * a [View.OnAttachStateChangeListener]. Without view to anchor on, [retrievePassphrase] will + * suspend until one is available. */ -class LcpDialogAuthentication : LcpAuthenticating { +public class LcpDialogAuthentication : LcpAuthenticating { + + private class SuspendedCall( + val continuation: Continuation<String?>, + val license: LcpAuthenticating.AuthenticatedLicense, + val reason: LcpAuthenticating.AuthenticationReason, + var currentInput: Editable? = null + ) + + private val mutex: Mutex = Mutex() + private var suspendedCall: SuspendedCall? = null + private var parentView: View? = null + + /** + * Call this method every time the anchor view gets attached to the window. + */ + public fun onParentViewAttachedToWindow(parentView: View) { + this@LcpDialogAuthentication.parentView = parentView + suspendedCall?.let { showPopupWindow(it, parentView) } + } + + /** + * Call this method every time the anchor view gets detached from the window. + */ + public fun onParentViewDetachedFromWindow() { + this.parentView = null + } override suspend fun retrievePassphrase( license: LcpAuthenticating.AuthenticatedLicense, reason: LcpAuthenticating.AuthenticationReason, - allowUserInteraction: Boolean, - sender: Any? + allowUserInteraction: Boolean ): String? = - if (allowUserInteraction) withContext(Dispatchers.Main) { askPassphrase(license, reason, sender) } - else null + if (allowUserInteraction) { + withContext(Dispatchers.Main) { + askPassphrase( + license, + reason + ) + } + } else { + null + } private suspend fun askPassphrase( license: LcpAuthenticating.AuthenticatedLicense, - reason: LcpAuthenticating.AuthenticationReason, - sender: Any? + reason: LcpAuthenticating.AuthenticationReason ): String? { - val hostView = (sender as? View) ?: (sender as? Activity)?.findViewById<ViewGroup>(android.R.id.content)?.getChildAt(0) ?: (sender as? Fragment)?.view - ?: run { - Timber.e("No valid [sender] was passed to `LcpDialogAuthentication::retrievePassphrase()`. Make sure it is an Activity, a Fragment or a View.") - return null - } + mutex.lock() + + return suspendCoroutine { cont -> + val suspendedCall = SuspendedCall(cont, license, reason) + this.suspendedCall = suspendedCall + parentView?.let { showPopupWindow(suspendedCall, it) } + } + } + + private fun terminateCall() { + suspendedCall = null + mutex.unlock() + } + + private fun showPopupWindow( + suspendedCall: SuspendedCall, + hostView: View + ) { val context = hostView.context val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + @SuppressLint("InflateParams") // https://stackoverflow.com/q/26404951/1474476 - val dialogView = inflater.inflate(R.layout.r2_lcp_auth_dialog, null) + val dialogView = inflater.inflate(R.layout.readium_lcp_auth_dialog, null) val title = dialogView.findViewById(R.id.r2_title) as TextView val description = dialogView.findViewById(R.id.r2_description) as TextView @@ -80,61 +130,75 @@ class LcpDialogAuthentication : LcpAuthenticating { val forgotButton = dialogView.findViewById(R.id.r2_forgotButton) as Button val helpButton = dialogView.findViewById(R.id.r2_helpButton) as Button - forgotButton.isVisible = license.hintLink != null - helpButton.isVisible = license.supportLinks.isNotEmpty() + password.text = suspendedCall.currentInput + password.addTextChangedListener { suspendedCall.currentInput = it } + + forgotButton.isVisible = suspendedCall.license.hintLink != null + helpButton.isVisible = suspendedCall.license.supportLinks.isNotEmpty() - when (reason) { + when (suspendedCall.reason) { LcpAuthenticating.AuthenticationReason.PassphraseNotFound -> { - title.text = context.getString(R.string.r2_lcp_dialog_reason_passphraseNotFound) + title.text = context.getString( + R.string.readium_lcp_dialog_reason_passphraseNotFound + ) } + LcpAuthenticating.AuthenticationReason.InvalidPassphrase -> { - title.text = context.getString(R.string.r2_lcp_dialog_reason_invalidPassphrase) - passwordLayout.error = context.getString(R.string.r2_lcp_dialog_reason_invalidPassphrase) + title.text = context.getString(R.string.readium_lcp_dialog_reason_invalidPassphrase) + passwordLayout.error = context.getString( + R.string.readium_lcp_dialog_reason_invalidPassphrase + ) } } - val provider = tryOr(license.provider) { Uri.parse(license.provider).host } - description.text = context.getString(R.string.r2_lcp_dialog_prompt, provider) - - hint.text = license.hint - - return suspendCoroutine { cont -> - val popupWindow = PopupWindow(dialogView, ListPopupWindow.MATCH_PARENT, ListPopupWindow.MATCH_PARENT).apply { - isOutsideTouchable = false - isFocusable = true - elevation = 5.0f - } - - cancelButton.setOnClickListener { - popupWindow.dismiss() - cont.resume(null) - } + val provider = tryOr(suspendedCall.license.provider) { + Uri.parse(suspendedCall.license.provider).host + } + description.text = context.getString(R.string.readium_lcp_dialog_prompt, provider) + + hint.text = suspendedCall.license.hint + + val popupWindow = PopupWindow( + dialogView, + ListPopupWindow.MATCH_PARENT, + ListPopupWindow.MATCH_PARENT + ).apply { + isOutsideTouchable = false + isFocusable = true + elevation = 5.0f + } - confirmButton.setOnClickListener { - popupWindow.dismiss() - cont.resume(password.text.toString()) - } + cancelButton.setOnClickListener { + popupWindow.dismiss() + terminateCall() + suspendedCall.continuation.resume(null) + } - forgotButton.setOnClickListener { - license.hintLink?.let { context.startActivityForLink(it) } - } + confirmButton.setOnClickListener { + popupWindow.dismiss() + terminateCall() + suspendedCall.continuation.resume(password.text.toString()) + } - helpButton.setOnClickListener { - showHelpDialog(context, license.supportLinks) - } + forgotButton.setOnClickListener { + suspendedCall.license.hintLink?.let { context.startActivityForLink(it) } + } - popupWindow.showAtLocation(hostView, Gravity.CENTER, 0, 0) + helpButton.setOnClickListener { + showHelpDialog(context, suspendedCall.license.supportLinks) } + + popupWindow.showAtLocation(hostView, Gravity.CENTER, 0, 0) } private fun showHelpDialog(context: Context, links: List<Link>) { val titles = links.map { - it.title ?: tryOr(context.getString(R.string.r2_lcp_dialog_support)) { - when (Uri.parse(it.href).scheme) { - "http", "https" -> context.getString(R.string.r2_lcp_dialog_support_web) - "tel" -> context.getString(R.string.r2_lcp_dialog_support_phone) - "mailto" -> context.getString(R.string.r2_lcp_dialog_support_mail) - else -> context.getString(R.string.r2_lcp_dialog_support) + it.title ?: tryOr(context.getString(R.string.readium_lcp_dialog_support)) { + when ((it.url() as? AbsoluteUrl)?.scheme?.value) { + "http", "https" -> context.getString(R.string.readium_lcp_dialog_support_web) + "tel" -> context.getString(R.string.readium_lcp_dialog_support_phone) + "mailto" -> context.getString(R.string.readium_lcp_dialog_support_mail) + else -> context.getString(R.string.readium_lcp_dialog_support) } } }.toTypedArray() @@ -147,9 +211,9 @@ class LcpDialogAuthentication : LcpAuthenticating { } private fun Context.startActivityForLink(link: Link) { - val url = tryOrNull { Uri.parse(link.href) } ?: return + val url = tryOrNull { (link.url() as? AbsoluteUrl) } ?: return - val action = when (url.scheme?.lowercase(Locale.ROOT)) { + val action = when (url.scheme.value) { "http", "https" -> Intent(Intent.ACTION_VIEW) "tel" -> Intent(Intent.ACTION_CALL) "mailto" -> Intent(Intent.ACTION_SEND) @@ -158,7 +222,7 @@ class LcpDialogAuthentication : LcpAuthenticating { startActivity( Intent(action).apply { - data = url + data = url.toUri() } ) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/auth/LcpPassphraseAuthentication.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/auth/LcpPassphraseAuthentication.kt index 84f87b0462..fb7aca869d 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/auth/LcpPassphraseAuthentication.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/auth/LcpPassphraseAuthentication.kt @@ -14,7 +14,7 @@ import org.readium.r2.lcp.LcpAuthenticating * * If the provided [passphrase] is incorrect, the given [fallback] authentication is used. */ -class LcpPassphraseAuthentication( +public class LcpPassphraseAuthentication( private val passphrase: String, private val fallback: LcpAuthenticating? = null ) : LcpAuthenticating { @@ -22,11 +22,14 @@ class LcpPassphraseAuthentication( override suspend fun retrievePassphrase( license: LcpAuthenticating.AuthenticatedLicense, reason: LcpAuthenticating.AuthenticationReason, - allowUserInteraction: Boolean, - sender: Any? + allowUserInteraction: Boolean ): String? { if (reason != LcpAuthenticating.AuthenticationReason.PassphraseNotFound) { - return fallback?.retrievePassphrase(license, reason, allowUserInteraction = allowUserInteraction, sender = sender) + return fallback?.retrievePassphrase( + license, + reason, + allowUserInteraction = allowUserInteraction + ) } return passphrase diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt index faef7f453a..efcc9a363c 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt @@ -10,12 +10,17 @@ package org.readium.r2.lcp.license import java.net.HttpURLConnection -import java.util.* -import kotlin.time.ExperimentalTime +import java.util.Date import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import org.readium.r2.lcp.BuildConfig.DEBUG +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.LcpLicense import org.readium.r2.lcp.license.model.LicenseDocument @@ -29,24 +34,61 @@ import org.readium.r2.shared.extensions.toIso8601String import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.mediatype.MediaType import timber.log.Timber -@OptIn(ExperimentalTime::class) -internal class License( +internal class License private constructor( + private val coroutineScope: CoroutineScope, private var documents: ValidatedDocuments, private val validation: LicenseValidation, private val licenses: LicensesRepository, private val device: DeviceService, - private val network: NetworkService + private val network: NetworkService, + private val printsLeft: StateFlow<Int?>, + private val copiesLeft: StateFlow<Int?> ) : LcpLicense { + companion object { + + suspend operator fun invoke( + documents: ValidatedDocuments, + validation: LicenseValidation, + licenses: LicensesRepository, + device: DeviceService, + network: NetworkService + ): License { + val coroutineScope = MainScope() + + val printsLeft = licenses + .printsLeft(documents.license.id) + .stateIn(coroutineScope) + + val copiesLeft = licenses + .copiesLeft(documents.license.id) + .stateIn(coroutineScope) + + return License( + coroutineScope = coroutineScope, + documents = documents, + validation = validation, + licenses = licenses, + device = device, + network = network, + printsLeft = printsLeft, + copiesLeft = copiesLeft + ) + } + } + override val license: LicenseDocument get() = documents.license override val status: StatusDocument? get() = documents.status - override suspend fun decrypt(data: ByteArray): Try<ByteArray, LcpException> = withContext(Dispatchers.Default) { + override suspend fun decrypt(data: ByteArray): Try<ByteArray, LcpError> = withContext( + Dispatchers.Default + ) { try { // LCP lib crashes if we call decrypt on an empty ByteArray if (data.isEmpty()) { @@ -57,87 +99,55 @@ internal class License( Try.success(decryptedData) } } catch (e: Exception) { - Try.failure(LcpException.wrap(e)) + Try.failure(LcpError.wrap(e)) } } - override val charactersToCopyLeft: Int? - get() { - try { - val charactersLeft = licenses.copiesLeft(license.id) - if (charactersLeft != null) { - return charactersLeft - } - } catch (error: Error) { - if (DEBUG) Timber.e(error) - } - return null - } + override val charactersToCopyLeft: StateFlow<Int?> + get() = copiesLeft override val canCopy: Boolean - get() = (charactersToCopyLeft ?: 1) > 0 + get() = (charactersToCopyLeft.value ?: 1) > 0 override fun canCopy(text: String): Boolean = - charactersToCopyLeft?.let { it <= text.length } + charactersToCopyLeft.value?.let { it <= text.length } ?: true - override fun copy(text: String): Boolean { - var charactersLeft = charactersToCopyLeft ?: return true - if (text.length > charactersLeft) { - return false - } - - try { - charactersLeft = maxOf(0, charactersLeft - text.length) - licenses.setCopiesLeft(charactersLeft, license.id) - } catch (error: Error) { - if (DEBUG) Timber.e(error) + override suspend fun copy(text: String): Boolean { + return try { + licenses.tryCopy(text.length, license.id) + } catch (e: Exception) { + if (DEBUG) Timber.e(e) + false } - return true } - override val pagesToPrintLeft: Int? - get() { - try { - val pagesLeft = licenses.printsLeft(license.id) - if (pagesLeft != null) { - return pagesLeft - } - } catch (error: Error) { - if (DEBUG) Timber.e(error) - } - return null - } + override val pagesToPrintLeft: StateFlow<Int?> = + printsLeft override val canPrint: Boolean - get() = (pagesToPrintLeft ?: 1) > 0 + get() = (pagesToPrintLeft.value ?: 1) > 0 override fun canPrint(pageCount: Int): Boolean = - pagesToPrintLeft?.let { it <= pageCount } + pagesToPrintLeft.value?.let { it <= pageCount } ?: true - override fun print(pageCount: Int): Boolean { - var pagesLeft = pagesToPrintLeft ?: return true - if (pagesLeft < pageCount) { - return false - } - try { - pagesLeft = maxOf(0, pagesLeft - pageCount) - licenses.setPrintsLeft(pagesLeft, license.id) - } catch (error: Error) { - if (DEBUG) Timber.e(error) + override suspend fun print(pageCount: Int): Boolean { + return try { + licenses.tryPrint(pageCount, license.id) + } catch (e: Exception) { + if (DEBUG) Timber.e(e) + false } - return true } override val canRenewLoan: Boolean - get() = status?.link(StatusDocument.Rel.renew) != null + get() = status?.link(StatusDocument.Rel.Renew) != null override val maxRenewDate: Date? get() = status?.potentialRights?.end - override suspend fun renewLoan(listener: LcpLicense.RenewListener, prefersWebPage: Boolean): Try<Date?, LcpException> { - + override suspend fun renewLoan(listener: LcpLicense.RenewListener, prefersWebPage: Boolean): Try<Date?, LcpError> { // Finds the renew link according to `prefersWebPage`. fun findRenewLink(): Link? { val status = documents.status ?: return null @@ -150,34 +160,43 @@ internal class License( } for (type in types) { - return status.link(StatusDocument.Rel.renew, type = type) + return status.link(StatusDocument.Rel.Renew, type = type) ?: continue } // Fallback on the first renew link with no media type set and assume it's a PUT action. - return status.linkWithNoType(StatusDocument.Rel.renew) + return status.linkWithNoType(StatusDocument.Rel.Renew) } // Programmatically renew the loan with a PUT request. suspend fun renewProgrammatically(link: Link): ByteArray { val endDate = - if (link.templateParameters.contains("end")) + if (link.href.parameters?.contains("end") == true) { listener.preferredEndDate(maxRenewDate) - else null + } else { + null + } val parameters = this.device.asQueryParameters.toMutableMap() if (endDate != null) { parameters["end"] = endDate.toIso8601String() } - val url = link.url(parameters) + val url = link.url(parameters = parameters) return network.fetch(url.toString(), NetworkService.Method.PUT) .getOrElse { error -> when (error.status) { - HttpURLConnection.HTTP_BAD_REQUEST -> throw LcpException.Renew.RenewFailed - HttpURLConnection.HTTP_FORBIDDEN -> throw LcpException.Renew.InvalidRenewalPeriod(maxRenewDate = this.maxRenewDate) - else -> throw LcpException.Renew.UnexpectedServerError + HttpURLConnection.HTTP_BAD_REQUEST -> + throw LcpException(LcpError.Renew.RenewFailed) + HttpURLConnection.HTTP_FORBIDDEN -> + throw LcpException( + LcpError.Renew.InvalidRenewalPeriod( + maxRenewDate = this.maxRenewDate + ) + ) + else -> + throw LcpException(LcpError.Renew.UnexpectedServerError) } } } @@ -185,21 +204,27 @@ internal class License( // Renew the loan by presenting a web page to the user. suspend fun renewWithWebPage(link: Link): ByteArray { // The reading app will open the URL in a web view and return when it is dismissed. - listener.openWebPage(link.url) + listener.openWebPage(link.url()) val statusURL = tryOrNull { - license.url(LicenseDocument.Rel.status, preferredType = MediaType.LCP_STATUS_DOCUMENT) - } ?: throw LcpException.LicenseInteractionNotAvailable - - return network.fetch(statusURL.toString(), headers = mapOf("Accept" to MediaType.LCP_STATUS_DOCUMENT.toString())).getOrThrow() + license.url( + LicenseDocument.Rel.Status, + preferredType = MediaType.LCP_STATUS_DOCUMENT + ) + } ?: throw LcpException(LcpError.LicenseInteractionNotAvailable) + + return network.fetch( + statusURL.toString(), + headers = mapOf("Accept" to MediaType.LCP_STATUS_DOCUMENT.toString()) + ).getOrThrow() } try { val link = findRenewLink() - ?: throw LcpException.LicenseInteractionNotAvailable + ?: throw LcpException(LcpError.LicenseInteractionNotAvailable) val data = - if (link.mediaType.isHtml) { + if (link.mediaType?.isHtml == true) { renewWithWebPage(link) } else { renewProgrammatically(link) @@ -212,41 +237,52 @@ internal class License( // Passthrough for cancelled coroutines throw e } catch (e: Exception) { - return Try.failure(LcpException.wrap(e)) + return Try.failure(LcpError.wrap(e)) } } override val canReturnPublication: Boolean - get() = status?.link(StatusDocument.Rel.`return`) != null + get() = status?.link(StatusDocument.Rel.Return) != null - override suspend fun returnPublication(): Try<Unit, LcpException> { + override suspend fun returnPublication(): Try<Unit, LcpError> { try { val status = this.documents.status val url = try { - status?.url(StatusDocument.Rel.`return`, preferredType = null, parameters = device.asQueryParameters) + status?.url( + StatusDocument.Rel.Return, + preferredType = null, + parameters = device.asQueryParameters + ) } catch (e: Throwable) { null } if (status == null || url == null) { - throw LcpException.LicenseInteractionNotAvailable + throw LcpException(LcpError.LicenseInteractionNotAvailable) } network.fetch(url.toString(), method = NetworkService.Method.PUT) .onSuccess { validateStatusDocument(it) } .onFailure { error -> when (error.status) { - HttpURLConnection.HTTP_BAD_REQUEST -> throw LcpException.Return.ReturnFailed - HttpURLConnection.HTTP_FORBIDDEN -> throw LcpException.Return.AlreadyReturnedOrExpired - else -> throw LcpException.Return.UnexpectedServerError + HttpURLConnection.HTTP_BAD_REQUEST -> throw LcpException( + LcpError.Return.ReturnFailed + ) + HttpURLConnection.HTTP_FORBIDDEN -> throw LcpException( + LcpError.Return.AlreadyReturnedOrExpired + ) + else -> throw LcpException(LcpError.Return.UnexpectedServerError) } } return Try.success(Unit) } catch (e: Exception) { - return Try.failure(LcpException.wrap(e)) + return Try.failure(LcpError.wrap(e)) } } + private fun validateStatusDocument(data: ByteArray): Unit = + validation.validate(LicenseValidation.Document.status(data)) { _, _ -> } + init { LicenseValidation.observe(validation) { documents, _ -> documents?.let { @@ -255,6 +291,7 @@ internal class License( } } - private fun validateStatusDocument(data: ByteArray): Unit = - validation.validate(LicenseValidation.Document.status(data)) { _, _ -> } + override fun close() { + coroutineScope.cancel() + } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/LicenseValidation.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/LicenseValidation.kt index a918497a76..8c9d60435a 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/LicenseValidation.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/LicenseValidation.kt @@ -11,15 +11,19 @@ package org.readium.r2.lcp.license import java.util.* import kotlin.time.Duration.Companion.seconds -import kotlin.time.ExperimentalTime import kotlinx.coroutines.runBlocking import org.readium.r2.lcp.BuildConfig.DEBUG import org.readium.r2.lcp.LcpAuthenticating +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.lcp.license.model.StatusDocument import org.readium.r2.lcp.license.model.components.Link -import org.readium.r2.lcp.service.* +import org.readium.r2.lcp.service.CRLService +import org.readium.r2.lcp.service.DeviceService +import org.readium.r2.lcp.service.LcpClient +import org.readium.r2.lcp.service.NetworkService +import org.readium.r2.lcp.service.PassphrasesService import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import timber.log.Timber @@ -29,17 +33,20 @@ internal sealed class Either<A, B> { class Right<A, B>(val right: B) : Either<A, B>() } -private val supportedProfiles = listOf("http://readium.org/lcp/basic-profile", "http://readium.org/lcp/profile-1.0") +private val supportedProfiles = listOf( + "http://readium.org/lcp/basic-profile", + "http://readium.org/lcp/profile-1.0" +) -internal typealias Context = Either<LcpClient.Context, LcpException.LicenseStatus> +internal typealias Context = Either<LcpClient.Context, LcpError.LicenseStatus> internal typealias Observer = (ValidatedDocuments?, Exception?) -> Unit private var observers: MutableList<Pair<Observer, ObserverPolicy>> = mutableListOf() internal enum class ObserverPolicy { - once, - always + Once, + Always } internal data class ValidatedDocuments constructor( @@ -50,7 +57,7 @@ internal data class ValidatedDocuments constructor( fun getContext(): LcpClient.Context { when (context) { is Either.Left -> return context.left - is Either.Right -> throw context.right + is Either.Right -> throw LcpException(context.right) } } } @@ -61,7 +68,11 @@ internal sealed class State { data class fetchStatus(val license: LicenseDocument) : State() data class validateStatus(val license: LicenseDocument, val data: ByteArray) : State() data class fetchLicense(val license: LicenseDocument, val status: StatusDocument) : State() - data class checkLicenseStatus(val license: LicenseDocument, val status: StatusDocument?) : State() + data class checkLicenseStatus( + val license: LicenseDocument, + val status: StatusDocument?, + val statusDocumentTakesPrecedence: Boolean + ) : State() data class retrievePassphrase(val license: LicenseDocument, val status: StatusDocument?) : State() data class validateIntegrity( val license: LicenseDocument, @@ -79,7 +90,7 @@ internal sealed class Event { data class validatedLicense(val license: LicenseDocument) : Event() data class retrievedStatusData(val data: ByteArray) : Event() data class validatedStatus(val status: StatusDocument) : Event() - data class checkedLicenseStatus(val error: LcpException.LicenseStatus?) : Event() + data class checkedLicenseStatus(val error: LcpError.LicenseStatus?) : Event() data class retrievedPassphrase(val passphrase: String) : Event() data class validatedIntegrity(val context: LcpClient.Context) : Event() data class registeredDevice(val statusData: ByteArray?) : Event() @@ -87,11 +98,14 @@ internal sealed class Event { object cancelled : Event() } -@OptIn(ExperimentalTime::class) +/** + * If [ignoreInternetErrors] is true, then the validation won't fail on [LcpError.Network] errors. + * This should be the case with writable licenses (such as local ones) but not with read-only licences. + */ internal class LicenseValidation( var authentication: LcpAuthenticating?, val allowUserInteraction: Boolean, - val sender: Any?, + val ignoreInternetErrors: Boolean, val crl: CRLService, val device: DeviceService, val network: NetworkService, @@ -142,7 +156,7 @@ internal class LicenseValidation( on<Event.validatedLicense> { status?.let { status -> if (DEBUG) Timber.d("State.checkLicenseStatus(it.license, status)") - transitionTo(State.checkLicenseStatus(it.license, status)) + transitionTo(State.checkLicenseStatus(it.license, status, false)) } ?: run { if (DEBUG) Timber.d("State.fetchStatus(it.license)") transitionTo(State.fetchStatus(it.license)) @@ -159,8 +173,13 @@ internal class LicenseValidation( transitionTo(State.validateStatus(license, it.data)) } on<Event.failed> { - if (DEBUG) Timber.d("State.checkLicenseStatus(license, null)") - transitionTo(State.checkLicenseStatus(license, null)) + if (!ignoreInternetErrors && it.error is LcpException && it.error.error is LcpError.Network) { + if (DEBUG) Timber.d("State.failure(it.error)") + transitionTo(State.failure(it.error)) + } else { + if (DEBUG) Timber.d("State.checkLicenseStatus(license, null)") + transitionTo(State.checkLicenseStatus(license, null, false)) + } } } state<State.validateStatus> { @@ -170,12 +189,12 @@ internal class LicenseValidation( transitionTo(State.fetchLicense(license, it.status)) } else { if (DEBUG) Timber.d("State.checkLicenseStatus(license, it.status)") - transitionTo(State.checkLicenseStatus(license, it.status)) + transitionTo(State.checkLicenseStatus(license, it.status, false)) } } on<Event.failed> { if (DEBUG) Timber.d("State.checkLicenseStatus(license, null)") - transitionTo(State.checkLicenseStatus(license, null)) + transitionTo(State.checkLicenseStatus(license, null, false)) } } state<State.fetchLicense> { @@ -185,14 +204,20 @@ internal class LicenseValidation( } on<Event.failed> { if (DEBUG) Timber.d("State.checkLicenseStatus(license, status)") - transitionTo(State.checkLicenseStatus(license, status)) + transitionTo(State.checkLicenseStatus(license, status, true)) } } state<State.checkLicenseStatus> { on<Event.checkedLicenseStatus> { it.error?.let { error -> - if (DEBUG) Timber.d("State.valid(ValidatedDocuments(license, Either.Right(error), status))") - transitionTo(State.valid(ValidatedDocuments(license, Either.Right(error), status))) + if (DEBUG) { + Timber.d( + "State.valid(ValidatedDocuments(license, Either.Right(error), status))" + ) + } + transitionTo( + State.valid(ValidatedDocuments(license, Either.Right(error), status)) + ) } ?: run { if (DEBUG) Timber.d("State.requestPassphrase(license, status)") transitionTo(State.retrievePassphrase(license, status)) @@ -216,7 +241,7 @@ internal class LicenseValidation( state<State.validateIntegrity> { on<Event.validatedIntegrity> { val documents = ValidatedDocuments(license, Either.Left(it.context), status) - val link = status?.link(StatusDocument.Rel.register) + val link = status?.link(StatusDocument.Rel.Register) link?.let { if (DEBUG) Timber.d("State.registerDevice(documents, link)") transitionTo(State.registerDevice(documents, link)) @@ -280,7 +305,11 @@ internal class LicenseValidation( is State.fetchStatus -> fetchStatus(state.license) is State.validateStatus -> validateStatus(state.data) is State.fetchLicense -> fetchLicense(state.status) - is State.checkLicenseStatus -> checkLicenseStatus(state.license, state.status) + is State.checkLicenseStatus -> checkLicenseStatus( + state.license, + state.status, + state.statusDocumentTakesPrecedence + ) is State.retrievePassphrase -> requestPassphrase(state.license) is State.validateIntegrity -> validateIntegrity(state.license, state.passphrase) is State.registerDevice -> registerDevice(state.documents.license, state.link) @@ -297,7 +326,7 @@ internal class LicenseValidation( private fun observe(event: Event, observer: Observer) { raise(event) - Companion.observe(this, ObserverPolicy.once, observer) + Companion.observe(this, ObserverPolicy.Once, observer) } private fun notifyObservers(documents: ValidatedDocuments?, error: Exception?) { @@ -306,24 +335,32 @@ internal class LicenseValidation( observer.first(documents, error) } // Timber.d("observers $observers") - observers = (observers.filter { it.second != ObserverPolicy.once }).toMutableList() + observers = (observers.filter { it.second != ObserverPolicy.Once }).toMutableList() // Timber.d("observers $observers") } private fun validateLicense(data: ByteArray) { val license = LicenseDocument(data = data) if (!isProduction && license.encryption.profile != "http://readium.org/lcp/basic-profile") { - throw LcpException.LicenseProfileNotSupported + throw LcpException(LcpError.LicenseProfileNotSupported) } onLicenseValidated(license) raise(Event.validatedLicense(license)) } private suspend fun fetchStatus(license: LicenseDocument) { - val url = license.url(LicenseDocument.Rel.status, preferredType = MediaType.LCP_STATUS_DOCUMENT).toString() - // Short timeout to avoid blocking the License, since the LSD is optional. - val data = network.fetch(url, timeout = 5.seconds, headers = mapOf("Accept" to MediaType.LCP_STATUS_DOCUMENT.toString())) - .getOrElse { throw LcpException.Network(it) } + val url = license.url( + LicenseDocument.Rel.Status, + preferredType = MediaType.LCP_STATUS_DOCUMENT + ).toString() + // Short timeout to avoid blocking the License, when the LSD is optional. + val timeout = 5.seconds.takeIf { ignoreInternetErrors } + val data = network.fetch( + url, + timeout = timeout, + headers = mapOf("Accept" to MediaType.LCP_STATUS_DOCUMENT.toString()) + ) + .getOrElse { throw LcpException(LcpError.Network(it)) } raise(Event.retrievedStatusData(data)) } @@ -334,41 +371,60 @@ internal class LicenseValidation( } private suspend fun fetchLicense(status: StatusDocument) { - val url = status.url(StatusDocument.Rel.license, preferredType = MediaType.LCP_LICENSE_DOCUMENT).toString() + val url = status.url( + StatusDocument.Rel.License, + preferredType = MediaType.LCP_LICENSE_DOCUMENT + ).toString() // Short timeout to avoid blocking the License, since it can be updated next time. val data = network.fetch(url, timeout = 5.seconds) - .getOrElse { throw LcpException.Network(it) } + .getOrElse { throw LcpException(LcpError.Network(it)) } raise(Event.retrievedLicenseData(data)) } - private fun checkLicenseStatus(license: LicenseDocument, status: StatusDocument?) { - var error: LcpException.LicenseStatus? = null + private fun checkLicenseStatus( + license: LicenseDocument, + status: StatusDocument?, + statusDocumentTakesPrecedence: Boolean + ) { + var error: LcpError.LicenseStatus? = null val now = Date() val start = license.rights.start ?: now val end = license.rights.end ?: now - if (start > now || now > end) { + val isLicenseExpired = (start > now || now > end) + val isStatusValid = status?.status in listOf( + null, + StatusDocument.Status.Active, + StatusDocument.Status.Ready + ) + + // We only check the Status Document's status if the License itself is expired, to get a proper status error message. + // But in the case where the Status Document takes precedence (eg. after a failed License update), + // then we also check the status validity. + if (isLicenseExpired || statusDocumentTakesPrecedence && !isStatusValid) { error = if (status != null) { val date = status.statusUpdated when (status.status) { - StatusDocument.Status.ready, StatusDocument.Status.active, StatusDocument.Status.expired -> + StatusDocument.Status.Ready, StatusDocument.Status.Active, StatusDocument.Status.Expired -> if (start > now) { - LcpException.LicenseStatus.NotStarted(start) + LcpError.LicenseStatus.NotStarted(start) } else { - LcpException.LicenseStatus.Expired(end) + LcpError.LicenseStatus.Expired(end) } - StatusDocument.Status.returned -> LcpException.LicenseStatus.Returned(date) - StatusDocument.Status.revoked -> { - val devicesCount = status.events(org.readium.r2.lcp.license.model.components.lsd.Event.EventType.register).size - LcpException.LicenseStatus.Revoked(date, devicesCount = devicesCount) + StatusDocument.Status.Returned -> LcpError.LicenseStatus.Returned(date) + StatusDocument.Status.Revoked -> { + val devicesCount = status.events( + org.readium.r2.lcp.license.model.components.lsd.Event.EventType.Register + ).size + LcpError.LicenseStatus.Revoked(date, devicesCount = devicesCount) } - StatusDocument.Status.cancelled -> LcpException.LicenseStatus.Cancelled(date) + StatusDocument.Status.Cancelled -> LcpError.LicenseStatus.Cancelled(date) } } else { if (start > now) { - LcpException.LicenseStatus.NotStarted(start) + LcpError.LicenseStatus.NotStarted(start) } else { - LcpException.LicenseStatus.Expired(end) + LcpError.LicenseStatus.Expired(end) } } } @@ -377,18 +433,19 @@ internal class LicenseValidation( private suspend fun requestPassphrase(license: LicenseDocument) { if (DEBUG) Timber.d("requestPassphrase") - val passphrase = passphrases.request(license, authentication, allowUserInteraction, sender) - if (passphrase == null) + val passphrase = passphrases.request(license, authentication, allowUserInteraction) + if (passphrase == null) { raise(Event.cancelled) - else + } else { raise(Event.retrievedPassphrase(passphrase)) + } } private suspend fun validateIntegrity(license: LicenseDocument, passphrase: String) { if (DEBUG) Timber.d("validateIntegrity") val profile = license.encryption.profile if (!supportedProfiles.contains(profile)) { - throw LcpException.LicenseProfileNotSupported + throw LcpException(LcpError.LicenseProfileNotSupported) } val context = LcpClient.createContext(license.json.toString(), passphrase, crl.retrieve()) raise(Event.validatedIntegrity(context)) @@ -403,17 +460,23 @@ internal class LicenseValidation( companion object { fun observe( licenseValidation: LicenseValidation, - policy: ObserverPolicy = ObserverPolicy.always, + policy: ObserverPolicy = ObserverPolicy.Always, observer: Observer ) { var notified = true when (licenseValidation.stateMachine.state) { - is State.valid -> observer((licenseValidation.stateMachine.state as State.valid).documents, null) - is State.failure -> observer(null, (licenseValidation.stateMachine.state as State.failure).error) + is State.valid -> observer( + (licenseValidation.stateMachine.state as State.valid).documents, + null + ) + is State.failure -> observer( + null, + (licenseValidation.stateMachine.state as State.failure).error + ) is State.cancelled -> observer(null, null) else -> notified = false } - if (notified && policy != ObserverPolicy.always) { + if (notified && policy != ObserverPolicy.Always) { return } observers.add(Pair(observer, policy)) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/StateMachine.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/StateMachine.kt index 6a2b7df110..569084c3ff 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/StateMachine.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/StateMachine.kt @@ -165,7 +165,11 @@ internal class StateMachine<STATE : Any, EVENT : Any> private constructor( } fun build(): Graph<STATE, EVENT> { - return Graph(requireNotNull(initialState), stateDefinitions.toMap(), onTransitionListeners.toList()) + return Graph( + requireNotNull(initialState), + stateDefinitions.toMap(), + onTransitionListeners.toList() + ) } inner class StateDefinitionBuilder<S : STATE> { diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/BytesLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/BytesLicenseContainer.kt index 0692e18c46..a512348179 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/BytesLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/BytesLicenseContainer.kt @@ -14,11 +14,11 @@ import org.readium.r2.lcp.license.model.LicenseDocument /** * Access a License Document from its raw bytes. */ -internal class BytesLicenseContainer(private var bytes: ByteArray) : LicenseContainer { +internal class BytesLicenseContainer(private var bytes: ByteArray) : WritableLicenseContainer { override fun read(): ByteArray = bytes override fun write(license: LicenseDocument) { - bytes = license.data + bytes = license.toByteArray() } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt new file mode 100644 index 0000000000..21889e7fe8 --- /dev/null +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.lcp.license.container + +import kotlinx.coroutines.runBlocking +import org.readium.r2.lcp.LcpError +import org.readium.r2.lcp.LcpException +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.resource.Resource + +/** + * Access to a License Document stored in a read-only container. + */ +internal class ContainerLicenseContainer( + private val container: Container<Resource>, + private val entryUrl: Url +) : LicenseContainer { + + override fun read(): ByteArray { + return runBlocking { + val resource = container.get(entryUrl) + ?: throw LcpException(LcpError.Container.FileNotFound(entryUrl)) + + resource.read() + .mapFailure { + LcpException(LcpError.Container.ReadFailed(entryUrl)) + } + .getOrThrow() + } + } +} diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt new file mode 100644 index 0000000000..a09ab849af --- /dev/null +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.lcp.license.container + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import java.io.ByteArrayInputStream +import java.io.File +import java.io.FileOutputStream +import java.util.UUID +import java.util.zip.ZipFile +import org.readium.r2.lcp.LcpError +import org.readium.r2.lcp.LcpException +import org.readium.r2.lcp.license.model.LicenseDocument +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.toUri + +internal class ContentZipLicenseContainer( + context: Context, + private val container: Container<Resource>, + private val pathInZip: Url +) : LicenseContainer by ContainerLicenseContainer(container, pathInZip), WritableLicenseContainer { + + private val zipUri: Uri = + requireNotNull(container.sourceUrl).toUri() + + private val contentResolver: ContentResolver = + context.contentResolver + + private val cache: File = + context.externalCacheDir ?: context.cacheDir + + override fun write(license: LicenseDocument) { + try { + val tmpZip = File(cache, UUID.randomUUID().toString()) + contentResolver.openInputStream(zipUri) + ?.use { it.copyTo(FileOutputStream(tmpZip)) } + ?: throw LcpException(LcpError.Container.WriteFailed(pathInZip)) + val tmpZipFile = ZipFile(tmpZip) + + val outStream = contentResolver.openOutputStream(zipUri, "wt") + ?: throw LcpException(LcpError.Container.WriteFailed(pathInZip)) + tmpZipFile.addOrReplaceEntry( + pathInZip.toString(), + ByteArrayInputStream(license.toByteArray()), + outStream + ) + + outStream.close() + tmpZipFile.close() + tmpZip.delete() + } catch (e: Exception) { + throw LcpException(LcpError.Container.WriteFailed(pathInZip)) + } + } +} diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/EPUBLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/EPUBLicenseContainer.kt deleted file mode 100644 index c44bc7a0f2..0000000000 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/EPUBLicenseContainer.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Module: r2-lcp-kotlin - * Developers: Aferdita Muriqi, Mickaël Menu - * - * Copyright (c) 2019. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.lcp.license.container - -/** - * Access a License Document stored in an EPUB archive, under META-INF/license.lcpl. - */ -internal class EPUBLicenseContainer(epub: String) : - ZIPLicenseContainer(zip = epub, pathInZIP = "META-INF/license.lcpl") diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/FileUtil.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/FileUtil.kt new file mode 100644 index 0000000000..31b389fed0 --- /dev/null +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/FileUtil.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.lcp.license.container + +import java.io.File +internal fun File.moveTo(target: File) { + if (!this.renameTo(target)) { + this.copyTo(target, overwrite = true) + this.delete() + } +} diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ZIPLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/FileZipLicenseContainer.kt similarity index 56% rename from readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ZIPLicenseContainer.kt rename to readium/lcp/src/main/java/org/readium/r2/lcp/license/container/FileZipLicenseContainer.kt index a3115aa5f8..c30897b07c 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ZIPLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/FileZipLicenseContainer.kt @@ -9,35 +9,38 @@ package org.readium.r2.lcp.license.container +import java.io.ByteArrayInputStream import java.io.File import java.util.zip.ZipFile +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument -import org.zeroturnaround.zip.ZipUtil +import org.readium.r2.shared.util.Url /** * Access to a License Document stored in a ZIP archive. - * Meant to be subclassed to customize the pathInZIP property, eg. [EPUBLicenseContainer]. */ -internal open class ZIPLicenseContainer(private val zip: String, private val pathInZIP: String) : LicenseContainer { +internal class FileZipLicenseContainer( + private val zip: String, + private val pathInZIP: Url +) : WritableLicenseContainer { override fun read(): ByteArray { - val archive = try { ZipFile(zip) } catch (e: Exception) { - throw LcpException.Container.OpenFailed + throw LcpException(LcpError.Container.OpenFailed) } val entry = try { - archive.getEntry(pathInZIP) + archive.getEntry(pathInZIP.toString())!! } catch (e: Exception) { - throw LcpException.Container.FileNotFound(pathInZIP) + throw LcpException(LcpError.Container.FileNotFound(pathInZIP)) } return try { archive.getInputStream(entry).readBytes() } catch (e: Exception) { - throw LcpException.Container.ReadFailed(pathInZIP) + throw LcpException(LcpError.Container.ReadFailed(pathInZIP)) } } @@ -45,16 +48,16 @@ internal open class ZIPLicenseContainer(private val zip: String, private val pat try { val source = File(zip) val tmpZip = File("$zip.tmp") - tmpZip.delete() - source.copyTo(tmpZip) - source.delete() - if (ZipUtil.containsEntry(tmpZip, pathInZIP)) { - ZipUtil.removeEntry(tmpZip, pathInZIP) - } - ZipUtil.addEntry(tmpZip, pathInZIP, license.data, source) - tmpZip.delete() + val zipFile = ZipFile(source) + zipFile.addOrReplaceEntry( + pathInZIP.toString(), + ByteArrayInputStream(license.toByteArray()), + tmpZip + ) + zipFile.close() + tmpZip.moveTo(source) } catch (e: Exception) { - throw LcpException.Container.WriteFailed(pathInZIP) + throw LcpException(LcpError.Container.WriteFailed(pathInZIP)) } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LCPLLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplLicenseContainer.kt similarity index 65% rename from readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LCPLLicenseContainer.kt rename to readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplLicenseContainer.kt index b5dd5590aa..8ba1fcd33f 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LCPLLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplLicenseContainer.kt @@ -6,29 +6,32 @@ * Use of this source code is governed by a BSD-style license which is detailed in the * LICENSE file present in the project repository where this source code is maintained. */ + package org.readium.r2.lcp.license.container import java.io.File +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument +import org.readium.r2.shared.util.toUrl /** * Access a License Document stored in an LCP License Document file (LCPL). */ -internal class LCPLLicenseContainer(private val lcpl: String) : LicenseContainer { +internal class LcplLicenseContainer(private val licenseFile: File) : WritableLicenseContainer { override fun read(): ByteArray = try { - File(lcpl).readBytes() + licenseFile.readBytes() } catch (e: Exception) { - throw LcpException.Container.OpenFailed + throw LcpException(LcpError.Container.OpenFailed) } override fun write(license: LicenseDocument) { try { - File(lcpl).writeBytes(license.data) + licenseFile.writeBytes(license.toByteArray()) } catch (e: Exception) { - throw LcpException.Container.WriteFailed(lcpl) + throw LcpException(LcpError.Container.WriteFailed(licenseFile.toUrl())) } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt new file mode 100644 index 0000000000..8d5fa01c50 --- /dev/null +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt @@ -0,0 +1,29 @@ +/* + * Module: r2-lcp-kotlin + * Developers: Aferdita Muriqi, Mickaël Menu + * + * Copyright (c) 2019. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.lcp.license.container + +import kotlinx.coroutines.runBlocking +import org.readium.r2.lcp.LcpError +import org.readium.r2.lcp.LcpException +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.resource.Resource + +/** + * Access a License Document stored in an LCP License Document file (LCPL) readable through a + * [Resource]. + */ +internal class LcplResourceLicenseContainer(private val resource: Resource) : LicenseContainer { + + override fun read(): ByteArray = + runBlocking { + resource.read() + .getOrElse { throw LcpException(LcpError.Container.OpenFailed) } + } +} diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt index c65cc3c04d..b966d1b387 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt @@ -9,9 +9,23 @@ package org.readium.r2.lcp.license.container +import android.content.Context +import java.io.File +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.format.EpubSpecification +import org.readium.r2.shared.util.format.FormatSpecification +import org.readium.r2.shared.util.format.LcpLicenseSpecification +import org.readium.r2.shared.util.resource.Resource + +private val LICENSE_IN_EPUB = Url("META-INF/license.lcpl")!! +private val LICENSE_IN_RPF = Url("license.lcpl")!! /** * Encapsulates the read/write access to the packaged License Document (eg. in an EPUB container, @@ -19,22 +33,72 @@ import org.readium.r2.shared.util.mediatype.MediaType */ internal interface LicenseContainer { fun read(): ByteArray +} + +internal interface WritableLicenseContainer : LicenseContainer { fun write(license: LicenseDocument) } -internal suspend fun createLicenseContainer( - filepath: String, - mediaTypes: List<String> = emptyList() +internal fun createLicenseContainer( + file: File, + formatSpecification: FormatSpecification +): WritableLicenseContainer = + when { + formatSpecification.conformsTo(EpubSpecification) -> FileZipLicenseContainer( + file.path, + LICENSE_IN_EPUB + ) + formatSpecification.conformsTo(LcpLicenseSpecification) -> LcplLicenseContainer(file) + // Assuming it's a Readium WebPub package (e.g. audiobook, LCPDF, etc.) as a fallback + else -> FileZipLicenseContainer(file.path, LICENSE_IN_RPF) + } + +internal fun createLicenseContainer( + context: Context, + asset: Asset +): LicenseContainer = + when (asset) { + is ResourceAsset -> createLicenseContainer(asset.resource, asset.format.specification) + is ContainerAsset -> createLicenseContainer( + context, + asset.container, + asset.format.specification + ) + } + +internal fun createLicenseContainer( + resource: Resource, + formatSpecification: FormatSpecification ): LicenseContainer { - val mediaType = MediaType.ofFile(filepath, mediaTypes = mediaTypes, fileExtensions = emptyList()) - ?: throw LcpException.Container.OpenFailed - return createLicenseContainer(filepath, mediaType) + if (!formatSpecification.conformsTo(LcpLicenseSpecification)) { + throw LcpException(LcpError.Container.OpenFailed) + } + + return when { + resource.sourceUrl?.isFile == true -> + LcplLicenseContainer(resource.sourceUrl!!.toFile()!!) + else -> + LcplResourceLicenseContainer(resource) + } } -internal fun createLicenseContainer(filepath: String, mediaType: MediaType): LicenseContainer = - when (mediaType) { - MediaType.EPUB -> EPUBLicenseContainer(filepath) - MediaType.LCP_LICENSE_DOCUMENT -> LCPLLicenseContainer(filepath) +internal fun createLicenseContainer( + context: Context, + container: Container<Resource>, + formatSpecification: FormatSpecification +): LicenseContainer { + val licensePath = when { + formatSpecification.conformsTo(EpubSpecification) -> LICENSE_IN_EPUB // Assuming it's a Readium WebPub package (e.g. audiobook, LCPDF, etc.) as a fallback - else -> WebPubLicenseContainer(filepath) + else -> LICENSE_IN_RPF } + + return when { + container.sourceUrl?.isFile == true -> + FileZipLicenseContainer(container.sourceUrl!!.path!!, licensePath) + container.sourceUrl?.isContent == true -> + ContentZipLicenseContainer(context, container, licensePath) + else -> + ContainerLicenseContainer(container, licensePath) + } +} diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/WebPubLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/WebPubLicenseContainer.kt deleted file mode 100644 index c7033c224c..0000000000 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/WebPubLicenseContainer.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Module: r2-lcp-kotlin - * Developers: Mickaël Menu - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.lcp.license.container - -/** - * Access a License Document stored in a Readium WebPub package (e.g. WebPub, Audiobook, LCPDF or DiViNa). - */ -internal class WebPubLicenseContainer(path: String) : - ZIPLicenseContainer(zip = path, pathInZIP = "license.lcpl") diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ZipUtil.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ZipUtil.kt new file mode 100644 index 0000000000..fdcfc35c57 --- /dev/null +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ZipUtil.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.lcp.license.container + +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipOutputStream + +internal fun ZipFile.addOrReplaceEntry( + name: String, + inputStream: InputStream, + dest: File +) { + addOrReplaceEntry(name, inputStream, FileOutputStream(dest)) +} + +internal fun ZipFile.addOrReplaceEntry( + name: String, + inputStream: InputStream, + dest: OutputStream +) { + val outZip = ZipOutputStream(dest) + var entryAdded = false + + val newEntry = ZipEntry(name) + newEntry.method = ZipEntry.DEFLATED + getEntry(name)?.let { originalEntry -> + newEntry.extra = originalEntry.extra + newEntry.comment = originalEntry.comment + } + + for (entry in entries()) { + if (entry.name == name) { + addEntry(newEntry, inputStream, outZip) + entryAdded = true + } else { + copyEntry(entry.copy(), this, outZip) + } + } + + if (!entryAdded) { + addEntry(newEntry, inputStream, outZip) + } + + outZip.finish() + outZip.close() +} + +private fun ZipEntry.copy(): ZipEntry { + val copy = ZipEntry(name) + if (crc != -1L) { + copy.crc = crc + } + if (method != -1) { + copy.method = method + } + if (size >= 0) { + copy.size = size + } + if (extra != null) { + copy.extra = extra + } + copy.comment = comment + copy.time = time + return copy +} + +/** + * If STORED method is used, entry must contain CRC and size. + */ +private fun addEntry( + entry: ZipEntry, + source: InputStream, + outStream: ZipOutputStream +) { + outStream.putNextEntry(entry) + source.copyTo(outStream) + outStream.closeEntry() +} + +private fun copyEntry( + entry: ZipEntry, + srcZip: ZipFile, + outStream: ZipOutputStream +) { + addEntry(entry, srcZip.getInputStream(entry), outStream) +} diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/LicenseDocument.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/LicenseDocument.kt index 0843124d27..2e584b2c13 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/LicenseDocument.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/LicenseDocument.kt @@ -9,10 +9,10 @@ package org.readium.r2.lcp.license.model -import java.net.URL import java.nio.charset.Charset import java.util.* import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.components.Link import org.readium.r2.lcp.license.model.components.Links @@ -23,68 +23,137 @@ import org.readium.r2.lcp.license.model.components.lcp.User import org.readium.r2.lcp.service.URLParameters import org.readium.r2.shared.extensions.iso8601ToDate import org.readium.r2.shared.extensions.optNullableString +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType -class LicenseDocument(val data: ByteArray) { - val provider: String - val id: String - val issued: Date - val updated: Date - val encryption: Encryption - val links: Links - val user: User - val rights: Rights - val signature: Signature - val json: JSONObject - - enum class Rel(val rawValue: String) { - hint("hint"), - publication("publication"), - self("self"), - support("support"), - status("status"); - - companion object { - operator fun invoke(rawValue: String) = values().firstOrNull { it.rawValue == rawValue } +public class LicenseDocument internal constructor(public val json: JSONObject) { + + public companion object { + + public fun fromJSON(json: JSONObject): Try<LicenseDocument, LcpError.Parsing> { + val document = try { + LicenseDocument(json) + } catch (e: Exception) { + check(e is LcpException) + check(e.error is LcpError.Parsing) + return Try.failure(e.error) + } + + return Try.success(document) + } + + public fun fromBytes(data: ByteArray): Try<LicenseDocument, LcpError.Parsing> { + val json = try { + JSONObject(data.decodeToString()) + } catch (e: Exception) { + return Try.failure(LcpError.Parsing.MalformedJSON) + } + + return fromJSON(json) } } + public val provider: String = + json.optNullableString("provider") + ?: throw LcpException(LcpError.Parsing.LicenseDocument) + + public val id: String = + json.optNullableString("id") + ?: throw LcpException(LcpError.Parsing.LicenseDocument) + + public val issued: Date = + json.optNullableString("issued") + ?.iso8601ToDate() + ?: throw LcpException(LcpError.Parsing.LicenseDocument) + + public val updated: Date = + json.optNullableString("updated") + ?.iso8601ToDate() + ?: issued + + public val encryption: Encryption = + json.optJSONObject("encryption") + ?.let { Encryption(it) } + ?: throw LcpException(LcpError.Parsing.LicenseDocument) + + public val links: Links = + json.optJSONArray("links") + ?.let { Links(it) } + ?: throw LcpException(LcpError.Parsing.LicenseDocument) + + public val user: User = + User(json.optJSONObject("user") ?: JSONObject()) + + public val rights: Rights = + Rights(json.optJSONObject("rights") ?: JSONObject()) + + public val signature: Signature = + json.optJSONObject("signature") + ?.let { Signature(it) } + ?: throw LcpException(LcpError.Parsing.LicenseDocument) + init { + if (link(Rel.Hint) == null || link(Rel.Publication) == null) { + throw LcpException(LcpError.Parsing.LicenseDocument) + } + + // Check that the acquisition link has a valid URL. try { - json = JSONObject(data.toString(Charset.defaultCharset())) + link(Rel.Publication)!!.url() as AbsoluteUrl } catch (e: Exception) { - throw LcpException.Parsing.MalformedJSON + throw LcpException(LcpError.Parsing.Url(rel = LicenseDocument.Rel.Publication.value)) } + } - provider = json.optNullableString("provider") ?: throw LcpException.Parsing.LicenseDocument - id = json.optNullableString("id") ?: throw LcpException.Parsing.LicenseDocument - issued = json.optNullableString("issued")?.iso8601ToDate() ?: throw LcpException.Parsing.LicenseDocument - encryption = json.optJSONObject("encryption")?.let { Encryption(it) } ?: throw LcpException.Parsing.LicenseDocument - signature = json.optJSONObject("signature")?.let { Signature(it) } ?: throw LcpException.Parsing.LicenseDocument - links = json.optJSONArray("links")?.let { Links(it) } ?: throw LcpException.Parsing.LicenseDocument - updated = json.optNullableString("updated")?.iso8601ToDate() ?: issued - user = User(json.optJSONObject("user") ?: JSONObject()) - rights = Rights(json.optJSONObject("rights") ?: JSONObject()) - - if (link(Rel.hint) == null || link(Rel.publication) == null) { - throw LcpException.Parsing.LicenseDocument + internal constructor(data: ByteArray) : this( + try { + JSONObject(data.decodeToString()) + } catch (e: Exception) { + throw LcpException(LcpError.Parsing.MalformedJSON) + } + ) + + public enum class Rel(public val value: String) { + Hint("hint"), + Publication("publication"), + Self("self"), + Support("support"), + Status("status"); + + @Deprecated("Use [value] instead", ReplaceWith("value"), level = DeprecationLevel.ERROR) + public val rawValue: String get() = value + + public companion object { + public operator fun invoke(value: String): Rel? = values().firstOrNull { it.value == value } } } - fun link(rel: Rel, type: MediaType? = null): Link? = - links.firstWithRel(rel.rawValue, type) + public val publicationLink: Link + get() = link(Rel.Publication)!! + + public fun link(rel: Rel, type: MediaType? = null): Link? = + links.firstWithRel(rel.value, type) - fun links(rel: Rel, type: MediaType? = null): List<Link> = - links.allWithRel(rel.rawValue, type) + public fun links(rel: Rel, type: MediaType? = null): List<Link> = + links.allWithRel(rel.value, type) - fun url(rel: Rel, preferredType: MediaType? = null, parameters: URLParameters = emptyMap()): URL { + public fun url( + rel: Rel, + preferredType: MediaType? = null, + parameters: URLParameters = emptyMap() + ): Url { val link = link(rel, preferredType) - ?: links.firstWithRelAndNoType(rel.rawValue) - ?: throw LcpException.Parsing.Url(rel = rel.rawValue) + ?: links.firstWithRelAndNoType(rel.value) + ?: throw LcpException(LcpError.Parsing.Url(rel = rel.value)) - return link.url(parameters) + return link.url(parameters = parameters) } - val description: String + public val description: String get() = "License($id)" + + public fun toByteArray(): ByteArray = + json.toString().toByteArray(Charset.defaultCharset()) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/StatusDocument.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/StatusDocument.kt index b15c8671f0..aea56dd363 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/StatusDocument.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/StatusDocument.kt @@ -9,10 +9,10 @@ package org.readium.r2.lcp.license.model -import java.net.URL import java.nio.charset.Charset import java.util.* import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.components.Link import org.readium.r2.lcp.license.model.components.Links @@ -22,41 +22,48 @@ import org.readium.r2.lcp.service.URLParameters import org.readium.r2.shared.extensions.iso8601ToDate import org.readium.r2.shared.extensions.mapNotNull import org.readium.r2.shared.extensions.optNullableString +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType -class StatusDocument(val data: ByteArray) { - val id: String - val status: Status - val message: String - val licenseUpdated: Date - val statusUpdated: Date - val links: Links - val potentialRights: PotentialRights? - val events: List<Event> - - val json: JSONObject - - enum class Status(val rawValue: String) { - ready("ready"), - active("active"), - revoked("revoked"), - returned("returned"), - cancelled("cancelled"), - expired("expired"); - - companion object { - operator fun invoke(rawValue: String) = values().firstOrNull { it.rawValue == rawValue } +public class StatusDocument(public val data: ByteArray) { + public val id: String + public val status: Status + public val message: String + public val licenseUpdated: Date + public val statusUpdated: Date + public val links: Links + public val potentialRights: PotentialRights? + public val events: List<Event> + + public val json: JSONObject + + public enum class Status(public val value: String) { + Ready("ready"), + Active("active"), + Revoked("revoked"), + Returned("returned"), + Cancelled("cancelled"), + Expired("expired"); + + @Deprecated("Use [value] instead", ReplaceWith("value"), level = DeprecationLevel.ERROR) + public val rawValue: String get() = value + + public companion object { + public operator fun invoke(value: String): Status? = values().firstOrNull { it.value == value } } } - enum class Rel(val rawValue: String) { - register("register"), - license("license"), - `return`("return"), - renew("renew"); + public enum class Rel(public val value: String) { + Register("register"), + License("license"), + Return("return"), + Renew("renew"); - companion object { - operator fun invoke(rawValue: String) = values().firstOrNull { it.rawValue == rawValue } + @Deprecated("Use [value] instead", ReplaceWith("value"), level = DeprecationLevel.ERROR) + public val rawValue: String get() = value + + public companion object { + public operator fun invoke(value: String): Rel? = values().firstOrNull { it.value == value } } } @@ -64,18 +71,28 @@ class StatusDocument(val data: ByteArray) { try { json = JSONObject(data.toString(Charset.defaultCharset())) } catch (e: Exception) { - throw LcpException.Parsing.MalformedJSON + throw LcpException(LcpError.Parsing.MalformedJSON) } - id = json.optNullableString("id") ?: throw LcpException.Parsing.StatusDocument - status = json.optNullableString("status")?.let { Status(it) } ?: throw LcpException.Parsing.StatusDocument - message = json.optNullableString("message") ?: throw LcpException.Parsing.StatusDocument + id = json.optNullableString("id") ?: throw LcpException(LcpError.Parsing.StatusDocument) + status = json.optNullableString("status")?.let { Status(it) } ?: throw LcpException( + LcpError.Parsing.StatusDocument + ) + message = json.optNullableString("message") ?: throw LcpException( + LcpError.Parsing.StatusDocument + ) val updated = json.optJSONObject("updated") ?: JSONObject() - licenseUpdated = updated.optNullableString("license")?.iso8601ToDate() ?: throw LcpException.Parsing.StatusDocument - statusUpdated = updated.optNullableString("status")?.iso8601ToDate() ?: throw LcpException.Parsing.StatusDocument + licenseUpdated = updated.optNullableString("license")?.iso8601ToDate() ?: throw LcpException( + LcpError.Parsing.StatusDocument + ) + statusUpdated = updated.optNullableString("status")?.iso8601ToDate() ?: throw LcpException( + LcpError.Parsing.StatusDocument + ) - links = json.optJSONArray("links")?.let { Links(it) } ?: throw LcpException.Parsing.StatusDocument + links = json.optJSONArray("links")?.let { Links(it) } ?: throw LcpException( + LcpError.Parsing.StatusDocument + ) potentialRights = json.optJSONObject("potential_rights")?.let { PotentialRights(it) } @@ -86,29 +103,33 @@ class StatusDocument(val data: ByteArray) { ?: emptyList() } - fun link(rel: Rel, type: MediaType? = null): Link? = - links.firstWithRel(rel.rawValue, type) + public fun link(rel: Rel, type: MediaType? = null): Link? = + links.firstWithRel(rel.value, type) - fun links(rel: Rel, type: MediaType? = null): List<Link> = - links.allWithRel(rel.rawValue, type) + public fun links(rel: Rel, type: MediaType? = null): List<Link> = + links.allWithRel(rel.value, type) internal fun linkWithNoType(rel: Rel): Link? = - links.firstWithRelAndNoType(rel.rawValue) + links.firstWithRelAndNoType(rel.value) - fun url(rel: Rel, preferredType: MediaType? = null, parameters: URLParameters = emptyMap()): URL { + public fun url( + rel: Rel, + preferredType: MediaType? = null, + parameters: URLParameters = emptyMap() + ): Url { val link = link(rel, preferredType) ?: linkWithNoType(rel) - ?: throw LcpException.Parsing.Url(rel = rel.rawValue) + ?: throw LcpException(LcpError.Parsing.Url(rel = rel.value)) - return link.url(parameters) + return link.url(parameters = parameters) } - fun events(type: Event.EventType): List<Event> = - events(type.rawValue) + public fun events(type: Event.EventType): List<Event> = + events(type.value) - fun events(type: String): List<Event> = + public fun events(type: String): List<Event> = events.filter { it.type == type } - val description: String - get() = "Status(${status.rawValue})" + public val description: String + get() = "Status(${status.value})" } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt index b001bf4315..c322e8d0f7 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt @@ -10,74 +10,74 @@ package org.readium.r2.lcp.license.model.components -import java.net.URL -import org.json.JSONArray import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException -import org.readium.r2.lcp.service.URLParameters -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.util.URITemplate +import org.readium.r2.shared.extensions.optNullableInt +import org.readium.r2.shared.extensions.optNullableString +import org.readium.r2.shared.extensions.optStringsFromArrayOrSingle +import org.readium.r2.shared.publication.Href +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType -data class Link(val json: JSONObject) { - val href: String - var rel = mutableListOf<String>() - val title: String? - val type: String? - val templated: Boolean - val profile: String? - val length: Int? - val hash: String? +public data class Link( + val href: Href, + val mediaType: MediaType? = null, + val title: String? = null, + val rels: Set<String> = setOf(), + val profile: String? = null, + val length: Int? = null, + val hash: String? = null +) { - init { - - href = if (json.has("href")) json.getString("href") else throw LcpException.Parsing.Link - - if (json.has("rel")) { - val rel = json["rel"] - if (rel is String) { - this.rel.add(rel) - } else if (rel is JSONArray) { - for (i in 0 until rel.length()) { - this.rel.add(rel[i].toString()) + public companion object { + public operator fun invoke( + json: JSONObject + ): Link { + val href = json.optNullableString("href") + ?.let { + Href( + href = it, + templated = json.optBoolean("templated", false) + ) } - } - } + ?: throw LcpException(LcpError.Parsing.Link) - if (rel.isEmpty()) { - throw LcpException.Parsing.Link + return Link( + href = href, + mediaType = json.optNullableString("type") + ?.let { MediaType(it) }, + title = json.optNullableString("title"), + rels = json.optStringsFromArrayOrSingle("rel").toSet() + .takeIf { it.isNotEmpty() } + ?: throw LcpException(LcpError.Parsing.Link), + profile = json.optNullableString("profile"), + length = json.optNullableInt("length"), + hash = json.optNullableString("hash") + ) } - - title = if (json.has("title")) json.getString("title") else null - type = if (json.has("type")) json.getString("type") else null - templated = if (json.has("templated")) json.getBoolean("templated") else false - profile = if (json.has("profile")) json.getString("profile") else null - length = if (json.has("length")) json.getInt("length") else null - hash = if (json.has("hash")) json.getString("hash") else null } - fun url(parameters: URLParameters): URL { - if (!templated) { - return URL(href) - } - - val expandedHref = URITemplate(href).expand(parameters.mapValues { it.value ?: "" }) - return URL(expandedHref) - } - - val url: URL - get() = url(parameters = emptyMap()) - - val mediaType: MediaType - get() = type?.let { MediaType.parse(it) } ?: MediaType.BINARY - /** - * List of URI template parameter keys, if the [Link] is templated. + * Returns the URL represented by this link's HREF. + * + * If the HREF is a template, the [parameters] are used to expand it according to RFC 6570. */ - internal val templateParameters: List<String> by lazy { - if (!templated) - emptyList() - else - URITemplate(href).parameters - } + public fun url( + parameters: Map<String, String> = emptyMap() + ): Url = href.resolve(parameters = parameters) + + @Deprecated( + "Use [mediaType.toString()] instead", + ReplaceWith("mediaType.toString()"), + level = DeprecationLevel.ERROR + ) + public val type: String? get() = throw NotImplementedError() + + @Deprecated( + "Renamed `rels`", + ReplaceWith("rels"), + level = DeprecationLevel.ERROR + ) + public val rel: List<String> get() = throw NotImplementedError() } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Links.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Links.kt index 02fda8880b..749783bcdb 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Links.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Links.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.extensions.mapNotNull import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.util.mediatype.MediaType -data class Links(val json: JSONArray) { +public data class Links(val json: JSONArray) { val links: List<Link> = json .mapNotNull { item -> @@ -24,17 +24,17 @@ data class Links(val json: JSONArray) { } } - fun firstWithRel(rel: String, type: MediaType? = null): Link? = + public fun firstWithRel(rel: String, type: MediaType? = null): Link? = links.firstOrNull { it.matches(rel, type) } internal fun firstWithRelAndNoType(rel: String): Link? = - links.firstOrNull { it.rel.contains(rel) && it.type == null } + links.firstOrNull { it.rels.contains(rel) && it.mediaType == null } - fun allWithRel(rel: String, type: MediaType? = null): List<Link> = + public fun allWithRel(rel: String, type: MediaType? = null): List<Link> = links.filter { it.matches(rel, type) } - private fun Link.matches(rel: String, type: MediaType?): Boolean = - this.rel.contains(rel) && (type?.matches(this.type) ?: true) + private fun Link.matches(rel: String, mediaType: MediaType?): Boolean = + this.rels.contains(rel) && (mediaType?.matches(this.mediaType) ?: true) - operator fun get(rel: String): List<Link> = allWithRel(rel) + public operator fun get(rel: String): List<Link> = allWithRel(rel) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/ContentKey.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/ContentKey.kt index c28dbe6dfb..56e5e6a50b 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/ContentKey.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/ContentKey.kt @@ -10,14 +10,27 @@ package org.readium.r2.lcp.license.model.components.lcp import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException -data class ContentKey(val json: JSONObject) { +public data class ContentKey(val json: JSONObject) { val algorithm: String val encryptedValue: String init { - algorithm = if (json.has("algorithm")) json.getString("algorithm") else throw LcpException.Parsing.Encryption - encryptedValue = if (json.has("encrypted_value")) json.getString("encrypted_value") else throw LcpException.Parsing.Encryption + algorithm = if (json.has("algorithm")) { + json.getString("algorithm") + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } + encryptedValue = if (json.has("encrypted_value")) { + json.getString("encrypted_value") + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Encryption.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Encryption.kt index d25f1366bd..0899a3f8fa 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Encryption.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Encryption.kt @@ -10,16 +10,35 @@ package org.readium.r2.lcp.license.model.components.lcp import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException -data class Encryption(val json: JSONObject) { +public data class Encryption(val json: JSONObject) { val profile: String val contentKey: ContentKey val userKey: UserKey init { - profile = if (json.has("profile")) json.getString("profile") else throw LcpException.Parsing.Encryption - contentKey = if (json.has("content_key")) ContentKey(json.getJSONObject("content_key")) else throw LcpException.Parsing.Encryption - userKey = if (json.has("user_key")) UserKey(json.getJSONObject("user_key")) else throw LcpException.Parsing.Encryption + profile = if (json.has("profile")) { + json.getString("profile") + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } + contentKey = if (json.has("content_key")) { + ContentKey(json.getJSONObject("content_key")) + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } + userKey = if (json.has("user_key")) { + UserKey(json.getJSONObject("user_key")) + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Rights.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Rights.kt index 104cdde54e..a3ca648390 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Rights.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Rights.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.extensions.iso8601ToDate import org.readium.r2.shared.extensions.optNullableInt import org.readium.r2.shared.extensions.optNullableString -data class Rights(val json: JSONObject) { +public data class Rights(val json: JSONObject) { val print: Int? val copy: Int? val start: Date? diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Signature.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Signature.kt index a8dd863f80..e17429c077 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Signature.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Signature.kt @@ -10,11 +10,18 @@ package org.readium.r2.lcp.license.model.components.lcp import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.extensions.optNullableString -data class Signature(val json: JSONObject) { - val algorithm: String = json.optNullableString("algorithm") ?: throw LcpException.Parsing.Signature - val certificate: String = json.optNullableString("certificate") ?: throw LcpException.Parsing.Signature - val value: String = json.optNullableString("value") ?: throw LcpException.Parsing.Signature +public data class Signature(val json: JSONObject) { + val algorithm: String = json.optNullableString("algorithm") ?: throw LcpException( + LcpError.Parsing.Signature + ) + val certificate: String = json.optNullableString("certificate") ?: throw LcpException( + LcpError.Parsing.Signature + ) + val value: String = json.optNullableString("value") ?: throw LcpException( + LcpError.Parsing.Signature + ) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/User.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/User.kt index ad5a5081fc..9411d85575 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/User.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/User.kt @@ -12,12 +12,12 @@ package org.readium.r2.lcp.license.model.components.lcp import org.json.JSONObject -data class User(val json: JSONObject) { +public data class User(val json: JSONObject) { val id: String? val email: String? val name: String? var extensions: JSONObject - var encrypted = mutableListOf<String>() + var encrypted: MutableList<String> = mutableListOf<String>() init { id = if (json.has("id")) json.getString("id") else null diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/UserKey.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/UserKey.kt index 8e55c83548..9b624d86b1 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/UserKey.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/UserKey.kt @@ -10,16 +10,35 @@ package org.readium.r2.lcp.license.model.components.lcp import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException -data class UserKey(val json: JSONObject) { +public data class UserKey(val json: JSONObject) { val textHint: String val algorithm: String val keyCheck: String init { - textHint = if (json.has("text_hint")) json.getString("text_hint") else throw LcpException.Parsing.Encryption - algorithm = if (json.has("algorithm")) json.getString("algorithm") else throw LcpException.Parsing.Encryption - keyCheck = if (json.has("key_check")) json.getString("key_check") else throw LcpException.Parsing.Encryption + textHint = if (json.has("text_hint")) { + json.getString("text_hint") + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } + algorithm = if (json.has("algorithm")) { + json.getString("algorithm") + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } + keyCheck = if (json.has("key_check")) { + json.getString("key_check") + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lsd/Event.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lsd/Event.kt index 291c175d21..8664ca9836 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lsd/Event.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lsd/Event.kt @@ -14,21 +14,24 @@ import org.json.JSONObject import org.readium.r2.shared.extensions.iso8601ToDate import org.readium.r2.shared.extensions.optNullableString -data class Event(val json: JSONObject) { +public data class Event(val json: JSONObject) { val type: String = json.optNullableString("type") ?: "" val name: String = json.optNullableString("name") ?: "" val id: String = json.optNullableString("id") ?: "" val date: Date? = json.optNullableString("timestamp")?.iso8601ToDate() - enum class EventType(val rawValue: String) { - register("register"), - renew("renew"), - `return`("return"), - revoke("revoke"), - cancel("cancel"); + public enum class EventType(public val value: String) { + Register("register"), + Renew("renew"), + Return("return"), + Revoke("revoke"), + Cancel("cancel"); - companion object { - operator fun invoke(rawValue: String) = values().firstOrNull { it.rawValue == rawValue } + @Deprecated("Use [value] instead", ReplaceWith("value"), level = DeprecationLevel.ERROR) + public val rawValue: String get() = value + + public companion object { + public operator fun invoke(value: String): EventType? = values().firstOrNull { it.value == value } } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lsd/PotentialRights.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lsd/PotentialRights.kt index 519740b860..389fc23fa3 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lsd/PotentialRights.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lsd/PotentialRights.kt @@ -14,6 +14,6 @@ import org.json.JSONObject import org.readium.r2.shared.extensions.iso8601ToDate import org.readium.r2.shared.extensions.optNullableString -data class PotentialRights(val json: JSONObject) { +public data class PotentialRights(val json: JSONObject) { val end: Date? = json.optNullableString("end")?.iso8601ToDate() } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/LcpDao.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/LcpDao.kt index 80f6b22975..c4c0b37c46 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/LcpDao.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/LcpDao.kt @@ -4,18 +4,24 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction +import kotlinx.coroutines.flow.Flow @Dao -interface LcpDao { +internal interface LcpDao { /** * Retrieve passphrase * @return Passphrase */ - @Query("SELECT ${Passphrase.PASSPHRASE} FROM ${Passphrase.TABLE_NAME} WHERE ${Passphrase.PROVIDER} = :licenseId") + @Query( + "SELECT ${Passphrase.PASSPHRASE} FROM ${Passphrase.TABLE_NAME} WHERE ${Passphrase.PROVIDER} = :licenseId" + ) suspend fun passphrase(licenseId: String): String? - @Query("SELECT ${Passphrase.PASSPHRASE} FROM ${Passphrase.TABLE_NAME} WHERE ${Passphrase.USERID} = :userId") + @Query( + "SELECT ${Passphrase.PASSPHRASE} FROM ${Passphrase.TABLE_NAME} WHERE ${Passphrase.USERID} = :userId" + ) suspend fun passphrases(userId: String): List<String> @Query("SELECT ${Passphrase.PASSPHRASE} FROM ${Passphrase.TABLE_NAME}") @@ -24,27 +30,83 @@ interface LcpDao { @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun addPassphrase(passphrase: Passphrase) - @Query("SELECT ${License.LICENSE_ID} FROM ${License.TABLE_NAME} WHERE ${License.LICENSE_ID} = :licenseId") + @Query( + "SELECT ${License.LICENSE_ID} FROM ${License.TABLE_NAME} WHERE ${License.LICENSE_ID} = :licenseId" + ) suspend fun exists(licenseId: String): String? - @Query("SELECT ${License.REGISTERED} FROM ${License.TABLE_NAME} WHERE ${License.LICENSE_ID} = :licenseId") + @Query( + "SELECT ${License.REGISTERED} FROM ${License.TABLE_NAME} WHERE ${License.LICENSE_ID} = :licenseId" + ) suspend fun isDeviceRegistered(licenseId: String): Boolean - @Query("UPDATE ${License.TABLE_NAME} SET ${License.REGISTERED} = 1 WHERE ${License.LICENSE_ID} = :licenseId") + @Query( + "UPDATE ${License.TABLE_NAME} SET ${License.REGISTERED} = 1 WHERE ${License.LICENSE_ID} = :licenseId" + ) suspend fun registerDevice(licenseId: String) @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun addLicense(license: License) - @Query("SELECT ${License.RIGHTCOPY} FROM ${License.TABLE_NAME} WHERE ${License.LICENSE_ID} = :licenseId") - fun getCopiesLeft(licenseId: String): Int? + @Query( + "SELECT ${License.RIGHTCOPY} FROM ${License.TABLE_NAME} WHERE ${License.LICENSE_ID} = :licenseId" + ) + suspend fun getCopiesLeft(licenseId: String): Int? - @Query("UPDATE ${License.TABLE_NAME} SET ${License.RIGHTCOPY} = :quantity WHERE ${License.LICENSE_ID} = :licenseId") - fun setCopiesLeft(quantity: Int, licenseId: String) + @Query( + "SELECT ${License.RIGHTCOPY} FROM ${License.TABLE_NAME} WHERE ${License.LICENSE_ID} = :licenseId" + ) + fun copiesLeftFlow(licenseId: String): Flow<Int?> - @Query("SELECT ${License.RIGHTPRINT} FROM ${License.TABLE_NAME} WHERE ${License.LICENSE_ID} = :licenseId") - fun getPrintsLeft(licenseId: String): Int? + @Query( + "UPDATE ${License.TABLE_NAME} SET ${License.RIGHTCOPY} = :quantity WHERE ${License.LICENSE_ID} = :licenseId" + ) + suspend fun setCopiesLeft(quantity: Int, licenseId: String) - @Query("UPDATE ${License.TABLE_NAME} SET ${License.RIGHTPRINT} = :quantity WHERE ${License.LICENSE_ID} = :licenseId") - fun setPrintsLeft(quantity: Int, licenseId: String) + @Transaction + suspend fun tryCopy(quantity: Int, licenseId: String): Boolean { + require(quantity >= 0) + val copiesLeft = getCopiesLeft(licenseId) + return when { + copiesLeft == null -> + true + copiesLeft < quantity -> + false + else -> { + setCopiesLeft(copiesLeft - quantity, licenseId) + return true + } + } + } + + @Query( + "SELECT ${License.RIGHTPRINT} FROM ${License.TABLE_NAME} WHERE ${License.LICENSE_ID} = :licenseId" + ) + suspend fun getPrintsLeft(licenseId: String): Int? + + @Query( + "SELECT ${License.RIGHTPRINT} FROM ${License.TABLE_NAME} WHERE ${License.LICENSE_ID} = :licenseId" + ) + fun printsLeftFlow(licenseId: String): Flow<Int?> + + @Query( + "UPDATE ${License.TABLE_NAME} SET ${License.RIGHTPRINT} = :quantity WHERE ${License.LICENSE_ID} = :licenseId" + ) + suspend fun setPrintsLeft(quantity: Int, licenseId: String) + + @Transaction + suspend fun tryPrint(quantity: Int, licenseId: String): Boolean { + require(quantity >= 0) + val printLeft = getPrintsLeft(licenseId) + return when { + printLeft == null -> + true + printLeft < quantity -> + false + else -> { + setPrintsLeft(printLeft - quantity, licenseId) + return true + } + } + } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/LcpDatabase.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/LcpDatabase.kt index d877001a6c..d7744f9bc1 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/LcpDatabase.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/LcpDatabase.kt @@ -35,8 +35,8 @@ internal abstract class LcpDatabase : RoomDatabase() { return tempInstance } val MIGRATION_1_2 = object : Migration(1, 2) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL( + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( """ CREATE TABLE passphrases ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -47,15 +47,15 @@ internal abstract class LcpDatabase : RoomDatabase() { ) """.trimIndent() ) - database.execSQL( + db.execSQL( """ INSERT INTO passphrases (license_id, provider, user_id, passphrase) SELECT id, origin, userId, passphrase FROM Transactions """.trimIndent() ) - database.execSQL("DROP TABLE Transactions") + db.execSQL("DROP TABLE Transactions") - database.execSQL( + db.execSQL( """ CREATE TABLE new_Licenses ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -66,14 +66,14 @@ internal abstract class LcpDatabase : RoomDatabase() { ) """.trimIndent() ) - database.execSQL( + db.execSQL( """ INSERT INTO new_Licenses (license_id, right_print, right_copy, registered) SELECT id, printsLeft, copiesLeft, registered FROM Licenses """.trimIndent() ) - database.execSQL("DROP TABLE Licenses") - database.execSQL("ALTER TABLE new_Licenses RENAME TO licenses") + db.execSQL("DROP TABLE Licenses") + db.execSQL("ALTER TABLE new_Licenses RENAME TO licenses") } } synchronized(this) { @@ -81,7 +81,7 @@ internal abstract class LcpDatabase : RoomDatabase() { context.applicationContext, LcpDatabase::class.java, "lcpdatabase" - ).allowMainThreadQueries().addMigrations(MIGRATION_1_2).build() + ).addMigrations(MIGRATION_1_2).build() INSTANCE = instance return instance } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/License.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/License.kt index 7f0f31be04..de02041a4c 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/License.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/License.kt @@ -14,7 +14,7 @@ import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = License.TABLE_NAME) -data class License( +internal data class License( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = ID) var id: Long? = null, diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/Passphrase.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/Passphrase.kt index 4b7fbfbc15..8e64c3ceae 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/Passphrase.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/persistence/Passphrase.kt @@ -14,7 +14,7 @@ import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = Passphrase.TABLE_NAME) -data class Passphrase( +internal data class Passphrase( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = ID) var id: Long? = null, diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/public/Deprecated.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/public/Deprecated.kt index f6b44cfa8f..ac5585d8c9 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/public/Deprecated.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/public/Deprecated.kt @@ -11,31 +11,70 @@ package org.readium.r2.lcp.public import android.content.Context import org.readium.r2.lcp.LcpAuthenticating -import org.readium.r2.lcp.LcpException +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpLicense import org.readium.r2.lcp.LcpService -@Deprecated("Renamed to `LcpService`", ReplaceWith("org.readium.r2.lcp.LcpService"), level = DeprecationLevel.ERROR) -typealias LCPService = LcpService -@Deprecated("Renamed to `LcpService.AcquiredPublication`", ReplaceWith("org.readium.r2.lcp.LcpService.AcquiredPublication"), level = DeprecationLevel.ERROR) -typealias LCPImportedPublication = LcpService.AcquiredPublication +@Deprecated( + "Renamed to `LcpService`", + ReplaceWith("org.readium.r2.lcp.LcpService"), + level = DeprecationLevel.ERROR +) +public typealias LCPService = LcpService + +@Deprecated( + "Renamed to `LcpService.AcquiredPublication`", + ReplaceWith("org.readium.r2.lcp.LcpService.AcquiredPublication"), + level = DeprecationLevel.ERROR +) +public typealias LCPImportedPublication = LcpService.AcquiredPublication + @Deprecated("Not used anymore", level = DeprecationLevel.ERROR) -typealias URLPresenter = () -> Unit -@Deprecated("Renamed to `LcpLicense`", ReplaceWith("org.readium.r2.lcp.LcpLicense"), level = DeprecationLevel.ERROR) -typealias LCPLicense = LcpLicense +public typealias URLPresenter = () -> Unit + +@Deprecated( + "Renamed to `LcpLicense`", + ReplaceWith("org.readium.r2.lcp.LcpLicense"), + level = DeprecationLevel.ERROR +) +public typealias LCPLicense = LcpLicense + +@Deprecated( + "Renamed to `LcpAuthenticating`", + ReplaceWith("org.readium.r2.lcp.LcpAuthenticating"), + level = DeprecationLevel.ERROR +) +public typealias LCPAuthenticating = LcpAuthenticating -@Deprecated("Renamed to `LcpAuthenticating`", ReplaceWith("org.readium.r2.lcp.LcpAuthenticating"), level = DeprecationLevel.ERROR) -typealias LCPAuthenticating = LcpAuthenticating @Deprecated("Not used anymore", level = DeprecationLevel.ERROR) -interface LCPAuthenticationDelegate -@Deprecated("Renamed to `LcpAuthenticating.AuthenticationReason`", ReplaceWith("org.readium.r2.lcp.LcpAuthenticating.AuthenticationReason"), level = DeprecationLevel.ERROR) -typealias LCPAuthenticationReason = LcpAuthenticating.AuthenticationReason -@Deprecated("Renamed to `LcpAuthenticating.AuthenticatedLicense`", ReplaceWith("org.readium.r2.lcp.LcpAuthenticating.AuthenticatedLicense"), level = DeprecationLevel.ERROR) -typealias LCPAuthenticatedLicense = LcpAuthenticating.AuthenticatedLicense - -@Deprecated("Renamed to `LcpException", ReplaceWith("org.readium.r2.lcp.LcpException"), level = DeprecationLevel.ERROR) -typealias LCPError = LcpException - -@Deprecated("Renamed to `LcpService()`", ReplaceWith("LcpService()"), level = DeprecationLevel.ERROR) -fun R2MakeLCPService(context: Context) = - LcpService(context) +public interface LCPAuthenticationDelegate + +@Deprecated( + "Renamed to `LcpAuthenticating.AuthenticationReason`", + ReplaceWith("org.readium.r2.lcp.LcpAuthenticating.AuthenticationReason"), + level = DeprecationLevel.ERROR +) +public typealias LCPAuthenticationReason = LcpAuthenticating.AuthenticationReason + +@Deprecated( + "Renamed to `LcpAuthenticating.AuthenticatedLicense`", + ReplaceWith("org.readium.r2.lcp.LcpAuthenticating.AuthenticatedLicense"), + level = DeprecationLevel.ERROR +) +public typealias LCPAuthenticatedLicense = LcpAuthenticating.AuthenticatedLicense + +@Deprecated( + "Renamed to `LcpException", + ReplaceWith("org.readium.r2.lcp.LcpException"), + level = DeprecationLevel.ERROR +) +public typealias LCPError = LcpError + +@Deprecated( + "Renamed to `LcpService()`", + ReplaceWith("LcpService()"), + level = DeprecationLevel.ERROR +) +@Suppress("UNUSED_PARAMETER") +public fun R2MakeLCPService(context: Context): LcpService? = + throw NotImplementedError() diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/CRLService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/CRLService.kt index 992ca8e40b..c896fbc666 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/CRLService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/CRLService.kt @@ -17,6 +17,7 @@ import kotlin.time.ExperimentalTime import org.joda.time.DateTime import org.joda.time.Days import org.readium.r2.lcp.BuildConfig.DEBUG +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.getOrElse import timber.log.Timber @@ -24,7 +25,10 @@ import timber.log.Timber @OptIn(ExperimentalTime::class) internal class CRLService(val network: NetworkService, val context: Context) { - private val preferences: SharedPreferences = context.getSharedPreferences("org.readium.r2.lcp", Context.MODE_PRIVATE) + private val preferences: SharedPreferences = context.getSharedPreferences( + "org.readium.r2.lcp", + Context.MODE_PRIVATE + ) companion object { const val expiration = 7 @@ -50,12 +54,15 @@ internal class CRLService(val network: NetworkService, val context: Context) { private suspend fun fetch(): String { val url = "http://crl.edrlab.telesec.de/rl/EDRLab_CA.crl" val data = network.fetch(url, NetworkService.Method.GET) - .getOrElse { throw LcpException.CrlFetching } + .getOrElse { throw LcpException(LcpError.CrlFetching) } return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { "-----BEGIN X509 CRL-----${Base64.getEncoder().encodeToString(data)}-----END X509 CRL-----" } else { - "-----BEGIN X509 CRL-----${android.util.Base64.encodeToString(data, android.util.Base64.DEFAULT)}-----END X509 CRL-----" + "-----BEGIN X509 CRL-----${android.util.Base64.encodeToString( + data, + android.util.Base64.DEFAULT + )}-----END X509 CRL-----" } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/DeviceRepository.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/DeviceRepository.kt index 9ded1bcfa5..dfd7f64209 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/DeviceRepository.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/DeviceRepository.kt @@ -9,6 +9,7 @@ package org.readium.r2.lcp.service +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.lcp.persistence.LcpDao @@ -17,14 +18,14 @@ internal class DeviceRepository(private val lcpDao: LcpDao) { suspend fun isDeviceRegistered(license: LicenseDocument): Boolean { if (lcpDao.exists(license.id) == null) { - throw LcpException.Runtime("The LCP License doesn't exist in the database") + throw LcpException(LcpError.Runtime("The LCP License doesn't exist in the database")) } return lcpDao.isDeviceRegistered(license.id) } suspend fun registerDevice(license: LicenseDocument) { if (lcpDao.exists(license.id) == null) { - throw LcpException.Runtime("The LCP License doesn't exist in the database") + throw LcpException(LcpError.Runtime("The LCP License doesn't exist in the database")) } lcpDao.registerDevice(license.id) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/DeviceService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/DeviceService.kt index cde7889faf..07182b2f29 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/DeviceService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/DeviceService.kt @@ -29,7 +29,10 @@ internal class DeviceService( val context: Context ) : Serializable { - private val preferences: SharedPreferences = context.getSharedPreferences("org.readium.r2.settings", Context.MODE_PRIVATE) + private val preferences: SharedPreferences = context.getSharedPreferences( + "org.readium.r2.settings", + Context.MODE_PRIVATE + ) val id: String get() { @@ -67,7 +70,7 @@ internal class DeviceService( return null } - val url = link.url(asQueryParameters).toString() + val url = link.url(parameters = asQueryParameters).toString() val data = network.fetch(url, NetworkService.Method.POST, asQueryParameters) .getOrNull() ?: return null diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LcpClient.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LcpClient.kt index f361d9a522..5be0dd1dd7 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LcpClient.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LcpClient.kt @@ -1,6 +1,7 @@ package org.readium.r2.lcp.service import java.lang.reflect.InvocationTargetException +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.extensions.tryOr @@ -26,12 +27,17 @@ internal object LcpClient { fun toDRMContext(): Any = Class.forName("org.readium.lcp.sdk.DRMContext") - .getConstructor(String::class.java, String::class.java, String::class.java, String::class.java) + .getConstructor( + String::class.java, + String::class.java, + String::class.java, + String::class.java + ) .newInstance(hashedPassphrase, encryptedContentKey, token, profile) } private val instance: Any by lazy { - klass.newInstance() + klass.getDeclaredConstructor().newInstance() } private val klass: Class<*> by lazy { @@ -46,7 +52,12 @@ internal object LcpClient { fun createContext(jsonLicense: String, hashedPassphrases: String, pemCrl: String): Context = try { val drmContext = klass - .getMethod("createContext", String::class.java, String::class.java, String::class.java) + .getMethod( + "createContext", + String::class.java, + String::class.java, + String::class.java + ) .invoke(instance, jsonLicense, hashedPassphrases, pemCrl)!! Context.fromDRMContext(drmContext) @@ -57,7 +68,11 @@ internal object LcpClient { fun decrypt(context: Context, encryptedData: ByteArray): ByteArray = try { klass - .getMethod("decrypt", Class.forName("org.readium.lcp.sdk.DRMContext"), ByteArray::class.java) + .getMethod( + "decrypt", + Class.forName("org.readium.lcp.sdk.DRMContext"), + ByteArray::class.java + ) .invoke(instance, context.toDRMContext(), encryptedData) as ByteArray } catch (e: InvocationTargetException) { @@ -74,11 +89,11 @@ internal object LcpClient { } private fun mapException(e: Throwable): LcpException { - val drmExceptionClass = Class.forName("org.readium.lcp.sdk.DRMException") - if (!drmExceptionClass.isInstance(e)) - return LcpException.Runtime("the Lcp client threw an unhandled exception") + if (!drmExceptionClass.isInstance(e)) { + return LcpException(LcpError.Runtime("the Lcp client threw an unhandled exception")) + } val drmError = drmExceptionClass .getMethod("getDrmError") @@ -89,19 +104,21 @@ internal object LcpClient { .getMethod("getCode") .invoke(drmError) as Int - return when (errorCode) { + val error = when (errorCode) { // Error code 11 should never occur since we check the start/end date before calling createContext - 11 -> LcpException.Runtime("License is out of date (check start and end date).") - 101 -> LcpException.LicenseIntegrity.CertificateRevoked - 102 -> LcpException.LicenseIntegrity.InvalidCertificateSignature - 111 -> LcpException.LicenseIntegrity.InvalidLicenseSignatureDate - 112 -> LcpException.LicenseIntegrity.InvalidLicenseSignature + 11 -> LcpError.Runtime("License is out of date (check start and end date).") + 101 -> LcpError.LicenseIntegrity.CertificateRevoked + 102 -> LcpError.LicenseIntegrity.InvalidCertificateSignature + 111 -> LcpError.LicenseIntegrity.InvalidLicenseSignatureDate + 112 -> LcpError.LicenseIntegrity.InvalidLicenseSignature // Error code 121 seems to be unused in the C++ lib. - 121 -> LcpException.Runtime("The drm context is invalid.") - 131 -> LcpException.Decryption.ContentKeyDecryptError - 141 -> LcpException.LicenseIntegrity.InvalidUserKeyCheck - 151 -> LcpException.Decryption.ContentDecryptError - else -> LcpException.Unknown(e) + 121 -> LcpError.Runtime("The drm context is invalid.") + 131 -> LcpError.Decryption.ContentKeyDecryptError + 141 -> LcpError.LicenseIntegrity.InvalidUserKeyCheck + 151 -> LcpError.Decryption.ContentDecryptError + else -> LcpError.Unknown(e) } + + return LcpException(error) } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesRepository.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesRepository.kt index 7dd818c036..93ef9e716f 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesRepository.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesRepository.kt @@ -9,6 +9,7 @@ package org.readium.r2.lcp.service +import kotlinx.coroutines.flow.Flow import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.lcp.persistence.LcpDao import org.readium.r2.lcp.persistence.License @@ -27,19 +28,17 @@ internal class LicensesRepository(private val lcpDao: LcpDao) { lcpDao.addLicense(license) } - fun copiesLeft(licenseId: String): Int? { - return lcpDao.getCopiesLeft(licenseId) + fun copiesLeft(licenseId: String): Flow<Int?> { + return lcpDao.copiesLeftFlow(licenseId) } - fun setCopiesLeft(quantity: Int, licenseId: String) { - lcpDao.setCopiesLeft(quantity, licenseId) - } + suspend fun tryCopy(quantity: Int, licenseId: String): Boolean = + lcpDao.tryCopy(quantity, licenseId) - fun printsLeft(licenseId: String): Int? { - return lcpDao.getPrintsLeft(licenseId) + fun printsLeft(licenseId: String): Flow<Int?> { + return lcpDao.printsLeftFlow(licenseId) } - fun setPrintsLeft(quantity: Int, licenseId: String) { - lcpDao.setPrintsLeft(quantity, licenseId) - } + suspend fun tryPrint(quantity: Int, licenseId: String): Boolean = + lcpDao.tryPrint(quantity, licenseId) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index 5bf9cd5dce..aabdbf8b5d 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -10,21 +10,31 @@ package org.readium.r2.lcp.service import android.content.Context -import java.io.File import kotlin.coroutines.resume -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext import org.readium.r2.lcp.LcpAuthenticating -import org.readium.r2.lcp.LcpException +import org.readium.r2.lcp.LcpContentProtection +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpLicense +import org.readium.r2.lcp.LcpPublicationRetriever import org.readium.r2.lcp.LcpService import org.readium.r2.lcp.license.License import org.readium.r2.lcp.license.LicenseValidation import org.readium.r2.lcp.license.container.LicenseContainer +import org.readium.r2.lcp.license.container.WritableLicenseContainer import org.readium.r2.lcp.license.container.createLicenseContainer import org.readium.r2.lcp.license.model.LicenseDocument -import org.readium.r2.shared.extensions.tryOr +import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.downloads.DownloadManager import timber.log.Timber internal class LicensesService( @@ -33,52 +43,73 @@ internal class LicensesService( private val device: DeviceService, private val network: NetworkService, private val passphrases: PassphrasesService, - private val context: Context + private val context: Context, + private val assetRetriever: AssetRetriever, + private val downloadManager: DownloadManager ) : LcpService, CoroutineScope by MainScope() { - override suspend fun isLcpProtected(file: File): Boolean = - tryOr(false) { - createLicenseContainer(file.path).read() - true - } + override fun contentProtection( + authentication: LcpAuthenticating + ): ContentProtection = + LcpContentProtection(this, authentication, assetRetriever) - override suspend fun acquirePublication(lcpl: ByteArray, onProgress: (Double) -> Unit): Try<LcpService.AcquiredPublication, LcpException> = - try { - val licenseDocument = LicenseDocument(lcpl) - Timber.d("license ${licenseDocument.json}") - fetchPublication(licenseDocument, onProgress).let { Try.success(it) } - } catch (e: Exception) { - Try.failure(LcpException.wrap(e)) - } + override fun publicationRetriever(): LcpPublicationRetriever { + return LcpPublicationRetriever( + context, + downloadManager, + assetRetriever + ) + } override suspend fun retrieveLicense( - file: File, + asset: Asset, authentication: LcpAuthenticating, - allowUserInteraction: Boolean, - sender: Any? - ): Try<LcpLicense, LcpException>? = + allowUserInteraction: Boolean + ): Try<LcpLicense, LcpError> = try { - val container = createLicenseContainer(file.path) - // WARNING: Using the Default dispatcher in the state machine code is critical. If we were using the Main Dispatcher, - // calling runBlocking in LicenseValidation.handle would block the main thread and cause a severe issue - // with LcpAuthenticating.retrievePassphrase. Specifically, the interaction of runBlocking and suspendCoroutine - // blocks the current thread before the passphrase popup has been showed until some button not yet showed is clicked. - val license = withContext(Dispatchers.Default) { retrieveLicense(container, authentication, allowUserInteraction, sender) } - Timber.d("license retrieved ${license?.license}") - - license?.let { Try.success(it) } + val licenseContainer = createLicenseContainer(context, asset) + val license = retrieveLicense( + licenseContainer, + authentication, + allowUserInteraction + ) + Try.success(license) } catch (e: Exception) { - Try.failure(LcpException.wrap(e)) + Try.failure(LcpError.wrap(e)) } private suspend fun retrieveLicense( + container: LicenseContainer, + authentication: LcpAuthenticating, + allowUserInteraction: Boolean + ): LcpLicense { + // WARNING: Using the Default dispatcher in the state machine code is critical. If we were using the Main Dispatcher, + // calling runBlocking in LicenseValidation.handle would block the main thread and cause a severe issue + // with LcpAuthenticating.retrievePassphrase. Specifically, the interaction of runBlocking and suspendCoroutine + // blocks the current thread before the passphrase popup has been showed until some button not yet showed is clicked. + val license = withContext(Dispatchers.Default) { + retrieveLicenseUnsafe( + container, + authentication, + allowUserInteraction + ) + } + Timber.d("license retrieved ${license.license}") + + return license + } + + private suspend fun retrieveLicenseUnsafe( container: LicenseContainer, authentication: LcpAuthenticating?, - allowUserInteraction: Boolean, - sender: Any? - ): License? = + allowUserInteraction: Boolean + ): License = suspendCancellableCoroutine { cont -> - retrieveLicense(container, authentication, allowUserInteraction, sender) { license -> + retrieveLicense( + container, + authentication, + allowUserInteraction + ) { license -> if (cont.isActive) { cont.resume(license) } @@ -89,17 +120,20 @@ internal class LicensesService( container: LicenseContainer, authentication: LcpAuthenticating?, allowUserInteraction: Boolean, - sender: Any?, - completion: (License?) -> Unit + completion: (License) -> Unit ) { - var initialData = container.read() Timber.d("license ${LicenseDocument(data = initialData).json}") val validation = LicenseValidation( - authentication = authentication, crl = this.crl, - device = this.device, network = this.network, passphrases = this.passphrases, context = this.context, - allowUserInteraction = allowUserInteraction, sender = sender + authentication = authentication, + crl = this.crl, + device = this.device, + network = this.network, + passphrases = this.passphrases, + context = this.context, + allowUserInteraction = allowUserInteraction, + ignoreInternetErrors = container is WritableLicenseContainer ) { licenseDocument -> try { launch { @@ -108,9 +142,11 @@ internal class LicensesService( } catch (error: Error) { Timber.d("Failed to add the LCP License to the local database: $error") } - if (!licenseDocument.data.contentEquals(initialData)) { + if (!licenseDocument.toByteArray().contentEquals(initialData)) { try { - container.write(licenseDocument) + (container as? WritableLicenseContainer) + ?.let { container.write(licenseDocument) } + Timber.d("licenseDocument ${licenseDocument.json}") initialData = container.read() @@ -127,38 +163,27 @@ internal class LicensesService( Timber.d("validated documents $it") try { documents.getContext() - completion(License(documents = it, validation = validation, licenses = this.licenses, device = this.device, network = this.network)) + launch { + completion( + License( + documents = it, + validation = validation, + licenses = this@LicensesService.licenses, + device = this@LicensesService.device, + network = this@LicensesService.network + ) + ) + } } catch (e: Exception) { throw e } } error?.let { throw error } - if (documents == null && error == null) { - completion(null) + // Both error and documents can be null if the user cancelled the passphrase prompt. + if (documents == null) { + throw CancellationException("License validation was interrupted.") } } } - - private suspend fun fetchPublication(license: LicenseDocument, onProgress: (Double) -> Unit): LcpService.AcquiredPublication { - val link = license.link(LicenseDocument.Rel.publication) - val url = link?.url - ?: throw LcpException.Parsing.Url(rel = LicenseDocument.Rel.publication.rawValue) - - val destination = withContext(Dispatchers.IO) { - File.createTempFile("lcp-${System.currentTimeMillis()}", ".tmp") - } - Timber.i("LCP destination $destination") - - val mediaType = network.download(url, destination, mediaType = link.type, onProgress = onProgress) ?: MediaType.of(mediaType = link.type) ?: MediaType.EPUB - - // Saves the License Document into the downloaded publication - val container = createLicenseContainer(destination.path, mediaType) - container.write(license) - - return LcpService.AcquiredPublication( - localFile = destination, - suggestedFilename = "${license.id}.${mediaType.fileExtension}" - ) - } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt index 182592ecaf..a41116f05c 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt @@ -17,26 +17,28 @@ import java.net.HttpURLConnection import java.net.URL import kotlin.math.round import kotlin.time.Duration -import kotlin.time.ExperimentalTime import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.sniffMediaType import timber.log.Timber -internal typealias URLParameters = Map<String, String?> +internal typealias URLParameters = Map<String, String> -internal class NetworkException(val status: Int?, cause: Throwable? = null) : Exception("Network failure with status $status", cause) +internal class NetworkException(val status: Int?, cause: Throwable? = null) : Exception( + "Network failure with status $status", + cause +) -@OptIn(ExperimentalTime::class) internal class NetworkService { - enum class Method(val rawValue: String) { + enum class Method(val value: String) { GET("GET"), POST("POST"), PUT("PUT"); companion object { - operator fun invoke(rawValue: String) = values().firstOrNull { it.rawValue == rawValue } + operator fun invoke(value: String) = values().firstOrNull { it.value == value } } } @@ -50,10 +52,12 @@ internal class NetworkService { withContext(Dispatchers.IO) { try { @Suppress("NAME_SHADOWING") - val url = URL(Uri.parse(url).buildUpon().appendQueryParameters(parameters).build().toString()) + val url = URL( + Uri.parse(url).buildUpon().appendQueryParameters(parameters).build().toString() + ) val connection = url.openConnection() as HttpURLConnection - connection.requestMethod = method.rawValue + connection.requestMethod = method.value if (timeout != null) { connection.connectTimeout = timeout.inWholeMilliseconds.toInt() } @@ -81,22 +85,20 @@ internal class NetworkService { private fun Uri.Builder.appendQueryParameters(parameters: URLParameters): Uri.Builder = apply { for ((key, value) in parameters) { - if (value != null) { - appendQueryParameter(key, value) - } + appendQueryParameter(key, value) } } suspend fun download( - url: URL, + url: Url, destination: File, - mediaType: String? = null, + mediaType: MediaType? = null, onProgress: (Double) -> Unit ): MediaType? = withContext(Dispatchers.IO) { try { - val connection = url.openConnection() as HttpURLConnection + val connection = URL(url.toString()).openConnection() as HttpURLConnection if (connection.responseCode >= 400) { - throw LcpException.Network(NetworkException(connection.responseCode)) + throw LcpException(LcpError.Network(NetworkException(connection.responseCode))) } var readLength = 0L @@ -132,10 +134,12 @@ internal class NetworkService { } } - connection.sniffMediaType(mediaTypes = listOfNotNull(mediaType)) + connection.contentType + ?.let { MediaType(it) } + ?: mediaType } catch (e: Exception) { Timber.e(e) - throw LcpException.Network(e) + throw LcpException(LcpError.Network(e)) } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/PassphrasesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/PassphrasesService.kt index bc9be10499..a356be015f 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/PassphrasesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/PassphrasesService.kt @@ -18,8 +18,7 @@ internal class PassphrasesService(private val repository: PassphrasesRepository) suspend fun request( license: LicenseDocument, authentication: LcpAuthenticating?, - allowUserInteraction: Boolean, - sender: Any? + allowUserInteraction: Boolean ): String? { val candidates = this@PassphrasesService.possiblePassphrasesFromRepository(license) val passphrase = try { @@ -29,7 +28,12 @@ internal class PassphrasesService(private val repository: PassphrasesRepository) } return when { passphrase != null -> passphrase - authentication != null -> this@PassphrasesService.authenticate(license, LcpAuthenticating.AuthenticationReason.PassphraseNotFound, authentication, allowUserInteraction, sender) + authentication != null -> this@PassphrasesService.authenticate( + license, + LcpAuthenticating.AuthenticationReason.PassphraseNotFound, + authentication, + allowUserInteraction + ) else -> null } } @@ -38,11 +42,14 @@ internal class PassphrasesService(private val repository: PassphrasesRepository) license: LicenseDocument, reason: LcpAuthenticating.AuthenticationReason, authentication: LcpAuthenticating, - allowUserInteraction: Boolean, - sender: Any? + allowUserInteraction: Boolean ): String? { val authenticatedLicense = LcpAuthenticating.AuthenticatedLicense(document = license) - val clearPassphrase = authentication.retrievePassphrase(authenticatedLicense, reason, allowUserInteraction, sender) + val clearPassphrase = authentication.retrievePassphrase( + authenticatedLicense, + reason, + allowUserInteraction + ) ?: return null val hashedPassphrase = HASH.sha256(clearPassphrase) val passphrases = mutableListOf(hashedPassphrase) @@ -57,7 +64,12 @@ internal class PassphrasesService(private val repository: PassphrasesRepository) addPassphrase(passphrase, true, license.id, license.provider, license.user.id) passphrase } catch (e: Exception) { - authenticate(license, LcpAuthenticating.AuthenticationReason.InvalidPassphrase, authentication, allowUserInteraction, sender) + authenticate( + license, + LcpAuthenticating.AuthenticationReason.InvalidPassphrase, + authentication, + allowUserInteraction + ) } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/util/Digest.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/util/Digest.kt new file mode 100644 index 0000000000..5365e51b68 --- /dev/null +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/util/Digest.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.lcp.util + +import java.io.File +import java.security.MessageDigest +import org.readium.r2.shared.extensions.tryOrNull + +/** + * Returns the SHA-256 sum of file content or null if computation failed. + */ +internal fun File.sha256(): ByteArray? = + tryOrNull<ByteArray> { + val md = MessageDigest.getInstance("SHA-256") + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + inputStream().use { + var bytes = it.read(buffer) + while (bytes >= 0) { + md.update(buffer, 0, bytes) + bytes = it.read(buffer) + } + } + return md.digest() + } diff --git a/readium/lcp/src/main/res/layout/r2_lcp_auth_dialog.xml b/readium/lcp/src/main/res/layout/readium_lcp_auth_dialog.xml similarity index 95% rename from readium/lcp/src/main/res/layout/r2_lcp_auth_dialog.xml rename to readium/lcp/src/main/res/layout/readium_lcp_auth_dialog.xml index 67921e8ace..a226959842 100644 --- a/readium/lcp/src/main/res/layout/r2_lcp_auth_dialog.xml +++ b/readium/lcp/src/main/res/layout/readium_lcp_auth_dialog.xml @@ -16,7 +16,7 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" - android:text="@string/r2_lcp_dialog_cancel" + android:text="@string/readium_lcp_dialog_cancel" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -85,7 +85,7 @@ android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" - android:text="@string/r2_lcp_dialog_continue" + android:text="@string/readium_lcp_dialog_continue" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/r2_passwordLayout" /> @@ -98,7 +98,7 @@ android:layout_marginTop="16dp" android:layout_marginEnd="8dp" android:background="@android:color/transparent" - android:text="@string/r2_lcp_dialog_forgotPassphrase" + android:text="@string/readium_lcp_dialog_forgotPassphrase" android:textAlignment="viewStart" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -112,7 +112,7 @@ android:layout_marginTop="16dp" android:layout_marginEnd="8dp" android:background="@android:color/transparent" - android:text="@string/r2_lcp_dialog_help" + android:text="@string/readium_lcp_dialog_help" android:textAlignment="viewStart" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" diff --git a/readium/lcp/src/main/res/values/strings.xml b/readium/lcp/src/main/res/values/strings.xml index e744f7e0c8..7a2bab272c 100644 --- a/readium/lcp/src/main/res/values/strings.xml +++ b/readium/lcp/src/main/res/values/strings.xml @@ -9,61 +9,16 @@ <!-- LCP Authentication Dialog --> - <string name="r2_lcp_dialog_continue">Continue</string> - <string name="r2_lcp_dialog_cancel">Cancel</string> - <string name="r2_lcp_dialog_reason_passphraseNotFound">Passphrase Required</string> - <string name="r2_lcp_dialog_reason_invalidPassphrase">Incorrect Passphrase</string> - <string name="r2_lcp_dialog_prompt">This publication is protected by Readium LCP.\n\nIn order to open it, we need to know the passphrase required by: \n\n%1$s.\n\nTo help you remember it, the following hint is available:</string> - <string name="r2_lcp_dialog_forgotPassphrase">Forgot your passphrase?</string> - <string name="r2_lcp_dialog_help">Need more help?</string> - <string name="r2_lcp_dialog_support">Support</string> - <string name="r2_lcp_dialog_support_web">Website</string> - <string name="r2_lcp_dialog_support_phone">Phone</string> - <string name="r2_lcp_dialog_support_mail">Mail</string> - - <!-- LCP Exceptions User Messages --> - - <string name="r2.lcp.exception.license_interaction_not_available">This interaction is not available</string> - <string name="r2.lcp.exception.license_profile_not_supported">This License has a profile identifier that this app cannot handle, the publication cannot be processed</string> - <string name="r2.lcp.exception.crl_fetching">Can\'t retrieve the Certificate Revocation List</string> - <string name="r2.lcp.exception.network">Network error</string> - <string name="r2.lcp.exception.runtime">Unexpected LCP error</string> - <string name="r2.lcp.exception.unknown">Unknown LCP error</string> - - <string name="r2.lcp.exception.license_status.cancelled">This license was cancelled on %1$s</string> - <string name="r2.lcp.exception.license_status.returned">This license has been returned on %1$s</string> - <string name="r2.lcp.exception.license_status.not_started">This license starts on %1$s</string> - <string name="r2.lcp.exception.license_status.expired">This license expired on %1$s</string> - <plurals name="r2.lcp.exception.license_status.revoked"> - <item quantity="one">This license was revoked by its provider on %1$s. It was registered by %2$d device.</item> - <item quantity="other">This license was revoked by its provider on %1$s. It was registered by %2$d devices.</item> - </plurals> - - <string name="r2.lcp.exception.renew.renew_failed">Your publication could not be renewed properly</string> - <string name="r2.lcp.exception.renew.invalid_renewal_period">Incorrect renewal period, your publication could not be renewed</string> - <string name="r2.lcp.exception.renew.unexpected_server_error">An unexpected error has occurred on the server</string> - - <string name="r2.lcp.exception.return.return_failed">Your publication could not be returned properly</string> - <string name="r2.lcp.exception.return.already_returned_or_expired">Your publication has already been returned before or is expired</string> - <string name="r2.lcp.exception.return.unexpected_server_error">An unexpected error has occurred on the server</string> - - <string name="r2.lcp.exception.parsing">The JSON is not representing a valid document</string> - <string name="r2.lcp.exception.parsing.malformed_json">The JSON is malformed and can\'t be parsed</string> - <string name="r2.lcp.exception.parsing.license_document">The JSON is not representing a valid License Document</string> - <string name="r2.lcp.exception.parsing.status_document">The JSON is not representing a valid Status Document</string> - - <string name="r2.lcp.exception.container.open_failed">Can\'t open the license container</string> - <string name="r2.lcp.exception.container.file_not_found">License not found in container</string> - <string name="r2.lcp.exception.container.read_failed">Can\'t read license from container</string> - <string name="r2.lcp.exception.container.write_failed">Can\'t write license in container</string> - - <string name="r2.lcp.exception.license_integrity.certificate_revoked">Certificate has been revoked in the CRL</string> - <string name="r2.lcp.exception.license_integrity.invalid_certificate_signature">Certificate has not been signed by CA</string> - <string name="r2.lcp.exception.license_integrity.invalid_license_signature_date">License has been issued by an expired certificate</string> - <string name="r2.lcp.exception.license_integrity.invalid_license_signature">License signature does not match</string> - <string name="r2.lcp.exception.license_integrity.invalid_user_key_check">User key check invalid</string> - - <string name="r2.lcp.exception.decryption.content_key_decrypt_error">Unable to decrypt encrypted content key from user key</string> - <string name="r2.lcp.exception.decryption.content_decrypt_error">Unable to decrypt encrypted content from content key</string> + <string name="readium_lcp_dialog_continue">Continue</string> + <string name="readium_lcp_dialog_cancel">Cancel</string> + <string name="readium_lcp_dialog_reason_passphraseNotFound">Passphrase Required</string> + <string name="readium_lcp_dialog_reason_invalidPassphrase">Incorrect Passphrase</string> + <string name="readium_lcp_dialog_prompt">This publication is protected by Readium LCP.\n\nIn order to open it, we need to know the passphrase required by: \n\n%1$s.\n\nTo help you remember it, the following hint is available:</string> + <string name="readium_lcp_dialog_forgotPassphrase">Forgot your passphrase?</string> + <string name="readium_lcp_dialog_help">Need more help?</string> + <string name="readium_lcp_dialog_support">Support</string> + <string name="readium_lcp_dialog_support_web">Website</string> + <string name="readium_lcp_dialog_support_phone">Phone</string> + <string name="readium_lcp_dialog_support_mail">Mail</string> </resources> \ No newline at end of file diff --git a/readium/lcp/src/test/java/org/readium/r2/lcp/license/container/ZipUtilTest.kt b/readium/lcp/src/test/java/org/readium/r2/lcp/license/container/ZipUtilTest.kt new file mode 100644 index 0000000000..8b461df4b0 --- /dev/null +++ b/readium/lcp/src/test/java/org/readium/r2/lcp/license/container/ZipUtilTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.lcp.license.container + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.FileInputStream +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertNotNull + +class ZipUtilTest { + + private val zipPath: String = + ZipUtilTest::class.java.getResource("futuristic_tales.cbz")!!.path + + private val zipFile: ZipFile = + ZipFile(zipPath) + + private val entryNames: List<String> = listOf( + "Cory Doctorow's Futuristic Tales of the Here and Now/a-fc.jpg", + "Cory Doctorow's Futuristic Tales of the Here and Now/x-002.jpg", + "Cory Doctorow's Futuristic Tales of the Here and Now/x-003.jpg", + "Cory Doctorow's Futuristic Tales of the Here and Now/x-004.jpg" + ) + + private val aFcPath: String = + ZipUtilTest::class.java.getResource("a-fc.jpg")!!.path + + private fun ZipFile.readEntry(name: String): ByteArray? { + val entry = getEntry(name) ?: return null + val stream = getInputStream(entry) + return stream.readBytes() + } + + private fun ZipInputStream.readEntries(): Map<String, ByteArray> { + val modifiedEntries = mutableMapOf<String, ByteArray>() + + do { + val entry = nextEntry + if (entry != null) { + modifiedEntries[entry.name] = readBytes() + } + } while (entry != null) + + return modifiedEntries + } + + @Test + fun addEntryWorks() { + val entryToAdd = "Cory Doctorow's Futuristic Tales of the Here and Now/x-005.jpg" + + val modifiedZip = run { + val outStream = ByteArrayOutputStream() + zipFile.addOrReplaceEntry( + entryToAdd, + FileInputStream(aFcPath), + outStream + ) + outStream.toByteArray() + } + + val modifiedZipStream = ZipInputStream(ByteArrayInputStream(modifiedZip)) + val modifiedEntries = modifiedZipStream.readEntries() + + for (name in entryNames) { + val modifiedEntry = assertNotNull(modifiedEntries[name]) + val expected = zipFile.readEntry(name) + assertContentEquals(expected, modifiedEntry) + } + + assert(entryToAdd in modifiedEntries.keys) + assertContentEquals( + zipFile.readEntry(entryNames[0]), + modifiedEntries[entryToAdd] + ) + } + + @Test + fun replaceEntryWorks() { + val entryToReplace = "Cory Doctorow's Futuristic Tales of the Here and Now/x-004.jpg" + + val modifiedZip = run { + val outStream = ByteArrayOutputStream() + zipFile.addOrReplaceEntry( + entryToReplace, + FileInputStream(aFcPath), + outStream + ) + outStream.toByteArray() + } + + val modifiedZipStream = ZipInputStream(ByteArrayInputStream(modifiedZip)) + val modifiedEntries = modifiedZipStream.readEntries() + + for (name in entryNames) { + val expected = if (name == entryToReplace) { + zipFile.readEntry(entryNames[0]) + } else { + zipFile.readEntry(name) + } + + val modifiedEntry = assertNotNull(modifiedEntries[name]) + assertContentEquals(expected, modifiedEntry) + } + } +} diff --git a/readium/lcp/src/test/java/org/readium/r2/lcp/util/DigestTest.kt b/readium/lcp/src/test/java/org/readium/r2/lcp/util/DigestTest.kt new file mode 100644 index 0000000000..30ff7f7c2d --- /dev/null +++ b/readium/lcp/src/test/java/org/readium/r2/lcp/util/DigestTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.lcp.util + +import java.io.File +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import org.junit.Test + +class DigestTest { + + private val file: File = + File(DigestTest::class.java.getResource("a-fc.jpg")!!.path) + + @OptIn(ExperimentalEncodingApi::class, ExperimentalStdlibApi::class) + @Test + fun `sha256 is correct`() { + val digest = assertNotNull(file.sha256()) + assertEquals("GI42TOamBYJ4q4KKBcmMzlkfvld8bTVRcbjjQ20OvLI=", Base64.encode(digest)) + assertEquals( + "188e364ce6a6058278ab828a05c98cce591fbe577c6d355171b8e3436d0ebcb2", + digest.toHexString() + ) + } +} diff --git a/readium/lcp/src/test/resources/org/readium/r2/lcp/license/container/a-fc.jpg b/readium/lcp/src/test/resources/org/readium/r2/lcp/license/container/a-fc.jpg new file mode 100644 index 0000000000..8455b3d4ad Binary files /dev/null and b/readium/lcp/src/test/resources/org/readium/r2/lcp/license/container/a-fc.jpg differ diff --git a/readium/lcp/src/test/resources/org/readium/r2/lcp/license/container/futuristic_tales.cbz b/readium/lcp/src/test/resources/org/readium/r2/lcp/license/container/futuristic_tales.cbz new file mode 100644 index 0000000000..48da598b30 Binary files /dev/null and b/readium/lcp/src/test/resources/org/readium/r2/lcp/license/container/futuristic_tales.cbz differ diff --git a/readium/lcp/src/test/resources/org/readium/r2/lcp/util/a-fc.jpg b/readium/lcp/src/test/resources/org/readium/r2/lcp/util/a-fc.jpg new file mode 100644 index 0000000000..8455b3d4ad Binary files /dev/null and b/readium/lcp/src/test/resources/org/readium/r2/lcp/util/a-fc.jpg differ diff --git a/readium/navigator-media2/build.gradle.kts b/readium/navigator-media2/build.gradle.kts index 2512435bf7..1bed109721 100644 --- a/readium/navigator-media2/build.gradle.kts +++ b/readium/navigator-media2/build.gradle.kts @@ -13,19 +13,19 @@ plugins { android { resourcePrefix = "readium_" - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs = freeCompilerArgs + listOf( "-opt-in=kotlin.RequiresOptIn", "-opt-in=org.readium.r2.shared.InternalReadiumApi" @@ -43,6 +43,10 @@ android { namespace = "org.readium.navigator.media2" } +kotlin { + explicitApi() +} + rootProject.ext["publish.artifactId"] = "readium-navigator-media2" apply(from = "$rootDir/scripts/publish-module.gradle") diff --git a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/DefaultMetadataFactory.kt b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/DefaultMetadataFactory.kt index 036693ecce..1e9311ce04 100644 --- a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/DefaultMetadataFactory.kt +++ b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/DefaultMetadataFactory.kt @@ -51,7 +51,7 @@ internal class DefaultMetadataFactory(private val publication: Publication) : Me val builder = MediaMetadata.Builder() val link = publication.readingOrder[index] builder.putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, index.toLong()) - builder.putString(MediaMetadata.METADATA_KEY_MEDIA_URI, link.href) + builder.putString(MediaMetadata.METADATA_KEY_MEDIA_URI, link.href.toString()) builder.putString(MediaMetadata.METADATA_KEY_TITLE, link.title) builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, publication.metadata.title) builder.putString(MediaMetadata.METADATA_KEY_ALBUM, publication.metadata.title) diff --git a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt index 16836b801b..12807146f0 100644 --- a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt +++ b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt @@ -4,6 +4,9 @@ * available in the top-level LICENSE file of the project. */ +// Everything in this file will be deprecated +@file:Suppress("DEPRECATION") + package org.readium.navigator.media2 import android.net.Uri @@ -15,22 +18,33 @@ import com.google.android.exoplayer2.upstream.DataSpec import com.google.android.exoplayer2.upstream.TransferListener import java.io.IOException import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.buffered import org.readium.r2.shared.publication.Publication - -sealed class ExoPlayerDataSourceException(message: String, cause: Throwable?) : IOException(message, cause) { - class NotOpened(message: String) : ExoPlayerDataSourceException(message, null) - class NotFound(message: String) : ExoPlayerDataSourceException(message, null) - class ReadFailed(uri: Uri, offset: Int, readLength: Int, cause: Throwable) : ExoPlayerDataSourceException("Failed to read $readLength bytes of URI $uri at offset $offset.", cause) +import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.buffered +import org.readium.r2.shared.util.toUrl + +public sealed class ExoPlayerDataSourceException(message: String, cause: Throwable?) : IOException( + message, + cause +) { + public class NotOpened(message: String) : ExoPlayerDataSourceException(message, null) + public class NotFound(message: String) : ExoPlayerDataSourceException(message, null) + public class ReadFailed(uri: Uri, offset: Int, readLength: Int, cause: Throwable) : ExoPlayerDataSourceException( + "Failed to read $readLength bytes of URI $uri at offset $offset.", + cause + ) } /** * An ExoPlayer's [DataSource] which retrieves resources from a [Publication]. */ -class ExoPlayerDataSource internal constructor(private val publication: Publication) : BaseDataSource(/* isNetwork = */ true) { +public class ExoPlayerDataSource internal constructor(private val publication: Publication) : BaseDataSource(/* isNetwork = */ + true +) { - class Factory( + public class Factory( private val publication: Publication, private val transferListener: TransferListener? = null ) : DataSource.Factory { @@ -46,23 +60,25 @@ class ExoPlayerDataSource internal constructor(private val publication: Publicat private data class OpenedResource( val resource: Resource, val uri: Uri, - var position: Long, + var position: Long ) private var openedResource: OpenedResource? = null override fun open(dataSpec: DataSpec): Long { - val link = publication.linkWithHref(dataSpec.uri.toString()) - ?: throw ExoPlayerDataSourceException.NotFound("Can't find a [Link] for URI: ${dataSpec.uri}. Make sure you only request resources declared in the manifest.") - - val resource = publication.get(link) + val resource = dataSpec.uri.toUrl() + ?.let { publication.linkWithHref(it) } + ?.let { publication.get(it) } // Significantly improves performances, in particular with deflated ZIP entries. - .buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) + ?.buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) + ?: throw ExoPlayerDataSourceException.NotFound( + "Can't find a [Link] for URI: ${dataSpec.uri}. Make sure you only request resources declared in the manifest." + ) openedResource = OpenedResource( resource = resource, uri = dataSpec.uri, - position = dataSpec.position, + position = dataSpec.position ) val bytesToRead = @@ -95,12 +111,15 @@ class ExoPlayerDataSource internal constructor(private val publication: Publicat return 0 } - val openedResource = openedResource ?: throw ExoPlayerDataSourceException.NotOpened("No opened resource to read from. Did you call open()?") + val openedResource = openedResource ?: throw ExoPlayerDataSourceException.NotOpened( + "No opened resource to read from. Did you call open()?" + ) try { val data = runBlocking { openedResource.resource .read(range = openedResource.position until (openedResource.position + length)) + .mapFailure { ReadException(it) } .getOrThrow() } diff --git a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/MediaMetadataFactory.kt b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/MediaMetadataFactory.kt index 02ba2b3ce3..d378433196 100644 --- a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/MediaMetadataFactory.kt +++ b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/MediaMetadataFactory.kt @@ -7,16 +7,16 @@ import androidx.media2.common.MediaMetadata * * The metadata are used for example in the media-style Android notification. */ -@ExperimentalMedia2 -interface MediaMetadataFactory { +@Deprecated("Use the new MediaMetadataFactory from the readium-navigator-media-common module.") +public interface MediaMetadataFactory { /** * Creates the [MediaMetadata] for the whole publication. */ - suspend fun publicationMetadata(): MediaMetadata + public suspend fun publicationMetadata(): MediaMetadata /** * Creates the [MediaMetadata] for the reading order resource at the given [index]. */ - suspend fun resourceMetadata(index: Int): MediaMetadata + public suspend fun resourceMetadata(index: Int): MediaMetadata } diff --git a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/MediaNavigator.kt b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/MediaNavigator.kt index 3332e877fa..9534a6eaf5 100644 --- a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/MediaNavigator.kt +++ b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/MediaNavigator.kt @@ -4,6 +4,9 @@ * available in the top-level LICENSE file of the project. */ +// Everything in this file will be deprecated +@file:Suppress("DEPRECATION") + package org.readium.navigator.media2 import android.app.PendingIntent @@ -31,6 +34,8 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.readium.navigator.media2.MediaNavigator.Companion.create import org.readium.r2.navigator.Navigator +import org.readium.r2.navigator.extensions.normalizeLocator +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication @@ -50,10 +55,10 @@ import timber.log.Timber * providing [create] with it. If you don't, ExoPlayer will be used, without cache. * You can build your own [SessionPlayer] based on [ExoPlayer] using [ExoPlayerDataSource]. */ -@ExperimentalMedia2 +@Deprecated("Use the new AudioNavigator from the readium-navigator-media-audio module.") @OptIn(ExperimentalTime::class) -class MediaNavigator private constructor( - override val publication: Publication, +public class MediaNavigator private constructor( + public val publication: Publication, private val playerFacade: SessionPlayerFacade, private val playerCallback: SessionPlayerCallback, private val configuration: Configuration @@ -124,15 +129,18 @@ class MediaNavigator private constructor( this.playerFacade.playlist!!.metadata.durations?.sum() private fun computeLocator( - item: ItemState, + item: ItemState ): Locator { val playlist = this.playerFacade.playlist!!.map { it.metadata!! } val position = item.position val link = publication.readingOrder[item.index] val itemStartPosition = playlist.slice(0 until item.index).durations?.sum() val totalProgression = - if (itemStartPosition == null) null - else totalDuration?.let { (itemStartPosition + position) / it } + if (itemStartPosition == null) { + null + } else { + totalDuration?.let { (itemStartPosition + position) / it } + } val locator = requireNotNull(publication.locatorFromLink(link)) return locator.copyWithLocations( @@ -151,8 +159,8 @@ class MediaNavigator private constructor( val state = when (currentState) { SessionPlayerState.Playing -> Playback.State.Playing - SessionPlayerState.Idle, SessionPlayerState.Error -> - Playback.State.Error + SessionPlayerState.Idle, SessionPlayerState.Failure -> + Playback.State.Failure SessionPlayerState.Paused -> if (playerCallback.playbackCompleted) { Playback.State.Finished @@ -200,7 +208,7 @@ class MediaNavigator private constructor( /** * Indicates the navigator current state. */ - val playback: StateFlow<Playback> = + public val playback: StateFlow<Playback> = playbackMutable /** @@ -208,35 +216,39 @@ class MediaNavigator private constructor( * * Normal speed is 1.0 and 0.0 is incorrect. */ - suspend fun setPlaybackRate(rate: Double): Try<Unit, Exception> = executeCommand { + public suspend fun setPlaybackRate(rate: Double): Try<Unit, Exception> = executeCommand { playerFacade.setPlaybackSpeed(rate).toNavigatorResult() } /** * Resumes or start the playback at the current location. */ - suspend fun play(): Try<Unit, Exception> = executeCommand { + public suspend fun play(): Try<Unit, Exception> = executeCommand { playerFacade.play().toNavigatorResult() } /** * Pauses the playback. */ - suspend fun pause(): Try<Unit, Exception> = executeCommand { + public suspend fun pause(): Try<Unit, Exception> = executeCommand { playerFacade.pause().toNavigatorResult() } /** * Seeks to the given time at the given resource. */ - suspend fun seek(index: Int, position: Duration): Try<Unit, Exception> = executeCommand { + public suspend fun seek(index: Int, position: Duration): Try<Unit, Exception> = executeCommand { playerFacade.seekTo(index, position).toNavigatorResult() } /** * Seeks to the given locator. */ - suspend fun go(locator: Locator): Try<Unit, Exception> { + @OptIn(DelicateReadiumApi::class) + public suspend fun go(locator: Locator): Try<Unit, Exception> { + @Suppress("NAME_SHADOWING") + val locator = publication.normalizeLocator(locator) + val itemIndex = publication.readingOrder.indexOfFirstWithHref(locator.href) ?: return Try.failure(Exception.InvalidArgument("Invalid href ${locator.href}.")) val position = locator.locations.time ?: Duration.ZERO @@ -247,7 +259,7 @@ class MediaNavigator private constructor( /** * Seeks to the beginning of the given link. */ - suspend fun go(link: Link): Try<Unit, Exception> { + public suspend fun go(link: Link): Try<Unit, Exception> { val locator = publication.locatorFromLink(link) ?: return Try.failure(Exception.InvalidArgument("Resource not found at ${link.href}")) return go(locator) @@ -256,13 +268,13 @@ class MediaNavigator private constructor( /** * Skips to a little amount of time later. */ - suspend fun goForward(): Try<Unit, Exception> = + public suspend fun goForward(): Try<Unit, Exception> = seekBy(configuration.skipForwardInterval) /** * Skips to a little amount of time before. */ - suspend fun goBackward(): Try<Unit, Exception> = + public suspend fun goBackward(): Try<Unit, Exception> = seekBy(-configuration.skipBackwardInterval) private suspend fun seekBy(offset: Duration): Try<Unit, Exception> = executeCommand { @@ -298,7 +310,7 @@ class MediaNavigator private constructor( * Compared to [pause], the navigator may clear its state in whatever way is appropriate. For * example, recovering a player's resources. */ - fun close() { + public fun close() { playerFacade.unregisterPlayerCallback(playerCallback) playerCallback.close() playerFacade.close() @@ -308,50 +320,50 @@ class MediaNavigator private constructor( /** * Builds a [MediaSession] for this navigator. */ - fun session(context: Context, activityIntent: PendingIntent, id: String? = null): MediaSession = + public fun session(context: Context, activityIntent: PendingIntent, id: String? = null): MediaSession = playerFacade.session(context, id, activityIntent) - data class Configuration( + public data class Configuration( val positionRefreshRate: Double = 2.0, // Hz val skipForwardInterval: Duration = 30.seconds, - val skipBackwardInterval: Duration = 30.seconds, + val skipBackwardInterval: Duration = 30.seconds ) @ExperimentalTime - data class Playback( + public data class Playback( val state: State, val rate: Double, val resource: Resource, val buffer: Buffer ) { - enum class State { + public enum class State { Playing, Paused, Finished, - Error + Failure } - data class Resource( + public data class Resource( val index: Int, val link: Link, val position: Duration, val duration: Duration? ) - data class Buffer( + public data class Buffer( val isPlayable: Boolean, val position: Duration ) } - sealed class Exception(override val message: String) : kotlin.Exception(message) { + public sealed class Exception(override val message: String) : kotlin.Exception(message) { - class SessionPlayer internal constructor( + public class SessionPlayer internal constructor( internal val error: SessionPlayerError ) : Exception("${error.name} error occurred in SessionPlayer.") - class InvalidArgument(message: String) : Exception(message) + public class InvalidArgument(message: String) : Exception(message) } /* @@ -371,19 +383,19 @@ class MediaNavigator private constructor( return true } - override fun goForward(animated: Boolean, completion: () -> Unit): Boolean { + public fun goForward(animated: Boolean, completion: () -> Unit): Boolean { launchAndRun({ goForward() }, completion) return true } - override fun goBackward(animated: Boolean, completion: () -> Unit): Boolean { + public fun goBackward(animated: Boolean, completion: () -> Unit): Boolean { launchAndRun({ goBackward() }, completion) return true } - companion object { + public companion object { - suspend fun create( + public suspend fun create( context: Context, publication: Publication, initialLocator: Locator?, @@ -391,7 +403,6 @@ class MediaNavigator private constructor( player: SessionPlayer = createPlayer(context, publication), metadataFactory: MediaMetadataFactory = DefaultMetadataFactory(publication) ): Try<MediaNavigator, Exception> { - val positionRefreshDelay = (1.0 / configuration.positionRefreshRate).seconds val seekCompletedChannel = Channel<Long>(Channel.UNLIMITED) val callback = SessionPlayerCallback(positionRefreshDelay, seekCompletedChannel) @@ -459,9 +470,10 @@ class MediaNavigator private constructor( } internal fun SessionPlayerResult.toNavigatorResult(): Try<Unit, Exception> = - if (isSuccess) + if (isSuccess) { Try.success(Unit) - else + } else { this.mapFailure { Exception.SessionPlayer(it.error) } + } } } diff --git a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/OptIn.kt b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/OptIn.kt index d67319d39d..cb209b8ed3 100644 --- a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/OptIn.kt +++ b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/OptIn.kt @@ -13,5 +13,10 @@ package org.readium.navigator.media2 message = "The new Audiobook navigator is still experimental. The API may be changed in the future without notice." ) @Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY) -annotation class ExperimentalMedia2 +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS, + AnnotationTarget.PROPERTY +) +public annotation class ExperimentalMedia2 diff --git a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/SessionPlayerFacade.kt b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/SessionPlayerFacade.kt index 081f536af9..a8853c2e33 100644 --- a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/SessionPlayerFacade.kt +++ b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/SessionPlayerFacade.kt @@ -44,7 +44,7 @@ import timber.log.Timber internal class SessionPlayerFacade( private val sessionPlayer: SessionPlayer, private val seekCompletedReceiver: ReceiveChannel<Long>, - playerStateFlow: Flow<SessionPlayerState>, + playerStateFlow: Flow<SessionPlayerState> ) { private val coroutineScope = MainScope() diff --git a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/SessionPlayerHelpers.kt b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/SessionPlayerHelpers.kt index f6895327cc..b8750c2fbe 100644 --- a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/SessionPlayerHelpers.kt +++ b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/SessionPlayerHelpers.kt @@ -18,14 +18,14 @@ internal enum class SessionPlayerState { Idle, Paused, Playing, - Error; + Failure; companion object { fun fromCode(sessionPlayerState: Int) = when (sessionPlayerState) { SessionPlayer.PLAYER_STATE_IDLE -> Idle SessionPlayer.PLAYER_STATE_PAUSED -> Paused SessionPlayer.PLAYER_STATE_PLAYING -> Playing - else -> Error // SessionPlayer.PLAYER_STATE_ERROR + else -> Failure // SessionPlayer.PLAYER_STATE_ERROR } } } @@ -106,7 +106,7 @@ internal data class ItemState( val index: Int, val position: Duration, val buffered: Duration, - val duration: Duration?, + val duration: Duration? ) @OptIn(ExperimentalTime::class) @@ -149,10 +149,11 @@ internal val SessionPlayer.currentDuration: Duration? @ExperimentalTime private fun msToDuration(ms: Long): Duration? = - if (ms == SessionPlayer.UNKNOWN_TIME) + if (ms == SessionPlayer.UNKNOWN_TIME) { null - else + } else { ms.milliseconds + } @ExperimentalTime internal val MediaMetadata.duration: Duration? diff --git a/readium/navigator-media2/src/test/java/org/readium/navigator/media2/SmartSeekerTest.kt b/readium/navigator-media2/src/test/java/org/readium/navigator/media2/SmartSeekerTest.kt index 52713327b7..6337faadeb 100644 --- a/readium/navigator-media2/src/test/java/org/readium/navigator/media2/SmartSeekerTest.kt +++ b/readium/navigator-media2/src/test/java/org/readium/navigator/media2/SmartSeekerTest.kt @@ -10,7 +10,14 @@ import org.junit.Test class SmartSeekerTest { private val playlist: List<Duration> = listOf( - 10, 20, 15, 800, 10, 230, 20, 10 + 10, + 20, + 15, + 800, + 10, + 230, + 20, + 10 ).map { it.seconds } private val forwardOffset = 50.seconds diff --git a/readium/navigator/build.gradle.kts b/readium/navigator/build.gradle.kts index e357ad38a6..3efe7385f0 100644 --- a/readium/navigator/build.gradle.kts +++ b/readium/navigator/build.gradle.kts @@ -12,22 +12,21 @@ plugins { } android { - // FIXME: This doesn't pass the lint because some resources don't start with readium_ yet. We need to rename all resources for the next major version. -// resourcePrefix "readium_" + resourcePrefix = "readium_" - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs = freeCompilerArgs + listOf( "-opt-in=kotlin.RequiresOptIn", "-opt-in=org.readium.r2.shared.InternalReadiumApi" @@ -44,10 +43,15 @@ android { } buildFeatures { viewBinding = true + buildConfig = true } namespace = "org.readium.r2.navigator" } +kotlin { + explicitApi() +} + rootProject.ext["publish.artifactId"] = "readium-navigator" apply(from = "$rootDir/scripts/publish-module.gradle") @@ -70,8 +74,6 @@ dependencies { implementation(libs.bundles.media3) implementation(libs.androidx.viewpager2) implementation(libs.androidx.webkit) - // Needed to avoid a crash with API 31, see https://stackoverflow.com/a/69152986/1474476 - implementation("androidx.work:work-runtime-ktx:2.7.1") implementation(libs.bundles.media2) // ExoPlayer is used by the Audio Navigator. diff --git a/readium/navigator/src/main/AndroidManifest.xml b/readium/navigator/src/main/AndroidManifest.xml index 7205db866e..61bba1e2b9 100644 --- a/readium/navigator/src/main/AndroidManifest.xml +++ b/readium/navigator/src/main/AndroidManifest.xml @@ -1,37 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools"> - - <uses-permission android:name="android.permission.WRITE_SETTINGS" - tools:ignore="ProtectedPermissions" /> - - <application> - - <activity - android:name=".epub.R2EpubActivity" - android:theme="@style/AppTheme" /> - <activity - android:name=".cbz.R2CbzActivity" - android:theme="@style/AppTheme" /> - <activity - android:name=".audiobook.R2AudiobookActivity" - android:theme="@style/AppTheme" /> - <activity - android:name=".divina.R2DiViNaActivity" - android:theme="@style/AppTheme" /> - <activity - android:name=".pdf.R2PdfActivity" - android:theme="@style/AppTheme" /> - - </application> - -</manifest> +<manifest /> \ No newline at end of file diff --git a/readium/navigator/src/main/assets/_scripts/package.json b/readium/navigator/src/main/assets/_scripts/package.json index 0cbea619aa..bcc4932da7 100644 --- a/readium/navigator/src/main/assets/_scripts/package.json +++ b/readium/navigator/src/main/assets/_scripts/package.json @@ -20,13 +20,14 @@ "babel-loader": "^8.2.3", "eslint": "^7.29.0", "prettier": "2.3.1", - "webpack": "^5.40.0", - "webpack-cli": "^4.7.2" + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4" }, "dependencies": { "approx-string-match": "^1.1.0", "css-selector-generator": "^3.6.0", "hash.js": "^1.1.7", "string.prototype.matchall": "^4.0.5" - } + }, + "packageManager": "pnpm@8.8.0+sha256.d713a5750e41c3660d1e090608c7f607ad00d1dd5ba9b6552b5f390bf37924e9" } diff --git a/readium/navigator/src/main/assets/_scripts/pnpm-lock.yaml b/readium/navigator/src/main/assets/_scripts/pnpm-lock.yaml new file mode 100644 index 0000000000..1da4d696d4 --- /dev/null +++ b/readium/navigator/src/main/assets/_scripts/pnpm-lock.yaml @@ -0,0 +1,3348 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + approx-string-match: + specifier: ^1.1.0 + version: 1.1.0 + css-selector-generator: + specifier: ^3.6.0 + version: 3.6.4 + hash.js: + specifier: ^1.1.7 + version: 1.1.7 + string.prototype.matchall: + specifier: ^4.0.5 + version: 4.0.10 + +devDependencies: + '@babel/core': + specifier: ^7.16.0 + version: 7.23.0 + '@babel/preset-env': + specifier: ^7.16.0 + version: 7.22.20(@babel/core@7.23.0) + babel-loader: + specifier: ^8.2.3 + version: 8.3.0(@babel/core@7.23.0)(webpack@5.88.2) + eslint: + specifier: ^7.29.0 + version: 7.32.0 + prettier: + specifier: 2.3.1 + version: 2.3.1 + webpack: + specifier: ^5.88.2 + version: 5.88.2(webpack-cli@5.1.4) + webpack-cli: + specifier: ^5.1.4 + version: 5.1.4(webpack@5.88.2) + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + dev: true + + /@babel/code-frame@7.12.11: + resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==} + dependencies: + '@babel/highlight': 7.22.20 + dev: true + + /@babel/code-frame@7.22.13: + resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.22.20 + chalk: 2.4.2 + dev: true + + /@babel/compat-data@7.22.20: + resolution: {integrity: sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.23.0: + resolution: {integrity: sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.22.13 + '@babel/generator': 7.23.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.23.0) + '@babel/helpers': 7.23.1 + '@babel/parser': 7.23.0 + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.0 + '@babel/types': 7.23.0 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.23.0: + resolution: {integrity: sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + jsesc: 2.5.2 + dev: true + + /@babel/helper-annotate-as-pure@7.22.5: + resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15: + resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-compilation-targets@7.22.15: + resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.22.20 + '@babel/helper-validator-option': 7.22.15 + browserslist: 4.22.1 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-create-class-features-plugin@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + semver: 6.3.1 + dev: true + + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-annotate-as-pure': 7.22.5 + regexpu-core: 5.3.2 + semver: 6.3.1 + dev: true + + /@babel/helper-define-polyfill-provider@0.4.2(@babel/core@7.23.0): + resolution: {integrity: sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + debug: 4.3.4 + lodash.debounce: 4.0.8 + resolve: 1.22.6 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-member-expression-to-functions@7.23.0: + resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-module-imports@7.22.15: + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-module-transforms@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + + /@babel/helper-optimise-call-expression@7.22.5: + resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-plugin-utils@7.22.5: + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.23.0): + resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-wrap-function': 7.22.20 + dev: true + + /@babel/helper-replace-supers@7.22.20(@babel/core@7.23.0): + resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + dev: true + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-skip-transparent-expression-wrappers@7.22.5: + resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-string-parser@7.22.5: + resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.22.15: + resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-wrap-function@7.22.20: + resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-function-name': 7.23.0 + '@babel/template': 7.22.15 + '@babel/types': 7.23.0 + dev: true + + /@babel/helpers@7.23.1: + resolution: {integrity: sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.0 + '@babel/types': 7.23.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight@7.22.20: + resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser@7.23.0: + resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.23.0) + dev: true + + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.0): + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + dev: true + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.0): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.0): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.0): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-import-attributes@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.0): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.0): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.0): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.0): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.0): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.0): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.23.0): + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-arrow-functions@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-async-generator-functions@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.0) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-block-scoped-functions@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-block-scoping@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-class-properties@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-class-static-block@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-classes@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.0) + '@babel/helper-split-export-declaration': 7.22.6 + globals: 11.12.0 + dev: true + + /@babel/plugin-transform-computed-properties@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/template': 7.22.15 + dev: true + + /@babel/plugin-transform-destructuring@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-dotall-regex@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-duplicate-keys@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-dynamic-import@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-exponentiation-operator@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-export-namespace-from@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-for-of@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-function-name@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-json-strings@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-literals@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-logical-assignment-operators@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-member-expression-literals@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-amd@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-commonjs@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-systemjs@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + + /@babel/plugin-transform-modules-umd@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-new-target@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-nullish-coalescing-operator@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-numeric-separator@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-object-rest-spread@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.22.20 + '@babel/core': 7.23.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-object-super@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-optional-catch-binding@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-optional-chaining@7.23.0(@babel/core@7.23.0): + resolution: {integrity: sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-parameters@7.22.15(@babel/core@7.23.0): + resolution: {integrity: sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-private-methods@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-private-property-in-object@7.22.11(@babel/core@7.23.0): + resolution: {integrity: sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.0) + dev: true + + /@babel/plugin-transform-property-literals@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-regenerator@7.22.10(@babel/core@7.23.0): + resolution: {integrity: sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + regenerator-transform: 0.15.2 + dev: true + + /@babel/plugin-transform-reserved-words@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-shorthand-properties@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-spread@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + dev: true + + /@babel/plugin-transform-sticky-regex@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-template-literals@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-typeof-symbol@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-escapes@7.22.10(@babel/core@7.23.0): + resolution: {integrity: sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-property-regex@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-regex@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-sets-regex@7.22.5(@babel/core@7.23.0): + resolution: {integrity: sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.0) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/preset-env@7.22.20(@babel/core@7.23.0): + resolution: {integrity: sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.22.20 + '@babel/core': 7.23.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.0) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.0) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-import-assertions': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-syntax-import-attributes': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.23.0) + '@babel/plugin-transform-arrow-functions': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-async-generator-functions': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-block-scoped-functions': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-block-scoping': 7.23.0(@babel/core@7.23.0) + '@babel/plugin-transform-class-properties': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-class-static-block': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-classes': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-transform-computed-properties': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-destructuring': 7.23.0(@babel/core@7.23.0) + '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-duplicate-keys': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-dynamic-import': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-exponentiation-operator': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-export-namespace-from': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-for-of': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-transform-function-name': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-json-strings': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-literals': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-logical-assignment-operators': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-member-expression-literals': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-modules-amd': 7.23.0(@babel/core@7.23.0) + '@babel/plugin-transform-modules-commonjs': 7.23.0(@babel/core@7.23.0) + '@babel/plugin-transform-modules-systemjs': 7.23.0(@babel/core@7.23.0) + '@babel/plugin-transform-modules-umd': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-new-target': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-numeric-separator': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-object-rest-spread': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-transform-object-super': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-optional-catch-binding': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.23.0) + '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.23.0) + '@babel/plugin-transform-private-methods': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-private-property-in-object': 7.22.11(@babel/core@7.23.0) + '@babel/plugin-transform-property-literals': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-regenerator': 7.22.10(@babel/core@7.23.0) + '@babel/plugin-transform-reserved-words': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-shorthand-properties': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-spread': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-sticky-regex': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-template-literals': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-typeof-symbol': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-unicode-escapes': 7.22.10(@babel/core@7.23.0) + '@babel/plugin-transform-unicode-property-regex': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.23.0) + '@babel/plugin-transform-unicode-sets-regex': 7.22.5(@babel/core@7.23.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.23.0) + '@babel/types': 7.23.0 + babel-plugin-polyfill-corejs2: 0.4.5(@babel/core@7.23.0) + babel-plugin-polyfill-corejs3: 0.8.4(@babel/core@7.23.0) + babel-plugin-polyfill-regenerator: 0.5.2(@babel/core@7.23.0) + core-js-compat: 3.33.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.23.0): + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/types': 7.23.0 + esutils: 2.0.3 + dev: true + + /@babel/regjsgen@0.8.0: + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + dev: true + + /@babel/runtime@7.23.1: + resolution: {integrity: sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: true + + /@babel/template@7.22.15: + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.13 + '@babel/parser': 7.23.0 + '@babel/types': 7.23.0 + dev: true + + /@babel/traverse@7.23.0: + resolution: {integrity: sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.13 + '@babel/generator': 7.23.0 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.0 + '@babel/types': 7.23.0 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.23.0: + resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + + /@discoveryjs/json-ext@0.5.7: + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + dev: true + + /@eslint/eslintrc@0.4.3: + resolution: {integrity: sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 7.3.1 + globals: 13.22.0 + ignore: 4.0.6 + import-fresh: 3.3.0 + js-yaml: 3.14.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/config-array@0.5.0: + resolution: {integrity: sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/object-schema@1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.19 + dev: true + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/source-map@0.3.5: + resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@types/eslint-scope@3.7.5: + resolution: {integrity: sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==} + dependencies: + '@types/eslint': 8.44.3 + '@types/estree': 1.0.2 + dev: true + + /@types/eslint@8.44.3: + resolution: {integrity: sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==} + dependencies: + '@types/estree': 1.0.2 + '@types/json-schema': 7.0.13 + dev: true + + /@types/estree@1.0.2: + resolution: {integrity: sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==} + dev: true + + /@types/json-schema@7.0.13: + resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==} + dev: true + + /@types/node@20.8.2: + resolution: {integrity: sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==} + dev: true + + /@webassemblyjs/ast@1.11.6: + resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==} + dependencies: + '@webassemblyjs/helper-numbers': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + dev: true + + /@webassemblyjs/floating-point-hex-parser@1.11.6: + resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} + dev: true + + /@webassemblyjs/helper-api-error@1.11.6: + resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} + dev: true + + /@webassemblyjs/helper-buffer@1.11.6: + resolution: {integrity: sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==} + dev: true + + /@webassemblyjs/helper-numbers@1.11.6: + resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/helper-wasm-bytecode@1.11.6: + resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} + dev: true + + /@webassemblyjs/helper-wasm-section@1.11.6: + resolution: {integrity: sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + dev: true + + /@webassemblyjs/ieee754@1.11.6: + resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} + dependencies: + '@xtuc/ieee754': 1.2.0 + dev: true + + /@webassemblyjs/leb128@1.11.6: + resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} + dependencies: + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/utf8@1.11.6: + resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} + dev: true + + /@webassemblyjs/wasm-edit@1.11.6: + resolution: {integrity: sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/helper-wasm-section': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + '@webassemblyjs/wasm-opt': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + '@webassemblyjs/wast-printer': 1.11.6 + dev: true + + /@webassemblyjs/wasm-gen@1.11.6: + resolution: {integrity: sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + dev: true + + /@webassemblyjs/wasm-opt@1.11.6: + resolution: {integrity: sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + dev: true + + /@webassemblyjs/wasm-parser@1.11.6: + resolution: {integrity: sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + dev: true + + /@webassemblyjs/wast-printer@1.11.6: + resolution: {integrity: sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@xtuc/long': 4.2.2 + dev: true + + /@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.88.2): + resolution: {integrity: sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + dependencies: + webpack: 5.88.2(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack@5.88.2) + dev: true + + /@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.88.2): + resolution: {integrity: sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + dependencies: + webpack: 5.88.2(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack@5.88.2) + dev: true + + /@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.88.2): + resolution: {integrity: sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + webpack-dev-server: '*' + peerDependenciesMeta: + webpack-dev-server: + optional: true + dependencies: + webpack: 5.88.2(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack@5.88.2) + dev: true + + /@xtuc/ieee754@1.2.0: + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + dev: true + + /@xtuc/long@4.2.2: + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + dev: true + + /acorn-import-assertions@1.9.0(acorn@8.10.0): + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.10.0 + dev: true + + /acorn-jsx@5.3.2(acorn@7.4.1): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 7.4.1 + dev: true + + /acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ajv-keywords@3.5.2(ajv@6.12.6): + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + dependencies: + ajv: 6.12.6 + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: true + + /ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /approx-string-match@1.1.0: + resolution: {integrity: sha512-j1yQB9XhfGWsvTfHEuNsR/SrUT4XQDkAc0PEjMifyi97931LmNQyLsO6HbuvZ3HeMx+3Dvk8m8XGkUF+8lCeqw==} + dev: false + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: false + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: false + + /astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + dev: true + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: false + + /babel-loader@8.3.0(@babel/core@7.23.0)(webpack@5.88.2): + resolution: {integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==} + engines: {node: '>= 8.9'} + peerDependencies: + '@babel/core': ^7.0.0 + webpack: '>=2' + dependencies: + '@babel/core': 7.23.0 + find-cache-dir: 3.3.2 + loader-utils: 2.0.4 + make-dir: 3.1.0 + schema-utils: 2.7.1 + webpack: 5.88.2(webpack-cli@5.1.4) + dev: true + + /babel-plugin-polyfill-corejs2@0.4.5(@babel/core@7.23.0): + resolution: {integrity: sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/compat-data': 7.22.20 + '@babel/core': 7.23.0 + '@babel/helper-define-polyfill-provider': 0.4.2(@babel/core@7.23.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-corejs3@0.8.4(@babel/core@7.23.0): + resolution: {integrity: sha512-9l//BZZsPR+5XjyJMPtZSK4jv0BsTO1zDac2GC6ygx9WLGlcsnRd1Co0B2zT5fF5Ic6BZy+9m3HNZ3QcOeDKfg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-define-polyfill-provider': 0.4.2(@babel/core@7.23.0) + core-js-compat: 3.33.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-regenerator@0.5.2(@babel/core@7.23.0): + resolution: {integrity: sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.0 + '@babel/helper-define-polyfill-provider': 0.4.2(@babel/core@7.23.0) + transitivePeerDependencies: + - supports-color + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /browserslist@4.22.1: + resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001546 + electron-to-chromium: 1.4.542 + node-releases: 2.0.13 + update-browserslist-db: 1.0.13(browserslist@4.22.1) + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: false + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /caniuse-lite@1.0.30001546: + resolution: {integrity: sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==} + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chrome-trace-event@1.0.3: + resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} + engines: {node: '>=6.0'} + dev: true + + /clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + dev: true + + /commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + dev: true + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + + /commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /core-js-compat@3.33.0: + resolution: {integrity: sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==} + dependencies: + browserslist: 4.22.1 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /css-selector-generator@3.6.4: + resolution: {integrity: sha512-/VDUW5KAoxGXJPNcn+5Y0DLJMQumLk9zNMWl2NChuTwGLEFeqfnddCpRhY6uZ77ar0uyM7W7ilvIWsk7DpjKxQ==} + dev: false + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /define-data-property@1.1.0: + resolution: {integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + dev: false + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: false + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /electron-to-chromium@1.4.542: + resolution: {integrity: sha512-6+cpa00G09N3sfh2joln4VUXHquWrOFx3FLZqiVQvl45+zS9DskDBTPvob+BhvFRmTBkyDSk0vvLMMRo/qc6mQ==} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + dev: true + + /enhanced-resolve@5.15.0: + resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: true + + /enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + dev: true + + /envinfo@7.10.0: + resolution: {integrity: sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /es-abstract@1.22.2: + resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.4 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + dev: false + + /es-module-lexer@1.3.1: + resolution: {integrity: sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==} + dev: true + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.4 + has-tostringtag: 1.0.0 + dev: false + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: false + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + dependencies: + eslint-visitor-keys: 1.3.0 + dev: true + + /eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + dev: true + + /eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: true + + /eslint@7.32.0: + resolution: {integrity: sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==} + engines: {node: ^10.12.0 || >=12.0.0} + hasBin: true + dependencies: + '@babel/code-frame': 7.12.11 + '@eslint/eslintrc': 0.4.3 + '@humanwhocodes/config-array': 0.5.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + enquirer: 2.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 5.1.1 + eslint-utils: 2.1.0 + eslint-visitor-keys: 2.1.0 + espree: 7.3.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + functional-red-black-tree: 1.0.1 + glob-parent: 5.1.2 + globals: 13.22.0 + ignore: 4.0.6 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + js-yaml: 3.14.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + progress: 2.0.3 + regexpp: 3.2.0 + semver: 7.5.4 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + table: 6.8.1 + text-table: 0.2.0 + v8-compile-cache: 2.4.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@7.3.1: + resolution: {integrity: sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + acorn: 7.4.1 + acorn-jsx: 5.3.2(acorn@7.4.1) + eslint-visitor-keys: 1.3.0 + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.1.0 + dev: true + + /find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + dev: true + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.1.0: + resolution: {integrity: sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==} + engines: {node: '>=12.0.0'} + dependencies: + flatted: 3.2.9 + keyv: 4.5.3 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: false + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + functions-have-names: 1.2.3 + dev: false + + /functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: false + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.4 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: false + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globals@13.22.0: + resolution: {integrity: sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: false + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: false + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: false + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: false + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /has@1.0.4: + resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} + engines: {node: '>= 0.4.0'} + + /hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: false + + /ignore@4.0.6: + resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.4 + side-channel: 1.0.4 + dev: false + + /interpret@3.1.1: + resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} + engines: {node: '>=10.13.0'} + dev: true + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: false + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: false + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: false + + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.4 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: false + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: false + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.11 + dev: false + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: false + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: false + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + dev: true + + /jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 20.8.2 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + + /jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /keyv@4.5.3: + resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + dev: true + + /loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.1 + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: true + + /node-releases@2.0.13: + resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} + dev: true + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: false + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: false + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier@2.3.1: + resolution: {integrity: sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + dev: true + + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + dev: true + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + dependencies: + resolve: 1.22.6 + dev: true + + /regenerate-unicode-properties@10.1.1: + resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + dev: true + + /regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + dev: true + + /regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + dev: true + + /regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + dependencies: + '@babel/runtime': 7.23.1 + dev: true + + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + set-function-name: 2.0.1 + dev: false + + /regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + + /regexpu-core@5.3.2: + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} + engines: {node: '>=4'} + dependencies: + '@babel/regjsgen': 0.8.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.1 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + dev: true + + /regjsparser@0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + dependencies: + jsesc: 0.5.0 + dev: true + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + + /resolve@1.22.6: + resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: false + + /schema-utils@2.7.1: + resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} + engines: {node: '>= 8.9.0'} + dependencies: + '@types/json-schema': 7.0.13 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: true + + /schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.13 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /serialize-javascript@6.0.1: + resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} + dependencies: + randombytes: 2.1.0 + dev: true + + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.0 + dev: false + + /shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + dependencies: + kind-of: 6.0.3 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: false + + /slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string.prototype.matchall@4.0.10: + resolution: {integrity: sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + regexp.prototype.flags: 1.5.1 + set-function-name: 2.0.1 + side-channel: 1.0.4 + dev: false + + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: false + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: false + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: false + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /table@6.8.1: + resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} + engines: {node: '>=10.0.0'} + dependencies: + ajv: 8.12.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: true + + /terser-webpack-plugin@5.3.9(webpack@5.88.2): + resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.1 + terser: 5.21.0 + webpack: 5.88.2(webpack-cli@5.1.4) + dev: true + + /terser@5.21.0: + resolution: {integrity: sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.5 + acorn: 8.10.0 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: false + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: false + + /unicode-canonical-property-names-ecmascript@2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + dev: true + + /unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + dev: true + + /unicode-match-property-value-ecmascript@2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + dev: true + + /unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + dev: true + + /update-browserslist-db@1.0.13(browserslist@4.22.1): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.22.1 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + dev: true + + /v8-compile-cache@2.4.0: + resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} + dev: true + + /watchpack@2.4.0: + resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + dev: true + + /webpack-cli@5.1.4(webpack@5.88.2): + resolution: {integrity: sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==} + engines: {node: '>=14.15.0'} + hasBin: true + peerDependencies: + '@webpack-cli/generators': '*' + webpack: 5.x.x + webpack-bundle-analyzer: '*' + webpack-dev-server: '*' + peerDependenciesMeta: + '@webpack-cli/generators': + optional: true + webpack-bundle-analyzer: + optional: true + webpack-dev-server: + optional: true + dependencies: + '@discoveryjs/json-ext': 0.5.7 + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.88.2) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.88.2) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack@5.88.2) + colorette: 2.0.20 + commander: 10.0.1 + cross-spawn: 7.0.3 + envinfo: 7.10.0 + fastest-levenshtein: 1.0.16 + import-local: 3.1.0 + interpret: 3.1.1 + rechoir: 0.8.0 + webpack: 5.88.2(webpack-cli@5.1.4) + webpack-merge: 5.9.0 + dev: true + + /webpack-merge@5.9.0: + resolution: {integrity: sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==} + engines: {node: '>=10.0.0'} + dependencies: + clone-deep: 4.0.1 + wildcard: 2.0.1 + dev: true + + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: true + + /webpack@5.88.2(webpack-cli@5.1.4): + resolution: {integrity: sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.5 + '@types/estree': 1.0.2 + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/wasm-edit': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + acorn: 8.10.0 + acorn-import-assertions: 1.9.0(acorn@8.10.0) + browserslist: 4.22.1 + chrome-trace-event: 1.0.3 + enhanced-resolve: 5.15.0 + es-module-lexer: 1.3.1 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.9(webpack@5.88.2) + watchpack: 2.4.0 + webpack-cli: 5.1.4(webpack@5.88.2) + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: false + + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: false + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true diff --git a/readium/navigator/src/main/assets/_scripts/src/dom.js b/readium/navigator/src/main/assets/_scripts/src/dom.js index befcaceda9..89ad87e50e 100644 --- a/readium/navigator/src/main/assets/_scripts/src/dom.js +++ b/readium/navigator/src/main/assets/_scripts/src/dom.js @@ -7,6 +7,45 @@ import { isScrollModeEnabled } from "./utils"; import { getCssSelector } from "css-selector-generator"; +// See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling +export function nearestInteractiveElement(element) { + if (element == null) { + return null; + } + var interactiveTags = [ + "a", + "audio", + "button", + "canvas", + "details", + "input", + "label", + "option", + "select", + "submit", + "textarea", + "video", + ]; + if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) { + return element.outerHTML; + } + + // Checks whether the element is editable by the user. + if ( + element.hasAttribute("contenteditable") && + element.getAttribute("contenteditable").toLowerCase() != "false" + ) { + return element.outerHTML; + } + + // Checks parents recursively because the touch might be for example on an <em> inside a <a>. + if (element.parentElement) { + return nearestInteractiveElement(element.parentElement); + } + + return null; +} + export function findFirstVisibleLocator() { const element = findElement(document.body); return { diff --git a/readium/navigator/src/main/assets/_scripts/src/gestures.js b/readium/navigator/src/main/assets/_scripts/src/gestures.js index a49f837057..9bee231b02 100644 --- a/readium/navigator/src/main/assets/_scripts/src/gestures.js +++ b/readium/navigator/src/main/assets/_scripts/src/gestures.js @@ -5,6 +5,7 @@ */ import { handleDecorationClickEvent } from "./decorator"; +import { nearestInteractiveElement } from "./dom"; window.addEventListener("DOMContentLoaded", function () { document.addEventListener("click", onClick, false); @@ -103,39 +104,3 @@ function bindDragGesture(element) { state = undefined; } } - -// See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling -function nearestInteractiveElement(element) { - var interactiveTags = [ - "a", - "audio", - "button", - "canvas", - "details", - "input", - "label", - "option", - "select", - "submit", - "textarea", - "video", - ]; - if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) { - return element.outerHTML; - } - - // Checks whether the element is editable by the user. - if ( - element.hasAttribute("contenteditable") && - element.getAttribute("contenteditable").toLowerCase() != "false" - ) { - return element.outerHTML; - } - - // Checks parents recursively because the touch might be for example on an <em> inside a <a>. - if (element.parentElement) { - return nearestInteractiveElement(element.parentElement); - } - - return null; -} diff --git a/readium/navigator/src/main/assets/_scripts/src/highlight.js b/readium/navigator/src/main/assets/_scripts/src/highlight.js deleted file mode 100644 index fda03f35df..0000000000 --- a/readium/navigator/src/main/assets/_scripts/src/highlight.js +++ /dev/null @@ -1,2362 +0,0 @@ -/* eslint-disable */ -// -// highlight.js -// r2-navigator-kotlin -// -// Organized by Taehyun Kim on 6/27/19 from r2-navigator-js. -// -// Copyright 2019 Readium Foundation. All rights reserved. -// Use of this source code is governed by a BSD-style license which is detailed -// in the LICENSE file present in the project repository where this source code is maintained. -// - -const ROOT_CLASS_REDUCE_MOTION = "r2-reduce-motion"; -const ROOT_CLASS_NO_FOOTNOTES = "r2-no-popup-foonotes"; -const POPUP_DIALOG_CLASS = "r2-popup-dialog"; -const FOOTNOTES_CONTAINER_CLASS = "r2-footnote-container"; -const FOOTNOTES_CLOSE_BUTTON_CLASS = "r2-footnote-close"; -const FOOTNOTE_FORCE_SHOW = "r2-footnote-force-show"; -const TTS_ID_PREVIOUS = "r2-tts-previous"; -const TTS_ID_NEXT = "r2-tts-next"; -const TTS_ID_SLIDER = "r2-tts-slider"; -const TTS_ID_ACTIVE_WORD = "r2-tts-active-word"; -const TTS_ID_CONTAINER = "r2-tts-txt"; -const TTS_ID_INFO = "r2-tts-info"; -const TTS_NAV_BUTTON_CLASS = "r2-tts-button"; -const TTS_ID_SPEAKING_DOC_ELEMENT = "r2-tts-speaking-el"; -const TTS_CLASS_INJECTED_SPAN = "r2-tts-speaking-txt"; -const TTS_CLASS_INJECTED_SUBSPAN = "r2-tts-speaking-word"; -const TTS_ID_INJECTED_PARENT = "r2-tts-speaking-txt-parent"; -const ID_HIGHLIGHTS_CONTAINER = "R2_ID_HIGHLIGHTS_CONTAINER"; -const ID_ANNOTATION_CONTAINER = "R2_ID_ANNOTATION_CONTAINER"; -const CLASS_HIGHLIGHT_CONTAINER = "R2_CLASS_HIGHLIGHT_CONTAINER"; -const CLASS_ANNOTATION_CONTAINER = "R2_CLASS_ANNOTATION_CONTAINER"; -const CLASS_HIGHLIGHT_AREA = "R2_CLASS_HIGHLIGHT_AREA"; -const CLASS_ANNOTATION_AREA = "R2_CLASS_ANNOTATION_AREA"; -const CLASS_HIGHLIGHT_BOUNDING_AREA = "R2_CLASS_HIGHLIGHT_BOUNDING_AREA"; -const CLASS_ANNOTATION_BOUNDING_AREA = "R2_CLASS_ANNOTATION_BOUNDING_AREA"; -// tslint:disable-next-line:max-line-length -const _blacklistIdClassForCFI = [ - POPUP_DIALOG_CLASS, - TTS_CLASS_INJECTED_SPAN, - TTS_CLASS_INJECTED_SUBSPAN, - ID_HIGHLIGHTS_CONTAINER, - CLASS_HIGHLIGHT_CONTAINER, - CLASS_HIGHLIGHT_AREA, - CLASS_HIGHLIGHT_BOUNDING_AREA, - "resize-sensor", -]; -const CLASS_PAGINATED = "r2-css-paginated"; - -//const IS_DEV = (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "dev"); -const IS_DEV = false; -const _highlights = []; - -let _highlightsContainer; -let _annotationContainer; -let lastMouseDownX = -1; -let lastMouseDownY = -1; -let bodyEventListenersSet = false; - -const USE_SVG = false; -const DEFAULT_BACKGROUND_COLOR_OPACITY = 0.3; -const ALT_BACKGROUND_COLOR_OPACITY = 0.45; - -//const DEBUG_VISUALS = false; -const DEBUG_VISUALS = false; -const DEFAULT_BACKGROUND_COLOR = { - blue: 100, - green: 50, - red: 230, -}; - -const ANNOTATION_WIDTH = 15; - -function resetHighlightBoundingStyle(_win, highlightBounding) { - if ( - highlightBounding.getAttribute("class") == CLASS_ANNOTATION_BOUNDING_AREA - ) { - return; - } - highlightBounding.style.outline = "none"; - highlightBounding.style.setProperty( - "background-color", - "transparent", - "important" - ); -} - -function setHighlightAreaStyle(win, highlightAreas, highlight) { - const useSVG = !DEBUG_VISUALS && USE_SVG; - for (const highlightArea of highlightAreas) { - const isSVG = useSVG && highlightArea.namespaceURI === SVG_XML_NAMESPACE; - const opacity = ALT_BACKGROUND_COLOR_OPACITY; - if (isSVG) { - highlightArea.style.setProperty( - "fill", - `rgb(${highlight.color.red}, ${highlight.color.green}, ${highlight.color.blue})`, - "important" - ); - highlightArea.style.setProperty( - "fill-opacity", - `${opacity}`, - "important" - ); - highlightArea.style.setProperty( - "stroke", - `rgb(${highlight.color.red}, ${highlight.color.green}, ${highlight.color.blue})`, - "important" - ); - highlightArea.style.setProperty( - "stroke-opacity", - `${opacity}`, - "important" - ); - } else { - highlightArea.style.setProperty( - "background-color", - `rgba(${highlight.color.red}, ${highlight.color.green}, ${highlight.color.blue}, ${opacity})`, - "important" - ); - } - } -} - -function resetHighlightAreaStyle(win, highlightArea) { - const useSVG = !DEBUG_VISUALS && USE_SVG; - //const useSVG = USE_SVG; - const isSVG = useSVG && highlightArea.namespaceURI === SVG_XML_NAMESPACE; - const id = isSVG - ? highlightArea.parentNode && - highlightArea.parentNode.parentNode && - highlightArea.parentNode.parentNode.nodeType === Node.ELEMENT_NODE && - highlightArea.parentNode.parentNode.getAttribute - ? highlightArea.parentNode.parentNode.getAttribute("id") - : undefined - : highlightArea.parentNode && - highlightArea.parentNode.nodeType === Node.ELEMENT_NODE && - highlightArea.parentNode.getAttribute - ? highlightArea.parentNode.getAttribute("id") - : undefined; - if (id) { - const highlight = _highlights.find((h) => { - return h.id === id; - }); - if (highlight) { - const opacity = DEFAULT_BACKGROUND_COLOR_OPACITY; - if (isSVG) { - highlightArea.style.setProperty( - "fill", - `rgb(${highlight.color.red}, ${highlight.color.green}, ${highlight.color.blue})`, - "important" - ); - highlightArea.style.setProperty( - "fill-opacity", - `${opacity}`, - "important" - ); - highlightArea.style.setProperty( - "stroke", - `rgb(${highlight.color.red}, ${highlight.color.green}, ${highlight.color.blue})`, - "important" - ); - highlightArea.style.setProperty( - "stroke-opacity", - `${opacity}`, - "important" - ); - } else { - highlightArea.style.setProperty( - "background-color", - `rgba(${highlight.color.red}, ${highlight.color.green}, ${highlight.color.blue}, ${opacity})`, - "important" - ); - } - } - } -} -function processTouchEvent(win, ev) { - const document = win.document; - const scrollElement = getScrollingElement(document); - const x = ev.changedTouches[0].clientX; - const y = ev.changedTouches[0].clientY; - if (!_highlightsContainer) { - return; - } - const paginated = isPaginated(document); - const bodyRect = document.body.getBoundingClientRect(); - let xOffset; - let yOffset; - if (navigator.userAgent.match(/Android/i)) { - xOffset = paginated ? -scrollElement.scrollLeft : bodyRect.left; - yOffset = paginated ? -scrollElement.scrollTop : bodyRect.top; - } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) { - xOffset = paginated ? 0 : -scrollElement.scrollLeft; - yOffset = paginated ? 0 : bodyRect.top; - } - let foundHighlight; - let foundElement; - let foundRect; - // _highlights.sort(function(a, b) { - // console.log(JSON.stringify(a.selectionInfo)) - // return a.selectionInfo.cleanText.length < b.selectionInfo.cleanText.length - // }) - for (let i = _highlights.length - 1; i >= 0; i--) { - const highlight = _highlights[i]; - let highlightParent = document.getElementById(`${highlight.id}`); - if (!highlightParent) { - highlightParent = _highlightsContainer.querySelector(`#${highlight.id}`); - } - if (!highlightParent) { - continue; - } - let hit = false; - const highlightFragments = highlightParent.querySelectorAll( - `.${CLASS_HIGHLIGHT_AREA}` - ); - for (const highlightFragment of highlightFragments) { - const withRect = highlightFragment; - const left = withRect.rect.left + xOffset; - const top = withRect.rect.top + yOffset; - foundRect = withRect.rect; - if ( - x >= left && - x < left + withRect.rect.width && - y >= top && - y < top + withRect.rect.height - ) { - hit = true; - break; - } - } - if (hit) { - foundHighlight = highlight; - foundElement = highlightParent; - break; - } - } - if (!foundHighlight || !foundElement) { - const highlightBoundings = _highlightsContainer.querySelectorAll( - `.${CLASS_HIGHLIGHT_BOUNDING_AREA}` - ); - for (const highlightBounding of highlightBoundings) { - resetHighlightBoundingStyle(win, highlightBounding); - } - const allHighlightAreas = Array.from( - _highlightsContainer.querySelectorAll(`.${CLASS_HIGHLIGHT_AREA}`) - ); - for (const highlightArea of allHighlightAreas) { - resetHighlightAreaStyle(win, highlightArea); - } - return; - } - - if (foundElement.getAttribute("data-click")) { - if (ev.type === "mousemove") { - const foundElementHighlightAreas = Array.from( - foundElement.querySelectorAll(`.${CLASS_HIGHLIGHT_AREA}`) - ); - const allHighlightAreas = _highlightsContainer.querySelectorAll( - `.${CLASS_HIGHLIGHT_AREA}` - ); - for (const highlightArea of allHighlightAreas) { - if (foundElementHighlightAreas.indexOf(highlightArea) < 0) { - resetHighlightAreaStyle(win, highlightArea); - } - } - setHighlightAreaStyle(win, foundElementHighlightAreas, foundHighlight); - const foundElementHighlightBounding = foundElement.querySelector( - `.${CLASS_HIGHLIGHT_BOUNDING_AREA}` - ); - const allHighlightBoundings = _highlightsContainer.querySelectorAll( - `.${CLASS_HIGHLIGHT_BOUNDING_AREA}` - ); - for (const highlightBounding of allHighlightBoundings) { - if ( - !foundElementHighlightBounding || - highlightBounding !== foundElementHighlightBounding - ) { - resetHighlightBoundingStyle(win, highlightBounding); - } - } - if (foundElementHighlightBounding) { - if (DEBUG_VISUALS) { - setHighlightBoundingStyle( - win, - foundElementHighlightBounding, - foundHighlight - ); - } - } - } else if (ev.type === "touchstart" || ev.type === "touchend") { - const size = { - screenWidth: window.outerWidth, - screenHeight: window.outerHeight, - left: foundRect.left, - width: foundRect.width, - top: foundRect.top, - height: foundRect.height, - }; - const payload = { - highlight: foundHighlight.id, - size: size, - }; - - if ( - typeof window !== "undefined" && - typeof window.process === "object" && - window.process.type === "renderer" - ) { - electron_1.ipcRenderer.sendToHost(R2_EVENT_HIGHLIGHT_CLICK, payload); - } else if (window.webkitURL) { - console.log(foundHighlight.id.includes("R2_ANNOTATION_")); - if (foundHighlight.id.search("R2_ANNOTATION_") >= 0) { - if (navigator.userAgent.match(/Android/i)) { - Android.highlightAnnotationMarkActivated(foundHighlight.id); - } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) { - webkit.messageHandlers.highlightAnnotationMarkActivated.postMessage( - foundHighlight.id - ); - } - } else if (foundHighlight.id.search("R2_HIGHLIGHT_") >= 0) { - if (navigator.userAgent.match(/Android/i)) { - Android.highlightActivated(foundHighlight.id); - } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) { - webkit.messageHandlers.highlightActivated.postMessage( - foundHighlight.id - ); - } - } - } - - ev.stopPropagation(); - ev.preventDefault(); - } - } -} - -function processMouseEvent(win, ev) { - const document = win.document; - const scrollElement = getScrollingElement(document); - const x = ev.clientX; - const y = ev.clientY; - if (!_highlightsContainer) { - return; - } - - const paginated = isPaginated(document); - const bodyRect = document.body.getBoundingClientRect(); - let xOffset; - let yOffset; - if (navigator.userAgent.match(/Android/i)) { - xOffset = paginated ? -scrollElement.scrollLeft : bodyRect.left; - yOffset = paginated ? -scrollElement.scrollTop : bodyRect.top; - } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) { - xOffset = paginated ? 0 : -scrollElement.scrollLeft; - yOffset = paginated ? 0 : bodyRect.top; - } - let foundHighlight; - let foundElement; - let foundRect; - for (let i = _highlights.length - 1; i >= 0; i--) { - const highlight = _highlights[i]; - let highlightParent = document.getElementById(`${highlight.id}`); - if (!highlightParent) { - highlightParent = _highlightsContainer.querySelector(`#${highlight.id}`); - } - if (!highlightParent) { - continue; - } - let hit = false; - const highlightFragments = highlightParent.querySelectorAll( - `.${CLASS_HIGHLIGHT_AREA}` - ); - for (const highlightFragment of highlightFragments) { - const withRect = highlightFragment; - const left = withRect.rect.left + xOffset; - const top = withRect.rect.top + yOffset; - foundRect = withRect.rect; - if ( - x >= left && - x < left + withRect.rect.width && - y >= top && - y < top + withRect.rect.height - ) { - hit = true; - break; - } - } - if (hit) { - foundHighlight = highlight; - foundElement = highlightParent; - break; - } - } - - if (!foundHighlight || !foundElement) { - const highlightBoundings = _highlightsContainer.querySelectorAll( - `.${CLASS_HIGHLIGHT_BOUNDING_AREA}` - ); - for (const highlightBounding of highlightBoundings) { - resetHighlightBoundingStyle(win, highlightBounding); - } - const allHighlightAreas = Array.from( - _highlightsContainer.querySelectorAll(`.${CLASS_HIGHLIGHT_AREA}`) - ); - for (const highlightArea of allHighlightAreas) { - resetHighlightAreaStyle(win, highlightArea); - } - return; - } - - if (foundElement.getAttribute("data-click")) { - if (ev.type === "mousemove") { - const foundElementHighlightAreas = Array.from( - foundElement.querySelectorAll(`.${CLASS_HIGHLIGHT_AREA}`) - ); - const allHighlightAreas = _highlightsContainer.querySelectorAll( - `.${CLASS_HIGHLIGHT_AREA}` - ); - for (const highlightArea of allHighlightAreas) { - if (foundElementHighlightAreas.indexOf(highlightArea) < 0) { - resetHighlightAreaStyle(win, highlightArea); - } - } - setHighlightAreaStyle(win, foundElementHighlightAreas, foundHighlight); - const foundElementHighlightBounding = foundElement.querySelector( - `.${CLASS_HIGHLIGHT_BOUNDING_AREA}` - ); - const allHighlightBoundings = _highlightsContainer.querySelectorAll( - `.${CLASS_HIGHLIGHT_BOUNDING_AREA}` - ); - for (const highlightBounding of allHighlightBoundings) { - if ( - !foundElementHighlightBounding || - highlightBounding !== foundElementHighlightBounding - ) { - resetHighlightBoundingStyle(win, highlightBounding); - } - } - if (foundElementHighlightBounding) { - if (DEBUG_VISUALS) { - setHighlightBoundingStyle( - win, - foundElementHighlightBounding, - foundHighlight - ); - } - } - } else if (ev.type === "mouseup" || ev.type === "touchend") { - const touchedPosition = { - screenWidth: window.outerWidth, - screenHeight: window.innerHeight, - left: foundRect.left, - width: foundRect.width, - top: foundRect.top, - height: foundRect.height, - }; - - const payload = { - highlight: foundHighlight, - position: touchedPosition, - }; - - if ( - typeof window !== "undefined" && - typeof window.process === "object" && - window.process.type === "renderer" - ) { - electron_1.ipcRenderer.sendToHost(R2_EVENT_HIGHLIGHT_CLICK, payload); - } else if (window.webkitURL) { - if (foundHighlight.id.search("R2_ANNOTATION_") >= 0) { - if (navigator.userAgent.match(/Android/i)) { - Android.highlightAnnotationMarkActivated(foundHighlight.id); - } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) { - webkit.messageHandlers.highlightAnnotationMarkActivated.postMessage( - foundHighlight.id - ); - } - } else if (foundHighlight.id.search("R2_HIGHLIGHT_") >= 0) { - if (navigator.userAgent.match(/Android/i)) { - Android.highlightActivated(foundHighlight.id); - } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) { - webkit.messageHandlers.highlightActivated.postMessage( - foundHighlight.id - ); - } - } - } - - ev.stopPropagation(); - } - } -} - -function rectsTouchOrOverlap(rect1, rect2, tolerance) { - return ( - (rect1.left < rect2.right || - (tolerance >= 0 && almostEqual(rect1.left, rect2.right, tolerance))) && - (rect2.left < rect1.right || - (tolerance >= 0 && almostEqual(rect2.left, rect1.right, tolerance))) && - (rect1.top < rect2.bottom || - (tolerance >= 0 && almostEqual(rect1.top, rect2.bottom, tolerance))) && - (rect2.top < rect1.bottom || - (tolerance >= 0 && almostEqual(rect2.top, rect1.bottom, tolerance))) - ); -} - -function replaceOverlapingRects(rects) { - for (let i = 0; i < rects.length; i++) { - for (let j = i + 1; j < rects.length; j++) { - const rect1 = rects[i]; - const rect2 = rects[j]; - if (rect1 === rect2) { - if (IS_DEV) { - console.log("replaceOverlapingRects rect1 === rect2 ??!"); - } - continue; - } - if (rectsTouchOrOverlap(rect1, rect2, -1)) { - let toAdd = []; - let toRemove; - let toPreserve; - const subtractRects1 = rectSubtract(rect1, rect2); - if (subtractRects1.length === 1) { - toAdd = subtractRects1; - toRemove = rect1; - toPreserve = rect2; - } else { - const subtractRects2 = rectSubtract(rect2, rect1); - if (subtractRects1.length < subtractRects2.length) { - toAdd = subtractRects1; - toRemove = rect1; - toPreserve = rect2; - } else { - toAdd = subtractRects2; - toRemove = rect2; - toPreserve = rect1; - } - } - if (IS_DEV) { - const toCheck = []; - toCheck.push(toPreserve); - Array.prototype.push.apply(toCheck, toAdd); - checkOverlaps(toCheck); - } - if (IS_DEV) { - console.log( - `CLIENT RECT: overlap, cut one rect into ${toAdd.length}` - ); - } - const newRects = rects.filter((rect) => { - return rect !== toRemove; - }); - Array.prototype.push.apply(newRects, toAdd); - return replaceOverlapingRects(newRects); - } - } - } - return rects; -} - -function checkOverlaps(rects) { - const stillOverlapingRects = []; - for (const rect1 of rects) { - for (const rect2 of rects) { - if (rect1 === rect2) { - continue; - } - const has1 = stillOverlapingRects.indexOf(rect1) >= 0; - const has2 = stillOverlapingRects.indexOf(rect2) >= 0; - if (!has1 || !has2) { - if (rectsTouchOrOverlap(rect1, rect2, -1)) { - if (!has1) { - stillOverlapingRects.push(rect1); - } - if (!has2) { - stillOverlapingRects.push(rect2); - } - console.log("CLIENT RECT: overlap ---"); - console.log( - `#1 TOP:${rect1.top} BOTTOM:${rect1.bottom} LEFT:${rect1.left} RIGHT:${rect1.right} WIDTH:${rect1.width} HEIGHT:${rect1.height}` - ); - console.log( - `#2 TOP:${rect2.top} BOTTOM:${rect2.bottom} LEFT:${rect2.left} RIGHT:${rect2.right} WIDTH:${rect2.width} HEIGHT:${rect2.height}` - ); - const xOverlap = getRectOverlapX(rect1, rect2); - console.log(`xOverlap: ${xOverlap}`); - const yOverlap = getRectOverlapY(rect1, rect2); - console.log(`yOverlap: ${yOverlap}`); - } - } - } - } - if (stillOverlapingRects.length) { - console.log(`CLIENT RECT: overlaps ${stillOverlapingRects.length}`); - } -} - -function removeContainedRects(rects, tolerance) { - const rectsToKeep = new Set(rects); - for (const rect of rects) { - const bigEnough = rect.width > 1 && rect.height > 1; - if (!bigEnough) { - if (IS_DEV) { - console.log("CLIENT RECT: remove tiny"); - } - rectsToKeep.delete(rect); - continue; - } - for (const possiblyContainingRect of rects) { - if (rect === possiblyContainingRect) { - continue; - } - if (!rectsToKeep.has(possiblyContainingRect)) { - continue; - } - if (rectContains(possiblyContainingRect, rect, tolerance)) { - if (IS_DEV) { - console.log("CLIENT RECT: remove contained"); - } - rectsToKeep.delete(rect); - break; - } - } - } - return Array.from(rectsToKeep); -} - -function almostEqual(a, b, tolerance) { - return Math.abs(a - b) <= tolerance; -} - -function rectIntersect(rect1, rect2) { - const maxLeft = Math.max(rect1.left, rect2.left); - const minRight = Math.min(rect1.right, rect2.right); - const maxTop = Math.max(rect1.top, rect2.top); - const minBottom = Math.min(rect1.bottom, rect2.bottom); - const rect = { - bottom: minBottom, - height: Math.max(0, minBottom - maxTop), - left: maxLeft, - right: minRight, - top: maxTop, - width: Math.max(0, minRight - maxLeft), - }; - return rect; -} - -function rectSubtract(rect1, rect2) { - const rectIntersected = rectIntersect(rect2, rect1); - if (rectIntersected.height === 0 || rectIntersected.width === 0) { - return [rect1]; - } - const rects = []; - { - const rectA = { - bottom: rect1.bottom, - height: 0, - left: rect1.left, - right: rectIntersected.left, - top: rect1.top, - width: 0, - }; - rectA.width = rectA.right - rectA.left; - rectA.height = rectA.bottom - rectA.top; - if (rectA.height !== 0 && rectA.width !== 0) { - rects.push(rectA); - } - } - { - const rectB = { - bottom: rectIntersected.top, - height: 0, - left: rectIntersected.left, - right: rectIntersected.right, - top: rect1.top, - width: 0, - }; - rectB.width = rectB.right - rectB.left; - rectB.height = rectB.bottom - rectB.top; - if (rectB.height !== 0 && rectB.width !== 0) { - rects.push(rectB); - } - } - { - const rectC = { - bottom: rect1.bottom, - height: 0, - left: rectIntersected.left, - right: rectIntersected.right, - top: rectIntersected.bottom, - width: 0, - }; - rectC.width = rectC.right - rectC.left; - rectC.height = rectC.bottom - rectC.top; - if (rectC.height !== 0 && rectC.width !== 0) { - rects.push(rectC); - } - } - { - const rectD = { - bottom: rect1.bottom, - height: 0, - left: rectIntersected.right, - right: rect1.right, - top: rect1.top, - width: 0, - }; - rectD.width = rectD.right - rectD.left; - rectD.height = rectD.bottom - rectD.top; - if (rectD.height !== 0 && rectD.width !== 0) { - rects.push(rectD); - } - } - return rects; -} - -function rectContainsPoint(rect, x, y, tolerance) { - return ( - (rect.left < x || almostEqual(rect.left, x, tolerance)) && - (rect.right > x || almostEqual(rect.right, x, tolerance)) && - (rect.top < y || almostEqual(rect.top, y, tolerance)) && - (rect.bottom > y || almostEqual(rect.bottom, y, tolerance)) - ); -} - -function rectContains(rect1, rect2, tolerance) { - return ( - rectContainsPoint(rect1, rect2.left, rect2.top, tolerance) && - rectContainsPoint(rect1, rect2.right, rect2.top, tolerance) && - rectContainsPoint(rect1, rect2.left, rect2.bottom, tolerance) && - rectContainsPoint(rect1, rect2.right, rect2.bottom, tolerance) - ); -} - -function getBoundingRect(rect1, rect2) { - const left = Math.min(rect1.left, rect2.left); - const right = Math.max(rect1.right, rect2.right); - const top = Math.min(rect1.top, rect2.top); - const bottom = Math.max(rect1.bottom, rect2.bottom); - return { - bottom, - height: bottom - top, - left, - right, - top, - width: right - left, - }; -} - -function mergeTouchingRects( - rects, - tolerance, - doNotMergeHorizontallyAlignedRects -) { - for (let i = 0; i < rects.length; i++) { - for (let j = i + 1; j < rects.length; j++) { - const rect1 = rects[i]; - const rect2 = rects[j]; - if (rect1 === rect2) { - if (IS_DEV) { - console.log("mergeTouchingRects rect1 === rect2 ??!"); - } - continue; - } - const rectsLineUpVertically = - almostEqual(rect1.top, rect2.top, tolerance) && - almostEqual(rect1.bottom, rect2.bottom, tolerance); - const rectsLineUpHorizontally = - almostEqual(rect1.left, rect2.left, tolerance) && - almostEqual(rect1.right, rect2.right, tolerance); - const horizontalAllowed = !doNotMergeHorizontallyAlignedRects; - const aligned = - (rectsLineUpHorizontally && horizontalAllowed) || - (rectsLineUpVertically && !rectsLineUpHorizontally); - const canMerge = aligned && rectsTouchOrOverlap(rect1, rect2, tolerance); - if (canMerge) { - if (IS_DEV) { - console.log( - `CLIENT RECT: merging two into one, VERTICAL: ${rectsLineUpVertically} HORIZONTAL: ${rectsLineUpHorizontally} (${doNotMergeHorizontallyAlignedRects})` - ); - } - const newRects = rects.filter((rect) => { - return rect !== rect1 && rect !== rect2; - }); - const replacementClientRect = getBoundingRect(rect1, rect2); - newRects.push(replacementClientRect); - return mergeTouchingRects( - newRects, - tolerance, - doNotMergeHorizontallyAlignedRects - ); - } - } - } - return rects; -} - -function getClientRectsNoOverlap(range, doNotMergeHorizontallyAlignedRects) { - const rangeClientRects = range.getClientRects(); - return getClientRectsNoOverlap_( - rangeClientRects, - doNotMergeHorizontallyAlignedRects - ); -} - -function getClientRectsNoOverlap_( - clientRects, - doNotMergeHorizontallyAlignedRects -) { - const tolerance = 1; - const originalRects = []; - for (const rangeClientRect of clientRects) { - originalRects.push({ - bottom: rangeClientRect.bottom, - height: rangeClientRect.height, - left: rangeClientRect.left, - right: rangeClientRect.right, - top: rangeClientRect.top, - width: rangeClientRect.width, - }); - } - const mergedRects = mergeTouchingRects( - originalRects, - tolerance, - doNotMergeHorizontallyAlignedRects - ); - const noContainedRects = removeContainedRects(mergedRects, tolerance); - const newRects = replaceOverlapingRects(noContainedRects); - const minArea = 2 * 2; - for (let j = newRects.length - 1; j >= 0; j--) { - const rect = newRects[j]; - const bigEnough = rect.width * rect.height > minArea; - if (!bigEnough) { - if (newRects.length > 1) { - if (IS_DEV) { - console.log("CLIENT RECT: remove small"); - } - newRects.splice(j, 1); - } else { - if (IS_DEV) { - console.log("CLIENT RECT: remove small, but keep otherwise empty!"); - } - break; - } - } - } - if (IS_DEV) { - checkOverlaps(newRects); - } - if (IS_DEV) { - console.log( - `CLIENT RECT: reduced ${originalRects.length} --> ${newRects.length}` - ); - } - return newRects; -} - -function isPaginated(document) { - return ( - document && - document.documentElement && - document.documentElement.classList.contains(CLASS_PAGINATED) - ); -} - -function getScrollingElement(document) { - if (document.scrollingElement) { - return document.scrollingElement; - } - return document.body; -} - -function ensureContainer(win, annotationFlag) { - const document = win.document; - - if (!_highlightsContainer) { - if (!bodyEventListenersSet) { - bodyEventListenersSet = true; - document.body.addEventListener( - "mousedown", - (ev) => { - lastMouseDownX = ev.clientX; - lastMouseDownY = ev.clientY; - }, - false - ); - document.body.addEventListener( - "mouseup", - (ev) => { - if ( - Math.abs(lastMouseDownX - ev.clientX) < 3 && - Math.abs(lastMouseDownY - ev.clientY) < 3 - ) { - processMouseEvent(win, ev); - } - }, - false - ); - document.body.addEventListener( - "mousemove", - (ev) => { - processMouseEvent(win, ev); - }, - false - ); - - document.body.addEventListener( - "touchend", - function touchEnd(e) { - processTouchEvent(win, e); - }, - false - ); - } - _highlightsContainer = document.createElement("div"); - _highlightsContainer.setAttribute("id", ID_HIGHLIGHTS_CONTAINER); - - _highlightsContainer.style.setProperty("pointer-events", "none"); - document.body.append(_highlightsContainer); - } - - return _highlightsContainer; -} - -function hideAllhighlights() { - if (_highlightsContainer) { - _highlightsContainer.remove(); - _highlightsContainer = null; - } -} - -function destroyAllhighlights() { - hideAllhighlights(); - _highlights.splice(0, _highlights.length); -} - -export function destroyHighlight(id) { - let i = -1; - let _document = window.document; - const highlight = _highlights.find((h, j) => { - i = j; - return h.id === id; - }); - if (highlight && i >= 0 && i < _highlights.length) { - _highlights.splice(i, 1); - } - const highlightContainer = _document.getElementById(id); - if (highlightContainer) { - highlightContainer.remove(); - } -} - -function isCfiTextNode(node) { - return node.nodeType !== Node.ELEMENT_NODE; -} - -function getChildTextNodeCfiIndex(element, child) { - let found = -1; - let textNodeIndex = -1; - let previousWasElement = false; - for (let i = 0; i < element.childNodes.length; i++) { - const childNode = element.childNodes[i]; - const isText = isCfiTextNode(childNode); - if (isText || previousWasElement) { - textNodeIndex += 2; - } - if (isText) { - if (childNode === child) { - found = textNodeIndex; - break; - } - } - previousWasElement = childNode.nodeType === Node.ELEMENT_NODE; - } - return found; -} - -function getCommonAncestorElement(node1, node2) { - if (node1.nodeType === Node.ELEMENT_NODE && node1 === node2) { - return node1; - } - if (node1.nodeType === Node.ELEMENT_NODE && node1.contains(node2)) { - return node1; - } - if (node2.nodeType === Node.ELEMENT_NODE && node2.contains(node1)) { - return node2; - } - const node1ElementAncestorChain = []; - let parent = node1.parentNode; - while (parent && parent.nodeType === Node.ELEMENT_NODE) { - node1ElementAncestorChain.push(parent); - parent = parent.parentNode; - } - const node2ElementAncestorChain = []; - parent = node2.parentNode; - while (parent && parent.nodeType === Node.ELEMENT_NODE) { - node2ElementAncestorChain.push(parent); - parent = parent.parentNode; - } - let commonAncestor = node1ElementAncestorChain.find( - (node1ElementAncestor) => { - return node2ElementAncestorChain.indexOf(node1ElementAncestor) >= 0; - } - ); - if (!commonAncestor) { - commonAncestor = node2ElementAncestorChain.find((node2ElementAncestor) => { - return node1ElementAncestorChain.indexOf(node2ElementAncestor) >= 0; - }); - } - return commonAncestor; -} - -function fullQualifiedSelector(node) { - if (node.nodeType !== Node.ELEMENT_NODE) { - const lowerCaseName = - (node.localName && node.localName.toLowerCase()) || - node.nodeName.toLowerCase(); - return lowerCaseName; - } - //return cssPath(node, justSelector); - return cssPath(node, true); -} - -export function getCurrentSelectionInfo() { - const selection = window.getSelection(); - if (!selection) { - return undefined; - } - if (selection.isCollapsed) { - console.log("^^^ SELECTION COLLAPSED."); - return undefined; - } - const rawText = selection.toString(); - const cleanText = rawText.trim().replace(/\n/g, " ").replace(/\s\s+/g, " "); - if (cleanText.length === 0) { - console.log("^^^ SELECTION TEXT EMPTY."); - return undefined; - } - if (!selection.anchorNode || !selection.focusNode) { - return undefined; - } - const range = - selection.rangeCount === 1 - ? selection.getRangeAt(0) - : createOrderedRange( - selection.anchorNode, - selection.anchorOffset, - selection.focusNode, - selection.focusOffset - ); - if (!range || range.collapsed) { - console.log("$$$$$$$$$$$$$$$$$ CANNOT GET NON-COLLAPSED SELECTION RANGE?!"); - return undefined; - } - const rangeInfo = convertRange(range, fullQualifiedSelector, computeCFI); - if (!rangeInfo) { - console.log("^^^ SELECTION RANGE INFO FAIL?!"); - return undefined; - } - - if (IS_DEV && DEBUG_VISUALS) { - const restoredRange = convertRangeInfo(win.document, rangeInfo); - if (restoredRange) { - if ( - restoredRange.startOffset === range.startOffset && - restoredRange.endOffset === range.endOffset && - restoredRange.startContainer === range.startContainer && - restoredRange.endContainer === range.endContainer - ) { - console.log("SELECTION RANGE RESTORED OKAY (dev check)."); - } else { - console.log("SELECTION RANGE RESTORE FAIL (dev check)."); - dumpDebug( - "SELECTION", - selection.anchorNode, - selection.anchorOffset, - selection.focusNode, - selection.focusOffset, - getCssSelector - ); - dumpDebug( - "ORDERED RANGE FROM SELECTION", - range.startContainer, - range.startOffset, - range.endContainer, - range.endOffset, - getCssSelector - ); - dumpDebug( - "RESTORED RANGE", - restoredRange.startContainer, - restoredRange.startOffset, - restoredRange.endContainer, - restoredRange.endOffset, - getCssSelector - ); - } - } else { - console.log("CANNOT RESTORE SELECTION RANGE ??!"); - } - } else { - } - - return { - locations: rangeInfo2Location(rangeInfo), - text: { - highlight: rawText, - }, - }; -} - -function checkBlacklisted(el) { - let blacklistedId; - const id = el.getAttribute("id"); - if (id && _blacklistIdClassForCFI.indexOf(id) >= 0) { - console.log("checkBlacklisted ID: " + id); - blacklistedId = id; - } - let blacklistedClass; - for (const item of _blacklistIdClassForCFI) { - if (el.classList.contains(item)) { - console.log("checkBlacklisted CLASS: " + item); - blacklistedClass = item; - break; - } - } - if (blacklistedId || blacklistedClass) { - return true; - } - - return false; -} - -function cssPath(node, optimized) { - if (node.nodeType !== Node.ELEMENT_NODE) { - return ""; - } - - const steps = []; - let contextNode = node; - while (contextNode) { - const step = _cssPathStep(contextNode, !!optimized, contextNode === node); - if (!step) { - break; // Error - bail out early. - } - steps.push(step.value); - if (step.optimized) { - break; - } - contextNode = contextNode.parentNode; - } - steps.reverse(); - return steps.join(" > "); -} -// tslint:disable-next-line:max-line-length -// https://chromium.googlesource.com/chromium/blink/+/master/Source/devtools/front_end/components/DOMPresentationUtils.js#316 -function _cssPathStep(node, optimized, isTargetNode) { - function prefixedElementClassNames(nd) { - const classAttribute = nd.getAttribute("class"); - if (!classAttribute) { - return []; - } - - return classAttribute - .split(/\s+/g) - .filter(Boolean) - .map((nm) => { - // The prefix is required to store "__proto__" in a object-based map. - return "$" + nm; - }); - } - - function idSelector(idd) { - return "#" + escapeIdentifierIfNeeded(idd); - } - - function escapeIdentifierIfNeeded(ident) { - if (isCSSIdentifier(ident)) { - return ident; - } - - const shouldEscapeFirst = /^(?:[0-9]|-[0-9-]?)/.test(ident); - const lastIndex = ident.length - 1; - return ident.replace(/./g, function (c, ii) { - return (shouldEscapeFirst && ii === 0) || !isCSSIdentChar(c) - ? escapeAsciiChar(c, ii === lastIndex) - : c; - }); - } - - function escapeAsciiChar(c, isLast) { - return "\\" + toHexByte(c) + (isLast ? "" : " "); - } - - function toHexByte(c) { - let hexByte = c.charCodeAt(0).toString(16); - if (hexByte.length === 1) { - hexByte = "0" + hexByte; - } - return hexByte; - } - - function isCSSIdentChar(c) { - if (/[a-zA-Z0-9_-]/.test(c)) { - return true; - } - return c.charCodeAt(0) >= 0xa0; - } - - function isCSSIdentifier(value) { - return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value); - } - - if (node.nodeType !== Node.ELEMENT_NODE) { - return undefined; - } - const lowerCaseName = - (node.localName && node.localName.toLowerCase()) || - node.nodeName.toLowerCase(); - - const element = node; - - const id = element.getAttribute("id"); - - if (optimized) { - if (id) { - return { - optimized: true, - value: idSelector(id), - }; - } - if ( - lowerCaseName === "body" || - lowerCaseName === "head" || - lowerCaseName === "html" - ) { - return { - optimized: true, - value: lowerCaseName, // node.nodeNameInCorrectCase(), - }; - } - } - - const nodeName = lowerCaseName; // node.nodeNameInCorrectCase(); - if (id) { - return { - optimized: true, - value: nodeName + idSelector(id), - }; - } - - const parent = node.parentNode; - - if (!parent || parent.nodeType === Node.DOCUMENT_NODE) { - return { - optimized: true, - value: nodeName, - }; - } - - const prefixedOwnClassNamesArray_ = prefixedElementClassNames(element); - - const prefixedOwnClassNamesArray = []; // .keySet() - prefixedOwnClassNamesArray_.forEach((arrItem) => { - if (prefixedOwnClassNamesArray.indexOf(arrItem) < 0) { - prefixedOwnClassNamesArray.push(arrItem); - } - }); - - let needsClassNames = false; - let needsNthChild = false; - let ownIndex = -1; - let elementIndex = -1; - const siblings = parent.children; - - for ( - let i = 0; - (ownIndex === -1 || !needsNthChild) && i < siblings.length; - ++i - ) { - const sibling = siblings[i]; - if (sibling.nodeType !== Node.ELEMENT_NODE) { - continue; - } - elementIndex += 1; - if (sibling === node) { - ownIndex = elementIndex; - continue; - } - if (needsNthChild) { - continue; - } - - // sibling.nodeNameInCorrectCase() - const siblingName = - (sibling.localName && sibling.localName.toLowerCase()) || - sibling.nodeName.toLowerCase(); - if (siblingName !== nodeName) { - continue; - } - needsClassNames = true; - - const ownClassNames = []; - prefixedOwnClassNamesArray.forEach((arrItem) => { - ownClassNames.push(arrItem); - }); - let ownClassNameCount = ownClassNames.length; - - if (ownClassNameCount === 0) { - needsNthChild = true; - continue; - } - const siblingClassNamesArray_ = prefixedElementClassNames(sibling); - const siblingClassNamesArray = []; // .keySet() - siblingClassNamesArray_.forEach((arrItem) => { - if (siblingClassNamesArray.indexOf(arrItem) < 0) { - siblingClassNamesArray.push(arrItem); - } - }); - - for (const siblingClass of siblingClassNamesArray) { - const ind = ownClassNames.indexOf(siblingClass); - if (ind < 0) { - continue; - } - - ownClassNames.splice(ind, 1); // delete ownClassNames[siblingClass]; - - if (!--ownClassNameCount) { - needsNthChild = true; - break; - } - } - } - - let result = nodeName; - if ( - isTargetNode && - nodeName === "input" && - element.getAttribute("type") && - !element.getAttribute("id") && - !element.getAttribute("class") - ) { - result += '[type="' + element.getAttribute("type") + '"]'; - } - if (needsNthChild) { - result += ":nth-child(" + (ownIndex + 1) + ")"; - } else if (needsClassNames) { - for (const prefixedName of prefixedOwnClassNamesArray) { - result += "." + escapeIdentifierIfNeeded(prefixedName.substr(1)); - } - } - - return { - optimized: false, - value: result, - }; -} - -function computeCFI(node) { - // TODO: handle character position inside text node - if (node.nodeType !== Node.ELEMENT_NODE) { - return undefined; - } - - let cfi = ""; - - let currentElement = node; - while ( - currentElement.parentNode && - currentElement.parentNode.nodeType === Node.ELEMENT_NODE - ) { - const blacklisted = checkBlacklisted(currentElement); - if (!blacklisted) { - const currentElementParentChildren = currentElement.parentNode.children; - let currentElementIndex = -1; - for (let i = 0; i < currentElementParentChildren.length; i++) { - if (currentElement === currentElementParentChildren[i]) { - currentElementIndex = i; - break; - } - } - if (currentElementIndex >= 0) { - const cfiIndex = (currentElementIndex + 1) * 2; - cfi = - cfiIndex + - (currentElement.id ? "[" + currentElement.id + "]" : "") + - (cfi.length ? "/" + cfi : ""); - } - } - currentElement = currentElement.parentNode; - } - - return "/" + cfi; -} - -function _createHighlight(locations, color, pointerInteraction, type) { - const rangeInfo = location2RangeInfo(locations); - const uniqueStr = `${rangeInfo.cfi}${rangeInfo.startContainerElementCssSelector}${rangeInfo.startContainerChildTextNodeIndex}${rangeInfo.startOffset}${rangeInfo.endContainerElementCssSelector}${rangeInfo.endContainerChildTextNodeIndex}${rangeInfo.endOffset}`; - - const hash = require("hash.js"); - const sha256Hex = hash.sha256().update(uniqueStr).digest("hex"); - - var id; - if (type == ID_HIGHLIGHTS_CONTAINER) { - id = "R2_HIGHLIGHT_" + sha256Hex; - } else { - id = "R2_ANNOTATION_" + sha256Hex; - } - - destroyHighlight(id); - - const highlight = { - color: color ? color : DEFAULT_BACKGROUND_COLOR, - id, - pointerInteraction, - rangeInfo, - }; - _highlights.push(highlight); - createHighlightDom( - window, - highlight, - type == ID_ANNOTATION_CONTAINER ? true : false - ); - - return highlight; -} - -export function createHighlight(selectionInfo, color, pointerInteraction) { - return _createHighlight( - selectionInfo, - color, - pointerInteraction, - ID_HIGHLIGHTS_CONTAINER - ); -} - -export function createAnnotation(id) { - let i = -1; - - const highlight = _highlights.find((h, j) => { - i = j; - return h.id === id; - }); - if (i == _highlights.length) return; - - var locations = { - locations: rangeInfo2Location(highlight.rangeInfo), - }; - - return _createHighlight( - locations, - highlight.color, - true, - ID_ANNOTATION_CONTAINER - ); -} - -function createHighlightDom(win, highlight, annotationFlag) { - const document = win.document; - - const scale = - 1 / - (win.READIUM2 && win.READIUM2.isFixedLayout - ? win.READIUM2.fxlViewportScale - : 1); - - const scrollElement = getScrollingElement(document); - - const range = convertRangeInfo(document, highlight.rangeInfo); - if (!range) { - return undefined; - } - - const paginated = isPaginated(document); - const highlightsContainer = ensureContainer(win, annotationFlag); - const highlightParent = document.createElement("div"); - - highlightParent.setAttribute("id", highlight.id); - highlightParent.setAttribute("class", CLASS_HIGHLIGHT_CONTAINER); - - document.body.style.position = "relative"; - highlightParent.style.setProperty("pointer-events", "none"); - if (highlight.pointerInteraction) { - highlightParent.setAttribute("data-click", "1"); - } - - const bodyRect = document.body.getBoundingClientRect(); - const useSVG = !DEBUG_VISUALS && USE_SVG; - //const useSVG = USE_SVG; - const drawUnderline = false; - const drawStrikeThrough = false; - const doNotMergeHorizontallyAlignedRects = drawUnderline || drawStrikeThrough; - //const clientRects = DEBUG_VISUALS ? range.getClientRects() : - const clientRects = getClientRectsNoOverlap( - range, - doNotMergeHorizontallyAlignedRects - ); - let highlightAreaSVGDocFrag; - const roundedCorner = 3; - const underlineThickness = 2; - const strikeThroughLineThickness = 3; - const opacity = DEFAULT_BACKGROUND_COLOR_OPACITY; - let extra = ""; - const rangeAnnotationBoundingClientRect = - frameForHighlightAnnotationMarkWithID(win, highlight.id); - - let xOffset; - let yOffset; - let annotationOffset; - - if (navigator.userAgent.match(/Android/i)) { - xOffset = paginated ? -scrollElement.scrollLeft : bodyRect.left; - yOffset = paginated ? -scrollElement.scrollTop : bodyRect.top; - annotationOffset = - parseInt( - (rangeAnnotationBoundingClientRect.right - xOffset) / window.innerWidth - ) + 1; - } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) { - xOffset = paginated ? 0 : -scrollElement.scrollLeft; - yOffset = paginated ? 0 : bodyRect.top; - annotationOffset = parseInt( - rangeAnnotationBoundingClientRect.right / window.innerWidth + 1 - ); - } - - for (const clientRect of clientRects) { - if (useSVG) { - const borderThickness = 0; - if (!highlightAreaSVGDocFrag) { - highlightAreaSVGDocFrag = document.createDocumentFragment(); - } - const highlightAreaSVGRect = document.createElementNS( - SVG_XML_NAMESPACE, - "rect" - ); - - highlightAreaSVGRect.setAttribute("class", CLASS_HIGHLIGHT_AREA); - highlightAreaSVGRect.setAttribute( - "style", - `fill: rgb(${highlight.color.red}, ${highlight.color.green}, ${highlight.color.blue}) !important; fill-opacity: ${opacity} !important; stroke-width: 0;` - ); - highlightAreaSVGRect.scale = scale; - - /* - highlightAreaSVGRect.rect = { - height: clientRect.height, - left: clientRect.left - xOffset, - top: clientRect.top - yOffset, - width: clientRect.width, - }; - */ - - if (annotationFlag) { - highlightAreaSVGRect.rect = { - height: ANNOTATION_WIDTH, //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4, - left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH, - top: rangeAnnotationBoundingClientRect.top - yOffset, - width: ANNOTATION_WIDTH, - }; - } else { - highlightAreaSVGRect.rect = { - height: clientRect.height, - left: clientRect.left - xOffset, - top: clientRect.top - yOffset, - width: clientRect.width, - }; - } - - highlightAreaSVGRect.setAttribute("rx", `${roundedCorner * scale}`); - highlightAreaSVGRect.setAttribute("ry", `${roundedCorner * scale}`); - highlightAreaSVGRect.setAttribute( - "x", - `${(highlightAreaSVGRect.rect.left - borderThickness) * scale}` - ); - highlightAreaSVGRect.setAttribute( - "y", - `${(highlightAreaSVGRect.rect.top - borderThickness) * scale}` - ); - highlightAreaSVGRect.setAttribute( - "height", - `${(highlightAreaSVGRect.rect.height + borderThickness * 2) * scale}` - ); - highlightAreaSVGRect.setAttribute( - "width", - `${(highlightAreaSVGRect.rect.width + borderThickness * 2) * scale}` - ); - highlightAreaSVGDocFrag.appendChild(highlightAreaSVGRect); - if (drawUnderline) { - const highlightAreaSVGLine = document.createElementNS( - SVG_XML_NAMESPACE, - "line" - ); - highlightAreaSVGRect.setAttribute("class", CLASS_HIGHLIGHT_AREA); - highlightAreaSVGLine.setAttribute( - "style", - `stroke-linecap: round; stroke-width: ${ - underlineThickness * scale - }; stroke: rgb(${highlight.color.red}, ${highlight.color.green}, ${ - highlight.color.blue - }) !important; stroke-opacity: ${opacity} !important` - ); - highlightAreaSVGLine.scale = scale; - /* - highlightAreaSVGLine.rect = { - height: clientRect.height, - left: clientRect.left - xOffset, - top: clientRect.top - yOffset, - width: clientRect.width, - }; - */ - if (annotationFlag) { - highlightAreaSVGLine.rect = { - height: ANNOTATION_WIDTH, //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4, - left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH, - top: rangeAnnotationBoundingClientRect.top - yOffset, - width: ANNOTATION_WIDTH, - }; - } else { - highlightAreaSVGLine.rect = { - height: clientRect.height, - left: clientRect.left - xOffset, - top: clientRect.top - yOffset, - width: clientRect.width, - }; - } - - const lineOffset = - highlightAreaSVGLine.rect.width > roundedCorner ? roundedCorner : 0; - highlightAreaSVGLine.setAttribute( - "x1", - `${(highlightAreaSVGLine.rect.left + lineOffset) * scale}` - ); - highlightAreaSVGLine.setAttribute( - "x2", - `${ - (highlightAreaSVGLine.rect.left + - highlightAreaSVGLine.rect.width - - lineOffset) * - scale - }` - ); - const y = - (highlightAreaSVGLine.rect.top + - highlightAreaSVGLine.rect.height - - underlineThickness / 2) * - scale; - highlightAreaSVGLine.setAttribute("y1", `${y}`); - highlightAreaSVGLine.setAttribute("y2", `${y}`); - highlightAreaSVGLine.setAttribute( - "height", - `${highlightAreaSVGLine.rect.height * scale}` - ); - highlightAreaSVGLine.setAttribute( - "width", - `${highlightAreaSVGLine.rect.width * scale}` - ); - highlightAreaSVGDocFrag.appendChild(highlightAreaSVGLine); - } - if (drawStrikeThrough) { - const highlightAreaSVGLine = document.createElementNS( - SVG_XML_NAMESPACE, - "line" - ); - - highlightAreaSVGRect.setAttribute("class", CLASS_HIGHLIGHT_AREA); - highlightAreaSVGLine.setAttribute( - "style", - `stroke-linecap: butt; stroke-width: ${ - strikeThroughLineThickness * scale - }; stroke: rgb(${highlight.color.red}, ${highlight.color.green}, ${ - highlight.color.blue - }) !important; stroke-opacity: ${opacity} !important` - ); - highlightAreaSVGLine.scale = scale; - - /* - highlightAreaSVGLine.rect = { - height: clientRect.height, - left: clientRect.left - xOffset, - top: clientRect.top - yOffset, - width: clientRect.width, - }; - */ - - if (annotationFlag) { - highlightAreaSVGLine.rect = { - height: ANNOTATION_WIDTH, //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4, - left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH, - top: rangeAnnotationBoundingClientRect.top - yOffset, - width: ANNOTATION_WIDTH, - }; - } else { - highlightAreaSVGLine.rect = { - height: clientRect.height, - left: clientRect.left - xOffset, - top: clientRect.top - yOffset, - width: clientRect.width, - }; - } - - highlightAreaSVGLine.setAttribute( - "x1", - `${highlightAreaSVGLine.rect.left * scale}` - ); - highlightAreaSVGLine.setAttribute( - "x2", - `${ - (highlightAreaSVGLine.rect.left + highlightAreaSVGLine.rect.width) * - scale - }` - ); - const lineOffset = highlightAreaSVGLine.rect.height / 2; - const y = (highlightAreaSVGLine.rect.top + lineOffset) * scale; - highlightAreaSVGLine.setAttribute("y1", `${y}`); - highlightAreaSVGLine.setAttribute("y2", `${y}`); - highlightAreaSVGLine.setAttribute( - "height", - `${highlightAreaSVGLine.rect.height * scale}` - ); - highlightAreaSVGLine.setAttribute( - "width", - `${highlightAreaSVGLine.rect.width * scale}` - ); - highlightAreaSVGDocFrag.appendChild(highlightAreaSVGLine); - } - } else { - const highlightArea = document.createElement("div"); - - highlightArea.setAttribute("class", CLASS_HIGHLIGHT_AREA); - - if (DEBUG_VISUALS) { - const rgb = Math.round(0xffffff * Math.random()); - const r = rgb >> 16; - const g = (rgb >> 8) & 255; - const b = rgb & 255; - extra = `outline-color: rgb(${r}, ${g}, ${b}); outline-style: solid; outline-width: 1px; outline-offset: -1px;`; - } else { - if (drawUnderline) { - extra += `border-bottom: ${underlineThickness * scale}px solid rgba(${ - highlight.color.red - }, ${highlight.color.green}, ${ - highlight.color.blue - }, ${opacity}) !important`; - } - } - highlightArea.setAttribute( - "style", - `border-radius: ${roundedCorner}px !important; background-color: rgba(${highlight.color.red}, ${highlight.color.green}, ${highlight.color.blue}, ${opacity}) !important; ${extra}` - ); - highlightArea.style.setProperty("pointer-events", "none"); - highlightArea.style.position = paginated ? "fixed" : "absolute"; - highlightArea.scale = scale; - /* - highlightArea.rect = { - height: clientRect.height, - left: clientRect.left - xOffset, - top: clientRect.top - yOffset, - width: clientRect.width, - }; - */ - if (annotationFlag) { - highlightArea.rect = { - height: ANNOTATION_WIDTH, //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4, - left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH, - top: rangeAnnotationBoundingClientRect.top - yOffset, - width: ANNOTATION_WIDTH, - }; - } else { - highlightArea.rect = { - height: clientRect.height, - left: clientRect.left - xOffset, - top: clientRect.top - yOffset, - width: clientRect.width, - }; - } - - highlightArea.style.width = `${highlightArea.rect.width * scale}px`; - highlightArea.style.height = `${highlightArea.rect.height * scale}px`; - highlightArea.style.left = `${highlightArea.rect.left * scale}px`; - highlightArea.style.top = `${highlightArea.rect.top * scale}px`; - highlightParent.append(highlightArea); - if (!DEBUG_VISUALS && drawStrikeThrough) { - //if (drawStrikeThrough) { - const highlightAreaLine = document.createElement("div"); - highlightAreaLine.setAttribute("class", CLASS_HIGHLIGHT_AREA); - - highlightAreaLine.setAttribute( - "style", - `background-color: rgba(${highlight.color.red}, ${highlight.color.green}, ${highlight.color.blue}, ${opacity}) !important;` - ); - highlightAreaLine.style.setProperty("pointer-events", "none"); - highlightAreaLine.style.position = paginated ? "fixed" : "absolute"; - highlightAreaLine.scale = scale; - /* - highlightAreaLine.rect = { - height: clientRect.height, - left: clientRect.left - xOffset, - top: clientRect.top - yOffset, - width: clientRect.width, - }; - */ - - if (annotationFlag) { - highlightAreaLine.rect = { - height: ANNOTATION_WIDTH, //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4, - left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH, - top: rangeAnnotationBoundingClientRect.top - yOffset, - width: ANNOTATION_WIDTH, - }; - } else { - highlightAreaLine.rect = { - height: clientRect.height, - left: clientRect.left - xOffset, - top: clientRect.top - yOffset, - width: clientRect.width, - }; - } - - highlightAreaLine.style.width = `${ - highlightAreaLine.rect.width * scale - }px`; - highlightAreaLine.style.height = `${ - strikeThroughLineThickness * scale - }px`; - highlightAreaLine.style.left = `${ - highlightAreaLine.rect.left * scale - }px`; - highlightAreaLine.style.top = `${ - (highlightAreaLine.rect.top + - highlightAreaLine.rect.height / 2 - - strikeThroughLineThickness / 2) * - scale - }px`; - highlightParent.append(highlightAreaLine); - } - } - - if (annotationFlag) { - break; - } - } - - if (useSVG && highlightAreaSVGDocFrag) { - const highlightAreaSVG = document.createElementNS(SVG_XML_NAMESPACE, "svg"); - highlightAreaSVG.setAttribute("pointer-events", "none"); - highlightAreaSVG.style.position = paginated ? "fixed" : "absolute"; - highlightAreaSVG.style.overflow = "visible"; - highlightAreaSVG.style.left = "0"; - highlightAreaSVG.style.top = "0"; - highlightAreaSVG.append(highlightAreaSVGDocFrag); - highlightParent.append(highlightAreaSVG); - } - - const highlightBounding = document.createElement("div"); - - if (annotationFlag) { - highlightBounding.setAttribute("class", CLASS_ANNOTATION_BOUNDING_AREA); - highlightBounding.setAttribute( - "style", - `border-radius: ${roundedCorner}px !important; background-color: rgba(${highlight.color.red}, ${highlight.color.green}, ${highlight.color.blue}, ${opacity}) !important; ${extra}` - ); - } else { - highlightBounding.setAttribute("class", CLASS_HIGHLIGHT_BOUNDING_AREA); - } - - highlightBounding.style.setProperty("pointer-events", "none"); - highlightBounding.style.position = paginated ? "fixed" : "absolute"; - highlightBounding.scale = scale; - - if (DEBUG_VISUALS) { - highlightBounding.setAttribute( - "style", - `outline-color: magenta; outline-style: solid; outline-width: 1px; outline-offset: -1px;` - ); - } - - if (annotationFlag) { - highlightBounding.rect = { - height: ANNOTATION_WIDTH, //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4, - left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH, - top: rangeAnnotationBoundingClientRect.top - yOffset, - width: ANNOTATION_WIDTH, - }; - } else { - const rangeBoundingClientRect = range.getBoundingClientRect(); - highlightBounding.rect = { - height: rangeBoundingClientRect.height, - left: rangeBoundingClientRect.left - xOffset, - top: rangeBoundingClientRect.top - yOffset, - width: rangeBoundingClientRect.width, - }; - } - - highlightBounding.style.width = `${highlightBounding.rect.width * scale}px`; - highlightBounding.style.height = `${highlightBounding.rect.height * scale}px`; - highlightBounding.style.left = `${highlightBounding.rect.left * scale}px`; - highlightBounding.style.top = `${highlightBounding.rect.top * scale}px`; - - highlightParent.append(highlightBounding); - highlightsContainer.append(highlightParent); - - return highlightParent; -} - -function createOrderedRange(startNode, startOffset, endNode, endOffset) { - const range = new Range(); - range.setStart(startNode, startOffset); - range.setEnd(endNode, endOffset); - if (!range.collapsed) { - return range; - } - console.log(">>> createOrderedRange COLLAPSED ... RANGE REVERSE?"); - const rangeReverse = new Range(); - rangeReverse.setStart(endNode, endOffset); - rangeReverse.setEnd(startNode, startOffset); - if (!rangeReverse.collapsed) { - console.log(">>> createOrderedRange RANGE REVERSE OK."); - return range; - } - console.log(">>> createOrderedRange RANGE REVERSE ALSO COLLAPSED?!"); - return undefined; -} - -function convertRange(range, getCssSelector, computeElementCFI) { - const startIsElement = range.startContainer.nodeType === Node.ELEMENT_NODE; - const startContainerElement = startIsElement - ? range.startContainer - : range.startContainer.parentNode && - range.startContainer.parentNode.nodeType === Node.ELEMENT_NODE - ? range.startContainer.parentNode - : undefined; - if (!startContainerElement) { - return undefined; - } - const startContainerChildTextNodeIndex = startIsElement - ? -1 - : Array.from(startContainerElement.childNodes).indexOf( - range.startContainer - ); - if (startContainerChildTextNodeIndex < -1) { - return undefined; - } - const startContainerElementCssSelector = getCssSelector( - startContainerElement - ); - const endIsElement = range.endContainer.nodeType === Node.ELEMENT_NODE; - const endContainerElement = endIsElement - ? range.endContainer - : range.endContainer.parentNode && - range.endContainer.parentNode.nodeType === Node.ELEMENT_NODE - ? range.endContainer.parentNode - : undefined; - if (!endContainerElement) { - return undefined; - } - const endContainerChildTextNodeIndex = endIsElement - ? -1 - : Array.from(endContainerElement.childNodes).indexOf(range.endContainer); - if (endContainerChildTextNodeIndex < -1) { - return undefined; - } - const endContainerElementCssSelector = getCssSelector(endContainerElement); - const commonElementAncestor = getCommonAncestorElement( - range.startContainer, - range.endContainer - ); - if (!commonElementAncestor) { - console.log("^^^ NO RANGE COMMON ANCESTOR?!"); - return undefined; - } - if (range.commonAncestorContainer) { - const rangeCommonAncestorElement = - range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE - ? range.commonAncestorContainer - : range.commonAncestorContainer.parentNode; - if ( - rangeCommonAncestorElement && - rangeCommonAncestorElement.nodeType === Node.ELEMENT_NODE - ) { - if (commonElementAncestor !== rangeCommonAncestorElement) { - console.log(">>>>>> COMMON ANCESTOR CONTAINER DIFF??!"); - console.log(getCssSelector(commonElementAncestor)); - console.log(getCssSelector(rangeCommonAncestorElement)); - } - } - } - const rootElementCfi = computeElementCFI(commonElementAncestor); - const startElementCfi = computeElementCFI(startContainerElement); - const endElementCfi = computeElementCFI(endContainerElement); - let cfi; - if (rootElementCfi && startElementCfi && endElementCfi) { - let startElementOrTextCfi = startElementCfi; - if (!startIsElement) { - const startContainerChildTextNodeIndexForCfi = getChildTextNodeCfiIndex( - startContainerElement, - range.startContainer - ); - startElementOrTextCfi = - startElementCfi + - "/" + - startContainerChildTextNodeIndexForCfi + - ":" + - range.startOffset; - } else { - if ( - range.startOffset >= 0 && - range.startOffset < startContainerElement.childNodes.length - ) { - const childNode = startContainerElement.childNodes[range.startOffset]; - if (childNode.nodeType === Node.ELEMENT_NODE) { - startElementOrTextCfi = - startElementCfi + "/" + (range.startOffset + 1) * 2; - } else { - const cfiTextNodeIndex = getChildTextNodeCfiIndex( - startContainerElement, - childNode - ); - startElementOrTextCfi = startElementCfi + "/" + cfiTextNodeIndex; - } - } else { - const cfiIndexOfLastElement = - startContainerElement.childElementCount * 2; - const lastChildNode = - startContainerElement.childNodes[ - startContainerElement.childNodes.length - 1 - ]; - if (lastChildNode.nodeType === Node.ELEMENT_NODE) { - startElementOrTextCfi = - startElementCfi + "/" + (cfiIndexOfLastElement + 1); - } else { - startElementOrTextCfi = - startElementCfi + "/" + (cfiIndexOfLastElement + 2); - } - } - } - let endElementOrTextCfi = endElementCfi; - if (!endIsElement) { - const endContainerChildTextNodeIndexForCfi = getChildTextNodeCfiIndex( - endContainerElement, - range.endContainer - ); - endElementOrTextCfi = - endElementCfi + - "/" + - endContainerChildTextNodeIndexForCfi + - ":" + - range.endOffset; - } else { - if ( - range.endOffset >= 0 && - range.endOffset < endContainerElement.childNodes.length - ) { - const childNode = endContainerElement.childNodes[range.endOffset]; - if (childNode.nodeType === Node.ELEMENT_NODE) { - endElementOrTextCfi = endElementCfi + "/" + (range.endOffset + 1) * 2; - } else { - const cfiTextNodeIndex = getChildTextNodeCfiIndex( - endContainerElement, - childNode - ); - endElementOrTextCfi = endElementCfi + "/" + cfiTextNodeIndex; - } - } else { - const cfiIndexOfLastElement = endContainerElement.childElementCount * 2; - const lastChildNode = - endContainerElement.childNodes[ - endContainerElement.childNodes.length - 1 - ]; - if (lastChildNode.nodeType === Node.ELEMENT_NODE) { - endElementOrTextCfi = - endElementCfi + "/" + (cfiIndexOfLastElement + 1); - } else { - endElementOrTextCfi = - endElementCfi + "/" + (cfiIndexOfLastElement + 2); - } - } - } - cfi = - rootElementCfi + - "," + - startElementOrTextCfi.replace(rootElementCfi, "") + - "," + - endElementOrTextCfi.replace(rootElementCfi, ""); - } - return { - cfi, - endContainerChildTextNodeIndex, - endContainerElementCssSelector, - endOffset: range.endOffset, - startContainerChildTextNodeIndex, - startContainerElementCssSelector, - startOffset: range.startOffset, - }; -} - -function convertRangeInfo(document, rangeInfo) { - const startElement = document.querySelector( - rangeInfo.startContainerElementCssSelector - ); - if (!startElement) { - console.log("^^^ convertRangeInfo NO START ELEMENT CSS SELECTOR?!"); - return undefined; - } - let startContainer = startElement; - if (rangeInfo.startContainerChildTextNodeIndex >= 0) { - if ( - rangeInfo.startContainerChildTextNodeIndex >= - startElement.childNodes.length - ) { - console.log( - "^^^ convertRangeInfo rangeInfo.startContainerChildTextNodeIndex >= startElement.childNodes.length?!" - ); - return undefined; - } - startContainer = - startElement.childNodes[rangeInfo.startContainerChildTextNodeIndex]; - if (startContainer.nodeType !== Node.TEXT_NODE) { - console.log( - "^^^ convertRangeInfo startContainer.nodeType !== Node.TEXT_NODE?!" - ); - return undefined; - } - } - const endElement = document.querySelector( - rangeInfo.endContainerElementCssSelector - ); - if (!endElement) { - console.log("^^^ convertRangeInfo NO END ELEMENT CSS SELECTOR?!"); - return undefined; - } - let endContainer = endElement; - if (rangeInfo.endContainerChildTextNodeIndex >= 0) { - if ( - rangeInfo.endContainerChildTextNodeIndex >= endElement.childNodes.length - ) { - console.log( - "^^^ convertRangeInfo rangeInfo.endContainerChildTextNodeIndex >= endElement.childNodes.length?!" - ); - return undefined; - } - endContainer = - endElement.childNodes[rangeInfo.endContainerChildTextNodeIndex]; - if (endContainer.nodeType !== Node.TEXT_NODE) { - console.log( - "^^^ convertRangeInfo endContainer.nodeType !== Node.TEXT_NODE?!" - ); - return undefined; - } - } - return createOrderedRange( - startContainer, - rangeInfo.startOffset, - endContainer, - rangeInfo.endOffset - ); -} - -function frameForHighlightAnnotationMarkWithID(win, id) { - let clientRects = frameForHighlightWithID(id); - if (!clientRects) return; - - var topClientRect = clientRects[0]; - var maxHeight = topClientRect.height; - for (const clientRect of clientRects) { - if (clientRect.top < topClientRect.top) topClientRect = clientRect; - if (clientRect.height > maxHeight) maxHeight = clientRect.height; - } - - const document = win.document; - - const scrollElement = getScrollingElement(document); - const paginated = isPaginated(document); - const bodyRect = document.body.getBoundingClientRect(); - let yOffset; - if (navigator.userAgent.match(/Android/i)) { - yOffset = paginated ? -scrollElement.scrollTop : bodyRect.top; - } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) { - yOffset = paginated ? 0 : bodyRect.top; - } - var newTop = topClientRect.top; - - if (_highlightsContainer) { - do { - var boundingAreas = document.getElementsByClassName( - CLASS_ANNOTATION_BOUNDING_AREA - ); - var found = false; - //for (let i = 0, length = boundingAreas.snapshotLength; i < length; ++i) { - for ( - var i = 0, len = boundingAreas.length | 0; - i < len; - i = (i + 1) | 0 - ) { - var boundingArea = boundingAreas[i]; - if (Math.abs(boundingArea.rect.top - (newTop - yOffset)) < 3) { - newTop += boundingArea.rect.height; - found = true; - break; - } - } - } while (found); - } - - topClientRect.top = newTop; - topClientRect.height = maxHeight; - - return topClientRect; -} - -function highlightWithID(id) { - let i = -1; - const highlight = _highlights.find((h, j) => { - i = j; - return h.id === id; - }); - return highlight; -} - -function frameForHighlightWithID(id) { - const highlight = highlightWithID(id); - if (!highlight) return; - - const document = window.document; - const scrollElement = getScrollingElement(document); - const range = convertRangeInfo(document, highlight.rangeInfo); - if (!range) { - return undefined; - } - - const drawUnderline = false; - const drawStrikeThrough = false; - const doNotMergeHorizontallyAlignedRects = drawUnderline || drawStrikeThrough; - //const clientRects = DEBUG_VISUALS ? range.getClientRects() : - const clientRects = getClientRectsNoOverlap( - range, - doNotMergeHorizontallyAlignedRects - ); - - return clientRects; -} - -function rangeInfo2Location(rangeInfo) { - return { - cssSelector: rangeInfo.startContainerElementCssSelector, - partialCfi: rangeInfo.cfi, - domRange: { - start: { - cssSelector: rangeInfo.startContainerElementCssSelector, - textNodeIndex: rangeInfo.startContainerChildTextNodeIndex, - offset: rangeInfo.startOffset, - }, - end: { - cssSelector: rangeInfo.endContainerElementCssSelector, - textNodeIndex: rangeInfo.endContainerChildTextNodeIndex, - offset: rangeInfo.endOffset, - }, - }, - }; -} - -function location2RangeInfo(location) { - const locations = location.locations; - const domRange = locations.domRange; - const start = domRange.start; - const end = domRange.end; - - return { - cfi: location.partialCfi, - endContainerChildTextNodeIndex: end.textNodeIndex, - endContainerElementCssSelector: end.cssSelector, - endOffset: end.offset, - startContainerChildTextNodeIndex: start.textNodeIndex, - startContainerElementCssSelector: start.cssSelector, - startOffset: start.offset, - }; -} - -export function rectangleForHighlightWithID(id) { - const highlight = highlightWithID(id); - if (!highlight) return; - - const document = window.document; - const scrollElement = getScrollingElement(document); - const range = convertRangeInfo(document, highlight.rangeInfo); - if (!range) { - return undefined; - } - - const drawUnderline = false; - const drawStrikeThrough = false; - const doNotMergeHorizontallyAlignedRects = drawUnderline || drawStrikeThrough; - //const clientRects = DEBUG_VISUALS ? range.getClientRects() : - const clientRects = getClientRectsNoOverlap( - range, - doNotMergeHorizontallyAlignedRects - ); - var size = { - screenWidth: window.outerWidth, - screenHeight: window.outerHeight, - left: clientRects[0].left, - width: clientRects[0].width, - top: clientRects[0].top, - height: clientRects[0].height, - }; - - return size; -} - -export function getSelectionRect() { - try { - var sel = window.getSelection(); - if (!sel) { - return; - } - var range = sel.getRangeAt(0); - - const clientRect = range.getBoundingClientRect(); - - var handleBounds = { - screenWidth: window.outerWidth, - screenHeight: window.outerHeight, - left: clientRect.left, - width: clientRect.width, - top: clientRect.top, - height: clientRect.height, - }; - return handleBounds; - } catch (e) { - return null; - } -} - -export function setScrollMode(flag) { - if (!flag) { - document.documentElement.classList.add(CLASS_PAGINATED); - } else { - document.documentElement.classList.remove(CLASS_PAGINATED); - } -} - -/* - if (document.addEventListener) { // IE >= 9; other browsers - document.addEventListener('contextmenu', function(e) { - //alert("You've tried to open context menu"); //here you draw your own menu - //e.preventDefault(); - //let getCssSelector = fullQualifiedSelector; - - let str = window.getSelection(); - let selectionInfo = getCurrentSelectionInfo(); - let pos = createHighlight(selectionInfo,{red:10,green:50,blue:230},true); - let ret2 = createAnnotation(pos.id); - - }, false); - } else { // IE < 9 - document.attachEvent('oncontextmenu', function() { - alert("You've tried to open context menu"); - window.event.returnValue = false; - }); - } -*/ diff --git a/readium/navigator/src/main/assets/_scripts/src/index.js b/readium/navigator/src/main/assets/_scripts/src/index.js index f39a59214e..63c59f36de 100644 --- a/readium/navigator/src/main/assets/_scripts/src/index.js +++ b/readium/navigator/src/main/assets/_scripts/src/index.js @@ -7,6 +7,7 @@ // Base script used by both reflowable and fixed layout resources. import "./gestures"; +import "./keyboard"; import { removeProperty, scrollLeft, @@ -19,15 +20,6 @@ import { setProperty, setCSSProperties, } from "./utils"; -import { - createAnnotation, - createHighlight, - destroyHighlight, - getCurrentSelectionInfo, - getSelectionRect, - rectangleForHighlightWithID, - setScrollMode, -} from "./highlight"; import { findFirstVisibleLocator } from "./dom"; import { getCurrentSelection } from "./selection"; import { getDecorations, registerTemplates } from "./decorator"; @@ -56,12 +48,3 @@ window.readium = { // DOM findFirstVisibleLocator: findFirstVisibleLocator, }; - -// Legacy highlights API. -window.createAnnotation = createAnnotation; -window.createHighlight = createHighlight; -window.destroyHighlight = destroyHighlight; -window.getCurrentSelectionInfo = getCurrentSelectionInfo; -window.getSelectionRect = getSelectionRect; -window.rectangleForHighlightWithID = rectangleForHighlightWithID; -window.setScrollMode = setScrollMode; diff --git a/readium/navigator/src/main/assets/_scripts/src/keyboard.js b/readium/navigator/src/main/assets/_scripts/src/keyboard.js new file mode 100644 index 0000000000..d021314698 --- /dev/null +++ b/readium/navigator/src/main/assets/_scripts/src/keyboard.js @@ -0,0 +1,55 @@ +// +// Copyright 2023 Readium Foundation. All rights reserved. +// Use of this source code is governed by the BSD-style license +// available in the top-level LICENSE file of the project. +// + +import { nearestInteractiveElement } from "./dom"; + +window.addEventListener("keydown", (event) => { + if (shouldIgnoreEvent(event)) { + return; + } + + preventDefault(event); + sendPressKeyMessage(event, "down"); +}); + +window.addEventListener("keyup", (event) => { + if (shouldIgnoreEvent(event)) { + return; + } + + preventDefault(event); + sendPressKeyMessage(event, "up"); +}); + +function shouldIgnoreEvent(event) { + return ( + event.defaultPrevented || + nearestInteractiveElement(document.activeElement) != null + ); +} + +// We prevent the default behavior for keyboard events, otherwise the web view +// might scroll. +function preventDefault(event) { + event.stopPropagation(); + event.preventDefault(); +} + +function sendPressKeyMessage(event, type) { + if (event.repeat) return; + + let keyEvent = { + type: type, + code: event.code, + characters: String.fromCharCode(event.keyCode), + alt: event.altKey, + control: event.ctrlKey, + shift: event.shiftKey, + meta: event.metaKey, + }; + + Android.onKey(JSON.stringify(keyEvent)); +} diff --git a/readium/navigator/src/main/assets/_scripts/webpack.config.js b/readium/navigator/src/main/assets/_scripts/webpack.config.js index 9b7d5f7899..42305b87a7 100644 --- a/readium/navigator/src/main/assets/_scripts/webpack.config.js +++ b/readium/navigator/src/main/assets/_scripts/webpack.config.js @@ -2,7 +2,7 @@ const path = require("path"); module.exports = { mode: "production", - devtool: "eval-source-map", + devtool: "source-map", entry: { reflowable: "./src/index-reflowable.js", fixed: "./src/index-fixed.js", diff --git a/readium/navigator/src/main/assets/_scripts/yarn.lock b/readium/navigator/src/main/assets/_scripts/yarn.lock deleted file mode 100644 index 21c66cacbb..0000000000 --- a/readium/navigator/src/main/assets/_scripts/yarn.lock +++ /dev/null @@ -1,2914 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/code-frame@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" - integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA== - dependencies: - "@babel/highlight" "^7.16.0" - -"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.0", "@babel/compat-data@^7.16.4": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.4.tgz#081d6bbc336ec5c2435c6346b2ae1fb98b5ac68e" - integrity sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q== - -"@babel/core@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.0.tgz#c4ff44046f5fe310525cc9eb4ef5147f0c5374d4" - integrity sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ== - dependencies: - "@babel/code-frame" "^7.16.0" - "@babel/generator" "^7.16.0" - "@babel/helper-compilation-targets" "^7.16.0" - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helpers" "^7.16.0" - "@babel/parser" "^7.16.0" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.1.2" - semver "^6.3.0" - source-map "^0.5.0" - -"@babel/generator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2" - integrity sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew== - dependencies: - "@babel/types" "^7.16.0" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/helper-annotate-as-pure@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.0.tgz#9a1f0ebcda53d9a2d00108c4ceace6a5d5f1f08d" - integrity sha512-ItmYF9vR4zA8cByDocY05o0LGUkp1zhbTQOH1NFyl5xXEqlTJQCEJjieriw+aFpxo16swMxUnUiKS7a/r4vtHg== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.0.tgz#f1a686b92da794020c26582eb852e9accd0d7882" - integrity sha512-9KuleLT0e77wFUku6TUkqZzCEymBdtuQQ27MhEKzf9UOOJu3cYj98kyaDAzxpC7lV6DGiZFuC8XqDsq8/Kl6aQ== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.0", "@babel/helper-compilation-targets@^7.16.3": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.3.tgz#5b480cd13f68363df6ec4dc8ac8e2da11363cbf0" - integrity sha512-vKsoSQAyBmxS35JUOOt+07cLc6Nk/2ljLIHwmq2/NM6hdioUaqEXq/S+nXvbvXbZkNDlWOymPanJGOc4CBjSJA== - dependencies: - "@babel/compat-data" "^7.16.0" - "@babel/helper-validator-option" "^7.14.5" - browserslist "^4.17.5" - semver "^6.3.0" - -"@babel/helper-create-class-features-plugin@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.0.tgz#090d4d166b342a03a9fec37ef4fd5aeb9c7c6a4b" - integrity sha512-XLwWvqEaq19zFlF5PTgOod4bUA+XbkR4WLQBct1bkzmxJGB0ZEJaoKF4c8cgH9oBtCDuYJ8BP5NB9uFiEgO5QA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-member-expression-to-functions" "^7.16.0" - "@babel/helper-optimise-call-expression" "^7.16.0" - "@babel/helper-replace-supers" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" - -"@babel/helper-create-regexp-features-plugin@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.0.tgz#06b2348ce37fccc4f5e18dcd8d75053f2a7c44ff" - integrity sha512-3DyG0zAFAZKcOp7aVr33ddwkxJ0Z0Jr5V99y3I690eYLpukJsJvAbzTy1ewoCqsML8SbIrjH14Jc/nSQ4TvNPA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - regexpu-core "^4.7.1" - -"@babel/helper-define-polyfill-provider@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.0.tgz#c5b10cf4b324ff840140bb07e05b8564af2ae971" - integrity sha512-7hfT8lUljl/tM3h+izTX/pO3W3frz2ok6Pk+gzys8iJqDfZrZy2pXjRTZAvG2YmfHun1X4q8/UZRLatMfqc5Tg== - dependencies: - "@babel/helper-compilation-targets" "^7.13.0" - "@babel/helper-module-imports" "^7.12.13" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/traverse" "^7.13.0" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - semver "^6.1.2" - -"@babel/helper-explode-assignable-expression@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.0.tgz#753017337a15f46f9c09f674cff10cee9b9d7778" - integrity sha512-Hk2SLxC9ZbcOhLpg/yMznzJ11W++lg5GMbxt1ev6TXUiJB0N42KPC+7w8a+eWGuqDnUYuwStJoZHM7RgmIOaGQ== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-function-name@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.0.tgz#b7dd0797d00bbfee4f07e9c4ea5b0e30c8bb1481" - integrity sha512-BZh4mEk1xi2h4HFjWUXRQX5AEx4rvaZxHgax9gcjdLWdkjsY7MKt5p0otjsg5noXw+pB+clMCjw+aEVYADMjog== - dependencies: - "@babel/helper-get-function-arity" "^7.16.0" - "@babel/template" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helper-get-function-arity@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.0.tgz#0088c7486b29a9cb5d948b1a1de46db66e089cfa" - integrity sha512-ASCquNcywC1NkYh/z7Cgp3w31YW8aojjYIlNg4VeJiHkqyP4AzIvr4qx7pYDb4/s8YcsZWqqOSxgkvjUz1kpDQ== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-hoist-variables@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.0.tgz#4c9023c2f1def7e28ff46fc1dbcd36a39beaa81a" - integrity sha512-1AZlpazjUR0EQZQv3sgRNfM9mEVWPK3M6vlalczA+EECcPz3XPh6VplbErL5UoMpChhSck5wAJHthlj1bYpcmg== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-member-expression-to-functions@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.0.tgz#29287040efd197c77636ef75188e81da8bccd5a4" - integrity sha512-bsjlBFPuWT6IWhl28EdrQ+gTvSvj5tqVP5Xeftp07SEuz5pLnsXZuDkDD3Rfcxy0IsHmbZ+7B2/9SHzxO0T+sQ== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3" - integrity sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-module-transforms@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz#1c82a8dd4cb34577502ebd2909699b194c3e9bb5" - integrity sha512-My4cr9ATcaBbmaEa8M0dZNA74cfI6gitvUAskgDtAFmAqyFKDSHQo5YstxPbN+lzHl2D9l/YOEFqb2mtUh4gfA== - dependencies: - "@babel/helper-module-imports" "^7.16.0" - "@babel/helper-replace-supers" "^7.16.0" - "@babel/helper-simple-access" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" - "@babel/helper-validator-identifier" "^7.15.7" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helper-optimise-call-expression@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.0.tgz#cecdb145d70c54096b1564f8e9f10cd7d193b338" - integrity sha512-SuI467Gi2V8fkofm2JPnZzB/SUuXoJA5zXe/xzyPP2M04686RzFKFHPK6HDVN6JvWBIEW8tt9hPR7fXdn2Lgpw== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" - integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== - -"@babel/helper-remap-async-to-generator@^7.16.0", "@babel/helper-remap-async-to-generator@^7.16.4": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.4.tgz#5d7902f61349ff6b963e07f06a389ce139fbfe6e" - integrity sha512-vGERmmhR+s7eH5Y/cp8PCVzj4XEjerq8jooMfxFdA5xVtAk9Sh4AQsrWgiErUEBjtGrBtOFKDUcWQFW4/dFwMA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-wrap-function" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helper-replace-supers@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.0.tgz#73055e8d3cf9bcba8ddb55cad93fedc860f68f17" - integrity sha512-TQxuQfSCdoha7cpRNJvfaYxxxzmbxXw/+6cS7V02eeDYyhxderSoMVALvwupA54/pZcOTtVeJ0xccp1nGWladA== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.16.0" - "@babel/helper-optimise-call-expression" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helper-simple-access@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.0.tgz#21d6a27620e383e37534cf6c10bba019a6f90517" - integrity sha512-o1rjBT/gppAqKsYfUdfHq5Rk03lMQrkPHG1OWzHWpLgVXRH4HnMM9Et9CVdIqwkCQlobnGHEJMsgWP/jE1zUiw== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-skip-transparent-expression-wrappers@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz#0ee3388070147c3ae051e487eca3ebb0e2e8bb09" - integrity sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-split-export-declaration@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.0.tgz#29672f43663e936df370aaeb22beddb3baec7438" - integrity sha512-0YMMRpuDFNGTHNRiiqJX19GjNXA4H0E8jZ2ibccfSxaCogbm3am5WN/2nQNj0YnQwGWM1J06GOcQ2qnh3+0paw== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.15.7": - version "7.15.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" - integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== - -"@babel/helper-validator-option@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" - integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== - -"@babel/helper-wrap-function@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.0.tgz#b3cf318afce774dfe75b86767cd6d68f3482e57c" - integrity sha512-VVMGzYY3vkWgCJML+qVLvGIam902mJW0FvT7Avj1zEe0Gn7D93aWdLblYARTxEw+6DhZmtzhBM2zv0ekE5zg1g== - dependencies: - "@babel/helper-function-name" "^7.16.0" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helpers@^7.16.0": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.3.tgz#27fc64f40b996e7074dc73128c3e5c3e7f55c43c" - integrity sha512-Xn8IhDlBPhvYTvgewPKawhADichOsbkZuzN7qz2BusOM0brChsyXMDJvldWaYMMUNiCQdQzNEioXTp3sC8Nt8w== - dependencies: - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.3" - "@babel/types" "^7.16.0" - -"@babel/highlight@^7.10.4": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" - integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== - dependencies: - "@babel/helper-validator-identifier" "^7.14.5" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/highlight@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a" - integrity sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g== - dependencies: - "@babel/helper-validator-identifier" "^7.15.7" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/parser@^7.16.0", "@babel/parser@^7.16.3": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.4.tgz#d5f92f57cf2c74ffe9b37981c0e72fee7311372e" - integrity sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng== - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.2": - version "7.16.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.2.tgz#2977fca9b212db153c195674e57cfab807733183" - integrity sha512-h37CvpLSf8gb2lIJ2CgC3t+EjFbi0t8qS7LCS1xcJIlEXE4czlofwaW7W1HA8zpgOCzI9C1nmoqNR1zWkk0pQg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.0.tgz#358972eaab006f5eb0826183b0c93cbcaf13e1e2" - integrity sha512-4tcFwwicpWTrpl9qjf7UsoosaArgImF85AxqCRZlgc3IQDvkUHjJpruXAL58Wmj+T6fypWTC/BakfEkwIL/pwA== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" - "@babel/plugin-proposal-optional-chaining" "^7.16.0" - -"@babel/plugin-proposal-async-generator-functions@^7.16.4": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.4.tgz#e606eb6015fec6fa5978c940f315eae4e300b081" - integrity sha512-/CUekqaAaZCQHleSK/9HajvcD/zdnJiKRiuUFq8ITE+0HsPzquf53cpFiqAwl/UfmJbR6n5uGPQSPdrmKOvHHg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-remap-async-to-generator" "^7.16.4" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-proposal-class-properties@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.0.tgz#c029618267ddebc7280fa286e0f8ca2a278a2d1a" - integrity sha512-mCF3HcuZSY9Fcx56Lbn+CGdT44ioBMMvjNVldpKtj8tpniETdLjnxdHI1+sDWXIM1nNt+EanJOZ3IG9lzVjs7A== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-proposal-class-static-block@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.0.tgz#5296942c564d8144c83eea347d0aa8a0b89170e7" - integrity sha512-mAy3sdcY9sKAkf3lQbDiv3olOfiLqI51c9DR9b19uMoR2Z6r5pmGl7dfNFqEvqOyqbf1ta4lknK4gc5PJn3mfA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-proposal-dynamic-import@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.0.tgz#783eca61d50526202f9b296095453977e88659f1" - integrity sha512-QGSA6ExWk95jFQgwz5GQ2Dr95cf7eI7TKutIXXTb7B1gCLTCz5hTjFTQGfLFBBiC5WSNi7udNwWsqbbMh1c4yQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-proposal-export-namespace-from@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.0.tgz#9c01dee40b9d6b847b656aaf4a3976a71740f222" - integrity sha512-CjI4nxM/D+5wCnhD11MHB1AwRSAYeDT+h8gCdcVJZ/OK7+wRzFsf7PFPWVpVpNRkHMmMkQWAHpTq+15IXQ1diA== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-proposal-json-strings@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.0.tgz#cae35a95ed1d2a7fa29c4dc41540b84a72e9ab25" - integrity sha512-kouIPuiv8mSi5JkEhzApg5Gn6hFyKPnlkO0a9YSzqRurH8wYzSlf6RJdzluAsbqecdW5pBvDJDfyDIUR/vLxvg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-proposal-logical-assignment-operators@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.0.tgz#a711b8ceb3ffddd3ef88d3a49e86dbd3cc7db3fd" - integrity sha512-pbW0fE30sVTYXXm9lpVQQ/Vc+iTeQKiXlaNRZPPN2A2VdlWyAtsUrsQ3xydSlDW00TFMK7a8m3cDTkBF5WnV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.0.tgz#44e1cce08fe2427482cf446a91bb451528ed0596" - integrity sha512-3bnHA8CAFm7cG93v8loghDYyQ8r97Qydf63BeYiGgYbjKKB/XP53W15wfRC7dvKfoiJ34f6Rbyyx2btExc8XsQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-proposal-numeric-separator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.0.tgz#5d418e4fbbf8b9b7d03125d3a52730433a373734" - integrity sha512-FAhE2I6mjispy+vwwd6xWPyEx3NYFS13pikDBWUAFGZvq6POGs5eNchw8+1CYoEgBl9n11I3NkzD7ghn25PQ9Q== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.16.0.tgz#5fb32f6d924d6e6712810362a60e12a2609872e6" - integrity sha512-LU/+jp89efe5HuWJLmMmFG0+xbz+I2rSI7iLc1AlaeSMDMOGzWlc5yJrMN1d04osXN4sSfpo4O+azkBNBes0jg== - dependencies: - "@babel/compat-data" "^7.16.0" - "@babel/helper-compilation-targets" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.16.0" - -"@babel/plugin-proposal-optional-catch-binding@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.0.tgz#5910085811ab4c28b00d6ebffa4ab0274d1e5f16" - integrity sha512-kicDo0A/5J0nrsCPbn89mTG3Bm4XgYi0CZtvex9Oyw7gGZE3HXGD0zpQNH+mo+tEfbo8wbmMvJftOwpmPy7aVw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-proposal-optional-chaining@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.0.tgz#56dbc3970825683608e9efb55ea82c2a2d6c8dc0" - integrity sha512-Y4rFpkZODfHrVo70Uaj6cC1JJOt3Pp0MdWSwIKtb8z1/lsjl9AmnB7ErRFV+QNGIfcY1Eruc2UMx5KaRnXjMyg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-proposal-private-methods@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.0.tgz#b4dafb9c717e4301c5776b30d080d6383c89aff6" - integrity sha512-IvHmcTHDFztQGnn6aWq4t12QaBXTKr1whF/dgp9kz84X6GUcwq9utj7z2wFCUfeOup/QKnOlt2k0zxkGFx9ubg== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-proposal-private-property-in-object@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.0.tgz#69e935b2c5c79d2488112d886f0c4e2790fee76f" - integrity sha512-3jQUr/HBbMVZmi72LpjQwlZ55i1queL8KcDTQEkAHihttJnAPrcvG9ZNXIfsd2ugpizZo595egYV6xy+pv4Ofw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-create-class-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-proposal-unicode-property-regex@^7.16.0", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.0.tgz#890482dfc5ea378e42e19a71e709728cabf18612" - integrity sha512-ti7IdM54NXv29cA4+bNNKEMS4jLMCbJgl+Drv+FgYy0erJLAxNAIXcNjNjrRZEcWq0xJHsNVwQezskMFpF8N9g== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-arrow-functions@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.0.tgz#951706f8b449c834ed07bd474c0924c944b95a8e" - integrity sha512-vIFb5250Rbh7roWARvCLvIJ/PtAU5Lhv7BtZ1u24COwpI9Ypjsh+bZcKk6rlIyalK+r0jOc1XQ8I4ovNxNrWrA== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-async-to-generator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.0.tgz#df12637f9630ddfa0ef9d7a11bc414d629d38604" - integrity sha512-PbIr7G9kR8tdH6g8Wouir5uVjklETk91GMVSUq+VaOgiinbCkBP6Q7NN/suM/QutZkMJMvcyAriogcYAdhg8Gw== - dependencies: - "@babel/helper-module-imports" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-remap-async-to-generator" "^7.16.0" - -"@babel/plugin-transform-block-scoped-functions@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.0.tgz#c618763233ad02847805abcac4c345ce9de7145d" - integrity sha512-V14As3haUOP4ZWrLJ3VVx5rCnrYhMSHN/jX7z6FAt5hjRkLsb0snPCmJwSOML5oxkKO4FNoNv7V5hw/y2bjuvg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-block-scoping@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.0.tgz#bcf433fb482fe8c3d3b4e8a66b1c4a8e77d37c16" - integrity sha512-27n3l67/R3UrXfizlvHGuTwsRIFyce3D/6a37GRxn28iyTPvNXaW4XvznexRh1zUNLPjbLL22Id0XQElV94ruw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-classes@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.0.tgz#54cf5ff0b2242c6573d753cd4bfc7077a8b282f5" - integrity sha512-HUxMvy6GtAdd+GKBNYDWCIA776byUQH8zjnfjxwT1P1ARv/wFu8eBDpmXQcLS/IwRtrxIReGiplOwMeyO7nsDQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-optimise-call-expression" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-replace-supers" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.0.tgz#e0c385507d21e1b0b076d66bed6d5231b85110b7" - integrity sha512-63l1dRXday6S8V3WFY5mXJwcRAnPYxvFfTlt67bwV1rTyVTM5zrp0DBBb13Kl7+ehkCVwIZPumPpFP/4u70+Tw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-destructuring@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.16.0.tgz#ad3d7e74584ad5ea4eadb1e6642146c590dee33c" - integrity sha512-Q7tBUwjxLTsHEoqktemHBMtb3NYwyJPTJdM+wDwb0g8PZ3kQUIzNvwD5lPaqW/p54TXBc/MXZu9Jr7tbUEUM8Q== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-dotall-regex@^7.16.0", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.0.tgz#50bab00c1084b6162d0a58a818031cf57798e06f" - integrity sha512-FXlDZfQeLILfJlC6I1qyEwcHK5UpRCFkaoVyA1nk9A1L1Yu583YO4un2KsLBsu3IJb4CUbctZks8tD9xPQubLw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-duplicate-keys@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.0.tgz#8bc2e21813e3e89e5e5bf3b60aa5fc458575a176" - integrity sha512-LIe2kcHKAZOJDNxujvmp6z3mfN6V9lJxubU4fJIGoQCkKe3Ec2OcbdlYP+vW++4MpxwG0d1wSDOJtQW5kLnkZQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-exponentiation-operator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.0.tgz#a180cd2881e3533cef9d3901e48dad0fbeff4be4" - integrity sha512-OwYEvzFI38hXklsrbNivzpO3fh87skzx8Pnqi4LoSYeav0xHlueSoCJrSgTPfnbyzopo5b3YVAJkFIcUpK2wsw== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-for-of@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.0.tgz#f7abaced155260e2461359bbc7c7248aca5e6bd2" - integrity sha512-5QKUw2kO+GVmKr2wMYSATCTTnHyscl6sxFRAY+rvN7h7WB0lcG0o4NoV6ZQU32OZGVsYUsfLGgPQpDFdkfjlJQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-function-name@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.0.tgz#02e3699c284c6262236599f751065c5d5f1f400e" - integrity sha512-lBzMle9jcOXtSOXUpc7tvvTpENu/NuekNJVova5lCCWCV9/U1ho2HH2y0p6mBg8fPm/syEAbfaaemYGOHCY3mg== - dependencies: - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-literals@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.0.tgz#79711e670ffceb31bd298229d50f3621f7980cac" - integrity sha512-gQDlsSF1iv9RU04clgXqRjrPyyoJMTclFt3K1cjLmTKikc0s/6vE3hlDeEVC71wLTRu72Fq7650kABrdTc2wMQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-member-expression-literals@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.0.tgz#5251b4cce01eaf8314403d21aedb269d79f5e64b" - integrity sha512-WRpw5HL4Jhnxw8QARzRvwojp9MIE7Tdk3ez6vRyUk1MwgjJN0aNpRoXainLR5SgxmoXx/vsXGZ6OthP6t/RbUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-modules-amd@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.0.tgz#09abd41e18dcf4fd479c598c1cef7bd39eb1337e" - integrity sha512-rWFhWbCJ9Wdmzln1NmSCqn7P0RAD+ogXG/bd9Kg5c7PKWkJtkiXmYsMBeXjDlzHpVTJ4I/hnjs45zX4dEv81xw== - dependencies: - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-commonjs@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.0.tgz#add58e638c8ddc4875bd9a9ecb5c594613f6c922" - integrity sha512-Dzi+NWqyEotgzk/sb7kgQPJQf7AJkQBWsVp1N6JWc1lBVo0vkElUnGdr1PzUBmfsCCN5OOFya3RtpeHk15oLKQ== - dependencies: - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-simple-access" "^7.16.0" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-systemjs@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.0.tgz#a92cf240afeb605f4ca16670453024425e421ea4" - integrity sha512-yuGBaHS3lF1m/5R+6fjIke64ii5luRUg97N2wr+z1sF0V+sNSXPxXDdEEL/iYLszsN5VKxVB1IPfEqhzVpiqvg== - dependencies: - "@babel/helper-hoist-variables" "^7.16.0" - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-validator-identifier" "^7.15.7" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-umd@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.0.tgz#195f26c2ad6d6a391b70880effce18ce625e06a7" - integrity sha512-nx4f6no57himWiHhxDM5pjwhae5vLpTK2zCnDH8+wNLJy0TVER/LJRHl2bkt6w9Aad2sPD5iNNoUpY3X9sTGDg== - dependencies: - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.0.tgz#d3db61cc5d5b97986559967cd5ea83e5c32096ca" - integrity sha512-LogN88uO+7EhxWc8WZuQ8vxdSyVGxhkh8WTC3tzlT8LccMuQdA81e9SGV6zY7kY2LjDhhDOFdQVxdGwPyBCnvg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.0" - -"@babel/plugin-transform-new-target@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.0.tgz#af823ab576f752215a49937779a41ca65825ab35" - integrity sha512-fhjrDEYv2DBsGN/P6rlqakwRwIp7rBGLPbrKxwh7oVt5NNkIhZVOY2GRV+ULLsQri1bDqwDWnU3vhlmx5B2aCw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-object-super@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.0.tgz#fb20d5806dc6491a06296ac14ea8e8d6fedda72b" - integrity sha512-fds+puedQHn4cPLshoHcR1DTMN0q1V9ou0mUjm8whx9pGcNvDrVVrgw+KJzzCaiTdaYhldtrUps8DWVMgrSEyg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-replace-supers" "^7.16.0" - -"@babel/plugin-transform-parameters@^7.16.0", "@babel/plugin-transform-parameters@^7.16.3": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.3.tgz#fa9e4c874ee5223f891ee6fa8d737f4766d31d15" - integrity sha512-3MaDpJrOXT1MZ/WCmkOFo7EtmVVC8H4EUZVrHvFOsmwkk4lOjQj8rzv8JKUZV4YoQKeoIgk07GO+acPU9IMu/w== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-property-literals@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.0.tgz#a95c552189a96a00059f6776dc4e00e3690c78d1" - integrity sha512-XLldD4V8+pOqX2hwfWhgwXzGdnDOThxaNTgqagOcpBgIxbUvpgU2FMvo5E1RyHbk756WYgdbS0T8y0Cj9FKkWQ== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-regenerator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.0.tgz#eaee422c84b0232d03aea7db99c97deeaf6125a4" - integrity sha512-JAvGxgKuwS2PihiSFaDrp94XOzzTUeDeOQlcKzVAyaPap7BnZXK/lvMDiubkPTdotPKOIZq9xWXWnggUMYiExg== - dependencies: - regenerator-transform "^0.14.2" - -"@babel/plugin-transform-reserved-words@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.0.tgz#fff4b9dcb19e12619394bda172d14f2d04c0379c" - integrity sha512-Dgs8NNCehHSvXdhEhln8u/TtJxfVwGYCgP2OOr5Z3Ar+B+zXicEOKNTyc+eca2cuEOMtjW6m9P9ijOt8QdqWkg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-shorthand-properties@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.0.tgz#090372e3141f7cc324ed70b3daf5379df2fa384d" - integrity sha512-iVb1mTcD8fuhSv3k99+5tlXu5N0v8/DPm2mO3WACLG6al1CGZH7v09HJyUb1TtYl/Z+KrM6pHSIJdZxP5A+xow== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-spread@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.0.tgz#d21ca099bbd53ab307a8621e019a7bd0f40cdcfb" - integrity sha512-Ao4MSYRaLAQczZVp9/7E7QHsCuK92yHRrmVNRe/SlEJjhzivq0BSn8mEraimL8wizHZ3fuaHxKH0iwzI13GyGg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" - -"@babel/plugin-transform-sticky-regex@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.0.tgz#c35ea31a02d86be485f6aa510184b677a91738fd" - integrity sha512-/ntT2NljR9foobKk4E/YyOSwcGUXtYWv5tinMK/3RkypyNBNdhHUaq6Orw5DWq9ZcNlS03BIlEALFeQgeVAo4Q== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-template-literals@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.0.tgz#a8eced3a8e7b8e2d40ec4ec4548a45912630d302" - integrity sha512-Rd4Ic89hA/f7xUSJQk5PnC+4so50vBoBfxjdQAdvngwidM8jYIBVxBZ/sARxD4e0yMXRbJVDrYf7dyRtIIKT6Q== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-typeof-symbol@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.0.tgz#8b19a244c6f8c9d668dca6a6f754ad6ead1128f2" - integrity sha512-++V2L8Bdf4vcaHi2raILnptTBjGEFxn5315YU+e8+EqXIucA+q349qWngCLpUYqqv233suJ6NOienIVUpS9cqg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-unicode-escapes@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.0.tgz#1a354064b4c45663a32334f46fa0cf6100b5b1f3" - integrity sha512-VFi4dhgJM7Bpk8lRc5CMaRGlKZ29W9C3geZjt9beuzSUrlJxsNwX7ReLwaL6WEvsOf2EQkyIJEPtF8EXjB/g2A== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-unicode-regex@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.0.tgz#293b80950177c8c85aede87cef280259fb995402" - integrity sha512-jHLK4LxhHjvCeZDWyA9c+P9XH1sOxRd1RO9xMtDVRAOND/PczPqizEtVdx4TQF/wyPaewqpT+tgQFYMnN/P94A== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.0" - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/preset-env@^7.16.0": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.16.4.tgz#4f6ec33b2a3fe72d6bfdcdf3859500232563a2e3" - integrity sha512-v0QtNd81v/xKj4gNKeuAerQ/azeNn/G1B1qMLeXOcV8+4TWlD2j3NV1u8q29SDFBXx/NBq5kyEAO+0mpRgacjA== - dependencies: - "@babel/compat-data" "^7.16.4" - "@babel/helper-compilation-targets" "^7.16.3" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-validator-option" "^7.14.5" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.16.2" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.16.0" - "@babel/plugin-proposal-async-generator-functions" "^7.16.4" - "@babel/plugin-proposal-class-properties" "^7.16.0" - "@babel/plugin-proposal-class-static-block" "^7.16.0" - "@babel/plugin-proposal-dynamic-import" "^7.16.0" - "@babel/plugin-proposal-export-namespace-from" "^7.16.0" - "@babel/plugin-proposal-json-strings" "^7.16.0" - "@babel/plugin-proposal-logical-assignment-operators" "^7.16.0" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.16.0" - "@babel/plugin-proposal-numeric-separator" "^7.16.0" - "@babel/plugin-proposal-object-rest-spread" "^7.16.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.16.0" - "@babel/plugin-proposal-optional-chaining" "^7.16.0" - "@babel/plugin-proposal-private-methods" "^7.16.0" - "@babel/plugin-proposal-private-property-in-object" "^7.16.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.16.0" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.16.0" - "@babel/plugin-transform-async-to-generator" "^7.16.0" - "@babel/plugin-transform-block-scoped-functions" "^7.16.0" - "@babel/plugin-transform-block-scoping" "^7.16.0" - "@babel/plugin-transform-classes" "^7.16.0" - "@babel/plugin-transform-computed-properties" "^7.16.0" - "@babel/plugin-transform-destructuring" "^7.16.0" - "@babel/plugin-transform-dotall-regex" "^7.16.0" - "@babel/plugin-transform-duplicate-keys" "^7.16.0" - "@babel/plugin-transform-exponentiation-operator" "^7.16.0" - "@babel/plugin-transform-for-of" "^7.16.0" - "@babel/plugin-transform-function-name" "^7.16.0" - "@babel/plugin-transform-literals" "^7.16.0" - "@babel/plugin-transform-member-expression-literals" "^7.16.0" - "@babel/plugin-transform-modules-amd" "^7.16.0" - "@babel/plugin-transform-modules-commonjs" "^7.16.0" - "@babel/plugin-transform-modules-systemjs" "^7.16.0" - "@babel/plugin-transform-modules-umd" "^7.16.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.16.0" - "@babel/plugin-transform-new-target" "^7.16.0" - "@babel/plugin-transform-object-super" "^7.16.0" - "@babel/plugin-transform-parameters" "^7.16.3" - "@babel/plugin-transform-property-literals" "^7.16.0" - "@babel/plugin-transform-regenerator" "^7.16.0" - "@babel/plugin-transform-reserved-words" "^7.16.0" - "@babel/plugin-transform-shorthand-properties" "^7.16.0" - "@babel/plugin-transform-spread" "^7.16.0" - "@babel/plugin-transform-sticky-regex" "^7.16.0" - "@babel/plugin-transform-template-literals" "^7.16.0" - "@babel/plugin-transform-typeof-symbol" "^7.16.0" - "@babel/plugin-transform-unicode-escapes" "^7.16.0" - "@babel/plugin-transform-unicode-regex" "^7.16.0" - "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.16.0" - babel-plugin-polyfill-corejs2 "^0.3.0" - babel-plugin-polyfill-corejs3 "^0.4.0" - babel-plugin-polyfill-regenerator "^0.3.0" - core-js-compat "^3.19.1" - semver "^6.3.0" - -"@babel/preset-modules@^0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/runtime@^7.8.4": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" - integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/template@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6" - integrity sha512-MnZdpFD/ZdYhXwiunMqqgyZyucaYsbL0IrjoGjaVhGilz+x8YB++kRfygSOIj1yOtWKPlx7NBp+9I1RQSgsd5A== - dependencies: - "@babel/code-frame" "^7.16.0" - "@babel/parser" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.3": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.3.tgz#f63e8a938cc1b780f66d9ed3c54f532ca2d14787" - integrity sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag== - dependencies: - "@babel/code-frame" "^7.16.0" - "@babel/generator" "^7.16.0" - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-hoist-variables" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" - "@babel/parser" "^7.16.3" - "@babel/types" "^7.16.0" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/types@^7.16.0", "@babel/types@^7.4.4": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.0.tgz#db3b313804f96aadd0b776c4823e127ad67289ba" - integrity sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg== - dependencies: - "@babel/helper-validator-identifier" "^7.15.7" - to-fast-properties "^2.0.0" - -"@discoveryjs/json-ext@^0.5.0": - version "0.5.5" - resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz#9283c9ce5b289a3c4f61c12757469e59377f81f3" - integrity sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA== - -"@eslint/eslintrc@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" - integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== - dependencies: - ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^13.9.0" - ignore "^4.0.6" - import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" - strip-json-comments "^3.1.1" - -"@humanwhocodes/config-array@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" - integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== - dependencies: - "@humanwhocodes/object-schema" "^1.2.0" - debug "^4.1.1" - minimatch "^3.0.4" - -"@humanwhocodes/object-schema@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" - integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== - -"@types/eslint-scope@^3.7.0": - version "3.7.1" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.1.tgz#8dc390a7b4f9dd9f1284629efce982e41612116e" - integrity sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.28.0.tgz#7e41f2481d301c68e14f483fe10b017753ce8d5a" - integrity sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*", "@types/estree@^0.0.50": - version "0.0.50" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" - integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== - -"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8": - version "7.0.9" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" - integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== - -"@types/node@*": - version "16.10.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.3.tgz#7a8f2838603ea314d1d22bb3171d899e15c57bd5" - integrity sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ== - -"@webassemblyjs/ast@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" - integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== - dependencies: - "@webassemblyjs/helper-numbers" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - -"@webassemblyjs/floating-point-hex-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" - integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== - -"@webassemblyjs/helper-api-error@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" - integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== - -"@webassemblyjs/helper-buffer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" - integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== - -"@webassemblyjs/helper-numbers@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" - integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== - dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/helper-wasm-bytecode@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" - integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== - -"@webassemblyjs/helper-wasm-section@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" - integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - -"@webassemblyjs/ieee754@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" - integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" - integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" - integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== - -"@webassemblyjs/wasm-edit@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" - integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/helper-wasm-section" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-opt" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - "@webassemblyjs/wast-printer" "1.11.1" - -"@webassemblyjs/wasm-gen@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" - integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wasm-opt@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" - integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - -"@webassemblyjs/wasm-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" - integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wast-printer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" - integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@xtuc/long" "4.2.2" - -"@webpack-cli/configtest@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.1.0.tgz#8342bef0badfb7dfd3b576f2574ab80c725be043" - integrity sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg== - -"@webpack-cli/info@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.4.0.tgz#b9179c3227ab09cbbb149aa733475fcf99430223" - integrity sha512-F6b+Man0rwE4n0409FyAJHStYA5OIZERxmnUfLVwv0mc0V1wLad3V7jqRlMkgKBeAq07jUvglacNaa6g9lOpuw== - dependencies: - envinfo "^7.7.3" - -"@webpack-cli/serve@^1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.6.0.tgz#2c275aa05c895eccebbfc34cfb223c6e8bd591a2" - integrity sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA== - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -acorn-import-assertions@^1.7.6: - version "1.8.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" - integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== - -acorn-jsx@^5.3.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn@^7.4.0: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== - -acorn@^8.4.1: - version "8.5.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" - integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== - -ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^8.0.1: - version "8.6.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764" - integrity sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -approx-string-match@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/approx-string-match/-/approx-string-match-1.1.0.tgz#2fb8e1d6dcb640acc1c0d1ae9f0895348d06f4c0" - integrity sha512-j1yQB9XhfGWsvTfHEuNsR/SrUT4XQDkAc0PEjMifyi97931LmNQyLsO6HbuvZ3HeMx+3Dvk8m8XGkUF+8lCeqw== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - -babel-loader@^8.2.3: - version "8.2.3" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.3.tgz#8986b40f1a64cacfcb4b8429320085ef68b1342d" - integrity sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw== - dependencies: - find-cache-dir "^3.3.1" - loader-utils "^1.4.0" - make-dir "^3.1.0" - schema-utils "^2.6.5" - -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-polyfill-corejs2@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.0.tgz#407082d0d355ba565af24126fb6cb8e9115251fd" - integrity sha512-wMDoBJ6uG4u4PNFh72Ty6t3EgfA91puCuAwKIazbQlci+ENb/UU9A3xG5lutjUIiXCIn1CY5L15r9LimiJyrSA== - dependencies: - "@babel/compat-data" "^7.13.11" - "@babel/helper-define-polyfill-provider" "^0.3.0" - semver "^6.1.1" - -babel-plugin-polyfill-corejs3@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.4.0.tgz#0b571f4cf3d67f911512f5c04842a7b8e8263087" - integrity sha512-YxFreYwUfglYKdLUGvIF2nJEsGwj+RhWSX/ije3D2vQPOXuyMLMtg/cCGMDpOA7Nd+MwlNdnGODbd2EwUZPlsw== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.0" - core-js-compat "^3.18.0" - -babel-plugin-polyfill-regenerator@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.0.tgz#9ebbcd7186e1a33e21c5e20cae4e7983949533be" - integrity sha512-dhAPTDLGoMW5/84wkgwiLRwMnio2i1fUe53EuvtKMv0pn2p3S8OCoV1xAzfJPl0KOX7IB89s2ib85vbYiea3jg== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.0" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -browserslist@^4.14.5: - version "4.17.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.3.tgz#2844cd6eebe14d12384b0122d217550160d2d624" - integrity sha512-59IqHJV5VGdcJZ+GZ2hU5n4Kv3YiASzW6Xk5g9tf5a/MAzGeFwgGWU39fVzNIOVcgB3+Gp+kiQu0HEfTVU/3VQ== - dependencies: - caniuse-lite "^1.0.30001264" - electron-to-chromium "^1.3.857" - escalade "^3.1.1" - node-releases "^1.1.77" - picocolors "^0.2.1" - -browserslist@^4.17.5, browserslist@^4.17.6: - version "4.18.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.18.1.tgz#60d3920f25b6860eb917c6c7b185576f4d8b017f" - integrity sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ== - dependencies: - caniuse-lite "^1.0.30001280" - electron-to-chromium "^1.3.896" - escalade "^3.1.1" - node-releases "^2.0.1" - picocolors "^1.0.0" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -caniuse-lite@^1.0.30001264: - version "1.0.30001265" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz#0613c9e6c922e422792e6fcefdf9a3afeee4f8c3" - integrity sha512-YzBnspggWV5hep1m9Z6sZVLOt7vrju8xWooFAgN6BA5qvy98qPAPb7vNUzypFaoh2pb3vlfzbDO8tB57UPGbtw== - -caniuse-lite@^1.0.30001280: - version "1.0.30001283" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001283.tgz#8573685bdae4d733ef18f78d44ba0ca5fe9e896b" - integrity sha512-9RoKo841j1GQFSJz/nCXOj0sD7tHBtlowjYlrqIUS812x9/emfBLBt6IyMz1zIaYc/eRL8Cs6HPUVi2Hzq4sIg== - -cartesian@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cartesian/-/cartesian-1.0.1.tgz#ae3fc8a63e2ba7e2c4989ce696207457bcae65af" - integrity sha1-rj/Ipj4rp+LEmJzmliB0V7yuZa8= - dependencies: - xtend "^4.0.1" - -chalk@^2.0.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chrome-trace-event@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" - integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colorette@^2.0.14: - version "2.0.16" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" - integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -convert-source-map@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" - integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== - dependencies: - safe-buffer "~5.1.1" - -core-js-compat@^3.18.0, core-js-compat@^3.19.1: - version "3.19.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.19.1.tgz#fe598f1a9bf37310d77c3813968e9f7c7bb99476" - integrity sha512-Q/VJ7jAF/y68+aUsQJ/afPOewdsGkDtcMb40J8MbuWKlK3Y+wtHq8bTHKPj2WKWLIqmS5JhHs4CzHtz6pT2W6g== - dependencies: - browserslist "^4.17.6" - semver "7.0.0" - -cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -css-selector-generator@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/css-selector-generator/-/css-selector-generator-3.6.0.tgz#06293fb3d9303cc4b55f2d8e40aa6b85bf1c405b" - integrity sha512-t1wTGaASlIyCd5nYA0sptNgn97bkAWlZlVSvQKXbR9xhWANDvTu9blgAFyZ8db8xGyTzUIDYjngjAmwmMNM+KQ== - dependencies: - cartesian "^1.0.1" - iselement "^1.1.4" - -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== - dependencies: - ms "2.1.2" - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -electron-to-chromium@^1.3.857: - version "1.3.862" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.862.tgz#c1c5d4382449e2c9b0e67fe1652f4fc451d6d8c0" - integrity sha512-o+FMbCD+hAUJ9S8bfz/FaqA0gE8OpCCm58KhhGogOEqiA1BLFSoVYLi+tW+S/ZavnqBn++n0XZm7HQiBVPs8Jg== - -electron-to-chromium@^1.3.896: - version "1.4.3" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.3.tgz#82480df3ef607f04bb38cc3f30a628d8b895339f" - integrity sha512-hfpppjYhqIZB8jrNb0rNceQRkSnBN7QJl3W26O1jUv3F3BkQknqy1YTqVXkFnIcFtBc3Qnv5M7r5Lez2iOLgZA== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -enhanced-resolve@^5.8.3: - version "5.8.3" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0" - integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -enquirer@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== - dependencies: - ansi-colors "^4.1.1" - -envinfo@^7.7.3: - version "7.8.1" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" - integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== - -es-abstract@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" - integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^1.1.1" - get-symbol-description "^1.0.0" - has "^1.0.3" - has-symbols "^1.0.2" - internal-slot "^1.0.3" - is-callable "^1.2.4" - is-negative-zero "^2.0.1" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.1" - is-string "^1.0.7" - is-weakref "^1.0.1" - object-inspect "^1.11.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.1" - -es-module-lexer@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-scope@5.1.1, eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint@^7.29.0: - version "7.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" - integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== - dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.3" - "@humanwhocodes/config-array" "^0.5.0" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.0.1" - doctrine "^3.0.0" - enquirer "^2.3.5" - escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" - globals "^13.6.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - js-yaml "^3.13.1" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.0.4" - natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" - strip-json-comments "^3.1.0" - table "^6.0.9" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== - dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -events@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -fastest-levenshtein@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" - integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -find-cache-dir@^3.3.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" - integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - -find-up@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== - dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" - -flatted@^3.1.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" - integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" - integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - -glob-parent@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -glob@^7.1.3: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^13.6.0, globals@^13.9.0: - version "13.11.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.11.0.tgz#40ef678da117fe7bd2e28f1fab24951bd0255be7" - integrity sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g== - dependencies: - type-fest "^0.20.2" - -graceful-fs@^4.1.2, graceful-fs@^4.2.4: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== - -has-bigints@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-symbols@^1.0.1, has-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== - -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hash.js@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-local@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.3.tgz#4d51c2c495ca9393da259ec66b62e022920211e0" - integrity sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA== - dependencies: - pkg-dir "^4.2.0" - resolve-cwd "^3.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -internal-slot@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" - integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== - dependencies: - get-intrinsic "^1.1.0" - has "^1.0.3" - side-channel "^1.0.4" - -interpret@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" - integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== - -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-callable@^1.1.4, is-callable@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" - integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== - -is-core-module@^2.2.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.7.0.tgz#3c0ef7d31b4acfc574f80c58409d568a836848e3" - integrity sha512-ByY+tjCciCr+9nLryBYcSD50EOGWt95c7tIsKTG1J2ixKKXPvF7Ej3AVd+UfDydAJom3biBGDBALaO79ktwgEQ== - dependencies: - has "^1.0.3" - -is-date-object@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^4.0.0, is-glob@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-negative-zero@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" - integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== - -is-number-object@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" - integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== - dependencies: - has-tostringtag "^1.0.0" - -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-shared-array-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" - integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-weakref@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2" - integrity sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ== - dependencies: - call-bind "^1.0.0" - -iselement@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/iselement/-/iselement-1.1.4.tgz#7e55b52a8ebca50a7e2e80e5b8d2840f32353146" - integrity sha1-flW1Ko68pQp+LoDluNKEDzI1MUY= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -jest-worker@^27.0.6: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.2.5.tgz#ed42865661959488aa020e8a325df010597c36d4" - integrity sha512-HTjEPZtcNKZ4LnhSp02NEH4vE+5OpJ0EsOWYvGQpHgUMLngydESAAMH5Wd/asPf29+XUDQZszxpLg1BkIIA2aw== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - -json-parse-better-errors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - -json5@^2.1.2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== - dependencies: - minimist "^1.2.5" - -kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -loader-runner@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" - integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== - -loader-utils@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -make-dir@^3.0.2, make-dir@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -mime-db@1.50.0: - version "1.50.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.50.0.tgz#abd4ac94e98d3c0e185016c67ab45d5fde40c11f" - integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A== - -mime-types@^2.1.27: - version "2.1.33" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.33.tgz#1fa12a904472fafd068e48d9e8401f74d3f70edb" - integrity sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g== - dependencies: - mime-db "1.50.0" - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.5: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -node-releases@^1.1.77: - version "1.1.77" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.77.tgz#50b0cfede855dd374e7585bf228ff34e57c1c32e" - integrity sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ== - -node-releases@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" - integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -object-inspect@^1.11.0, object-inspect@^1.9.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" - integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.0, object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.3" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -picocolors@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" - integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -pkg-dir@^4.1.0, pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -prettier@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" - integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== - -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -rechoir@^0.7.0: - version "0.7.1" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" - integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== - dependencies: - resolve "^1.9.0" - -regenerate-unicode-properties@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326" - integrity sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA== - dependencies: - regenerate "^1.4.2" - -regenerate@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.13.4: - version "0.13.9" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" - integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== - -regenerator-transform@^0.14.2: - version "0.14.5" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" - integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== - dependencies: - "@babel/runtime" "^7.8.4" - -regexp.prototype.flags@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" - integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -regexpp@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - -regexpu-core@^4.7.1: - version "4.8.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.8.0.tgz#e5605ba361b67b1718478501327502f4479a98f0" - integrity sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg== - dependencies: - regenerate "^1.4.2" - regenerate-unicode-properties "^9.0.0" - regjsgen "^0.5.2" - regjsparser "^0.7.0" - unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.0.0" - -regjsgen@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" - integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== - -regjsparser@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.7.0.tgz#a6b667b54c885e18b52554cb4960ef71187e9968" - integrity sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ== - dependencies: - jsesc "~0.5.0" - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -resolve-cwd@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" - integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== - dependencies: - resolve-from "^5.0.0" - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve@^1.14.2, resolve@^1.9.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== - dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -safe-buffer@^5.1.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -schema-utils@^2.6.5: - version "2.7.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" - integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== - dependencies: - "@types/json-schema" "^7.0.5" - ajv "^6.12.4" - ajv-keywords "^3.5.2" - -schema-utils@^3.1.0, schema-utils@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" - integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -semver@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.2.1: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - -serialize-javascript@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" - integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== - dependencies: - randombytes "^2.1.0" - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - -signal-exit@^3.0.3: - version "3.0.5" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f" - integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== - -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -source-map-support@~0.5.20: - version "0.5.20" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" - integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.5.0: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0, source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -source-map@~0.7.2: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string.prototype.matchall@^4.0.5: - version "4.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa" - integrity sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - get-intrinsic "^1.1.1" - has-symbols "^1.0.2" - internal-slot "^1.0.3" - regexp.prototype.flags "^1.3.1" - side-channel "^1.0.4" - -string.prototype.trimend@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -table@^6.0.9: - version "6.7.2" - resolved "https://registry.yarnpkg.com/table/-/table-6.7.2.tgz#a8d39b9f5966693ca8b0feba270a78722cbaf3b0" - integrity sha512-UFZK67uvyNivLeQbVtkiUs8Uuuxv24aSL4/Vil2PJVtMgU8Lx0CYkP12uCGa3kjyQzOSgV1+z9Wkb82fCGsO0g== - dependencies: - ajv "^8.0.1" - lodash.clonedeep "^4.5.0" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - -tapable@^2.1.1, tapable@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -terser-webpack-plugin@^5.1.3: - version "5.2.4" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.2.4.tgz#ad1be7639b1cbe3ea49fab995cbe7224b31747a1" - integrity sha512-E2CkNMN+1cho04YpdANyRrn8CyN4yMy+WdFKZIySFZrGXZxJwJP6PMNGGc/Mcr6qygQHUUqRxnAPmi0M9f00XA== - dependencies: - jest-worker "^27.0.6" - p-limit "^3.1.0" - schema-utils "^3.1.1" - serialize-javascript "^6.0.0" - source-map "^0.6.1" - terser "^5.7.2" - -terser@^5.7.2: - version "5.9.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.9.0.tgz#47d6e629a522963240f2b55fcaa3c99083d2c351" - integrity sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ== - dependencies: - commander "^2.20.0" - source-map "~0.7.2" - source-map-support "~0.5.20" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -unbox-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" - integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== - dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.1" - has-symbols "^1.0.2" - which-boxed-primitive "^1.0.2" - -unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== - -unicode-match-property-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" - integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== - dependencies: - unicode-canonical-property-names-ecmascript "^2.0.0" - unicode-property-aliases-ecmascript "^2.0.0" - -unicode-match-property-value-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" - integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== - -unicode-property-aliases-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" - integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - -watchpack@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce" - integrity sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -webpack-cli@^4.7.2: - version "4.9.0" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.9.0.tgz#dc43e6e0f80dd52e89cbf73d5294bcd7ad6eb343" - integrity sha512-n/jZZBMzVEl4PYIBs+auy2WI0WTQ74EnJDiyD98O2JZY6IVIHJNitkYp/uTXOviIOMfgzrNvC9foKv/8o8KSZw== - dependencies: - "@discoveryjs/json-ext" "^0.5.0" - "@webpack-cli/configtest" "^1.1.0" - "@webpack-cli/info" "^1.4.0" - "@webpack-cli/serve" "^1.6.0" - colorette "^2.0.14" - commander "^7.0.0" - execa "^5.0.0" - fastest-levenshtein "^1.0.12" - import-local "^3.0.2" - interpret "^2.2.0" - rechoir "^0.7.0" - v8-compile-cache "^2.2.0" - webpack-merge "^5.7.3" - -webpack-merge@^5.7.3: - version "5.8.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" - integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== - dependencies: - clone-deep "^4.0.1" - wildcard "^2.0.0" - -webpack-sources@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.1.tgz#251a7d9720d75ada1469ca07dbb62f3641a05b6d" - integrity sha512-t6BMVLQ0AkjBOoRTZgqrWm7xbXMBzD+XDq2EZ96+vMfn3qKgsvdXZhbPZ4ElUOpdv4u+iiGe+w3+J75iy/bYGA== - -webpack@^5.40.0: - version "5.58.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.58.1.tgz#df8aad72b617a9d0db8c89d4f410784ee93320d7" - integrity sha512-4Z/dmbTU+VmkCb2XNgW7wkE5TfEcSooclprn/UEuVeAkwHhn07OcgUsyaKHGtCY/VobjnsYBlyhKeMLiSoOqPg== - dependencies: - "@types/eslint-scope" "^3.7.0" - "@types/estree" "^0.0.50" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - acorn "^8.4.1" - acorn-import-assertions "^1.7.6" - browserslist "^4.14.5" - chrome-trace-event "^1.0.2" - enhanced-resolve "^5.8.3" - es-module-lexer "^0.9.0" - eslint-scope "5.1.1" - events "^3.2.0" - glob-to-regexp "^0.4.1" - graceful-fs "^4.2.4" - json-parse-better-errors "^1.0.2" - loader-runner "^4.2.0" - mime-types "^2.1.27" - neo-async "^2.6.2" - schema-utils "^3.1.0" - tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" - watchpack "^2.2.0" - webpack-sources "^3.2.0" - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wildcard@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" - integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== - -word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -xtend@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/readium/navigator/src/main/assets/readium/error.xhtml b/readium/navigator/src/main/assets/readium/error.xhtml index 66d7f90ca8..ab268c488c 100644 --- a/readium/navigator/src/main/assets/readium/error.xhtml +++ b/readium/navigator/src/main/assets/readium/error.xhtml @@ -1,13 +1,11 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" - "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> + "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> -<head> - <title>${error}</title> -</head> -<body style="text-align: center;"> -<h1 style="font-size: 5em;">${error}</h1> -<h2><pre style="white-space: pre-wrap;">${href}</pre></h2> +<body> + <svg xmlns="http://www.w3.org/2000/svg" style="position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; width: 30%; height: 30%" height="24" viewBox="0 -960 960 960" width="24"> + <path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm40-337 160-160 160 160 160-160 40 40v-183H200v263l40 40Zm-40 257h560v-264l-40-40-160 160-160-160-160 160-40-40v184Zm0 0v-264 80-376 560Z"/> + </svg> </body> </html> diff --git a/readium/navigator/src/main/assets/readium/scripts/readium-fixed.js b/readium/navigator/src/main/assets/readium/scripts/readium-fixed.js index b0730f4a78..407d142503 100644 --- a/readium/navigator/src/main/assets/readium/scripts/readium-fixed.js +++ b/readium/navigator/src/main/assets/readium/scripts/readium-fixed.js @@ -1 +1,2 @@ -(function(){var __webpack_modules__={3089:function(__unused_webpack_module,exports){"use strict";eval('var __webpack_unused_export__;\n\n/**\n * Implementation of Myers\' online approximate string matching algorithm [1],\n * with additional optimizations suggested by [2].\n *\n * This has O((k/w) * n) complexity where `n` is the length of the text, `k` is\n * the maximum number of errors allowed (always <= the pattern length) and `w`\n * is the word size. Because JS only supports bitwise operations on 32 bit\n * integers, `w` is 32.\n *\n * As far as I am aware, there aren\'t any online algorithms which are\n * significantly better for a wide range of input parameters. The problem can be\n * solved faster using "filter then verify" approaches which first filter out\n * regions of the text that cannot match using a "cheap" check and then verify\n * the remaining potential matches. The verify step requires an algorithm such\n * as this one however.\n *\n * The algorithm\'s approach is essentially to optimize the classic dynamic\n * programming solution to the problem by computing columns of the matrix in\n * word-sized chunks (ie. dealing with 32 chars of the pattern at a time) and\n * avoiding calculating regions of the matrix where the minimum error count is\n * guaranteed to exceed the input threshold.\n *\n * The paper consists of two parts, the first describes the core algorithm for\n * matching patterns <= the size of a word (implemented by `advanceBlock` here).\n * The second uses the core algorithm as part of a larger block-based algorithm\n * to handle longer patterns.\n *\n * [1] G. Myers, “A Fast Bit-Vector Algorithm for Approximate String Matching\n * Based on Dynamic Programming,” vol. 46, no. 3, pp. 395–415, 1999.\n *\n * [2] Šošić, M. (2014). An simd dynamic programming c/c++ library (Doctoral\n * dissertation, Fakultet Elektrotehnike i računarstva, Sveučilište u Zagrebu).\n */\n__webpack_unused_export__ = ({ value: true });\nfunction reverse(s) {\n return s\n .split("")\n .reverse()\n .join("");\n}\n/**\n * Given the ends of approximate matches for `pattern` in `text`, find\n * the start of the matches.\n *\n * @param findEndFn - Function for finding the end of matches in\n * text.\n * @return Matches with the `start` property set.\n */\nfunction findMatchStarts(text, pattern, matches) {\n var patRev = reverse(pattern);\n return matches.map(function (m) {\n // Find start of each match by reversing the pattern and matching segment\n // of text and searching for an approx match with the same number of\n // errors.\n var minStart = Math.max(0, m.end - pattern.length - m.errors);\n var textRev = reverse(text.slice(minStart, m.end));\n // If there are multiple possible start points, choose the one that\n // maximizes the length of the match.\n var start = findMatchEnds(textRev, patRev, m.errors).reduce(function (min, rm) {\n if (m.end - rm.end < min) {\n return m.end - rm.end;\n }\n return min;\n }, m.end);\n return {\n start: start,\n end: m.end,\n errors: m.errors\n };\n });\n}\n/**\n * Return 1 if a number is non-zero or zero otherwise, without using\n * conditional operators.\n *\n * This should get inlined into `advanceBlock` below by the JIT.\n *\n * Adapted from https://stackoverflow.com/a/3912218/434243\n */\nfunction oneIfNotZero(n) {\n return ((n | -n) >> 31) & 1;\n}\n/**\n * Block calculation step of the algorithm.\n *\n * From Fig 8. on p. 408 of [1], additionally optimized to replace conditional\n * checks with bitwise operations as per Section 4.2.3 of [2].\n *\n * @param ctx - The pattern context object\n * @param peq - The `peq` array for the current character (`ctx.peq.get(ch)`)\n * @param b - The block level\n * @param hIn - Horizontal input delta ∈ {1,0,-1}\n * @return Horizontal output delta ∈ {1,0,-1}\n */\nfunction advanceBlock(ctx, peq, b, hIn) {\n var pV = ctx.P[b];\n var mV = ctx.M[b];\n var hInIsNegative = hIn >>> 31; // 1 if hIn < 0 or 0 otherwise.\n var eq = peq[b] | hInIsNegative;\n // Step 1: Compute horizontal deltas.\n var xV = eq | mV;\n var xH = (((eq & pV) + pV) ^ pV) | eq;\n var pH = mV | ~(xH | pV);\n var mH = pV & xH;\n // Step 2: Update score (value of last row of this block).\n var hOut = oneIfNotZero(pH & ctx.lastRowMask[b]) -\n oneIfNotZero(mH & ctx.lastRowMask[b]);\n // Step 3: Update vertical deltas for use when processing next char.\n pH <<= 1;\n mH <<= 1;\n mH |= hInIsNegative;\n pH |= oneIfNotZero(hIn) - hInIsNegative; // set pH[0] if hIn > 0\n pV = mH | ~(xV | pH);\n mV = pH & xV;\n ctx.P[b] = pV;\n ctx.M[b] = mV;\n return hOut;\n}\n/**\n * Find the ends and error counts for matches of `pattern` in `text`.\n *\n * Only the matches with the lowest error count are reported. Other matches\n * with error counts <= maxErrors are discarded.\n *\n * This is the block-based search algorithm from Fig. 9 on p.410 of [1].\n */\nfunction findMatchEnds(text, pattern, maxErrors) {\n if (pattern.length === 0) {\n return [];\n }\n // Clamp error count so we can rely on the `maxErrors` and `pattern.length`\n // rows being in the same block below.\n maxErrors = Math.min(maxErrors, pattern.length);\n var matches = [];\n // Word size.\n var w = 32;\n // Index of maximum block level.\n var bMax = Math.ceil(pattern.length / w) - 1;\n // Context used across block calculations.\n var ctx = {\n P: new Uint32Array(bMax + 1),\n M: new Uint32Array(bMax + 1),\n lastRowMask: new Uint32Array(bMax + 1)\n };\n ctx.lastRowMask.fill(1 << 31);\n ctx.lastRowMask[bMax] = 1 << (pattern.length - 1) % w;\n // Dummy "peq" array for chars in the text which do not occur in the pattern.\n var emptyPeq = new Uint32Array(bMax + 1);\n // Map of UTF-16 character code to bit vector indicating positions in the\n // pattern that equal that character.\n var peq = new Map();\n // Version of `peq` that only stores mappings for small characters. This\n // allows faster lookups when iterating through the text because a simple\n // array lookup can be done instead of a hash table lookup.\n var asciiPeq = [];\n for (var i = 0; i < 256; i++) {\n asciiPeq.push(emptyPeq);\n }\n // Calculate `ctx.peq` - a map of character values to bitmasks indicating\n // positions of that character within the pattern, where each bit represents\n // a position in the pattern.\n for (var c = 0; c < pattern.length; c += 1) {\n var val = pattern.charCodeAt(c);\n if (peq.has(val)) {\n // Duplicate char in pattern.\n continue;\n }\n var charPeq = new Uint32Array(bMax + 1);\n peq.set(val, charPeq);\n if (val < asciiPeq.length) {\n asciiPeq[val] = charPeq;\n }\n for (var b = 0; b <= bMax; b += 1) {\n charPeq[b] = 0;\n // Set all the bits where the pattern matches the current char (ch).\n // For indexes beyond the end of the pattern, always set the bit as if the\n // pattern contained a wildcard char in that position.\n for (var r = 0; r < w; r += 1) {\n var idx = b * w + r;\n if (idx >= pattern.length) {\n continue;\n }\n var match = pattern.charCodeAt(idx) === val;\n if (match) {\n charPeq[b] |= 1 << r;\n }\n }\n }\n }\n // Index of last-active block level in the column.\n var y = Math.max(0, Math.ceil(maxErrors / w) - 1);\n // Initialize maximum error count at bottom of each block.\n var score = new Uint32Array(bMax + 1);\n for (var b = 0; b <= y; b += 1) {\n score[b] = (b + 1) * w;\n }\n score[bMax] = pattern.length;\n // Initialize vertical deltas for each block.\n for (var b = 0; b <= y; b += 1) {\n ctx.P[b] = ~0;\n ctx.M[b] = 0;\n }\n // Process each char of the text, computing the error count for `w` chars of\n // the pattern at a time.\n for (var j = 0; j < text.length; j += 1) {\n // Lookup the bitmask representing the positions of the current char from\n // the text within the pattern.\n var charCode = text.charCodeAt(j);\n var charPeq = void 0;\n if (charCode < asciiPeq.length) {\n // Fast array lookup.\n charPeq = asciiPeq[charCode];\n }\n else {\n // Slower hash table lookup.\n charPeq = peq.get(charCode);\n if (typeof charPeq === "undefined") {\n charPeq = emptyPeq;\n }\n }\n // Calculate error count for blocks that we definitely have to process for\n // this column.\n var carry = 0;\n for (var b = 0; b <= y; b += 1) {\n carry = advanceBlock(ctx, charPeq, b, carry);\n score[b] += carry;\n }\n // Check if we also need to compute an additional block, or if we can reduce\n // the number of blocks processed for the next column.\n if (score[y] - carry <= maxErrors &&\n y < bMax &&\n (charPeq[y + 1] & 1 || carry < 0)) {\n // Error count for bottom block is under threshold, increase the number of\n // blocks processed for this column & next by 1.\n y += 1;\n ctx.P[y] = ~0;\n ctx.M[y] = 0;\n var maxBlockScore = y === bMax ? pattern.length % w : w;\n score[y] =\n score[y - 1] +\n maxBlockScore -\n carry +\n advanceBlock(ctx, charPeq, y, carry);\n }\n else {\n // Error count for bottom block exceeds threshold, reduce the number of\n // blocks processed for the next column.\n while (y > 0 && score[y] >= maxErrors + w) {\n y -= 1;\n }\n }\n // If error count is under threshold, report a match.\n if (y === bMax && score[y] <= maxErrors) {\n if (score[y] < maxErrors) {\n // Discard any earlier, worse matches.\n matches.splice(0, matches.length);\n }\n matches.push({\n start: -1,\n end: j + 1,\n errors: score[y]\n });\n // Because `search` only reports the matches with the lowest error count,\n // we can "ratchet down" the max error threshold whenever a match is\n // encountered and thereby save a small amount of work for the remainder\n // of the text.\n maxErrors = score[y];\n }\n }\n return matches;\n}\n/**\n * Search for matches for `pattern` in `text` allowing up to `maxErrors` errors.\n *\n * Returns the start, and end positions and error counts for each lowest-cost\n * match. Only the "best" matches are returned.\n */\nfunction search(text, pattern, maxErrors) {\n var matches = findMatchEnds(text, pattern, maxErrors);\n return findMatchStarts(text, pattern, matches);\n}\nexports.Z = search;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzA4OS5qcyIsIm1hcHBpbmdzIjoiO0FBQWE7QUFDYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSw2QkFBNkMsRUFBRSxhQUFhLENBQUM7QUFDN0Q7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLFNBQVM7QUFDVDtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsS0FBSztBQUNMO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLDBDQUEwQztBQUMxQyxzQ0FBc0M7QUFDdEM7QUFDQTtBQUNBO0FBQ0E7QUFDQSxvQ0FBb0M7QUFDcEM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSw2Q0FBNkM7QUFDN0M7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxvQkFBb0IsU0FBUztBQUM3QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esb0JBQW9CLG9CQUFvQjtBQUN4QztBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLHdCQUF3QixXQUFXO0FBQ25DO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsNEJBQTRCLE9BQU87QUFDbkM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esb0JBQW9CLFFBQVE7QUFDNUI7QUFDQTtBQUNBO0FBQ0E7QUFDQSxvQkFBb0IsUUFBUTtBQUM1QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esb0JBQW9CLGlCQUFpQjtBQUNyQztBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSx3QkFBd0IsUUFBUTtBQUNoQztBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLGFBQWE7QUFDYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLFNBQWUiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2FwcHJveC1zdHJpbmctbWF0Y2gvZGlzdC9pbmRleC5qcz83MjMwIl0sInNvdXJjZXNDb250ZW50IjpbIlwidXNlIHN0cmljdFwiO1xuLyoqXG4gKiBJbXBsZW1lbnRhdGlvbiBvZiBNeWVycycgb25saW5lIGFwcHJveGltYXRlIHN0cmluZyBtYXRjaGluZyBhbGdvcml0aG0gWzFdLFxuICogd2l0aCBhZGRpdGlvbmFsIG9wdGltaXphdGlvbnMgc3VnZ2VzdGVkIGJ5IFsyXS5cbiAqXG4gKiBUaGlzIGhhcyBPKChrL3cpICogbikgY29tcGxleGl0eSB3aGVyZSBgbmAgaXMgdGhlIGxlbmd0aCBvZiB0aGUgdGV4dCwgYGtgIGlzXG4gKiB0aGUgbWF4aW11bSBudW1iZXIgb2YgZXJyb3JzIGFsbG93ZWQgKGFsd2F5cyA8PSB0aGUgcGF0dGVybiBsZW5ndGgpIGFuZCBgd2BcbiAqIGlzIHRoZSB3b3JkIHNpemUuIEJlY2F1c2UgSlMgb25seSBzdXBwb3J0cyBiaXR3aXNlIG9wZXJhdGlvbnMgb24gMzIgYml0XG4gKiBpbnRlZ2VycywgYHdgIGlzIDMyLlxuICpcbiAqIEFzIGZhciBhcyBJIGFtIGF3YXJlLCB0aGVyZSBhcmVuJ3QgYW55IG9ubGluZSBhbGdvcml0aG1zIHdoaWNoIGFyZVxuICogc2lnbmlmaWNhbnRseSBiZXR0ZXIgZm9yIGEgd2lkZSByYW5nZSBvZiBpbnB1dCBwYXJhbWV0ZXJzLiBUaGUgcHJvYmxlbSBjYW4gYmVcbiAqIHNvbHZlZCBmYXN0ZXIgdXNpbmcgXCJmaWx0ZXIgdGhlbiB2ZXJpZnlcIiBhcHByb2FjaGVzIHdoaWNoIGZpcnN0IGZpbHRlciBvdXRcbiAqIHJlZ2lvbnMgb2YgdGhlIHRleHQgdGhhdCBjYW5ub3QgbWF0Y2ggdXNpbmcgYSBcImNoZWFwXCIgY2hlY2sgYW5kIHRoZW4gdmVyaWZ5XG4gKiB0aGUgcmVtYWluaW5nIHBvdGVudGlhbCBtYXRjaGVzLiBUaGUgdmVyaWZ5IHN0ZXAgcmVxdWlyZXMgYW4gYWxnb3JpdGhtIHN1Y2hcbiAqIGFzIHRoaXMgb25lIGhvd2V2ZXIuXG4gKlxuICogVGhlIGFsZ29yaXRobSdzIGFwcHJvYWNoIGlzIGVzc2VudGlhbGx5IHRvIG9wdGltaXplIHRoZSBjbGFzc2ljIGR5bmFtaWNcbiAqIHByb2dyYW1taW5nIHNvbHV0aW9uIHRvIHRoZSBwcm9ibGVtIGJ5IGNvbXB1dGluZyBjb2x1bW5zIG9mIHRoZSBtYXRyaXggaW5cbiAqIHdvcmQtc2l6ZWQgY2h1bmtzIChpZS4gZGVhbGluZyB3aXRoIDMyIGNoYXJzIG9mIHRoZSBwYXR0ZXJuIGF0IGEgdGltZSkgYW5kXG4gKiBhdm9pZGluZyBjYWxjdWxhdGluZyByZWdpb25zIG9mIHRoZSBtYXRyaXggd2hlcmUgdGhlIG1pbmltdW0gZXJyb3IgY291bnQgaXNcbiAqIGd1YXJhbnRlZWQgdG8gZXhjZWVkIHRoZSBpbnB1dCB0aHJlc2hvbGQuXG4gKlxuICogVGhlIHBhcGVyIGNvbnNpc3RzIG9mIHR3byBwYXJ0cywgdGhlIGZpcnN0IGRlc2NyaWJlcyB0aGUgY29yZSBhbGdvcml0aG0gZm9yXG4gKiBtYXRjaGluZyBwYXR0ZXJucyA8PSB0aGUgc2l6ZSBvZiBhIHdvcmQgKGltcGxlbWVudGVkIGJ5IGBhZHZhbmNlQmxvY2tgIGhlcmUpLlxuICogVGhlIHNlY29uZCB1c2VzIHRoZSBjb3JlIGFsZ29yaXRobSBhcyBwYXJ0IG9mIGEgbGFyZ2VyIGJsb2NrLWJhc2VkIGFsZ29yaXRobVxuICogdG8gaGFuZGxlIGxvbmdlciBwYXR0ZXJucy5cbiAqXG4gKiBbMV0gRy4gTXllcnMsIOKAnEEgRmFzdCBCaXQtVmVjdG9yIEFsZ29yaXRobSBmb3IgQXBwcm94aW1hdGUgU3RyaW5nIE1hdGNoaW5nXG4gKiBCYXNlZCBvbiBEeW5hbWljIFByb2dyYW1taW5nLOKAnSB2b2wuIDQ2LCBuby4gMywgcHAuIDM5NeKAkzQxNSwgMTk5OS5cbiAqXG4gKiBbMl0gxaBvxaFpxIcsIE0uICgyMDE0KS4gQW4gc2ltZCBkeW5hbWljIHByb2dyYW1taW5nIGMvYysrIGxpYnJhcnkgKERvY3RvcmFsXG4gKiBkaXNzZXJ0YXRpb24sIEZha3VsdGV0IEVsZWt0cm90ZWhuaWtlIGkgcmHEjXVuYXJzdHZhLCBTdmV1xI1pbGnFoXRlIHUgWmFncmVidSkuXG4gKi9cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCBcIl9fZXNNb2R1bGVcIiwgeyB2YWx1ZTogdHJ1ZSB9KTtcbmZ1bmN0aW9uIHJldmVyc2Uocykge1xuICAgIHJldHVybiBzXG4gICAgICAgIC5zcGxpdChcIlwiKVxuICAgICAgICAucmV2ZXJzZSgpXG4gICAgICAgIC5qb2luKFwiXCIpO1xufVxuLyoqXG4gKiBHaXZlbiB0aGUgZW5kcyBvZiBhcHByb3hpbWF0ZSBtYXRjaGVzIGZvciBgcGF0dGVybmAgaW4gYHRleHRgLCBmaW5kXG4gKiB0aGUgc3RhcnQgb2YgdGhlIG1hdGNoZXMuXG4gKlxuICogQHBhcmFtIGZpbmRFbmRGbiAtIEZ1bmN0aW9uIGZvciBmaW5kaW5nIHRoZSBlbmQgb2YgbWF0Y2hlcyBpblxuICogdGV4dC5cbiAqIEByZXR1cm4gTWF0Y2hlcyB3aXRoIHRoZSBgc3RhcnRgIHByb3BlcnR5IHNldC5cbiAqL1xuZnVuY3Rpb24gZmluZE1hdGNoU3RhcnRzKHRleHQsIHBhdHRlcm4sIG1hdGNoZXMpIHtcbiAgICB2YXIgcGF0UmV2ID0gcmV2ZXJzZShwYXR0ZXJuKTtcbiAgICByZXR1cm4gbWF0Y2hlcy5tYXAoZnVuY3Rpb24gKG0pIHtcbiAgICAgICAgLy8gRmluZCBzdGFydCBvZiBlYWNoIG1hdGNoIGJ5IHJldmVyc2luZyB0aGUgcGF0dGVybiBhbmQgbWF0Y2hpbmcgc2VnbWVudFxuICAgICAgICAvLyBvZiB0ZXh0IGFuZCBzZWFyY2hpbmcgZm9yIGFuIGFwcHJveCBtYXRjaCB3aXRoIHRoZSBzYW1lIG51bWJlciBvZlxuICAgICAgICAvLyBlcnJvcnMuXG4gICAgICAgIHZhciBtaW5TdGFydCA9IE1hdGgubWF4KDAsIG0uZW5kIC0gcGF0dGVybi5sZW5ndGggLSBtLmVycm9ycyk7XG4gICAgICAgIHZhciB0ZXh0UmV2ID0gcmV2ZXJzZSh0ZXh0LnNsaWNlKG1pblN0YXJ0LCBtLmVuZCkpO1xuICAgICAgICAvLyBJZiB0aGVyZSBhcmUgbXVsdGlwbGUgcG9zc2libGUgc3RhcnQgcG9pbnRzLCBjaG9vc2UgdGhlIG9uZSB0aGF0XG4gICAgICAgIC8vIG1heGltaXplcyB0aGUgbGVuZ3RoIG9mIHRoZSBtYXRjaC5cbiAgICAgICAgdmFyIHN0YXJ0ID0gZmluZE1hdGNoRW5kcyh0ZXh0UmV2LCBwYXRSZXYsIG0uZXJyb3JzKS5yZWR1Y2UoZnVuY3Rpb24gKG1pbiwgcm0pIHtcbiAgICAgICAgICAgIGlmIChtLmVuZCAtIHJtLmVuZCA8IG1pbikge1xuICAgICAgICAgICAgICAgIHJldHVybiBtLmVuZCAtIHJtLmVuZDtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIHJldHVybiBtaW47XG4gICAgICAgIH0sIG0uZW5kKTtcbiAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICAgIHN0YXJ0OiBzdGFydCxcbiAgICAgICAgICAgIGVuZDogbS5lbmQsXG4gICAgICAgICAgICBlcnJvcnM6IG0uZXJyb3JzXG4gICAgICAgIH07XG4gICAgfSk7XG59XG4vKipcbiAqIFJldHVybiAxIGlmIGEgbnVtYmVyIGlzIG5vbi16ZXJvIG9yIHplcm8gb3RoZXJ3aXNlLCB3aXRob3V0IHVzaW5nXG4gKiBjb25kaXRpb25hbCBvcGVyYXRvcnMuXG4gKlxuICogVGhpcyBzaG91bGQgZ2V0IGlubGluZWQgaW50byBgYWR2YW5jZUJsb2NrYCBiZWxvdyBieSB0aGUgSklULlxuICpcbiAqIEFkYXB0ZWQgZnJvbSBodHRwczovL3N0YWNrb3ZlcmZsb3cuY29tL2EvMzkxMjIxOC80MzQyNDNcbiAqL1xuZnVuY3Rpb24gb25lSWZOb3RaZXJvKG4pIHtcbiAgICByZXR1cm4gKChuIHwgLW4pID4+IDMxKSAmIDE7XG59XG4vKipcbiAqIEJsb2NrIGNhbGN1bGF0aW9uIHN0ZXAgb2YgdGhlIGFsZ29yaXRobS5cbiAqXG4gKiBGcm9tIEZpZyA4LiBvbiBwLiA0MDggb2YgWzFdLCBhZGRpdGlvbmFsbHkgb3B0aW1pemVkIHRvIHJlcGxhY2UgY29uZGl0aW9uYWxcbiAqIGNoZWNrcyB3aXRoIGJpdHdpc2Ugb3BlcmF0aW9ucyBhcyBwZXIgU2VjdGlvbiA0LjIuMyBvZiBbMl0uXG4gKlxuICogQHBhcmFtIGN0eCAtIFRoZSBwYXR0ZXJuIGNvbnRleHQgb2JqZWN0XG4gKiBAcGFyYW0gcGVxIC0gVGhlIGBwZXFgIGFycmF5IGZvciB0aGUgY3VycmVudCBjaGFyYWN0ZXIgKGBjdHgucGVxLmdldChjaClgKVxuICogQHBhcmFtIGIgLSBUaGUgYmxvY2sgbGV2ZWxcbiAqIEBwYXJhbSBoSW4gLSBIb3Jpem9udGFsIGlucHV0IGRlbHRhIOKIiCB7MSwwLC0xfVxuICogQHJldHVybiBIb3Jpem9udGFsIG91dHB1dCBkZWx0YSDiiIggezEsMCwtMX1cbiAqL1xuZnVuY3Rpb24gYWR2YW5jZUJsb2NrKGN0eCwgcGVxLCBiLCBoSW4pIHtcbiAgICB2YXIgcFYgPSBjdHguUFtiXTtcbiAgICB2YXIgbVYgPSBjdHguTVtiXTtcbiAgICB2YXIgaEluSXNOZWdhdGl2ZSA9IGhJbiA+Pj4gMzE7IC8vIDEgaWYgaEluIDwgMCBvciAwIG90aGVyd2lzZS5cbiAgICB2YXIgZXEgPSBwZXFbYl0gfCBoSW5Jc05lZ2F0aXZlO1xuICAgIC8vIFN0ZXAgMTogQ29tcHV0ZSBob3Jpem9udGFsIGRlbHRhcy5cbiAgICB2YXIgeFYgPSBlcSB8IG1WO1xuICAgIHZhciB4SCA9ICgoKGVxICYgcFYpICsgcFYpIF4gcFYpIHwgZXE7XG4gICAgdmFyIHBIID0gbVYgfCB+KHhIIHwgcFYpO1xuICAgIHZhciBtSCA9IHBWICYgeEg7XG4gICAgLy8gU3RlcCAyOiBVcGRhdGUgc2NvcmUgKHZhbHVlIG9mIGxhc3Qgcm93IG9mIHRoaXMgYmxvY2spLlxuICAgIHZhciBoT3V0ID0gb25lSWZOb3RaZXJvKHBIICYgY3R4Lmxhc3RSb3dNYXNrW2JdKSAtXG4gICAgICAgIG9uZUlmTm90WmVybyhtSCAmIGN0eC5sYXN0Um93TWFza1tiXSk7XG4gICAgLy8gU3RlcCAzOiBVcGRhdGUgdmVydGljYWwgZGVsdGFzIGZvciB1c2Ugd2hlbiBwcm9jZXNzaW5nIG5leHQgY2hhci5cbiAgICBwSCA8PD0gMTtcbiAgICBtSCA8PD0gMTtcbiAgICBtSCB8PSBoSW5Jc05lZ2F0aXZlO1xuICAgIHBIIHw9IG9uZUlmTm90WmVybyhoSW4pIC0gaEluSXNOZWdhdGl2ZTsgLy8gc2V0IHBIWzBdIGlmIGhJbiA+IDBcbiAgICBwViA9IG1IIHwgfih4ViB8IHBIKTtcbiAgICBtViA9IHBIICYgeFY7XG4gICAgY3R4LlBbYl0gPSBwVjtcbiAgICBjdHguTVtiXSA9IG1WO1xuICAgIHJldHVybiBoT3V0O1xufVxuLyoqXG4gKiBGaW5kIHRoZSBlbmRzIGFuZCBlcnJvciBjb3VudHMgZm9yIG1hdGNoZXMgb2YgYHBhdHRlcm5gIGluIGB0ZXh0YC5cbiAqXG4gKiBPbmx5IHRoZSBtYXRjaGVzIHdpdGggdGhlIGxvd2VzdCBlcnJvciBjb3VudCBhcmUgcmVwb3J0ZWQuIE90aGVyIG1hdGNoZXNcbiAqIHdpdGggZXJyb3IgY291bnRzIDw9IG1heEVycm9ycyBhcmUgZGlzY2FyZGVkLlxuICpcbiAqIFRoaXMgaXMgdGhlIGJsb2NrLWJhc2VkIHNlYXJjaCBhbGdvcml0aG0gZnJvbSBGaWcuIDkgb24gcC40MTAgb2YgWzFdLlxuICovXG5mdW5jdGlvbiBmaW5kTWF0Y2hFbmRzKHRleHQsIHBhdHRlcm4sIG1heEVycm9ycykge1xuICAgIGlmIChwYXR0ZXJuLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICByZXR1cm4gW107XG4gICAgfVxuICAgIC8vIENsYW1wIGVycm9yIGNvdW50IHNvIHdlIGNhbiByZWx5IG9uIHRoZSBgbWF4RXJyb3JzYCBhbmQgYHBhdHRlcm4ubGVuZ3RoYFxuICAgIC8vIHJvd3MgYmVpbmcgaW4gdGhlIHNhbWUgYmxvY2sgYmVsb3cuXG4gICAgbWF4RXJyb3JzID0gTWF0aC5taW4obWF4RXJyb3JzLCBwYXR0ZXJuLmxlbmd0aCk7XG4gICAgdmFyIG1hdGNoZXMgPSBbXTtcbiAgICAvLyBXb3JkIHNpemUuXG4gICAgdmFyIHcgPSAzMjtcbiAgICAvLyBJbmRleCBvZiBtYXhpbXVtIGJsb2NrIGxldmVsLlxuICAgIHZhciBiTWF4ID0gTWF0aC5jZWlsKHBhdHRlcm4ubGVuZ3RoIC8gdykgLSAxO1xuICAgIC8vIENvbnRleHQgdXNlZCBhY3Jvc3MgYmxvY2sgY2FsY3VsYXRpb25zLlxuICAgIHZhciBjdHggPSB7XG4gICAgICAgIFA6IG5ldyBVaW50MzJBcnJheShiTWF4ICsgMSksXG4gICAgICAgIE06IG5ldyBVaW50MzJBcnJheShiTWF4ICsgMSksXG4gICAgICAgIGxhc3RSb3dNYXNrOiBuZXcgVWludDMyQXJyYXkoYk1heCArIDEpXG4gICAgfTtcbiAgICBjdHgubGFzdFJvd01hc2suZmlsbCgxIDw8IDMxKTtcbiAgICBjdHgubGFzdFJvd01hc2tbYk1heF0gPSAxIDw8IChwYXR0ZXJuLmxlbmd0aCAtIDEpICUgdztcbiAgICAvLyBEdW1teSBcInBlcVwiIGFycmF5IGZvciBjaGFycyBpbiB0aGUgdGV4dCB3aGljaCBkbyBub3Qgb2NjdXIgaW4gdGhlIHBhdHRlcm4uXG4gICAgdmFyIGVtcHR5UGVxID0gbmV3IFVpbnQzMkFycmF5KGJNYXggKyAxKTtcbiAgICAvLyBNYXAgb2YgVVRGLTE2IGNoYXJhY3RlciBjb2RlIHRvIGJpdCB2ZWN0b3IgaW5kaWNhdGluZyBwb3NpdGlvbnMgaW4gdGhlXG4gICAgLy8gcGF0dGVybiB0aGF0IGVxdWFsIHRoYXQgY2hhcmFjdGVyLlxuICAgIHZhciBwZXEgPSBuZXcgTWFwKCk7XG4gICAgLy8gVmVyc2lvbiBvZiBgcGVxYCB0aGF0IG9ubHkgc3RvcmVzIG1hcHBpbmdzIGZvciBzbWFsbCBjaGFyYWN0ZXJzLiBUaGlzXG4gICAgLy8gYWxsb3dzIGZhc3RlciBsb29rdXBzIHdoZW4gaXRlcmF0aW5nIHRocm91Z2ggdGhlIHRleHQgYmVjYXVzZSBhIHNpbXBsZVxuICAgIC8vIGFycmF5IGxvb2t1cCBjYW4gYmUgZG9uZSBpbnN0ZWFkIG9mIGEgaGFzaCB0YWJsZSBsb29rdXAuXG4gICAgdmFyIGFzY2lpUGVxID0gW107XG4gICAgZm9yICh2YXIgaSA9IDA7IGkgPCAyNTY7IGkrKykge1xuICAgICAgICBhc2NpaVBlcS5wdXNoKGVtcHR5UGVxKTtcbiAgICB9XG4gICAgLy8gQ2FsY3VsYXRlIGBjdHgucGVxYCAtIGEgbWFwIG9mIGNoYXJhY3RlciB2YWx1ZXMgdG8gYml0bWFza3MgaW5kaWNhdGluZ1xuICAgIC8vIHBvc2l0aW9ucyBvZiB0aGF0IGNoYXJhY3RlciB3aXRoaW4gdGhlIHBhdHRlcm4sIHdoZXJlIGVhY2ggYml0IHJlcHJlc2VudHNcbiAgICAvLyBhIHBvc2l0aW9uIGluIHRoZSBwYXR0ZXJuLlxuICAgIGZvciAodmFyIGMgPSAwOyBjIDwgcGF0dGVybi5sZW5ndGg7IGMgKz0gMSkge1xuICAgICAgICB2YXIgdmFsID0gcGF0dGVybi5jaGFyQ29kZUF0KGMpO1xuICAgICAgICBpZiAocGVxLmhhcyh2YWwpKSB7XG4gICAgICAgICAgICAvLyBEdXBsaWNhdGUgY2hhciBpbiBwYXR0ZXJuLlxuICAgICAgICAgICAgY29udGludWU7XG4gICAgICAgIH1cbiAgICAgICAgdmFyIGNoYXJQZXEgPSBuZXcgVWludDMyQXJyYXkoYk1heCArIDEpO1xuICAgICAgICBwZXEuc2V0KHZhbCwgY2hhclBlcSk7XG4gICAgICAgIGlmICh2YWwgPCBhc2NpaVBlcS5sZW5ndGgpIHtcbiAgICAgICAgICAgIGFzY2lpUGVxW3ZhbF0gPSBjaGFyUGVxO1xuICAgICAgICB9XG4gICAgICAgIGZvciAodmFyIGIgPSAwOyBiIDw9IGJNYXg7IGIgKz0gMSkge1xuICAgICAgICAgICAgY2hhclBlcVtiXSA9IDA7XG4gICAgICAgICAgICAvLyBTZXQgYWxsIHRoZSBiaXRzIHdoZXJlIHRoZSBwYXR0ZXJuIG1hdGNoZXMgdGhlIGN1cnJlbnQgY2hhciAoY2gpLlxuICAgICAgICAgICAgLy8gRm9yIGluZGV4ZXMgYmV5b25kIHRoZSBlbmQgb2YgdGhlIHBhdHRlcm4sIGFsd2F5cyBzZXQgdGhlIGJpdCBhcyBpZiB0aGVcbiAgICAgICAgICAgIC8vIHBhdHRlcm4gY29udGFpbmVkIGEgd2lsZGNhcmQgY2hhciBpbiB0aGF0IHBvc2l0aW9uLlxuICAgICAgICAgICAgZm9yICh2YXIgciA9IDA7IHIgPCB3OyByICs9IDEpIHtcbiAgICAgICAgICAgICAgICB2YXIgaWR4ID0gYiAqIHcgKyByO1xuICAgICAgICAgICAgICAgIGlmIChpZHggPj0gcGF0dGVybi5sZW5ndGgpIHtcbiAgICAgICAgICAgICAgICAgICAgY29udGludWU7XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIHZhciBtYXRjaCA9IHBhdHRlcm4uY2hhckNvZGVBdChpZHgpID09PSB2YWw7XG4gICAgICAgICAgICAgICAgaWYgKG1hdGNoKSB7XG4gICAgICAgICAgICAgICAgICAgIGNoYXJQZXFbYl0gfD0gMSA8PCByO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cbiAgICAvLyBJbmRleCBvZiBsYXN0LWFjdGl2ZSBibG9jayBsZXZlbCBpbiB0aGUgY29sdW1uLlxuICAgIHZhciB5ID0gTWF0aC5tYXgoMCwgTWF0aC5jZWlsKG1heEVycm9ycyAvIHcpIC0gMSk7XG4gICAgLy8gSW5pdGlhbGl6ZSBtYXhpbXVtIGVycm9yIGNvdW50IGF0IGJvdHRvbSBvZiBlYWNoIGJsb2NrLlxuICAgIHZhciBzY29yZSA9IG5ldyBVaW50MzJBcnJheShiTWF4ICsgMSk7XG4gICAgZm9yICh2YXIgYiA9IDA7IGIgPD0geTsgYiArPSAxKSB7XG4gICAgICAgIHNjb3JlW2JdID0gKGIgKyAxKSAqIHc7XG4gICAgfVxuICAgIHNjb3JlW2JNYXhdID0gcGF0dGVybi5sZW5ndGg7XG4gICAgLy8gSW5pdGlhbGl6ZSB2ZXJ0aWNhbCBkZWx0YXMgZm9yIGVhY2ggYmxvY2suXG4gICAgZm9yICh2YXIgYiA9IDA7IGIgPD0geTsgYiArPSAxKSB7XG4gICAgICAgIGN0eC5QW2JdID0gfjA7XG4gICAgICAgIGN0eC5NW2JdID0gMDtcbiAgICB9XG4gICAgLy8gUHJvY2VzcyBlYWNoIGNoYXIgb2YgdGhlIHRleHQsIGNvbXB1dGluZyB0aGUgZXJyb3IgY291bnQgZm9yIGB3YCBjaGFycyBvZlxuICAgIC8vIHRoZSBwYXR0ZXJuIGF0IGEgdGltZS5cbiAgICBmb3IgKHZhciBqID0gMDsgaiA8IHRleHQubGVuZ3RoOyBqICs9IDEpIHtcbiAgICAgICAgLy8gTG9va3VwIHRoZSBiaXRtYXNrIHJlcHJlc2VudGluZyB0aGUgcG9zaXRpb25zIG9mIHRoZSBjdXJyZW50IGNoYXIgZnJvbVxuICAgICAgICAvLyB0aGUgdGV4dCB3aXRoaW4gdGhlIHBhdHRlcm4uXG4gICAgICAgIHZhciBjaGFyQ29kZSA9IHRleHQuY2hhckNvZGVBdChqKTtcbiAgICAgICAgdmFyIGNoYXJQZXEgPSB2b2lkIDA7XG4gICAgICAgIGlmIChjaGFyQ29kZSA8IGFzY2lpUGVxLmxlbmd0aCkge1xuICAgICAgICAgICAgLy8gRmFzdCBhcnJheSBsb29rdXAuXG4gICAgICAgICAgICBjaGFyUGVxID0gYXNjaWlQZXFbY2hhckNvZGVdO1xuICAgICAgICB9XG4gICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgLy8gU2xvd2VyIGhhc2ggdGFibGUgbG9va3VwLlxuICAgICAgICAgICAgY2hhclBlcSA9IHBlcS5nZXQoY2hhckNvZGUpO1xuICAgICAgICAgICAgaWYgKHR5cGVvZiBjaGFyUGVxID09PSBcInVuZGVmaW5lZFwiKSB7XG4gICAgICAgICAgICAgICAgY2hhclBlcSA9IGVtcHR5UGVxO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgIC8vIENhbGN1bGF0ZSBlcnJvciBjb3VudCBmb3IgYmxvY2tzIHRoYXQgd2UgZGVmaW5pdGVseSBoYXZlIHRvIHByb2Nlc3MgZm9yXG4gICAgICAgIC8vIHRoaXMgY29sdW1uLlxuICAgICAgICB2YXIgY2FycnkgPSAwO1xuICAgICAgICBmb3IgKHZhciBiID0gMDsgYiA8PSB5OyBiICs9IDEpIHtcbiAgICAgICAgICAgIGNhcnJ5ID0gYWR2YW5jZUJsb2NrKGN0eCwgY2hhclBlcSwgYiwgY2FycnkpO1xuICAgICAgICAgICAgc2NvcmVbYl0gKz0gY2Fycnk7XG4gICAgICAgIH1cbiAgICAgICAgLy8gQ2hlY2sgaWYgd2UgYWxzbyBuZWVkIHRvIGNvbXB1dGUgYW4gYWRkaXRpb25hbCBibG9jaywgb3IgaWYgd2UgY2FuIHJlZHVjZVxuICAgICAgICAvLyB0aGUgbnVtYmVyIG9mIGJsb2NrcyBwcm9jZXNzZWQgZm9yIHRoZSBuZXh0IGNvbHVtbi5cbiAgICAgICAgaWYgKHNjb3JlW3ldIC0gY2FycnkgPD0gbWF4RXJyb3JzICYmXG4gICAgICAgICAgICB5IDwgYk1heCAmJlxuICAgICAgICAgICAgKGNoYXJQZXFbeSArIDFdICYgMSB8fCBjYXJyeSA8IDApKSB7XG4gICAgICAgICAgICAvLyBFcnJvciBjb3VudCBmb3IgYm90dG9tIGJsb2NrIGlzIHVuZGVyIHRocmVzaG9sZCwgaW5jcmVhc2UgdGhlIG51bWJlciBvZlxuICAgICAgICAgICAgLy8gYmxvY2tzIHByb2Nlc3NlZCBmb3IgdGhpcyBjb2x1bW4gJiBuZXh0IGJ5IDEuXG4gICAgICAgICAgICB5ICs9IDE7XG4gICAgICAgICAgICBjdHguUFt5XSA9IH4wO1xuICAgICAgICAgICAgY3R4Lk1beV0gPSAwO1xuICAgICAgICAgICAgdmFyIG1heEJsb2NrU2NvcmUgPSB5ID09PSBiTWF4ID8gcGF0dGVybi5sZW5ndGggJSB3IDogdztcbiAgICAgICAgICAgIHNjb3JlW3ldID1cbiAgICAgICAgICAgICAgICBzY29yZVt5IC0gMV0gK1xuICAgICAgICAgICAgICAgICAgICBtYXhCbG9ja1Njb3JlIC1cbiAgICAgICAgICAgICAgICAgICAgY2FycnkgK1xuICAgICAgICAgICAgICAgICAgICBhZHZhbmNlQmxvY2soY3R4LCBjaGFyUGVxLCB5LCBjYXJyeSk7XG4gICAgICAgIH1cbiAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICAvLyBFcnJvciBjb3VudCBmb3IgYm90dG9tIGJsb2NrIGV4Y2VlZHMgdGhyZXNob2xkLCByZWR1Y2UgdGhlIG51bWJlciBvZlxuICAgICAgICAgICAgLy8gYmxvY2tzIHByb2Nlc3NlZCBmb3IgdGhlIG5leHQgY29sdW1uLlxuICAgICAgICAgICAgd2hpbGUgKHkgPiAwICYmIHNjb3JlW3ldID49IG1heEVycm9ycyArIHcpIHtcbiAgICAgICAgICAgICAgICB5IC09IDE7XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgLy8gSWYgZXJyb3IgY291bnQgaXMgdW5kZXIgdGhyZXNob2xkLCByZXBvcnQgYSBtYXRjaC5cbiAgICAgICAgaWYgKHkgPT09IGJNYXggJiYgc2NvcmVbeV0gPD0gbWF4RXJyb3JzKSB7XG4gICAgICAgICAgICBpZiAoc2NvcmVbeV0gPCBtYXhFcnJvcnMpIHtcbiAgICAgICAgICAgICAgICAvLyBEaXNjYXJkIGFueSBlYXJsaWVyLCB3b3JzZSBtYXRjaGVzLlxuICAgICAgICAgICAgICAgIG1hdGNoZXMuc3BsaWNlKDAsIG1hdGNoZXMubGVuZ3RoKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIG1hdGNoZXMucHVzaCh7XG4gICAgICAgICAgICAgICAgc3RhcnQ6IC0xLFxuICAgICAgICAgICAgICAgIGVuZDogaiArIDEsXG4gICAgICAgICAgICAgICAgZXJyb3JzOiBzY29yZVt5XVxuICAgICAgICAgICAgfSk7XG4gICAgICAgICAgICAvLyBCZWNhdXNlIGBzZWFyY2hgIG9ubHkgcmVwb3J0cyB0aGUgbWF0Y2hlcyB3aXRoIHRoZSBsb3dlc3QgZXJyb3IgY291bnQsXG4gICAgICAgICAgICAvLyB3ZSBjYW4gXCJyYXRjaGV0IGRvd25cIiB0aGUgbWF4IGVycm9yIHRocmVzaG9sZCB3aGVuZXZlciBhIG1hdGNoIGlzXG4gICAgICAgICAgICAvLyBlbmNvdW50ZXJlZCBhbmQgdGhlcmVieSBzYXZlIGEgc21hbGwgYW1vdW50IG9mIHdvcmsgZm9yIHRoZSByZW1haW5kZXJcbiAgICAgICAgICAgIC8vIG9mIHRoZSB0ZXh0LlxuICAgICAgICAgICAgbWF4RXJyb3JzID0gc2NvcmVbeV07XG4gICAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIG1hdGNoZXM7XG59XG4vKipcbiAqIFNlYXJjaCBmb3IgbWF0Y2hlcyBmb3IgYHBhdHRlcm5gIGluIGB0ZXh0YCBhbGxvd2luZyB1cCB0byBgbWF4RXJyb3JzYCBlcnJvcnMuXG4gKlxuICogUmV0dXJucyB0aGUgc3RhcnQsIGFuZCBlbmQgcG9zaXRpb25zIGFuZCBlcnJvciBjb3VudHMgZm9yIGVhY2ggbG93ZXN0LWNvc3RcbiAqIG1hdGNoLiBPbmx5IHRoZSBcImJlc3RcIiBtYXRjaGVzIGFyZSByZXR1cm5lZC5cbiAqL1xuZnVuY3Rpb24gc2VhcmNoKHRleHQsIHBhdHRlcm4sIG1heEVycm9ycykge1xuICAgIHZhciBtYXRjaGVzID0gZmluZE1hdGNoRW5kcyh0ZXh0LCBwYXR0ZXJuLCBtYXhFcnJvcnMpO1xuICAgIHJldHVybiBmaW5kTWF0Y2hTdGFydHModGV4dCwgcGF0dGVybiwgbWF0Y2hlcyk7XG59XG5leHBvcnRzLmRlZmF1bHQgPSBzZWFyY2g7XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3089\n')},6396:function(__unused_webpack_module,__unused_webpack___webpack_exports__,__webpack_require__){"use strict";eval('\n// EXTERNAL MODULE: ./node_modules/approx-string-match/dist/index.js\nvar dist = __webpack_require__(3089);\n;// CONCATENATED MODULE: ./src/vendor/hypothesis/anchoring/match-quote.js\n\n/**\n * @typedef {import(\'approx-string-match\').Match} StringMatch\n */\n\n/**\n * @typedef Match\n * @prop {number} start - Start offset of match in text\n * @prop {number} end - End offset of match in text\n * @prop {number} score -\n * Score for the match between 0 and 1.0, where 1.0 indicates a perfect match\n * for the quote and context.\n */\n\n/**\n * Find the best approximate matches for `str` in `text` allowing up to `maxErrors` errors.\n *\n * @param {string} text\n * @param {string} str\n * @param {number} maxErrors\n * @return {StringMatch[]}\n */\n\nfunction search(text, str, maxErrors) {\n // Do a fast search for exact matches. The `approx-string-match` library\n // doesn\'t currently incorporate this optimization itself.\n var matchPos = 0;\n var exactMatches = [];\n\n while (matchPos !== -1) {\n matchPos = text.indexOf(str, matchPos);\n\n if (matchPos !== -1) {\n exactMatches.push({\n start: matchPos,\n end: matchPos + str.length,\n errors: 0\n });\n matchPos += 1;\n }\n }\n\n if (exactMatches.length > 0) {\n return exactMatches;\n } // If there are no exact matches, do a more expensive search for matches\n // with errors.\n\n\n return (0,dist/* default */.Z)(text, str, maxErrors);\n}\n/**\n * Compute a score between 0 and 1.0 for the similarity between `text` and `str`.\n *\n * @param {string} text\n * @param {string} str\n */\n\n\nfunction textMatchScore(text, str) {\n /* istanbul ignore next - `scoreMatch` will never pass an empty string */\n if (str.length === 0 || text.length === 0) {\n return 0.0;\n }\n\n var matches = search(text, str, str.length); // prettier-ignore\n\n return 1 - matches[0].errors / str.length;\n}\n/**\n * Find the best approximate match for `quote` in `text`.\n *\n * Returns `null` if no match exceeding the minimum quality threshold was found.\n *\n * @param {string} text - Document text to search\n * @param {string} quote - String to find within `text`\n * @param {Object} context -\n * Context in which the quote originally appeared. This is used to choose the\n * best match.\n * @param {string} [context.prefix] - Expected text before the quote\n * @param {string} [context.suffix] - Expected text after the quote\n * @param {number} [context.hint] - Expected offset of match within text\n * @return {Match|null}\n */\n\n\nfunction matchQuote(text, quote) {\n var context = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};\n\n if (quote.length === 0) {\n return null;\n } // Choose the maximum number of errors to allow for the initial search.\n // This choice involves a tradeoff between:\n //\n // - Recall (proportion of "good" matches found)\n // - Precision (proportion of matches found which are "good")\n // - Cost of the initial search and of processing the candidate matches [1]\n //\n // [1] Specifically, the expected-time complexity of the initial search is\n // `O((maxErrors / 32) * text.length)`. See `approx-string-match` docs.\n\n\n var maxErrors = Math.min(256, quote.length / 2); // Find closest matches for `quote` in `text` based on edit distance.\n\n var matches = search(text, quote, maxErrors);\n\n if (matches.length === 0) {\n return null;\n }\n /**\n * Compute a score between 0 and 1.0 for a match candidate.\n *\n * @param {StringMatch} match\n */\n\n\n var scoreMatch = function scoreMatch(match) {\n var quoteWeight = 50; // Similarity of matched text to quote.\n\n var prefixWeight = 20; // Similarity of text before matched text to `context.prefix`.\n\n var suffixWeight = 20; // Similarity of text after matched text to `context.suffix`.\n\n var posWeight = 2; // Proximity to expected location. Used as a tie-breaker.\n\n var quoteScore = 1 - match.errors / quote.length;\n var prefixScore = context.prefix ? textMatchScore(text.slice(Math.max(0, match.start - context.prefix.length), match.start), context.prefix) : 1.0;\n var suffixScore = context.suffix ? textMatchScore(text.slice(match.end, match.end + context.suffix.length), context.suffix) : 1.0;\n var posScore = 1.0;\n\n if (typeof context.hint === \'number\') {\n var offset = Math.abs(match.start - context.hint);\n posScore = 1.0 - offset / text.length;\n }\n\n var rawScore = quoteWeight * quoteScore + prefixWeight * prefixScore + suffixWeight * suffixScore + posWeight * posScore;\n var maxScore = quoteWeight + prefixWeight + suffixWeight + posWeight;\n var normalizedScore = rawScore / maxScore;\n return normalizedScore;\n }; // Rank matches based on similarity of actual and expected surrounding text\n // and actual/expected offset in the document text.\n\n\n var scoredMatches = matches.map(function (m) {\n return {\n start: m.start,\n end: m.end,\n score: scoreMatch(m)\n };\n }); // Choose match with highest score.\n\n scoredMatches.sort(function (a, b) {\n return b.score - a.score;\n });\n return scoredMatches[0];\n}\n;// CONCATENATED MODULE: ./src/vendor/hypothesis/anchoring/text-range.js\nfunction _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }\n\nfunction _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }\n\nfunction _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }\n\nfunction _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\nfunction _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }\n\nfunction _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\n/**\n * Return the combined length of text nodes contained in `node`.\n *\n * @param {Node} node\n */\nfunction nodeTextLength(node) {\n switch (node.nodeType) {\n case Node.ELEMENT_NODE:\n case Node.TEXT_NODE:\n // nb. `textContent` excludes text in comments and processing instructions\n // when called on a parent element, so we don\'t need to subtract that here.\n return (\n /** @type {string} */\n node.textContent.length\n );\n\n default:\n return 0;\n }\n}\n/**\n * Return the total length of the text of all previous siblings of `node`.\n *\n * @param {Node} node\n */\n\n\nfunction previousSiblingsTextLength(node) {\n var sibling = node.previousSibling;\n var length = 0;\n\n while (sibling) {\n length += nodeTextLength(sibling);\n sibling = sibling.previousSibling;\n }\n\n return length;\n}\n/**\n * Resolve one or more character offsets within an element to (text node, position)\n * pairs.\n *\n * @param {Element} element\n * @param {number[]} offsets - Offsets, which must be sorted in ascending order\n * @return {{ node: Text, offset: number }[]}\n */\n\n\nfunction resolveOffsets(element) {\n for (var _len = arguments.length, offsets = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n offsets[_key - 1] = arguments[_key];\n }\n\n var nextOffset = offsets.shift();\n var nodeIter =\n /** @type {Document} */\n element.ownerDocument.createNodeIterator(element, NodeFilter.SHOW_TEXT);\n var results = [];\n var currentNode = nodeIter.nextNode();\n var textNode;\n var length = 0; // Find the text node containing the `nextOffset`th character from the start\n // of `element`.\n\n while (nextOffset !== undefined && currentNode) {\n textNode =\n /** @type {Text} */\n currentNode;\n\n if (length + textNode.data.length > nextOffset) {\n results.push({\n node: textNode,\n offset: nextOffset - length\n });\n nextOffset = offsets.shift();\n } else {\n currentNode = nodeIter.nextNode();\n length += textNode.data.length;\n }\n } // Boundary case.\n\n\n while (nextOffset !== undefined && textNode && length === nextOffset) {\n results.push({\n node: textNode,\n offset: textNode.data.length\n });\n nextOffset = offsets.shift();\n }\n\n if (nextOffset !== undefined) {\n throw new RangeError(\'Offset exceeds text length\');\n }\n\n return results;\n}\n\nvar RESOLVE_FORWARDS = 1;\nvar RESOLVE_BACKWARDS = 2;\n/**\n * Represents an offset within the text content of an element.\n *\n * This position can be resolved to a specific descendant node in the current\n * DOM subtree of the element using the `resolve` method.\n */\n\nvar text_range_TextPosition = /*#__PURE__*/function () {\n /**\n * Construct a `TextPosition` that refers to the text position `offset` within\n * the text content of `element`.\n *\n * @param {Element} element\n * @param {number} offset\n */\n function TextPosition(element, offset) {\n _classCallCheck(this, TextPosition);\n\n if (offset < 0) {\n throw new Error(\'Offset is invalid\');\n }\n /** Element that `offset` is relative to. */\n\n\n this.element = element;\n /** Character offset from the start of the element\'s `textContent`. */\n\n this.offset = offset;\n }\n /**\n * Return a copy of this position with offset relative to a given ancestor\n * element.\n *\n * @param {Element} parent - Ancestor of `this.element`\n * @return {TextPosition}\n */\n\n\n _createClass(TextPosition, [{\n key: "relativeTo",\n value: function relativeTo(parent) {\n if (!parent.contains(this.element)) {\n throw new Error(\'Parent is not an ancestor of current element\');\n }\n\n var el = this.element;\n var offset = this.offset;\n\n while (el !== parent) {\n offset += previousSiblingsTextLength(el);\n el =\n /** @type {Element} */\n el.parentElement;\n }\n\n return new TextPosition(el, offset);\n }\n /**\n * Resolve the position to a specific text node and offset within that node.\n *\n * Throws if `this.offset` exceeds the length of the element\'s text. In the\n * case where the element has no text and `this.offset` is 0, the `direction`\n * option determines what happens.\n *\n * Offsets at the boundary between two nodes are resolved to the start of the\n * node that begins at the boundary.\n *\n * @param {Object} [options]\n * @param {RESOLVE_FORWARDS|RESOLVE_BACKWARDS} [options.direction] -\n * Specifies in which direction to search for the nearest text node if\n * `this.offset` is `0` and `this.element` has no text. If not specified\n * an error is thrown.\n * @return {{ node: Text, offset: number }}\n * @throws {RangeError}\n */\n\n }, {\n key: "resolve",\n value: function resolve() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n\n try {\n return resolveOffsets(this.element, this.offset)[0];\n } catch (err) {\n if (this.offset === 0 && options.direction !== undefined) {\n var tw = document.createTreeWalker(this.element.getRootNode(), NodeFilter.SHOW_TEXT);\n tw.currentNode = this.element;\n var forwards = options.direction === RESOLVE_FORWARDS;\n var text =\n /** @type {Text|null} */\n forwards ? tw.nextNode() : tw.previousNode();\n\n if (!text) {\n throw err;\n }\n\n return {\n node: text,\n offset: forwards ? 0 : text.data.length\n };\n } else {\n throw err;\n }\n }\n }\n /**\n * Construct a `TextPosition` that refers to the `offset`th character within\n * `node`.\n *\n * @param {Node} node\n * @param {number} offset\n * @return {TextPosition}\n */\n\n }], [{\n key: "fromCharOffset",\n value: function fromCharOffset(node, offset) {\n switch (node.nodeType) {\n case Node.TEXT_NODE:\n return TextPosition.fromPoint(node, offset);\n\n case Node.ELEMENT_NODE:\n return new TextPosition(\n /** @type {Element} */\n node, offset);\n\n default:\n throw new Error(\'Node is not an element or text node\');\n }\n }\n /**\n * Construct a `TextPosition` representing the range start or end point (node, offset).\n *\n * @param {Node} node - Text or Element node\n * @param {number} offset - Offset within the node.\n * @return {TextPosition}\n */\n\n }, {\n key: "fromPoint",\n value: function fromPoint(node, offset) {\n switch (node.nodeType) {\n case Node.TEXT_NODE:\n {\n if (offset < 0 || offset >\n /** @type {Text} */\n node.data.length) {\n throw new Error(\'Text node offset is out of range\');\n }\n\n if (!node.parentElement) {\n throw new Error(\'Text node has no parent\');\n } // Get the offset from the start of the parent element.\n\n\n var textOffset = previousSiblingsTextLength(node) + offset;\n return new TextPosition(node.parentElement, textOffset);\n }\n\n case Node.ELEMENT_NODE:\n {\n if (offset < 0 || offset > node.childNodes.length) {\n throw new Error(\'Child node offset is out of range\');\n } // Get the text length before the `offset`th child of element.\n\n\n var _textOffset = 0;\n\n for (var i = 0; i < offset; i++) {\n _textOffset += nodeTextLength(node.childNodes[i]);\n }\n\n return new TextPosition(\n /** @type {Element} */\n node, _textOffset);\n }\n\n default:\n throw new Error(\'Point is not in an element or text node\');\n }\n }\n }]);\n\n return TextPosition;\n}();\n/**\n * Represents a region of a document as a (start, end) pair of `TextPosition` points.\n *\n * Representing a range in this way allows for changes in the DOM content of the\n * range which don\'t affect its text content, without affecting the text content\n * of the range itself.\n */\n\nvar text_range_TextRange = /*#__PURE__*/function () {\n /**\n * Construct an immutable `TextRange` from a `start` and `end` point.\n *\n * @param {TextPosition} start\n * @param {TextPosition} end\n */\n function TextRange(start, end) {\n _classCallCheck(this, TextRange);\n\n this.start = start;\n this.end = end;\n }\n /**\n * Return a copy of this range with start and end positions relative to a\n * given ancestor. See `TextPosition.relativeTo`.\n *\n * @param {Element} element\n */\n\n\n _createClass(TextRange, [{\n key: "relativeTo",\n value: function relativeTo(element) {\n return new TextRange(this.start.relativeTo(element), this.end.relativeTo(element));\n }\n /**\n * Resolve the `TextRange` to a DOM range.\n *\n * The resulting DOM Range will always start and end in a `Text` node.\n * Hence `TextRange.fromRange(range).toRange()` can be used to "shrink" a\n * range to the text it contains.\n *\n * May throw if the `start` or `end` positions cannot be resolved to a range.\n *\n * @return {Range}\n */\n\n }, {\n key: "toRange",\n value: function toRange() {\n var start;\n var end;\n\n if (this.start.element === this.end.element && this.start.offset <= this.end.offset) {\n // Fast path for start and end points in same element.\n var _resolveOffsets = resolveOffsets(this.start.element, this.start.offset, this.end.offset);\n\n var _resolveOffsets2 = _slicedToArray(_resolveOffsets, 2);\n\n start = _resolveOffsets2[0];\n end = _resolveOffsets2[1];\n } else {\n start = this.start.resolve({\n direction: RESOLVE_FORWARDS\n });\n end = this.end.resolve({\n direction: RESOLVE_BACKWARDS\n });\n }\n\n var range = new Range();\n range.setStart(start.node, start.offset);\n range.setEnd(end.node, end.offset);\n return range;\n }\n /**\n * Convert an existing DOM `Range` to a `TextRange`\n *\n * @param {Range} range\n * @return {TextRange}\n */\n\n }], [{\n key: "fromRange",\n value: function fromRange(range) {\n var start = text_range_TextPosition.fromPoint(range.startContainer, range.startOffset);\n var end = text_range_TextPosition.fromPoint(range.endContainer, range.endOffset);\n return new TextRange(start, end);\n }\n /**\n * Return a `TextRange` from the `start`th to `end`th characters in `root`.\n *\n * @param {Element} root\n * @param {number} start\n * @param {number} end\n */\n\n }, {\n key: "fromOffsets",\n value: function fromOffsets(root, start, end) {\n return new TextRange(new text_range_TextPosition(root, start), new text_range_TextPosition(root, end));\n }\n }]);\n\n return TextRange;\n}();\n;// CONCATENATED MODULE: ./src/vendor/hypothesis/anchoring/types.js\nfunction ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }\n\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nfunction types_classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }\n\nfunction types_defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction types_createClass(Constructor, protoProps, staticProps) { if (protoProps) types_defineProperties(Constructor.prototype, protoProps); if (staticProps) types_defineProperties(Constructor, staticProps); return Constructor; }\n\n/**\n * This module exports a set of classes for converting between DOM `Range`\n * objects and different types of selectors. It is mostly a thin wrapper around a\n * set of anchoring libraries. It serves two main purposes:\n *\n * 1. Providing a consistent interface across different types of anchors.\n * 2. Insulating the rest of the code from API changes in the underlying anchoring\n * libraries.\n */\n\n\n\n/**\n * @typedef {import(\'../../types/api\').RangeSelector} RangeSelector\n * @typedef {import(\'../../types/api\').TextPositionSelector} TextPositionSelector\n * @typedef {import(\'../../types/api\').TextQuoteSelector} TextQuoteSelector\n */\n\n/**\n * Converts between `RangeSelector` selectors and `Range` objects.\n */\n\nvar RangeAnchor = /*#__PURE__*/(/* unused pure expression or super */ null && (function () {\n /**\n * @param {Node} root - A root element from which to anchor.\n * @param {Range} range - A range describing the anchor.\n */\n function RangeAnchor(root, range) {\n types_classCallCheck(this, RangeAnchor);\n\n this.root = root;\n this.range = range;\n }\n /**\n * @param {Node} root - A root element from which to anchor.\n * @param {Range} range - A range describing the anchor.\n */\n\n\n types_createClass(RangeAnchor, [{\n key: "toRange",\n value: function toRange() {\n return this.range;\n }\n /**\n * @return {RangeSelector}\n */\n\n }, {\n key: "toSelector",\n value: function toSelector() {\n // "Shrink" the range so that it tightly wraps its text. This ensures more\n // predictable output for a given text selection.\n var normalizedRange = TextRange.fromRange(this.range).toRange();\n var textRange = TextRange.fromRange(normalizedRange);\n var startContainer = xpathFromNode(textRange.start.element, this.root);\n var endContainer = xpathFromNode(textRange.end.element, this.root);\n return {\n type: \'RangeSelector\',\n startContainer: startContainer,\n startOffset: textRange.start.offset,\n endContainer: endContainer,\n endOffset: textRange.end.offset\n };\n }\n }], [{\n key: "fromRange",\n value: function fromRange(root, range) {\n return new RangeAnchor(root, range);\n }\n /**\n * Create an anchor from a serialized `RangeSelector` selector.\n *\n * @param {Element} root - A root element from which to anchor.\n * @param {RangeSelector} selector\n */\n\n }, {\n key: "fromSelector",\n value: function fromSelector(root, selector) {\n var startContainer = nodeFromXPath(selector.startContainer, root);\n\n if (!startContainer) {\n throw new Error(\'Failed to resolve startContainer XPath\');\n }\n\n var endContainer = nodeFromXPath(selector.endContainer, root);\n\n if (!endContainer) {\n throw new Error(\'Failed to resolve endContainer XPath\');\n }\n\n var startPos = TextPosition.fromCharOffset(startContainer, selector.startOffset);\n var endPos = TextPosition.fromCharOffset(endContainer, selector.endOffset);\n var range = new TextRange(startPos, endPos).toRange();\n return new RangeAnchor(root, range);\n }\n }]);\n\n return RangeAnchor;\n}()));\n/**\n * Converts between `TextPositionSelector` selectors and `Range` objects.\n */\n\nvar TextPositionAnchor = /*#__PURE__*/function () {\n /**\n * @param {Element} root\n * @param {number} start\n * @param {number} end\n */\n function TextPositionAnchor(root, start, end) {\n types_classCallCheck(this, TextPositionAnchor);\n\n this.root = root;\n this.start = start;\n this.end = end;\n }\n /**\n * @param {Element} root\n * @param {Range} range\n */\n\n\n types_createClass(TextPositionAnchor, [{\n key: "toSelector",\n value:\n /**\n * @return {TextPositionSelector}\n */\n function toSelector() {\n return {\n type: \'TextPositionSelector\',\n start: this.start,\n end: this.end\n };\n }\n }, {\n key: "toRange",\n value: function toRange() {\n return text_range_TextRange.fromOffsets(this.root, this.start, this.end).toRange();\n }\n }], [{\n key: "fromRange",\n value: function fromRange(root, range) {\n var textRange = text_range_TextRange.fromRange(range).relativeTo(root);\n return new TextPositionAnchor(root, textRange.start.offset, textRange.end.offset);\n }\n /**\n * @param {Element} root\n * @param {TextPositionSelector} selector\n */\n\n }, {\n key: "fromSelector",\n value: function fromSelector(root, selector) {\n return new TextPositionAnchor(root, selector.start, selector.end);\n }\n }]);\n\n return TextPositionAnchor;\n}();\n/**\n * @typedef QuoteMatchOptions\n * @prop {number} [hint] - Expected position of match in text. See `matchQuote`.\n */\n\n/**\n * Converts between `TextQuoteSelector` selectors and `Range` objects.\n */\n\nvar TextQuoteAnchor = /*#__PURE__*/function () {\n /**\n * @param {Element} root - A root element from which to anchor.\n * @param {string} exact\n * @param {Object} context\n * @param {string} [context.prefix]\n * @param {string} [context.suffix]\n */\n function TextQuoteAnchor(root, exact) {\n var context = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};\n\n types_classCallCheck(this, TextQuoteAnchor);\n\n this.root = root;\n this.exact = exact;\n this.context = context;\n }\n /**\n * Create a `TextQuoteAnchor` from a range.\n *\n * Will throw if `range` does not contain any text nodes.\n *\n * @param {Element} root\n * @param {Range} range\n */\n\n\n types_createClass(TextQuoteAnchor, [{\n key: "toSelector",\n value:\n /**\n * @return {TextQuoteSelector}\n */\n function toSelector() {\n return {\n type: \'TextQuoteSelector\',\n exact: this.exact,\n prefix: this.context.prefix,\n suffix: this.context.suffix\n };\n }\n /**\n * @param {QuoteMatchOptions} [options]\n */\n\n }, {\n key: "toRange",\n value: function toRange() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n return this.toPositionAnchor(options).toRange();\n }\n /**\n * @param {QuoteMatchOptions} [options]\n */\n\n }, {\n key: "toPositionAnchor",\n value: function toPositionAnchor() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n var text =\n /** @type {string} */\n this.root.textContent;\n var match = matchQuote(text, this.exact, _objectSpread(_objectSpread({}, this.context), {}, {\n hint: options.hint\n }));\n\n if (!match) {\n throw new Error(\'Quote not found\');\n }\n\n return new TextPositionAnchor(this.root, match.start, match.end);\n }\n }], [{\n key: "fromRange",\n value: function fromRange(root, range) {\n var text =\n /** @type {string} */\n root.textContent;\n var textRange = text_range_TextRange.fromRange(range).relativeTo(root);\n var start = textRange.start.offset;\n var end = textRange.end.offset; // Number of characters around the quote to capture as context. We currently\n // always use a fixed amount, but it would be better if this code was aware\n // of logical boundaries in the document (paragraph, article etc.) to avoid\n // capturing text unrelated to the quote.\n //\n // In regular prose the ideal content would often be the surrounding sentence.\n // This is a natural unit of meaning which enables displaying quotes in\n // context even when the document is not available. We could use `Intl.Segmenter`\n // for this when available.\n\n var contextLen = 32;\n return new TextQuoteAnchor(root, text.slice(start, end), {\n prefix: text.slice(Math.max(0, start - contextLen), start),\n suffix: text.slice(end, Math.min(text.length, end + contextLen))\n });\n }\n /**\n * @param {Element} root\n * @param {TextQuoteSelector} selector\n */\n\n }, {\n key: "fromSelector",\n value: function fromSelector(root, selector) {\n var prefix = selector.prefix,\n suffix = selector.suffix;\n return new TextQuoteAnchor(root, selector.exact, {\n prefix: prefix,\n suffix: suffix\n });\n }\n }]);\n\n return TextQuoteAnchor;\n}();\n;// CONCATENATED MODULE: ./src/utils.js\nfunction _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = utils_unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }\n\nfunction utils_unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return utils_arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return utils_arrayLikeToArray(o, minLen); }\n\nfunction utils_arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\n//\n// Copyright 2021 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n // Catch JS errors to log them in the app.\n\nwindow.addEventListener("error", function (event) {\n Android.logError(event.message, event.filename, event.lineno);\n}, false);\nwindow.addEventListener("load", function () {\n var observer = new ResizeObserver(function () {\n onViewportWidthChanged();\n snapCurrentOffset();\n });\n observer.observe(document.body);\n}, false);\n/**\n * Having an odd number of columns when displaying two columns per screen causes snapping and page\n * turning issues. To fix this, we insert a blank virtual column at the end of the resource.\n */\n\nfunction appendVirtualColumnIfNeeded() {\n var id = "readium-virtual-page";\n var virtualCol = document.getElementById(id);\n\n if (isScrollModeEnabled() || getColumnCountPerScreen() != 2) {\n if (virtualCol) {\n virtualCol.remove();\n }\n } else {\n var documentWidth = document.scrollingElement.scrollWidth;\n var colCount = documentWidth / pageWidth;\n var hasOddColCount = Math.round(colCount * 2) / 2 % 1 > 0.1;\n\n if (hasOddColCount) {\n if (virtualCol) {\n virtualCol.remove();\n } else {\n virtualCol = document.createElement("div");\n virtualCol.setAttribute("id", id);\n virtualCol.style.breakBefore = "column";\n virtualCol.innerHTML = "​"; // zero-width space\n\n document.body.appendChild(virtualCol);\n }\n }\n }\n}\n\nvar pageWidth = 1;\n\nfunction onViewportWidthChanged() {\n // We can\'t rely on window.innerWidth for the pageWidth on Android, because if the\n // device pixel ratio is not an integer, we get rounding issues offsetting the pages.\n //\n // See https://github.com/readium/readium-css/issues/97\n // and https://github.com/readium/r2-navigator-kotlin/issues/146\n var width = Android.getViewportWidth();\n pageWidth = width / window.devicePixelRatio;\n setProperty("--RS__viewportWidth", "calc(" + width + "px / " + window.devicePixelRatio + ")");\n appendVirtualColumnIfNeeded();\n}\n\nfunction getColumnCountPerScreen() {\n return parseInt(window.getComputedStyle(document.documentElement).getPropertyValue("column-count"));\n}\nfunction isScrollModeEnabled() {\n var style = document.documentElement.style;\n return style.getPropertyValue("--USER__view").trim() == "readium-scroll-on" || // FIXME: Will need to be removed in Readium 3.0, --USER__scroll was incorrect.\n style.getPropertyValue("--USER__scroll").trim() == "readium-scroll-on";\n}\nfunction isRTL() {\n return document.body.dir.toLowerCase() == "rtl";\n} // Scroll to the given TagId in document and snap.\n\nfunction scrollToId(id) {\n var element = document.getElementById(id);\n\n if (!element) {\n return false;\n }\n\n return scrollToRect(element.getBoundingClientRect());\n} // Position must be in the range [0 - 1], 0-100%.\n\nfunction scrollToPosition(position) {\n // Android.log("scrollToPosition " + position);\n if (position < 0 || position > 1) {\n throw "scrollToPosition() must be given a position from 0.0 to 1.0";\n }\n\n var offset;\n\n if (isScrollModeEnabled()) {\n offset = document.scrollingElement.scrollHeight * position;\n document.scrollingElement.scrollTop = offset; // window.scrollTo(0, offset);\n } else {\n var documentWidth = document.scrollingElement.scrollWidth;\n var factor = isRTL() ? -1 : 1;\n offset = documentWidth * position * factor;\n document.scrollingElement.scrollLeft = snapOffset(offset);\n }\n} // Scrolls to the first occurrence of the given text snippet.\n//\n// The expected text argument is a Locator Text object, as defined here:\n// https://readium.org/architecture/models/locators/\n\nfunction scrollToText(text) {\n var range = rangeFromLocator({\n text: text\n });\n\n if (!range) {\n return false;\n }\n\n scrollToRange(range);\n return true;\n}\n\nfunction scrollToRange(range) {\n return scrollToRect(range.getBoundingClientRect());\n}\n\nfunction scrollToRect(rect) {\n if (isScrollModeEnabled()) {\n document.scrollingElement.scrollTop = rect.top + window.scrollY;\n } else {\n document.scrollingElement.scrollLeft = snapOffset(rect.left + window.scrollX);\n }\n\n return true;\n}\n\nfunction scrollToStart() {\n // Android.log("scrollToStart");\n if (!isScrollModeEnabled()) {\n document.scrollingElement.scrollLeft = 0;\n } else {\n document.scrollingElement.scrollTop = 0;\n window.scrollTo(0, 0);\n }\n}\nfunction scrollToEnd() {\n // Android.log("scrollToEnd");\n if (!isScrollModeEnabled()) {\n var factor = isRTL() ? -1 : 1;\n document.scrollingElement.scrollLeft = snapOffset(document.scrollingElement.scrollWidth * factor);\n } else {\n document.scrollingElement.scrollTop = document.body.scrollHeight;\n window.scrollTo(0, document.body.scrollHeight);\n }\n} // Returns false if the page is already at the left-most scroll offset.\n\nfunction scrollLeft() {\n var documentWidth = document.scrollingElement.scrollWidth;\n var offset = window.scrollX - pageWidth;\n var minOffset = isRTL() ? -(documentWidth - pageWidth) : 0;\n return scrollToOffset(Math.max(offset, minOffset));\n} // Returns false if the page is already at the right-most scroll offset.\n\nfunction scrollRight() {\n var documentWidth = document.scrollingElement.scrollWidth;\n var offset = window.scrollX + pageWidth;\n var maxOffset = isRTL() ? 0 : documentWidth - pageWidth;\n return scrollToOffset(Math.min(offset, maxOffset));\n} // Scrolls to the given left offset.\n// Returns false if the page scroll position is already close enough to the given offset.\n\nfunction scrollToOffset(offset) {\n // Android.log("scrollToOffset " + offset);\n if (isScrollModeEnabled()) {\n throw "Called scrollToOffset() with scroll mode enabled. This can only be used in paginated mode.";\n }\n\n var currentOffset = window.scrollX;\n document.scrollingElement.scrollLeft = snapOffset(offset); // In some case the scrollX cannot reach the position respecting to innerWidth\n\n var diff = Math.abs(currentOffset - offset) / pageWidth;\n return diff > 0.01;\n} // Snap the offset to the screen width (page width).\n\n\nfunction snapOffset(offset) {\n var value = offset + (isRTL() ? -1 : 1);\n return value - value % pageWidth;\n} // Snaps the current offset to the page width.\n\n\nfunction snapCurrentOffset() {\n // Android.log("snapCurrentOffset");\n if (isScrollModeEnabled()) {\n return;\n }\n\n var currentOffset = window.scrollX; // Adds half a page to make sure we don\'t snap to the previous page.\n\n var factor = isRTL() ? -1 : 1;\n var delta = factor * (pageWidth / 2);\n document.scrollingElement.scrollLeft = snapOffset(currentOffset + delta);\n}\nfunction rangeFromLocator(locator) {\n try {\n var locations = locator.locations;\n var text = locator.text;\n\n if (text && text.highlight) {\n var root;\n\n if (locations && locations.cssSelector) {\n root = document.querySelector(locations.cssSelector);\n }\n\n if (!root) {\n root = document.body;\n }\n\n var anchor = new TextQuoteAnchor(root, text.highlight, {\n prefix: text.before,\n suffix: text.after\n });\n return anchor.toRange();\n }\n\n if (locations) {\n var element = null;\n\n if (!element && locations.cssSelector) {\n element = document.querySelector(locations.cssSelector);\n }\n\n if (!element && locations.fragments) {\n var _iterator = _createForOfIteratorHelper(locations.fragments),\n _step;\n\n try {\n for (_iterator.s(); !(_step = _iterator.n()).done;) {\n var htmlId = _step.value;\n element = document.getElementById(htmlId);\n\n if (element) {\n break;\n }\n }\n } catch (err) {\n _iterator.e(err);\n } finally {\n _iterator.f();\n }\n }\n\n if (element) {\n var range = document.createRange();\n range.setStartBefore(element);\n range.setEndAfter(element);\n return range;\n }\n }\n } catch (e) {\n logError(e);\n }\n\n return null;\n} /// User Settings.\n\nfunction setCSSProperties(properties) {\n for (var name in properties) {\n setProperty(name, properties[name]);\n }\n} // For setting user setting.\n\nfunction setProperty(key, value) {\n if (value === null || value === "") {\n removeProperty(key);\n } else {\n var root = document.documentElement; // The `!important` annotation is added with `setProperty()` because if it\'s part of the\n // `value`, it will be ignored by the Web View.\n\n root.style.setProperty(key, value, "important");\n }\n} // For removing user setting.\n\nfunction removeProperty(key) {\n var root = document.documentElement;\n root.style.removeProperty(key);\n} /// Toolkit\n\nfunction log() {\n var message = Array.prototype.slice.call(arguments).join(" ");\n Android.log(message);\n}\nfunction logError(message) {\n Android.logError(message, "", 0);\n}\n;// CONCATENATED MODULE: ./src/rect.js\nfunction _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }\n\nfunction rect_createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = rect_unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }\n\nfunction rect_unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return rect_arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return rect_arrayLikeToArray(o, minLen); }\n\nfunction rect_arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\n//\n// Copyright 2021 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n\nvar debug = false;\n/**\n * Converts a DOMRect into a JSON object understandable by the native side.\n */\n\nfunction toNativeRect(rect) {\n var pixelRatio = window.devicePixelRatio;\n var width = rect.width * pixelRatio;\n var height = rect.height * pixelRatio;\n var left = rect.left * pixelRatio;\n var top = rect.top * pixelRatio;\n var right = left + width;\n var bottom = top + height;\n return {\n width: width,\n height: height,\n left: left,\n top: top,\n right: right,\n bottom: bottom\n };\n}\nfunction getClientRectsNoOverlap(range, doNotMergeHorizontallyAlignedRects) {\n var clientRects = range.getClientRects();\n var tolerance = 1;\n var originalRects = [];\n\n var _iterator = rect_createForOfIteratorHelper(clientRects),\n _step;\n\n try {\n for (_iterator.s(); !(_step = _iterator.n()).done;) {\n var rangeClientRect = _step.value;\n originalRects.push({\n bottom: rangeClientRect.bottom,\n height: rangeClientRect.height,\n left: rangeClientRect.left,\n right: rangeClientRect.right,\n top: rangeClientRect.top,\n width: rangeClientRect.width\n });\n }\n } catch (err) {\n _iterator.e(err);\n } finally {\n _iterator.f();\n }\n\n var mergedRects = mergeTouchingRects(originalRects, tolerance, doNotMergeHorizontallyAlignedRects);\n var noContainedRects = removeContainedRects(mergedRects, tolerance);\n var newRects = replaceOverlapingRects(noContainedRects);\n var minArea = 2 * 2;\n\n for (var j = newRects.length - 1; j >= 0; j--) {\n var rect = newRects[j];\n var bigEnough = rect.width * rect.height > minArea;\n\n if (!bigEnough) {\n if (newRects.length > 1) {\n rect_log("CLIENT RECT: remove small");\n newRects.splice(j, 1);\n } else {\n rect_log("CLIENT RECT: remove small, but keep otherwise empty!");\n break;\n }\n }\n }\n\n rect_log("CLIENT RECT: reduced ".concat(originalRects.length, " --\x3e ").concat(newRects.length));\n return newRects;\n}\n\nfunction mergeTouchingRects(rects, tolerance, doNotMergeHorizontallyAlignedRects) {\n for (var i = 0; i < rects.length; i++) {\n var _loop = function _loop(j) {\n var rect1 = rects[i];\n var rect2 = rects[j];\n\n if (rect1 === rect2) {\n rect_log("mergeTouchingRects rect1 === rect2 ??!");\n return "continue";\n }\n\n var rectsLineUpVertically = almostEqual(rect1.top, rect2.top, tolerance) && almostEqual(rect1.bottom, rect2.bottom, tolerance);\n var rectsLineUpHorizontally = almostEqual(rect1.left, rect2.left, tolerance) && almostEqual(rect1.right, rect2.right, tolerance);\n var horizontalAllowed = !doNotMergeHorizontallyAlignedRects;\n var aligned = rectsLineUpHorizontally && horizontalAllowed || rectsLineUpVertically && !rectsLineUpHorizontally;\n var canMerge = aligned && rectsTouchOrOverlap(rect1, rect2, tolerance);\n\n if (canMerge) {\n rect_log("CLIENT RECT: merging two into one, VERTICAL: ".concat(rectsLineUpVertically, " HORIZONTAL: ").concat(rectsLineUpHorizontally, " (").concat(doNotMergeHorizontallyAlignedRects, ")"));\n var newRects = rects.filter(function (rect) {\n return rect !== rect1 && rect !== rect2;\n });\n var replacementClientRect = getBoundingRect(rect1, rect2);\n newRects.push(replacementClientRect);\n return {\n v: mergeTouchingRects(newRects, tolerance, doNotMergeHorizontallyAlignedRects)\n };\n }\n };\n\n for (var j = i + 1; j < rects.length; j++) {\n var _ret = _loop(j);\n\n if (_ret === "continue") continue;\n if (_typeof(_ret) === "object") return _ret.v;\n }\n }\n\n return rects;\n}\n\nfunction getBoundingRect(rect1, rect2) {\n var left = Math.min(rect1.left, rect2.left);\n var right = Math.max(rect1.right, rect2.right);\n var top = Math.min(rect1.top, rect2.top);\n var bottom = Math.max(rect1.bottom, rect2.bottom);\n return {\n bottom: bottom,\n height: bottom - top,\n left: left,\n right: right,\n top: top,\n width: right - left\n };\n}\n\nfunction removeContainedRects(rects, tolerance) {\n var rectsToKeep = new Set(rects);\n\n var _iterator2 = rect_createForOfIteratorHelper(rects),\n _step2;\n\n try {\n for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {\n var rect = _step2.value;\n var bigEnough = rect.width > 1 && rect.height > 1;\n\n if (!bigEnough) {\n rect_log("CLIENT RECT: remove tiny");\n rectsToKeep.delete(rect);\n continue;\n }\n\n var _iterator3 = rect_createForOfIteratorHelper(rects),\n _step3;\n\n try {\n for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {\n var possiblyContainingRect = _step3.value;\n\n if (rect === possiblyContainingRect) {\n continue;\n }\n\n if (!rectsToKeep.has(possiblyContainingRect)) {\n continue;\n }\n\n if (rectContains(possiblyContainingRect, rect, tolerance)) {\n rect_log("CLIENT RECT: remove contained");\n rectsToKeep.delete(rect);\n break;\n }\n }\n } catch (err) {\n _iterator3.e(err);\n } finally {\n _iterator3.f();\n }\n }\n } catch (err) {\n _iterator2.e(err);\n } finally {\n _iterator2.f();\n }\n\n return Array.from(rectsToKeep);\n}\n\nfunction rectContains(rect1, rect2, tolerance) {\n return rectContainsPoint(rect1, rect2.left, rect2.top, tolerance) && rectContainsPoint(rect1, rect2.right, rect2.top, tolerance) && rectContainsPoint(rect1, rect2.left, rect2.bottom, tolerance) && rectContainsPoint(rect1, rect2.right, rect2.bottom, tolerance);\n}\n\nfunction rectContainsPoint(rect, x, y, tolerance) {\n return (rect.left < x || almostEqual(rect.left, x, tolerance)) && (rect.right > x || almostEqual(rect.right, x, tolerance)) && (rect.top < y || almostEqual(rect.top, y, tolerance)) && (rect.bottom > y || almostEqual(rect.bottom, y, tolerance));\n}\n\nfunction replaceOverlapingRects(rects) {\n for (var i = 0; i < rects.length; i++) {\n for (var j = i + 1; j < rects.length; j++) {\n var rect1 = rects[i];\n var rect2 = rects[j];\n\n if (rect1 === rect2) {\n rect_log("replaceOverlapingRects rect1 === rect2 ??!");\n continue;\n }\n\n if (rectsTouchOrOverlap(rect1, rect2, -1)) {\n var _ret2 = function () {\n var toAdd = [];\n var toRemove = void 0;\n var subtractRects1 = rectSubtract(rect1, rect2);\n\n if (subtractRects1.length === 1) {\n toAdd = subtractRects1;\n toRemove = rect1;\n } else {\n var subtractRects2 = rectSubtract(rect2, rect1);\n\n if (subtractRects1.length < subtractRects2.length) {\n toAdd = subtractRects1;\n toRemove = rect1;\n } else {\n toAdd = subtractRects2;\n toRemove = rect2;\n }\n }\n\n rect_log("CLIENT RECT: overlap, cut one rect into ".concat(toAdd.length));\n var newRects = rects.filter(function (rect) {\n return rect !== toRemove;\n });\n Array.prototype.push.apply(newRects, toAdd);\n return {\n v: replaceOverlapingRects(newRects)\n };\n }();\n\n if (_typeof(_ret2) === "object") return _ret2.v;\n }\n }\n }\n\n return rects;\n}\n\nfunction rectSubtract(rect1, rect2) {\n var rectIntersected = rectIntersect(rect2, rect1);\n\n if (rectIntersected.height === 0 || rectIntersected.width === 0) {\n return [rect1];\n }\n\n var rects = [];\n {\n var rectA = {\n bottom: rect1.bottom,\n height: 0,\n left: rect1.left,\n right: rectIntersected.left,\n top: rect1.top,\n width: 0\n };\n rectA.width = rectA.right - rectA.left;\n rectA.height = rectA.bottom - rectA.top;\n\n if (rectA.height !== 0 && rectA.width !== 0) {\n rects.push(rectA);\n }\n }\n {\n var rectB = {\n bottom: rectIntersected.top,\n height: 0,\n left: rectIntersected.left,\n right: rectIntersected.right,\n top: rect1.top,\n width: 0\n };\n rectB.width = rectB.right - rectB.left;\n rectB.height = rectB.bottom - rectB.top;\n\n if (rectB.height !== 0 && rectB.width !== 0) {\n rects.push(rectB);\n }\n }\n {\n var rectC = {\n bottom: rect1.bottom,\n height: 0,\n left: rectIntersected.left,\n right: rectIntersected.right,\n top: rectIntersected.bottom,\n width: 0\n };\n rectC.width = rectC.right - rectC.left;\n rectC.height = rectC.bottom - rectC.top;\n\n if (rectC.height !== 0 && rectC.width !== 0) {\n rects.push(rectC);\n }\n }\n {\n var rectD = {\n bottom: rect1.bottom,\n height: 0,\n left: rectIntersected.right,\n right: rect1.right,\n top: rect1.top,\n width: 0\n };\n rectD.width = rectD.right - rectD.left;\n rectD.height = rectD.bottom - rectD.top;\n\n if (rectD.height !== 0 && rectD.width !== 0) {\n rects.push(rectD);\n }\n }\n return rects;\n}\n\nfunction rectIntersect(rect1, rect2) {\n var maxLeft = Math.max(rect1.left, rect2.left);\n var minRight = Math.min(rect1.right, rect2.right);\n var maxTop = Math.max(rect1.top, rect2.top);\n var minBottom = Math.min(rect1.bottom, rect2.bottom);\n return {\n bottom: minBottom,\n height: Math.max(0, minBottom - maxTop),\n left: maxLeft,\n right: minRight,\n top: maxTop,\n width: Math.max(0, minRight - maxLeft)\n };\n}\n\nfunction rectsTouchOrOverlap(rect1, rect2, tolerance) {\n return (rect1.left < rect2.right || tolerance >= 0 && almostEqual(rect1.left, rect2.right, tolerance)) && (rect2.left < rect1.right || tolerance >= 0 && almostEqual(rect2.left, rect1.right, tolerance)) && (rect1.top < rect2.bottom || tolerance >= 0 && almostEqual(rect1.top, rect2.bottom, tolerance)) && (rect2.top < rect1.bottom || tolerance >= 0 && almostEqual(rect2.top, rect1.bottom, tolerance));\n}\n\nfunction almostEqual(a, b, tolerance) {\n return Math.abs(a - b) <= tolerance;\n}\n\nfunction rect_log() {\n if (debug) {\n log.apply(null, arguments);\n }\n}\n;// CONCATENATED MODULE: ./src/decorator.js\nfunction decorator_createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = decorator_unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }\n\nfunction decorator_slicedToArray(arr, i) { return decorator_arrayWithHoles(arr) || decorator_iterableToArrayLimit(arr, i) || decorator_unsupportedIterableToArray(arr, i) || decorator_nonIterableRest(); }\n\nfunction decorator_nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }\n\nfunction decorator_unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return decorator_arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return decorator_arrayLikeToArray(o, minLen); }\n\nfunction decorator_arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\nfunction decorator_iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }\n\nfunction decorator_arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }\n\n//\n// Copyright 2021 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n\n\nvar styles = new Map();\nvar groups = new Map();\nvar lastGroupId = 0;\n/**\n * Registers a list of additional supported Decoration Templates.\n *\n * Each template object is indexed by the style ID.\n */\n\nfunction registerTemplates(newStyles) {\n var stylesheet = "";\n\n for (var _i = 0, _Object$entries = Object.entries(newStyles); _i < _Object$entries.length; _i++) {\n var _Object$entries$_i = decorator_slicedToArray(_Object$entries[_i], 2),\n id = _Object$entries$_i[0],\n style = _Object$entries$_i[1];\n\n styles.set(id, style);\n\n if (style.stylesheet) {\n stylesheet += style.stylesheet + "\\n";\n }\n }\n\n if (stylesheet) {\n var styleElement = document.createElement("style");\n styleElement.innerHTML = stylesheet;\n document.getElementsByTagName("head")[0].appendChild(styleElement);\n }\n}\n/**\n * Returns an instance of DecorationGroup for the given group name.\n */\n\nfunction getDecorations(groupName) {\n var group = groups.get(groupName);\n\n if (!group) {\n var id = "r2-decoration-" + lastGroupId++;\n group = DecorationGroup(id, groupName);\n groups.set(groupName, group);\n }\n\n return group;\n}\n/**\n * Handles click events on a Decoration.\n * Returns whether a decoration matched this event.\n */\n\nfunction handleDecorationClickEvent(event, clickEvent) {\n if (groups.size === 0) {\n return false;\n }\n\n function findTarget() {\n var _iterator = decorator_createForOfIteratorHelper(groups),\n _step;\n\n try {\n for (_iterator.s(); !(_step = _iterator.n()).done;) {\n var _step$value = decorator_slicedToArray(_step.value, 2),\n group = _step$value[0],\n groupContent = _step$value[1];\n\n var _iterator2 = decorator_createForOfIteratorHelper(groupContent.items.reverse()),\n _step2;\n\n try {\n for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {\n var item = _step2.value;\n\n if (!item.clickableElements) {\n continue;\n }\n\n var _iterator3 = decorator_createForOfIteratorHelper(item.clickableElements),\n _step3;\n\n try {\n for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {\n var element = _step3.value;\n var rect = element.getBoundingClientRect().toJSON();\n\n if (rectContainsPoint(rect, event.clientX, event.clientY, 1)) {\n return {\n group: group,\n item: item,\n element: element,\n rect: rect\n };\n }\n }\n } catch (err) {\n _iterator3.e(err);\n } finally {\n _iterator3.f();\n }\n }\n } catch (err) {\n _iterator2.e(err);\n } finally {\n _iterator2.f();\n }\n }\n } catch (err) {\n _iterator.e(err);\n } finally {\n _iterator.f();\n }\n }\n\n var target = findTarget();\n\n if (!target) {\n return false;\n }\n\n return Android.onDecorationActivated(JSON.stringify({\n id: target.item.decoration.id,\n group: target.group,\n rect: toNativeRect(target.item.range.getBoundingClientRect()),\n click: clickEvent\n }));\n}\n/**\n * Creates a DecorationGroup object from a unique HTML ID and its name.\n */\n\nfunction DecorationGroup(groupId, groupName) {\n var items = [];\n var lastItemId = 0;\n var container = null;\n /**\n * Adds a new decoration to the group.\n */\n\n function add(decoration) {\n var id = groupId + "-" + lastItemId++;\n var range = rangeFromLocator(decoration.locator);\n\n if (!range) {\n log("Can\'t locate DOM range for decoration", decoration);\n return;\n }\n\n var item = {\n id: id,\n decoration: decoration,\n range: range\n };\n items.push(item);\n layout(item);\n }\n /**\n * Removes the decoration with given ID from the group.\n */\n\n\n function remove(decorationId) {\n var index = items.findIndex(function (i) {\n return i.decoration.id === decorationId;\n });\n\n if (index === -1) {\n return;\n }\n\n var item = items[index];\n items.splice(index, 1);\n item.clickableElements = null;\n\n if (item.container) {\n item.container.remove();\n item.container = null;\n }\n }\n /**\n * Notifies that the given decoration was modified and needs to be updated.\n */\n\n\n function update(decoration) {\n remove(decoration.id);\n add(decoration);\n }\n /**\n * Removes all decorations from this group.\n */\n\n\n function clear() {\n clearContainer();\n items.length = 0;\n }\n /**\n * Recreates the decoration elements.\n *\n * To be called after reflowing the resource, for example.\n */\n\n\n function requestLayout() {\n clearContainer();\n items.forEach(function (item) {\n return layout(item);\n });\n }\n /**\n * Layouts a single Decoration item.\n */\n\n\n function layout(item) {\n var groupContainer = requireContainer();\n var style = styles.get(item.decoration.style);\n\n if (!style) {\n logError("Unknown decoration style: ".concat(item.decoration.style));\n return;\n }\n\n var itemContainer = document.createElement("div");\n itemContainer.setAttribute("id", item.id);\n itemContainer.setAttribute("data-style", item.decoration.style);\n itemContainer.style.setProperty("pointer-events", "none");\n var viewportWidth = window.innerWidth;\n var columnCount = parseInt(getComputedStyle(document.documentElement).getPropertyValue("column-count"));\n var pageWidth = viewportWidth / (columnCount || 1);\n var scrollingElement = document.scrollingElement;\n var xOffset = scrollingElement.scrollLeft;\n var yOffset = scrollingElement.scrollTop;\n\n function positionElement(element, rect, boundingRect) {\n element.style.position = "absolute";\n\n if (style.width === "wrap") {\n element.style.width = "".concat(rect.width, "px");\n element.style.height = "".concat(rect.height, "px");\n element.style.left = "".concat(rect.left + xOffset, "px");\n element.style.top = "".concat(rect.top + yOffset, "px");\n } else if (style.width === "viewport") {\n element.style.width = "".concat(viewportWidth, "px");\n element.style.height = "".concat(rect.height, "px");\n var left = Math.floor(rect.left / viewportWidth) * viewportWidth;\n element.style.left = "".concat(left + xOffset, "px");\n element.style.top = "".concat(rect.top + yOffset, "px");\n } else if (style.width === "bounds") {\n element.style.width = "".concat(boundingRect.width, "px");\n element.style.height = "".concat(rect.height, "px");\n element.style.left = "".concat(boundingRect.left + xOffset, "px");\n element.style.top = "".concat(rect.top + yOffset, "px");\n } else if (style.width === "page") {\n element.style.width = "".concat(pageWidth, "px");\n element.style.height = "".concat(rect.height, "px");\n\n var _left = Math.floor(rect.left / pageWidth) * pageWidth;\n\n element.style.left = "".concat(_left + xOffset, "px");\n element.style.top = "".concat(rect.top + yOffset, "px");\n }\n }\n\n var boundingRect = item.range.getBoundingClientRect();\n var elementTemplate;\n\n try {\n var template = document.createElement("template");\n template.innerHTML = item.decoration.element.trim();\n elementTemplate = template.content.firstElementChild;\n } catch (error) {\n logError("Invalid decoration element \\"".concat(item.decoration.element, "\\": ").concat(error.message));\n return;\n }\n\n if (style.layout === "boxes") {\n var doNotMergeHorizontallyAlignedRects = true;\n var clientRects = getClientRectsNoOverlap(item.range, doNotMergeHorizontallyAlignedRects);\n clientRects = clientRects.sort(function (r1, r2) {\n if (r1.top < r2.top) {\n return -1;\n } else if (r1.top > r2.top) {\n return 1;\n } else {\n return 0;\n }\n });\n\n var _iterator4 = decorator_createForOfIteratorHelper(clientRects),\n _step4;\n\n try {\n for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {\n var clientRect = _step4.value;\n var line = elementTemplate.cloneNode(true);\n line.style.setProperty("pointer-events", "none");\n positionElement(line, clientRect, boundingRect);\n itemContainer.append(line);\n }\n } catch (err) {\n _iterator4.e(err);\n } finally {\n _iterator4.f();\n }\n } else if (style.layout === "bounds") {\n var bounds = elementTemplate.cloneNode(true);\n bounds.style.setProperty("pointer-events", "none");\n positionElement(bounds, boundingRect, boundingRect);\n itemContainer.append(bounds);\n }\n\n groupContainer.append(itemContainer);\n item.container = itemContainer;\n item.clickableElements = Array.from(itemContainer.querySelectorAll("[data-activable=\'1\']"));\n\n if (item.clickableElements.length === 0) {\n item.clickableElements = Array.from(itemContainer.children);\n }\n }\n /**\n * Returns the group container element, after making sure it exists.\n */\n\n\n function requireContainer() {\n if (!container) {\n container = document.createElement("div");\n container.setAttribute("id", groupId);\n container.setAttribute("data-group", groupName);\n container.style.setProperty("pointer-events", "none");\n document.body.append(container);\n }\n\n return container;\n }\n /**\n * Removes the group container.\n */\n\n\n function clearContainer() {\n if (container) {\n container.remove();\n container = null;\n }\n }\n\n return {\n add: add,\n remove: remove,\n update: update,\n clear: clear,\n items: items,\n requestLayout: requestLayout\n };\n}\nwindow.addEventListener("load", function () {\n // Will relayout all the decorations when the document body is resized.\n var body = document.body;\n var lastSize = {\n width: 0,\n height: 0\n };\n var observer = new ResizeObserver(function () {\n if (lastSize.width === body.clientWidth && lastSize.height === body.clientHeight) {\n return;\n }\n\n lastSize = {\n width: body.clientWidth,\n height: body.clientHeight\n };\n groups.forEach(function (group) {\n group.requestLayout();\n });\n });\n observer.observe(body);\n}, false);\n;// CONCATENATED MODULE: ./src/gestures.js\n/*\n * Copyright 2021 Readium Foundation. All rights reserved.\n * Use of this source code is governed by the BSD-style license\n * available in the top-level LICENSE file of the project.\n */\n\nwindow.addEventListener("DOMContentLoaded", function () {\n document.addEventListener("click", onClick, false);\n bindDragGesture(document);\n});\n\nfunction onClick(event) {\n if (!window.getSelection().isCollapsed) {\n // There\'s an on-going selection, the tap will dismiss it so we don\'t forward it.\n return;\n }\n\n var pixelRatio = window.devicePixelRatio;\n var clickEvent = {\n defaultPrevented: event.defaultPrevented,\n x: event.clientX * pixelRatio,\n y: event.clientY * pixelRatio,\n targetElement: event.target.outerHTML,\n interactiveElement: nearestInteractiveElement(event.target)\n };\n\n if (handleDecorationClickEvent(event, clickEvent)) {\n return;\n } // Send the tap data over the JS bridge even if it\'s been handled within the web view, so that\n // it can be preserved and used by the toolkit if needed.\n\n\n var shouldPreventDefault = Android.onTap(JSON.stringify(clickEvent));\n\n if (shouldPreventDefault) {\n event.stopPropagation();\n event.preventDefault();\n }\n}\n\nfunction bindDragGesture(element) {\n // passive: false is necessary to be able to prevent the default behavior.\n element.addEventListener("touchstart", onStart, {\n passive: false\n });\n element.addEventListener("touchend", onEnd, {\n passive: false\n });\n element.addEventListener("touchmove", onMove, {\n passive: false\n });\n var state = undefined;\n var isStartingDrag = false;\n var pixelRatio = window.devicePixelRatio;\n\n function onStart(event) {\n isStartingDrag = true;\n var startX = event.touches[0].clientX * pixelRatio;\n var startY = event.touches[0].clientY * pixelRatio;\n state = {\n defaultPrevented: event.defaultPrevented,\n startX: startX,\n startY: startY,\n currentX: startX,\n currentY: startY,\n offsetX: 0,\n offsetY: 0,\n interactiveElement: nearestInteractiveElement(event.target)\n };\n }\n\n function onMove(event) {\n if (!state) return;\n state.currentX = event.touches[0].clientX * pixelRatio;\n state.currentY = event.touches[0].clientY * pixelRatio;\n state.offsetX = state.currentX - state.startX;\n state.offsetY = state.currentY - state.startY;\n var shouldPreventDefault = false; // Wait for a movement of at least 6 pixels before reporting a drag.\n\n if (isStartingDrag) {\n if (Math.abs(state.offsetX) >= 6 || Math.abs(state.offsetY) >= 6) {\n isStartingDrag = false;\n shouldPreventDefault = Android.onDragStart(JSON.stringify(state));\n }\n } else {\n shouldPreventDefault = Android.onDragMove(JSON.stringify(state));\n }\n\n if (shouldPreventDefault) {\n event.stopPropagation();\n event.preventDefault();\n }\n }\n\n function onEnd(event) {\n if (!state) return;\n var shouldPreventDefault = Android.onDragEnd(JSON.stringify(state));\n\n if (shouldPreventDefault) {\n event.stopPropagation();\n event.preventDefault();\n }\n\n state = undefined;\n }\n} // See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling\n\n\nfunction nearestInteractiveElement(element) {\n var interactiveTags = ["a", "audio", "button", "canvas", "details", "input", "label", "option", "select", "submit", "textarea", "video"];\n\n if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) {\n return element.outerHTML;\n } // Checks whether the element is editable by the user.\n\n\n if (element.hasAttribute("contenteditable") && element.getAttribute("contenteditable").toLowerCase() != "false") {\n return element.outerHTML;\n } // Checks parents recursively because the touch might be for example on an <em> inside a <a>.\n\n\n if (element.parentElement) {\n return nearestInteractiveElement(element.parentElement);\n }\n\n return null;\n}\n;// CONCATENATED MODULE: ./src/highlight.js\nfunction highlight_typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { highlight_typeof = function _typeof(obj) { return typeof obj; }; } else { highlight_typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return highlight_typeof(obj); }\n\nfunction highlight_createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = highlight_unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }\n\nfunction highlight_unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return highlight_arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return highlight_arrayLikeToArray(o, minLen); }\n\nfunction highlight_arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\n/* eslint-disable */\n//\n// highlight.js\n// r2-navigator-kotlin\n//\n// Organized by Taehyun Kim on 6/27/19 from r2-navigator-js.\n//\n// Copyright 2019 Readium Foundation. All rights reserved.\n// Use of this source code is governed by a BSD-style license which is detailed\n// in the LICENSE file present in the project repository where this source code is maintained.\n//\nvar ROOT_CLASS_REDUCE_MOTION = "r2-reduce-motion";\nvar ROOT_CLASS_NO_FOOTNOTES = "r2-no-popup-foonotes";\nvar POPUP_DIALOG_CLASS = "r2-popup-dialog";\nvar FOOTNOTES_CONTAINER_CLASS = "r2-footnote-container";\nvar FOOTNOTES_CLOSE_BUTTON_CLASS = "r2-footnote-close";\nvar FOOTNOTE_FORCE_SHOW = "r2-footnote-force-show";\nvar TTS_ID_PREVIOUS = "r2-tts-previous";\nvar TTS_ID_NEXT = "r2-tts-next";\nvar TTS_ID_SLIDER = "r2-tts-slider";\nvar TTS_ID_ACTIVE_WORD = "r2-tts-active-word";\nvar TTS_ID_CONTAINER = "r2-tts-txt";\nvar TTS_ID_INFO = "r2-tts-info";\nvar TTS_NAV_BUTTON_CLASS = "r2-tts-button";\nvar TTS_ID_SPEAKING_DOC_ELEMENT = "r2-tts-speaking-el";\nvar TTS_CLASS_INJECTED_SPAN = "r2-tts-speaking-txt";\nvar TTS_CLASS_INJECTED_SUBSPAN = "r2-tts-speaking-word";\nvar TTS_ID_INJECTED_PARENT = "r2-tts-speaking-txt-parent";\nvar ID_HIGHLIGHTS_CONTAINER = "R2_ID_HIGHLIGHTS_CONTAINER";\nvar ID_ANNOTATION_CONTAINER = "R2_ID_ANNOTATION_CONTAINER";\nvar CLASS_HIGHLIGHT_CONTAINER = "R2_CLASS_HIGHLIGHT_CONTAINER";\nvar CLASS_ANNOTATION_CONTAINER = "R2_CLASS_ANNOTATION_CONTAINER";\nvar CLASS_HIGHLIGHT_AREA = "R2_CLASS_HIGHLIGHT_AREA";\nvar CLASS_ANNOTATION_AREA = "R2_CLASS_ANNOTATION_AREA";\nvar CLASS_HIGHLIGHT_BOUNDING_AREA = "R2_CLASS_HIGHLIGHT_BOUNDING_AREA";\nvar CLASS_ANNOTATION_BOUNDING_AREA = "R2_CLASS_ANNOTATION_BOUNDING_AREA"; // tslint:disable-next-line:max-line-length\n\nvar _blacklistIdClassForCFI = [POPUP_DIALOG_CLASS, TTS_CLASS_INJECTED_SPAN, TTS_CLASS_INJECTED_SUBSPAN, ID_HIGHLIGHTS_CONTAINER, CLASS_HIGHLIGHT_CONTAINER, CLASS_HIGHLIGHT_AREA, CLASS_HIGHLIGHT_BOUNDING_AREA, "resize-sensor"];\nvar CLASS_PAGINATED = "r2-css-paginated"; //const IS_DEV = (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "dev");\n\nvar IS_DEV = false;\nvar _highlights = [];\n\nvar _highlightsContainer;\n\nvar _annotationContainer;\n\nvar lastMouseDownX = -1;\nvar lastMouseDownY = -1;\nvar bodyEventListenersSet = false;\nvar USE_SVG = false;\nvar DEFAULT_BACKGROUND_COLOR_OPACITY = 0.3;\nvar ALT_BACKGROUND_COLOR_OPACITY = 0.45; //const DEBUG_VISUALS = false;\n\nvar DEBUG_VISUALS = false;\nvar DEFAULT_BACKGROUND_COLOR = {\n blue: 100,\n green: 50,\n red: 230\n};\nvar ANNOTATION_WIDTH = 15;\n\nfunction resetHighlightBoundingStyle(_win, highlightBounding) {\n if (highlightBounding.getAttribute("class") == CLASS_ANNOTATION_BOUNDING_AREA) {\n return;\n }\n\n highlightBounding.style.outline = "none";\n highlightBounding.style.setProperty("background-color", "transparent", "important");\n}\n\nfunction setHighlightAreaStyle(win, highlightAreas, highlight) {\n var useSVG = !DEBUG_VISUALS && USE_SVG;\n\n var _iterator = highlight_createForOfIteratorHelper(highlightAreas),\n _step;\n\n try {\n for (_iterator.s(); !(_step = _iterator.n()).done;) {\n var highlightArea = _step.value;\n var isSVG = useSVG && highlightArea.namespaceURI === SVG_XML_NAMESPACE;\n var opacity = ALT_BACKGROUND_COLOR_OPACITY;\n\n if (isSVG) {\n highlightArea.style.setProperty("fill", "rgb(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ")"), "important");\n highlightArea.style.setProperty("fill-opacity", "".concat(opacity), "important");\n highlightArea.style.setProperty("stroke", "rgb(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ")"), "important");\n highlightArea.style.setProperty("stroke-opacity", "".concat(opacity), "important");\n } else {\n highlightArea.style.setProperty("background-color", "rgba(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ", ").concat(opacity, ")"), "important");\n }\n }\n } catch (err) {\n _iterator.e(err);\n } finally {\n _iterator.f();\n }\n}\n\nfunction resetHighlightAreaStyle(win, highlightArea) {\n var useSVG = !DEBUG_VISUALS && USE_SVG; //const useSVG = USE_SVG;\n\n var isSVG = useSVG && highlightArea.namespaceURI === SVG_XML_NAMESPACE;\n var id = isSVG ? highlightArea.parentNode && highlightArea.parentNode.parentNode && highlightArea.parentNode.parentNode.nodeType === Node.ELEMENT_NODE && highlightArea.parentNode.parentNode.getAttribute ? highlightArea.parentNode.parentNode.getAttribute("id") : undefined : highlightArea.parentNode && highlightArea.parentNode.nodeType === Node.ELEMENT_NODE && highlightArea.parentNode.getAttribute ? highlightArea.parentNode.getAttribute("id") : undefined;\n\n if (id) {\n var highlight = _highlights.find(function (h) {\n return h.id === id;\n });\n\n if (highlight) {\n var opacity = DEFAULT_BACKGROUND_COLOR_OPACITY;\n\n if (isSVG) {\n highlightArea.style.setProperty("fill", "rgb(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ")"), "important");\n highlightArea.style.setProperty("fill-opacity", "".concat(opacity), "important");\n highlightArea.style.setProperty("stroke", "rgb(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ")"), "important");\n highlightArea.style.setProperty("stroke-opacity", "".concat(opacity), "important");\n } else {\n highlightArea.style.setProperty("background-color", "rgba(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ", ").concat(opacity, ")"), "important");\n }\n }\n }\n}\n\nfunction processTouchEvent(win, ev) {\n var document = win.document;\n var scrollElement = getScrollingElement(document);\n var x = ev.changedTouches[0].clientX;\n var y = ev.changedTouches[0].clientY;\n\n if (!_highlightsContainer) {\n return;\n }\n\n var paginated = isPaginated(document);\n var bodyRect = document.body.getBoundingClientRect();\n var xOffset;\n var yOffset;\n\n if (navigator.userAgent.match(/Android/i)) {\n xOffset = paginated ? -scrollElement.scrollLeft : bodyRect.left;\n yOffset = paginated ? -scrollElement.scrollTop : bodyRect.top;\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n xOffset = paginated ? 0 : -scrollElement.scrollLeft;\n yOffset = paginated ? 0 : bodyRect.top;\n }\n\n var foundHighlight;\n var foundElement;\n var foundRect; // _highlights.sort(function(a, b) {\n // console.log(JSON.stringify(a.selectionInfo))\n // return a.selectionInfo.cleanText.length < b.selectionInfo.cleanText.length\n // })\n\n for (var i = _highlights.length - 1; i >= 0; i--) {\n var highlight = _highlights[i];\n var highlightParent = document.getElementById("".concat(highlight.id));\n\n if (!highlightParent) {\n highlightParent = _highlightsContainer.querySelector("#".concat(highlight.id));\n }\n\n if (!highlightParent) {\n continue;\n }\n\n var hit = false;\n var highlightFragments = highlightParent.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA));\n\n var _iterator2 = highlight_createForOfIteratorHelper(highlightFragments),\n _step2;\n\n try {\n for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {\n var highlightFragment = _step2.value;\n var withRect = highlightFragment;\n var left = withRect.rect.left + xOffset;\n var top = withRect.rect.top + yOffset;\n foundRect = withRect.rect;\n\n if (x >= left && x < left + withRect.rect.width && y >= top && y < top + withRect.rect.height) {\n hit = true;\n break;\n }\n }\n } catch (err) {\n _iterator2.e(err);\n } finally {\n _iterator2.f();\n }\n\n if (hit) {\n foundHighlight = highlight;\n foundElement = highlightParent;\n break;\n }\n }\n\n if (!foundHighlight || !foundElement) {\n var highlightBoundings = _highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_BOUNDING_AREA));\n\n var _iterator3 = highlight_createForOfIteratorHelper(highlightBoundings),\n _step3;\n\n try {\n for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {\n var highlightBounding = _step3.value;\n resetHighlightBoundingStyle(win, highlightBounding);\n }\n } catch (err) {\n _iterator3.e(err);\n } finally {\n _iterator3.f();\n }\n\n var allHighlightAreas = Array.from(_highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA)));\n\n for (var _i = 0, _allHighlightAreas = allHighlightAreas; _i < _allHighlightAreas.length; _i++) {\n var highlightArea = _allHighlightAreas[_i];\n resetHighlightAreaStyle(win, highlightArea);\n }\n\n return;\n }\n\n if (foundElement.getAttribute("data-click")) {\n if (ev.type === "mousemove") {\n var foundElementHighlightAreas = Array.from(foundElement.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA)));\n\n var _allHighlightAreas2 = _highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA));\n\n var _iterator4 = highlight_createForOfIteratorHelper(_allHighlightAreas2),\n _step4;\n\n try {\n for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {\n var _highlightArea = _step4.value;\n\n if (foundElementHighlightAreas.indexOf(_highlightArea) < 0) {\n resetHighlightAreaStyle(win, _highlightArea);\n }\n }\n } catch (err) {\n _iterator4.e(err);\n } finally {\n _iterator4.f();\n }\n\n setHighlightAreaStyle(win, foundElementHighlightAreas, foundHighlight);\n var foundElementHighlightBounding = foundElement.querySelector(".".concat(CLASS_HIGHLIGHT_BOUNDING_AREA));\n\n var allHighlightBoundings = _highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_BOUNDING_AREA));\n\n var _iterator5 = highlight_createForOfIteratorHelper(allHighlightBoundings),\n _step5;\n\n try {\n for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {\n var _highlightBounding = _step5.value;\n\n if (!foundElementHighlightBounding || _highlightBounding !== foundElementHighlightBounding) {\n resetHighlightBoundingStyle(win, _highlightBounding);\n }\n }\n } catch (err) {\n _iterator5.e(err);\n } finally {\n _iterator5.f();\n }\n\n if (foundElementHighlightBounding) {\n if (DEBUG_VISUALS) {\n setHighlightBoundingStyle(win, foundElementHighlightBounding, foundHighlight);\n }\n }\n } else if (ev.type === "touchstart" || ev.type === "touchend") {\n var size = {\n screenWidth: window.outerWidth,\n screenHeight: window.outerHeight,\n left: foundRect.left,\n width: foundRect.width,\n top: foundRect.top,\n height: foundRect.height\n };\n var payload = {\n highlight: foundHighlight.id,\n size: size\n };\n\n if (typeof window !== "undefined" && highlight_typeof(window.process) === "object" && window.process.type === "renderer") {\n electron_1.ipcRenderer.sendToHost(R2_EVENT_HIGHLIGHT_CLICK, payload);\n } else if (window.webkitURL) {\n console.log(foundHighlight.id.includes("R2_ANNOTATION_"));\n\n if (foundHighlight.id.search("R2_ANNOTATION_") >= 0) {\n if (navigator.userAgent.match(/Android/i)) {\n Android.highlightAnnotationMarkActivated(foundHighlight.id);\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n webkit.messageHandlers.highlightAnnotationMarkActivated.postMessage(foundHighlight.id);\n }\n } else if (foundHighlight.id.search("R2_HIGHLIGHT_") >= 0) {\n if (navigator.userAgent.match(/Android/i)) {\n Android.highlightActivated(foundHighlight.id);\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n webkit.messageHandlers.highlightActivated.postMessage(foundHighlight.id);\n }\n }\n }\n\n ev.stopPropagation();\n ev.preventDefault();\n }\n }\n}\n\nfunction processMouseEvent(win, ev) {\n var document = win.document;\n var scrollElement = getScrollingElement(document);\n var x = ev.clientX;\n var y = ev.clientY;\n\n if (!_highlightsContainer) {\n return;\n }\n\n var paginated = isPaginated(document);\n var bodyRect = document.body.getBoundingClientRect();\n var xOffset;\n var yOffset;\n\n if (navigator.userAgent.match(/Android/i)) {\n xOffset = paginated ? -scrollElement.scrollLeft : bodyRect.left;\n yOffset = paginated ? -scrollElement.scrollTop : bodyRect.top;\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n xOffset = paginated ? 0 : -scrollElement.scrollLeft;\n yOffset = paginated ? 0 : bodyRect.top;\n }\n\n var foundHighlight;\n var foundElement;\n var foundRect;\n\n for (var i = _highlights.length - 1; i >= 0; i--) {\n var highlight = _highlights[i];\n var highlightParent = document.getElementById("".concat(highlight.id));\n\n if (!highlightParent) {\n highlightParent = _highlightsContainer.querySelector("#".concat(highlight.id));\n }\n\n if (!highlightParent) {\n continue;\n }\n\n var hit = false;\n var highlightFragments = highlightParent.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA));\n\n var _iterator6 = highlight_createForOfIteratorHelper(highlightFragments),\n _step6;\n\n try {\n for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {\n var highlightFragment = _step6.value;\n var withRect = highlightFragment;\n var left = withRect.rect.left + xOffset;\n var top = withRect.rect.top + yOffset;\n foundRect = withRect.rect;\n\n if (x >= left && x < left + withRect.rect.width && y >= top && y < top + withRect.rect.height) {\n hit = true;\n break;\n }\n }\n } catch (err) {\n _iterator6.e(err);\n } finally {\n _iterator6.f();\n }\n\n if (hit) {\n foundHighlight = highlight;\n foundElement = highlightParent;\n break;\n }\n }\n\n if (!foundHighlight || !foundElement) {\n var highlightBoundings = _highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_BOUNDING_AREA));\n\n var _iterator7 = highlight_createForOfIteratorHelper(highlightBoundings),\n _step7;\n\n try {\n for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {\n var highlightBounding = _step7.value;\n resetHighlightBoundingStyle(win, highlightBounding);\n }\n } catch (err) {\n _iterator7.e(err);\n } finally {\n _iterator7.f();\n }\n\n var allHighlightAreas = Array.from(_highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA)));\n\n for (var _i2 = 0, _allHighlightAreas3 = allHighlightAreas; _i2 < _allHighlightAreas3.length; _i2++) {\n var highlightArea = _allHighlightAreas3[_i2];\n resetHighlightAreaStyle(win, highlightArea);\n }\n\n return;\n }\n\n if (foundElement.getAttribute("data-click")) {\n if (ev.type === "mousemove") {\n var foundElementHighlightAreas = Array.from(foundElement.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA)));\n\n var _allHighlightAreas4 = _highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA));\n\n var _iterator8 = highlight_createForOfIteratorHelper(_allHighlightAreas4),\n _step8;\n\n try {\n for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {\n var _highlightArea2 = _step8.value;\n\n if (foundElementHighlightAreas.indexOf(_highlightArea2) < 0) {\n resetHighlightAreaStyle(win, _highlightArea2);\n }\n }\n } catch (err) {\n _iterator8.e(err);\n } finally {\n _iterator8.f();\n }\n\n setHighlightAreaStyle(win, foundElementHighlightAreas, foundHighlight);\n var foundElementHighlightBounding = foundElement.querySelector(".".concat(CLASS_HIGHLIGHT_BOUNDING_AREA));\n\n var allHighlightBoundings = _highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_BOUNDING_AREA));\n\n var _iterator9 = highlight_createForOfIteratorHelper(allHighlightBoundings),\n _step9;\n\n try {\n for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {\n var _highlightBounding2 = _step9.value;\n\n if (!foundElementHighlightBounding || _highlightBounding2 !== foundElementHighlightBounding) {\n resetHighlightBoundingStyle(win, _highlightBounding2);\n }\n }\n } catch (err) {\n _iterator9.e(err);\n } finally {\n _iterator9.f();\n }\n\n if (foundElementHighlightBounding) {\n if (DEBUG_VISUALS) {\n setHighlightBoundingStyle(win, foundElementHighlightBounding, foundHighlight);\n }\n }\n } else if (ev.type === "mouseup" || ev.type === "touchend") {\n var touchedPosition = {\n screenWidth: window.outerWidth,\n screenHeight: window.innerHeight,\n left: foundRect.left,\n width: foundRect.width,\n top: foundRect.top,\n height: foundRect.height\n };\n var payload = {\n highlight: foundHighlight,\n position: touchedPosition\n };\n\n if (typeof window !== "undefined" && highlight_typeof(window.process) === "object" && window.process.type === "renderer") {\n electron_1.ipcRenderer.sendToHost(R2_EVENT_HIGHLIGHT_CLICK, payload);\n } else if (window.webkitURL) {\n if (foundHighlight.id.search("R2_ANNOTATION_") >= 0) {\n if (navigator.userAgent.match(/Android/i)) {\n Android.highlightAnnotationMarkActivated(foundHighlight.id);\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n webkit.messageHandlers.highlightAnnotationMarkActivated.postMessage(foundHighlight.id);\n }\n } else if (foundHighlight.id.search("R2_HIGHLIGHT_") >= 0) {\n if (navigator.userAgent.match(/Android/i)) {\n Android.highlightActivated(foundHighlight.id);\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n webkit.messageHandlers.highlightActivated.postMessage(foundHighlight.id);\n }\n }\n }\n\n ev.stopPropagation();\n }\n }\n}\n\nfunction highlight_rectsTouchOrOverlap(rect1, rect2, tolerance) {\n return (rect1.left < rect2.right || tolerance >= 0 && highlight_almostEqual(rect1.left, rect2.right, tolerance)) && (rect2.left < rect1.right || tolerance >= 0 && highlight_almostEqual(rect2.left, rect1.right, tolerance)) && (rect1.top < rect2.bottom || tolerance >= 0 && highlight_almostEqual(rect1.top, rect2.bottom, tolerance)) && (rect2.top < rect1.bottom || tolerance >= 0 && highlight_almostEqual(rect2.top, rect1.bottom, tolerance));\n}\n\nfunction highlight_replaceOverlapingRects(rects) {\n for (var i = 0; i < rects.length; i++) {\n for (var j = i + 1; j < rects.length; j++) {\n var rect1 = rects[i];\n var rect2 = rects[j];\n\n if (rect1 === rect2) {\n if (IS_DEV) {\n console.log("replaceOverlapingRects rect1 === rect2 ??!");\n }\n\n continue;\n }\n\n if (highlight_rectsTouchOrOverlap(rect1, rect2, -1)) {\n var _ret = function () {\n var toAdd = [];\n var toRemove = void 0;\n var toPreserve = void 0;\n var subtractRects1 = highlight_rectSubtract(rect1, rect2);\n\n if (subtractRects1.length === 1) {\n toAdd = subtractRects1;\n toRemove = rect1;\n toPreserve = rect2;\n } else {\n var subtractRects2 = highlight_rectSubtract(rect2, rect1);\n\n if (subtractRects1.length < subtractRects2.length) {\n toAdd = subtractRects1;\n toRemove = rect1;\n toPreserve = rect2;\n } else {\n toAdd = subtractRects2;\n toRemove = rect2;\n toPreserve = rect1;\n }\n }\n\n if (IS_DEV) {\n var toCheck = [];\n toCheck.push(toPreserve);\n Array.prototype.push.apply(toCheck, toAdd);\n checkOverlaps(toCheck);\n }\n\n if (IS_DEV) {\n console.log("CLIENT RECT: overlap, cut one rect into ".concat(toAdd.length));\n }\n\n var newRects = rects.filter(function (rect) {\n return rect !== toRemove;\n });\n Array.prototype.push.apply(newRects, toAdd);\n return {\n v: highlight_replaceOverlapingRects(newRects)\n };\n }();\n\n if (highlight_typeof(_ret) === "object") return _ret.v;\n }\n }\n }\n\n return rects;\n}\n\nfunction checkOverlaps(rects) {\n var stillOverlapingRects = [];\n\n var _iterator10 = highlight_createForOfIteratorHelper(rects),\n _step10;\n\n try {\n for (_iterator10.s(); !(_step10 = _iterator10.n()).done;) {\n var rect1 = _step10.value;\n\n var _iterator11 = highlight_createForOfIteratorHelper(rects),\n _step11;\n\n try {\n for (_iterator11.s(); !(_step11 = _iterator11.n()).done;) {\n var rect2 = _step11.value;\n\n if (rect1 === rect2) {\n continue;\n }\n\n var has1 = stillOverlapingRects.indexOf(rect1) >= 0;\n var has2 = stillOverlapingRects.indexOf(rect2) >= 0;\n\n if (!has1 || !has2) {\n if (highlight_rectsTouchOrOverlap(rect1, rect2, -1)) {\n if (!has1) {\n stillOverlapingRects.push(rect1);\n }\n\n if (!has2) {\n stillOverlapingRects.push(rect2);\n }\n\n console.log("CLIENT RECT: overlap ---");\n console.log("#1 TOP:".concat(rect1.top, " BOTTOM:").concat(rect1.bottom, " LEFT:").concat(rect1.left, " RIGHT:").concat(rect1.right, " WIDTH:").concat(rect1.width, " HEIGHT:").concat(rect1.height));\n console.log("#2 TOP:".concat(rect2.top, " BOTTOM:").concat(rect2.bottom, " LEFT:").concat(rect2.left, " RIGHT:").concat(rect2.right, " WIDTH:").concat(rect2.width, " HEIGHT:").concat(rect2.height));\n var xOverlap = getRectOverlapX(rect1, rect2);\n console.log("xOverlap: ".concat(xOverlap));\n var yOverlap = getRectOverlapY(rect1, rect2);\n console.log("yOverlap: ".concat(yOverlap));\n }\n }\n }\n } catch (err) {\n _iterator11.e(err);\n } finally {\n _iterator11.f();\n }\n }\n } catch (err) {\n _iterator10.e(err);\n } finally {\n _iterator10.f();\n }\n\n if (stillOverlapingRects.length) {\n console.log("CLIENT RECT: overlaps ".concat(stillOverlapingRects.length));\n }\n}\n\nfunction highlight_removeContainedRects(rects, tolerance) {\n var rectsToKeep = new Set(rects);\n\n var _iterator12 = highlight_createForOfIteratorHelper(rects),\n _step12;\n\n try {\n for (_iterator12.s(); !(_step12 = _iterator12.n()).done;) {\n var rect = _step12.value;\n var bigEnough = rect.width > 1 && rect.height > 1;\n\n if (!bigEnough) {\n if (IS_DEV) {\n console.log("CLIENT RECT: remove tiny");\n }\n\n rectsToKeep.delete(rect);\n continue;\n }\n\n var _iterator13 = highlight_createForOfIteratorHelper(rects),\n _step13;\n\n try {\n for (_iterator13.s(); !(_step13 = _iterator13.n()).done;) {\n var possiblyContainingRect = _step13.value;\n\n if (rect === possiblyContainingRect) {\n continue;\n }\n\n if (!rectsToKeep.has(possiblyContainingRect)) {\n continue;\n }\n\n if (highlight_rectContains(possiblyContainingRect, rect, tolerance)) {\n if (IS_DEV) {\n console.log("CLIENT RECT: remove contained");\n }\n\n rectsToKeep.delete(rect);\n break;\n }\n }\n } catch (err) {\n _iterator13.e(err);\n } finally {\n _iterator13.f();\n }\n }\n } catch (err) {\n _iterator12.e(err);\n } finally {\n _iterator12.f();\n }\n\n return Array.from(rectsToKeep);\n}\n\nfunction highlight_almostEqual(a, b, tolerance) {\n return Math.abs(a - b) <= tolerance;\n}\n\nfunction highlight_rectIntersect(rect1, rect2) {\n var maxLeft = Math.max(rect1.left, rect2.left);\n var minRight = Math.min(rect1.right, rect2.right);\n var maxTop = Math.max(rect1.top, rect2.top);\n var minBottom = Math.min(rect1.bottom, rect2.bottom);\n var rect = {\n bottom: minBottom,\n height: Math.max(0, minBottom - maxTop),\n left: maxLeft,\n right: minRight,\n top: maxTop,\n width: Math.max(0, minRight - maxLeft)\n };\n return rect;\n}\n\nfunction highlight_rectSubtract(rect1, rect2) {\n var rectIntersected = highlight_rectIntersect(rect2, rect1);\n\n if (rectIntersected.height === 0 || rectIntersected.width === 0) {\n return [rect1];\n }\n\n var rects = [];\n {\n var rectA = {\n bottom: rect1.bottom,\n height: 0,\n left: rect1.left,\n right: rectIntersected.left,\n top: rect1.top,\n width: 0\n };\n rectA.width = rectA.right - rectA.left;\n rectA.height = rectA.bottom - rectA.top;\n\n if (rectA.height !== 0 && rectA.width !== 0) {\n rects.push(rectA);\n }\n }\n {\n var rectB = {\n bottom: rectIntersected.top,\n height: 0,\n left: rectIntersected.left,\n right: rectIntersected.right,\n top: rect1.top,\n width: 0\n };\n rectB.width = rectB.right - rectB.left;\n rectB.height = rectB.bottom - rectB.top;\n\n if (rectB.height !== 0 && rectB.width !== 0) {\n rects.push(rectB);\n }\n }\n {\n var rectC = {\n bottom: rect1.bottom,\n height: 0,\n left: rectIntersected.left,\n right: rectIntersected.right,\n top: rectIntersected.bottom,\n width: 0\n };\n rectC.width = rectC.right - rectC.left;\n rectC.height = rectC.bottom - rectC.top;\n\n if (rectC.height !== 0 && rectC.width !== 0) {\n rects.push(rectC);\n }\n }\n {\n var rectD = {\n bottom: rect1.bottom,\n height: 0,\n left: rectIntersected.right,\n right: rect1.right,\n top: rect1.top,\n width: 0\n };\n rectD.width = rectD.right - rectD.left;\n rectD.height = rectD.bottom - rectD.top;\n\n if (rectD.height !== 0 && rectD.width !== 0) {\n rects.push(rectD);\n }\n }\n return rects;\n}\n\nfunction highlight_rectContainsPoint(rect, x, y, tolerance) {\n return (rect.left < x || highlight_almostEqual(rect.left, x, tolerance)) && (rect.right > x || highlight_almostEqual(rect.right, x, tolerance)) && (rect.top < y || highlight_almostEqual(rect.top, y, tolerance)) && (rect.bottom > y || highlight_almostEqual(rect.bottom, y, tolerance));\n}\n\nfunction highlight_rectContains(rect1, rect2, tolerance) {\n return highlight_rectContainsPoint(rect1, rect2.left, rect2.top, tolerance) && highlight_rectContainsPoint(rect1, rect2.right, rect2.top, tolerance) && highlight_rectContainsPoint(rect1, rect2.left, rect2.bottom, tolerance) && highlight_rectContainsPoint(rect1, rect2.right, rect2.bottom, tolerance);\n}\n\nfunction highlight_getBoundingRect(rect1, rect2) {\n var left = Math.min(rect1.left, rect2.left);\n var right = Math.max(rect1.right, rect2.right);\n var top = Math.min(rect1.top, rect2.top);\n var bottom = Math.max(rect1.bottom, rect2.bottom);\n return {\n bottom: bottom,\n height: bottom - top,\n left: left,\n right: right,\n top: top,\n width: right - left\n };\n}\n\nfunction highlight_mergeTouchingRects(rects, tolerance, doNotMergeHorizontallyAlignedRects) {\n for (var i = 0; i < rects.length; i++) {\n var _loop = function _loop(j) {\n var rect1 = rects[i];\n var rect2 = rects[j];\n\n if (rect1 === rect2) {\n if (IS_DEV) {\n console.log("mergeTouchingRects rect1 === rect2 ??!");\n }\n\n return "continue";\n }\n\n var rectsLineUpVertically = highlight_almostEqual(rect1.top, rect2.top, tolerance) && highlight_almostEqual(rect1.bottom, rect2.bottom, tolerance);\n var rectsLineUpHorizontally = highlight_almostEqual(rect1.left, rect2.left, tolerance) && highlight_almostEqual(rect1.right, rect2.right, tolerance);\n var horizontalAllowed = !doNotMergeHorizontallyAlignedRects;\n var aligned = rectsLineUpHorizontally && horizontalAllowed || rectsLineUpVertically && !rectsLineUpHorizontally;\n var canMerge = aligned && highlight_rectsTouchOrOverlap(rect1, rect2, tolerance);\n\n if (canMerge) {\n if (IS_DEV) {\n console.log("CLIENT RECT: merging two into one, VERTICAL: ".concat(rectsLineUpVertically, " HORIZONTAL: ").concat(rectsLineUpHorizontally, " (").concat(doNotMergeHorizontallyAlignedRects, ")"));\n }\n\n var newRects = rects.filter(function (rect) {\n return rect !== rect1 && rect !== rect2;\n });\n var replacementClientRect = highlight_getBoundingRect(rect1, rect2);\n newRects.push(replacementClientRect);\n return {\n v: highlight_mergeTouchingRects(newRects, tolerance, doNotMergeHorizontallyAlignedRects)\n };\n }\n };\n\n for (var j = i + 1; j < rects.length; j++) {\n var _ret2 = _loop(j);\n\n if (_ret2 === "continue") continue;\n if (highlight_typeof(_ret2) === "object") return _ret2.v;\n }\n }\n\n return rects;\n}\n\nfunction highlight_getClientRectsNoOverlap(range, doNotMergeHorizontallyAlignedRects) {\n var rangeClientRects = range.getClientRects();\n return getClientRectsNoOverlap_(rangeClientRects, doNotMergeHorizontallyAlignedRects);\n}\n\nfunction getClientRectsNoOverlap_(clientRects, doNotMergeHorizontallyAlignedRects) {\n var tolerance = 1;\n var originalRects = [];\n\n var _iterator14 = highlight_createForOfIteratorHelper(clientRects),\n _step14;\n\n try {\n for (_iterator14.s(); !(_step14 = _iterator14.n()).done;) {\n var rangeClientRect = _step14.value;\n originalRects.push({\n bottom: rangeClientRect.bottom,\n height: rangeClientRect.height,\n left: rangeClientRect.left,\n right: rangeClientRect.right,\n top: rangeClientRect.top,\n width: rangeClientRect.width\n });\n }\n } catch (err) {\n _iterator14.e(err);\n } finally {\n _iterator14.f();\n }\n\n var mergedRects = highlight_mergeTouchingRects(originalRects, tolerance, doNotMergeHorizontallyAlignedRects);\n var noContainedRects = highlight_removeContainedRects(mergedRects, tolerance);\n var newRects = highlight_replaceOverlapingRects(noContainedRects);\n var minArea = 2 * 2;\n\n for (var j = newRects.length - 1; j >= 0; j--) {\n var rect = newRects[j];\n var bigEnough = rect.width * rect.height > minArea;\n\n if (!bigEnough) {\n if (newRects.length > 1) {\n if (IS_DEV) {\n console.log("CLIENT RECT: remove small");\n }\n\n newRects.splice(j, 1);\n } else {\n if (IS_DEV) {\n console.log("CLIENT RECT: remove small, but keep otherwise empty!");\n }\n\n break;\n }\n }\n }\n\n if (IS_DEV) {\n checkOverlaps(newRects);\n }\n\n if (IS_DEV) {\n console.log("CLIENT RECT: reduced ".concat(originalRects.length, " --\x3e ").concat(newRects.length));\n }\n\n return newRects;\n}\n\nfunction isPaginated(document) {\n return document && document.documentElement && document.documentElement.classList.contains(CLASS_PAGINATED);\n}\n\nfunction getScrollingElement(document) {\n if (document.scrollingElement) {\n return document.scrollingElement;\n }\n\n return document.body;\n}\n\nfunction ensureContainer(win, annotationFlag) {\n var document = win.document;\n\n if (!_highlightsContainer) {\n if (!bodyEventListenersSet) {\n bodyEventListenersSet = true;\n document.body.addEventListener("mousedown", function (ev) {\n lastMouseDownX = ev.clientX;\n lastMouseDownY = ev.clientY;\n }, false);\n document.body.addEventListener("mouseup", function (ev) {\n if (Math.abs(lastMouseDownX - ev.clientX) < 3 && Math.abs(lastMouseDownY - ev.clientY) < 3) {\n processMouseEvent(win, ev);\n }\n }, false);\n document.body.addEventListener("mousemove", function (ev) {\n processMouseEvent(win, ev);\n }, false);\n document.body.addEventListener("touchend", function touchEnd(e) {\n processTouchEvent(win, e);\n }, false);\n }\n\n _highlightsContainer = document.createElement("div");\n\n _highlightsContainer.setAttribute("id", ID_HIGHLIGHTS_CONTAINER);\n\n _highlightsContainer.style.setProperty("pointer-events", "none");\n\n document.body.append(_highlightsContainer);\n }\n\n return _highlightsContainer;\n}\n\nfunction hideAllhighlights() {\n if (_highlightsContainer) {\n _highlightsContainer.remove();\n\n _highlightsContainer = null;\n }\n}\n\nfunction destroyAllhighlights() {\n hideAllhighlights();\n\n _highlights.splice(0, _highlights.length);\n}\n\nfunction destroyHighlight(id) {\n var i = -1;\n var _document = window.document;\n\n var highlight = _highlights.find(function (h, j) {\n i = j;\n return h.id === id;\n });\n\n if (highlight && i >= 0 && i < _highlights.length) {\n _highlights.splice(i, 1);\n }\n\n var highlightContainer = _document.getElementById(id);\n\n if (highlightContainer) {\n highlightContainer.remove();\n }\n}\n\nfunction isCfiTextNode(node) {\n return node.nodeType !== Node.ELEMENT_NODE;\n}\n\nfunction getChildTextNodeCfiIndex(element, child) {\n var found = -1;\n var textNodeIndex = -1;\n var previousWasElement = false;\n\n for (var i = 0; i < element.childNodes.length; i++) {\n var childNode = element.childNodes[i];\n var isText = isCfiTextNode(childNode);\n\n if (isText || previousWasElement) {\n textNodeIndex += 2;\n }\n\n if (isText) {\n if (childNode === child) {\n found = textNodeIndex;\n break;\n }\n }\n\n previousWasElement = childNode.nodeType === Node.ELEMENT_NODE;\n }\n\n return found;\n}\n\nfunction getCommonAncestorElement(node1, node2) {\n if (node1.nodeType === Node.ELEMENT_NODE && node1 === node2) {\n return node1;\n }\n\n if (node1.nodeType === Node.ELEMENT_NODE && node1.contains(node2)) {\n return node1;\n }\n\n if (node2.nodeType === Node.ELEMENT_NODE && node2.contains(node1)) {\n return node2;\n }\n\n var node1ElementAncestorChain = [];\n var parent = node1.parentNode;\n\n while (parent && parent.nodeType === Node.ELEMENT_NODE) {\n node1ElementAncestorChain.push(parent);\n parent = parent.parentNode;\n }\n\n var node2ElementAncestorChain = [];\n parent = node2.parentNode;\n\n while (parent && parent.nodeType === Node.ELEMENT_NODE) {\n node2ElementAncestorChain.push(parent);\n parent = parent.parentNode;\n }\n\n var commonAncestor = node1ElementAncestorChain.find(function (node1ElementAncestor) {\n return node2ElementAncestorChain.indexOf(node1ElementAncestor) >= 0;\n });\n\n if (!commonAncestor) {\n commonAncestor = node2ElementAncestorChain.find(function (node2ElementAncestor) {\n return node1ElementAncestorChain.indexOf(node2ElementAncestor) >= 0;\n });\n }\n\n return commonAncestor;\n}\n\nfunction fullQualifiedSelector(node) {\n if (node.nodeType !== Node.ELEMENT_NODE) {\n var lowerCaseName = node.localName && node.localName.toLowerCase() || node.nodeName.toLowerCase();\n return lowerCaseName;\n } //return cssPath(node, justSelector);\n\n\n return cssPath(node, true);\n}\n\nfunction getCurrentSelectionInfo() {\n var selection = window.getSelection();\n\n if (!selection) {\n return undefined;\n }\n\n if (selection.isCollapsed) {\n console.log("^^^ SELECTION COLLAPSED.");\n return undefined;\n }\n\n var rawText = selection.toString();\n var cleanText = rawText.trim().replace(/\\n/g, " ").replace(/\\s\\s+/g, " ");\n\n if (cleanText.length === 0) {\n console.log("^^^ SELECTION TEXT EMPTY.");\n return undefined;\n }\n\n if (!selection.anchorNode || !selection.focusNode) {\n return undefined;\n }\n\n var range = selection.rangeCount === 1 ? selection.getRangeAt(0) : createOrderedRange(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);\n\n if (!range || range.collapsed) {\n console.log("$$$$$$$$$$$$$$$$$ CANNOT GET NON-COLLAPSED SELECTION RANGE?!");\n return undefined;\n }\n\n var rangeInfo = convertRange(range, fullQualifiedSelector, computeCFI);\n\n if (!rangeInfo) {\n console.log("^^^ SELECTION RANGE INFO FAIL?!");\n return undefined;\n }\n\n if (IS_DEV && DEBUG_VISUALS) {\n var restoredRange = convertRangeInfo(win.document, rangeInfo);\n\n if (restoredRange) {\n if (restoredRange.startOffset === range.startOffset && restoredRange.endOffset === range.endOffset && restoredRange.startContainer === range.startContainer && restoredRange.endContainer === range.endContainer) {\n console.log("SELECTION RANGE RESTORED OKAY (dev check).");\n } else {\n console.log("SELECTION RANGE RESTORE FAIL (dev check).");\n dumpDebug("SELECTION", selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset, getCssSelector);\n dumpDebug("ORDERED RANGE FROM SELECTION", range.startContainer, range.startOffset, range.endContainer, range.endOffset, getCssSelector);\n dumpDebug("RESTORED RANGE", restoredRange.startContainer, restoredRange.startOffset, restoredRange.endContainer, restoredRange.endOffset, getCssSelector);\n }\n } else {\n console.log("CANNOT RESTORE SELECTION RANGE ??!");\n }\n } else {}\n\n return {\n locations: rangeInfo2Location(rangeInfo),\n text: {\n highlight: rawText\n }\n };\n}\n\nfunction checkBlacklisted(el) {\n var blacklistedId;\n var id = el.getAttribute("id");\n\n if (id && _blacklistIdClassForCFI.indexOf(id) >= 0) {\n console.log("checkBlacklisted ID: " + id);\n blacklistedId = id;\n }\n\n var blacklistedClass;\n\n var _iterator15 = highlight_createForOfIteratorHelper(_blacklistIdClassForCFI),\n _step15;\n\n try {\n for (_iterator15.s(); !(_step15 = _iterator15.n()).done;) {\n var item = _step15.value;\n\n if (el.classList.contains(item)) {\n console.log("checkBlacklisted CLASS: " + item);\n blacklistedClass = item;\n break;\n }\n }\n } catch (err) {\n _iterator15.e(err);\n } finally {\n _iterator15.f();\n }\n\n if (blacklistedId || blacklistedClass) {\n return true;\n }\n\n return false;\n}\n\nfunction cssPath(node, optimized) {\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return "";\n }\n\n var steps = [];\n var contextNode = node;\n\n while (contextNode) {\n var step = _cssPathStep(contextNode, !!optimized, contextNode === node);\n\n if (!step) {\n break; // Error - bail out early.\n }\n\n steps.push(step.value);\n\n if (step.optimized) {\n break;\n }\n\n contextNode = contextNode.parentNode;\n }\n\n steps.reverse();\n return steps.join(" > ");\n} // tslint:disable-next-line:max-line-length\n// https://chromium.googlesource.com/chromium/blink/+/master/Source/devtools/front_end/components/DOMPresentationUtils.js#316\n\n\nfunction _cssPathStep(node, optimized, isTargetNode) {\n function prefixedElementClassNames(nd) {\n var classAttribute = nd.getAttribute("class");\n\n if (!classAttribute) {\n return [];\n }\n\n return classAttribute.split(/\\s+/g).filter(Boolean).map(function (nm) {\n // The prefix is required to store "__proto__" in a object-based map.\n return "$" + nm;\n });\n }\n\n function idSelector(idd) {\n return "#" + escapeIdentifierIfNeeded(idd);\n }\n\n function escapeIdentifierIfNeeded(ident) {\n if (isCSSIdentifier(ident)) {\n return ident;\n }\n\n var shouldEscapeFirst = /^(?:[0-9]|-[0-9-]?)/.test(ident);\n var lastIndex = ident.length - 1;\n return ident.replace(/./g, function (c, ii) {\n return shouldEscapeFirst && ii === 0 || !isCSSIdentChar(c) ? escapeAsciiChar(c, ii === lastIndex) : c;\n });\n }\n\n function escapeAsciiChar(c, isLast) {\n return "\\\\" + toHexByte(c) + (isLast ? "" : " ");\n }\n\n function toHexByte(c) {\n var hexByte = c.charCodeAt(0).toString(16);\n\n if (hexByte.length === 1) {\n hexByte = "0" + hexByte;\n }\n\n return hexByte;\n }\n\n function isCSSIdentChar(c) {\n if (/[a-zA-Z0-9_-]/.test(c)) {\n return true;\n }\n\n return c.charCodeAt(0) >= 0xa0;\n }\n\n function isCSSIdentifier(value) {\n return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value);\n }\n\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return undefined;\n }\n\n var lowerCaseName = node.localName && node.localName.toLowerCase() || node.nodeName.toLowerCase();\n var element = node;\n var id = element.getAttribute("id");\n\n if (optimized) {\n if (id) {\n return {\n optimized: true,\n value: idSelector(id)\n };\n }\n\n if (lowerCaseName === "body" || lowerCaseName === "head" || lowerCaseName === "html") {\n return {\n optimized: true,\n value: lowerCaseName // node.nodeNameInCorrectCase(),\n\n };\n }\n }\n\n var nodeName = lowerCaseName; // node.nodeNameInCorrectCase();\n\n if (id) {\n return {\n optimized: true,\n value: nodeName + idSelector(id)\n };\n }\n\n var parent = node.parentNode;\n\n if (!parent || parent.nodeType === Node.DOCUMENT_NODE) {\n return {\n optimized: true,\n value: nodeName\n };\n }\n\n var prefixedOwnClassNamesArray_ = prefixedElementClassNames(element);\n var prefixedOwnClassNamesArray = []; // .keySet()\n\n prefixedOwnClassNamesArray_.forEach(function (arrItem) {\n if (prefixedOwnClassNamesArray.indexOf(arrItem) < 0) {\n prefixedOwnClassNamesArray.push(arrItem);\n }\n });\n var needsClassNames = false;\n var needsNthChild = false;\n var ownIndex = -1;\n var elementIndex = -1;\n var siblings = parent.children;\n\n var _loop2 = function _loop2(i) {\n var sibling = siblings[i];\n\n if (sibling.nodeType !== Node.ELEMENT_NODE) {\n return "continue";\n }\n\n elementIndex += 1;\n\n if (sibling === node) {\n ownIndex = elementIndex;\n return "continue";\n }\n\n if (needsNthChild) {\n return "continue";\n } // sibling.nodeNameInCorrectCase()\n\n\n var siblingName = sibling.localName && sibling.localName.toLowerCase() || sibling.nodeName.toLowerCase();\n\n if (siblingName !== nodeName) {\n return "continue";\n }\n\n needsClassNames = true;\n var ownClassNames = [];\n prefixedOwnClassNamesArray.forEach(function (arrItem) {\n ownClassNames.push(arrItem);\n });\n var ownClassNameCount = ownClassNames.length;\n\n if (ownClassNameCount === 0) {\n needsNthChild = true;\n return "continue";\n }\n\n var siblingClassNamesArray_ = prefixedElementClassNames(sibling);\n var siblingClassNamesArray = []; // .keySet()\n\n siblingClassNamesArray_.forEach(function (arrItem) {\n if (siblingClassNamesArray.indexOf(arrItem) < 0) {\n siblingClassNamesArray.push(arrItem);\n }\n });\n\n for (var _i3 = 0, _siblingClassNamesArr = siblingClassNamesArray; _i3 < _siblingClassNamesArr.length; _i3++) {\n var siblingClass = _siblingClassNamesArr[_i3];\n var ind = ownClassNames.indexOf(siblingClass);\n\n if (ind < 0) {\n continue;\n }\n\n ownClassNames.splice(ind, 1); // delete ownClassNames[siblingClass];\n\n if (! --ownClassNameCount) {\n needsNthChild = true;\n break;\n }\n }\n };\n\n for (var i = 0; (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) {\n var _ret3 = _loop2(i);\n\n if (_ret3 === "continue") continue;\n }\n\n var result = nodeName;\n\n if (isTargetNode && nodeName === "input" && element.getAttribute("type") && !element.getAttribute("id") && !element.getAttribute("class")) {\n result += \'[type="\' + element.getAttribute("type") + \'"]\';\n }\n\n if (needsNthChild) {\n result += ":nth-child(" + (ownIndex + 1) + ")";\n } else if (needsClassNames) {\n var _iterator16 = highlight_createForOfIteratorHelper(prefixedOwnClassNamesArray),\n _step16;\n\n try {\n for (_iterator16.s(); !(_step16 = _iterator16.n()).done;) {\n var prefixedName = _step16.value;\n result += "." + escapeIdentifierIfNeeded(prefixedName.substr(1));\n }\n } catch (err) {\n _iterator16.e(err);\n } finally {\n _iterator16.f();\n }\n }\n\n return {\n optimized: false,\n value: result\n };\n}\n\nfunction computeCFI(node) {\n // TODO: handle character position inside text node\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return undefined;\n }\n\n var cfi = "";\n var currentElement = node;\n\n while (currentElement.parentNode && currentElement.parentNode.nodeType === Node.ELEMENT_NODE) {\n var blacklisted = checkBlacklisted(currentElement);\n\n if (!blacklisted) {\n var currentElementParentChildren = currentElement.parentNode.children;\n var currentElementIndex = -1;\n\n for (var i = 0; i < currentElementParentChildren.length; i++) {\n if (currentElement === currentElementParentChildren[i]) {\n currentElementIndex = i;\n break;\n }\n }\n\n if (currentElementIndex >= 0) {\n var cfiIndex = (currentElementIndex + 1) * 2;\n cfi = cfiIndex + (currentElement.id ? "[" + currentElement.id + "]" : "") + (cfi.length ? "/" + cfi : "");\n }\n }\n\n currentElement = currentElement.parentNode;\n }\n\n return "/" + cfi;\n}\n\nfunction _createHighlight(locations, color, pointerInteraction, type) {\n var rangeInfo = location2RangeInfo(locations);\n var uniqueStr = "".concat(rangeInfo.cfi).concat(rangeInfo.startContainerElementCssSelector).concat(rangeInfo.startContainerChildTextNodeIndex).concat(rangeInfo.startOffset).concat(rangeInfo.endContainerElementCssSelector).concat(rangeInfo.endContainerChildTextNodeIndex).concat(rangeInfo.endOffset);\n\n var hash = __webpack_require__(3715);\n\n var sha256Hex = hash.sha256().update(uniqueStr).digest("hex");\n var id;\n\n if (type == ID_HIGHLIGHTS_CONTAINER) {\n id = "R2_HIGHLIGHT_" + sha256Hex;\n } else {\n id = "R2_ANNOTATION_" + sha256Hex;\n }\n\n destroyHighlight(id);\n var highlight = {\n color: color ? color : DEFAULT_BACKGROUND_COLOR,\n id: id,\n pointerInteraction: pointerInteraction,\n rangeInfo: rangeInfo\n };\n\n _highlights.push(highlight);\n\n createHighlightDom(window, highlight, type == ID_ANNOTATION_CONTAINER ? true : false);\n return highlight;\n}\n\nfunction createHighlight(selectionInfo, color, pointerInteraction) {\n return _createHighlight(selectionInfo, color, pointerInteraction, ID_HIGHLIGHTS_CONTAINER);\n}\nfunction createAnnotation(id) {\n var i = -1;\n\n var highlight = _highlights.find(function (h, j) {\n i = j;\n return h.id === id;\n });\n\n if (i == _highlights.length) return;\n var locations = {\n locations: rangeInfo2Location(highlight.rangeInfo)\n };\n return _createHighlight(locations, highlight.color, true, ID_ANNOTATION_CONTAINER);\n}\n\nfunction createHighlightDom(win, highlight, annotationFlag) {\n var document = win.document;\n var scale = 1 / (win.READIUM2 && win.READIUM2.isFixedLayout ? win.READIUM2.fxlViewportScale : 1);\n var scrollElement = getScrollingElement(document);\n var range = convertRangeInfo(document, highlight.rangeInfo);\n\n if (!range) {\n return undefined;\n }\n\n var paginated = isPaginated(document);\n var highlightsContainer = ensureContainer(win, annotationFlag);\n var highlightParent = document.createElement("div");\n highlightParent.setAttribute("id", highlight.id);\n highlightParent.setAttribute("class", CLASS_HIGHLIGHT_CONTAINER);\n document.body.style.position = "relative";\n highlightParent.style.setProperty("pointer-events", "none");\n\n if (highlight.pointerInteraction) {\n highlightParent.setAttribute("data-click", "1");\n }\n\n var bodyRect = document.body.getBoundingClientRect();\n var useSVG = !DEBUG_VISUALS && USE_SVG; //const useSVG = USE_SVG;\n\n var drawUnderline = false;\n var drawStrikeThrough = false;\n var doNotMergeHorizontallyAlignedRects = drawUnderline || drawStrikeThrough; //const clientRects = DEBUG_VISUALS ? range.getClientRects() :\n\n var clientRects = highlight_getClientRectsNoOverlap(range, doNotMergeHorizontallyAlignedRects);\n var highlightAreaSVGDocFrag;\n var roundedCorner = 3;\n var underlineThickness = 2;\n var strikeThroughLineThickness = 3;\n var opacity = DEFAULT_BACKGROUND_COLOR_OPACITY;\n var extra = "";\n var rangeAnnotationBoundingClientRect = frameForHighlightAnnotationMarkWithID(win, highlight.id);\n var xOffset;\n var yOffset;\n var annotationOffset;\n\n if (navigator.userAgent.match(/Android/i)) {\n xOffset = paginated ? -scrollElement.scrollLeft : bodyRect.left;\n yOffset = paginated ? -scrollElement.scrollTop : bodyRect.top;\n annotationOffset = parseInt((rangeAnnotationBoundingClientRect.right - xOffset) / window.innerWidth) + 1;\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n xOffset = paginated ? 0 : -scrollElement.scrollLeft;\n yOffset = paginated ? 0 : bodyRect.top;\n annotationOffset = parseInt(rangeAnnotationBoundingClientRect.right / window.innerWidth + 1);\n }\n\n var _iterator17 = highlight_createForOfIteratorHelper(clientRects),\n _step17;\n\n try {\n for (_iterator17.s(); !(_step17 = _iterator17.n()).done;) {\n var clientRect = _step17.value;\n\n if (useSVG) {\n var borderThickness = 0;\n\n if (!highlightAreaSVGDocFrag) {\n highlightAreaSVGDocFrag = document.createDocumentFragment();\n }\n\n var highlightAreaSVGRect = document.createElementNS(SVG_XML_NAMESPACE, "rect");\n highlightAreaSVGRect.setAttribute("class", CLASS_HIGHLIGHT_AREA);\n highlightAreaSVGRect.setAttribute("style", "fill: rgb(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ") !important; fill-opacity: ").concat(opacity, " !important; stroke-width: 0;"));\n highlightAreaSVGRect.scale = scale;\n /*\n highlightAreaSVGRect.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width,\n };\n */\n\n if (annotationFlag) {\n highlightAreaSVGRect.rect = {\n height: ANNOTATION_WIDTH,\n //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4,\n left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH,\n top: rangeAnnotationBoundingClientRect.top - yOffset,\n width: ANNOTATION_WIDTH\n };\n } else {\n highlightAreaSVGRect.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width\n };\n }\n\n highlightAreaSVGRect.setAttribute("rx", "".concat(roundedCorner * scale));\n highlightAreaSVGRect.setAttribute("ry", "".concat(roundedCorner * scale));\n highlightAreaSVGRect.setAttribute("x", "".concat((highlightAreaSVGRect.rect.left - borderThickness) * scale));\n highlightAreaSVGRect.setAttribute("y", "".concat((highlightAreaSVGRect.rect.top - borderThickness) * scale));\n highlightAreaSVGRect.setAttribute("height", "".concat((highlightAreaSVGRect.rect.height + borderThickness * 2) * scale));\n highlightAreaSVGRect.setAttribute("width", "".concat((highlightAreaSVGRect.rect.width + borderThickness * 2) * scale));\n highlightAreaSVGDocFrag.appendChild(highlightAreaSVGRect);\n\n if (drawUnderline) {\n var highlightAreaSVGLine = document.createElementNS(SVG_XML_NAMESPACE, "line");\n highlightAreaSVGRect.setAttribute("class", CLASS_HIGHLIGHT_AREA);\n highlightAreaSVGLine.setAttribute("style", "stroke-linecap: round; stroke-width: ".concat(underlineThickness * scale, "; stroke: rgb(").concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ") !important; stroke-opacity: ").concat(opacity, " !important"));\n highlightAreaSVGLine.scale = scale;\n /*\n highlightAreaSVGLine.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width,\n };\n */\n\n if (annotationFlag) {\n highlightAreaSVGLine.rect = {\n height: ANNOTATION_WIDTH,\n //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4,\n left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH,\n top: rangeAnnotationBoundingClientRect.top - yOffset,\n width: ANNOTATION_WIDTH\n };\n } else {\n highlightAreaSVGLine.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width\n };\n }\n\n var lineOffset = highlightAreaSVGLine.rect.width > roundedCorner ? roundedCorner : 0;\n highlightAreaSVGLine.setAttribute("x1", "".concat((highlightAreaSVGLine.rect.left + lineOffset) * scale));\n highlightAreaSVGLine.setAttribute("x2", "".concat((highlightAreaSVGLine.rect.left + highlightAreaSVGLine.rect.width - lineOffset) * scale));\n var y = (highlightAreaSVGLine.rect.top + highlightAreaSVGLine.rect.height - underlineThickness / 2) * scale;\n highlightAreaSVGLine.setAttribute("y1", "".concat(y));\n highlightAreaSVGLine.setAttribute("y2", "".concat(y));\n highlightAreaSVGLine.setAttribute("height", "".concat(highlightAreaSVGLine.rect.height * scale));\n highlightAreaSVGLine.setAttribute("width", "".concat(highlightAreaSVGLine.rect.width * scale));\n highlightAreaSVGDocFrag.appendChild(highlightAreaSVGLine);\n }\n\n if (drawStrikeThrough) {\n var _highlightAreaSVGLine = document.createElementNS(SVG_XML_NAMESPACE, "line");\n\n highlightAreaSVGRect.setAttribute("class", CLASS_HIGHLIGHT_AREA);\n\n _highlightAreaSVGLine.setAttribute("style", "stroke-linecap: butt; stroke-width: ".concat(strikeThroughLineThickness * scale, "; stroke: rgb(").concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ") !important; stroke-opacity: ").concat(opacity, " !important"));\n\n _highlightAreaSVGLine.scale = scale;\n /*\n highlightAreaSVGLine.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width,\n };\n */\n\n if (annotationFlag) {\n _highlightAreaSVGLine.rect = {\n height: ANNOTATION_WIDTH,\n //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4,\n left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH,\n top: rangeAnnotationBoundingClientRect.top - yOffset,\n width: ANNOTATION_WIDTH\n };\n } else {\n _highlightAreaSVGLine.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width\n };\n }\n\n _highlightAreaSVGLine.setAttribute("x1", "".concat(_highlightAreaSVGLine.rect.left * scale));\n\n _highlightAreaSVGLine.setAttribute("x2", "".concat((_highlightAreaSVGLine.rect.left + _highlightAreaSVGLine.rect.width) * scale));\n\n var _lineOffset = _highlightAreaSVGLine.rect.height / 2;\n\n var _y = (_highlightAreaSVGLine.rect.top + _lineOffset) * scale;\n\n _highlightAreaSVGLine.setAttribute("y1", "".concat(_y));\n\n _highlightAreaSVGLine.setAttribute("y2", "".concat(_y));\n\n _highlightAreaSVGLine.setAttribute("height", "".concat(_highlightAreaSVGLine.rect.height * scale));\n\n _highlightAreaSVGLine.setAttribute("width", "".concat(_highlightAreaSVGLine.rect.width * scale));\n\n highlightAreaSVGDocFrag.appendChild(_highlightAreaSVGLine);\n }\n } else {\n var highlightArea = document.createElement("div");\n highlightArea.setAttribute("class", CLASS_HIGHLIGHT_AREA);\n\n if (DEBUG_VISUALS) {\n var rgb = Math.round(0xffffff * Math.random());\n var r = rgb >> 16;\n var g = rgb >> 8 & 255;\n var b = rgb & 255;\n extra = "outline-color: rgb(".concat(r, ", ").concat(g, ", ").concat(b, "); outline-style: solid; outline-width: 1px; outline-offset: -1px;");\n } else {\n if (drawUnderline) {\n extra += "border-bottom: ".concat(underlineThickness * scale, "px solid rgba(").concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ", ").concat(opacity, ") !important");\n }\n }\n\n highlightArea.setAttribute("style", "border-radius: ".concat(roundedCorner, "px !important; background-color: rgba(").concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ", ").concat(opacity, ") !important; ").concat(extra));\n highlightArea.style.setProperty("pointer-events", "none");\n highlightArea.style.position = paginated ? "fixed" : "absolute";\n highlightArea.scale = scale;\n /*\n highlightArea.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width,\n };\n */\n\n if (annotationFlag) {\n highlightArea.rect = {\n height: ANNOTATION_WIDTH,\n //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4,\n left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH,\n top: rangeAnnotationBoundingClientRect.top - yOffset,\n width: ANNOTATION_WIDTH\n };\n } else {\n highlightArea.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width\n };\n }\n\n highlightArea.style.width = "".concat(highlightArea.rect.width * scale, "px");\n highlightArea.style.height = "".concat(highlightArea.rect.height * scale, "px");\n highlightArea.style.left = "".concat(highlightArea.rect.left * scale, "px");\n highlightArea.style.top = "".concat(highlightArea.rect.top * scale, "px");\n highlightParent.append(highlightArea);\n\n if (!DEBUG_VISUALS && drawStrikeThrough) {\n //if (drawStrikeThrough) {\n var highlightAreaLine = document.createElement("div");\n highlightAreaLine.setAttribute("class", CLASS_HIGHLIGHT_AREA);\n highlightAreaLine.setAttribute("style", "background-color: rgba(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ", ").concat(opacity, ") !important;"));\n highlightAreaLine.style.setProperty("pointer-events", "none");\n highlightAreaLine.style.position = paginated ? "fixed" : "absolute";\n highlightAreaLine.scale = scale;\n /*\n highlightAreaLine.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width,\n };\n */\n\n if (annotationFlag) {\n highlightAreaLine.rect = {\n height: ANNOTATION_WIDTH,\n //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4,\n left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH,\n top: rangeAnnotationBoundingClientRect.top - yOffset,\n width: ANNOTATION_WIDTH\n };\n } else {\n highlightAreaLine.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width\n };\n }\n\n highlightAreaLine.style.width = "".concat(highlightAreaLine.rect.width * scale, "px");\n highlightAreaLine.style.height = "".concat(strikeThroughLineThickness * scale, "px");\n highlightAreaLine.style.left = "".concat(highlightAreaLine.rect.left * scale, "px");\n highlightAreaLine.style.top = "".concat((highlightAreaLine.rect.top + highlightAreaLine.rect.height / 2 - strikeThroughLineThickness / 2) * scale, "px");\n highlightParent.append(highlightAreaLine);\n }\n }\n\n if (annotationFlag) {\n break;\n }\n }\n } catch (err) {\n _iterator17.e(err);\n } finally {\n _iterator17.f();\n }\n\n if (useSVG && highlightAreaSVGDocFrag) {\n var highlightAreaSVG = document.createElementNS(SVG_XML_NAMESPACE, "svg");\n highlightAreaSVG.setAttribute("pointer-events", "none");\n highlightAreaSVG.style.position = paginated ? "fixed" : "absolute";\n highlightAreaSVG.style.overflow = "visible";\n highlightAreaSVG.style.left = "0";\n highlightAreaSVG.style.top = "0";\n highlightAreaSVG.append(highlightAreaSVGDocFrag);\n highlightParent.append(highlightAreaSVG);\n }\n\n var highlightBounding = document.createElement("div");\n\n if (annotationFlag) {\n highlightBounding.setAttribute("class", CLASS_ANNOTATION_BOUNDING_AREA);\n highlightBounding.setAttribute("style", "border-radius: ".concat(roundedCorner, "px !important; background-color: rgba(").concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ", ").concat(opacity, ") !important; ").concat(extra));\n } else {\n highlightBounding.setAttribute("class", CLASS_HIGHLIGHT_BOUNDING_AREA);\n }\n\n highlightBounding.style.setProperty("pointer-events", "none");\n highlightBounding.style.position = paginated ? "fixed" : "absolute";\n highlightBounding.scale = scale;\n\n if (DEBUG_VISUALS) {\n highlightBounding.setAttribute("style", "outline-color: magenta; outline-style: solid; outline-width: 1px; outline-offset: -1px;");\n }\n\n if (annotationFlag) {\n highlightBounding.rect = {\n height: ANNOTATION_WIDTH,\n //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4,\n left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH,\n top: rangeAnnotationBoundingClientRect.top - yOffset,\n width: ANNOTATION_WIDTH\n };\n } else {\n var rangeBoundingClientRect = range.getBoundingClientRect();\n highlightBounding.rect = {\n height: rangeBoundingClientRect.height,\n left: rangeBoundingClientRect.left - xOffset,\n top: rangeBoundingClientRect.top - yOffset,\n width: rangeBoundingClientRect.width\n };\n }\n\n highlightBounding.style.width = "".concat(highlightBounding.rect.width * scale, "px");\n highlightBounding.style.height = "".concat(highlightBounding.rect.height * scale, "px");\n highlightBounding.style.left = "".concat(highlightBounding.rect.left * scale, "px");\n highlightBounding.style.top = "".concat(highlightBounding.rect.top * scale, "px");\n highlightParent.append(highlightBounding);\n highlightsContainer.append(highlightParent);\n return highlightParent;\n}\n\nfunction createOrderedRange(startNode, startOffset, endNode, endOffset) {\n var range = new Range();\n range.setStart(startNode, startOffset);\n range.setEnd(endNode, endOffset);\n\n if (!range.collapsed) {\n return range;\n }\n\n console.log(">>> createOrderedRange COLLAPSED ... RANGE REVERSE?");\n var rangeReverse = new Range();\n rangeReverse.setStart(endNode, endOffset);\n rangeReverse.setEnd(startNode, startOffset);\n\n if (!rangeReverse.collapsed) {\n console.log(">>> createOrderedRange RANGE REVERSE OK.");\n return range;\n }\n\n console.log(">>> createOrderedRange RANGE REVERSE ALSO COLLAPSED?!");\n return undefined;\n}\n\nfunction convertRange(range, getCssSelector, computeElementCFI) {\n var startIsElement = range.startContainer.nodeType === Node.ELEMENT_NODE;\n var startContainerElement = startIsElement ? range.startContainer : range.startContainer.parentNode && range.startContainer.parentNode.nodeType === Node.ELEMENT_NODE ? range.startContainer.parentNode : undefined;\n\n if (!startContainerElement) {\n return undefined;\n }\n\n var startContainerChildTextNodeIndex = startIsElement ? -1 : Array.from(startContainerElement.childNodes).indexOf(range.startContainer);\n\n if (startContainerChildTextNodeIndex < -1) {\n return undefined;\n }\n\n var startContainerElementCssSelector = getCssSelector(startContainerElement);\n var endIsElement = range.endContainer.nodeType === Node.ELEMENT_NODE;\n var endContainerElement = endIsElement ? range.endContainer : range.endContainer.parentNode && range.endContainer.parentNode.nodeType === Node.ELEMENT_NODE ? range.endContainer.parentNode : undefined;\n\n if (!endContainerElement) {\n return undefined;\n }\n\n var endContainerChildTextNodeIndex = endIsElement ? -1 : Array.from(endContainerElement.childNodes).indexOf(range.endContainer);\n\n if (endContainerChildTextNodeIndex < -1) {\n return undefined;\n }\n\n var endContainerElementCssSelector = getCssSelector(endContainerElement);\n var commonElementAncestor = getCommonAncestorElement(range.startContainer, range.endContainer);\n\n if (!commonElementAncestor) {\n console.log("^^^ NO RANGE COMMON ANCESTOR?!");\n return undefined;\n }\n\n if (range.commonAncestorContainer) {\n var rangeCommonAncestorElement = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE ? range.commonAncestorContainer : range.commonAncestorContainer.parentNode;\n\n if (rangeCommonAncestorElement && rangeCommonAncestorElement.nodeType === Node.ELEMENT_NODE) {\n if (commonElementAncestor !== rangeCommonAncestorElement) {\n console.log(">>>>>> COMMON ANCESTOR CONTAINER DIFF??!");\n console.log(getCssSelector(commonElementAncestor));\n console.log(getCssSelector(rangeCommonAncestorElement));\n }\n }\n }\n\n var rootElementCfi = computeElementCFI(commonElementAncestor);\n var startElementCfi = computeElementCFI(startContainerElement);\n var endElementCfi = computeElementCFI(endContainerElement);\n var cfi;\n\n if (rootElementCfi && startElementCfi && endElementCfi) {\n var startElementOrTextCfi = startElementCfi;\n\n if (!startIsElement) {\n var startContainerChildTextNodeIndexForCfi = getChildTextNodeCfiIndex(startContainerElement, range.startContainer);\n startElementOrTextCfi = startElementCfi + "/" + startContainerChildTextNodeIndexForCfi + ":" + range.startOffset;\n } else {\n if (range.startOffset >= 0 && range.startOffset < startContainerElement.childNodes.length) {\n var childNode = startContainerElement.childNodes[range.startOffset];\n\n if (childNode.nodeType === Node.ELEMENT_NODE) {\n startElementOrTextCfi = startElementCfi + "/" + (range.startOffset + 1) * 2;\n } else {\n var cfiTextNodeIndex = getChildTextNodeCfiIndex(startContainerElement, childNode);\n startElementOrTextCfi = startElementCfi + "/" + cfiTextNodeIndex;\n }\n } else {\n var cfiIndexOfLastElement = startContainerElement.childElementCount * 2;\n var lastChildNode = startContainerElement.childNodes[startContainerElement.childNodes.length - 1];\n\n if (lastChildNode.nodeType === Node.ELEMENT_NODE) {\n startElementOrTextCfi = startElementCfi + "/" + (cfiIndexOfLastElement + 1);\n } else {\n startElementOrTextCfi = startElementCfi + "/" + (cfiIndexOfLastElement + 2);\n }\n }\n }\n\n var endElementOrTextCfi = endElementCfi;\n\n if (!endIsElement) {\n var endContainerChildTextNodeIndexForCfi = getChildTextNodeCfiIndex(endContainerElement, range.endContainer);\n endElementOrTextCfi = endElementCfi + "/" + endContainerChildTextNodeIndexForCfi + ":" + range.endOffset;\n } else {\n if (range.endOffset >= 0 && range.endOffset < endContainerElement.childNodes.length) {\n var _childNode = endContainerElement.childNodes[range.endOffset];\n\n if (_childNode.nodeType === Node.ELEMENT_NODE) {\n endElementOrTextCfi = endElementCfi + "/" + (range.endOffset + 1) * 2;\n } else {\n var _cfiTextNodeIndex = getChildTextNodeCfiIndex(endContainerElement, _childNode);\n\n endElementOrTextCfi = endElementCfi + "/" + _cfiTextNodeIndex;\n }\n } else {\n var _cfiIndexOfLastElement = endContainerElement.childElementCount * 2;\n\n var _lastChildNode = endContainerElement.childNodes[endContainerElement.childNodes.length - 1];\n\n if (_lastChildNode.nodeType === Node.ELEMENT_NODE) {\n endElementOrTextCfi = endElementCfi + "/" + (_cfiIndexOfLastElement + 1);\n } else {\n endElementOrTextCfi = endElementCfi + "/" + (_cfiIndexOfLastElement + 2);\n }\n }\n }\n\n cfi = rootElementCfi + "," + startElementOrTextCfi.replace(rootElementCfi, "") + "," + endElementOrTextCfi.replace(rootElementCfi, "");\n }\n\n return {\n cfi: cfi,\n endContainerChildTextNodeIndex: endContainerChildTextNodeIndex,\n endContainerElementCssSelector: endContainerElementCssSelector,\n endOffset: range.endOffset,\n startContainerChildTextNodeIndex: startContainerChildTextNodeIndex,\n startContainerElementCssSelector: startContainerElementCssSelector,\n startOffset: range.startOffset\n };\n}\n\nfunction convertRangeInfo(document, rangeInfo) {\n var startElement = document.querySelector(rangeInfo.startContainerElementCssSelector);\n\n if (!startElement) {\n console.log("^^^ convertRangeInfo NO START ELEMENT CSS SELECTOR?!");\n return undefined;\n }\n\n var startContainer = startElement;\n\n if (rangeInfo.startContainerChildTextNodeIndex >= 0) {\n if (rangeInfo.startContainerChildTextNodeIndex >= startElement.childNodes.length) {\n console.log("^^^ convertRangeInfo rangeInfo.startContainerChildTextNodeIndex >= startElement.childNodes.length?!");\n return undefined;\n }\n\n startContainer = startElement.childNodes[rangeInfo.startContainerChildTextNodeIndex];\n\n if (startContainer.nodeType !== Node.TEXT_NODE) {\n console.log("^^^ convertRangeInfo startContainer.nodeType !== Node.TEXT_NODE?!");\n return undefined;\n }\n }\n\n var endElement = document.querySelector(rangeInfo.endContainerElementCssSelector);\n\n if (!endElement) {\n console.log("^^^ convertRangeInfo NO END ELEMENT CSS SELECTOR?!");\n return undefined;\n }\n\n var endContainer = endElement;\n\n if (rangeInfo.endContainerChildTextNodeIndex >= 0) {\n if (rangeInfo.endContainerChildTextNodeIndex >= endElement.childNodes.length) {\n console.log("^^^ convertRangeInfo rangeInfo.endContainerChildTextNodeIndex >= endElement.childNodes.length?!");\n return undefined;\n }\n\n endContainer = endElement.childNodes[rangeInfo.endContainerChildTextNodeIndex];\n\n if (endContainer.nodeType !== Node.TEXT_NODE) {\n console.log("^^^ convertRangeInfo endContainer.nodeType !== Node.TEXT_NODE?!");\n return undefined;\n }\n }\n\n return createOrderedRange(startContainer, rangeInfo.startOffset, endContainer, rangeInfo.endOffset);\n}\n\nfunction frameForHighlightAnnotationMarkWithID(win, id) {\n var clientRects = frameForHighlightWithID(id);\n if (!clientRects) return;\n var topClientRect = clientRects[0];\n var maxHeight = topClientRect.height;\n\n var _iterator18 = highlight_createForOfIteratorHelper(clientRects),\n _step18;\n\n try {\n for (_iterator18.s(); !(_step18 = _iterator18.n()).done;) {\n var clientRect = _step18.value;\n if (clientRect.top < topClientRect.top) topClientRect = clientRect;\n if (clientRect.height > maxHeight) maxHeight = clientRect.height;\n }\n } catch (err) {\n _iterator18.e(err);\n } finally {\n _iterator18.f();\n }\n\n var document = win.document;\n var scrollElement = getScrollingElement(document);\n var paginated = isPaginated(document);\n var bodyRect = document.body.getBoundingClientRect();\n var yOffset;\n\n if (navigator.userAgent.match(/Android/i)) {\n yOffset = paginated ? -scrollElement.scrollTop : bodyRect.top;\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n yOffset = paginated ? 0 : bodyRect.top;\n }\n\n var newTop = topClientRect.top;\n\n if (_highlightsContainer) {\n do {\n var boundingAreas = document.getElementsByClassName(CLASS_ANNOTATION_BOUNDING_AREA);\n var found = false; //for (let i = 0, length = boundingAreas.snapshotLength; i < length; ++i) {\n\n for (var i = 0, len = boundingAreas.length | 0; i < len; i = i + 1 | 0) {\n var boundingArea = boundingAreas[i];\n\n if (Math.abs(boundingArea.rect.top - (newTop - yOffset)) < 3) {\n newTop += boundingArea.rect.height;\n found = true;\n break;\n }\n }\n } while (found);\n }\n\n topClientRect.top = newTop;\n topClientRect.height = maxHeight;\n return topClientRect;\n}\n\nfunction highlightWithID(id) {\n var i = -1;\n\n var highlight = _highlights.find(function (h, j) {\n i = j;\n return h.id === id;\n });\n\n return highlight;\n}\n\nfunction frameForHighlightWithID(id) {\n var highlight = highlightWithID(id);\n if (!highlight) return;\n var document = window.document;\n var scrollElement = getScrollingElement(document);\n var range = convertRangeInfo(document, highlight.rangeInfo);\n\n if (!range) {\n return undefined;\n }\n\n var drawUnderline = false;\n var drawStrikeThrough = false;\n var doNotMergeHorizontallyAlignedRects = drawUnderline || drawStrikeThrough; //const clientRects = DEBUG_VISUALS ? range.getClientRects() :\n\n var clientRects = highlight_getClientRectsNoOverlap(range, doNotMergeHorizontallyAlignedRects);\n return clientRects;\n}\n\nfunction rangeInfo2Location(rangeInfo) {\n return {\n cssSelector: rangeInfo.startContainerElementCssSelector,\n partialCfi: rangeInfo.cfi,\n domRange: {\n start: {\n cssSelector: rangeInfo.startContainerElementCssSelector,\n textNodeIndex: rangeInfo.startContainerChildTextNodeIndex,\n offset: rangeInfo.startOffset\n },\n end: {\n cssSelector: rangeInfo.endContainerElementCssSelector,\n textNodeIndex: rangeInfo.endContainerChildTextNodeIndex,\n offset: rangeInfo.endOffset\n }\n }\n };\n}\n\nfunction location2RangeInfo(location) {\n var locations = location.locations;\n var domRange = locations.domRange;\n var start = domRange.start;\n var end = domRange.end;\n return {\n cfi: location.partialCfi,\n endContainerChildTextNodeIndex: end.textNodeIndex,\n endContainerElementCssSelector: end.cssSelector,\n endOffset: end.offset,\n startContainerChildTextNodeIndex: start.textNodeIndex,\n startContainerElementCssSelector: start.cssSelector,\n startOffset: start.offset\n };\n}\n\nfunction rectangleForHighlightWithID(id) {\n var highlight = highlightWithID(id);\n if (!highlight) return;\n var document = window.document;\n var scrollElement = getScrollingElement(document);\n var range = convertRangeInfo(document, highlight.rangeInfo);\n\n if (!range) {\n return undefined;\n }\n\n var drawUnderline = false;\n var drawStrikeThrough = false;\n var doNotMergeHorizontallyAlignedRects = drawUnderline || drawStrikeThrough; //const clientRects = DEBUG_VISUALS ? range.getClientRects() :\n\n var clientRects = highlight_getClientRectsNoOverlap(range, doNotMergeHorizontallyAlignedRects);\n var size = {\n screenWidth: window.outerWidth,\n screenHeight: window.outerHeight,\n left: clientRects[0].left,\n width: clientRects[0].width,\n top: clientRects[0].top,\n height: clientRects[0].height\n };\n return size;\n}\nfunction getSelectionRect() {\n try {\n var sel = window.getSelection();\n\n if (!sel) {\n return;\n }\n\n var range = sel.getRangeAt(0);\n var clientRect = range.getBoundingClientRect();\n var handleBounds = {\n screenWidth: window.outerWidth,\n screenHeight: window.outerHeight,\n left: clientRect.left,\n width: clientRect.width,\n top: clientRect.top,\n height: clientRect.height\n };\n return handleBounds;\n } catch (e) {\n return null;\n }\n}\nfunction setScrollMode(flag) {\n if (!flag) {\n document.documentElement.classList.add(CLASS_PAGINATED);\n } else {\n document.documentElement.classList.remove(CLASS_PAGINATED);\n }\n}\n/*\n if (document.addEventListener) { // IE >= 9; other browsers\n document.addEventListener(\'contextmenu\', function(e) {\n //alert("You\'ve tried to open context menu"); //here you draw your own menu\n //e.preventDefault();\n //let getCssSelector = fullQualifiedSelector;\n \n\t\t\tlet str = window.getSelection();\n\t\t\tlet selectionInfo = getCurrentSelectionInfo();\n\t\t\tlet pos = createHighlight(selectionInfo,{red:10,green:50,blue:230},true);\n\t\t\tlet ret2 = createAnnotation(pos.id);\n\t\t\t\n }, false);\n } else { // IE < 9\n document.attachEvent(\'oncontextmenu\', function() {\n alert("You\'ve tried to open context menu");\n window.event.returnValue = false;\n });\n }\n*/\n// EXTERNAL MODULE: ./node_modules/css-selector-generator/build/index.js\nvar build = __webpack_require__(4766);\n;// CONCATENATED MODULE: ./src/dom.js\n//\n// Copyright 2022 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n\n\nfunction findFirstVisibleLocator() {\n var element = findElement(document.body);\n return {\n href: "#",\n type: "application/xhtml+xml",\n locations: {\n cssSelector: (0,build.getCssSelector)(element)\n },\n text: {\n highlight: element.textContent\n }\n };\n}\n\nfunction findElement(rootElement) {\n for (var i = 0; i < rootElement.children.length; i++) {\n var child = rootElement.children[i];\n\n if (!shouldIgnoreElement(child) && isElementVisible(child)) {\n return findElement(child);\n }\n }\n\n return rootElement;\n}\n\nfunction isElementVisible(element) {\n if (readium.isFixedLayout) return true;\n\n if (element === document.body || element === document.documentElement) {\n return true;\n }\n\n if (!document || !document.documentElement || !document.body) {\n return false;\n }\n\n var rect = element.getBoundingClientRect();\n\n if (isScrollModeEnabled()) {\n return rect.bottom > 0 && rect.top < window.innerHeight;\n } else {\n return rect.right > 0 && rect.left < window.innerWidth;\n }\n}\n\nfunction shouldIgnoreElement(element) {\n var elStyle = getComputedStyle(element);\n\n if (elStyle) {\n var display = elStyle.getPropertyValue("display");\n\n if (display != "block") {\n return true;\n } // Cannot be relied upon, because web browser engine reports invisible when out of view in\n // scrolled columns!\n // const visibility = elStyle.getPropertyValue("visibility");\n // if (visibility === "hidden") {\n // return false;\n // }\n\n\n var opacity = elStyle.getPropertyValue("opacity");\n\n if (opacity === "0") {\n return true;\n }\n }\n\n return false;\n}\n// EXTERNAL MODULE: ./node_modules/string.prototype.matchall/index.js\nvar string_prototype_matchall = __webpack_require__(4956);\nvar string_prototype_matchall_default = /*#__PURE__*/__webpack_require__.n(string_prototype_matchall);\n;// CONCATENATED MODULE: ./src/selection.js\n//\n// Copyright 2021 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n\n\n // Polyfill for Android API 26\n\n\nstring_prototype_matchall_default().shim();\nvar selection_debug = true; // Notify native code that the selection changes.\n\nwindow.addEventListener("load", function () {\n var isSelecting = false;\n document.addEventListener("selectionchange", function () {\n var collapsed = window.getSelection().isCollapsed;\n\n if (collapsed && isSelecting) {\n isSelecting = false;\n Android.onSelectionEnd(); // Snaps the current column in case the user shifted the scroll by dragging the text selection.\n\n snapCurrentOffset();\n } else if (!collapsed && !isSelecting) {\n isSelecting = true;\n Android.onSelectionStart();\n }\n });\n}, false);\nfunction getCurrentSelection() {\n var text = getCurrentSelectionText();\n\n if (!text) {\n return null;\n }\n\n var rect = selection_getSelectionRect();\n return {\n text: text,\n rect: rect\n };\n}\n\nfunction selection_getSelectionRect() {\n try {\n var sel = window.getSelection();\n\n if (!sel) {\n return;\n }\n\n var range = sel.getRangeAt(0);\n return toNativeRect(range.getBoundingClientRect());\n } catch (e) {\n logError(e);\n return null;\n }\n}\n\nfunction getCurrentSelectionText() {\n var selection = window.getSelection();\n\n if (!selection) {\n return undefined;\n }\n\n if (selection.isCollapsed) {\n return undefined;\n }\n\n var highlight = selection.toString();\n var cleanHighlight = highlight.trim().replace(/\\n/g, " ").replace(/\\s\\s+/g, " ");\n\n if (cleanHighlight.length === 0) {\n return undefined;\n }\n\n if (!selection.anchorNode || !selection.focusNode) {\n return undefined;\n }\n\n var range = selection.rangeCount === 1 ? selection.getRangeAt(0) : selection_createOrderedRange(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);\n\n if (!range || range.collapsed) {\n selection_log("$$$$$$$$$$$$$$$$$ CANNOT GET NON-COLLAPSED SELECTION RANGE?!");\n return undefined;\n }\n\n var text = document.body.textContent;\n var textRange = text_range_TextRange.fromRange(range).relativeTo(document.body);\n var start = textRange.start.offset;\n var end = textRange.end.offset;\n var snippetLength = 200; // Compute the text before the highlight, ignoring the first "word", which might be cut.\n\n var before = text.slice(Math.max(0, start - snippetLength), start);\n var firstWordStart = before.search(/(?:[\\0-@\\[-`\\{-\\xA9\\xAB-\\xB4\\xB6-\\xB9\\xBB-\\xBF\\xD7\\xF7\\u02C2-\\u02C5\\u02D2-\\u02DF\\u02E5-\\u02EB\\u02ED\\u02EF-\\u036F\\u0375\\u0378\\u0379\\u037E\\u0380-\\u0385\\u0387\\u038B\\u038D\\u03A2\\u03F6\\u0482-\\u0489\\u0530\\u0557\\u0558\\u055A-\\u055F\\u0589-\\u05CF\\u05EB-\\u05EE\\u05F3-\\u061F\\u064B-\\u066D\\u0670\\u06D4\\u06D6-\\u06E4\\u06E7-\\u06ED\\u06F0-\\u06F9\\u06FD\\u06FE\\u0700-\\u070F\\u0711\\u0730-\\u074C\\u07A6-\\u07B0\\u07B2-\\u07C9\\u07EB-\\u07F3\\u07F6-\\u07F9\\u07FB-\\u07FF\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u083F\\u0859-\\u085F\\u086B-\\u086F\\u0888\\u088F-\\u089F\\u08CA-\\u0903\\u093A-\\u093C\\u093E-\\u094F\\u0951-\\u0957\\u0962-\\u0970\\u0981-\\u0984\\u098D\\u098E\\u0991\\u0992\\u09A9\\u09B1\\u09B3-\\u09B5\\u09BA-\\u09BC\\u09BE-\\u09CD\\u09CF-\\u09DB\\u09DE\\u09E2-\\u09EF\\u09F2-\\u09FB\\u09FD-\\u0A04\\u0A0B-\\u0A0E\\u0A11\\u0A12\\u0A29\\u0A31\\u0A34\\u0A37\\u0A3A-\\u0A58\\u0A5D\\u0A5F-\\u0A71\\u0A75-\\u0A84\\u0A8E\\u0A92\\u0AA9\\u0AB1\\u0AB4\\u0ABA-\\u0ABC\\u0ABE-\\u0ACF\\u0AD1-\\u0ADF\\u0AE2-\\u0AF8\\u0AFA-\\u0B04\\u0B0D\\u0B0E\\u0B11\\u0B12\\u0B29\\u0B31\\u0B34\\u0B3A-\\u0B3C\\u0B3E-\\u0B5B\\u0B5E\\u0B62-\\u0B70\\u0B72-\\u0B82\\u0B84\\u0B8B-\\u0B8D\\u0B91\\u0B96-\\u0B98\\u0B9B\\u0B9D\\u0BA0-\\u0BA2\\u0BA5-\\u0BA7\\u0BAB-\\u0BAD\\u0BBA-\\u0BCF\\u0BD1-\\u0C04\\u0C0D\\u0C11\\u0C29\\u0C3A-\\u0C3C\\u0C3E-\\u0C57\\u0C5B\\u0C5C\\u0C5E\\u0C5F\\u0C62-\\u0C7F\\u0C81-\\u0C84\\u0C8D\\u0C91\\u0CA9\\u0CB4\\u0CBA-\\u0CBC\\u0CBE-\\u0CDC\\u0CDF\\u0CE2-\\u0CF0\\u0CF3-\\u0D03\\u0D0D\\u0D11\\u0D3B\\u0D3C\\u0D3E-\\u0D4D\\u0D4F-\\u0D53\\u0D57-\\u0D5E\\u0D62-\\u0D79\\u0D80-\\u0D84\\u0D97-\\u0D99\\u0DB2\\u0DBC\\u0DBE\\u0DBF\\u0DC7-\\u0E00\\u0E31\\u0E34-\\u0E3F\\u0E47-\\u0E80\\u0E83\\u0E85\\u0E8B\\u0EA4\\u0EA6\\u0EB1\\u0EB4-\\u0EBC\\u0EBE\\u0EBF\\u0EC5\\u0EC7-\\u0EDB\\u0EE0-\\u0EFF\\u0F01-\\u0F3F\\u0F48\\u0F6D-\\u0F87\\u0F8D-\\u0FFF\\u102B-\\u103E\\u1040-\\u104F\\u1056-\\u1059\\u105E-\\u1060\\u1062-\\u1064\\u1067-\\u106D\\u1071-\\u1074\\u1082-\\u108D\\u108F-\\u109F\\u10C6\\u10C8-\\u10CC\\u10CE\\u10CF\\u10FB\\u1249\\u124E\\u124F\\u1257\\u1259\\u125E\\u125F\\u1289\\u128E\\u128F\\u12B1\\u12B6\\u12B7\\u12BF\\u12C1\\u12C6\\u12C7\\u12D7\\u1311\\u1316\\u1317\\u135B-\\u137F\\u1390-\\u139F\\u13F6\\u13F7\\u13FE-\\u1400\\u166D\\u166E\\u1680\\u169B-\\u169F\\u16EB-\\u16F0\\u16F9-\\u16FF\\u1712-\\u171E\\u1732-\\u173F\\u1752-\\u175F\\u176D\\u1771-\\u177F\\u17B4-\\u17D6\\u17D8-\\u17DB\\u17DD-\\u181F\\u1879-\\u187F\\u1885\\u1886\\u18A9\\u18AB-\\u18AF\\u18F6-\\u18FF\\u191F-\\u194F\\u196E\\u196F\\u1975-\\u197F\\u19AC-\\u19AF\\u19CA-\\u19FF\\u1A17-\\u1A1F\\u1A55-\\u1AA6\\u1AA8-\\u1B04\\u1B34-\\u1B44\\u1B4D-\\u1B82\\u1BA1-\\u1BAD\\u1BB0-\\u1BB9\\u1BE6-\\u1BFF\\u1C24-\\u1C4C\\u1C50-\\u1C59\\u1C7E\\u1C7F\\u1C89-\\u1C8F\\u1CBB\\u1CBC\\u1CC0-\\u1CE8\\u1CED\\u1CF4\\u1CF7-\\u1CF9\\u1CFB-\\u1CFF\\u1DC0-\\u1DFF\\u1F16\\u1F17\\u1F1E\\u1F1F\\u1F46\\u1F47\\u1F4E\\u1F4F\\u1F58\\u1F5A\\u1F5C\\u1F5E\\u1F7E\\u1F7F\\u1FB5\\u1FBD\\u1FBF-\\u1FC1\\u1FC5\\u1FCD-\\u1FCF\\u1FD4\\u1FD5\\u1FDC-\\u1FDF\\u1FED-\\u1FF1\\u1FF5\\u1FFD-\\u2070\\u2072-\\u207E\\u2080-\\u208F\\u209D-\\u2101\\u2103-\\u2106\\u2108\\u2109\\u2114\\u2116-\\u2118\\u211E-\\u2123\\u2125\\u2127\\u2129\\u212E\\u213A\\u213B\\u2140-\\u2144\\u214A-\\u214D\\u214F-\\u2182\\u2185-\\u2BFF\\u2CE5-\\u2CEA\\u2CEF-\\u2CF1\\u2CF4-\\u2CFF\\u2D26\\u2D28-\\u2D2C\\u2D2E\\u2D2F\\u2D68-\\u2D6E\\u2D70-\\u2D7F\\u2D97-\\u2D9F\\u2DA7\\u2DAF\\u2DB7\\u2DBF\\u2DC7\\u2DCF\\u2DD7\\u2DDF-\\u2E2E\\u2E30-\\u3004\\u3007-\\u3030\\u3036-\\u303A\\u303D-\\u3040\\u3097-\\u309C\\u30A0\\u30FB\\u3100-\\u3104\\u3130\\u318F-\\u319F\\u31C0-\\u31EF\\u3200-\\u33FF\\u4DC0-\\u4DFF\\uA48D-\\uA4CF\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA620-\\uA629\\uA62C-\\uA63F\\uA66F-\\uA67E\\uA69E\\uA69F\\uA6E6-\\uA716\\uA720\\uA721\\uA789\\uA78A\\uA7CB-\\uA7CF\\uA7D2\\uA7D4\\uA7DA-\\uA7F1\\uA802\\uA806\\uA80B\\uA823-\\uA83F\\uA874-\\uA881\\uA8B4-\\uA8F1\\uA8F8-\\uA8FA\\uA8FC\\uA8FF-\\uA909\\uA926-\\uA92F\\uA947-\\uA95F\\uA97D-\\uA983\\uA9B3-\\uA9CE\\uA9D0-\\uA9DF\\uA9E5\\uA9F0-\\uA9F9\\uA9FF\\uAA29-\\uAA3F\\uAA43\\uAA4C-\\uAA5F\\uAA77-\\uAA79\\uAA7B-\\uAA7D\\uAAB0\\uAAB2-\\uAAB4\\uAAB7\\uAAB8\\uAABE\\uAABF\\uAAC1\\uAAC3-\\uAADA\\uAADE\\uAADF\\uAAEB-\\uAAF1\\uAAF5-\\uAB00\\uAB07\\uAB08\\uAB0F\\uAB10\\uAB17-\\uAB1F\\uAB27\\uAB2F\\uAB5B\\uAB6A-\\uAB6F\\uABE3-\\uABFF\\uD7A4-\\uD7AF\\uD7C7-\\uD7CA\\uD7FC-\\uD7FF\\uE000-\\uF8FF\\uFA6E\\uFA6F\\uFADA-\\uFAFF\\uFB07-\\uFB12\\uFB18-\\uFB1C\\uFB1E\\uFB29\\uFB37\\uFB3D\\uFB3F\\uFB42\\uFB45\\uFBB2-\\uFBD2\\uFD3E-\\uFD4F\\uFD90\\uFD91\\uFDC8-\\uFDEF\\uFDFC-\\uFE6F\\uFE75\\uFEFD-\\uFF20\\uFF3B-\\uFF40\\uFF5B-\\uFF65\\uFFBF-\\uFFC1\\uFFC8\\uFFC9\\uFFD0\\uFFD1\\uFFD8\\uFFD9\\uFFDD-\\uFFFF]|\\uD800[\\uDC0C\\uDC27\\uDC3B\\uDC3E\\uDC4E\\uDC4F\\uDC5E-\\uDC7F\\uDCFB-\\uDE7F\\uDE9D-\\uDE9F\\uDED1-\\uDEFF\\uDF20-\\uDF2C\\uDF41\\uDF4A-\\uDF4F\\uDF76-\\uDF7F\\uDF9E\\uDF9F\\uDFC4-\\uDFC7\\uDFD0-\\uDFFF]|\\uD801[\\uDC9E-\\uDCAF\\uDCD4-\\uDCD7\\uDCFC-\\uDCFF\\uDD28-\\uDD2F\\uDD64-\\uDD6F\\uDD7B\\uDD8B\\uDD93\\uDD96\\uDDA2\\uDDB2\\uDDBA\\uDDBD-\\uDDFF\\uDF37-\\uDF3F\\uDF56-\\uDF5F\\uDF68-\\uDF7F\\uDF86\\uDFB1\\uDFBB-\\uDFFF]|\\uD802[\\uDC06\\uDC07\\uDC09\\uDC36\\uDC39-\\uDC3B\\uDC3D\\uDC3E\\uDC56-\\uDC5F\\uDC77-\\uDC7F\\uDC9F-\\uDCDF\\uDCF3\\uDCF6-\\uDCFF\\uDD16-\\uDD1F\\uDD3A-\\uDD7F\\uDDB8-\\uDDBD\\uDDC0-\\uDDFF\\uDE01-\\uDE0F\\uDE14\\uDE18\\uDE36-\\uDE5F\\uDE7D-\\uDE7F\\uDE9D-\\uDEBF\\uDEC8\\uDEE5-\\uDEFF\\uDF36-\\uDF3F\\uDF56-\\uDF5F\\uDF73-\\uDF7F\\uDF92-\\uDFFF]|\\uD803[\\uDC49-\\uDC7F\\uDCB3-\\uDCBF\\uDCF3-\\uDCFF\\uDD24-\\uDE7F\\uDEAA-\\uDEAF\\uDEB2-\\uDEFF\\uDF1D-\\uDF26\\uDF28-\\uDF2F\\uDF46-\\uDF6F\\uDF82-\\uDFAF\\uDFC5-\\uDFDF\\uDFF7-\\uDFFF]|\\uD804[\\uDC00-\\uDC02\\uDC38-\\uDC70\\uDC73\\uDC74\\uDC76-\\uDC82\\uDCB0-\\uDCCF\\uDCE9-\\uDD02\\uDD27-\\uDD43\\uDD45\\uDD46\\uDD48-\\uDD4F\\uDD73-\\uDD75\\uDD77-\\uDD82\\uDDB3-\\uDDC0\\uDDC5-\\uDDD9\\uDDDB\\uDDDD-\\uDDFF\\uDE12\\uDE2C-\\uDE7F\\uDE87\\uDE89\\uDE8E\\uDE9E\\uDEA9-\\uDEAF\\uDEDF-\\uDF04\\uDF0D\\uDF0E\\uDF11\\uDF12\\uDF29\\uDF31\\uDF34\\uDF3A-\\uDF3C\\uDF3E-\\uDF4F\\uDF51-\\uDF5C\\uDF62-\\uDFFF]|\\uD805[\\uDC35-\\uDC46\\uDC4B-\\uDC5E\\uDC62-\\uDC7F\\uDCB0-\\uDCC3\\uDCC6\\uDCC8-\\uDD7F\\uDDAF-\\uDDD7\\uDDDC-\\uDDFF\\uDE30-\\uDE43\\uDE45-\\uDE7F\\uDEAB-\\uDEB7\\uDEB9-\\uDEFF\\uDF1B-\\uDF3F\\uDF47-\\uDFFF]|\\uD806[\\uDC2C-\\uDC9F\\uDCE0-\\uDCFE\\uDD07\\uDD08\\uDD0A\\uDD0B\\uDD14\\uDD17\\uDD30-\\uDD3E\\uDD40\\uDD42-\\uDD9F\\uDDA8\\uDDA9\\uDDD1-\\uDDE0\\uDDE2\\uDDE4-\\uDDFF\\uDE01-\\uDE0A\\uDE33-\\uDE39\\uDE3B-\\uDE4F\\uDE51-\\uDE5B\\uDE8A-\\uDE9C\\uDE9E-\\uDEAF\\uDEF9-\\uDFFF]|\\uD807[\\uDC09\\uDC2F-\\uDC3F\\uDC41-\\uDC71\\uDC90-\\uDCFF\\uDD07\\uDD0A\\uDD31-\\uDD45\\uDD47-\\uDD5F\\uDD66\\uDD69\\uDD8A-\\uDD97\\uDD99-\\uDEDF\\uDEF3-\\uDFAF\\uDFB1-\\uDFFF]|\\uD808[\\uDF9A-\\uDFFF]|\\uD809[\\uDC00-\\uDC7F\\uDD44-\\uDFFF]|[\\uD80A\\uD80E-\\uD810\\uD812-\\uD819\\uD824-\\uD82A\\uD82D\\uD82E\\uD830-\\uD834\\uD836\\uD83C-\\uD83F\\uD87B-\\uD87D\\uD87F\\uD885-\\uDBFF][\\uDC00-\\uDFFF]|\\uD80B[\\uDC00-\\uDF8F\\uDFF1-\\uDFFF]|\\uD80D[\\uDC2F-\\uDFFF]|\\uD811[\\uDE47-\\uDFFF]|\\uD81A[\\uDE39-\\uDE3F\\uDE5F-\\uDE6F\\uDEBF-\\uDECF\\uDEEE-\\uDEFF\\uDF30-\\uDF3F\\uDF44-\\uDF62\\uDF78-\\uDF7C\\uDF90-\\uDFFF]|\\uD81B[\\uDC00-\\uDE3F\\uDE80-\\uDEFF\\uDF4B-\\uDF4F\\uDF51-\\uDF92\\uDFA0-\\uDFDF\\uDFE2\\uDFE4-\\uDFFF]|\\uD821[\\uDFF8-\\uDFFF]|\\uD823[\\uDCD6-\\uDCFF\\uDD09-\\uDFFF]|\\uD82B[\\uDC00-\\uDFEF\\uDFF4\\uDFFC\\uDFFF]|\\uD82C[\\uDD23-\\uDD4F\\uDD53-\\uDD63\\uDD68-\\uDD6F\\uDEFC-\\uDFFF]|\\uD82F[\\uDC6B-\\uDC6F\\uDC7D-\\uDC7F\\uDC89-\\uDC8F\\uDC9A-\\uDFFF]|\\uD835[\\uDC55\\uDC9D\\uDCA0\\uDCA1\\uDCA3\\uDCA4\\uDCA7\\uDCA8\\uDCAD\\uDCBA\\uDCBC\\uDCC4\\uDD06\\uDD0B\\uDD0C\\uDD15\\uDD1D\\uDD3A\\uDD3F\\uDD45\\uDD47-\\uDD49\\uDD51\\uDEA6\\uDEA7\\uDEC1\\uDEDB\\uDEFB\\uDF15\\uDF35\\uDF4F\\uDF6F\\uDF89\\uDFA9\\uDFC3\\uDFCC-\\uDFFF]|\\uD837[\\uDC00-\\uDEFF\\uDF1F-\\uDFFF]|\\uD838[\\uDC00-\\uDCFF\\uDD2D-\\uDD36\\uDD3E-\\uDD4D\\uDD4F-\\uDE8F\\uDEAE-\\uDEBF\\uDEEC-\\uDFFF]|\\uD839[\\uDC00-\\uDFDF\\uDFE7\\uDFEC\\uDFEF\\uDFFF]|\\uD83A[\\uDCC5-\\uDCFF\\uDD44-\\uDD4A\\uDD4C-\\uDFFF]|\\uD83B[\\uDC00-\\uDDFF\\uDE04\\uDE20\\uDE23\\uDE25\\uDE26\\uDE28\\uDE33\\uDE38\\uDE3A\\uDE3C-\\uDE41\\uDE43-\\uDE46\\uDE48\\uDE4A\\uDE4C\\uDE50\\uDE53\\uDE55\\uDE56\\uDE58\\uDE5A\\uDE5C\\uDE5E\\uDE60\\uDE63\\uDE65\\uDE66\\uDE6B\\uDE73\\uDE78\\uDE7D\\uDE7F\\uDE8A\\uDE9C-\\uDEA0\\uDEA4\\uDEAA\\uDEBC-\\uDFFF]|\\uD869[\\uDEE0-\\uDEFF]|\\uD86D[\\uDF39-\\uDF3F]|\\uD86E[\\uDC1E\\uDC1F]|\\uD873[\\uDEA2-\\uDEAF]|\\uD87A[\\uDFE1-\\uDFFF]|\\uD87E[\\uDE1E-\\uDFFF]|\\uD884[\\uDF4B-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF])(?:[A-Za-z\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088E\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5D\\u0C60\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C88\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183\\u2184\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005\\u3006\\u3031-\\u3035\\u303B\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7CA\\uA7D0\\uA7D1\\uA7D3\\uA7D5-\\uA7D9\\uA7F2-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC]|\\uD800[\\uDC00-\\uDC0B\\uDC0D-\\uDC26\\uDC28-\\uDC3A\\uDC3C\\uDC3D\\uDC3F-\\uDC4D\\uDC50-\\uDC5D\\uDC80-\\uDCFA\\uDE80-\\uDE9C\\uDEA0-\\uDED0\\uDF00-\\uDF1F\\uDF2D-\\uDF40\\uDF42-\\uDF49\\uDF50-\\uDF75\\uDF80-\\uDF9D\\uDFA0-\\uDFC3\\uDFC8-\\uDFCF]|\\uD801[\\uDC00-\\uDC9D\\uDCB0-\\uDCD3\\uDCD8-\\uDCFB\\uDD00-\\uDD27\\uDD30-\\uDD63\\uDD70-\\uDD7A\\uDD7C-\\uDD8A\\uDD8C-\\uDD92\\uDD94\\uDD95\\uDD97-\\uDDA1\\uDDA3-\\uDDB1\\uDDB3-\\uDDB9\\uDDBB\\uDDBC\\uDE00-\\uDF36\\uDF40-\\uDF55\\uDF60-\\uDF67\\uDF80-\\uDF85\\uDF87-\\uDFB0\\uDFB2-\\uDFBA]|\\uD802[\\uDC00-\\uDC05\\uDC08\\uDC0A-\\uDC35\\uDC37\\uDC38\\uDC3C\\uDC3F-\\uDC55\\uDC60-\\uDC76\\uDC80-\\uDC9E\\uDCE0-\\uDCF2\\uDCF4\\uDCF5\\uDD00-\\uDD15\\uDD20-\\uDD39\\uDD80-\\uDDB7\\uDDBE\\uDDBF\\uDE00\\uDE10-\\uDE13\\uDE15-\\uDE17\\uDE19-\\uDE35\\uDE60-\\uDE7C\\uDE80-\\uDE9C\\uDEC0-\\uDEC7\\uDEC9-\\uDEE4\\uDF00-\\uDF35\\uDF40-\\uDF55\\uDF60-\\uDF72\\uDF80-\\uDF91]|\\uD803[\\uDC00-\\uDC48\\uDC80-\\uDCB2\\uDCC0-\\uDCF2\\uDD00-\\uDD23\\uDE80-\\uDEA9\\uDEB0\\uDEB1\\uDF00-\\uDF1C\\uDF27\\uDF30-\\uDF45\\uDF70-\\uDF81\\uDFB0-\\uDFC4\\uDFE0-\\uDFF6]|\\uD804[\\uDC03-\\uDC37\\uDC71\\uDC72\\uDC75\\uDC83-\\uDCAF\\uDCD0-\\uDCE8\\uDD03-\\uDD26\\uDD44\\uDD47\\uDD50-\\uDD72\\uDD76\\uDD83-\\uDDB2\\uDDC1-\\uDDC4\\uDDDA\\uDDDC\\uDE00-\\uDE11\\uDE13-\\uDE2B\\uDE80-\\uDE86\\uDE88\\uDE8A-\\uDE8D\\uDE8F-\\uDE9D\\uDE9F-\\uDEA8\\uDEB0-\\uDEDE\\uDF05-\\uDF0C\\uDF0F\\uDF10\\uDF13-\\uDF28\\uDF2A-\\uDF30\\uDF32\\uDF33\\uDF35-\\uDF39\\uDF3D\\uDF50\\uDF5D-\\uDF61]|\\uD805[\\uDC00-\\uDC34\\uDC47-\\uDC4A\\uDC5F-\\uDC61\\uDC80-\\uDCAF\\uDCC4\\uDCC5\\uDCC7\\uDD80-\\uDDAE\\uDDD8-\\uDDDB\\uDE00-\\uDE2F\\uDE44\\uDE80-\\uDEAA\\uDEB8\\uDF00-\\uDF1A\\uDF40-\\uDF46]|\\uD806[\\uDC00-\\uDC2B\\uDCA0-\\uDCDF\\uDCFF-\\uDD06\\uDD09\\uDD0C-\\uDD13\\uDD15\\uDD16\\uDD18-\\uDD2F\\uDD3F\\uDD41\\uDDA0-\\uDDA7\\uDDAA-\\uDDD0\\uDDE1\\uDDE3\\uDE00\\uDE0B-\\uDE32\\uDE3A\\uDE50\\uDE5C-\\uDE89\\uDE9D\\uDEB0-\\uDEF8]|\\uD807[\\uDC00-\\uDC08\\uDC0A-\\uDC2E\\uDC40\\uDC72-\\uDC8F\\uDD00-\\uDD06\\uDD08\\uDD09\\uDD0B-\\uDD30\\uDD46\\uDD60-\\uDD65\\uDD67\\uDD68\\uDD6A-\\uDD89\\uDD98\\uDEE0-\\uDEF2\\uDFB0]|\\uD808[\\uDC00-\\uDF99]|\\uD809[\\uDC80-\\uDD43]|\\uD80B[\\uDF90-\\uDFF0]|[\\uD80C\\uD81C-\\uD820\\uD822\\uD840-\\uD868\\uD86A-\\uD86C\\uD86F-\\uD872\\uD874-\\uD879\\uD880-\\uD883][\\uDC00-\\uDFFF]|\\uD80D[\\uDC00-\\uDC2E]|\\uD811[\\uDC00-\\uDE46]|\\uD81A[\\uDC00-\\uDE38\\uDE40-\\uDE5E\\uDE70-\\uDEBE\\uDED0-\\uDEED\\uDF00-\\uDF2F\\uDF40-\\uDF43\\uDF63-\\uDF77\\uDF7D-\\uDF8F]|\\uD81B[\\uDE40-\\uDE7F\\uDF00-\\uDF4A\\uDF50\\uDF93-\\uDF9F\\uDFE0\\uDFE1\\uDFE3]|\\uD821[\\uDC00-\\uDFF7]|\\uD823[\\uDC00-\\uDCD5\\uDD00-\\uDD08]|\\uD82B[\\uDFF0-\\uDFF3\\uDFF5-\\uDFFB\\uDFFD\\uDFFE]|\\uD82C[\\uDC00-\\uDD22\\uDD50-\\uDD52\\uDD64-\\uDD67\\uDD70-\\uDEFB]|\\uD82F[\\uDC00-\\uDC6A\\uDC70-\\uDC7C\\uDC80-\\uDC88\\uDC90-\\uDC99]|\\uD835[\\uDC00-\\uDC54\\uDC56-\\uDC9C\\uDC9E\\uDC9F\\uDCA2\\uDCA5\\uDCA6\\uDCA9-\\uDCAC\\uDCAE-\\uDCB9\\uDCBB\\uDCBD-\\uDCC3\\uDCC5-\\uDD05\\uDD07-\\uDD0A\\uDD0D-\\uDD14\\uDD16-\\uDD1C\\uDD1E-\\uDD39\\uDD3B-\\uDD3E\\uDD40-\\uDD44\\uDD46\\uDD4A-\\uDD50\\uDD52-\\uDEA5\\uDEA8-\\uDEC0\\uDEC2-\\uDEDA\\uDEDC-\\uDEFA\\uDEFC-\\uDF14\\uDF16-\\uDF34\\uDF36-\\uDF4E\\uDF50-\\uDF6E\\uDF70-\\uDF88\\uDF8A-\\uDFA8\\uDFAA-\\uDFC2\\uDFC4-\\uDFCB]|\\uD837[\\uDF00-\\uDF1E]|\\uD838[\\uDD00-\\uDD2C\\uDD37-\\uDD3D\\uDD4E\\uDE90-\\uDEAD\\uDEC0-\\uDEEB]|\\uD839[\\uDFE0-\\uDFE6\\uDFE8-\\uDFEB\\uDFED\\uDFEE\\uDFF0-\\uDFFE]|\\uD83A[\\uDC00-\\uDCC4\\uDD00-\\uDD43\\uDD4B]|\\uD83B[\\uDE00-\\uDE03\\uDE05-\\uDE1F\\uDE21\\uDE22\\uDE24\\uDE27\\uDE29-\\uDE32\\uDE34-\\uDE37\\uDE39\\uDE3B\\uDE42\\uDE47\\uDE49\\uDE4B\\uDE4D-\\uDE4F\\uDE51\\uDE52\\uDE54\\uDE57\\uDE59\\uDE5B\\uDE5D\\uDE5F\\uDE61\\uDE62\\uDE64\\uDE67-\\uDE6A\\uDE6C-\\uDE72\\uDE74-\\uDE77\\uDE79-\\uDE7C\\uDE7E\\uDE80-\\uDE89\\uDE8B-\\uDE9B\\uDEA1-\\uDEA3\\uDEA5-\\uDEA9\\uDEAB-\\uDEBB]|\\uD869[\\uDC00-\\uDEDF\\uDF00-\\uDFFF]|\\uD86D[\\uDC00-\\uDF38\\uDF40-\\uDFFF]|\\uD86E[\\uDC00-\\uDC1D\\uDC20-\\uDFFF]|\\uD873[\\uDC00-\\uDEA1\\uDEB0-\\uDFFF]|\\uD87A[\\uDC00-\\uDFE0]|\\uD87E[\\uDC00-\\uDE1D]|\\uD884[\\uDC00-\\uDF4A])/g);\n\n if (firstWordStart !== -1) {\n before = before.slice(firstWordStart + 1);\n } // Compute the text after the highlight, ignoring the last "word", which might be cut.\n\n\n var after = text.slice(end, Math.min(text.length, end + snippetLength));\n var lastWordEnd = Array.from(after.matchAll(/(?:[A-Za-z\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088E\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5D\\u0C60\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C88\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183\\u2184\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005\\u3006\\u3031-\\u3035\\u303B\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7CA\\uA7D0\\uA7D1\\uA7D3\\uA7D5-\\uA7D9\\uA7F2-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC]|\\uD800[\\uDC00-\\uDC0B\\uDC0D-\\uDC26\\uDC28-\\uDC3A\\uDC3C\\uDC3D\\uDC3F-\\uDC4D\\uDC50-\\uDC5D\\uDC80-\\uDCFA\\uDE80-\\uDE9C\\uDEA0-\\uDED0\\uDF00-\\uDF1F\\uDF2D-\\uDF40\\uDF42-\\uDF49\\uDF50-\\uDF75\\uDF80-\\uDF9D\\uDFA0-\\uDFC3\\uDFC8-\\uDFCF]|\\uD801[\\uDC00-\\uDC9D\\uDCB0-\\uDCD3\\uDCD8-\\uDCFB\\uDD00-\\uDD27\\uDD30-\\uDD63\\uDD70-\\uDD7A\\uDD7C-\\uDD8A\\uDD8C-\\uDD92\\uDD94\\uDD95\\uDD97-\\uDDA1\\uDDA3-\\uDDB1\\uDDB3-\\uDDB9\\uDDBB\\uDDBC\\uDE00-\\uDF36\\uDF40-\\uDF55\\uDF60-\\uDF67\\uDF80-\\uDF85\\uDF87-\\uDFB0\\uDFB2-\\uDFBA]|\\uD802[\\uDC00-\\uDC05\\uDC08\\uDC0A-\\uDC35\\uDC37\\uDC38\\uDC3C\\uDC3F-\\uDC55\\uDC60-\\uDC76\\uDC80-\\uDC9E\\uDCE0-\\uDCF2\\uDCF4\\uDCF5\\uDD00-\\uDD15\\uDD20-\\uDD39\\uDD80-\\uDDB7\\uDDBE\\uDDBF\\uDE00\\uDE10-\\uDE13\\uDE15-\\uDE17\\uDE19-\\uDE35\\uDE60-\\uDE7C\\uDE80-\\uDE9C\\uDEC0-\\uDEC7\\uDEC9-\\uDEE4\\uDF00-\\uDF35\\uDF40-\\uDF55\\uDF60-\\uDF72\\uDF80-\\uDF91]|\\uD803[\\uDC00-\\uDC48\\uDC80-\\uDCB2\\uDCC0-\\uDCF2\\uDD00-\\uDD23\\uDE80-\\uDEA9\\uDEB0\\uDEB1\\uDF00-\\uDF1C\\uDF27\\uDF30-\\uDF45\\uDF70-\\uDF81\\uDFB0-\\uDFC4\\uDFE0-\\uDFF6]|\\uD804[\\uDC03-\\uDC37\\uDC71\\uDC72\\uDC75\\uDC83-\\uDCAF\\uDCD0-\\uDCE8\\uDD03-\\uDD26\\uDD44\\uDD47\\uDD50-\\uDD72\\uDD76\\uDD83-\\uDDB2\\uDDC1-\\uDDC4\\uDDDA\\uDDDC\\uDE00-\\uDE11\\uDE13-\\uDE2B\\uDE80-\\uDE86\\uDE88\\uDE8A-\\uDE8D\\uDE8F-\\uDE9D\\uDE9F-\\uDEA8\\uDEB0-\\uDEDE\\uDF05-\\uDF0C\\uDF0F\\uDF10\\uDF13-\\uDF28\\uDF2A-\\uDF30\\uDF32\\uDF33\\uDF35-\\uDF39\\uDF3D\\uDF50\\uDF5D-\\uDF61]|\\uD805[\\uDC00-\\uDC34\\uDC47-\\uDC4A\\uDC5F-\\uDC61\\uDC80-\\uDCAF\\uDCC4\\uDCC5\\uDCC7\\uDD80-\\uDDAE\\uDDD8-\\uDDDB\\uDE00-\\uDE2F\\uDE44\\uDE80-\\uDEAA\\uDEB8\\uDF00-\\uDF1A\\uDF40-\\uDF46]|\\uD806[\\uDC00-\\uDC2B\\uDCA0-\\uDCDF\\uDCFF-\\uDD06\\uDD09\\uDD0C-\\uDD13\\uDD15\\uDD16\\uDD18-\\uDD2F\\uDD3F\\uDD41\\uDDA0-\\uDDA7\\uDDAA-\\uDDD0\\uDDE1\\uDDE3\\uDE00\\uDE0B-\\uDE32\\uDE3A\\uDE50\\uDE5C-\\uDE89\\uDE9D\\uDEB0-\\uDEF8]|\\uD807[\\uDC00-\\uDC08\\uDC0A-\\uDC2E\\uDC40\\uDC72-\\uDC8F\\uDD00-\\uDD06\\uDD08\\uDD09\\uDD0B-\\uDD30\\uDD46\\uDD60-\\uDD65\\uDD67\\uDD68\\uDD6A-\\uDD89\\uDD98\\uDEE0-\\uDEF2\\uDFB0]|\\uD808[\\uDC00-\\uDF99]|\\uD809[\\uDC80-\\uDD43]|\\uD80B[\\uDF90-\\uDFF0]|[\\uD80C\\uD81C-\\uD820\\uD822\\uD840-\\uD868\\uD86A-\\uD86C\\uD86F-\\uD872\\uD874-\\uD879\\uD880-\\uD883][\\uDC00-\\uDFFF]|\\uD80D[\\uDC00-\\uDC2E]|\\uD811[\\uDC00-\\uDE46]|\\uD81A[\\uDC00-\\uDE38\\uDE40-\\uDE5E\\uDE70-\\uDEBE\\uDED0-\\uDEED\\uDF00-\\uDF2F\\uDF40-\\uDF43\\uDF63-\\uDF77\\uDF7D-\\uDF8F]|\\uD81B[\\uDE40-\\uDE7F\\uDF00-\\uDF4A\\uDF50\\uDF93-\\uDF9F\\uDFE0\\uDFE1\\uDFE3]|\\uD821[\\uDC00-\\uDFF7]|\\uD823[\\uDC00-\\uDCD5\\uDD00-\\uDD08]|\\uD82B[\\uDFF0-\\uDFF3\\uDFF5-\\uDFFB\\uDFFD\\uDFFE]|\\uD82C[\\uDC00-\\uDD22\\uDD50-\\uDD52\\uDD64-\\uDD67\\uDD70-\\uDEFB]|\\uD82F[\\uDC00-\\uDC6A\\uDC70-\\uDC7C\\uDC80-\\uDC88\\uDC90-\\uDC99]|\\uD835[\\uDC00-\\uDC54\\uDC56-\\uDC9C\\uDC9E\\uDC9F\\uDCA2\\uDCA5\\uDCA6\\uDCA9-\\uDCAC\\uDCAE-\\uDCB9\\uDCBB\\uDCBD-\\uDCC3\\uDCC5-\\uDD05\\uDD07-\\uDD0A\\uDD0D-\\uDD14\\uDD16-\\uDD1C\\uDD1E-\\uDD39\\uDD3B-\\uDD3E\\uDD40-\\uDD44\\uDD46\\uDD4A-\\uDD50\\uDD52-\\uDEA5\\uDEA8-\\uDEC0\\uDEC2-\\uDEDA\\uDEDC-\\uDEFA\\uDEFC-\\uDF14\\uDF16-\\uDF34\\uDF36-\\uDF4E\\uDF50-\\uDF6E\\uDF70-\\uDF88\\uDF8A-\\uDFA8\\uDFAA-\\uDFC2\\uDFC4-\\uDFCB]|\\uD837[\\uDF00-\\uDF1E]|\\uD838[\\uDD00-\\uDD2C\\uDD37-\\uDD3D\\uDD4E\\uDE90-\\uDEAD\\uDEC0-\\uDEEB]|\\uD839[\\uDFE0-\\uDFE6\\uDFE8-\\uDFEB\\uDFED\\uDFEE\\uDFF0-\\uDFFE]|\\uD83A[\\uDC00-\\uDCC4\\uDD00-\\uDD43\\uDD4B]|\\uD83B[\\uDE00-\\uDE03\\uDE05-\\uDE1F\\uDE21\\uDE22\\uDE24\\uDE27\\uDE29-\\uDE32\\uDE34-\\uDE37\\uDE39\\uDE3B\\uDE42\\uDE47\\uDE49\\uDE4B\\uDE4D-\\uDE4F\\uDE51\\uDE52\\uDE54\\uDE57\\uDE59\\uDE5B\\uDE5D\\uDE5F\\uDE61\\uDE62\\uDE64\\uDE67-\\uDE6A\\uDE6C-\\uDE72\\uDE74-\\uDE77\\uDE79-\\uDE7C\\uDE7E\\uDE80-\\uDE89\\uDE8B-\\uDE9B\\uDEA1-\\uDEA3\\uDEA5-\\uDEA9\\uDEAB-\\uDEBB]|\\uD869[\\uDC00-\\uDEDF\\uDF00-\\uDFFF]|\\uD86D[\\uDC00-\\uDF38\\uDF40-\\uDFFF]|\\uD86E[\\uDC00-\\uDC1D\\uDC20-\\uDFFF]|\\uD873[\\uDC00-\\uDEA1\\uDEB0-\\uDFFF]|\\uD87A[\\uDC00-\\uDFE0]|\\uD87E[\\uDC00-\\uDE1D]|\\uD884[\\uDC00-\\uDF4A])(?:[\\0-@\\[-`\\{-\\xA9\\xAB-\\xB4\\xB6-\\xB9\\xBB-\\xBF\\xD7\\xF7\\u02C2-\\u02C5\\u02D2-\\u02DF\\u02E5-\\u02EB\\u02ED\\u02EF-\\u036F\\u0375\\u0378\\u0379\\u037E\\u0380-\\u0385\\u0387\\u038B\\u038D\\u03A2\\u03F6\\u0482-\\u0489\\u0530\\u0557\\u0558\\u055A-\\u055F\\u0589-\\u05CF\\u05EB-\\u05EE\\u05F3-\\u061F\\u064B-\\u066D\\u0670\\u06D4\\u06D6-\\u06E4\\u06E7-\\u06ED\\u06F0-\\u06F9\\u06FD\\u06FE\\u0700-\\u070F\\u0711\\u0730-\\u074C\\u07A6-\\u07B0\\u07B2-\\u07C9\\u07EB-\\u07F3\\u07F6-\\u07F9\\u07FB-\\u07FF\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u083F\\u0859-\\u085F\\u086B-\\u086F\\u0888\\u088F-\\u089F\\u08CA-\\u0903\\u093A-\\u093C\\u093E-\\u094F\\u0951-\\u0957\\u0962-\\u0970\\u0981-\\u0984\\u098D\\u098E\\u0991\\u0992\\u09A9\\u09B1\\u09B3-\\u09B5\\u09BA-\\u09BC\\u09BE-\\u09CD\\u09CF-\\u09DB\\u09DE\\u09E2-\\u09EF\\u09F2-\\u09FB\\u09FD-\\u0A04\\u0A0B-\\u0A0E\\u0A11\\u0A12\\u0A29\\u0A31\\u0A34\\u0A37\\u0A3A-\\u0A58\\u0A5D\\u0A5F-\\u0A71\\u0A75-\\u0A84\\u0A8E\\u0A92\\u0AA9\\u0AB1\\u0AB4\\u0ABA-\\u0ABC\\u0ABE-\\u0ACF\\u0AD1-\\u0ADF\\u0AE2-\\u0AF8\\u0AFA-\\u0B04\\u0B0D\\u0B0E\\u0B11\\u0B12\\u0B29\\u0B31\\u0B34\\u0B3A-\\u0B3C\\u0B3E-\\u0B5B\\u0B5E\\u0B62-\\u0B70\\u0B72-\\u0B82\\u0B84\\u0B8B-\\u0B8D\\u0B91\\u0B96-\\u0B98\\u0B9B\\u0B9D\\u0BA0-\\u0BA2\\u0BA5-\\u0BA7\\u0BAB-\\u0BAD\\u0BBA-\\u0BCF\\u0BD1-\\u0C04\\u0C0D\\u0C11\\u0C29\\u0C3A-\\u0C3C\\u0C3E-\\u0C57\\u0C5B\\u0C5C\\u0C5E\\u0C5F\\u0C62-\\u0C7F\\u0C81-\\u0C84\\u0C8D\\u0C91\\u0CA9\\u0CB4\\u0CBA-\\u0CBC\\u0CBE-\\u0CDC\\u0CDF\\u0CE2-\\u0CF0\\u0CF3-\\u0D03\\u0D0D\\u0D11\\u0D3B\\u0D3C\\u0D3E-\\u0D4D\\u0D4F-\\u0D53\\u0D57-\\u0D5E\\u0D62-\\u0D79\\u0D80-\\u0D84\\u0D97-\\u0D99\\u0DB2\\u0DBC\\u0DBE\\u0DBF\\u0DC7-\\u0E00\\u0E31\\u0E34-\\u0E3F\\u0E47-\\u0E80\\u0E83\\u0E85\\u0E8B\\u0EA4\\u0EA6\\u0EB1\\u0EB4-\\u0EBC\\u0EBE\\u0EBF\\u0EC5\\u0EC7-\\u0EDB\\u0EE0-\\u0EFF\\u0F01-\\u0F3F\\u0F48\\u0F6D-\\u0F87\\u0F8D-\\u0FFF\\u102B-\\u103E\\u1040-\\u104F\\u1056-\\u1059\\u105E-\\u1060\\u1062-\\u1064\\u1067-\\u106D\\u1071-\\u1074\\u1082-\\u108D\\u108F-\\u109F\\u10C6\\u10C8-\\u10CC\\u10CE\\u10CF\\u10FB\\u1249\\u124E\\u124F\\u1257\\u1259\\u125E\\u125F\\u1289\\u128E\\u128F\\u12B1\\u12B6\\u12B7\\u12BF\\u12C1\\u12C6\\u12C7\\u12D7\\u1311\\u1316\\u1317\\u135B-\\u137F\\u1390-\\u139F\\u13F6\\u13F7\\u13FE-\\u1400\\u166D\\u166E\\u1680\\u169B-\\u169F\\u16EB-\\u16F0\\u16F9-\\u16FF\\u1712-\\u171E\\u1732-\\u173F\\u1752-\\u175F\\u176D\\u1771-\\u177F\\u17B4-\\u17D6\\u17D8-\\u17DB\\u17DD-\\u181F\\u1879-\\u187F\\u1885\\u1886\\u18A9\\u18AB-\\u18AF\\u18F6-\\u18FF\\u191F-\\u194F\\u196E\\u196F\\u1975-\\u197F\\u19AC-\\u19AF\\u19CA-\\u19FF\\u1A17-\\u1A1F\\u1A55-\\u1AA6\\u1AA8-\\u1B04\\u1B34-\\u1B44\\u1B4D-\\u1B82\\u1BA1-\\u1BAD\\u1BB0-\\u1BB9\\u1BE6-\\u1BFF\\u1C24-\\u1C4C\\u1C50-\\u1C59\\u1C7E\\u1C7F\\u1C89-\\u1C8F\\u1CBB\\u1CBC\\u1CC0-\\u1CE8\\u1CED\\u1CF4\\u1CF7-\\u1CF9\\u1CFB-\\u1CFF\\u1DC0-\\u1DFF\\u1F16\\u1F17\\u1F1E\\u1F1F\\u1F46\\u1F47\\u1F4E\\u1F4F\\u1F58\\u1F5A\\u1F5C\\u1F5E\\u1F7E\\u1F7F\\u1FB5\\u1FBD\\u1FBF-\\u1FC1\\u1FC5\\u1FCD-\\u1FCF\\u1FD4\\u1FD5\\u1FDC-\\u1FDF\\u1FED-\\u1FF1\\u1FF5\\u1FFD-\\u2070\\u2072-\\u207E\\u2080-\\u208F\\u209D-\\u2101\\u2103-\\u2106\\u2108\\u2109\\u2114\\u2116-\\u2118\\u211E-\\u2123\\u2125\\u2127\\u2129\\u212E\\u213A\\u213B\\u2140-\\u2144\\u214A-\\u214D\\u214F-\\u2182\\u2185-\\u2BFF\\u2CE5-\\u2CEA\\u2CEF-\\u2CF1\\u2CF4-\\u2CFF\\u2D26\\u2D28-\\u2D2C\\u2D2E\\u2D2F\\u2D68-\\u2D6E\\u2D70-\\u2D7F\\u2D97-\\u2D9F\\u2DA7\\u2DAF\\u2DB7\\u2DBF\\u2DC7\\u2DCF\\u2DD7\\u2DDF-\\u2E2E\\u2E30-\\u3004\\u3007-\\u3030\\u3036-\\u303A\\u303D-\\u3040\\u3097-\\u309C\\u30A0\\u30FB\\u3100-\\u3104\\u3130\\u318F-\\u319F\\u31C0-\\u31EF\\u3200-\\u33FF\\u4DC0-\\u4DFF\\uA48D-\\uA4CF\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA620-\\uA629\\uA62C-\\uA63F\\uA66F-\\uA67E\\uA69E\\uA69F\\uA6E6-\\uA716\\uA720\\uA721\\uA789\\uA78A\\uA7CB-\\uA7CF\\uA7D2\\uA7D4\\uA7DA-\\uA7F1\\uA802\\uA806\\uA80B\\uA823-\\uA83F\\uA874-\\uA881\\uA8B4-\\uA8F1\\uA8F8-\\uA8FA\\uA8FC\\uA8FF-\\uA909\\uA926-\\uA92F\\uA947-\\uA95F\\uA97D-\\uA983\\uA9B3-\\uA9CE\\uA9D0-\\uA9DF\\uA9E5\\uA9F0-\\uA9F9\\uA9FF\\uAA29-\\uAA3F\\uAA43\\uAA4C-\\uAA5F\\uAA77-\\uAA79\\uAA7B-\\uAA7D\\uAAB0\\uAAB2-\\uAAB4\\uAAB7\\uAAB8\\uAABE\\uAABF\\uAAC1\\uAAC3-\\uAADA\\uAADE\\uAADF\\uAAEB-\\uAAF1\\uAAF5-\\uAB00\\uAB07\\uAB08\\uAB0F\\uAB10\\uAB17-\\uAB1F\\uAB27\\uAB2F\\uAB5B\\uAB6A-\\uAB6F\\uABE3-\\uABFF\\uD7A4-\\uD7AF\\uD7C7-\\uD7CA\\uD7FC-\\uD7FF\\uE000-\\uF8FF\\uFA6E\\uFA6F\\uFADA-\\uFAFF\\uFB07-\\uFB12\\uFB18-\\uFB1C\\uFB1E\\uFB29\\uFB37\\uFB3D\\uFB3F\\uFB42\\uFB45\\uFBB2-\\uFBD2\\uFD3E-\\uFD4F\\uFD90\\uFD91\\uFDC8-\\uFDEF\\uFDFC-\\uFE6F\\uFE75\\uFEFD-\\uFF20\\uFF3B-\\uFF40\\uFF5B-\\uFF65\\uFFBF-\\uFFC1\\uFFC8\\uFFC9\\uFFD0\\uFFD1\\uFFD8\\uFFD9\\uFFDD-\\uFFFF]|\\uD800[\\uDC0C\\uDC27\\uDC3B\\uDC3E\\uDC4E\\uDC4F\\uDC5E-\\uDC7F\\uDCFB-\\uDE7F\\uDE9D-\\uDE9F\\uDED1-\\uDEFF\\uDF20-\\uDF2C\\uDF41\\uDF4A-\\uDF4F\\uDF76-\\uDF7F\\uDF9E\\uDF9F\\uDFC4-\\uDFC7\\uDFD0-\\uDFFF]|\\uD801[\\uDC9E-\\uDCAF\\uDCD4-\\uDCD7\\uDCFC-\\uDCFF\\uDD28-\\uDD2F\\uDD64-\\uDD6F\\uDD7B\\uDD8B\\uDD93\\uDD96\\uDDA2\\uDDB2\\uDDBA\\uDDBD-\\uDDFF\\uDF37-\\uDF3F\\uDF56-\\uDF5F\\uDF68-\\uDF7F\\uDF86\\uDFB1\\uDFBB-\\uDFFF]|\\uD802[\\uDC06\\uDC07\\uDC09\\uDC36\\uDC39-\\uDC3B\\uDC3D\\uDC3E\\uDC56-\\uDC5F\\uDC77-\\uDC7F\\uDC9F-\\uDCDF\\uDCF3\\uDCF6-\\uDCFF\\uDD16-\\uDD1F\\uDD3A-\\uDD7F\\uDDB8-\\uDDBD\\uDDC0-\\uDDFF\\uDE01-\\uDE0F\\uDE14\\uDE18\\uDE36-\\uDE5F\\uDE7D-\\uDE7F\\uDE9D-\\uDEBF\\uDEC8\\uDEE5-\\uDEFF\\uDF36-\\uDF3F\\uDF56-\\uDF5F\\uDF73-\\uDF7F\\uDF92-\\uDFFF]|\\uD803[\\uDC49-\\uDC7F\\uDCB3-\\uDCBF\\uDCF3-\\uDCFF\\uDD24-\\uDE7F\\uDEAA-\\uDEAF\\uDEB2-\\uDEFF\\uDF1D-\\uDF26\\uDF28-\\uDF2F\\uDF46-\\uDF6F\\uDF82-\\uDFAF\\uDFC5-\\uDFDF\\uDFF7-\\uDFFF]|\\uD804[\\uDC00-\\uDC02\\uDC38-\\uDC70\\uDC73\\uDC74\\uDC76-\\uDC82\\uDCB0-\\uDCCF\\uDCE9-\\uDD02\\uDD27-\\uDD43\\uDD45\\uDD46\\uDD48-\\uDD4F\\uDD73-\\uDD75\\uDD77-\\uDD82\\uDDB3-\\uDDC0\\uDDC5-\\uDDD9\\uDDDB\\uDDDD-\\uDDFF\\uDE12\\uDE2C-\\uDE7F\\uDE87\\uDE89\\uDE8E\\uDE9E\\uDEA9-\\uDEAF\\uDEDF-\\uDF04\\uDF0D\\uDF0E\\uDF11\\uDF12\\uDF29\\uDF31\\uDF34\\uDF3A-\\uDF3C\\uDF3E-\\uDF4F\\uDF51-\\uDF5C\\uDF62-\\uDFFF]|\\uD805[\\uDC35-\\uDC46\\uDC4B-\\uDC5E\\uDC62-\\uDC7F\\uDCB0-\\uDCC3\\uDCC6\\uDCC8-\\uDD7F\\uDDAF-\\uDDD7\\uDDDC-\\uDDFF\\uDE30-\\uDE43\\uDE45-\\uDE7F\\uDEAB-\\uDEB7\\uDEB9-\\uDEFF\\uDF1B-\\uDF3F\\uDF47-\\uDFFF]|\\uD806[\\uDC2C-\\uDC9F\\uDCE0-\\uDCFE\\uDD07\\uDD08\\uDD0A\\uDD0B\\uDD14\\uDD17\\uDD30-\\uDD3E\\uDD40\\uDD42-\\uDD9F\\uDDA8\\uDDA9\\uDDD1-\\uDDE0\\uDDE2\\uDDE4-\\uDDFF\\uDE01-\\uDE0A\\uDE33-\\uDE39\\uDE3B-\\uDE4F\\uDE51-\\uDE5B\\uDE8A-\\uDE9C\\uDE9E-\\uDEAF\\uDEF9-\\uDFFF]|\\uD807[\\uDC09\\uDC2F-\\uDC3F\\uDC41-\\uDC71\\uDC90-\\uDCFF\\uDD07\\uDD0A\\uDD31-\\uDD45\\uDD47-\\uDD5F\\uDD66\\uDD69\\uDD8A-\\uDD97\\uDD99-\\uDEDF\\uDEF3-\\uDFAF\\uDFB1-\\uDFFF]|\\uD808[\\uDF9A-\\uDFFF]|\\uD809[\\uDC00-\\uDC7F\\uDD44-\\uDFFF]|[\\uD80A\\uD80E-\\uD810\\uD812-\\uD819\\uD824-\\uD82A\\uD82D\\uD82E\\uD830-\\uD834\\uD836\\uD83C-\\uD83F\\uD87B-\\uD87D\\uD87F\\uD885-\\uDBFF][\\uDC00-\\uDFFF]|\\uD80B[\\uDC00-\\uDF8F\\uDFF1-\\uDFFF]|\\uD80D[\\uDC2F-\\uDFFF]|\\uD811[\\uDE47-\\uDFFF]|\\uD81A[\\uDE39-\\uDE3F\\uDE5F-\\uDE6F\\uDEBF-\\uDECF\\uDEEE-\\uDEFF\\uDF30-\\uDF3F\\uDF44-\\uDF62\\uDF78-\\uDF7C\\uDF90-\\uDFFF]|\\uD81B[\\uDC00-\\uDE3F\\uDE80-\\uDEFF\\uDF4B-\\uDF4F\\uDF51-\\uDF92\\uDFA0-\\uDFDF\\uDFE2\\uDFE4-\\uDFFF]|\\uD821[\\uDFF8-\\uDFFF]|\\uD823[\\uDCD6-\\uDCFF\\uDD09-\\uDFFF]|\\uD82B[\\uDC00-\\uDFEF\\uDFF4\\uDFFC\\uDFFF]|\\uD82C[\\uDD23-\\uDD4F\\uDD53-\\uDD63\\uDD68-\\uDD6F\\uDEFC-\\uDFFF]|\\uD82F[\\uDC6B-\\uDC6F\\uDC7D-\\uDC7F\\uDC89-\\uDC8F\\uDC9A-\\uDFFF]|\\uD835[\\uDC55\\uDC9D\\uDCA0\\uDCA1\\uDCA3\\uDCA4\\uDCA7\\uDCA8\\uDCAD\\uDCBA\\uDCBC\\uDCC4\\uDD06\\uDD0B\\uDD0C\\uDD15\\uDD1D\\uDD3A\\uDD3F\\uDD45\\uDD47-\\uDD49\\uDD51\\uDEA6\\uDEA7\\uDEC1\\uDEDB\\uDEFB\\uDF15\\uDF35\\uDF4F\\uDF6F\\uDF89\\uDFA9\\uDFC3\\uDFCC-\\uDFFF]|\\uD837[\\uDC00-\\uDEFF\\uDF1F-\\uDFFF]|\\uD838[\\uDC00-\\uDCFF\\uDD2D-\\uDD36\\uDD3E-\\uDD4D\\uDD4F-\\uDE8F\\uDEAE-\\uDEBF\\uDEEC-\\uDFFF]|\\uD839[\\uDC00-\\uDFDF\\uDFE7\\uDFEC\\uDFEF\\uDFFF]|\\uD83A[\\uDCC5-\\uDCFF\\uDD44-\\uDD4A\\uDD4C-\\uDFFF]|\\uD83B[\\uDC00-\\uDDFF\\uDE04\\uDE20\\uDE23\\uDE25\\uDE26\\uDE28\\uDE33\\uDE38\\uDE3A\\uDE3C-\\uDE41\\uDE43-\\uDE46\\uDE48\\uDE4A\\uDE4C\\uDE50\\uDE53\\uDE55\\uDE56\\uDE58\\uDE5A\\uDE5C\\uDE5E\\uDE60\\uDE63\\uDE65\\uDE66\\uDE6B\\uDE73\\uDE78\\uDE7D\\uDE7F\\uDE8A\\uDE9C-\\uDEA0\\uDEA4\\uDEAA\\uDEBC-\\uDFFF]|\\uD869[\\uDEE0-\\uDEFF]|\\uD86D[\\uDF39-\\uDF3F]|\\uD86E[\\uDC1E\\uDC1F]|\\uD873[\\uDEA2-\\uDEAF]|\\uD87A[\\uDFE1-\\uDFFF]|\\uD87E[\\uDE1E-\\uDFFF]|\\uD884[\\uDF4B-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF])/g)).pop();\n\n if (lastWordEnd !== undefined && lastWordEnd.index > 1) {\n after = after.slice(0, lastWordEnd.index + 1);\n }\n\n return {\n highlight: highlight,\n before: before,\n after: after\n };\n}\n\nfunction selection_createOrderedRange(startNode, startOffset, endNode, endOffset) {\n var range = new Range();\n range.setStart(startNode, startOffset);\n range.setEnd(endNode, endOffset);\n\n if (!range.collapsed) {\n return range;\n }\n\n selection_log(">>> createOrderedRange COLLAPSED ... RANGE REVERSE?");\n var rangeReverse = new Range();\n rangeReverse.setStart(endNode, endOffset);\n rangeReverse.setEnd(startNode, startOffset);\n\n if (!rangeReverse.collapsed) {\n selection_log(">>> createOrderedRange RANGE REVERSE OK.");\n return range;\n }\n\n selection_log(">>> createOrderedRange RANGE REVERSE ALSO COLLAPSED?!");\n return undefined;\n}\n\nfunction selection_convertRangeInfo(document, rangeInfo) {\n var startElement = document.querySelector(rangeInfo.startContainerElementCssSelector);\n\n if (!startElement) {\n selection_log("^^^ convertRangeInfo NO START ELEMENT CSS SELECTOR?!");\n return undefined;\n }\n\n var startContainer = startElement;\n\n if (rangeInfo.startContainerChildTextNodeIndex >= 0) {\n if (rangeInfo.startContainerChildTextNodeIndex >= startElement.childNodes.length) {\n selection_log("^^^ convertRangeInfo rangeInfo.startContainerChildTextNodeIndex >= startElement.childNodes.length?!");\n return undefined;\n }\n\n startContainer = startElement.childNodes[rangeInfo.startContainerChildTextNodeIndex];\n\n if (startContainer.nodeType !== Node.TEXT_NODE) {\n selection_log("^^^ convertRangeInfo startContainer.nodeType !== Node.TEXT_NODE?!");\n return undefined;\n }\n }\n\n var endElement = document.querySelector(rangeInfo.endContainerElementCssSelector);\n\n if (!endElement) {\n selection_log("^^^ convertRangeInfo NO END ELEMENT CSS SELECTOR?!");\n return undefined;\n }\n\n var endContainer = endElement;\n\n if (rangeInfo.endContainerChildTextNodeIndex >= 0) {\n if (rangeInfo.endContainerChildTextNodeIndex >= endElement.childNodes.length) {\n selection_log("^^^ convertRangeInfo rangeInfo.endContainerChildTextNodeIndex >= endElement.childNodes.length?!");\n return undefined;\n }\n\n endContainer = endElement.childNodes[rangeInfo.endContainerChildTextNodeIndex];\n\n if (endContainer.nodeType !== Node.TEXT_NODE) {\n selection_log("^^^ convertRangeInfo endContainer.nodeType !== Node.TEXT_NODE?!");\n return undefined;\n }\n }\n\n return selection_createOrderedRange(startContainer, rangeInfo.startOffset, endContainer, rangeInfo.endOffset);\n}\nfunction selection_location2RangeInfo(location) {\n var locations = location.locations;\n var domRange = locations.domRange;\n var start = domRange.start;\n var end = domRange.end;\n return {\n endContainerChildTextNodeIndex: end.textNodeIndex,\n endContainerElementCssSelector: end.cssSelector,\n endOffset: end.offset,\n startContainerChildTextNodeIndex: start.textNodeIndex,\n startContainerElementCssSelector: start.cssSelector,\n startOffset: start.offset\n };\n}\n\nfunction selection_log() {\n if (selection_debug) {\n log.apply(null, arguments);\n }\n}\n;// CONCATENATED MODULE: ./src/index.js\n//\n// Copyright 2021 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n// Base script used by both reflowable and fixed layout resources.\n\n\n\n\n\n // Public API used by the navigator.\n\nwindow.readium = {\n // utils\n scrollToId: scrollToId,\n scrollToPosition: scrollToPosition,\n scrollToText: scrollToText,\n scrollLeft: scrollLeft,\n scrollRight: scrollRight,\n scrollToStart: scrollToStart,\n scrollToEnd: scrollToEnd,\n setCSSProperties: setCSSProperties,\n setProperty: setProperty,\n removeProperty: removeProperty,\n // selection\n getCurrentSelection: getCurrentSelection,\n // decoration\n registerDecorationTemplates: registerTemplates,\n getDecorations: getDecorations,\n // DOM\n findFirstVisibleLocator: findFirstVisibleLocator\n}; // Legacy highlights API.\n\nwindow.createAnnotation = createAnnotation;\nwindow.createHighlight = createHighlight;\nwindow.destroyHighlight = destroyHighlight;\nwindow.getCurrentSelectionInfo = getCurrentSelectionInfo;\nwindow.getSelectionRect = getSelectionRect;\nwindow.rectangleForHighlightWithID = rectangleForHighlightWithID;\nwindow.setScrollMode = setScrollMode;\n;// CONCATENATED MODULE: ./src/index-fixed.js\n//\n// Copyright 2021 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n// Script used for fixed layouts resources.\n\nwindow.readium.isFixedLayout = true;//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,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LE1BQU0sQ0FBQ21hLHVCQUFQLEdBQWlDQSx1QkFBakM7QUFDQW5hLE1BQU0sQ0FBQ29rQixnQkFBUCxHQUEwQkEsZ0JBQTFCO0FBQ0Fwa0IsTUFBTSxDQUFDbWtCLDJCQUFQLEdBQXFDQSwyQkFBckM7QUFDQW5rQixNQUFNLENBQUN1a0IsYUFBUCxHQUF1QkEsYUFBdkIsQzs7QUNsRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBRUE7QUFFQXZrQixNQUFNLENBQUMra0IsT0FBUCxDQUFlM0UsYUFBZixHQUErQixJQUEvQiIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9zcmMvdmVuZG9yL2h5cG90aGVzaXMvYW5jaG9yaW5nL21hdGNoLXF1b3RlLmpzP2RkNmEiLCJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL3NyYy92ZW5kb3IvaHlwb3RoZXNpcy9hbmNob3JpbmcvdGV4dC1yYW5nZS5qcz9mZGVlIiwid2VicGFjazovL3JlYWRpdW0tanMvLi9zcmMvdmVuZG9yL2h5cG90aGVzaXMvYW5jaG9yaW5nL3R5cGVzLmpzPzQwMDQiLCJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL3NyYy91dGlscy5qcz8wMjVlIiwid2VicGFjazovL3JlYWRpdW0tanMvLi9zcmMvcmVjdC5qcz80ZDVhIiwid2VicGFjazovL3JlYWRpdW0tanMvLi9zcmMvZGVjb3JhdG9yLmpzPzFiMDQiLCJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL3NyYy9nZXN0dXJlcy5qcz8xNGMyIiwid2VicGFjazovL3JlYWRpdW0tanMvLi9zcmMvaGlnaGxpZ2h0LmpzPzhkYTgiLCJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL3NyYy9kb20uanM/Y2JmMCIsIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vc3JjL3NlbGVjdGlvbi5qcz81OWFjIiwid2VicGFjazovL3JlYWRpdW0tanMvLi9zcmMvaW5kZXguanM/YjYzNSIsIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vc3JjL2luZGV4LWZpeGVkLmpzP2Q5OWYiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGFwcHJveFNlYXJjaCBmcm9tICdhcHByb3gtc3RyaW5nLW1hdGNoJztcblxuLyoqXG4gKiBAdHlwZWRlZiB7aW1wb3J0KCdhcHByb3gtc3RyaW5nLW1hdGNoJykuTWF0Y2h9IFN0cmluZ01hdGNoXG4gKi9cblxuLyoqXG4gKiBAdHlwZWRlZiBNYXRjaFxuICogQHByb3Age251bWJlcn0gc3RhcnQgLSBTdGFydCBvZmZzZXQgb2YgbWF0Y2ggaW4gdGV4dFxuICogQHByb3Age251bWJlcn0gZW5kIC0gRW5kIG9mZnNldCBvZiBtYXRjaCBpbiB0ZXh0XG4gKiBAcHJvcCB7bnVtYmVyfSBzY29yZSAtXG4gKiAgIFNjb3JlIGZvciB0aGUgbWF0Y2ggYmV0d2VlbiAwIGFuZCAxLjAsIHdoZXJlIDEuMCBpbmRpY2F0ZXMgYSBwZXJmZWN0IG1hdGNoXG4gKiAgIGZvciB0aGUgcXVvdGUgYW5kIGNvbnRleHQuXG4gKi9cblxuLyoqXG4gKiBGaW5kIHRoZSBiZXN0IGFwcHJveGltYXRlIG1hdGNoZXMgZm9yIGBzdHJgIGluIGB0ZXh0YCBhbGxvd2luZyB1cCB0byBgbWF4RXJyb3JzYCBlcnJvcnMuXG4gKlxuICogQHBhcmFtIHtzdHJpbmd9IHRleHRcbiAqIEBwYXJhbSB7c3RyaW5nfSBzdHJcbiAqIEBwYXJhbSB7bnVtYmVyfSBtYXhFcnJvcnNcbiAqIEByZXR1cm4ge1N0cmluZ01hdGNoW119XG4gKi9cbmZ1bmN0aW9uIHNlYXJjaCh0ZXh0LCBzdHIsIG1heEVycm9ycykge1xuICAvLyBEbyBhIGZhc3Qgc2VhcmNoIGZvciBleGFjdCBtYXRjaGVzLiBUaGUgYGFwcHJveC1zdHJpbmctbWF0Y2hgIGxpYnJhcnlcbiAgLy8gZG9lc24ndCBjdXJyZW50bHkgaW5jb3Jwb3JhdGUgdGhpcyBvcHRpbWl6YXRpb24gaXRzZWxmLlxuICBsZXQgbWF0Y2hQb3MgPSAwO1xuICBsZXQgZXhhY3RNYXRjaGVzID0gW107XG4gIHdoaWxlIChtYXRjaFBvcyAhPT0gLTEpIHtcbiAgICBtYXRjaFBvcyA9IHRleHQuaW5kZXhPZihzdHIsIG1hdGNoUG9zKTtcbiAgICBpZiAobWF0Y2hQb3MgIT09IC0xKSB7XG4gICAgICBleGFjdE1hdGNoZXMucHVzaCh7XG4gICAgICAgIHN0YXJ0OiBtYXRjaFBvcyxcbiAgICAgICAgZW5kOiBtYXRjaFBvcyArIHN0ci5sZW5ndGgsXG4gICAgICAgIGVycm9yczogMCxcbiAgICAgIH0pO1xuICAgICAgbWF0Y2hQb3MgKz0gMTtcbiAgICB9XG4gIH1cbiAgaWYgKGV4YWN0TWF0Y2hlcy5sZW5ndGggPiAwKSB7XG4gICAgcmV0dXJuIGV4YWN0TWF0Y2hlcztcbiAgfVxuXG4gIC8vIElmIHRoZXJlIGFyZSBubyBleGFjdCBtYXRjaGVzLCBkbyBhIG1vcmUgZXhwZW5zaXZlIHNlYXJjaCBmb3IgbWF0Y2hlc1xuICAvLyB3aXRoIGVycm9ycy5cbiAgcmV0dXJuIGFwcHJveFNlYXJjaCh0ZXh0LCBzdHIsIG1heEVycm9ycyk7XG59XG5cbi8qKlxuICogQ29tcHV0ZSBhIHNjb3JlIGJldHdlZW4gMCBhbmQgMS4wIGZvciB0aGUgc2ltaWxhcml0eSBiZXR3ZWVuIGB0ZXh0YCBhbmQgYHN0cmAuXG4gKlxuICogQHBhcmFtIHtzdHJpbmd9IHRleHRcbiAqIEBwYXJhbSB7c3RyaW5nfSBzdHJcbiAqL1xuZnVuY3Rpb24gdGV4dE1hdGNoU2NvcmUodGV4dCwgc3RyKSB7XG4gIC8qIGlzdGFuYnVsIGlnbm9yZSBuZXh0IC0gYHNjb3JlTWF0Y2hgIHdpbGwgbmV2ZXIgcGFzcyBhbiBlbXB0eSBzdHJpbmcgKi9cbiAgaWYgKHN0ci5sZW5ndGggPT09IDAgfHwgdGV4dC5sZW5ndGggPT09IDApIHtcbiAgICByZXR1cm4gMC4wO1xuICB9XG4gIGNvbnN0IG1hdGNoZXMgPSBzZWFyY2godGV4dCwgc3RyLCBzdHIubGVuZ3RoKTtcblxuICAvLyBwcmV0dGllci1pZ25vcmVcbiAgcmV0dXJuIDEgLSAobWF0Y2hlc1swXS5lcnJvcnMgLyBzdHIubGVuZ3RoKTtcbn1cblxuLyoqXG4gKiBGaW5kIHRoZSBiZXN0IGFwcHJveGltYXRlIG1hdGNoIGZvciBgcXVvdGVgIGluIGB0ZXh0YC5cbiAqXG4gKiBSZXR1cm5zIGBudWxsYCBpZiBubyBtYXRjaCBleGNlZWRpbmcgdGhlIG1pbmltdW0gcXVhbGl0eSB0aHJlc2hvbGQgd2FzIGZvdW5kLlxuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSB0ZXh0IC0gRG9jdW1lbnQgdGV4dCB0byBzZWFyY2hcbiAqIEBwYXJhbSB7c3RyaW5nfSBxdW90ZSAtIFN0cmluZyB0byBmaW5kIHdpdGhpbiBgdGV4dGBcbiAqIEBwYXJhbSB7T2JqZWN0fSBjb250ZXh0IC1cbiAqICAgQ29udGV4dCBpbiB3aGljaCB0aGUgcXVvdGUgb3JpZ2luYWxseSBhcHBlYXJlZC4gVGhpcyBpcyB1c2VkIHRvIGNob29zZSB0aGVcbiAqICAgYmVzdCBtYXRjaC5cbiAqICAgQHBhcmFtIHtzdHJpbmd9IFtjb250ZXh0LnByZWZpeF0gLSBFeHBlY3RlZCB0ZXh0IGJlZm9yZSB0aGUgcXVvdGVcbiAqICAgQHBhcmFtIHtzdHJpbmd9IFtjb250ZXh0LnN1ZmZpeF0gLSBFeHBlY3RlZCB0ZXh0IGFmdGVyIHRoZSBxdW90ZVxuICogICBAcGFyYW0ge251bWJlcn0gW2NvbnRleHQuaGludF0gLSBFeHBlY3RlZCBvZmZzZXQgb2YgbWF0Y2ggd2l0aGluIHRleHRcbiAqIEByZXR1cm4ge01hdGNofG51bGx9XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBtYXRjaFF1b3RlKHRleHQsIHF1b3RlLCBjb250ZXh0ID0ge30pIHtcbiAgaWYgKHF1b3RlLmxlbmd0aCA9PT0gMCkge1xuICAgIHJldHVybiBudWxsO1xuICB9XG5cbiAgLy8gQ2hvb3NlIHRoZSBtYXhpbXVtIG51bWJlciBvZiBlcnJvcnMgdG8gYWxsb3cgZm9yIHRoZSBpbml0aWFsIHNlYXJjaC5cbiAgLy8gVGhpcyBjaG9pY2UgaW52b2x2ZXMgYSB0cmFkZW9mZiBiZXR3ZWVuOlxuICAvL1xuICAvLyAgLSBSZWNhbGwgKHByb3BvcnRpb24gb2YgXCJnb29kXCIgbWF0Y2hlcyBmb3VuZClcbiAgLy8gIC0gUHJlY2lzaW9uIChwcm9wb3J0aW9uIG9mIG1hdGNoZXMgZm91bmQgd2hpY2ggYXJlIFwiZ29vZFwiKVxuICAvLyAgLSBDb3N0IG9mIHRoZSBpbml0aWFsIHNlYXJjaCBhbmQgb2YgcHJvY2Vzc2luZyB0aGUgY2FuZGlkYXRlIG1hdGNoZXMgWzFdXG4gIC8vXG4gIC8vIFsxXSBTcGVjaWZpY2FsbHksIHRoZSBleHBlY3RlZC10aW1lIGNvbXBsZXhpdHkgb2YgdGhlIGluaXRpYWwgc2VhcmNoIGlzXG4gIC8vICAgICBgTygobWF4RXJyb3JzIC8gMzIpICogdGV4dC5sZW5ndGgpYC4gU2VlIGBhcHByb3gtc3RyaW5nLW1hdGNoYCBkb2NzLlxuICBjb25zdCBtYXhFcnJvcnMgPSBNYXRoLm1pbigyNTYsIHF1b3RlLmxlbmd0aCAvIDIpO1xuXG4gIC8vIEZpbmQgY2xvc2VzdCBtYXRjaGVzIGZvciBgcXVvdGVgIGluIGB0ZXh0YCBiYXNlZCBvbiBlZGl0IGRpc3RhbmNlLlxuICBjb25zdCBtYXRjaGVzID0gc2VhcmNoKHRleHQsIHF1b3RlLCBtYXhFcnJvcnMpO1xuXG4gIGlmIChtYXRjaGVzLmxlbmd0aCA9PT0gMCkge1xuICAgIHJldHVybiBudWxsO1xuICB9XG5cbiAgLyoqXG4gICAqIENvbXB1dGUgYSBzY29yZSBiZXR3ZWVuIDAgYW5kIDEuMCBmb3IgYSBtYXRjaCBjYW5kaWRhdGUuXG4gICAqXG4gICAqIEBwYXJhbSB7U3RyaW5nTWF0Y2h9IG1hdGNoXG4gICAqL1xuICBjb25zdCBzY29yZU1hdGNoID0gbWF0Y2ggPT4ge1xuICAgIGNvbnN0IHF1b3RlV2VpZ2h0ID0gNTA7IC8vIFNpbWlsYXJpdHkgb2YgbWF0Y2hlZCB0ZXh0IHRvIHF1b3RlLlxuICAgIGNvbnN0IHByZWZpeFdlaWdodCA9IDIwOyAvLyBTaW1pbGFyaXR5IG9mIHRleHQgYmVmb3JlIG1hdGNoZWQgdGV4dCB0byBgY29udGV4dC5wcmVmaXhgLlxuICAgIGNvbnN0IHN1ZmZpeFdlaWdodCA9IDIwOyAvLyBTaW1pbGFyaXR5IG9mIHRleHQgYWZ0ZXIgbWF0Y2hlZCB0ZXh0IHRvIGBjb250ZXh0LnN1ZmZpeGAuXG4gICAgY29uc3QgcG9zV2VpZ2h0ID0gMjsgLy8gUHJveGltaXR5IHRvIGV4cGVjdGVkIGxvY2F0aW9uLiBVc2VkIGFzIGEgdGllLWJyZWFrZXIuXG5cbiAgICBjb25zdCBxdW90ZVNjb3JlID0gMSAtIG1hdGNoLmVycm9ycyAvIHF1b3RlLmxlbmd0aDtcblxuICAgIGNvbnN0IHByZWZpeFNjb3JlID0gY29udGV4dC5wcmVmaXhcbiAgICAgID8gdGV4dE1hdGNoU2NvcmUoXG4gICAgICAgICAgdGV4dC5zbGljZShNYXRoLm1heCgwLCBtYXRjaC5zdGFydCAtIGNvbnRleHQucHJlZml4Lmxlbmd0aCksIG1hdGNoLnN0YXJ0KSxcbiAgICAgICAgICBjb250ZXh0LnByZWZpeFxuICAgICAgICApXG4gICAgICA6IDEuMDtcbiAgICBjb25zdCBzdWZmaXhTY29yZSA9IGNvbnRleHQuc3VmZml4XG4gICAgICA/IHRleHRNYXRjaFNjb3JlKFxuICAgICAgICAgIHRleHQuc2xpY2UobWF0Y2guZW5kLCBtYXRjaC5lbmQgKyBjb250ZXh0LnN1ZmZpeC5sZW5ndGgpLFxuICAgICAgICAgIGNvbnRleHQuc3VmZml4XG4gICAgICAgIClcbiAgICAgIDogMS4wO1xuXG4gICAgbGV0IHBvc1Njb3JlID0gMS4wO1xuICAgIGlmICh0eXBlb2YgY29udGV4dC5oaW50ID09PSAnbnVtYmVyJykge1xuICAgICAgY29uc3Qgb2Zmc2V0ID0gTWF0aC5hYnMobWF0Y2guc3RhcnQgLSBjb250ZXh0LmhpbnQpO1xuICAgICAgcG9zU2NvcmUgPSAxLjAgLSBvZmZzZXQgLyB0ZXh0Lmxlbmd0aDtcbiAgICB9XG5cbiAgICBjb25zdCByYXdTY29yZSA9XG4gICAgICBxdW90ZVdlaWdodCAqIHF1b3RlU2NvcmUgK1xuICAgICAgcHJlZml4V2VpZ2h0ICogcHJlZml4U2NvcmUgK1xuICAgICAgc3VmZml4V2VpZ2h0ICogc3VmZml4U2NvcmUgK1xuICAgICAgcG9zV2VpZ2h0ICogcG9zU2NvcmU7XG4gICAgY29uc3QgbWF4U2NvcmUgPSBxdW90ZVdlaWdodCArIHByZWZpeFdlaWdodCArIHN1ZmZpeFdlaWdodCArIHBvc1dlaWdodDtcbiAgICBjb25zdCBub3JtYWxpemVkU2NvcmUgPSByYXdTY29yZSAvIG1heFNjb3JlO1xuXG4gICAgcmV0dXJuIG5vcm1hbGl6ZWRTY29yZTtcbiAgfTtcblxuICAvLyBSYW5rIG1hdGNoZXMgYmFzZWQgb24gc2ltaWxhcml0eSBvZiBhY3R1YWwgYW5kIGV4cGVjdGVkIHN1cnJvdW5kaW5nIHRleHRcbiAgLy8gYW5kIGFjdHVhbC9leHBlY3RlZCBvZmZzZXQgaW4gdGhlIGRvY3VtZW50IHRleHQuXG4gIGNvbnN0IHNjb3JlZE1hdGNoZXMgPSBtYXRjaGVzLm1hcChtID0+ICh7XG4gICAgc3RhcnQ6IG0uc3RhcnQsXG4gICAgZW5kOiBtLmVuZCxcbiAgICBzY29yZTogc2NvcmVNYXRjaChtKSxcbiAgfSkpO1xuXG4gIC8vIENob29zZSBtYXRjaCB3aXRoIGhpZ2hlc3Qgc2NvcmUuXG4gIHNjb3JlZE1hdGNoZXMuc29ydCgoYSwgYikgPT4gYi5zY29yZSAtIGEuc2NvcmUpO1xuICByZXR1cm4gc2NvcmVkTWF0Y2hlc1swXTtcbn1cbiIsIi8qKlxuICogUmV0dXJuIHRoZSBjb21iaW5lZCBsZW5ndGggb2YgdGV4dCBub2RlcyBjb250YWluZWQgaW4gYG5vZGVgLlxuICpcbiAqIEBwYXJhbSB7Tm9kZX0gbm9kZVxuICovXG5mdW5jdGlvbiBub2RlVGV4dExlbmd0aChub2RlKSB7XG4gIHN3aXRjaCAobm9kZS5ub2RlVHlwZSkge1xuICAgIGNhc2UgTm9kZS5FTEVNRU5UX05PREU6XG4gICAgY2FzZSBOb2RlLlRFWFRfTk9ERTpcbiAgICAgIC8vIG5iLiBgdGV4dENvbnRlbnRgIGV4Y2x1ZGVzIHRleHQgaW4gY29tbWVudHMgYW5kIHByb2Nlc3NpbmcgaW5zdHJ1Y3Rpb25zXG4gICAgICAvLyB3aGVuIGNhbGxlZCBvbiBhIHBhcmVudCBlbGVtZW50LCBzbyB3ZSBkb24ndCBuZWVkIHRvIHN1YnRyYWN0IHRoYXQgaGVyZS5cblxuICAgICAgcmV0dXJuIC8qKiBAdHlwZSB7c3RyaW5nfSAqLyAobm9kZS50ZXh0Q29udGVudCkubGVuZ3RoO1xuICAgIGRlZmF1bHQ6XG4gICAgICByZXR1cm4gMDtcbiAgfVxufVxuXG4vKipcbiAqIFJldHVybiB0aGUgdG90YWwgbGVuZ3RoIG9mIHRoZSB0ZXh0IG9mIGFsbCBwcmV2aW91cyBzaWJsaW5ncyBvZiBgbm9kZWAuXG4gKlxuICogQHBhcmFtIHtOb2RlfSBub2RlXG4gKi9cbmZ1bmN0aW9uIHByZXZpb3VzU2libGluZ3NUZXh0TGVuZ3RoKG5vZGUpIHtcbiAgbGV0IHNpYmxpbmcgPSBub2RlLnByZXZpb3VzU2libGluZztcbiAgbGV0IGxlbmd0aCA9IDA7XG4gIHdoaWxlIChzaWJsaW5nKSB7XG4gICAgbGVuZ3RoICs9IG5vZGVUZXh0TGVuZ3RoKHNpYmxpbmcpO1xuICAgIHNpYmxpbmcgPSBzaWJsaW5nLnByZXZpb3VzU2libGluZztcbiAgfVxuICByZXR1cm4gbGVuZ3RoO1xufVxuXG4vKipcbiAqIFJlc29sdmUgb25lIG9yIG1vcmUgY2hhcmFjdGVyIG9mZnNldHMgd2l0aGluIGFuIGVsZW1lbnQgdG8gKHRleHQgbm9kZSwgcG9zaXRpb24pXG4gKiBwYWlycy5cbiAqXG4gKiBAcGFyYW0ge0VsZW1lbnR9IGVsZW1lbnRcbiAqIEBwYXJhbSB7bnVtYmVyW119IG9mZnNldHMgLSBPZmZzZXRzLCB3aGljaCBtdXN0IGJlIHNvcnRlZCBpbiBhc2NlbmRpbmcgb3JkZXJcbiAqIEByZXR1cm4ge3sgbm9kZTogVGV4dCwgb2Zmc2V0OiBudW1iZXIgfVtdfVxuICovXG5mdW5jdGlvbiByZXNvbHZlT2Zmc2V0cyhlbGVtZW50LCAuLi5vZmZzZXRzKSB7XG4gIGxldCBuZXh0T2Zmc2V0ID0gb2Zmc2V0cy5zaGlmdCgpO1xuICBjb25zdCBub2RlSXRlciA9IC8qKiBAdHlwZSB7RG9jdW1lbnR9ICovIChcbiAgICBlbGVtZW50Lm93bmVyRG9jdW1lbnRcbiAgKS5jcmVhdGVOb2RlSXRlcmF0b3IoZWxlbWVudCwgTm9kZUZpbHRlci5TSE9XX1RFWFQpO1xuICBjb25zdCByZXN1bHRzID0gW107XG5cbiAgbGV0IGN1cnJlbnROb2RlID0gbm9kZUl0ZXIubmV4dE5vZGUoKTtcbiAgbGV0IHRleHROb2RlO1xuICBsZXQgbGVuZ3RoID0gMDtcblxuICAvLyBGaW5kIHRoZSB0ZXh0IG5vZGUgY29udGFpbmluZyB0aGUgYG5leHRPZmZzZXRgdGggY2hhcmFjdGVyIGZyb20gdGhlIHN0YXJ0XG4gIC8vIG9mIGBlbGVtZW50YC5cbiAgd2hpbGUgKG5leHRPZmZzZXQgIT09IHVuZGVmaW5lZCAmJiBjdXJyZW50Tm9kZSkge1xuICAgIHRleHROb2RlID0gLyoqIEB0eXBlIHtUZXh0fSAqLyAoY3VycmVudE5vZGUpO1xuICAgIGlmIChsZW5ndGggKyB0ZXh0Tm9kZS5kYXRhLmxlbmd0aCA+IG5leHRPZmZzZXQpIHtcbiAgICAgIHJlc3VsdHMucHVzaCh7IG5vZGU6IHRleHROb2RlLCBvZmZzZXQ6IG5leHRPZmZzZXQgLSBsZW5ndGggfSk7XG4gICAgICBuZXh0T2Zmc2V0ID0gb2Zmc2V0cy5zaGlmdCgpO1xuICAgIH0gZWxzZSB7XG4gICAgICBjdXJyZW50Tm9kZSA9IG5vZGVJdGVyLm5leHROb2RlKCk7XG4gICAgICBsZW5ndGggKz0gdGV4dE5vZGUuZGF0YS5sZW5ndGg7XG4gICAgfVxuICB9XG5cbiAgLy8gQm91bmRhcnkgY2FzZS5cbiAgd2hpbGUgKG5leHRPZmZzZXQgIT09IHVuZGVmaW5lZCAmJiB0ZXh0Tm9kZSAmJiBsZW5ndGggPT09IG5leHRPZmZzZXQpIHtcbiAgICByZXN1bHRzLnB1c2goeyBub2RlOiB0ZXh0Tm9kZSwgb2Zmc2V0OiB0ZXh0Tm9kZS5kYXRhLmxlbmd0aCB9KTtcbiAgICBuZXh0T2Zmc2V0ID0gb2Zmc2V0cy5zaGlmdCgpO1xuICB9XG5cbiAgaWYgKG5leHRPZmZzZXQgIT09IHVuZGVmaW5lZCkge1xuICAgIHRocm93IG5ldyBSYW5nZUVycm9yKCdPZmZzZXQgZXhjZWVkcyB0ZXh0IGxlbmd0aCcpO1xuICB9XG5cbiAgcmV0dXJuIHJlc3VsdHM7XG59XG5cbmV4cG9ydCBsZXQgUkVTT0xWRV9GT1JXQVJEUyA9IDE7XG5leHBvcnQgbGV0IFJFU09MVkVfQkFDS1dBUkRTID0gMjtcblxuLyoqXG4gKiBSZXByZXNlbnRzIGFuIG9mZnNldCB3aXRoaW4gdGhlIHRleHQgY29udGVudCBvZiBhbiBlbGVtZW50LlxuICpcbiAqIFRoaXMgcG9zaXRpb24gY2FuIGJlIHJlc29sdmVkIHRvIGEgc3BlY2lmaWMgZGVzY2VuZGFudCBub2RlIGluIHRoZSBjdXJyZW50XG4gKiBET00gc3VidHJlZSBvZiB0aGUgZWxlbWVudCB1c2luZyB0aGUgYHJlc29sdmVgIG1ldGhvZC5cbiAqL1xuZXhwb3J0IGNsYXNzIFRleHRQb3NpdGlvbiB7XG4gIC8qKlxuICAgKiBDb25zdHJ1Y3QgYSBgVGV4dFBvc2l0aW9uYCB0aGF0IHJlZmVycyB0byB0aGUgdGV4dCBwb3NpdGlvbiBgb2Zmc2V0YCB3aXRoaW5cbiAgICogdGhlIHRleHQgY29udGVudCBvZiBgZWxlbWVudGAuXG4gICAqXG4gICAqIEBwYXJhbSB7RWxlbWVudH0gZWxlbWVudFxuICAgKiBAcGFyYW0ge251bWJlcn0gb2Zmc2V0XG4gICAqL1xuICBjb25zdHJ1Y3RvcihlbGVtZW50LCBvZmZzZXQpIHtcbiAgICBpZiAob2Zmc2V0IDwgMCkge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdPZmZzZXQgaXMgaW52YWxpZCcpO1xuICAgIH1cblxuICAgIC8qKiBFbGVtZW50IHRoYXQgYG9mZnNldGAgaXMgcmVsYXRpdmUgdG8uICovXG4gICAgdGhpcy5lbGVtZW50ID0gZWxlbWVudDtcblxuICAgIC8qKiBDaGFyYWN0ZXIgb2Zmc2V0IGZyb20gdGhlIHN0YXJ0IG9mIHRoZSBlbGVtZW50J3MgYHRleHRDb250ZW50YC4gKi9cbiAgICB0aGlzLm9mZnNldCA9IG9mZnNldDtcbiAgfVxuXG4gIC8qKlxuICAgKiBSZXR1cm4gYSBjb3B5IG9mIHRoaXMgcG9zaXRpb24gd2l0aCBvZmZzZXQgcmVsYXRpdmUgdG8gYSBnaXZlbiBhbmNlc3RvclxuICAgKiBlbGVtZW50LlxuICAgKlxuICAgKiBAcGFyYW0ge0VsZW1lbnR9IHBhcmVudCAtIEFuY2VzdG9yIG9mIGB0aGlzLmVsZW1lbnRgXG4gICAqIEByZXR1cm4ge1RleHRQb3NpdGlvbn1cbiAgICovXG4gIHJlbGF0aXZlVG8ocGFyZW50KSB7XG4gICAgaWYgKCFwYXJlbnQuY29udGFpbnModGhpcy5lbGVtZW50KSkge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdQYXJlbnQgaXMgbm90IGFuIGFuY2VzdG9yIG9mIGN1cnJlbnQgZWxlbWVudCcpO1xuICAgIH1cblxuICAgIGxldCBlbCA9IHRoaXMuZWxlbWVudDtcbiAgICBsZXQgb2Zmc2V0ID0gdGhpcy5vZmZzZXQ7XG4gICAgd2hpbGUgKGVsICE9PSBwYXJlbnQpIHtcbiAgICAgIG9mZnNldCArPSBwcmV2aW91c1NpYmxpbmdzVGV4dExlbmd0aChlbCk7XG4gICAgICBlbCA9IC8qKiBAdHlwZSB7RWxlbWVudH0gKi8gKGVsLnBhcmVudEVsZW1lbnQpO1xuICAgIH1cblxuICAgIHJldHVybiBuZXcgVGV4dFBvc2l0aW9uKGVsLCBvZmZzZXQpO1xuICB9XG5cbiAgLyoqXG4gICAqIFJlc29sdmUgdGhlIHBvc2l0aW9uIHRvIGEgc3BlY2lmaWMgdGV4dCBub2RlIGFuZCBvZmZzZXQgd2l0aGluIHRoYXQgbm9kZS5cbiAgICpcbiAgICogVGhyb3dzIGlmIGB0aGlzLm9mZnNldGAgZXhjZWVkcyB0aGUgbGVuZ3RoIG9mIHRoZSBlbGVtZW50J3MgdGV4dC4gSW4gdGhlXG4gICAqIGNhc2Ugd2hlcmUgdGhlIGVsZW1lbnQgaGFzIG5vIHRleHQgYW5kIGB0aGlzLm9mZnNldGAgaXMgMCwgdGhlIGBkaXJlY3Rpb25gXG4gICAqIG9wdGlvbiBkZXRlcm1pbmVzIHdoYXQgaGFwcGVucy5cbiAgICpcbiAgICogT2Zmc2V0cyBhdCB0aGUgYm91bmRhcnkgYmV0d2VlbiB0d28gbm9kZXMgYXJlIHJlc29sdmVkIHRvIHRoZSBzdGFydCBvZiB0aGVcbiAgICogbm9kZSB0aGF0IGJlZ2lucyBhdCB0aGUgYm91bmRhcnkuXG4gICAqXG4gICAqIEBwYXJhbSB7T2JqZWN0fSBbb3B0aW9uc11cbiAgICogICBAcGFyYW0ge1JFU09MVkVfRk9SV0FSRFN8UkVTT0xWRV9CQUNLV0FSRFN9IFtvcHRpb25zLmRpcmVjdGlvbl0gLVxuICAgKiAgICAgU3BlY2lmaWVzIGluIHdoaWNoIGRpcmVjdGlvbiB0byBzZWFyY2ggZm9yIHRoZSBuZWFyZXN0IHRleHQgbm9kZSBpZlxuICAgKiAgICAgYHRoaXMub2Zmc2V0YCBpcyBgMGAgYW5kIGB0aGlzLmVsZW1lbnRgIGhhcyBubyB0ZXh0LiBJZiBub3Qgc3BlY2lmaWVkXG4gICAqICAgICBhbiBlcnJvciBpcyB0aHJvd24uXG4gICAqIEByZXR1cm4ge3sgbm9kZTogVGV4dCwgb2Zmc2V0OiBudW1iZXIgfX1cbiAgICogQHRocm93cyB7UmFuZ2VFcnJvcn1cbiAgICovXG4gIHJlc29sdmUob3B0aW9ucyA9IHt9KSB7XG4gICAgdHJ5IHtcbiAgICAgIHJldHVybiByZXNvbHZlT2Zmc2V0cyh0aGlzLmVsZW1lbnQsIHRoaXMub2Zmc2V0KVswXTtcbiAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgIGlmICh0aGlzLm9mZnNldCA9PT0gMCAmJiBvcHRpb25zLmRpcmVjdGlvbiAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGNvbnN0IHR3ID0gZG9jdW1lbnQuY3JlYXRlVHJlZVdhbGtlcihcbiAgICAgICAgICB0aGlzLmVsZW1lbnQuZ2V0Um9vdE5vZGUoKSxcbiAgICAgICAgICBOb2RlRmlsdGVyLlNIT1dfVEVYVFxuICAgICAgICApO1xuICAgICAgICB0dy5jdXJyZW50Tm9kZSA9IHRoaXMuZWxlbWVudDtcbiAgICAgICAgY29uc3QgZm9yd2FyZHMgPSBvcHRpb25zLmRpcmVjdGlvbiA9PT0gUkVTT0xWRV9GT1JXQVJEUztcbiAgICAgICAgY29uc3QgdGV4dCA9IC8qKiBAdHlwZSB7VGV4dHxudWxsfSAqLyAoXG4gICAgICAgICAgZm9yd2FyZHMgPyB0dy5uZXh0Tm9kZSgpIDogdHcucHJldmlvdXNOb2RlKClcbiAgICAgICAgKTtcbiAgICAgICAgaWYgKCF0ZXh0KSB7XG4gICAgICAgICAgdGhyb3cgZXJyO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB7IG5vZGU6IHRleHQsIG9mZnNldDogZm9yd2FyZHMgPyAwIDogdGV4dC5kYXRhLmxlbmd0aCB9O1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdGhyb3cgZXJyO1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBDb25zdHJ1Y3QgYSBgVGV4dFBvc2l0aW9uYCB0aGF0IHJlZmVycyB0byB0aGUgYG9mZnNldGB0aCBjaGFyYWN0ZXIgd2l0aGluXG4gICAqIGBub2RlYC5cbiAgICpcbiAgICogQHBhcmFtIHtOb2RlfSBub2RlXG4gICAqIEBwYXJhbSB7bnVtYmVyfSBvZmZzZXRcbiAgICogQHJldHVybiB7VGV4dFBvc2l0aW9ufVxuICAgKi9cbiAgc3RhdGljIGZyb21DaGFyT2Zmc2V0KG5vZGUsIG9mZnNldCkge1xuICAgIHN3aXRjaCAobm9kZS5ub2RlVHlwZSkge1xuICAgICAgY2FzZSBOb2RlLlRFWFRfTk9ERTpcbiAgICAgICAgcmV0dXJuIFRleHRQb3NpdGlvbi5mcm9tUG9pbnQobm9kZSwgb2Zmc2V0KTtcbiAgICAgIGNhc2UgTm9kZS5FTEVNRU5UX05PREU6XG4gICAgICAgIHJldHVybiBuZXcgVGV4dFBvc2l0aW9uKC8qKiBAdHlwZSB7RWxlbWVudH0gKi8gKG5vZGUpLCBvZmZzZXQpO1xuICAgICAgZGVmYXVsdDpcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCdOb2RlIGlzIG5vdCBhbiBlbGVtZW50IG9yIHRleHQgbm9kZScpO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBDb25zdHJ1Y3QgYSBgVGV4dFBvc2l0aW9uYCByZXByZXNlbnRpbmcgdGhlIHJhbmdlIHN0YXJ0IG9yIGVuZCBwb2ludCAobm9kZSwgb2Zmc2V0KS5cbiAgICpcbiAgICogQHBhcmFtIHtOb2RlfSBub2RlIC0gVGV4dCBvciBFbGVtZW50IG5vZGVcbiAgICogQHBhcmFtIHtudW1iZXJ9IG9mZnNldCAtIE9mZnNldCB3aXRoaW4gdGhlIG5vZGUuXG4gICAqIEByZXR1cm4ge1RleHRQb3NpdGlvbn1cbiAgICovXG4gIHN0YXRpYyBmcm9tUG9pbnQobm9kZSwgb2Zmc2V0KSB7XG4gICAgc3dpdGNoIChub2RlLm5vZGVUeXBlKSB7XG4gICAgICBjYXNlIE5vZGUuVEVYVF9OT0RFOiB7XG4gICAgICAgIGlmIChvZmZzZXQgPCAwIHx8IG9mZnNldCA+IC8qKiBAdHlwZSB7VGV4dH0gKi8gKG5vZGUpLmRhdGEubGVuZ3RoKSB7XG4gICAgICAgICAgdGhyb3cgbmV3IEVycm9yKCdUZXh0IG5vZGUgb2Zmc2V0IGlzIG91dCBvZiByYW5nZScpO1xuICAgICAgICB9XG5cbiAgICAgICAgaWYgKCFub2RlLnBhcmVudEVsZW1lbnQpIHtcbiAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoJ1RleHQgbm9kZSBoYXMgbm8gcGFyZW50Jyk7XG4gICAgICAgIH1cblxuICAgICAgICAvLyBHZXQgdGhlIG9mZnNldCBmcm9tIHRoZSBzdGFydCBvZiB0aGUgcGFyZW50IGVsZW1lbnQuXG4gICAgICAgIGNvbnN0IHRleHRPZmZzZXQgPSBwcmV2aW91c1NpYmxpbmdzVGV4dExlbmd0aChub2RlKSArIG9mZnNldDtcblxuICAgICAgICByZXR1cm4gbmV3IFRleHRQb3NpdGlvbihub2RlLnBhcmVudEVsZW1lbnQsIHRleHRPZmZzZXQpO1xuICAgICAgfVxuICAgICAgY2FzZSBOb2RlLkVMRU1FTlRfTk9ERToge1xuICAgICAgICBpZiAob2Zmc2V0IDwgMCB8fCBvZmZzZXQgPiBub2RlLmNoaWxkTm9kZXMubGVuZ3RoKSB7XG4gICAgICAgICAgdGhyb3cgbmV3IEVycm9yKCdDaGlsZCBub2RlIG9mZnNldCBpcyBvdXQgb2YgcmFuZ2UnKTtcbiAgICAgICAgfVxuXG4gICAgICAgIC8vIEdldCB0aGUgdGV4dCBsZW5ndGggYmVmb3JlIHRoZSBgb2Zmc2V0YHRoIGNoaWxkIG9mIGVsZW1lbnQuXG4gICAgICAgIGxldCB0ZXh0T2Zmc2V0ID0gMDtcbiAgICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCBvZmZzZXQ7IGkrKykge1xuICAgICAgICAgIHRleHRPZmZzZXQgKz0gbm9kZVRleHRMZW5ndGgobm9kZS5jaGlsZE5vZGVzW2ldKTtcbiAgICAgICAgfVxuXG4gICAgICAgIHJldHVybiBuZXcgVGV4dFBvc2l0aW9uKC8qKiBAdHlwZSB7RWxlbWVudH0gKi8gKG5vZGUpLCB0ZXh0T2Zmc2V0KTtcbiAgICAgIH1cbiAgICAgIGRlZmF1bHQ6XG4gICAgICAgIHRocm93IG5ldyBFcnJvcignUG9pbnQgaXMgbm90IGluIGFuIGVsZW1lbnQgb3IgdGV4dCBub2RlJyk7XG4gICAgfVxuICB9XG59XG5cbi8qKlxuICogUmVwcmVzZW50cyBhIHJlZ2lvbiBvZiBhIGRvY3VtZW50IGFzIGEgKHN0YXJ0LCBlbmQpIHBhaXIgb2YgYFRleHRQb3NpdGlvbmAgcG9pbnRzLlxuICpcbiAqIFJlcHJlc2VudGluZyBhIHJhbmdlIGluIHRoaXMgd2F5IGFsbG93cyBmb3IgY2hhbmdlcyBpbiB0aGUgRE9NIGNvbnRlbnQgb2YgdGhlXG4gKiByYW5nZSB3aGljaCBkb24ndCBhZmZlY3QgaXRzIHRleHQgY29udGVudCwgd2l0aG91dCBhZmZlY3RpbmcgdGhlIHRleHQgY29udGVudFxuICogb2YgdGhlIHJhbmdlIGl0c2VsZi5cbiAqL1xuZXhwb3J0IGNsYXNzIFRleHRSYW5nZSB7XG4gIC8qKlxuICAgKiBDb25zdHJ1Y3QgYW4gaW1tdXRhYmxlIGBUZXh0UmFuZ2VgIGZyb20gYSBgc3RhcnRgIGFuZCBgZW5kYCBwb2ludC5cbiAgICpcbiAgICogQHBhcmFtIHtUZXh0UG9zaXRpb259IHN0YXJ0XG4gICAqIEBwYXJhbSB7VGV4dFBvc2l0aW9ufSBlbmRcbiAgICovXG4gIGNvbnN0cnVjdG9yKHN0YXJ0LCBlbmQpIHtcbiAgICB0aGlzLnN0YXJ0ID0gc3RhcnQ7XG4gICAgdGhpcy5lbmQgPSBlbmQ7XG4gIH1cblxuICAvKipcbiAgICogUmV0dXJuIGEgY29weSBvZiB0aGlzIHJhbmdlIHdpdGggc3RhcnQgYW5kIGVuZCBwb3NpdGlvbnMgcmVsYXRpdmUgdG8gYVxuICAgKiBnaXZlbiBhbmNlc3Rvci4gU2VlIGBUZXh0UG9zaXRpb24ucmVsYXRpdmVUb2AuXG4gICAqXG4gICAqIEBwYXJhbSB7RWxlbWVudH0gZWxlbWVudFxuICAgKi9cbiAgcmVsYXRpdmVUbyhlbGVtZW50KSB7XG4gICAgcmV0dXJuIG5ldyBUZXh0UmFuZ2UoXG4gICAgICB0aGlzLnN0YXJ0LnJlbGF0aXZlVG8oZWxlbWVudCksXG4gICAgICB0aGlzLmVuZC5yZWxhdGl2ZVRvKGVsZW1lbnQpXG4gICAgKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBSZXNvbHZlIHRoZSBgVGV4dFJhbmdlYCB0byBhIERPTSByYW5nZS5cbiAgICpcbiAgICogVGhlIHJlc3VsdGluZyBET00gUmFuZ2Ugd2lsbCBhbHdheXMgc3RhcnQgYW5kIGVuZCBpbiBhIGBUZXh0YCBub2RlLlxuICAgKiBIZW5jZSBgVGV4dFJhbmdlLmZyb21SYW5nZShyYW5nZSkudG9SYW5nZSgpYCBjYW4gYmUgdXNlZCB0byBcInNocmlua1wiIGFcbiAgICogcmFuZ2UgdG8gdGhlIHRleHQgaXQgY29udGFpbnMuXG4gICAqXG4gICAqIE1heSB0aHJvdyBpZiB0aGUgYHN0YXJ0YCBvciBgZW5kYCBwb3NpdGlvbnMgY2Fubm90IGJlIHJlc29sdmVkIHRvIGEgcmFuZ2UuXG4gICAqXG4gICAqIEByZXR1cm4ge1JhbmdlfVxuICAgKi9cbiAgdG9SYW5nZSgpIHtcbiAgICBsZXQgc3RhcnQ7XG4gICAgbGV0IGVuZDtcblxuICAgIGlmIChcbiAgICAgIHRoaXMuc3RhcnQuZWxlbWVudCA9PT0gdGhpcy5lbmQuZWxlbWVudCAmJlxuICAgICAgdGhpcy5zdGFydC5vZmZzZXQgPD0gdGhpcy5lbmQub2Zmc2V0XG4gICAgKSB7XG4gICAgICAvLyBGYXN0IHBhdGggZm9yIHN0YXJ0IGFuZCBlbmQgcG9pbnRzIGluIHNhbWUgZWxlbWVudC5cbiAgICAgIFtzdGFydCwgZW5kXSA9IHJlc29sdmVPZmZzZXRzKFxuICAgICAgICB0aGlzLnN0YXJ0LmVsZW1lbnQsXG4gICAgICAgIHRoaXMuc3RhcnQub2Zmc2V0LFxuICAgICAgICB0aGlzLmVuZC5vZmZzZXRcbiAgICAgICk7XG4gICAgfSBlbHNlIHtcbiAgICAgIHN0YXJ0ID0gdGhpcy5zdGFydC5yZXNvbHZlKHsgZGlyZWN0aW9uOiBSRVNPTFZFX0ZPUldBUkRTIH0pO1xuICAgICAgZW5kID0gdGhpcy5lbmQucmVzb2x2ZSh7IGRpcmVjdGlvbjogUkVTT0xWRV9CQUNLV0FSRFMgfSk7XG4gICAgfVxuXG4gICAgY29uc3QgcmFuZ2UgPSBuZXcgUmFuZ2UoKTtcbiAgICByYW5nZS5zZXRTdGFydChzdGFydC5ub2RlLCBzdGFydC5vZmZzZXQpO1xuICAgIHJhbmdlLnNldEVuZChlbmQubm9kZSwgZW5kLm9mZnNldCk7XG4gICAgcmV0dXJuIHJhbmdlO1xuICB9XG5cbiAgLyoqXG4gICAqIENvbnZlcnQgYW4gZXhpc3RpbmcgRE9NIGBSYW5nZWAgdG8gYSBgVGV4dFJhbmdlYFxuICAgKlxuICAgKiBAcGFyYW0ge1JhbmdlfSByYW5nZVxuICAgKiBAcmV0dXJuIHtUZXh0UmFuZ2V9XG4gICAqL1xuICBzdGF0aWMgZnJvbVJhbmdlKHJhbmdlKSB7XG4gICAgY29uc3Qgc3RhcnQgPSBUZXh0UG9zaXRpb24uZnJvbVBvaW50KFxuICAgICAgcmFuZ2Uuc3RhcnRDb250YWluZXIsXG4gICAgICByYW5nZS5zdGFydE9mZnNldFxuICAgICk7XG4gICAgY29uc3QgZW5kID0gVGV4dFBvc2l0aW9uLmZyb21Qb2ludChyYW5nZS5lbmRDb250YWluZXIsIHJhbmdlLmVuZE9mZnNldCk7XG4gICAgcmV0dXJuIG5ldyBUZXh0UmFuZ2Uoc3RhcnQsIGVuZCk7XG4gIH1cblxuICAvKipcbiAgICogUmV0dXJuIGEgYFRleHRSYW5nZWAgZnJvbSB0aGUgYHN0YXJ0YHRoIHRvIGBlbmRgdGggY2hhcmFjdGVycyBpbiBgcm9vdGAuXG4gICAqXG4gICAqIEBwYXJhbSB7RWxlbWVudH0gcm9vdFxuICAgKiBAcGFyYW0ge251bWJlcn0gc3RhcnRcbiAgICogQHBhcmFtIHtudW1iZXJ9IGVuZFxuICAgKi9cbiAgc3RhdGljIGZyb21PZmZzZXRzKHJvb3QsIHN0YXJ0LCBlbmQpIHtcbiAgICByZXR1cm4gbmV3IFRleHRSYW5nZShcbiAgICAgIG5ldyBUZXh0UG9zaXRpb24ocm9vdCwgc3RhcnQpLFxuICAgICAgbmV3IFRleHRQb3NpdGlvbihyb290LCBlbmQpXG4gICAgKTtcbiAgfVxufVxuIiwiLyoqXG4gKiBUaGlzIG1vZHVsZSBleHBvcnRzIGEgc2V0IG9mIGNsYXNzZXMgZm9yIGNvbnZlcnRpbmcgYmV0d2VlbiBET00gYFJhbmdlYFxuICogb2JqZWN0cyBhbmQgZGlmZmVyZW50IHR5cGVzIG9mIHNlbGVjdG9ycy4gSXQgaXMgbW9zdGx5IGEgdGhpbiB3cmFwcGVyIGFyb3VuZCBhXG4gKiBzZXQgb2YgYW5jaG9yaW5nIGxpYnJhcmllcy4gSXQgc2VydmVzIHR3byBtYWluIHB1cnBvc2VzOlxuICpcbiAqICAxLiBQcm92aWRpbmcgYSBjb25zaXN0ZW50IGludGVyZmFjZSBhY3Jvc3MgZGlmZmVyZW50IHR5cGVzIG9mIGFuY2hvcnMuXG4gKiAgMi4gSW5zdWxhdGluZyB0aGUgcmVzdCBvZiB0aGUgY29kZSBmcm9tIEFQSSBjaGFuZ2VzIGluIHRoZSB1bmRlcmx5aW5nIGFuY2hvcmluZ1xuICogICAgIGxpYnJhcmllcy5cbiAqL1xuXG5pbXBvcnQgeyBtYXRjaFF1b3RlIH0gZnJvbSAnLi9tYXRjaC1xdW90ZSc7XG5pbXBvcnQgeyBUZXh0UmFuZ2UsIFRleHRQb3NpdGlvbiB9IGZyb20gJy4vdGV4dC1yYW5nZSc7XG5pbXBvcnQgeyBub2RlRnJvbVhQYXRoLCB4cGF0aEZyb21Ob2RlIH0gZnJvbSAnLi94cGF0aCc7XG5cbi8qKlxuICogQHR5cGVkZWYge2ltcG9ydCgnLi4vLi4vdHlwZXMvYXBpJykuUmFuZ2VTZWxlY3Rvcn0gUmFuZ2VTZWxlY3RvclxuICogQHR5cGVkZWYge2ltcG9ydCgnLi4vLi4vdHlwZXMvYXBpJykuVGV4dFBvc2l0aW9uU2VsZWN0b3J9IFRleHRQb3NpdGlvblNlbGVjdG9yXG4gKiBAdHlwZWRlZiB7aW1wb3J0KCcuLi8uLi90eXBlcy9hcGknKS5UZXh0UXVvdGVTZWxlY3Rvcn0gVGV4dFF1b3RlU2VsZWN0b3JcbiAqL1xuXG4vKipcbiAqIENvbnZlcnRzIGJldHdlZW4gYFJhbmdlU2VsZWN0b3JgIHNlbGVjdG9ycyBhbmQgYFJhbmdlYCBvYmplY3RzLlxuICovXG5leHBvcnQgY2xhc3MgUmFuZ2VBbmNob3Ige1xuICAvKipcbiAgICogQHBhcmFtIHtOb2RlfSByb290IC0gQSByb290IGVsZW1lbnQgZnJvbSB3aGljaCB0byBhbmNob3IuXG4gICAqIEBwYXJhbSB7UmFuZ2V9IHJhbmdlIC0gIEEgcmFuZ2UgZGVzY3JpYmluZyB0aGUgYW5jaG9yLlxuICAgKi9cbiAgY29uc3RydWN0b3Iocm9vdCwgcmFuZ2UpIHtcbiAgICB0aGlzLnJvb3QgPSByb290O1xuICAgIHRoaXMucmFuZ2UgPSByYW5nZTtcbiAgfVxuXG4gIC8qKlxuICAgKiBAcGFyYW0ge05vZGV9IHJvb3QgLSAgQSByb290IGVsZW1lbnQgZnJvbSB3aGljaCB0byBhbmNob3IuXG4gICAqIEBwYXJhbSB7UmFuZ2V9IHJhbmdlIC0gIEEgcmFuZ2UgZGVzY3JpYmluZyB0aGUgYW5jaG9yLlxuICAgKi9cbiAgc3RhdGljIGZyb21SYW5nZShyb290LCByYW5nZSkge1xuICAgIHJldHVybiBuZXcgUmFuZ2VBbmNob3Iocm9vdCwgcmFuZ2UpO1xuICB9XG5cbiAgLyoqXG4gICAqIENyZWF0ZSBhbiBhbmNob3IgZnJvbSBhIHNlcmlhbGl6ZWQgYFJhbmdlU2VsZWN0b3JgIHNlbGVjdG9yLlxuICAgKlxuICAgKiBAcGFyYW0ge0VsZW1lbnR9IHJvb3QgLSAgQSByb290IGVsZW1lbnQgZnJvbSB3aGljaCB0byBhbmNob3IuXG4gICAqIEBwYXJhbSB7UmFuZ2VTZWxlY3Rvcn0gc2VsZWN0b3JcbiAgICovXG4gIHN0YXRpYyBmcm9tU2VsZWN0b3Iocm9vdCwgc2VsZWN0b3IpIHtcbiAgICBjb25zdCBzdGFydENvbnRhaW5lciA9IG5vZGVGcm9tWFBhdGgoc2VsZWN0b3Iuc3RhcnRDb250YWluZXIsIHJvb3QpO1xuICAgIGlmICghc3RhcnRDb250YWluZXIpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignRmFpbGVkIHRvIHJlc29sdmUgc3RhcnRDb250YWluZXIgWFBhdGgnKTtcbiAgICB9XG5cbiAgICBjb25zdCBlbmRDb250YWluZXIgPSBub2RlRnJvbVhQYXRoKHNlbGVjdG9yLmVuZENvbnRhaW5lciwgcm9vdCk7XG4gICAgaWYgKCFlbmRDb250YWluZXIpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignRmFpbGVkIHRvIHJlc29sdmUgZW5kQ29udGFpbmVyIFhQYXRoJyk7XG4gICAgfVxuXG4gICAgY29uc3Qgc3RhcnRQb3MgPSBUZXh0UG9zaXRpb24uZnJvbUNoYXJPZmZzZXQoXG4gICAgICBzdGFydENvbnRhaW5lcixcbiAgICAgIHNlbGVjdG9yLnN0YXJ0T2Zmc2V0XG4gICAgKTtcbiAgICBjb25zdCBlbmRQb3MgPSBUZXh0UG9zaXRpb24uZnJvbUNoYXJPZmZzZXQoXG4gICAgICBlbmRDb250YWluZXIsXG4gICAgICBzZWxlY3Rvci5lbmRPZmZzZXRcbiAgICApO1xuXG4gICAgY29uc3QgcmFuZ2UgPSBuZXcgVGV4dFJhbmdlKHN0YXJ0UG9zLCBlbmRQb3MpLnRvUmFuZ2UoKTtcbiAgICByZXR1cm4gbmV3IFJhbmdlQW5jaG9yKHJvb3QsIHJhbmdlKTtcbiAgfVxuXG4gIHRvUmFuZ2UoKSB7XG4gICAgcmV0dXJuIHRoaXMucmFuZ2U7XG4gIH1cblxuICAvKipcbiAgICogQHJldHVybiB7UmFuZ2VTZWxlY3Rvcn1cbiAgICovXG4gIHRvU2VsZWN0b3IoKSB7XG4gICAgLy8gXCJTaHJpbmtcIiB0aGUgcmFuZ2Ugc28gdGhhdCBpdCB0aWdodGx5IHdyYXBzIGl0cyB0ZXh0LiBUaGlzIGVuc3VyZXMgbW9yZVxuICAgIC8vIHByZWRpY3RhYmxlIG91dHB1dCBmb3IgYSBnaXZlbiB0ZXh0IHNlbGVjdGlvbi5cbiAgICBjb25zdCBub3JtYWxpemVkUmFuZ2UgPSBUZXh0UmFuZ2UuZnJvbVJhbmdlKHRoaXMucmFuZ2UpLnRvUmFuZ2UoKTtcblxuICAgIGNvbnN0IHRleHRSYW5nZSA9IFRleHRSYW5nZS5mcm9tUmFuZ2Uobm9ybWFsaXplZFJhbmdlKTtcbiAgICBjb25zdCBzdGFydENvbnRhaW5lciA9IHhwYXRoRnJvbU5vZGUodGV4dFJhbmdlLnN0YXJ0LmVsZW1lbnQsIHRoaXMucm9vdCk7XG4gICAgY29uc3QgZW5kQ29udGFpbmVyID0geHBhdGhGcm9tTm9kZSh0ZXh0UmFuZ2UuZW5kLmVsZW1lbnQsIHRoaXMucm9vdCk7XG5cbiAgICByZXR1cm4ge1xuICAgICAgdHlwZTogJ1JhbmdlU2VsZWN0b3InLFxuICAgICAgc3RhcnRDb250YWluZXIsXG4gICAgICBzdGFydE9mZnNldDogdGV4dFJhbmdlLnN0YXJ0Lm9mZnNldCxcbiAgICAgIGVuZENvbnRhaW5lcixcbiAgICAgIGVuZE9mZnNldDogdGV4dFJhbmdlLmVuZC5vZmZzZXQsXG4gICAgfTtcbiAgfVxufVxuXG4vKipcbiAqIENvbnZlcnRzIGJldHdlZW4gYFRleHRQb3NpdGlvblNlbGVjdG9yYCBzZWxlY3RvcnMgYW5kIGBSYW5nZWAgb2JqZWN0cy5cbiAqL1xuZXhwb3J0IGNsYXNzIFRleHRQb3NpdGlvbkFuY2hvciB7XG4gIC8qKlxuICAgKiBAcGFyYW0ge0VsZW1lbnR9IHJvb3RcbiAgICogQHBhcmFtIHtudW1iZXJ9IHN0YXJ0XG4gICAqIEBwYXJhbSB7bnVtYmVyfSBlbmRcbiAgICovXG4gIGNvbnN0cnVjdG9yKHJvb3QsIHN0YXJ0LCBlbmQpIHtcbiAgICB0aGlzLnJvb3QgPSByb290O1xuICAgIHRoaXMuc3RhcnQgPSBzdGFydDtcbiAgICB0aGlzLmVuZCA9IGVuZDtcbiAgfVxuXG4gIC8qKlxuICAgKiBAcGFyYW0ge0VsZW1lbnR9IHJvb3RcbiAgICogQHBhcmFtIHtSYW5nZX0gcmFuZ2VcbiAgICovXG4gIHN0YXRpYyBmcm9tUmFuZ2Uocm9vdCwgcmFuZ2UpIHtcbiAgICBjb25zdCB0ZXh0UmFuZ2UgPSBUZXh0UmFuZ2UuZnJvbVJhbmdlKHJhbmdlKS5yZWxhdGl2ZVRvKHJvb3QpO1xuICAgIHJldHVybiBuZXcgVGV4dFBvc2l0aW9uQW5jaG9yKFxuICAgICAgcm9vdCxcbiAgICAgIHRleHRSYW5nZS5zdGFydC5vZmZzZXQsXG4gICAgICB0ZXh0UmFuZ2UuZW5kLm9mZnNldFxuICAgICk7XG4gIH1cbiAgLyoqXG4gICAqIEBwYXJhbSB7RWxlbWVudH0gcm9vdFxuICAgKiBAcGFyYW0ge1RleHRQb3NpdGlvblNlbGVjdG9yfSBzZWxlY3RvclxuICAgKi9cbiAgc3RhdGljIGZyb21TZWxlY3Rvcihyb290LCBzZWxlY3Rvcikge1xuICAgIHJldHVybiBuZXcgVGV4dFBvc2l0aW9uQW5jaG9yKHJvb3QsIHNlbGVjdG9yLnN0YXJ0LCBzZWxlY3Rvci5lbmQpO1xuICB9XG5cbiAgLyoqXG4gICAqIEByZXR1cm4ge1RleHRQb3NpdGlvblNlbGVjdG9yfVxuICAgKi9cbiAgdG9TZWxlY3RvcigpIHtcbiAgICByZXR1cm4ge1xuICAgICAgdHlwZTogJ1RleHRQb3NpdGlvblNlbGVjdG9yJyxcbiAgICAgIHN0YXJ0OiB0aGlzLnN0YXJ0LFxuICAgICAgZW5kOiB0aGlzLmVuZCxcbiAgICB9O1xuICB9XG5cbiAgdG9SYW5nZSgpIHtcbiAgICByZXR1cm4gVGV4dFJhbmdlLmZyb21PZmZzZXRzKHRoaXMucm9vdCwgdGhpcy5zdGFydCwgdGhpcy5lbmQpLnRvUmFuZ2UoKTtcbiAgfVxufVxuXG4vKipcbiAqIEB0eXBlZGVmIFF1b3RlTWF0Y2hPcHRpb25zXG4gKiBAcHJvcCB7bnVtYmVyfSBbaGludF0gLSBFeHBlY3RlZCBwb3NpdGlvbiBvZiBtYXRjaCBpbiB0ZXh0LiBTZWUgYG1hdGNoUXVvdGVgLlxuICovXG5cbi8qKlxuICogQ29udmVydHMgYmV0d2VlbiBgVGV4dFF1b3RlU2VsZWN0b3JgIHNlbGVjdG9ycyBhbmQgYFJhbmdlYCBvYmplY3RzLlxuICovXG5leHBvcnQgY2xhc3MgVGV4dFF1b3RlQW5jaG9yIHtcbiAgLyoqXG4gICAqIEBwYXJhbSB7RWxlbWVudH0gcm9vdCAtIEEgcm9vdCBlbGVtZW50IGZyb20gd2hpY2ggdG8gYW5jaG9yLlxuICAgKiBAcGFyYW0ge3N0cmluZ30gZXhhY3RcbiAgICogQHBhcmFtIHtPYmplY3R9IGNvbnRleHRcbiAgICogICBAcGFyYW0ge3N0cmluZ30gW2NvbnRleHQucHJlZml4XVxuICAgKiAgIEBwYXJhbSB7c3RyaW5nfSBbY29udGV4dC5zdWZmaXhdXG4gICAqL1xuICBjb25zdHJ1Y3Rvcihyb290LCBleGFjdCwgY29udGV4dCA9IHt9KSB7XG4gICAgdGhpcy5yb290ID0gcm9vdDtcbiAgICB0aGlzLmV4YWN0ID0gZXhhY3Q7XG4gICAgdGhpcy5jb250ZXh0ID0gY29udGV4dDtcbiAgfVxuXG4gIC8qKlxuICAgKiBDcmVhdGUgYSBgVGV4dFF1b3RlQW5jaG9yYCBmcm9tIGEgcmFuZ2UuXG4gICAqXG4gICAqIFdpbGwgdGhyb3cgaWYgYHJhbmdlYCBkb2VzIG5vdCBjb250YWluIGFueSB0ZXh0IG5vZGVzLlxuICAgKlxuICAgKiBAcGFyYW0ge0VsZW1lbnR9IHJvb3RcbiAgICogQHBhcmFtIHtSYW5nZX0gcmFuZ2VcbiAgICovXG4gIHN0YXRpYyBmcm9tUmFuZ2Uocm9vdCwgcmFuZ2UpIHtcbiAgICBjb25zdCB0ZXh0ID0gLyoqIEB0eXBlIHtzdHJpbmd9ICovIChyb290LnRleHRDb250ZW50KTtcbiAgICBjb25zdCB0ZXh0UmFuZ2UgPSBUZXh0UmFuZ2UuZnJvbVJhbmdlKHJhbmdlKS5yZWxhdGl2ZVRvKHJvb3QpO1xuXG4gICAgY29uc3Qgc3RhcnQgPSB0ZXh0UmFuZ2Uuc3RhcnQub2Zmc2V0O1xuICAgIGNvbnN0IGVuZCA9IHRleHRSYW5nZS5lbmQub2Zmc2V0O1xuXG4gICAgLy8gTnVtYmVyIG9mIGNoYXJhY3RlcnMgYXJvdW5kIHRoZSBxdW90ZSB0byBjYXB0dXJlIGFzIGNvbnRleHQuIFdlIGN1cnJlbnRseVxuICAgIC8vIGFsd2F5cyB1c2UgYSBmaXhlZCBhbW91bnQsIGJ1dCBpdCB3b3VsZCBiZSBiZXR0ZXIgaWYgdGhpcyBjb2RlIHdhcyBhd2FyZVxuICAgIC8vIG9mIGxvZ2ljYWwgYm91bmRhcmllcyBpbiB0aGUgZG9jdW1lbnQgKHBhcmFncmFwaCwgYXJ0aWNsZSBldGMuKSB0byBhdm9pZFxuICAgIC8vIGNhcHR1cmluZyB0ZXh0IHVucmVsYXRlZCB0byB0aGUgcXVvdGUuXG4gICAgLy9cbiAgICAvLyBJbiByZWd1bGFyIHByb3NlIHRoZSBpZGVhbCBjb250ZW50IHdvdWxkIG9mdGVuIGJlIHRoZSBzdXJyb3VuZGluZyBzZW50ZW5jZS5cbiAgICAvLyBUaGlzIGlzIGEgbmF0dXJhbCB1bml0IG9mIG1lYW5pbmcgd2hpY2ggZW5hYmxlcyBkaXNwbGF5aW5nIHF1b3RlcyBpblxuICAgIC8vIGNvbnRleHQgZXZlbiB3aGVuIHRoZSBkb2N1bWVudCBpcyBub3QgYXZhaWxhYmxlLiBXZSBjb3VsZCB1c2UgYEludGwuU2VnbWVudGVyYFxuICAgIC8vIGZvciB0aGlzIHdoZW4gYXZhaWxhYmxlLlxuICAgIGNvbnN0IGNvbnRleHRMZW4gPSAzMjtcblxuICAgIHJldHVybiBuZXcgVGV4dFF1b3RlQW5jaG9yKHJvb3QsIHRleHQuc2xpY2Uoc3RhcnQsIGVuZCksIHtcbiAgICAgIHByZWZpeDogdGV4dC5zbGljZShNYXRoLm1heCgwLCBzdGFydCAtIGNvbnRleHRMZW4pLCBzdGFydCksXG4gICAgICBzdWZmaXg6IHRleHQuc2xpY2UoZW5kLCBNYXRoLm1pbih0ZXh0Lmxlbmd0aCwgZW5kICsgY29udGV4dExlbikpLFxuICAgIH0pO1xuICB9XG5cbiAgLyoqXG4gICAqIEBwYXJhbSB7RWxlbWVudH0gcm9vdFxuICAgKiBAcGFyYW0ge1RleHRRdW90ZVNlbGVjdG9yfSBzZWxlY3RvclxuICAgKi9cbiAgc3RhdGljIGZyb21TZWxlY3Rvcihyb290LCBzZWxlY3Rvcikge1xuICAgIGNvbnN0IHsgcHJlZml4LCBzdWZmaXggfSA9IHNlbGVjdG9yO1xuICAgIHJldHVybiBuZXcgVGV4dFF1b3RlQW5jaG9yKHJvb3QsIHNlbGVjdG9yLmV4YWN0LCB7IHByZWZpeCwgc3VmZml4IH0pO1xuICB9XG5cbiAgLyoqXG4gICAqIEByZXR1cm4ge1RleHRRdW90ZVNlbGVjdG9yfVxuICAgKi9cbiAgdG9TZWxlY3RvcigpIHtcbiAgICByZXR1cm4ge1xuICAgICAgdHlwZTogJ1RleHRRdW90ZVNlbGVjdG9yJyxcbiAgICAgIGV4YWN0OiB0aGlzLmV4YWN0LFxuICAgICAgcHJlZml4OiB0aGlzLmNvbnRleHQucHJlZml4LFxuICAgICAgc3VmZml4OiB0aGlzLmNvbnRleHQuc3VmZml4LFxuICAgIH07XG4gIH1cblxuICAvKipcbiAgICogQHBhcmFtIHtRdW90ZU1hdGNoT3B0aW9uc30gW29wdGlvbnNdXG4gICAqL1xuICB0b1JhbmdlKG9wdGlvbnMgPSB7fSkge1xuICAgIHJldHVybiB0aGlzLnRvUG9zaXRpb25BbmNob3Iob3B0aW9ucykudG9SYW5nZSgpO1xuICB9XG5cbiAgLyoqXG4gICAqIEBwYXJhbSB7UXVvdGVNYXRjaE9wdGlvbnN9IFtvcHRpb25zXVxuICAgKi9cbiAgdG9Qb3NpdGlvbkFuY2hvcihvcHRpb25zID0ge30pIHtcbiAgICBjb25zdCB0ZXh0ID0gLyoqIEB0eXBlIHtzdHJpbmd9ICovICh0aGlzLnJvb3QudGV4dENvbnRlbnQpO1xuICAgIGNvbnN0IG1hdGNoID0gbWF0Y2hRdW90ZSh0ZXh0LCB0aGlzLmV4YWN0LCB7XG4gICAgICAuLi50aGlzLmNvbnRleHQsXG4gICAgICBoaW50OiBvcHRpb25zLmhpbnQsXG4gICAgfSk7XG4gICAgaWYgKCFtYXRjaCkge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdRdW90ZSBub3QgZm91bmQnKTtcbiAgICB9XG4gICAgcmV0dXJuIG5ldyBUZXh0UG9zaXRpb25BbmNob3IodGhpcy5yb290LCBtYXRjaC5zdGFydCwgbWF0Y2guZW5kKTtcbiAgfVxufVxuIiwiLy9cbi8vICBDb3B5cmlnaHQgMjAyMSBSZWFkaXVtIEZvdW5kYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuXG4vLyAgVXNlIG9mIHRoaXMgc291cmNlIGNvZGUgaXMgZ292ZXJuZWQgYnkgdGhlIEJTRC1zdHlsZSBsaWNlbnNlXG4vLyAgYXZhaWxhYmxlIGluIHRoZSB0b3AtbGV2ZWwgTElDRU5TRSBmaWxlIG9mIHRoZSBwcm9qZWN0LlxuLy9cblxuaW1wb3J0IHsgVGV4dFF1b3RlQW5jaG9yIH0gZnJvbSBcIi4vdmVuZG9yL2h5cG90aGVzaXMvYW5jaG9yaW5nL3R5cGVzXCI7XG5cbi8vIENhdGNoIEpTIGVycm9ycyB0byBsb2cgdGhlbSBpbiB0aGUgYXBwLlxud2luZG93LmFkZEV2ZW50TGlzdGVuZXIoXG4gIFwiZXJyb3JcIixcbiAgZnVuY3Rpb24gKGV2ZW50KSB7XG4gICAgQW5kcm9pZC5sb2dFcnJvcihldmVudC5tZXNzYWdlLCBldmVudC5maWxlbmFtZSwgZXZlbnQubGluZW5vKTtcbiAgfSxcbiAgZmFsc2Vcbik7XG5cbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKFxuICBcImxvYWRcIixcbiAgZnVuY3Rpb24gKCkge1xuICAgIGNvbnN0IG9ic2VydmVyID0gbmV3IFJlc2l6ZU9ic2VydmVyKCgpID0+IHtcbiAgICAgIG9uVmlld3BvcnRXaWR0aENoYW5nZWQoKTtcbiAgICAgIHNuYXBDdXJyZW50T2Zmc2V0KCk7XG4gICAgfSk7XG4gICAgb2JzZXJ2ZXIub2JzZXJ2ZShkb2N1bWVudC5ib2R5KTtcbiAgfSxcbiAgZmFsc2Vcbik7XG5cbi8qKlxuICogSGF2aW5nIGFuIG9kZCBudW1iZXIgb2YgY29sdW1ucyB3aGVuIGRpc3BsYXlpbmcgdHdvIGNvbHVtbnMgcGVyIHNjcmVlbiBjYXVzZXMgc25hcHBpbmcgYW5kIHBhZ2VcbiAqIHR1cm5pbmcgaXNzdWVzLiBUbyBmaXggdGhpcywgd2UgaW5zZXJ0IGEgYmxhbmsgdmlydHVhbCBjb2x1bW4gYXQgdGhlIGVuZCBvZiB0aGUgcmVzb3VyY2UuXG4gKi9cbmZ1bmN0aW9uIGFwcGVuZFZpcnR1YWxDb2x1bW5JZk5lZWRlZCgpIHtcbiAgY29uc3QgaWQgPSBcInJlYWRpdW0tdmlydHVhbC1wYWdlXCI7XG4gIHZhciB2aXJ0dWFsQ29sID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoaWQpO1xuICBpZiAoaXNTY3JvbGxNb2RlRW5hYmxlZCgpIHx8IGdldENvbHVtbkNvdW50UGVyU2NyZWVuKCkgIT0gMikge1xuICAgIGlmICh2aXJ0dWFsQ29sKSB7XG4gICAgICB2aXJ0dWFsQ29sLnJlbW92ZSgpO1xuICAgIH1cbiAgfSBlbHNlIHtcbiAgICB2YXIgZG9jdW1lbnRXaWR0aCA9IGRvY3VtZW50LnNjcm9sbGluZ0VsZW1lbnQuc2Nyb2xsV2lkdGg7XG4gICAgdmFyIGNvbENvdW50ID0gZG9jdW1lbnRXaWR0aCAvIHBhZ2VXaWR0aDtcbiAgICB2YXIgaGFzT2RkQ29sQ291bnQgPSAoTWF0aC5yb3VuZChjb2xDb3VudCAqIDIpIC8gMikgJSAxID4gMC4xO1xuICAgIGlmIChoYXNPZGRDb2xDb3VudCkge1xuICAgICAgaWYgKHZpcnR1YWxDb2wpIHtcbiAgICAgICAgdmlydHVhbENvbC5yZW1vdmUoKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHZpcnR1YWxDb2wgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KFwiZGl2XCIpO1xuICAgICAgICB2aXJ0dWFsQ29sLnNldEF0dHJpYnV0ZShcImlkXCIsIGlkKTtcbiAgICAgICAgdmlydHVhbENvbC5zdHlsZS5icmVha0JlZm9yZSA9IFwiY29sdW1uXCI7XG4gICAgICAgIHZpcnR1YWxDb2wuaW5uZXJIVE1MID0gXCImIzgyMDM7XCI7IC8vIHplcm8td2lkdGggc3BhY2VcbiAgICAgICAgZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZCh2aXJ0dWFsQ29sKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cbn1cblxuZXhwb3J0IHZhciBwYWdlV2lkdGggPSAxO1xuXG5mdW5jdGlvbiBvblZpZXdwb3J0V2lkdGhDaGFuZ2VkKCkge1xuICAvLyBXZSBjYW4ndCByZWx5IG9uIHdpbmRvdy5pbm5lcldpZHRoIGZvciB0aGUgcGFnZVdpZHRoIG9uIEFuZHJvaWQsIGJlY2F1c2UgaWYgdGhlXG4gIC8vIGRldmljZSBwaXhlbCByYXRpbyBpcyBub3QgYW4gaW50ZWdlciwgd2UgZ2V0IHJvdW5kaW5nIGlzc3VlcyBvZmZzZXR0aW5nIHRoZSBwYWdlcy5cbiAgLy9cbiAgLy8gU2VlIGh0dHBzOi8vZ2l0aHViLmNvbS9yZWFkaXVtL3JlYWRpdW0tY3NzL2lzc3Vlcy85N1xuICAvLyBhbmQgaHR0cHM6Ly9naXRodWIuY29tL3JlYWRpdW0vcjItbmF2aWdhdG9yLWtvdGxpbi9pc3N1ZXMvMTQ2XG4gIHZhciB3aWR0aCA9IEFuZHJvaWQuZ2V0Vmlld3BvcnRXaWR0aCgpO1xuICBwYWdlV2lkdGggPSB3aWR0aCAvIHdpbmRvdy5kZXZpY2VQaXhlbFJhdGlvO1xuICBzZXRQcm9wZXJ0eShcbiAgICBcIi0tUlNfX3ZpZXdwb3J0V2lkdGhcIixcbiAgICBcImNhbGMoXCIgKyB3aWR0aCArIFwicHggLyBcIiArIHdpbmRvdy5kZXZpY2VQaXhlbFJhdGlvICsgXCIpXCJcbiAgKTtcblxuICBhcHBlbmRWaXJ0dWFsQ29sdW1uSWZOZWVkZWQoKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGdldENvbHVtbkNvdW50UGVyU2NyZWVuKCkge1xuICByZXR1cm4gcGFyc2VJbnQoXG4gICAgd2luZG93XG4gICAgICAuZ2V0Q29tcHV0ZWRTdHlsZShkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQpXG4gICAgICAuZ2V0UHJvcGVydHlWYWx1ZShcImNvbHVtbi1jb3VudFwiKVxuICApO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gaXNTY3JvbGxNb2RlRW5hYmxlZCgpIHtcbiAgY29uc3Qgc3R5bGUgPSBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuc3R5bGU7XG4gIHJldHVybiAoXG4gICAgc3R5bGUuZ2V0UHJvcGVydHlWYWx1ZShcIi0tVVNFUl9fdmlld1wiKS50cmltKCkgPT0gXCJyZWFkaXVtLXNjcm9sbC1vblwiIHx8XG4gICAgLy8gRklYTUU6IFdpbGwgbmVlZCB0byBiZSByZW1vdmVkIGluIFJlYWRpdW0gMy4wLCAtLVVTRVJfX3Njcm9sbCB3YXMgaW5jb3JyZWN0LlxuICAgIHN0eWxlLmdldFByb3BlcnR5VmFsdWUoXCItLVVTRVJfX3Njcm9sbFwiKS50cmltKCkgPT0gXCJyZWFkaXVtLXNjcm9sbC1vblwiXG4gICk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBpc1JUTCgpIHtcbiAgcmV0dXJuIGRvY3VtZW50LmJvZHkuZGlyLnRvTG93ZXJDYXNlKCkgPT0gXCJydGxcIjtcbn1cblxuLy8gU2Nyb2xsIHRvIHRoZSBnaXZlbiBUYWdJZCBpbiBkb2N1bWVudCBhbmQgc25hcC5cbmV4cG9ydCBmdW5jdGlvbiBzY3JvbGxUb0lkKGlkKSB7XG4gIHZhciBlbGVtZW50ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoaWQpO1xuICBpZiAoIWVsZW1lbnQpIHtcbiAgICByZXR1cm4gZmFsc2U7XG4gIH1cblxuICByZXR1cm4gc2Nyb2xsVG9SZWN0KGVsZW1lbnQuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCkpO1xufVxuXG4vLyBQb3NpdGlvbiBtdXN0IGJlIGluIHRoZSByYW5nZSBbMCAtIDFdLCAwLTEwMCUuXG5leHBvcnQgZnVuY3Rpb24gc2Nyb2xsVG9Qb3NpdGlvbihwb3NpdGlvbikge1xuICAvLyAgICAgICAgQW5kcm9pZC5sb2coXCJzY3JvbGxUb1Bvc2l0aW9uIFwiICsgcG9zaXRpb24pO1xuICBpZiAocG9zaXRpb24gPCAwIHx8IHBvc2l0aW9uID4gMSkge1xuICAgIHRocm93IFwic2Nyb2xsVG9Qb3NpdGlvbigpIG11c3QgYmUgZ2l2ZW4gYSBwb3NpdGlvbiBmcm9tIDAuMCB0byAgMS4wXCI7XG4gIH1cblxuICBsZXQgb2Zmc2V0O1xuICBpZiAoaXNTY3JvbGxNb2RlRW5hYmxlZCgpKSB7XG4gICAgb2Zmc2V0ID0gZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudC5zY3JvbGxIZWlnaHQgKiBwb3NpdGlvbjtcbiAgICBkb2N1bWVudC5zY3JvbGxpbmdFbGVtZW50LnNjcm9sbFRvcCA9IG9mZnNldDtcbiAgICAvLyB3aW5kb3cuc2Nyb2xsVG8oMCwgb2Zmc2V0KTtcbiAgfSBlbHNlIHtcbiAgICB2YXIgZG9jdW1lbnRXaWR0aCA9IGRvY3VtZW50LnNjcm9sbGluZ0VsZW1lbnQuc2Nyb2xsV2lkdGg7XG4gICAgdmFyIGZhY3RvciA9IGlzUlRMKCkgPyAtMSA6IDE7XG4gICAgb2Zmc2V0ID0gZG9jdW1lbnRXaWR0aCAqIHBvc2l0aW9uICogZmFjdG9yO1xuICAgIGRvY3VtZW50LnNjcm9sbGluZ0VsZW1lbnQuc2Nyb2xsTGVmdCA9IHNuYXBPZmZzZXQob2Zmc2V0KTtcbiAgfVxufVxuXG4vLyBTY3JvbGxzIHRvIHRoZSBmaXJzdCBvY2N1cnJlbmNlIG9mIHRoZSBnaXZlbiB0ZXh0IHNuaXBwZXQuXG4vL1xuLy8gVGhlIGV4cGVjdGVkIHRleHQgYXJndW1lbnQgaXMgYSBMb2NhdG9yIFRleHQgb2JqZWN0LCBhcyBkZWZpbmVkIGhlcmU6XG4vLyBodHRwczovL3JlYWRpdW0ub3JnL2FyY2hpdGVjdHVyZS9tb2RlbHMvbG9jYXRvcnMvXG5leHBvcnQgZnVuY3Rpb24gc2Nyb2xsVG9UZXh0KHRleHQpIHtcbiAgbGV0IHJhbmdlID0gcmFuZ2VGcm9tTG9jYXRvcih7IHRleHQgfSk7XG4gIGlmICghcmFuZ2UpIHtcbiAgICByZXR1cm4gZmFsc2U7XG4gIH1cbiAgc2Nyb2xsVG9SYW5nZShyYW5nZSk7XG4gIHJldHVybiB0cnVlO1xufVxuXG5mdW5jdGlvbiBzY3JvbGxUb1JhbmdlKHJhbmdlKSB7XG4gIHJldHVybiBzY3JvbGxUb1JlY3QocmFuZ2UuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCkpO1xufVxuXG5mdW5jdGlvbiBzY3JvbGxUb1JlY3QocmVjdCkge1xuICBpZiAoaXNTY3JvbGxNb2RlRW5hYmxlZCgpKSB7XG4gICAgZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudC5zY3JvbGxUb3AgPSByZWN0LnRvcCArIHdpbmRvdy5zY3JvbGxZO1xuICB9IGVsc2Uge1xuICAgIGRvY3VtZW50LnNjcm9sbGluZ0VsZW1lbnQuc2Nyb2xsTGVmdCA9IHNuYXBPZmZzZXQoXG4gICAgICByZWN0LmxlZnQgKyB3aW5kb3cuc2Nyb2xsWFxuICAgICk7XG4gIH1cblxuICByZXR1cm4gdHJ1ZTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHNjcm9sbFRvU3RhcnQoKSB7XG4gIC8vICAgICAgICBBbmRyb2lkLmxvZyhcInNjcm9sbFRvU3RhcnRcIik7XG4gIGlmICghaXNTY3JvbGxNb2RlRW5hYmxlZCgpKSB7XG4gICAgZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudC5zY3JvbGxMZWZ0ID0gMDtcbiAgfSBlbHNlIHtcbiAgICBkb2N1bWVudC5zY3JvbGxpbmdFbGVtZW50LnNjcm9sbFRvcCA9IDA7XG4gICAgd2luZG93LnNjcm9sbFRvKDAsIDApO1xuICB9XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBzY3JvbGxUb0VuZCgpIHtcbiAgLy8gICAgICAgIEFuZHJvaWQubG9nKFwic2Nyb2xsVG9FbmRcIik7XG4gIGlmICghaXNTY3JvbGxNb2RlRW5hYmxlZCgpKSB7XG4gICAgdmFyIGZhY3RvciA9IGlzUlRMKCkgPyAtMSA6IDE7XG4gICAgZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudC5zY3JvbGxMZWZ0ID0gc25hcE9mZnNldChcbiAgICAgIGRvY3VtZW50LnNjcm9sbGluZ0VsZW1lbnQuc2Nyb2xsV2lkdGggKiBmYWN0b3JcbiAgICApO1xuICB9IGVsc2Uge1xuICAgIGRvY3VtZW50LnNjcm9sbGluZ0VsZW1lbnQuc2Nyb2xsVG9wID0gZG9jdW1lbnQuYm9keS5zY3JvbGxIZWlnaHQ7XG4gICAgd2luZG93LnNjcm9sbFRvKDAsIGRvY3VtZW50LmJvZHkuc2Nyb2xsSGVpZ2h0KTtcbiAgfVxufVxuXG4vLyBSZXR1cm5zIGZhbHNlIGlmIHRoZSBwYWdlIGlzIGFscmVhZHkgYXQgdGhlIGxlZnQtbW9zdCBzY3JvbGwgb2Zmc2V0LlxuZXhwb3J0IGZ1bmN0aW9uIHNjcm9sbExlZnQoKSB7XG4gIHZhciBkb2N1bWVudFdpZHRoID0gZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudC5zY3JvbGxXaWR0aDtcbiAgdmFyIG9mZnNldCA9IHdpbmRvdy5zY3JvbGxYIC0gcGFnZVdpZHRoO1xuICB2YXIgbWluT2Zmc2V0ID0gaXNSVEwoKSA/IC0oZG9jdW1lbnRXaWR0aCAtIHBhZ2VXaWR0aCkgOiAwO1xuICByZXR1cm4gc2Nyb2xsVG9PZmZzZXQoTWF0aC5tYXgob2Zmc2V0LCBtaW5PZmZzZXQpKTtcbn1cblxuLy8gUmV0dXJucyBmYWxzZSBpZiB0aGUgcGFnZSBpcyBhbHJlYWR5IGF0IHRoZSByaWdodC1tb3N0IHNjcm9sbCBvZmZzZXQuXG5leHBvcnQgZnVuY3Rpb24gc2Nyb2xsUmlnaHQoKSB7XG4gIHZhciBkb2N1bWVudFdpZHRoID0gZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudC5zY3JvbGxXaWR0aDtcbiAgdmFyIG9mZnNldCA9IHdpbmRvdy5zY3JvbGxYICsgcGFnZVdpZHRoO1xuICB2YXIgbWF4T2Zmc2V0ID0gaXNSVEwoKSA/IDAgOiBkb2N1bWVudFdpZHRoIC0gcGFnZVdpZHRoO1xuICByZXR1cm4gc2Nyb2xsVG9PZmZzZXQoTWF0aC5taW4ob2Zmc2V0LCBtYXhPZmZzZXQpKTtcbn1cblxuLy8gU2Nyb2xscyB0byB0aGUgZ2l2ZW4gbGVmdCBvZmZzZXQuXG4vLyBSZXR1cm5zIGZhbHNlIGlmIHRoZSBwYWdlIHNjcm9sbCBwb3NpdGlvbiBpcyBhbHJlYWR5IGNsb3NlIGVub3VnaCB0byB0aGUgZ2l2ZW4gb2Zmc2V0LlxuZnVuY3Rpb24gc2Nyb2xsVG9PZmZzZXQob2Zmc2V0KSB7XG4gIC8vICAgICAgICBBbmRyb2lkLmxvZyhcInNjcm9sbFRvT2Zmc2V0IFwiICsgb2Zmc2V0KTtcbiAgaWYgKGlzU2Nyb2xsTW9kZUVuYWJsZWQoKSkge1xuICAgIHRocm93IFwiQ2FsbGVkIHNjcm9sbFRvT2Zmc2V0KCkgd2l0aCBzY3JvbGwgbW9kZSBlbmFibGVkLiBUaGlzIGNhbiBvbmx5IGJlIHVzZWQgaW4gcGFnaW5hdGVkIG1vZGUuXCI7XG4gIH1cblxuICB2YXIgY3VycmVudE9mZnNldCA9IHdpbmRvdy5zY3JvbGxYO1xuICBkb2N1bWVudC5zY3JvbGxpbmdFbGVtZW50LnNjcm9sbExlZnQgPSBzbmFwT2Zmc2V0KG9mZnNldCk7XG4gIC8vIEluIHNvbWUgY2FzZSB0aGUgc2Nyb2xsWCBjYW5ub3QgcmVhY2ggdGhlIHBvc2l0aW9uIHJlc3BlY3RpbmcgdG8gaW5uZXJXaWR0aFxuICB2YXIgZGlmZiA9IE1hdGguYWJzKGN1cnJlbnRPZmZzZXQgLSBvZmZzZXQpIC8gcGFnZVdpZHRoO1xuICByZXR1cm4gZGlmZiA+IDAuMDE7XG59XG5cbi8vIFNuYXAgdGhlIG9mZnNldCB0byB0aGUgc2NyZWVuIHdpZHRoIChwYWdlIHdpZHRoKS5cbmZ1bmN0aW9uIHNuYXBPZmZzZXQob2Zmc2V0KSB7XG4gIHZhciB2YWx1ZSA9IG9mZnNldCArIChpc1JUTCgpID8gLTEgOiAxKTtcbiAgcmV0dXJuIHZhbHVlIC0gKHZhbHVlICUgcGFnZVdpZHRoKTtcbn1cblxuLy8gU25hcHMgdGhlIGN1cnJlbnQgb2Zmc2V0IHRvIHRoZSBwYWdlIHdpZHRoLlxuZXhwb3J0IGZ1bmN0aW9uIHNuYXBDdXJyZW50T2Zmc2V0KCkge1xuICAvLyAgICAgICAgQW5kcm9pZC5sb2coXCJzbmFwQ3VycmVudE9mZnNldFwiKTtcbiAgaWYgKGlzU2Nyb2xsTW9kZUVuYWJsZWQoKSkge1xuICAgIHJldHVybjtcbiAgfVxuICB2YXIgY3VycmVudE9mZnNldCA9IHdpbmRvdy5zY3JvbGxYO1xuICAvLyBBZGRzIGhhbGYgYSBwYWdlIHRvIG1ha2Ugc3VyZSB3ZSBkb24ndCBzbmFwIHRvIHRoZSBwcmV2aW91cyBwYWdlLlxuICB2YXIgZmFjdG9yID0gaXNSVEwoKSA/IC0xIDogMTtcbiAgdmFyIGRlbHRhID0gZmFjdG9yICogKHBhZ2VXaWR0aCAvIDIpO1xuICBkb2N1bWVudC5zY3JvbGxpbmdFbGVtZW50LnNjcm9sbExlZnQgPSBzbmFwT2Zmc2V0KGN1cnJlbnRPZmZzZXQgKyBkZWx0YSk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiByYW5nZUZyb21Mb2NhdG9yKGxvY2F0b3IpIHtcbiAgdHJ5IHtcbiAgICBsZXQgbG9jYXRpb25zID0gbG9jYXRvci5sb2NhdGlvbnM7XG4gICAgbGV0IHRleHQgPSBsb2NhdG9yLnRleHQ7XG4gICAgaWYgKHRleHQgJiYgdGV4dC5oaWdobGlnaHQpIHtcbiAgICAgIHZhciByb290O1xuICAgICAgaWYgKGxvY2F0aW9ucyAmJiBsb2NhdGlvbnMuY3NzU2VsZWN0b3IpIHtcbiAgICAgICAgcm9vdCA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IobG9jYXRpb25zLmNzc1NlbGVjdG9yKTtcbiAgICAgIH1cbiAgICAgIGlmICghcm9vdCkge1xuICAgICAgICByb290ID0gZG9jdW1lbnQuYm9keTtcbiAgICAgIH1cblxuICAgICAgbGV0IGFuY2hvciA9IG5ldyBUZXh0UXVvdGVBbmNob3Iocm9vdCwgdGV4dC5oaWdobGlnaHQsIHtcbiAgICAgICAgcHJlZml4OiB0ZXh0LmJlZm9yZSxcbiAgICAgICAgc3VmZml4OiB0ZXh0LmFmdGVyLFxuICAgICAgfSk7XG4gICAgICByZXR1cm4gYW5jaG9yLnRvUmFuZ2UoKTtcbiAgICB9XG5cbiAgICBpZiAobG9jYXRpb25zKSB7XG4gICAgICB2YXIgZWxlbWVudCA9IG51bGw7XG5cbiAgICAgIGlmICghZWxlbWVudCAmJiBsb2NhdGlvbnMuY3NzU2VsZWN0b3IpIHtcbiAgICAgICAgZWxlbWVudCA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IobG9jYXRpb25zLmNzc1NlbGVjdG9yKTtcbiAgICAgIH1cblxuICAgICAgaWYgKCFlbGVtZW50ICYmIGxvY2F0aW9ucy5mcmFnbWVudHMpIHtcbiAgICAgICAgZm9yIChjb25zdCBodG1sSWQgb2YgbG9jYXRpb25zLmZyYWdtZW50cykge1xuICAgICAgICAgIGVsZW1lbnQgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZChodG1sSWQpO1xuICAgICAgICAgIGlmIChlbGVtZW50KSB7XG4gICAgICAgICAgICBicmVhaztcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH1cblxuICAgICAgaWYgKGVsZW1lbnQpIHtcbiAgICAgICAgbGV0IHJhbmdlID0gZG9jdW1lbnQuY3JlYXRlUmFuZ2UoKTtcbiAgICAgICAgcmFuZ2Uuc2V0U3RhcnRCZWZvcmUoZWxlbWVudCk7XG4gICAgICAgIHJhbmdlLnNldEVuZEFmdGVyKGVsZW1lbnQpO1xuICAgICAgICByZXR1cm4gcmFuZ2U7XG4gICAgICB9XG4gICAgfVxuICB9IGNhdGNoIChlKSB7XG4gICAgbG9nRXJyb3IoZSk7XG4gIH1cblxuICByZXR1cm4gbnVsbDtcbn1cblxuLy8vIFVzZXIgU2V0dGluZ3MuXG5cbmV4cG9ydCBmdW5jdGlvbiBzZXRDU1NQcm9wZXJ0aWVzKHByb3BlcnRpZXMpIHtcbiAgZm9yIChjb25zdCBuYW1lIGluIHByb3BlcnRpZXMpIHtcbiAgICBzZXRQcm9wZXJ0eShuYW1lLCBwcm9wZXJ0aWVzW25hbWVdKTtcbiAgfVxufVxuXG4vLyBGb3Igc2V0dGluZyB1c2VyIHNldHRpbmcuXG5leHBvcnQgZnVuY3Rpb24gc2V0UHJvcGVydHkoa2V5LCB2YWx1ZSkge1xuICBpZiAodmFsdWUgPT09IG51bGwgfHwgdmFsdWUgPT09IFwiXCIpIHtcbiAgICByZW1vdmVQcm9wZXJ0eShrZXkpO1xuICB9IGVsc2Uge1xuICAgIHZhciByb290ID0gZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50O1xuICAgIC8vIFRoZSBgIWltcG9ydGFudGAgYW5ub3RhdGlvbiBpcyBhZGRlZCB3aXRoIGBzZXRQcm9wZXJ0eSgpYCBiZWNhdXNlIGlmIGl0J3MgcGFydCBvZiB0aGVcbiAgICAvLyBgdmFsdWVgLCBpdCB3aWxsIGJlIGlnbm9yZWQgYnkgdGhlIFdlYiBWaWV3LlxuICAgIHJvb3Quc3R5bGUuc2V0UHJvcGVydHkoa2V5LCB2YWx1ZSwgXCJpbXBvcnRhbnRcIik7XG4gIH1cbn1cblxuLy8gRm9yIHJlbW92aW5nIHVzZXIgc2V0dGluZy5cbmV4cG9ydCBmdW5jdGlvbiByZW1vdmVQcm9wZXJ0eShrZXkpIHtcbiAgdmFyIHJvb3QgPSBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQ7XG5cbiAgcm9vdC5zdHlsZS5yZW1vdmVQcm9wZXJ0eShrZXkpO1xufVxuXG4vLy8gVG9vbGtpdFxuXG5leHBvcnQgZnVuY3Rpb24gbG9nKCkge1xuICB2YXIgbWVzc2FnZSA9IEFycmF5LnByb3RvdHlwZS5zbGljZS5jYWxsKGFyZ3VtZW50cykuam9pbihcIiBcIik7XG4gIEFuZHJvaWQubG9nKG1lc3NhZ2UpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gbG9nRXJyb3IobWVzc2FnZSkge1xuICBBbmRyb2lkLmxvZ0Vycm9yKG1lc3NhZ2UsIFwiXCIsIDApO1xufVxuIiwiLy9cbi8vICBDb3B5cmlnaHQgMjAyMSBSZWFkaXVtIEZvdW5kYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuXG4vLyAgVXNlIG9mIHRoaXMgc291cmNlIGNvZGUgaXMgZ292ZXJuZWQgYnkgdGhlIEJTRC1zdHlsZSBsaWNlbnNlXG4vLyAgYXZhaWxhYmxlIGluIHRoZSB0b3AtbGV2ZWwgTElDRU5TRSBmaWxlIG9mIHRoZSBwcm9qZWN0LlxuLy9cblxuaW1wb3J0IHsgbG9nIGFzIGxvZ05hdGl2ZSB9IGZyb20gXCIuL3V0aWxzXCI7XG5cbmNvbnN0IGRlYnVnID0gZmFsc2U7XG5cbi8qKlxuICogQ29udmVydHMgYSBET01SZWN0IGludG8gYSBKU09OIG9iamVjdCB1bmRlcnN0YW5kYWJsZSBieSB0aGUgbmF0aXZlIHNpZGUuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiB0b05hdGl2ZVJlY3QocmVjdCkge1xuICBjb25zdCBwaXhlbFJhdGlvID0gd2luZG93LmRldmljZVBpeGVsUmF0aW87XG4gIGNvbnN0IHdpZHRoID0gcmVjdC53aWR0aCAqIHBpeGVsUmF0aW87XG4gIGNvbnN0IGhlaWdodCA9IHJlY3QuaGVpZ2h0ICogcGl4ZWxSYXRpbztcbiAgY29uc3QgbGVmdCA9IHJlY3QubGVmdCAqIHBpeGVsUmF0aW87XG4gIGNvbnN0IHRvcCA9IHJlY3QudG9wICogcGl4ZWxSYXRpbztcbiAgY29uc3QgcmlnaHQgPSBsZWZ0ICsgd2lkdGg7XG4gIGNvbnN0IGJvdHRvbSA9IHRvcCArIGhlaWdodDtcbiAgcmV0dXJuIHsgd2lkdGgsIGhlaWdodCwgbGVmdCwgdG9wLCByaWdodCwgYm90dG9tIH07XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBnZXRDbGllbnRSZWN0c05vT3ZlcmxhcChcbiAgcmFuZ2UsXG4gIGRvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHNcbikge1xuICBsZXQgY2xpZW50UmVjdHMgPSByYW5nZS5nZXRDbGllbnRSZWN0cygpO1xuXG4gIGNvbnN0IHRvbGVyYW5jZSA9IDE7XG4gIGNvbnN0IG9yaWdpbmFsUmVjdHMgPSBbXTtcbiAgZm9yIChjb25zdCByYW5nZUNsaWVudFJlY3Qgb2YgY2xpZW50UmVjdHMpIHtcbiAgICBvcmlnaW5hbFJlY3RzLnB1c2goe1xuICAgICAgYm90dG9tOiByYW5nZUNsaWVudFJlY3QuYm90dG9tLFxuICAgICAgaGVpZ2h0OiByYW5nZUNsaWVudFJlY3QuaGVpZ2h0LFxuICAgICAgbGVmdDogcmFuZ2VDbGllbnRSZWN0LmxlZnQsXG4gICAgICByaWdodDogcmFuZ2VDbGllbnRSZWN0LnJpZ2h0LFxuICAgICAgdG9wOiByYW5nZUNsaWVudFJlY3QudG9wLFxuICAgICAgd2lkdGg6IHJhbmdlQ2xpZW50UmVjdC53aWR0aCxcbiAgICB9KTtcbiAgfVxuICBjb25zdCBtZXJnZWRSZWN0cyA9IG1lcmdlVG91Y2hpbmdSZWN0cyhcbiAgICBvcmlnaW5hbFJlY3RzLFxuICAgIHRvbGVyYW5jZSxcbiAgICBkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzXG4gICk7XG4gIGNvbnN0IG5vQ29udGFpbmVkUmVjdHMgPSByZW1vdmVDb250YWluZWRSZWN0cyhtZXJnZWRSZWN0cywgdG9sZXJhbmNlKTtcbiAgY29uc3QgbmV3UmVjdHMgPSByZXBsYWNlT3ZlcmxhcGluZ1JlY3RzKG5vQ29udGFpbmVkUmVjdHMpO1xuICBjb25zdCBtaW5BcmVhID0gMiAqIDI7XG4gIGZvciAobGV0IGogPSBuZXdSZWN0cy5sZW5ndGggLSAxOyBqID49IDA7IGotLSkge1xuICAgIGNvbnN0IHJlY3QgPSBuZXdSZWN0c1tqXTtcbiAgICBjb25zdCBiaWdFbm91Z2ggPSByZWN0LndpZHRoICogcmVjdC5oZWlnaHQgPiBtaW5BcmVhO1xuICAgIGlmICghYmlnRW5vdWdoKSB7XG4gICAgICBpZiAobmV3UmVjdHMubGVuZ3RoID4gMSkge1xuICAgICAgICBsb2coXCJDTElFTlQgUkVDVDogcmVtb3ZlIHNtYWxsXCIpO1xuICAgICAgICBuZXdSZWN0cy5zcGxpY2UoaiwgMSk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBsb2coXCJDTElFTlQgUkVDVDogcmVtb3ZlIHNtYWxsLCBidXQga2VlcCBvdGhlcndpc2UgZW1wdHkhXCIpO1xuICAgICAgICBicmVhaztcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgbG9nKGBDTElFTlQgUkVDVDogcmVkdWNlZCAke29yaWdpbmFsUmVjdHMubGVuZ3RofSAtLT4gJHtuZXdSZWN0cy5sZW5ndGh9YCk7XG4gIHJldHVybiBuZXdSZWN0cztcbn1cblxuZnVuY3Rpb24gbWVyZ2VUb3VjaGluZ1JlY3RzKFxuICByZWN0cyxcbiAgdG9sZXJhbmNlLFxuICBkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzXG4pIHtcbiAgZm9yIChsZXQgaSA9IDA7IGkgPCByZWN0cy5sZW5ndGg7IGkrKykge1xuICAgIGZvciAobGV0IGogPSBpICsgMTsgaiA8IHJlY3RzLmxlbmd0aDsgaisrKSB7XG4gICAgICBjb25zdCByZWN0MSA9IHJlY3RzW2ldO1xuICAgICAgY29uc3QgcmVjdDIgPSByZWN0c1tqXTtcbiAgICAgIGlmIChyZWN0MSA9PT0gcmVjdDIpIHtcbiAgICAgICAgbG9nKFwibWVyZ2VUb3VjaGluZ1JlY3RzIHJlY3QxID09PSByZWN0MiA/PyFcIik7XG4gICAgICAgIGNvbnRpbnVlO1xuICAgICAgfVxuICAgICAgY29uc3QgcmVjdHNMaW5lVXBWZXJ0aWNhbGx5ID1cbiAgICAgICAgYWxtb3N0RXF1YWwocmVjdDEudG9wLCByZWN0Mi50b3AsIHRvbGVyYW5jZSkgJiZcbiAgICAgICAgYWxtb3N0RXF1YWwocmVjdDEuYm90dG9tLCByZWN0Mi5ib3R0b20sIHRvbGVyYW5jZSk7XG4gICAgICBjb25zdCByZWN0c0xpbmVVcEhvcml6b250YWxseSA9XG4gICAgICAgIGFsbW9zdEVxdWFsKHJlY3QxLmxlZnQsIHJlY3QyLmxlZnQsIHRvbGVyYW5jZSkgJiZcbiAgICAgICAgYWxtb3N0RXF1YWwocmVjdDEucmlnaHQsIHJlY3QyLnJpZ2h0LCB0b2xlcmFuY2UpO1xuICAgICAgY29uc3QgaG9yaXpvbnRhbEFsbG93ZWQgPSAhZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0cztcbiAgICAgIGNvbnN0IGFsaWduZWQgPVxuICAgICAgICAocmVjdHNMaW5lVXBIb3Jpem9udGFsbHkgJiYgaG9yaXpvbnRhbEFsbG93ZWQpIHx8XG4gICAgICAgIChyZWN0c0xpbmVVcFZlcnRpY2FsbHkgJiYgIXJlY3RzTGluZVVwSG9yaXpvbnRhbGx5KTtcbiAgICAgIGNvbnN0IGNhbk1lcmdlID0gYWxpZ25lZCAmJiByZWN0c1RvdWNoT3JPdmVybGFwKHJlY3QxLCByZWN0MiwgdG9sZXJhbmNlKTtcbiAgICAgIGlmIChjYW5NZXJnZSkge1xuICAgICAgICBsb2coXG4gICAgICAgICAgYENMSUVOVCBSRUNUOiBtZXJnaW5nIHR3byBpbnRvIG9uZSwgVkVSVElDQUw6ICR7cmVjdHNMaW5lVXBWZXJ0aWNhbGx5fSBIT1JJWk9OVEFMOiAke3JlY3RzTGluZVVwSG9yaXpvbnRhbGx5fSAoJHtkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzfSlgXG4gICAgICAgICk7XG4gICAgICAgIGNvbnN0IG5ld1JlY3RzID0gcmVjdHMuZmlsdGVyKChyZWN0KSA9PiB7XG4gICAgICAgICAgcmV0dXJuIHJlY3QgIT09IHJlY3QxICYmIHJlY3QgIT09IHJlY3QyO1xuICAgICAgICB9KTtcbiAgICAgICAgY29uc3QgcmVwbGFjZW1lbnRDbGllbnRSZWN0ID0gZ2V0Qm91bmRpbmdSZWN0KHJlY3QxLCByZWN0Mik7XG4gICAgICAgIG5ld1JlY3RzLnB1c2gocmVwbGFjZW1lbnRDbGllbnRSZWN0KTtcbiAgICAgICAgcmV0dXJuIG1lcmdlVG91Y2hpbmdSZWN0cyhcbiAgICAgICAgICBuZXdSZWN0cyxcbiAgICAgICAgICB0b2xlcmFuY2UsXG4gICAgICAgICAgZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0c1xuICAgICAgICApO1xuICAgICAgfVxuICAgIH1cbiAgfVxuICByZXR1cm4gcmVjdHM7XG59XG5cbmZ1bmN0aW9uIGdldEJvdW5kaW5nUmVjdChyZWN0MSwgcmVjdDIpIHtcbiAgY29uc3QgbGVmdCA9IE1hdGgubWluKHJlY3QxLmxlZnQsIHJlY3QyLmxlZnQpO1xuICBjb25zdCByaWdodCA9IE1hdGgubWF4KHJlY3QxLnJpZ2h0LCByZWN0Mi5yaWdodCk7XG4gIGNvbnN0IHRvcCA9IE1hdGgubWluKHJlY3QxLnRvcCwgcmVjdDIudG9wKTtcbiAgY29uc3QgYm90dG9tID0gTWF0aC5tYXgocmVjdDEuYm90dG9tLCByZWN0Mi5ib3R0b20pO1xuICByZXR1cm4ge1xuICAgIGJvdHRvbSxcbiAgICBoZWlnaHQ6IGJvdHRvbSAtIHRvcCxcbiAgICBsZWZ0LFxuICAgIHJpZ2h0LFxuICAgIHRvcCxcbiAgICB3aWR0aDogcmlnaHQgLSBsZWZ0LFxuICB9O1xufVxuXG5mdW5jdGlvbiByZW1vdmVDb250YWluZWRSZWN0cyhyZWN0cywgdG9sZXJhbmNlKSB7XG4gIGNvbnN0IHJlY3RzVG9LZWVwID0gbmV3IFNldChyZWN0cyk7XG4gIGZvciAoY29uc3QgcmVjdCBvZiByZWN0cykge1xuICAgIGNvbnN0IGJpZ0Vub3VnaCA9IHJlY3Qud2lkdGggPiAxICYmIHJlY3QuaGVpZ2h0ID4gMTtcbiAgICBpZiAoIWJpZ0Vub3VnaCkge1xuICAgICAgbG9nKFwiQ0xJRU5UIFJFQ1Q6IHJlbW92ZSB0aW55XCIpO1xuICAgICAgcmVjdHNUb0tlZXAuZGVsZXRlKHJlY3QpO1xuICAgICAgY29udGludWU7XG4gICAgfVxuICAgIGZvciAoY29uc3QgcG9zc2libHlDb250YWluaW5nUmVjdCBvZiByZWN0cykge1xuICAgICAgaWYgKHJlY3QgPT09IHBvc3NpYmx5Q29udGFpbmluZ1JlY3QpIHtcbiAgICAgICAgY29udGludWU7XG4gICAgICB9XG4gICAgICBpZiAoIXJlY3RzVG9LZWVwLmhhcyhwb3NzaWJseUNvbnRhaW5pbmdSZWN0KSkge1xuICAgICAgICBjb250aW51ZTtcbiAgICAgIH1cbiAgICAgIGlmIChyZWN0Q29udGFpbnMocG9zc2libHlDb250YWluaW5nUmVjdCwgcmVjdCwgdG9sZXJhbmNlKSkge1xuICAgICAgICBsb2coXCJDTElFTlQgUkVDVDogcmVtb3ZlIGNvbnRhaW5lZFwiKTtcbiAgICAgICAgcmVjdHNUb0tlZXAuZGVsZXRlKHJlY3QpO1xuICAgICAgICBicmVhaztcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgcmV0dXJuIEFycmF5LmZyb20ocmVjdHNUb0tlZXApO1xufVxuXG5mdW5jdGlvbiByZWN0Q29udGFpbnMocmVjdDEsIHJlY3QyLCB0b2xlcmFuY2UpIHtcbiAgcmV0dXJuIChcbiAgICByZWN0Q29udGFpbnNQb2ludChyZWN0MSwgcmVjdDIubGVmdCwgcmVjdDIudG9wLCB0b2xlcmFuY2UpICYmXG4gICAgcmVjdENvbnRhaW5zUG9pbnQocmVjdDEsIHJlY3QyLnJpZ2h0LCByZWN0Mi50b3AsIHRvbGVyYW5jZSkgJiZcbiAgICByZWN0Q29udGFpbnNQb2ludChyZWN0MSwgcmVjdDIubGVmdCwgcmVjdDIuYm90dG9tLCB0b2xlcmFuY2UpICYmXG4gICAgcmVjdENvbnRhaW5zUG9pbnQocmVjdDEsIHJlY3QyLnJpZ2h0LCByZWN0Mi5ib3R0b20sIHRvbGVyYW5jZSlcbiAgKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHJlY3RDb250YWluc1BvaW50KHJlY3QsIHgsIHksIHRvbGVyYW5jZSkge1xuICByZXR1cm4gKFxuICAgIChyZWN0LmxlZnQgPCB4IHx8IGFsbW9zdEVxdWFsKHJlY3QubGVmdCwgeCwgdG9sZXJhbmNlKSkgJiZcbiAgICAocmVjdC5yaWdodCA+IHggfHwgYWxtb3N0RXF1YWwocmVjdC5yaWdodCwgeCwgdG9sZXJhbmNlKSkgJiZcbiAgICAocmVjdC50b3AgPCB5IHx8IGFsbW9zdEVxdWFsKHJlY3QudG9wLCB5LCB0b2xlcmFuY2UpKSAmJlxuICAgIChyZWN0LmJvdHRvbSA+IHkgfHwgYWxtb3N0RXF1YWwocmVjdC5ib3R0b20sIHksIHRvbGVyYW5jZSkpXG4gICk7XG59XG5cbmZ1bmN0aW9uIHJlcGxhY2VPdmVybGFwaW5nUmVjdHMocmVjdHMpIHtcbiAgZm9yIChsZXQgaSA9IDA7IGkgPCByZWN0cy5sZW5ndGg7IGkrKykge1xuICAgIGZvciAobGV0IGogPSBpICsgMTsgaiA8IHJlY3RzLmxlbmd0aDsgaisrKSB7XG4gICAgICBjb25zdCByZWN0MSA9IHJlY3RzW2ldO1xuICAgICAgY29uc3QgcmVjdDIgPSByZWN0c1tqXTtcbiAgICAgIGlmIChyZWN0MSA9PT0gcmVjdDIpIHtcbiAgICAgICAgbG9nKFwicmVwbGFjZU92ZXJsYXBpbmdSZWN0cyByZWN0MSA9PT0gcmVjdDIgPz8hXCIpO1xuICAgICAgICBjb250aW51ZTtcbiAgICAgIH1cbiAgICAgIGlmIChyZWN0c1RvdWNoT3JPdmVybGFwKHJlY3QxLCByZWN0MiwgLTEpKSB7XG4gICAgICAgIGxldCB0b0FkZCA9IFtdO1xuICAgICAgICBsZXQgdG9SZW1vdmU7XG4gICAgICAgIGNvbnN0IHN1YnRyYWN0UmVjdHMxID0gcmVjdFN1YnRyYWN0KHJlY3QxLCByZWN0Mik7XG4gICAgICAgIGlmIChzdWJ0cmFjdFJlY3RzMS5sZW5ndGggPT09IDEpIHtcbiAgICAgICAgICB0b0FkZCA9IHN1YnRyYWN0UmVjdHMxO1xuICAgICAgICAgIHRvUmVtb3ZlID0gcmVjdDE7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgY29uc3Qgc3VidHJhY3RSZWN0czIgPSByZWN0U3VidHJhY3QocmVjdDIsIHJlY3QxKTtcbiAgICAgICAgICBpZiAoc3VidHJhY3RSZWN0czEubGVuZ3RoIDwgc3VidHJhY3RSZWN0czIubGVuZ3RoKSB7XG4gICAgICAgICAgICB0b0FkZCA9IHN1YnRyYWN0UmVjdHMxO1xuICAgICAgICAgICAgdG9SZW1vdmUgPSByZWN0MTtcbiAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgdG9BZGQgPSBzdWJ0cmFjdFJlY3RzMjtcbiAgICAgICAgICAgIHRvUmVtb3ZlID0gcmVjdDI7XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgIGxvZyhgQ0xJRU5UIFJFQ1Q6IG92ZXJsYXAsIGN1dCBvbmUgcmVjdCBpbnRvICR7dG9BZGQubGVuZ3RofWApO1xuICAgICAgICBjb25zdCBuZXdSZWN0cyA9IHJlY3RzLmZpbHRlcigocmVjdCkgPT4ge1xuICAgICAgICAgIHJldHVybiByZWN0ICE9PSB0b1JlbW92ZTtcbiAgICAgICAgfSk7XG4gICAgICAgIEFycmF5LnByb3RvdHlwZS5wdXNoLmFwcGx5KG5ld1JlY3RzLCB0b0FkZCk7XG4gICAgICAgIHJldHVybiByZXBsYWNlT3ZlcmxhcGluZ1JlY3RzKG5ld1JlY3RzKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgcmV0dXJuIHJlY3RzO1xufVxuXG5mdW5jdGlvbiByZWN0U3VidHJhY3QocmVjdDEsIHJlY3QyKSB7XG4gIGNvbnN0IHJlY3RJbnRlcnNlY3RlZCA9IHJlY3RJbnRlcnNlY3QocmVjdDIsIHJlY3QxKTtcbiAgaWYgKHJlY3RJbnRlcnNlY3RlZC5oZWlnaHQgPT09IDAgfHwgcmVjdEludGVyc2VjdGVkLndpZHRoID09PSAwKSB7XG4gICAgcmV0dXJuIFtyZWN0MV07XG4gIH1cbiAgY29uc3QgcmVjdHMgPSBbXTtcbiAge1xuICAgIGNvbnN0IHJlY3RBID0ge1xuICAgICAgYm90dG9tOiByZWN0MS5ib3R0b20sXG4gICAgICBoZWlnaHQ6IDAsXG4gICAgICBsZWZ0OiByZWN0MS5sZWZ0LFxuICAgICAgcmlnaHQ6IHJlY3RJbnRlcnNlY3RlZC5sZWZ0LFxuICAgICAgdG9wOiByZWN0MS50b3AsXG4gICAgICB3aWR0aDogMCxcbiAgICB9O1xuICAgIHJlY3RBLndpZHRoID0gcmVjdEEucmlnaHQgLSByZWN0QS5sZWZ0O1xuICAgIHJlY3RBLmhlaWdodCA9IHJlY3RBLmJvdHRvbSAtIHJlY3RBLnRvcDtcbiAgICBpZiAocmVjdEEuaGVpZ2h0ICE9PSAwICYmIHJlY3RBLndpZHRoICE9PSAwKSB7XG4gICAgICByZWN0cy5wdXNoKHJlY3RBKTtcbiAgICB9XG4gIH1cbiAge1xuICAgIGNvbnN0IHJlY3RCID0ge1xuICAgICAgYm90dG9tOiByZWN0SW50ZXJzZWN0ZWQudG9wLFxuICAgICAgaGVpZ2h0OiAwLFxuICAgICAgbGVmdDogcmVjdEludGVyc2VjdGVkLmxlZnQsXG4gICAgICByaWdodDogcmVjdEludGVyc2VjdGVkLnJpZ2h0LFxuICAgICAgdG9wOiByZWN0MS50b3AsXG4gICAgICB3aWR0aDogMCxcbiAgICB9O1xuICAgIHJlY3RCLndpZHRoID0gcmVjdEIucmlnaHQgLSByZWN0Qi5sZWZ0O1xuICAgIHJlY3RCLmhlaWdodCA9IHJlY3RCLmJvdHRvbSAtIHJlY3RCLnRvcDtcbiAgICBpZiAocmVjdEIuaGVpZ2h0ICE9PSAwICYmIHJlY3RCLndpZHRoICE9PSAwKSB7XG4gICAgICByZWN0cy5wdXNoKHJlY3RCKTtcbiAgICB9XG4gIH1cbiAge1xuICAgIGNvbnN0IHJlY3RDID0ge1xuICAgICAgYm90dG9tOiByZWN0MS5ib3R0b20sXG4gICAgICBoZWlnaHQ6IDAsXG4gICAgICBsZWZ0OiByZWN0SW50ZXJzZWN0ZWQubGVmdCxcbiAgICAgIHJpZ2h0OiByZWN0SW50ZXJzZWN0ZWQucmlnaHQsXG4gICAgICB0b3A6IHJlY3RJbnRlcnNlY3RlZC5ib3R0b20sXG4gICAgICB3aWR0aDogMCxcbiAgICB9O1xuICAgIHJlY3RDLndpZHRoID0gcmVjdEMucmlnaHQgLSByZWN0Qy5sZWZ0O1xuICAgIHJlY3RDLmhlaWdodCA9IHJlY3RDLmJvdHRvbSAtIHJlY3RDLnRvcDtcbiAgICBpZiAocmVjdEMuaGVpZ2h0ICE9PSAwICYmIHJlY3RDLndpZHRoICE9PSAwKSB7XG4gICAgICByZWN0cy5wdXNoKHJlY3RDKTtcbiAgICB9XG4gIH1cbiAge1xuICAgIGNvbnN0IHJlY3REID0ge1xuICAgICAgYm90dG9tOiByZWN0MS5ib3R0b20sXG4gICAgICBoZWlnaHQ6IDAsXG4gICAgICBsZWZ0OiByZWN0SW50ZXJzZWN0ZWQucmlnaHQsXG4gICAgICByaWdodDogcmVjdDEucmlnaHQsXG4gICAgICB0b3A6IHJlY3QxLnRvcCxcbiAgICAgIHdpZHRoOiAwLFxuICAgIH07XG4gICAgcmVjdEQud2lkdGggPSByZWN0RC5yaWdodCAtIHJlY3RELmxlZnQ7XG4gICAgcmVjdEQuaGVpZ2h0ID0gcmVjdEQuYm90dG9tIC0gcmVjdEQudG9wO1xuICAgIGlmIChyZWN0RC5oZWlnaHQgIT09IDAgJiYgcmVjdEQud2lkdGggIT09IDApIHtcbiAgICAgIHJlY3RzLnB1c2gocmVjdEQpO1xuICAgIH1cbiAgfVxuICByZXR1cm4gcmVjdHM7XG59XG5cbmZ1bmN0aW9uIHJlY3RJbnRlcnNlY3QocmVjdDEsIHJlY3QyKSB7XG4gIGNvbnN0IG1heExlZnQgPSBNYXRoLm1heChyZWN0MS5sZWZ0LCByZWN0Mi5sZWZ0KTtcbiAgY29uc3QgbWluUmlnaHQgPSBNYXRoLm1pbihyZWN0MS5yaWdodCwgcmVjdDIucmlnaHQpO1xuICBjb25zdCBtYXhUb3AgPSBNYXRoLm1heChyZWN0MS50b3AsIHJlY3QyLnRvcCk7XG4gIGNvbnN0IG1pbkJvdHRvbSA9IE1hdGgubWluKHJlY3QxLmJvdHRvbSwgcmVjdDIuYm90dG9tKTtcbiAgcmV0dXJuIHtcbiAgICBib3R0b206IG1pbkJvdHRvbSxcbiAgICBoZWlnaHQ6IE1hdGgubWF4KDAsIG1pbkJvdHRvbSAtIG1heFRvcCksXG4gICAgbGVmdDogbWF4TGVmdCxcbiAgICByaWdodDogbWluUmlnaHQsXG4gICAgdG9wOiBtYXhUb3AsXG4gICAgd2lkdGg6IE1hdGgubWF4KDAsIG1pblJpZ2h0IC0gbWF4TGVmdCksXG4gIH07XG59XG5cbmZ1bmN0aW9uIHJlY3RzVG91Y2hPck92ZXJsYXAocmVjdDEsIHJlY3QyLCB0b2xlcmFuY2UpIHtcbiAgcmV0dXJuIChcbiAgICAocmVjdDEubGVmdCA8IHJlY3QyLnJpZ2h0IHx8XG4gICAgICAodG9sZXJhbmNlID49IDAgJiYgYWxtb3N0RXF1YWwocmVjdDEubGVmdCwgcmVjdDIucmlnaHQsIHRvbGVyYW5jZSkpKSAmJlxuICAgIChyZWN0Mi5sZWZ0IDwgcmVjdDEucmlnaHQgfHxcbiAgICAgICh0b2xlcmFuY2UgPj0gMCAmJiBhbG1vc3RFcXVhbChyZWN0Mi5sZWZ0LCByZWN0MS5yaWdodCwgdG9sZXJhbmNlKSkpICYmXG4gICAgKHJlY3QxLnRvcCA8IHJlY3QyLmJvdHRvbSB8fFxuICAgICAgKHRvbGVyYW5jZSA+PSAwICYmIGFsbW9zdEVxdWFsKHJlY3QxLnRvcCwgcmVjdDIuYm90dG9tLCB0b2xlcmFuY2UpKSkgJiZcbiAgICAocmVjdDIudG9wIDwgcmVjdDEuYm90dG9tIHx8XG4gICAgICAodG9sZXJhbmNlID49IDAgJiYgYWxtb3N0RXF1YWwocmVjdDIudG9wLCByZWN0MS5ib3R0b20sIHRvbGVyYW5jZSkpKVxuICApO1xufVxuXG5mdW5jdGlvbiBhbG1vc3RFcXVhbChhLCBiLCB0b2xlcmFuY2UpIHtcbiAgcmV0dXJuIE1hdGguYWJzKGEgLSBiKSA8PSB0b2xlcmFuY2U7XG59XG5cbmZ1bmN0aW9uIGxvZygpIHtcbiAgaWYgKGRlYnVnKSB7XG4gICAgbG9nTmF0aXZlLmFwcGx5KG51bGwsIGFyZ3VtZW50cyk7XG4gIH1cbn1cbiIsIi8vXG4vLyAgQ29weXJpZ2h0IDIwMjEgUmVhZGl1bSBGb3VuZGF0aW9uLiBBbGwgcmlnaHRzIHJlc2VydmVkLlxuLy8gIFVzZSBvZiB0aGlzIHNvdXJjZSBjb2RlIGlzIGdvdmVybmVkIGJ5IHRoZSBCU0Qtc3R5bGUgbGljZW5zZVxuLy8gIGF2YWlsYWJsZSBpbiB0aGUgdG9wLWxldmVsIExJQ0VOU0UgZmlsZSBvZiB0aGUgcHJvamVjdC5cbi8vXG5cbmltcG9ydCB7XG4gIGdldENsaWVudFJlY3RzTm9PdmVybGFwLFxuICByZWN0Q29udGFpbnNQb2ludCxcbiAgdG9OYXRpdmVSZWN0LFxufSBmcm9tIFwiLi9yZWN0XCI7XG5pbXBvcnQgeyBsb2csIGxvZ0Vycm9yLCByYW5nZUZyb21Mb2NhdG9yIH0gZnJvbSBcIi4vdXRpbHNcIjtcblxubGV0IHN0eWxlcyA9IG5ldyBNYXAoKTtcbmxldCBncm91cHMgPSBuZXcgTWFwKCk7XG52YXIgbGFzdEdyb3VwSWQgPSAwO1xuXG4vKipcbiAqIFJlZ2lzdGVycyBhIGxpc3Qgb2YgYWRkaXRpb25hbCBzdXBwb3J0ZWQgRGVjb3JhdGlvbiBUZW1wbGF0ZXMuXG4gKlxuICogRWFjaCB0ZW1wbGF0ZSBvYmplY3QgaXMgaW5kZXhlZCBieSB0aGUgc3R5bGUgSUQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZWdpc3RlclRlbXBsYXRlcyhuZXdTdHlsZXMpIHtcbiAgdmFyIHN0eWxlc2hlZXQgPSBcIlwiO1xuXG4gIGZvciAoY29uc3QgW2lkLCBzdHlsZV0gb2YgT2JqZWN0LmVudHJpZXMobmV3U3R5bGVzKSkge1xuICAgIHN0eWxlcy5zZXQoaWQsIHN0eWxlKTtcbiAgICBpZiAoc3R5bGUuc3R5bGVzaGVldCkge1xuICAgICAgc3R5bGVzaGVldCArPSBzdHlsZS5zdHlsZXNoZWV0ICsgXCJcXG5cIjtcbiAgICB9XG4gIH1cblxuICBpZiAoc3R5bGVzaGVldCkge1xuICAgIGxldCBzdHlsZUVsZW1lbnQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KFwic3R5bGVcIik7XG4gICAgc3R5bGVFbGVtZW50LmlubmVySFRNTCA9IHN0eWxlc2hlZXQ7XG4gICAgZG9jdW1lbnQuZ2V0RWxlbWVudHNCeVRhZ05hbWUoXCJoZWFkXCIpWzBdLmFwcGVuZENoaWxkKHN0eWxlRWxlbWVudCk7XG4gIH1cbn1cblxuLyoqXG4gKiBSZXR1cm5zIGFuIGluc3RhbmNlIG9mIERlY29yYXRpb25Hcm91cCBmb3IgdGhlIGdpdmVuIGdyb3VwIG5hbWUuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBnZXREZWNvcmF0aW9ucyhncm91cE5hbWUpIHtcbiAgdmFyIGdyb3VwID0gZ3JvdXBzLmdldChncm91cE5hbWUpO1xuICBpZiAoIWdyb3VwKSB7XG4gICAgbGV0IGlkID0gXCJyMi1kZWNvcmF0aW9uLVwiICsgbGFzdEdyb3VwSWQrKztcbiAgICBncm91cCA9IERlY29yYXRpb25Hcm91cChpZCwgZ3JvdXBOYW1lKTtcbiAgICBncm91cHMuc2V0KGdyb3VwTmFtZSwgZ3JvdXApO1xuICB9XG4gIHJldHVybiBncm91cDtcbn1cblxuLyoqXG4gKiBIYW5kbGVzIGNsaWNrIGV2ZW50cyBvbiBhIERlY29yYXRpb24uXG4gKiBSZXR1cm5zIHdoZXRoZXIgYSBkZWNvcmF0aW9uIG1hdGNoZWQgdGhpcyBldmVudC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGhhbmRsZURlY29yYXRpb25DbGlja0V2ZW50KGV2ZW50LCBjbGlja0V2ZW50KSB7XG4gIGlmIChncm91cHMuc2l6ZSA9PT0gMCkge1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuXG4gIGZ1bmN0aW9uIGZpbmRUYXJnZXQoKSB7XG4gICAgZm9yIChjb25zdCBbZ3JvdXAsIGdyb3VwQ29udGVudF0gb2YgZ3JvdXBzKSB7XG4gICAgICBmb3IgKGNvbnN0IGl0ZW0gb2YgZ3JvdXBDb250ZW50Lml0ZW1zLnJldmVyc2UoKSkge1xuICAgICAgICBpZiAoIWl0ZW0uY2xpY2thYmxlRWxlbWVudHMpIHtcbiAgICAgICAgICBjb250aW51ZTtcbiAgICAgICAgfVxuICAgICAgICBmb3IgKGNvbnN0IGVsZW1lbnQgb2YgaXRlbS5jbGlja2FibGVFbGVtZW50cykge1xuICAgICAgICAgIGxldCByZWN0ID0gZWxlbWVudC5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKS50b0pTT04oKTtcbiAgICAgICAgICBpZiAocmVjdENvbnRhaW5zUG9pbnQocmVjdCwgZXZlbnQuY2xpZW50WCwgZXZlbnQuY2xpZW50WSwgMSkpIHtcbiAgICAgICAgICAgIHJldHVybiB7IGdyb3VwLCBpdGVtLCBlbGVtZW50LCByZWN0IH07XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgbGV0IHRhcmdldCA9IGZpbmRUYXJnZXQoKTtcbiAgaWYgKCF0YXJnZXQpIHtcbiAgICByZXR1cm4gZmFsc2U7XG4gIH1cblxuICByZXR1cm4gQW5kcm9pZC5vbkRlY29yYXRpb25BY3RpdmF0ZWQoXG4gICAgSlNPTi5zdHJpbmdpZnkoe1xuICAgICAgaWQ6IHRhcmdldC5pdGVtLmRlY29yYXRpb24uaWQsXG4gICAgICBncm91cDogdGFyZ2V0Lmdyb3VwLFxuICAgICAgcmVjdDogdG9OYXRpdmVSZWN0KHRhcmdldC5pdGVtLnJhbmdlLmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpKSxcbiAgICAgIGNsaWNrOiBjbGlja0V2ZW50LFxuICAgIH0pXG4gICk7XG59XG5cbi8qKlxuICogQ3JlYXRlcyBhIERlY29yYXRpb25Hcm91cCBvYmplY3QgZnJvbSBhIHVuaXF1ZSBIVE1MIElEIGFuZCBpdHMgbmFtZS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIERlY29yYXRpb25Hcm91cChncm91cElkLCBncm91cE5hbWUpIHtcbiAgdmFyIGl0ZW1zID0gW107XG4gIHZhciBsYXN0SXRlbUlkID0gMDtcbiAgdmFyIGNvbnRhaW5lciA9IG51bGw7XG5cbiAgLyoqXG4gICAqIEFkZHMgYSBuZXcgZGVjb3JhdGlvbiB0byB0aGUgZ3JvdXAuXG4gICAqL1xuICBmdW5jdGlvbiBhZGQoZGVjb3JhdGlvbikge1xuICAgIGxldCBpZCA9IGdyb3VwSWQgKyBcIi1cIiArIGxhc3RJdGVtSWQrKztcblxuICAgIGxldCByYW5nZSA9IHJhbmdlRnJvbUxvY2F0b3IoZGVjb3JhdGlvbi5sb2NhdG9yKTtcbiAgICBpZiAoIXJhbmdlKSB7XG4gICAgICBsb2coXCJDYW4ndCBsb2NhdGUgRE9NIHJhbmdlIGZvciBkZWNvcmF0aW9uXCIsIGRlY29yYXRpb24pO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGxldCBpdGVtID0geyBpZCwgZGVjb3JhdGlvbiwgcmFuZ2UgfTtcbiAgICBpdGVtcy5wdXNoKGl0ZW0pO1xuICAgIGxheW91dChpdGVtKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBSZW1vdmVzIHRoZSBkZWNvcmF0aW9uIHdpdGggZ2l2ZW4gSUQgZnJvbSB0aGUgZ3JvdXAuXG4gICAqL1xuICBmdW5jdGlvbiByZW1vdmUoZGVjb3JhdGlvbklkKSB7XG4gICAgbGV0IGluZGV4ID0gaXRlbXMuZmluZEluZGV4KChpKSA9PiBpLmRlY29yYXRpb24uaWQgPT09IGRlY29yYXRpb25JZCk7XG4gICAgaWYgKGluZGV4ID09PSAtMSkge1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGxldCBpdGVtID0gaXRlbXNbaW5kZXhdO1xuICAgIGl0ZW1zLnNwbGljZShpbmRleCwgMSk7XG4gICAgaXRlbS5jbGlja2FibGVFbGVtZW50cyA9IG51bGw7XG4gICAgaWYgKGl0ZW0uY29udGFpbmVyKSB7XG4gICAgICBpdGVtLmNvbnRhaW5lci5yZW1vdmUoKTtcbiAgICAgIGl0ZW0uY29udGFpbmVyID0gbnVsbDtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogTm90aWZpZXMgdGhhdCB0aGUgZ2l2ZW4gZGVjb3JhdGlvbiB3YXMgbW9kaWZpZWQgYW5kIG5lZWRzIHRvIGJlIHVwZGF0ZWQuXG4gICAqL1xuICBmdW5jdGlvbiB1cGRhdGUoZGVjb3JhdGlvbikge1xuICAgIHJlbW92ZShkZWNvcmF0aW9uLmlkKTtcbiAgICBhZGQoZGVjb3JhdGlvbik7XG4gIH1cblxuICAvKipcbiAgICogUmVtb3ZlcyBhbGwgZGVjb3JhdGlvbnMgZnJvbSB0aGlzIGdyb3VwLlxuICAgKi9cbiAgZnVuY3Rpb24gY2xlYXIoKSB7XG4gICAgY2xlYXJDb250YWluZXIoKTtcbiAgICBpdGVtcy5sZW5ndGggPSAwO1xuICB9XG5cbiAgLyoqXG4gICAqIFJlY3JlYXRlcyB0aGUgZGVjb3JhdGlvbiBlbGVtZW50cy5cbiAgICpcbiAgICogVG8gYmUgY2FsbGVkIGFmdGVyIHJlZmxvd2luZyB0aGUgcmVzb3VyY2UsIGZvciBleGFtcGxlLlxuICAgKi9cbiAgZnVuY3Rpb24gcmVxdWVzdExheW91dCgpIHtcbiAgICBjbGVhckNvbnRhaW5lcigpO1xuICAgIGl0ZW1zLmZvckVhY2goKGl0ZW0pID0+IGxheW91dChpdGVtKSk7XG4gIH1cblxuICAvKipcbiAgICogTGF5b3V0cyBhIHNpbmdsZSBEZWNvcmF0aW9uIGl0ZW0uXG4gICAqL1xuICBmdW5jdGlvbiBsYXlvdXQoaXRlbSkge1xuICAgIGxldCBncm91cENvbnRhaW5lciA9IHJlcXVpcmVDb250YWluZXIoKTtcblxuICAgIGxldCBzdHlsZSA9IHN0eWxlcy5nZXQoaXRlbS5kZWNvcmF0aW9uLnN0eWxlKTtcbiAgICBpZiAoIXN0eWxlKSB7XG4gICAgICBsb2dFcnJvcihgVW5rbm93biBkZWNvcmF0aW9uIHN0eWxlOiAke2l0ZW0uZGVjb3JhdGlvbi5zdHlsZX1gKTtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICBsZXQgaXRlbUNvbnRhaW5lciA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoXCJkaXZcIik7XG4gICAgaXRlbUNvbnRhaW5lci5zZXRBdHRyaWJ1dGUoXCJpZFwiLCBpdGVtLmlkKTtcbiAgICBpdGVtQ29udGFpbmVyLnNldEF0dHJpYnV0ZShcImRhdGEtc3R5bGVcIiwgaXRlbS5kZWNvcmF0aW9uLnN0eWxlKTtcbiAgICBpdGVtQ29udGFpbmVyLnN0eWxlLnNldFByb3BlcnR5KFwicG9pbnRlci1ldmVudHNcIiwgXCJub25lXCIpO1xuXG4gICAgbGV0IHZpZXdwb3J0V2lkdGggPSB3aW5kb3cuaW5uZXJXaWR0aDtcbiAgICBsZXQgY29sdW1uQ291bnQgPSBwYXJzZUludChcbiAgICAgIGdldENvbXB1dGVkU3R5bGUoZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50KS5nZXRQcm9wZXJ0eVZhbHVlKFxuICAgICAgICBcImNvbHVtbi1jb3VudFwiXG4gICAgICApXG4gICAgKTtcbiAgICBsZXQgcGFnZVdpZHRoID0gdmlld3BvcnRXaWR0aCAvIChjb2x1bW5Db3VudCB8fCAxKTtcbiAgICBsZXQgc2Nyb2xsaW5nRWxlbWVudCA9IGRvY3VtZW50LnNjcm9sbGluZ0VsZW1lbnQ7XG4gICAgbGV0IHhPZmZzZXQgPSBzY3JvbGxpbmdFbGVtZW50LnNjcm9sbExlZnQ7XG4gICAgbGV0IHlPZmZzZXQgPSBzY3JvbGxpbmdFbGVtZW50LnNjcm9sbFRvcDtcblxuICAgIGZ1bmN0aW9uIHBvc2l0aW9uRWxlbWVudChlbGVtZW50LCByZWN0LCBib3VuZGluZ1JlY3QpIHtcbiAgICAgIGVsZW1lbnQuc3R5bGUucG9zaXRpb24gPSBcImFic29sdXRlXCI7XG5cbiAgICAgIGlmIChzdHlsZS53aWR0aCA9PT0gXCJ3cmFwXCIpIHtcbiAgICAgICAgZWxlbWVudC5zdHlsZS53aWR0aCA9IGAke3JlY3Qud2lkdGh9cHhgO1xuICAgICAgICBlbGVtZW50LnN0eWxlLmhlaWdodCA9IGAke3JlY3QuaGVpZ2h0fXB4YDtcbiAgICAgICAgZWxlbWVudC5zdHlsZS5sZWZ0ID0gYCR7cmVjdC5sZWZ0ICsgeE9mZnNldH1weGA7XG4gICAgICAgIGVsZW1lbnQuc3R5bGUudG9wID0gYCR7cmVjdC50b3AgKyB5T2Zmc2V0fXB4YDtcbiAgICAgIH0gZWxzZSBpZiAoc3R5bGUud2lkdGggPT09IFwidmlld3BvcnRcIikge1xuICAgICAgICBlbGVtZW50LnN0eWxlLndpZHRoID0gYCR7dmlld3BvcnRXaWR0aH1weGA7XG4gICAgICAgIGVsZW1lbnQuc3R5bGUuaGVpZ2h0ID0gYCR7cmVjdC5oZWlnaHR9cHhgO1xuICAgICAgICBsZXQgbGVmdCA9IE1hdGguZmxvb3IocmVjdC5sZWZ0IC8gdmlld3BvcnRXaWR0aCkgKiB2aWV3cG9ydFdpZHRoO1xuICAgICAgICBlbGVtZW50LnN0eWxlLmxlZnQgPSBgJHtsZWZ0ICsgeE9mZnNldH1weGA7XG4gICAgICAgIGVsZW1lbnQuc3R5bGUudG9wID0gYCR7cmVjdC50b3AgKyB5T2Zmc2V0fXB4YDtcbiAgICAgIH0gZWxzZSBpZiAoc3R5bGUud2lkdGggPT09IFwiYm91bmRzXCIpIHtcbiAgICAgICAgZWxlbWVudC5zdHlsZS53aWR0aCA9IGAke2JvdW5kaW5nUmVjdC53aWR0aH1weGA7XG4gICAgICAgIGVsZW1lbnQuc3R5bGUuaGVpZ2h0ID0gYCR7cmVjdC5oZWlnaHR9cHhgO1xuICAgICAgICBlbGVtZW50LnN0eWxlLmxlZnQgPSBgJHtib3VuZGluZ1JlY3QubGVmdCArIHhPZmZzZXR9cHhgO1xuICAgICAgICBlbGVtZW50LnN0eWxlLnRvcCA9IGAke3JlY3QudG9wICsgeU9mZnNldH1weGA7XG4gICAgICB9IGVsc2UgaWYgKHN0eWxlLndpZHRoID09PSBcInBhZ2VcIikge1xuICAgICAgICBlbGVtZW50LnN0eWxlLndpZHRoID0gYCR7cGFnZVdpZHRofXB4YDtcbiAgICAgICAgZWxlbWVudC5zdHlsZS5oZWlnaHQgPSBgJHtyZWN0LmhlaWdodH1weGA7XG4gICAgICAgIGxldCBsZWZ0ID0gTWF0aC5mbG9vcihyZWN0LmxlZnQgLyBwYWdlV2lkdGgpICogcGFnZVdpZHRoO1xuICAgICAgICBlbGVtZW50LnN0eWxlLmxlZnQgPSBgJHtsZWZ0ICsgeE9mZnNldH1weGA7XG4gICAgICAgIGVsZW1lbnQuc3R5bGUudG9wID0gYCR7cmVjdC50b3AgKyB5T2Zmc2V0fXB4YDtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBsZXQgYm91bmRpbmdSZWN0ID0gaXRlbS5yYW5nZS5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKTtcblxuICAgIGxldCBlbGVtZW50VGVtcGxhdGU7XG4gICAgdHJ5IHtcbiAgICAgIGxldCB0ZW1wbGF0ZSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoXCJ0ZW1wbGF0ZVwiKTtcbiAgICAgIHRlbXBsYXRlLmlubmVySFRNTCA9IGl0ZW0uZGVjb3JhdGlvbi5lbGVtZW50LnRyaW0oKTtcbiAgICAgIGVsZW1lbnRUZW1wbGF0ZSA9IHRlbXBsYXRlLmNvbnRlbnQuZmlyc3RFbGVtZW50Q2hpbGQ7XG4gICAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAgIGxvZ0Vycm9yKFxuICAgICAgICBgSW52YWxpZCBkZWNvcmF0aW9uIGVsZW1lbnQgXCIke2l0ZW0uZGVjb3JhdGlvbi5lbGVtZW50fVwiOiAke2Vycm9yLm1lc3NhZ2V9YFxuICAgICAgKTtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICBpZiAoc3R5bGUubGF5b3V0ID09PSBcImJveGVzXCIpIHtcbiAgICAgIGxldCBkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzID0gdHJ1ZTtcbiAgICAgIGxldCBjbGllbnRSZWN0cyA9IGdldENsaWVudFJlY3RzTm9PdmVybGFwKFxuICAgICAgICBpdGVtLnJhbmdlLFxuICAgICAgICBkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzXG4gICAgICApO1xuXG4gICAgICBjbGllbnRSZWN0cyA9IGNsaWVudFJlY3RzLnNvcnQoKHIxLCByMikgPT4ge1xuICAgICAgICBpZiAocjEudG9wIDwgcjIudG9wKSB7XG4gICAgICAgICAgcmV0dXJuIC0xO1xuICAgICAgICB9IGVsc2UgaWYgKHIxLnRvcCA+IHIyLnRvcCkge1xuICAgICAgICAgIHJldHVybiAxO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIHJldHVybiAwO1xuICAgICAgICB9XG4gICAgICB9KTtcblxuICAgICAgZm9yIChsZXQgY2xpZW50UmVjdCBvZiBjbGllbnRSZWN0cykge1xuICAgICAgICBjb25zdCBsaW5lID0gZWxlbWVudFRlbXBsYXRlLmNsb25lTm9kZSh0cnVlKTtcbiAgICAgICAgbGluZS5zdHlsZS5zZXRQcm9wZXJ0eShcInBvaW50ZXItZXZlbnRzXCIsIFwibm9uZVwiKTtcbiAgICAgICAgcG9zaXRpb25FbGVtZW50KGxpbmUsIGNsaWVudFJlY3QsIGJvdW5kaW5nUmVjdCk7XG4gICAgICAgIGl0ZW1Db250YWluZXIuYXBwZW5kKGxpbmUpO1xuICAgICAgfVxuICAgIH0gZWxzZSBpZiAoc3R5bGUubGF5b3V0ID09PSBcImJvdW5kc1wiKSB7XG4gICAgICBjb25zdCBib3VuZHMgPSBlbGVtZW50VGVtcGxhdGUuY2xvbmVOb2RlKHRydWUpO1xuICAgICAgYm91bmRzLnN0eWxlLnNldFByb3BlcnR5KFwicG9pbnRlci1ldmVudHNcIiwgXCJub25lXCIpO1xuICAgICAgcG9zaXRpb25FbGVtZW50KGJvdW5kcywgYm91bmRpbmdSZWN0LCBib3VuZGluZ1JlY3QpO1xuXG4gICAgICBpdGVtQ29udGFpbmVyLmFwcGVuZChib3VuZHMpO1xuICAgIH1cblxuICAgIGdyb3VwQ29udGFpbmVyLmFwcGVuZChpdGVtQ29udGFpbmVyKTtcbiAgICBpdGVtLmNvbnRhaW5lciA9IGl0ZW1Db250YWluZXI7XG4gICAgaXRlbS5jbGlja2FibGVFbGVtZW50cyA9IEFycmF5LmZyb20oXG4gICAgICBpdGVtQ29udGFpbmVyLnF1ZXJ5U2VsZWN0b3JBbGwoXCJbZGF0YS1hY3RpdmFibGU9JzEnXVwiKVxuICAgICk7XG4gICAgaWYgKGl0ZW0uY2xpY2thYmxlRWxlbWVudHMubGVuZ3RoID09PSAwKSB7XG4gICAgICBpdGVtLmNsaWNrYWJsZUVsZW1lbnRzID0gQXJyYXkuZnJvbShpdGVtQ29udGFpbmVyLmNoaWxkcmVuKTtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogUmV0dXJucyB0aGUgZ3JvdXAgY29udGFpbmVyIGVsZW1lbnQsIGFmdGVyIG1ha2luZyBzdXJlIGl0IGV4aXN0cy5cbiAgICovXG4gIGZ1bmN0aW9uIHJlcXVpcmVDb250YWluZXIoKSB7XG4gICAgaWYgKCFjb250YWluZXIpIHtcbiAgICAgIGNvbnRhaW5lciA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoXCJkaXZcIik7XG4gICAgICBjb250YWluZXIuc2V0QXR0cmlidXRlKFwiaWRcIiwgZ3JvdXBJZCk7XG4gICAgICBjb250YWluZXIuc2V0QXR0cmlidXRlKFwiZGF0YS1ncm91cFwiLCBncm91cE5hbWUpO1xuICAgICAgY29udGFpbmVyLnN0eWxlLnNldFByb3BlcnR5KFwicG9pbnRlci1ldmVudHNcIiwgXCJub25lXCIpO1xuICAgICAgZG9jdW1lbnQuYm9keS5hcHBlbmQoY29udGFpbmVyKTtcbiAgICB9XG4gICAgcmV0dXJuIGNvbnRhaW5lcjtcbiAgfVxuXG4gIC8qKlxuICAgKiBSZW1vdmVzIHRoZSBncm91cCBjb250YWluZXIuXG4gICAqL1xuICBmdW5jdGlvbiBjbGVhckNvbnRhaW5lcigpIHtcbiAgICBpZiAoY29udGFpbmVyKSB7XG4gICAgICBjb250YWluZXIucmVtb3ZlKCk7XG4gICAgICBjb250YWluZXIgPSBudWxsO1xuICAgIH1cbiAgfVxuXG4gIHJldHVybiB7IGFkZCwgcmVtb3ZlLCB1cGRhdGUsIGNsZWFyLCBpdGVtcywgcmVxdWVzdExheW91dCB9O1xufVxuXG53aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcihcbiAgXCJsb2FkXCIsXG4gIGZ1bmN0aW9uICgpIHtcbiAgICAvLyBXaWxsIHJlbGF5b3V0IGFsbCB0aGUgZGVjb3JhdGlvbnMgd2hlbiB0aGUgZG9jdW1lbnQgYm9keSBpcyByZXNpemVkLlxuICAgIGNvbnN0IGJvZHkgPSBkb2N1bWVudC5ib2R5O1xuICAgIHZhciBsYXN0U2l6ZSA9IHsgd2lkdGg6IDAsIGhlaWdodDogMCB9O1xuICAgIGNvbnN0IG9ic2VydmVyID0gbmV3IFJlc2l6ZU9ic2VydmVyKCgpID0+IHtcbiAgICAgIGlmIChcbiAgICAgICAgbGFzdFNpemUud2lkdGggPT09IGJvZHkuY2xpZW50V2lkdGggJiZcbiAgICAgICAgbGFzdFNpemUuaGVpZ2h0ID09PSBib2R5LmNsaWVudEhlaWdodFxuICAgICAgKSB7XG4gICAgICAgIHJldHVybjtcbiAgICAgIH1cbiAgICAgIGxhc3RTaXplID0ge1xuICAgICAgICB3aWR0aDogYm9keS5jbGllbnRXaWR0aCxcbiAgICAgICAgaGVpZ2h0OiBib2R5LmNsaWVudEhlaWdodCxcbiAgICAgIH07XG5cbiAgICAgIGdyb3Vwcy5mb3JFYWNoKGZ1bmN0aW9uIChncm91cCkge1xuICAgICAgICBncm91cC5yZXF1ZXN0TGF5b3V0KCk7XG4gICAgICB9KTtcbiAgICB9KTtcbiAgICBvYnNlcnZlci5vYnNlcnZlKGJvZHkpO1xuICB9LFxuICBmYWxzZVxuKTtcbiIsIi8qXG4gKiBDb3B5cmlnaHQgMjAyMSBSZWFkaXVtIEZvdW5kYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuXG4gKiBVc2Ugb2YgdGhpcyBzb3VyY2UgY29kZSBpcyBnb3Zlcm5lZCBieSB0aGUgQlNELXN0eWxlIGxpY2Vuc2VcbiAqIGF2YWlsYWJsZSBpbiB0aGUgdG9wLWxldmVsIExJQ0VOU0UgZmlsZSBvZiB0aGUgcHJvamVjdC5cbiAqL1xuXG5pbXBvcnQgeyBoYW5kbGVEZWNvcmF0aW9uQ2xpY2tFdmVudCB9IGZyb20gXCIuL2RlY29yYXRvclwiO1xuXG53aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcihcIkRPTUNvbnRlbnRMb2FkZWRcIiwgZnVuY3Rpb24gKCkge1xuICBkb2N1bWVudC5hZGRFdmVudExpc3RlbmVyKFwiY2xpY2tcIiwgb25DbGljaywgZmFsc2UpO1xuICBiaW5kRHJhZ0dlc3R1cmUoZG9jdW1lbnQpO1xufSk7XG5cbmZ1bmN0aW9uIG9uQ2xpY2soZXZlbnQpIHtcbiAgaWYgKCF3aW5kb3cuZ2V0U2VsZWN0aW9uKCkuaXNDb2xsYXBzZWQpIHtcbiAgICAvLyBUaGVyZSdzIGFuIG9uLWdvaW5nIHNlbGVjdGlvbiwgdGhlIHRhcCB3aWxsIGRpc21pc3MgaXQgc28gd2UgZG9uJ3QgZm9yd2FyZCBpdC5cbiAgICByZXR1cm47XG4gIH1cblxuICB2YXIgcGl4ZWxSYXRpbyA9IHdpbmRvdy5kZXZpY2VQaXhlbFJhdGlvO1xuICBsZXQgY2xpY2tFdmVudCA9IHtcbiAgICBkZWZhdWx0UHJldmVudGVkOiBldmVudC5kZWZhdWx0UHJldmVudGVkLFxuICAgIHg6IGV2ZW50LmNsaWVudFggKiBwaXhlbFJhdGlvLFxuICAgIHk6IGV2ZW50LmNsaWVudFkgKiBwaXhlbFJhdGlvLFxuICAgIHRhcmdldEVsZW1lbnQ6IGV2ZW50LnRhcmdldC5vdXRlckhUTUwsXG4gICAgaW50ZXJhY3RpdmVFbGVtZW50OiBuZWFyZXN0SW50ZXJhY3RpdmVFbGVtZW50KGV2ZW50LnRhcmdldCksXG4gIH07XG5cbiAgaWYgKGhhbmRsZURlY29yYXRpb25DbGlja0V2ZW50KGV2ZW50LCBjbGlja0V2ZW50KSkge1xuICAgIHJldHVybjtcbiAgfVxuXG4gIC8vIFNlbmQgdGhlIHRhcCBkYXRhIG92ZXIgdGhlIEpTIGJyaWRnZSBldmVuIGlmIGl0J3MgYmVlbiBoYW5kbGVkIHdpdGhpbiB0aGUgd2ViIHZpZXcsIHNvIHRoYXRcbiAgLy8gaXQgY2FuIGJlIHByZXNlcnZlZCBhbmQgdXNlZCBieSB0aGUgdG9vbGtpdCBpZiBuZWVkZWQuXG4gIHZhciBzaG91bGRQcmV2ZW50RGVmYXVsdCA9IEFuZHJvaWQub25UYXAoSlNPTi5zdHJpbmdpZnkoY2xpY2tFdmVudCkpO1xuXG4gIGlmIChzaG91bGRQcmV2ZW50RGVmYXVsdCkge1xuICAgIGV2ZW50LnN0b3BQcm9wYWdhdGlvbigpO1xuICAgIGV2ZW50LnByZXZlbnREZWZhdWx0KCk7XG4gIH1cbn1cblxuZnVuY3Rpb24gYmluZERyYWdHZXN0dXJlKGVsZW1lbnQpIHtcbiAgLy8gcGFzc2l2ZTogZmFsc2UgaXMgbmVjZXNzYXJ5IHRvIGJlIGFibGUgdG8gcHJldmVudCB0aGUgZGVmYXVsdCBiZWhhdmlvci5cbiAgZWxlbWVudC5hZGRFdmVudExpc3RlbmVyKFwidG91Y2hzdGFydFwiLCBvblN0YXJ0LCB7IHBhc3NpdmU6IGZhbHNlIH0pO1xuICBlbGVtZW50LmFkZEV2ZW50TGlzdGVuZXIoXCJ0b3VjaGVuZFwiLCBvbkVuZCwgeyBwYXNzaXZlOiBmYWxzZSB9KTtcbiAgZWxlbWVudC5hZGRFdmVudExpc3RlbmVyKFwidG91Y2htb3ZlXCIsIG9uTW92ZSwgeyBwYXNzaXZlOiBmYWxzZSB9KTtcblxuICB2YXIgc3RhdGUgPSB1bmRlZmluZWQ7XG4gIHZhciBpc1N0YXJ0aW5nRHJhZyA9IGZhbHNlO1xuICBjb25zdCBwaXhlbFJhdGlvID0gd2luZG93LmRldmljZVBpeGVsUmF0aW87XG5cbiAgZnVuY3Rpb24gb25TdGFydChldmVudCkge1xuICAgIGlzU3RhcnRpbmdEcmFnID0gdHJ1ZTtcblxuICAgIGNvbnN0IHN0YXJ0WCA9IGV2ZW50LnRvdWNoZXNbMF0uY2xpZW50WCAqIHBpeGVsUmF0aW87XG4gICAgY29uc3Qgc3RhcnRZID0gZXZlbnQudG91Y2hlc1swXS5jbGllbnRZICogcGl4ZWxSYXRpbztcbiAgICBzdGF0ZSA9IHtcbiAgICAgIGRlZmF1bHRQcmV2ZW50ZWQ6IGV2ZW50LmRlZmF1bHRQcmV2ZW50ZWQsXG4gICAgICBzdGFydFg6IHN0YXJ0WCxcbiAgICAgIHN0YXJ0WTogc3RhcnRZLFxuICAgICAgY3VycmVudFg6IHN0YXJ0WCxcbiAgICAgIGN1cnJlbnRZOiBzdGFydFksXG4gICAgICBvZmZzZXRYOiAwLFxuICAgICAgb2Zmc2V0WTogMCxcbiAgICAgIGludGVyYWN0aXZlRWxlbWVudDogbmVhcmVzdEludGVyYWN0aXZlRWxlbWVudChldmVudC50YXJnZXQpLFxuICAgIH07XG4gIH1cblxuICBmdW5jdGlvbiBvbk1vdmUoZXZlbnQpIHtcbiAgICBpZiAoIXN0YXRlKSByZXR1cm47XG5cbiAgICBzdGF0ZS5jdXJyZW50WCA9IGV2ZW50LnRvdWNoZXNbMF0uY2xpZW50WCAqIHBpeGVsUmF0aW87XG4gICAgc3RhdGUuY3VycmVudFkgPSBldmVudC50b3VjaGVzWzBdLmNsaWVudFkgKiBwaXhlbFJhdGlvO1xuICAgIHN0YXRlLm9mZnNldFggPSBzdGF0ZS5jdXJyZW50WCAtIHN0YXRlLnN0YXJ0WDtcbiAgICBzdGF0ZS5vZmZzZXRZID0gc3RhdGUuY3VycmVudFkgLSBzdGF0ZS5zdGFydFk7XG5cbiAgICB2YXIgc2hvdWxkUHJldmVudERlZmF1bHQgPSBmYWxzZTtcbiAgICAvLyBXYWl0IGZvciBhIG1vdmVtZW50IG9mIGF0IGxlYXN0IDYgcGl4ZWxzIGJlZm9yZSByZXBvcnRpbmcgYSBkcmFnLlxuICAgIGlmIChpc1N0YXJ0aW5nRHJhZykge1xuICAgICAgaWYgKE1hdGguYWJzKHN0YXRlLm9mZnNldFgpID49IDYgfHwgTWF0aC5hYnMoc3RhdGUub2Zmc2V0WSkgPj0gNikge1xuICAgICAgICBpc1N0YXJ0aW5nRHJhZyA9IGZhbHNlO1xuICAgICAgICBzaG91bGRQcmV2ZW50RGVmYXVsdCA9IEFuZHJvaWQub25EcmFnU3RhcnQoSlNPTi5zdHJpbmdpZnkoc3RhdGUpKTtcbiAgICAgIH1cbiAgICB9IGVsc2Uge1xuICAgICAgc2hvdWxkUHJldmVudERlZmF1bHQgPSBBbmRyb2lkLm9uRHJhZ01vdmUoSlNPTi5zdHJpbmdpZnkoc3RhdGUpKTtcbiAgICB9XG5cbiAgICBpZiAoc2hvdWxkUHJldmVudERlZmF1bHQpIHtcbiAgICAgIGV2ZW50LnN0b3BQcm9wYWdhdGlvbigpO1xuICAgICAgZXZlbnQucHJldmVudERlZmF1bHQoKTtcbiAgICB9XG4gIH1cblxuICBmdW5jdGlvbiBvbkVuZChldmVudCkge1xuICAgIGlmICghc3RhdGUpIHJldHVybjtcblxuICAgIGNvbnN0IHNob3VsZFByZXZlbnREZWZhdWx0ID0gQW5kcm9pZC5vbkRyYWdFbmQoSlNPTi5zdHJpbmdpZnkoc3RhdGUpKTtcbiAgICBpZiAoc2hvdWxkUHJldmVudERlZmF1bHQpIHtcbiAgICAgIGV2ZW50LnN0b3BQcm9wYWdhdGlvbigpO1xuICAgICAgZXZlbnQucHJldmVudERlZmF1bHQoKTtcbiAgICB9XG4gICAgc3RhdGUgPSB1bmRlZmluZWQ7XG4gIH1cbn1cblxuLy8gU2VlLiBodHRwczovL2dpdGh1Yi5jb20vSmF5UGFub3ovYXJjaGl0ZWN0dXJlL3RyZWUvdG91Y2gtaGFuZGxpbmcvbWlzYy90b3VjaC1oYW5kbGluZ1xuZnVuY3Rpb24gbmVhcmVzdEludGVyYWN0aXZlRWxlbWVudChlbGVtZW50KSB7XG4gIHZhciBpbnRlcmFjdGl2ZVRhZ3MgPSBbXG4gICAgXCJhXCIsXG4gICAgXCJhdWRpb1wiLFxuICAgIFwiYnV0dG9uXCIsXG4gICAgXCJjYW52YXNcIixcbiAgICBcImRldGFpbHNcIixcbiAgICBcImlucHV0XCIsXG4gICAgXCJsYWJlbFwiLFxuICAgIFwib3B0aW9uXCIsXG4gICAgXCJzZWxlY3RcIixcbiAgICBcInN1Ym1pdFwiLFxuICAgIFwidGV4dGFyZWFcIixcbiAgICBcInZpZGVvXCIsXG4gIF07XG4gIGlmIChpbnRlcmFjdGl2ZVRhZ3MuaW5kZXhPZihlbGVtZW50Lm5vZGVOYW1lLnRvTG93ZXJDYXNlKCkpICE9IC0xKSB7XG4gICAgcmV0dXJuIGVsZW1lbnQub3V0ZXJIVE1MO1xuICB9XG5cbiAgLy8gQ2hlY2tzIHdoZXRoZXIgdGhlIGVsZW1lbnQgaXMgZWRpdGFibGUgYnkgdGhlIHVzZXIuXG4gIGlmIChcbiAgICBlbGVtZW50Lmhhc0F0dHJpYnV0ZShcImNvbnRlbnRlZGl0YWJsZVwiKSAmJlxuICAgIGVsZW1lbnQuZ2V0QXR0cmlidXRlKFwiY29udGVudGVkaXRhYmxlXCIpLnRvTG93ZXJDYXNlKCkgIT0gXCJmYWxzZVwiXG4gICkge1xuICAgIHJldHVybiBlbGVtZW50Lm91dGVySFRNTDtcbiAgfVxuXG4gIC8vIENoZWNrcyBwYXJlbnRzIHJlY3Vyc2l2ZWx5IGJlY2F1c2UgdGhlIHRvdWNoIG1pZ2h0IGJlIGZvciBleGFtcGxlIG9uIGFuIDxlbT4gaW5zaWRlIGEgPGE+LlxuICBpZiAoZWxlbWVudC5wYXJlbnRFbGVtZW50KSB7XG4gICAgcmV0dXJuIG5lYXJlc3RJbnRlcmFjdGl2ZUVsZW1lbnQoZWxlbWVudC5wYXJlbnRFbGVtZW50KTtcbiAgfVxuXG4gIHJldHVybiBudWxsO1xufVxuIiwiLyogZXNsaW50LWRpc2FibGUgKi9cbi8vXG4vLyAgaGlnaGxpZ2h0LmpzXG4vLyAgcjItbmF2aWdhdG9yLWtvdGxpblxuLy9cbi8vICBPcmdhbml6ZWQgYnkgVGFlaHl1biBLaW0gb24gNi8yNy8xOSBmcm9tIHIyLW5hdmlnYXRvci1qcy5cbi8vXG4vLyAgQ29weXJpZ2h0IDIwMTkgUmVhZGl1bSBGb3VuZGF0aW9uLiBBbGwgcmlnaHRzIHJlc2VydmVkLlxuLy8gIFVzZSBvZiB0aGlzIHNvdXJjZSBjb2RlIGlzIGdvdmVybmVkIGJ5IGEgQlNELXN0eWxlIGxpY2Vuc2Ugd2hpY2ggaXMgZGV0YWlsZWRcbi8vICBpbiB0aGUgTElDRU5TRSBmaWxlIHByZXNlbnQgaW4gdGhlIHByb2plY3QgcmVwb3NpdG9yeSB3aGVyZSB0aGlzIHNvdXJjZSBjb2RlIGlzIG1haW50YWluZWQuXG4vL1xuXG5jb25zdCBST09UX0NMQVNTX1JFRFVDRV9NT1RJT04gPSBcInIyLXJlZHVjZS1tb3Rpb25cIjtcbmNvbnN0IFJPT1RfQ0xBU1NfTk9fRk9PVE5PVEVTID0gXCJyMi1uby1wb3B1cC1mb29ub3Rlc1wiO1xuY29uc3QgUE9QVVBfRElBTE9HX0NMQVNTID0gXCJyMi1wb3B1cC1kaWFsb2dcIjtcbmNvbnN0IEZPT1ROT1RFU19DT05UQUlORVJfQ0xBU1MgPSBcInIyLWZvb3Rub3RlLWNvbnRhaW5lclwiO1xuY29uc3QgRk9PVE5PVEVTX0NMT1NFX0JVVFRPTl9DTEFTUyA9IFwicjItZm9vdG5vdGUtY2xvc2VcIjtcbmNvbnN0IEZPT1ROT1RFX0ZPUkNFX1NIT1cgPSBcInIyLWZvb3Rub3RlLWZvcmNlLXNob3dcIjtcbmNvbnN0IFRUU19JRF9QUkVWSU9VUyA9IFwicjItdHRzLXByZXZpb3VzXCI7XG5jb25zdCBUVFNfSURfTkVYVCA9IFwicjItdHRzLW5leHRcIjtcbmNvbnN0IFRUU19JRF9TTElERVIgPSBcInIyLXR0cy1zbGlkZXJcIjtcbmNvbnN0IFRUU19JRF9BQ1RJVkVfV09SRCA9IFwicjItdHRzLWFjdGl2ZS13b3JkXCI7XG5jb25zdCBUVFNfSURfQ09OVEFJTkVSID0gXCJyMi10dHMtdHh0XCI7XG5jb25zdCBUVFNfSURfSU5GTyA9IFwicjItdHRzLWluZm9cIjtcbmNvbnN0IFRUU19OQVZfQlVUVE9OX0NMQVNTID0gXCJyMi10dHMtYnV0dG9uXCI7XG5jb25zdCBUVFNfSURfU1BFQUtJTkdfRE9DX0VMRU1FTlQgPSBcInIyLXR0cy1zcGVha2luZy1lbFwiO1xuY29uc3QgVFRTX0NMQVNTX0lOSkVDVEVEX1NQQU4gPSBcInIyLXR0cy1zcGVha2luZy10eHRcIjtcbmNvbnN0IFRUU19DTEFTU19JTkpFQ1RFRF9TVUJTUEFOID0gXCJyMi10dHMtc3BlYWtpbmctd29yZFwiO1xuY29uc3QgVFRTX0lEX0lOSkVDVEVEX1BBUkVOVCA9IFwicjItdHRzLXNwZWFraW5nLXR4dC1wYXJlbnRcIjtcbmNvbnN0IElEX0hJR0hMSUdIVFNfQ09OVEFJTkVSID0gXCJSMl9JRF9ISUdITElHSFRTX0NPTlRBSU5FUlwiO1xuY29uc3QgSURfQU5OT1RBVElPTl9DT05UQUlORVIgPSBcIlIyX0lEX0FOTk9UQVRJT05fQ09OVEFJTkVSXCI7XG5jb25zdCBDTEFTU19ISUdITElHSFRfQ09OVEFJTkVSID0gXCJSMl9DTEFTU19ISUdITElHSFRfQ09OVEFJTkVSXCI7XG5jb25zdCBDTEFTU19BTk5PVEFUSU9OX0NPTlRBSU5FUiA9IFwiUjJfQ0xBU1NfQU5OT1RBVElPTl9DT05UQUlORVJcIjtcbmNvbnN0IENMQVNTX0hJR0hMSUdIVF9BUkVBID0gXCJSMl9DTEFTU19ISUdITElHSFRfQVJFQVwiO1xuY29uc3QgQ0xBU1NfQU5OT1RBVElPTl9BUkVBID0gXCJSMl9DTEFTU19BTk5PVEFUSU9OX0FSRUFcIjtcbmNvbnN0IENMQVNTX0hJR0hMSUdIVF9CT1VORElOR19BUkVBID0gXCJSMl9DTEFTU19ISUdITElHSFRfQk9VTkRJTkdfQVJFQVwiO1xuY29uc3QgQ0xBU1NfQU5OT1RBVElPTl9CT1VORElOR19BUkVBID0gXCJSMl9DTEFTU19BTk5PVEFUSU9OX0JPVU5ESU5HX0FSRUFcIjtcbi8vIHRzbGludDpkaXNhYmxlLW5leHQtbGluZTptYXgtbGluZS1sZW5ndGhcbmNvbnN0IF9ibGFja2xpc3RJZENsYXNzRm9yQ0ZJID0gW1xuICBQT1BVUF9ESUFMT0dfQ0xBU1MsXG4gIFRUU19DTEFTU19JTkpFQ1RFRF9TUEFOLFxuICBUVFNfQ0xBU1NfSU5KRUNURURfU1VCU1BBTixcbiAgSURfSElHSExJR0hUU19DT05UQUlORVIsXG4gIENMQVNTX0hJR0hMSUdIVF9DT05UQUlORVIsXG4gIENMQVNTX0hJR0hMSUdIVF9BUkVBLFxuICBDTEFTU19ISUdITElHSFRfQk9VTkRJTkdfQVJFQSxcbiAgXCJyZXNpemUtc2Vuc29yXCIsXG5dO1xuY29uc3QgQ0xBU1NfUEFHSU5BVEVEID0gXCJyMi1jc3MtcGFnaW5hdGVkXCI7XG5cbi8vY29uc3QgSVNfREVWID0gKHByb2Nlc3MuZW52Lk5PREVfRU5WID09PSBcImRldmVsb3BtZW50XCIgfHwgcHJvY2Vzcy5lbnYuTk9ERV9FTlYgPT09IFwiZGV2XCIpO1xuY29uc3QgSVNfREVWID0gZmFsc2U7XG5jb25zdCBfaGlnaGxpZ2h0cyA9IFtdO1xuXG5sZXQgX2hpZ2hsaWdodHNDb250YWluZXI7XG5sZXQgX2Fubm90YXRpb25Db250YWluZXI7XG5sZXQgbGFzdE1vdXNlRG93blggPSAtMTtcbmxldCBsYXN0TW91c2VEb3duWSA9IC0xO1xubGV0IGJvZHlFdmVudExpc3RlbmVyc1NldCA9IGZhbHNlO1xuXG5jb25zdCBVU0VfU1ZHID0gZmFsc2U7XG5jb25zdCBERUZBVUxUX0JBQ0tHUk9VTkRfQ09MT1JfT1BBQ0lUWSA9IDAuMztcbmNvbnN0IEFMVF9CQUNLR1JPVU5EX0NPTE9SX09QQUNJVFkgPSAwLjQ1O1xuXG4vL2NvbnN0IERFQlVHX1ZJU1VBTFMgPSBmYWxzZTtcbmNvbnN0IERFQlVHX1ZJU1VBTFMgPSBmYWxzZTtcbmNvbnN0IERFRkFVTFRfQkFDS0dST1VORF9DT0xPUiA9IHtcbiAgYmx1ZTogMTAwLFxuICBncmVlbjogNTAsXG4gIHJlZDogMjMwLFxufTtcblxuY29uc3QgQU5OT1RBVElPTl9XSURUSCA9IDE1O1xuXG5mdW5jdGlvbiByZXNldEhpZ2hsaWdodEJvdW5kaW5nU3R5bGUoX3dpbiwgaGlnaGxpZ2h0Qm91bmRpbmcpIHtcbiAgaWYgKFxuICAgIGhpZ2hsaWdodEJvdW5kaW5nLmdldEF0dHJpYnV0ZShcImNsYXNzXCIpID09IENMQVNTX0FOTk9UQVRJT05fQk9VTkRJTkdfQVJFQVxuICApIHtcbiAgICByZXR1cm47XG4gIH1cbiAgaGlnaGxpZ2h0Qm91bmRpbmcuc3R5bGUub3V0bGluZSA9IFwibm9uZVwiO1xuICBoaWdobGlnaHRCb3VuZGluZy5zdHlsZS5zZXRQcm9wZXJ0eShcbiAgICBcImJhY2tncm91bmQtY29sb3JcIixcbiAgICBcInRyYW5zcGFyZW50XCIsXG4gICAgXCJpbXBvcnRhbnRcIlxuICApO1xufVxuXG5mdW5jdGlvbiBzZXRIaWdobGlnaHRBcmVhU3R5bGUod2luLCBoaWdobGlnaHRBcmVhcywgaGlnaGxpZ2h0KSB7XG4gIGNvbnN0IHVzZVNWRyA9ICFERUJVR19WSVNVQUxTICYmIFVTRV9TVkc7XG4gIGZvciAoY29uc3QgaGlnaGxpZ2h0QXJlYSBvZiBoaWdobGlnaHRBcmVhcykge1xuICAgIGNvbnN0IGlzU1ZHID0gdXNlU1ZHICYmIGhpZ2hsaWdodEFyZWEubmFtZXNwYWNlVVJJID09PSBTVkdfWE1MX05BTUVTUEFDRTtcbiAgICBjb25zdCBvcGFjaXR5ID0gQUxUX0JBQ0tHUk9VTkRfQ09MT1JfT1BBQ0lUWTtcbiAgICBpZiAoaXNTVkcpIHtcbiAgICAgIGhpZ2hsaWdodEFyZWEuc3R5bGUuc2V0UHJvcGVydHkoXG4gICAgICAgIFwiZmlsbFwiLFxuICAgICAgICBgcmdiKCR7aGlnaGxpZ2h0LmNvbG9yLnJlZH0sICR7aGlnaGxpZ2h0LmNvbG9yLmdyZWVufSwgJHtoaWdobGlnaHQuY29sb3IuYmx1ZX0pYCxcbiAgICAgICAgXCJpbXBvcnRhbnRcIlxuICAgICAgKTtcbiAgICAgIGhpZ2hsaWdodEFyZWEuc3R5bGUuc2V0UHJvcGVydHkoXG4gICAgICAgIFwiZmlsbC1vcGFjaXR5XCIsXG4gICAgICAgIGAke29wYWNpdHl9YCxcbiAgICAgICAgXCJpbXBvcnRhbnRcIlxuICAgICAgKTtcbiAgICAgIGhpZ2hsaWdodEFyZWEuc3R5bGUuc2V0UHJvcGVydHkoXG4gICAgICAgIFwic3Ryb2tlXCIsXG4gICAgICAgIGByZ2IoJHtoaWdobGlnaHQuY29sb3IucmVkfSwgJHtoaWdobGlnaHQuY29sb3IuZ3JlZW59LCAke2hpZ2hsaWdodC5jb2xvci5ibHVlfSlgLFxuICAgICAgICBcImltcG9ydGFudFwiXG4gICAgICApO1xuICAgICAgaGlnaGxpZ2h0QXJlYS5zdHlsZS5zZXRQcm9wZXJ0eShcbiAgICAgICAgXCJzdHJva2Utb3BhY2l0eVwiLFxuICAgICAgICBgJHtvcGFjaXR5fWAsXG4gICAgICAgIFwiaW1wb3J0YW50XCJcbiAgICAgICk7XG4gICAgfSBlbHNlIHtcbiAgICAgIGhpZ2hsaWdodEFyZWEuc3R5bGUuc2V0UHJvcGVydHkoXG4gICAgICAgIFwiYmFja2dyb3VuZC1jb2xvclwiLFxuICAgICAgICBgcmdiYSgke2hpZ2hsaWdodC5jb2xvci5yZWR9LCAke2hpZ2hsaWdodC5jb2xvci5ncmVlbn0sICR7aGlnaGxpZ2h0LmNvbG9yLmJsdWV9LCAke29wYWNpdHl9KWAsXG4gICAgICAgIFwiaW1wb3J0YW50XCJcbiAgICAgICk7XG4gICAgfVxuICB9XG59XG5cbmZ1bmN0aW9uIHJlc2V0SGlnaGxpZ2h0QXJlYVN0eWxlKHdpbiwgaGlnaGxpZ2h0QXJlYSkge1xuICBjb25zdCB1c2VTVkcgPSAhREVCVUdfVklTVUFMUyAmJiBVU0VfU1ZHO1xuICAvL2NvbnN0IHVzZVNWRyA9IFVTRV9TVkc7XG4gIGNvbnN0IGlzU1ZHID0gdXNlU1ZHICYmIGhpZ2hsaWdodEFyZWEubmFtZXNwYWNlVVJJID09PSBTVkdfWE1MX05BTUVTUEFDRTtcbiAgY29uc3QgaWQgPSBpc1NWR1xuICAgID8gaGlnaGxpZ2h0QXJlYS5wYXJlbnROb2RlICYmXG4gICAgICBoaWdobGlnaHRBcmVhLnBhcmVudE5vZGUucGFyZW50Tm9kZSAmJlxuICAgICAgaGlnaGxpZ2h0QXJlYS5wYXJlbnROb2RlLnBhcmVudE5vZGUubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFICYmXG4gICAgICBoaWdobGlnaHRBcmVhLnBhcmVudE5vZGUucGFyZW50Tm9kZS5nZXRBdHRyaWJ1dGVcbiAgICAgID8gaGlnaGxpZ2h0QXJlYS5wYXJlbnROb2RlLnBhcmVudE5vZGUuZ2V0QXR0cmlidXRlKFwiaWRcIilcbiAgICAgIDogdW5kZWZpbmVkXG4gICAgOiBoaWdobGlnaHRBcmVhLnBhcmVudE5vZGUgJiZcbiAgICAgIGhpZ2hsaWdodEFyZWEucGFyZW50Tm9kZS5ub2RlVHlwZSA9PT0gTm9kZS5FTEVNRU5UX05PREUgJiZcbiAgICAgIGhpZ2hsaWdodEFyZWEucGFyZW50Tm9kZS5nZXRBdHRyaWJ1dGVcbiAgICA/IGhpZ2hsaWdodEFyZWEucGFyZW50Tm9kZS5nZXRBdHRyaWJ1dGUoXCJpZFwiKVxuICAgIDogdW5kZWZpbmVkO1xuICBpZiAoaWQpIHtcbiAgICBjb25zdCBoaWdobGlnaHQgPSBfaGlnaGxpZ2h0cy5maW5kKChoKSA9PiB7XG4gICAgICByZXR1cm4gaC5pZCA9PT0gaWQ7XG4gICAgfSk7XG4gICAgaWYgKGhpZ2hsaWdodCkge1xuICAgICAgY29uc3Qgb3BhY2l0eSA9IERFRkFVTFRfQkFDS0dST1VORF9DT0xPUl9PUEFDSVRZO1xuICAgICAgaWYgKGlzU1ZHKSB7XG4gICAgICAgIGhpZ2hsaWdodEFyZWEuc3R5bGUuc2V0UHJvcGVydHkoXG4gICAgICAgICAgXCJmaWxsXCIsXG4gICAgICAgICAgYHJnYigke2hpZ2hsaWdodC5jb2xvci5yZWR9LCAke2hpZ2hsaWdodC5jb2xvci5ncmVlbn0sICR7aGlnaGxpZ2h0LmNvbG9yLmJsdWV9KWAsXG4gICAgICAgICAgXCJpbXBvcnRhbnRcIlxuICAgICAgICApO1xuICAgICAgICBoaWdobGlnaHRBcmVhLnN0eWxlLnNldFByb3BlcnR5KFxuICAgICAgICAgIFwiZmlsbC1vcGFjaXR5XCIsXG4gICAgICAgICAgYCR7b3BhY2l0eX1gLFxuICAgICAgICAgIFwiaW1wb3J0YW50XCJcbiAgICAgICAgKTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYS5zdHlsZS5zZXRQcm9wZXJ0eShcbiAgICAgICAgICBcInN0cm9rZVwiLFxuICAgICAgICAgIGByZ2IoJHtoaWdobGlnaHQuY29sb3IucmVkfSwgJHtoaWdobGlnaHQuY29sb3IuZ3JlZW59LCAke2hpZ2hsaWdodC5jb2xvci5ibHVlfSlgLFxuICAgICAgICAgIFwiaW1wb3J0YW50XCJcbiAgICAgICAgKTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYS5zdHlsZS5zZXRQcm9wZXJ0eShcbiAgICAgICAgICBcInN0cm9rZS1vcGFjaXR5XCIsXG4gICAgICAgICAgYCR7b3BhY2l0eX1gLFxuICAgICAgICAgIFwiaW1wb3J0YW50XCJcbiAgICAgICAgKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGhpZ2hsaWdodEFyZWEuc3R5bGUuc2V0UHJvcGVydHkoXG4gICAgICAgICAgXCJiYWNrZ3JvdW5kLWNvbG9yXCIsXG4gICAgICAgICAgYHJnYmEoJHtoaWdobGlnaHQuY29sb3IucmVkfSwgJHtoaWdobGlnaHQuY29sb3IuZ3JlZW59LCAke2hpZ2hsaWdodC5jb2xvci5ibHVlfSwgJHtvcGFjaXR5fSlgLFxuICAgICAgICAgIFwiaW1wb3J0YW50XCJcbiAgICAgICAgKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cbn1cbmZ1bmN0aW9uIHByb2Nlc3NUb3VjaEV2ZW50KHdpbiwgZXYpIHtcbiAgY29uc3QgZG9jdW1lbnQgPSB3aW4uZG9jdW1lbnQ7XG4gIGNvbnN0IHNjcm9sbEVsZW1lbnQgPSBnZXRTY3JvbGxpbmdFbGVtZW50KGRvY3VtZW50KTtcbiAgY29uc3QgeCA9IGV2LmNoYW5nZWRUb3VjaGVzWzBdLmNsaWVudFg7XG4gIGNvbnN0IHkgPSBldi5jaGFuZ2VkVG91Y2hlc1swXS5jbGllbnRZO1xuICBpZiAoIV9oaWdobGlnaHRzQ29udGFpbmVyKSB7XG4gICAgcmV0dXJuO1xuICB9XG4gIGNvbnN0IHBhZ2luYXRlZCA9IGlzUGFnaW5hdGVkKGRvY3VtZW50KTtcbiAgY29uc3QgYm9keVJlY3QgPSBkb2N1bWVudC5ib2R5LmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpO1xuICBsZXQgeE9mZnNldDtcbiAgbGV0IHlPZmZzZXQ7XG4gIGlmIChuYXZpZ2F0b3IudXNlckFnZW50Lm1hdGNoKC9BbmRyb2lkL2kpKSB7XG4gICAgeE9mZnNldCA9IHBhZ2luYXRlZCA/IC1zY3JvbGxFbGVtZW50LnNjcm9sbExlZnQgOiBib2R5UmVjdC5sZWZ0O1xuICAgIHlPZmZzZXQgPSBwYWdpbmF0ZWQgPyAtc2Nyb2xsRWxlbWVudC5zY3JvbGxUb3AgOiBib2R5UmVjdC50b3A7XG4gIH0gZWxzZSBpZiAobmF2aWdhdG9yLnVzZXJBZ2VudC5tYXRjaCgvaVBob25lfGlQYWR8aVBvZC9pKSkge1xuICAgIHhPZmZzZXQgPSBwYWdpbmF0ZWQgPyAwIDogLXNjcm9sbEVsZW1lbnQuc2Nyb2xsTGVmdDtcbiAgICB5T2Zmc2V0ID0gcGFnaW5hdGVkID8gMCA6IGJvZHlSZWN0LnRvcDtcbiAgfVxuICBsZXQgZm91bmRIaWdobGlnaHQ7XG4gIGxldCBmb3VuZEVsZW1lbnQ7XG4gIGxldCBmb3VuZFJlY3Q7XG4gIC8vICAgIF9oaWdobGlnaHRzLnNvcnQoZnVuY3Rpb24oYSwgYikge1xuICAvLyAgICAgICAgY29uc29sZS5sb2coSlNPTi5zdHJpbmdpZnkoYS5zZWxlY3Rpb25JbmZvKSlcbiAgLy8gICAgICAgIHJldHVybiBhLnNlbGVjdGlvbkluZm8uY2xlYW5UZXh0Lmxlbmd0aCA8IGIuc2VsZWN0aW9uSW5mby5jbGVhblRleHQubGVuZ3RoXG4gIC8vICAgIH0pXG4gIGZvciAobGV0IGkgPSBfaGlnaGxpZ2h0cy5sZW5ndGggLSAxOyBpID49IDA7IGktLSkge1xuICAgIGNvbnN0IGhpZ2hsaWdodCA9IF9oaWdobGlnaHRzW2ldO1xuICAgIGxldCBoaWdobGlnaHRQYXJlbnQgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZChgJHtoaWdobGlnaHQuaWR9YCk7XG4gICAgaWYgKCFoaWdobGlnaHRQYXJlbnQpIHtcbiAgICAgIGhpZ2hsaWdodFBhcmVudCA9IF9oaWdobGlnaHRzQ29udGFpbmVyLnF1ZXJ5U2VsZWN0b3IoYCMke2hpZ2hsaWdodC5pZH1gKTtcbiAgICB9XG4gICAgaWYgKCFoaWdobGlnaHRQYXJlbnQpIHtcbiAgICAgIGNvbnRpbnVlO1xuICAgIH1cbiAgICBsZXQgaGl0ID0gZmFsc2U7XG4gICAgY29uc3QgaGlnaGxpZ2h0RnJhZ21lbnRzID0gaGlnaGxpZ2h0UGFyZW50LnF1ZXJ5U2VsZWN0b3JBbGwoXG4gICAgICBgLiR7Q0xBU1NfSElHSExJR0hUX0FSRUF9YFxuICAgICk7XG4gICAgZm9yIChjb25zdCBoaWdobGlnaHRGcmFnbWVudCBvZiBoaWdobGlnaHRGcmFnbWVudHMpIHtcbiAgICAgIGNvbnN0IHdpdGhSZWN0ID0gaGlnaGxpZ2h0RnJhZ21lbnQ7XG4gICAgICBjb25zdCBsZWZ0ID0gd2l0aFJlY3QucmVjdC5sZWZ0ICsgeE9mZnNldDtcbiAgICAgIGNvbnN0IHRvcCA9IHdpdGhSZWN0LnJlY3QudG9wICsgeU9mZnNldDtcbiAgICAgIGZvdW5kUmVjdCA9IHdpdGhSZWN0LnJlY3Q7XG4gICAgICBpZiAoXG4gICAgICAgIHggPj0gbGVmdCAmJlxuICAgICAgICB4IDwgbGVmdCArIHdpdGhSZWN0LnJlY3Qud2lkdGggJiZcbiAgICAgICAgeSA+PSB0b3AgJiZcbiAgICAgICAgeSA8IHRvcCArIHdpdGhSZWN0LnJlY3QuaGVpZ2h0XG4gICAgICApIHtcbiAgICAgICAgaGl0ID0gdHJ1ZTtcbiAgICAgICAgYnJlYWs7XG4gICAgICB9XG4gICAgfVxuICAgIGlmIChoaXQpIHtcbiAgICAgIGZvdW5kSGlnaGxpZ2h0ID0gaGlnaGxpZ2h0O1xuICAgICAgZm91bmRFbGVtZW50ID0gaGlnaGxpZ2h0UGFyZW50O1xuICAgICAgYnJlYWs7XG4gICAgfVxuICB9XG4gIGlmICghZm91bmRIaWdobGlnaHQgfHwgIWZvdW5kRWxlbWVudCkge1xuICAgIGNvbnN0IGhpZ2hsaWdodEJvdW5kaW5ncyA9IF9oaWdobGlnaHRzQ29udGFpbmVyLnF1ZXJ5U2VsZWN0b3JBbGwoXG4gICAgICBgLiR7Q0xBU1NfSElHSExJR0hUX0JPVU5ESU5HX0FSRUF9YFxuICAgICk7XG4gICAgZm9yIChjb25zdCBoaWdobGlnaHRCb3VuZGluZyBvZiBoaWdobGlnaHRCb3VuZGluZ3MpIHtcbiAgICAgIHJlc2V0SGlnaGxpZ2h0Qm91bmRpbmdTdHlsZSh3aW4sIGhpZ2hsaWdodEJvdW5kaW5nKTtcbiAgICB9XG4gICAgY29uc3QgYWxsSGlnaGxpZ2h0QXJlYXMgPSBBcnJheS5mcm9tKFxuICAgICAgX2hpZ2hsaWdodHNDb250YWluZXIucXVlcnlTZWxlY3RvckFsbChgLiR7Q0xBU1NfSElHSExJR0hUX0FSRUF9YClcbiAgICApO1xuICAgIGZvciAoY29uc3QgaGlnaGxpZ2h0QXJlYSBvZiBhbGxIaWdobGlnaHRBcmVhcykge1xuICAgICAgcmVzZXRIaWdobGlnaHRBcmVhU3R5bGUod2luLCBoaWdobGlnaHRBcmVhKTtcbiAgICB9XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgaWYgKGZvdW5kRWxlbWVudC5nZXRBdHRyaWJ1dGUoXCJkYXRhLWNsaWNrXCIpKSB7XG4gICAgaWYgKGV2LnR5cGUgPT09IFwibW91c2Vtb3ZlXCIpIHtcbiAgICAgIGNvbnN0IGZvdW5kRWxlbWVudEhpZ2hsaWdodEFyZWFzID0gQXJyYXkuZnJvbShcbiAgICAgICAgZm91bmRFbGVtZW50LnF1ZXJ5U2VsZWN0b3JBbGwoYC4ke0NMQVNTX0hJR0hMSUdIVF9BUkVBfWApXG4gICAgICApO1xuICAgICAgY29uc3QgYWxsSGlnaGxpZ2h0QXJlYXMgPSBfaGlnaGxpZ2h0c0NvbnRhaW5lci5xdWVyeVNlbGVjdG9yQWxsKFxuICAgICAgICBgLiR7Q0xBU1NfSElHSExJR0hUX0FSRUF9YFxuICAgICAgKTtcbiAgICAgIGZvciAoY29uc3QgaGlnaGxpZ2h0QXJlYSBvZiBhbGxIaWdobGlnaHRBcmVhcykge1xuICAgICAgICBpZiAoZm91bmRFbGVtZW50SGlnaGxpZ2h0QXJlYXMuaW5kZXhPZihoaWdobGlnaHRBcmVhKSA8IDApIHtcbiAgICAgICAgICByZXNldEhpZ2hsaWdodEFyZWFTdHlsZSh3aW4sIGhpZ2hsaWdodEFyZWEpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgICBzZXRIaWdobGlnaHRBcmVhU3R5bGUod2luLCBmb3VuZEVsZW1lbnRIaWdobGlnaHRBcmVhcywgZm91bmRIaWdobGlnaHQpO1xuICAgICAgY29uc3QgZm91bmRFbGVtZW50SGlnaGxpZ2h0Qm91bmRpbmcgPSBmb3VuZEVsZW1lbnQucXVlcnlTZWxlY3RvcihcbiAgICAgICAgYC4ke0NMQVNTX0hJR0hMSUdIVF9CT1VORElOR19BUkVBfWBcbiAgICAgICk7XG4gICAgICBjb25zdCBhbGxIaWdobGlnaHRCb3VuZGluZ3MgPSBfaGlnaGxpZ2h0c0NvbnRhaW5lci5xdWVyeVNlbGVjdG9yQWxsKFxuICAgICAgICBgLiR7Q0xBU1NfSElHSExJR0hUX0JPVU5ESU5HX0FSRUF9YFxuICAgICAgKTtcbiAgICAgIGZvciAoY29uc3QgaGlnaGxpZ2h0Qm91bmRpbmcgb2YgYWxsSGlnaGxpZ2h0Qm91bmRpbmdzKSB7XG4gICAgICAgIGlmIChcbiAgICAgICAgICAhZm91bmRFbGVtZW50SGlnaGxpZ2h0Qm91bmRpbmcgfHxcbiAgICAgICAgICBoaWdobGlnaHRCb3VuZGluZyAhPT0gZm91bmRFbGVtZW50SGlnaGxpZ2h0Qm91bmRpbmdcbiAgICAgICAgKSB7XG4gICAgICAgICAgcmVzZXRIaWdobGlnaHRCb3VuZGluZ1N0eWxlKHdpbiwgaGlnaGxpZ2h0Qm91bmRpbmcpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgICBpZiAoZm91bmRFbGVtZW50SGlnaGxpZ2h0Qm91bmRpbmcpIHtcbiAgICAgICAgaWYgKERFQlVHX1ZJU1VBTFMpIHtcbiAgICAgICAgICBzZXRIaWdobGlnaHRCb3VuZGluZ1N0eWxlKFxuICAgICAgICAgICAgd2luLFxuICAgICAgICAgICAgZm91bmRFbGVtZW50SGlnaGxpZ2h0Qm91bmRpbmcsXG4gICAgICAgICAgICBmb3VuZEhpZ2hsaWdodFxuICAgICAgICAgICk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9IGVsc2UgaWYgKGV2LnR5cGUgPT09IFwidG91Y2hzdGFydFwiIHx8IGV2LnR5cGUgPT09IFwidG91Y2hlbmRcIikge1xuICAgICAgY29uc3Qgc2l6ZSA9IHtcbiAgICAgICAgc2NyZWVuV2lkdGg6IHdpbmRvdy5vdXRlcldpZHRoLFxuICAgICAgICBzY3JlZW5IZWlnaHQ6IHdpbmRvdy5vdXRlckhlaWdodCxcbiAgICAgICAgbGVmdDogZm91bmRSZWN0LmxlZnQsXG4gICAgICAgIHdpZHRoOiBmb3VuZFJlY3Qud2lkdGgsXG4gICAgICAgIHRvcDogZm91bmRSZWN0LnRvcCxcbiAgICAgICAgaGVpZ2h0OiBmb3VuZFJlY3QuaGVpZ2h0LFxuICAgICAgfTtcbiAgICAgIGNvbnN0IHBheWxvYWQgPSB7XG4gICAgICAgIGhpZ2hsaWdodDogZm91bmRIaWdobGlnaHQuaWQsXG4gICAgICAgIHNpemU6IHNpemUsXG4gICAgICB9O1xuXG4gICAgICBpZiAoXG4gICAgICAgIHR5cGVvZiB3aW5kb3cgIT09IFwidW5kZWZpbmVkXCIgJiZcbiAgICAgICAgdHlwZW9mIHdpbmRvdy5wcm9jZXNzID09PSBcIm9iamVjdFwiICYmXG4gICAgICAgIHdpbmRvdy5wcm9jZXNzLnR5cGUgPT09IFwicmVuZGVyZXJcIlxuICAgICAgKSB7XG4gICAgICAgIGVsZWN0cm9uXzEuaXBjUmVuZGVyZXIuc2VuZFRvSG9zdChSMl9FVkVOVF9ISUdITElHSFRfQ0xJQ0ssIHBheWxvYWQpO1xuICAgICAgfSBlbHNlIGlmICh3aW5kb3cud2Via2l0VVJMKSB7XG4gICAgICAgIGNvbnNvbGUubG9nKGZvdW5kSGlnaGxpZ2h0LmlkLmluY2x1ZGVzKFwiUjJfQU5OT1RBVElPTl9cIikpO1xuICAgICAgICBpZiAoZm91bmRIaWdobGlnaHQuaWQuc2VhcmNoKFwiUjJfQU5OT1RBVElPTl9cIikgPj0gMCkge1xuICAgICAgICAgIGlmIChuYXZpZ2F0b3IudXNlckFnZW50Lm1hdGNoKC9BbmRyb2lkL2kpKSB7XG4gICAgICAgICAgICBBbmRyb2lkLmhpZ2hsaWdodEFubm90YXRpb25NYXJrQWN0aXZhdGVkKGZvdW5kSGlnaGxpZ2h0LmlkKTtcbiAgICAgICAgICB9IGVsc2UgaWYgKG5hdmlnYXRvci51c2VyQWdlbnQubWF0Y2goL2lQaG9uZXxpUGFkfGlQb2QvaSkpIHtcbiAgICAgICAgICAgIHdlYmtpdC5tZXNzYWdlSGFuZGxlcnMuaGlnaGxpZ2h0QW5ub3RhdGlvbk1hcmtBY3RpdmF0ZWQucG9zdE1lc3NhZ2UoXG4gICAgICAgICAgICAgIGZvdW5kSGlnaGxpZ2h0LmlkXG4gICAgICAgICAgICApO1xuICAgICAgICAgIH1cbiAgICAgICAgfSBlbHNlIGlmIChmb3VuZEhpZ2hsaWdodC5pZC5zZWFyY2goXCJSMl9ISUdITElHSFRfXCIpID49IDApIHtcbiAgICAgICAgICBpZiAobmF2aWdhdG9yLnVzZXJBZ2VudC5tYXRjaCgvQW5kcm9pZC9pKSkge1xuICAgICAgICAgICAgQW5kcm9pZC5oaWdobGlnaHRBY3RpdmF0ZWQoZm91bmRIaWdobGlnaHQuaWQpO1xuICAgICAgICAgIH0gZWxzZSBpZiAobmF2aWdhdG9yLnVzZXJBZ2VudC5tYXRjaCgvaVBob25lfGlQYWR8aVBvZC9pKSkge1xuICAgICAgICAgICAgd2Via2l0Lm1lc3NhZ2VIYW5kbGVycy5oaWdobGlnaHRBY3RpdmF0ZWQucG9zdE1lc3NhZ2UoXG4gICAgICAgICAgICAgIGZvdW5kSGlnaGxpZ2h0LmlkXG4gICAgICAgICAgICApO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICBldi5zdG9wUHJvcGFnYXRpb24oKTtcbiAgICAgIGV2LnByZXZlbnREZWZhdWx0KCk7XG4gICAgfVxuICB9XG59XG5cbmZ1bmN0aW9uIHByb2Nlc3NNb3VzZUV2ZW50KHdpbiwgZXYpIHtcbiAgY29uc3QgZG9jdW1lbnQgPSB3aW4uZG9jdW1lbnQ7XG4gIGNvbnN0IHNjcm9sbEVsZW1lbnQgPSBnZXRTY3JvbGxpbmdFbGVtZW50KGRvY3VtZW50KTtcbiAgY29uc3QgeCA9IGV2LmNsaWVudFg7XG4gIGNvbnN0IHkgPSBldi5jbGllbnRZO1xuICBpZiAoIV9oaWdobGlnaHRzQ29udGFpbmVyKSB7XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgY29uc3QgcGFnaW5hdGVkID0gaXNQYWdpbmF0ZWQoZG9jdW1lbnQpO1xuICBjb25zdCBib2R5UmVjdCA9IGRvY3VtZW50LmJvZHkuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCk7XG4gIGxldCB4T2Zmc2V0O1xuICBsZXQgeU9mZnNldDtcbiAgaWYgKG5hdmlnYXRvci51c2VyQWdlbnQubWF0Y2goL0FuZHJvaWQvaSkpIHtcbiAgICB4T2Zmc2V0ID0gcGFnaW5hdGVkID8gLXNjcm9sbEVsZW1lbnQuc2Nyb2xsTGVmdCA6IGJvZHlSZWN0LmxlZnQ7XG4gICAgeU9mZnNldCA9IHBhZ2luYXRlZCA/IC1zY3JvbGxFbGVtZW50LnNjcm9sbFRvcCA6IGJvZHlSZWN0LnRvcDtcbiAgfSBlbHNlIGlmIChuYXZpZ2F0b3IudXNlckFnZW50Lm1hdGNoKC9pUGhvbmV8aVBhZHxpUG9kL2kpKSB7XG4gICAgeE9mZnNldCA9IHBhZ2luYXRlZCA/IDAgOiAtc2Nyb2xsRWxlbWVudC5zY3JvbGxMZWZ0O1xuICAgIHlPZmZzZXQgPSBwYWdpbmF0ZWQgPyAwIDogYm9keVJlY3QudG9wO1xuICB9XG4gIGxldCBmb3VuZEhpZ2hsaWdodDtcbiAgbGV0IGZvdW5kRWxlbWVudDtcbiAgbGV0IGZvdW5kUmVjdDtcbiAgZm9yIChsZXQgaSA9IF9oaWdobGlnaHRzLmxlbmd0aCAtIDE7IGkgPj0gMDsgaS0tKSB7XG4gICAgY29uc3QgaGlnaGxpZ2h0ID0gX2hpZ2hsaWdodHNbaV07XG4gICAgbGV0IGhpZ2hsaWdodFBhcmVudCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGAke2hpZ2hsaWdodC5pZH1gKTtcbiAgICBpZiAoIWhpZ2hsaWdodFBhcmVudCkge1xuICAgICAgaGlnaGxpZ2h0UGFyZW50ID0gX2hpZ2hsaWdodHNDb250YWluZXIucXVlcnlTZWxlY3RvcihgIyR7aGlnaGxpZ2h0LmlkfWApO1xuICAgIH1cbiAgICBpZiAoIWhpZ2hsaWdodFBhcmVudCkge1xuICAgICAgY29udGludWU7XG4gICAgfVxuICAgIGxldCBoaXQgPSBmYWxzZTtcbiAgICBjb25zdCBoaWdobGlnaHRGcmFnbWVudHMgPSBoaWdobGlnaHRQYXJlbnQucXVlcnlTZWxlY3RvckFsbChcbiAgICAgIGAuJHtDTEFTU19ISUdITElHSFRfQVJFQX1gXG4gICAgKTtcbiAgICBmb3IgKGNvbnN0IGhpZ2hsaWdodEZyYWdtZW50IG9mIGhpZ2hsaWdodEZyYWdtZW50cykge1xuICAgICAgY29uc3Qgd2l0aFJlY3QgPSBoaWdobGlnaHRGcmFnbWVudDtcbiAgICAgIGNvbnN0IGxlZnQgPSB3aXRoUmVjdC5yZWN0LmxlZnQgKyB4T2Zmc2V0O1xuICAgICAgY29uc3QgdG9wID0gd2l0aFJlY3QucmVjdC50b3AgKyB5T2Zmc2V0O1xuICAgICAgZm91bmRSZWN0ID0gd2l0aFJlY3QucmVjdDtcbiAgICAgIGlmIChcbiAgICAgICAgeCA+PSBsZWZ0ICYmXG4gICAgICAgIHggPCBsZWZ0ICsgd2l0aFJlY3QucmVjdC53aWR0aCAmJlxuICAgICAgICB5ID49IHRvcCAmJlxuICAgICAgICB5IDwgdG9wICsgd2l0aFJlY3QucmVjdC5oZWlnaHRcbiAgICAgICkge1xuICAgICAgICBoaXQgPSB0cnVlO1xuICAgICAgICBicmVhaztcbiAgICAgIH1cbiAgICB9XG4gICAgaWYgKGhpdCkge1xuICAgICAgZm91bmRIaWdobGlnaHQgPSBoaWdobGlnaHQ7XG4gICAgICBmb3VuZEVsZW1lbnQgPSBoaWdobGlnaHRQYXJlbnQ7XG4gICAgICBicmVhaztcbiAgICB9XG4gIH1cblxuICBpZiAoIWZvdW5kSGlnaGxpZ2h0IHx8ICFmb3VuZEVsZW1lbnQpIHtcbiAgICBjb25zdCBoaWdobGlnaHRCb3VuZGluZ3MgPSBfaGlnaGxpZ2h0c0NvbnRhaW5lci5xdWVyeVNlbGVjdG9yQWxsKFxuICAgICAgYC4ke0NMQVNTX0hJR0hMSUdIVF9CT1VORElOR19BUkVBfWBcbiAgICApO1xuICAgIGZvciAoY29uc3QgaGlnaGxpZ2h0Qm91bmRpbmcgb2YgaGlnaGxpZ2h0Qm91bmRpbmdzKSB7XG4gICAgICByZXNldEhpZ2hsaWdodEJvdW5kaW5nU3R5bGUod2luLCBoaWdobGlnaHRCb3VuZGluZyk7XG4gICAgfVxuICAgIGNvbnN0IGFsbEhpZ2hsaWdodEFyZWFzID0gQXJyYXkuZnJvbShcbiAgICAgIF9oaWdobGlnaHRzQ29udGFpbmVyLnF1ZXJ5U2VsZWN0b3JBbGwoYC4ke0NMQVNTX0hJR0hMSUdIVF9BUkVBfWApXG4gICAgKTtcbiAgICBmb3IgKGNvbnN0IGhpZ2hsaWdodEFyZWEgb2YgYWxsSGlnaGxpZ2h0QXJlYXMpIHtcbiAgICAgIHJlc2V0SGlnaGxpZ2h0QXJlYVN0eWxlKHdpbiwgaGlnaGxpZ2h0QXJlYSk7XG4gICAgfVxuICAgIHJldHVybjtcbiAgfVxuXG4gIGlmIChmb3VuZEVsZW1lbnQuZ2V0QXR0cmlidXRlKFwiZGF0YS1jbGlja1wiKSkge1xuICAgIGlmIChldi50eXBlID09PSBcIm1vdXNlbW92ZVwiKSB7XG4gICAgICBjb25zdCBmb3VuZEVsZW1lbnRIaWdobGlnaHRBcmVhcyA9IEFycmF5LmZyb20oXG4gICAgICAgIGZvdW5kRWxlbWVudC5xdWVyeVNlbGVjdG9yQWxsKGAuJHtDTEFTU19ISUdITElHSFRfQVJFQX1gKVxuICAgICAgKTtcbiAgICAgIGNvbnN0IGFsbEhpZ2hsaWdodEFyZWFzID0gX2hpZ2hsaWdodHNDb250YWluZXIucXVlcnlTZWxlY3RvckFsbChcbiAgICAgICAgYC4ke0NMQVNTX0hJR0hMSUdIVF9BUkVBfWBcbiAgICAgICk7XG4gICAgICBmb3IgKGNvbnN0IGhpZ2hsaWdodEFyZWEgb2YgYWxsSGlnaGxpZ2h0QXJlYXMpIHtcbiAgICAgICAgaWYgKGZvdW5kRWxlbWVudEhpZ2hsaWdodEFyZWFzLmluZGV4T2YoaGlnaGxpZ2h0QXJlYSkgPCAwKSB7XG4gICAgICAgICAgcmVzZXRIaWdobGlnaHRBcmVhU3R5bGUod2luLCBoaWdobGlnaHRBcmVhKTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgICAgc2V0SGlnaGxpZ2h0QXJlYVN0eWxlKHdpbiwgZm91bmRFbGVtZW50SGlnaGxpZ2h0QXJlYXMsIGZvdW5kSGlnaGxpZ2h0KTtcbiAgICAgIGNvbnN0IGZvdW5kRWxlbWVudEhpZ2hsaWdodEJvdW5kaW5nID0gZm91bmRFbGVtZW50LnF1ZXJ5U2VsZWN0b3IoXG4gICAgICAgIGAuJHtDTEFTU19ISUdITElHSFRfQk9VTkRJTkdfQVJFQX1gXG4gICAgICApO1xuICAgICAgY29uc3QgYWxsSGlnaGxpZ2h0Qm91bmRpbmdzID0gX2hpZ2hsaWdodHNDb250YWluZXIucXVlcnlTZWxlY3RvckFsbChcbiAgICAgICAgYC4ke0NMQVNTX0hJR0hMSUdIVF9CT1VORElOR19BUkVBfWBcbiAgICAgICk7XG4gICAgICBmb3IgKGNvbnN0IGhpZ2hsaWdodEJvdW5kaW5nIG9mIGFsbEhpZ2hsaWdodEJvdW5kaW5ncykge1xuICAgICAgICBpZiAoXG4gICAgICAgICAgIWZvdW5kRWxlbWVudEhpZ2hsaWdodEJvdW5kaW5nIHx8XG4gICAgICAgICAgaGlnaGxpZ2h0Qm91bmRpbmcgIT09IGZvdW5kRWxlbWVudEhpZ2hsaWdodEJvdW5kaW5nXG4gICAgICAgICkge1xuICAgICAgICAgIHJlc2V0SGlnaGxpZ2h0Qm91bmRpbmdTdHlsZSh3aW4sIGhpZ2hsaWdodEJvdW5kaW5nKTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgICAgaWYgKGZvdW5kRWxlbWVudEhpZ2hsaWdodEJvdW5kaW5nKSB7XG4gICAgICAgIGlmIChERUJVR19WSVNVQUxTKSB7XG4gICAgICAgICAgc2V0SGlnaGxpZ2h0Qm91bmRpbmdTdHlsZShcbiAgICAgICAgICAgIHdpbixcbiAgICAgICAgICAgIGZvdW5kRWxlbWVudEhpZ2hsaWdodEJvdW5kaW5nLFxuICAgICAgICAgICAgZm91bmRIaWdobGlnaHRcbiAgICAgICAgICApO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfSBlbHNlIGlmIChldi50eXBlID09PSBcIm1vdXNldXBcIiB8fCBldi50eXBlID09PSBcInRvdWNoZW5kXCIpIHtcbiAgICAgIGNvbnN0IHRvdWNoZWRQb3NpdGlvbiA9IHtcbiAgICAgICAgc2NyZWVuV2lkdGg6IHdpbmRvdy5vdXRlcldpZHRoLFxuICAgICAgICBzY3JlZW5IZWlnaHQ6IHdpbmRvdy5pbm5lckhlaWdodCxcbiAgICAgICAgbGVmdDogZm91bmRSZWN0LmxlZnQsXG4gICAgICAgIHdpZHRoOiBmb3VuZFJlY3Qud2lkdGgsXG4gICAgICAgIHRvcDogZm91bmRSZWN0LnRvcCxcbiAgICAgICAgaGVpZ2h0OiBmb3VuZFJlY3QuaGVpZ2h0LFxuICAgICAgfTtcblxuICAgICAgY29uc3QgcGF5bG9hZCA9IHtcbiAgICAgICAgaGlnaGxpZ2h0OiBmb3VuZEhpZ2hsaWdodCxcbiAgICAgICAgcG9zaXRpb246IHRvdWNoZWRQb3NpdGlvbixcbiAgICAgIH07XG5cbiAgICAgIGlmIChcbiAgICAgICAgdHlwZW9mIHdpbmRvdyAhPT0gXCJ1bmRlZmluZWRcIiAmJlxuICAgICAgICB0eXBlb2Ygd2luZG93LnByb2Nlc3MgPT09IFwib2JqZWN0XCIgJiZcbiAgICAgICAgd2luZG93LnByb2Nlc3MudHlwZSA9PT0gXCJyZW5kZXJlclwiXG4gICAgICApIHtcbiAgICAgICAgZWxlY3Ryb25fMS5pcGNSZW5kZXJlci5zZW5kVG9Ib3N0KFIyX0VWRU5UX0hJR0hMSUdIVF9DTElDSywgcGF5bG9hZCk7XG4gICAgICB9IGVsc2UgaWYgKHdpbmRvdy53ZWJraXRVUkwpIHtcbiAgICAgICAgaWYgKGZvdW5kSGlnaGxpZ2h0LmlkLnNlYXJjaChcIlIyX0FOTk9UQVRJT05fXCIpID49IDApIHtcbiAgICAgICAgICBpZiAobmF2aWdhdG9yLnVzZXJBZ2VudC5tYXRjaCgvQW5kcm9pZC9pKSkge1xuICAgICAgICAgICAgQW5kcm9pZC5oaWdobGlnaHRBbm5vdGF0aW9uTWFya0FjdGl2YXRlZChmb3VuZEhpZ2hsaWdodC5pZCk7XG4gICAgICAgICAgfSBlbHNlIGlmIChuYXZpZ2F0b3IudXNlckFnZW50Lm1hdGNoKC9pUGhvbmV8aVBhZHxpUG9kL2kpKSB7XG4gICAgICAgICAgICB3ZWJraXQubWVzc2FnZUhhbmRsZXJzLmhpZ2hsaWdodEFubm90YXRpb25NYXJrQWN0aXZhdGVkLnBvc3RNZXNzYWdlKFxuICAgICAgICAgICAgICBmb3VuZEhpZ2hsaWdodC5pZFxuICAgICAgICAgICAgKTtcbiAgICAgICAgICB9XG4gICAgICAgIH0gZWxzZSBpZiAoZm91bmRIaWdobGlnaHQuaWQuc2VhcmNoKFwiUjJfSElHSExJR0hUX1wiKSA+PSAwKSB7XG4gICAgICAgICAgaWYgKG5hdmlnYXRvci51c2VyQWdlbnQubWF0Y2goL0FuZHJvaWQvaSkpIHtcbiAgICAgICAgICAgIEFuZHJvaWQuaGlnaGxpZ2h0QWN0aXZhdGVkKGZvdW5kSGlnaGxpZ2h0LmlkKTtcbiAgICAgICAgICB9IGVsc2UgaWYgKG5hdmlnYXRvci51c2VyQWdlbnQubWF0Y2goL2lQaG9uZXxpUGFkfGlQb2QvaSkpIHtcbiAgICAgICAgICAgIHdlYmtpdC5tZXNzYWdlSGFuZGxlcnMuaGlnaGxpZ2h0QWN0aXZhdGVkLnBvc3RNZXNzYWdlKFxuICAgICAgICAgICAgICBmb3VuZEhpZ2hsaWdodC5pZFxuICAgICAgICAgICAgKTtcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH1cblxuICAgICAgZXYuc3RvcFByb3BhZ2F0aW9uKCk7XG4gICAgfVxuICB9XG59XG5cbmZ1bmN0aW9uIHJlY3RzVG91Y2hPck92ZXJsYXAocmVjdDEsIHJlY3QyLCB0b2xlcmFuY2UpIHtcbiAgcmV0dXJuIChcbiAgICAocmVjdDEubGVmdCA8IHJlY3QyLnJpZ2h0IHx8XG4gICAgICAodG9sZXJhbmNlID49IDAgJiYgYWxtb3N0RXF1YWwocmVjdDEubGVmdCwgcmVjdDIucmlnaHQsIHRvbGVyYW5jZSkpKSAmJlxuICAgIChyZWN0Mi5sZWZ0IDwgcmVjdDEucmlnaHQgfHxcbiAgICAgICh0b2xlcmFuY2UgPj0gMCAmJiBhbG1vc3RFcXVhbChyZWN0Mi5sZWZ0LCByZWN0MS5yaWdodCwgdG9sZXJhbmNlKSkpICYmXG4gICAgKHJlY3QxLnRvcCA8IHJlY3QyLmJvdHRvbSB8fFxuICAgICAgKHRvbGVyYW5jZSA+PSAwICYmIGFsbW9zdEVxdWFsKHJlY3QxLnRvcCwgcmVjdDIuYm90dG9tLCB0b2xlcmFuY2UpKSkgJiZcbiAgICAocmVjdDIudG9wIDwgcmVjdDEuYm90dG9tIHx8XG4gICAgICAodG9sZXJhbmNlID49IDAgJiYgYWxtb3N0RXF1YWwocmVjdDIudG9wLCByZWN0MS5ib3R0b20sIHRvbGVyYW5jZSkpKVxuICApO1xufVxuXG5mdW5jdGlvbiByZXBsYWNlT3ZlcmxhcGluZ1JlY3RzKHJlY3RzKSB7XG4gIGZvciAobGV0IGkgPSAwOyBpIDwgcmVjdHMubGVuZ3RoOyBpKyspIHtcbiAgICBmb3IgKGxldCBqID0gaSArIDE7IGogPCByZWN0cy5sZW5ndGg7IGorKykge1xuICAgICAgY29uc3QgcmVjdDEgPSByZWN0c1tpXTtcbiAgICAgIGNvbnN0IHJlY3QyID0gcmVjdHNbal07XG4gICAgICBpZiAocmVjdDEgPT09IHJlY3QyKSB7XG4gICAgICAgIGlmIChJU19ERVYpIHtcbiAgICAgICAgICBjb25zb2xlLmxvZyhcInJlcGxhY2VPdmVybGFwaW5nUmVjdHMgcmVjdDEgPT09IHJlY3QyID8/IVwiKTtcbiAgICAgICAgfVxuICAgICAgICBjb250aW51ZTtcbiAgICAgIH1cbiAgICAgIGlmIChyZWN0c1RvdWNoT3JPdmVybGFwKHJlY3QxLCByZWN0MiwgLTEpKSB7XG4gICAgICAgIGxldCB0b0FkZCA9IFtdO1xuICAgICAgICBsZXQgdG9SZW1vdmU7XG4gICAgICAgIGxldCB0b1ByZXNlcnZlO1xuICAgICAgICBjb25zdCBzdWJ0cmFjdFJlY3RzMSA9IHJlY3RTdWJ0cmFjdChyZWN0MSwgcmVjdDIpO1xuICAgICAgICBpZiAoc3VidHJhY3RSZWN0czEubGVuZ3RoID09PSAxKSB7XG4gICAgICAgICAgdG9BZGQgPSBzdWJ0cmFjdFJlY3RzMTtcbiAgICAgICAgICB0b1JlbW92ZSA9IHJlY3QxO1xuICAgICAgICAgIHRvUHJlc2VydmUgPSByZWN0MjtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBjb25zdCBzdWJ0cmFjdFJlY3RzMiA9IHJlY3RTdWJ0cmFjdChyZWN0MiwgcmVjdDEpO1xuICAgICAgICAgIGlmIChzdWJ0cmFjdFJlY3RzMS5sZW5ndGggPCBzdWJ0cmFjdFJlY3RzMi5sZW5ndGgpIHtcbiAgICAgICAgICAgIHRvQWRkID0gc3VidHJhY3RSZWN0czE7XG4gICAgICAgICAgICB0b1JlbW92ZSA9IHJlY3QxO1xuICAgICAgICAgICAgdG9QcmVzZXJ2ZSA9IHJlY3QyO1xuICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICB0b0FkZCA9IHN1YnRyYWN0UmVjdHMyO1xuICAgICAgICAgICAgdG9SZW1vdmUgPSByZWN0MjtcbiAgICAgICAgICAgIHRvUHJlc2VydmUgPSByZWN0MTtcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgaWYgKElTX0RFVikge1xuICAgICAgICAgIGNvbnN0IHRvQ2hlY2sgPSBbXTtcbiAgICAgICAgICB0b0NoZWNrLnB1c2godG9QcmVzZXJ2ZSk7XG4gICAgICAgICAgQXJyYXkucHJvdG90eXBlLnB1c2guYXBwbHkodG9DaGVjaywgdG9BZGQpO1xuICAgICAgICAgIGNoZWNrT3ZlcmxhcHModG9DaGVjayk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKElTX0RFVikge1xuICAgICAgICAgIGNvbnNvbGUubG9nKFxuICAgICAgICAgICAgYENMSUVOVCBSRUNUOiBvdmVybGFwLCBjdXQgb25lIHJlY3QgaW50byAke3RvQWRkLmxlbmd0aH1gXG4gICAgICAgICAgKTtcbiAgICAgICAgfVxuICAgICAgICBjb25zdCBuZXdSZWN0cyA9IHJlY3RzLmZpbHRlcigocmVjdCkgPT4ge1xuICAgICAgICAgIHJldHVybiByZWN0ICE9PSB0b1JlbW92ZTtcbiAgICAgICAgfSk7XG4gICAgICAgIEFycmF5LnByb3RvdHlwZS5wdXNoLmFwcGx5KG5ld1JlY3RzLCB0b0FkZCk7XG4gICAgICAgIHJldHVybiByZXBsYWNlT3ZlcmxhcGluZ1JlY3RzKG5ld1JlY3RzKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgcmV0dXJuIHJlY3RzO1xufVxuXG5mdW5jdGlvbiBjaGVja092ZXJsYXBzKHJlY3RzKSB7XG4gIGNvbnN0IHN0aWxsT3ZlcmxhcGluZ1JlY3RzID0gW107XG4gIGZvciAoY29uc3QgcmVjdDEgb2YgcmVjdHMpIHtcbiAgICBmb3IgKGNvbnN0IHJlY3QyIG9mIHJlY3RzKSB7XG4gICAgICBpZiAocmVjdDEgPT09IHJlY3QyKSB7XG4gICAgICAgIGNvbnRpbnVlO1xuICAgICAgfVxuICAgICAgY29uc3QgaGFzMSA9IHN0aWxsT3ZlcmxhcGluZ1JlY3RzLmluZGV4T2YocmVjdDEpID49IDA7XG4gICAgICBjb25zdCBoYXMyID0gc3RpbGxPdmVybGFwaW5nUmVjdHMuaW5kZXhPZihyZWN0MikgPj0gMDtcbiAgICAgIGlmICghaGFzMSB8fCAhaGFzMikge1xuICAgICAgICBpZiAocmVjdHNUb3VjaE9yT3ZlcmxhcChyZWN0MSwgcmVjdDIsIC0xKSkge1xuICAgICAgICAgIGlmICghaGFzMSkge1xuICAgICAgICAgICAgc3RpbGxPdmVybGFwaW5nUmVjdHMucHVzaChyZWN0MSk7XG4gICAgICAgICAgfVxuICAgICAgICAgIGlmICghaGFzMikge1xuICAgICAgICAgICAgc3RpbGxPdmVybGFwaW5nUmVjdHMucHVzaChyZWN0Mik7XG4gICAgICAgICAgfVxuICAgICAgICAgIGNvbnNvbGUubG9nKFwiQ0xJRU5UIFJFQ1Q6IG92ZXJsYXAgLS0tXCIpO1xuICAgICAgICAgIGNvbnNvbGUubG9nKFxuICAgICAgICAgICAgYCMxIFRPUDoke3JlY3QxLnRvcH0gQk9UVE9NOiR7cmVjdDEuYm90dG9tfSBMRUZUOiR7cmVjdDEubGVmdH0gUklHSFQ6JHtyZWN0MS5yaWdodH0gV0lEVEg6JHtyZWN0MS53aWR0aH0gSEVJR0hUOiR7cmVjdDEuaGVpZ2h0fWBcbiAgICAgICAgICApO1xuICAgICAgICAgIGNvbnNvbGUubG9nKFxuICAgICAgICAgICAgYCMyIFRPUDoke3JlY3QyLnRvcH0gQk9UVE9NOiR7cmVjdDIuYm90dG9tfSBMRUZUOiR7cmVjdDIubGVmdH0gUklHSFQ6JHtyZWN0Mi5yaWdodH0gV0lEVEg6JHtyZWN0Mi53aWR0aH0gSEVJR0hUOiR7cmVjdDIuaGVpZ2h0fWBcbiAgICAgICAgICApO1xuICAgICAgICAgIGNvbnN0IHhPdmVybGFwID0gZ2V0UmVjdE92ZXJsYXBYKHJlY3QxLCByZWN0Mik7XG4gICAgICAgICAgY29uc29sZS5sb2coYHhPdmVybGFwOiAke3hPdmVybGFwfWApO1xuICAgICAgICAgIGNvbnN0IHlPdmVybGFwID0gZ2V0UmVjdE92ZXJsYXBZKHJlY3QxLCByZWN0Mik7XG4gICAgICAgICAgY29uc29sZS5sb2coYHlPdmVybGFwOiAke3lPdmVybGFwfWApO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuICB9XG4gIGlmIChzdGlsbE92ZXJsYXBpbmdSZWN0cy5sZW5ndGgpIHtcbiAgICBjb25zb2xlLmxvZyhgQ0xJRU5UIFJFQ1Q6IG92ZXJsYXBzICR7c3RpbGxPdmVybGFwaW5nUmVjdHMubGVuZ3RofWApO1xuICB9XG59XG5cbmZ1bmN0aW9uIHJlbW92ZUNvbnRhaW5lZFJlY3RzKHJlY3RzLCB0b2xlcmFuY2UpIHtcbiAgY29uc3QgcmVjdHNUb0tlZXAgPSBuZXcgU2V0KHJlY3RzKTtcbiAgZm9yIChjb25zdCByZWN0IG9mIHJlY3RzKSB7XG4gICAgY29uc3QgYmlnRW5vdWdoID0gcmVjdC53aWR0aCA+IDEgJiYgcmVjdC5oZWlnaHQgPiAxO1xuICAgIGlmICghYmlnRW5vdWdoKSB7XG4gICAgICBpZiAoSVNfREVWKSB7XG4gICAgICAgIGNvbnNvbGUubG9nKFwiQ0xJRU5UIFJFQ1Q6IHJlbW92ZSB0aW55XCIpO1xuICAgICAgfVxuICAgICAgcmVjdHNUb0tlZXAuZGVsZXRlKHJlY3QpO1xuICAgICAgY29udGludWU7XG4gICAgfVxuICAgIGZvciAoY29uc3QgcG9zc2libHlDb250YWluaW5nUmVjdCBvZiByZWN0cykge1xuICAgICAgaWYgKHJlY3QgPT09IHBvc3NpYmx5Q29udGFpbmluZ1JlY3QpIHtcbiAgICAgICAgY29udGludWU7XG4gICAgICB9XG4gICAgICBpZiAoIXJlY3RzVG9LZWVwLmhhcyhwb3NzaWJseUNvbnRhaW5pbmdSZWN0KSkge1xuICAgICAgICBjb250aW51ZTtcbiAgICAgIH1cbiAgICAgIGlmIChyZWN0Q29udGFpbnMocG9zc2libHlDb250YWluaW5nUmVjdCwgcmVjdCwgdG9sZXJhbmNlKSkge1xuICAgICAgICBpZiAoSVNfREVWKSB7XG4gICAgICAgICAgY29uc29sZS5sb2coXCJDTElFTlQgUkVDVDogcmVtb3ZlIGNvbnRhaW5lZFwiKTtcbiAgICAgICAgfVxuICAgICAgICByZWN0c1RvS2VlcC5kZWxldGUocmVjdCk7XG4gICAgICAgIGJyZWFrO1xuICAgICAgfVxuICAgIH1cbiAgfVxuICByZXR1cm4gQXJyYXkuZnJvbShyZWN0c1RvS2VlcCk7XG59XG5cbmZ1bmN0aW9uIGFsbW9zdEVxdWFsKGEsIGIsIHRvbGVyYW5jZSkge1xuICByZXR1cm4gTWF0aC5hYnMoYSAtIGIpIDw9IHRvbGVyYW5jZTtcbn1cblxuZnVuY3Rpb24gcmVjdEludGVyc2VjdChyZWN0MSwgcmVjdDIpIHtcbiAgY29uc3QgbWF4TGVmdCA9IE1hdGgubWF4KHJlY3QxLmxlZnQsIHJlY3QyLmxlZnQpO1xuICBjb25zdCBtaW5SaWdodCA9IE1hdGgubWluKHJlY3QxLnJpZ2h0LCByZWN0Mi5yaWdodCk7XG4gIGNvbnN0IG1heFRvcCA9IE1hdGgubWF4KHJlY3QxLnRvcCwgcmVjdDIudG9wKTtcbiAgY29uc3QgbWluQm90dG9tID0gTWF0aC5taW4ocmVjdDEuYm90dG9tLCByZWN0Mi5ib3R0b20pO1xuICBjb25zdCByZWN0ID0ge1xuICAgIGJvdHRvbTogbWluQm90dG9tLFxuICAgIGhlaWdodDogTWF0aC5tYXgoMCwgbWluQm90dG9tIC0gbWF4VG9wKSxcbiAgICBsZWZ0OiBtYXhMZWZ0LFxuICAgIHJpZ2h0OiBtaW5SaWdodCxcbiAgICB0b3A6IG1heFRvcCxcbiAgICB3aWR0aDogTWF0aC5tYXgoMCwgbWluUmlnaHQgLSBtYXhMZWZ0KSxcbiAgfTtcbiAgcmV0dXJuIHJlY3Q7XG59XG5cbmZ1bmN0aW9uIHJlY3RTdWJ0cmFjdChyZWN0MSwgcmVjdDIpIHtcbiAgY29uc3QgcmVjdEludGVyc2VjdGVkID0gcmVjdEludGVyc2VjdChyZWN0MiwgcmVjdDEpO1xuICBpZiAocmVjdEludGVyc2VjdGVkLmhlaWdodCA9PT0gMCB8fCByZWN0SW50ZXJzZWN0ZWQud2lkdGggPT09IDApIHtcbiAgICByZXR1cm4gW3JlY3QxXTtcbiAgfVxuICBjb25zdCByZWN0cyA9IFtdO1xuICB7XG4gICAgY29uc3QgcmVjdEEgPSB7XG4gICAgICBib3R0b206IHJlY3QxLmJvdHRvbSxcbiAgICAgIGhlaWdodDogMCxcbiAgICAgIGxlZnQ6IHJlY3QxLmxlZnQsXG4gICAgICByaWdodDogcmVjdEludGVyc2VjdGVkLmxlZnQsXG4gICAgICB0b3A6IHJlY3QxLnRvcCxcbiAgICAgIHdpZHRoOiAwLFxuICAgIH07XG4gICAgcmVjdEEud2lkdGggPSByZWN0QS5yaWdodCAtIHJlY3RBLmxlZnQ7XG4gICAgcmVjdEEuaGVpZ2h0ID0gcmVjdEEuYm90dG9tIC0gcmVjdEEudG9wO1xuICAgIGlmIChyZWN0QS5oZWlnaHQgIT09IDAgJiYgcmVjdEEud2lkdGggIT09IDApIHtcbiAgICAgIHJlY3RzLnB1c2gocmVjdEEpO1xuICAgIH1cbiAgfVxuICB7XG4gICAgY29uc3QgcmVjdEIgPSB7XG4gICAgICBib3R0b206IHJlY3RJbnRlcnNlY3RlZC50b3AsXG4gICAgICBoZWlnaHQ6IDAsXG4gICAgICBsZWZ0OiByZWN0SW50ZXJzZWN0ZWQubGVmdCxcbiAgICAgIHJpZ2h0OiByZWN0SW50ZXJzZWN0ZWQucmlnaHQsXG4gICAgICB0b3A6IHJlY3QxLnRvcCxcbiAgICAgIHdpZHRoOiAwLFxuICAgIH07XG4gICAgcmVjdEIud2lkdGggPSByZWN0Qi5yaWdodCAtIHJlY3RCLmxlZnQ7XG4gICAgcmVjdEIuaGVpZ2h0ID0gcmVjdEIuYm90dG9tIC0gcmVjdEIudG9wO1xuICAgIGlmIChyZWN0Qi5oZWlnaHQgIT09IDAgJiYgcmVjdEIud2lkdGggIT09IDApIHtcbiAgICAgIHJlY3RzLnB1c2gocmVjdEIpO1xuICAgIH1cbiAgfVxuICB7XG4gICAgY29uc3QgcmVjdEMgPSB7XG4gICAgICBib3R0b206IHJlY3QxLmJvdHRvbSxcbiAgICAgIGhlaWdodDogMCxcbiAgICAgIGxlZnQ6IHJlY3RJbnRlcnNlY3RlZC5sZWZ0LFxuICAgICAgcmlnaHQ6IHJlY3RJbnRlcnNlY3RlZC5yaWdodCxcbiAgICAgIHRvcDogcmVjdEludGVyc2VjdGVkLmJvdHRvbSxcbiAgICAgIHdpZHRoOiAwLFxuICAgIH07XG4gICAgcmVjdEMud2lkdGggPSByZWN0Qy5yaWdodCAtIHJlY3RDLmxlZnQ7XG4gICAgcmVjdEMuaGVpZ2h0ID0gcmVjdEMuYm90dG9tIC0gcmVjdEMudG9wO1xuICAgIGlmIChyZWN0Qy5oZWlnaHQgIT09IDAgJiYgcmVjdEMud2lkdGggIT09IDApIHtcbiAgICAgIHJlY3RzLnB1c2gocmVjdEMpO1xuICAgIH1cbiAgfVxuICB7XG4gICAgY29uc3QgcmVjdEQgPSB7XG4gICAgICBib3R0b206IHJlY3QxLmJvdHRvbSxcbiAgICAgIGhlaWdodDogMCxcbiAgICAgIGxlZnQ6IHJlY3RJbnRlcnNlY3RlZC5yaWdodCxcbiAgICAgIHJpZ2h0OiByZWN0MS5yaWdodCxcbiAgICAgIHRvcDogcmVjdDEudG9wLFxuICAgICAgd2lkdGg6IDAsXG4gICAgfTtcbiAgICByZWN0RC53aWR0aCA9IHJlY3RELnJpZ2h0IC0gcmVjdEQubGVmdDtcbiAgICByZWN0RC5oZWlnaHQgPSByZWN0RC5ib3R0b20gLSByZWN0RC50b3A7XG4gICAgaWYgKHJlY3RELmhlaWdodCAhPT0gMCAmJiByZWN0RC53aWR0aCAhPT0gMCkge1xuICAgICAgcmVjdHMucHVzaChyZWN0RCk7XG4gICAgfVxuICB9XG4gIHJldHVybiByZWN0cztcbn1cblxuZnVuY3Rpb24gcmVjdENvbnRhaW5zUG9pbnQocmVjdCwgeCwgeSwgdG9sZXJhbmNlKSB7XG4gIHJldHVybiAoXG4gICAgKHJlY3QubGVmdCA8IHggfHwgYWxtb3N0RXF1YWwocmVjdC5sZWZ0LCB4LCB0b2xlcmFuY2UpKSAmJlxuICAgIChyZWN0LnJpZ2h0ID4geCB8fCBhbG1vc3RFcXVhbChyZWN0LnJpZ2h0LCB4LCB0b2xlcmFuY2UpKSAmJlxuICAgIChyZWN0LnRvcCA8IHkgfHwgYWxtb3N0RXF1YWwocmVjdC50b3AsIHksIHRvbGVyYW5jZSkpICYmXG4gICAgKHJlY3QuYm90dG9tID4geSB8fCBhbG1vc3RFcXVhbChyZWN0LmJvdHRvbSwgeSwgdG9sZXJhbmNlKSlcbiAgKTtcbn1cblxuZnVuY3Rpb24gcmVjdENvbnRhaW5zKHJlY3QxLCByZWN0MiwgdG9sZXJhbmNlKSB7XG4gIHJldHVybiAoXG4gICAgcmVjdENvbnRhaW5zUG9pbnQocmVjdDEsIHJlY3QyLmxlZnQsIHJlY3QyLnRvcCwgdG9sZXJhbmNlKSAmJlxuICAgIHJlY3RDb250YWluc1BvaW50KHJlY3QxLCByZWN0Mi5yaWdodCwgcmVjdDIudG9wLCB0b2xlcmFuY2UpICYmXG4gICAgcmVjdENvbnRhaW5zUG9pbnQocmVjdDEsIHJlY3QyLmxlZnQsIHJlY3QyLmJvdHRvbSwgdG9sZXJhbmNlKSAmJlxuICAgIHJlY3RDb250YWluc1BvaW50KHJlY3QxLCByZWN0Mi5yaWdodCwgcmVjdDIuYm90dG9tLCB0b2xlcmFuY2UpXG4gICk7XG59XG5cbmZ1bmN0aW9uIGdldEJvdW5kaW5nUmVjdChyZWN0MSwgcmVjdDIpIHtcbiAgY29uc3QgbGVmdCA9IE1hdGgubWluKHJlY3QxLmxlZnQsIHJlY3QyLmxlZnQpO1xuICBjb25zdCByaWdodCA9IE1hdGgubWF4KHJlY3QxLnJpZ2h0LCByZWN0Mi5yaWdodCk7XG4gIGNvbnN0IHRvcCA9IE1hdGgubWluKHJlY3QxLnRvcCwgcmVjdDIudG9wKTtcbiAgY29uc3QgYm90dG9tID0gTWF0aC5tYXgocmVjdDEuYm90dG9tLCByZWN0Mi5ib3R0b20pO1xuICByZXR1cm4ge1xuICAgIGJvdHRvbSxcbiAgICBoZWlnaHQ6IGJvdHRvbSAtIHRvcCxcbiAgICBsZWZ0LFxuICAgIHJpZ2h0LFxuICAgIHRvcCxcbiAgICB3aWR0aDogcmlnaHQgLSBsZWZ0LFxuICB9O1xufVxuXG5mdW5jdGlvbiBtZXJnZVRvdWNoaW5nUmVjdHMoXG4gIHJlY3RzLFxuICB0b2xlcmFuY2UsXG4gIGRvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHNcbikge1xuICBmb3IgKGxldCBpID0gMDsgaSA8IHJlY3RzLmxlbmd0aDsgaSsrKSB7XG4gICAgZm9yIChsZXQgaiA9IGkgKyAxOyBqIDwgcmVjdHMubGVuZ3RoOyBqKyspIHtcbiAgICAgIGNvbnN0IHJlY3QxID0gcmVjdHNbaV07XG4gICAgICBjb25zdCByZWN0MiA9IHJlY3RzW2pdO1xuICAgICAgaWYgKHJlY3QxID09PSByZWN0Mikge1xuICAgICAgICBpZiAoSVNfREVWKSB7XG4gICAgICAgICAgY29uc29sZS5sb2coXCJtZXJnZVRvdWNoaW5nUmVjdHMgcmVjdDEgPT09IHJlY3QyID8/IVwiKTtcbiAgICAgICAgfVxuICAgICAgICBjb250aW51ZTtcbiAgICAgIH1cbiAgICAgIGNvbnN0IHJlY3RzTGluZVVwVmVydGljYWxseSA9XG4gICAgICAgIGFsbW9zdEVxdWFsKHJlY3QxLnRvcCwgcmVjdDIudG9wLCB0b2xlcmFuY2UpICYmXG4gICAgICAgIGFsbW9zdEVxdWFsKHJlY3QxLmJvdHRvbSwgcmVjdDIuYm90dG9tLCB0b2xlcmFuY2UpO1xuICAgICAgY29uc3QgcmVjdHNMaW5lVXBIb3Jpem9udGFsbHkgPVxuICAgICAgICBhbG1vc3RFcXVhbChyZWN0MS5sZWZ0LCByZWN0Mi5sZWZ0LCB0b2xlcmFuY2UpICYmXG4gICAgICAgIGFsbW9zdEVxdWFsKHJlY3QxLnJpZ2h0LCByZWN0Mi5yaWdodCwgdG9sZXJhbmNlKTtcbiAgICAgIGNvbnN0IGhvcml6b250YWxBbGxvd2VkID0gIWRvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHM7XG4gICAgICBjb25zdCBhbGlnbmVkID1cbiAgICAgICAgKHJlY3RzTGluZVVwSG9yaXpvbnRhbGx5ICYmIGhvcml6b250YWxBbGxvd2VkKSB8fFxuICAgICAgICAocmVjdHNMaW5lVXBWZXJ0aWNhbGx5ICYmICFyZWN0c0xpbmVVcEhvcml6b250YWxseSk7XG4gICAgICBjb25zdCBjYW5NZXJnZSA9IGFsaWduZWQgJiYgcmVjdHNUb3VjaE9yT3ZlcmxhcChyZWN0MSwgcmVjdDIsIHRvbGVyYW5jZSk7XG4gICAgICBpZiAoY2FuTWVyZ2UpIHtcbiAgICAgICAgaWYgKElTX0RFVikge1xuICAgICAgICAgIGNvbnNvbGUubG9nKFxuICAgICAgICAgICAgYENMSUVOVCBSRUNUOiBtZXJnaW5nIHR3byBpbnRvIG9uZSwgVkVSVElDQUw6ICR7cmVjdHNMaW5lVXBWZXJ0aWNhbGx5fSBIT1JJWk9OVEFMOiAke3JlY3RzTGluZVVwSG9yaXpvbnRhbGx5fSAoJHtkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzfSlgXG4gICAgICAgICAgKTtcbiAgICAgICAgfVxuICAgICAgICBjb25zdCBuZXdSZWN0cyA9IHJlY3RzLmZpbHRlcigocmVjdCkgPT4ge1xuICAgICAgICAgIHJldHVybiByZWN0ICE9PSByZWN0MSAmJiByZWN0ICE9PSByZWN0MjtcbiAgICAgICAgfSk7XG4gICAgICAgIGNvbnN0IHJlcGxhY2VtZW50Q2xpZW50UmVjdCA9IGdldEJvdW5kaW5nUmVjdChyZWN0MSwgcmVjdDIpO1xuICAgICAgICBuZXdSZWN0cy5wdXNoKHJlcGxhY2VtZW50Q2xpZW50UmVjdCk7XG4gICAgICAgIHJldHVybiBtZXJnZVRvdWNoaW5nUmVjdHMoXG4gICAgICAgICAgbmV3UmVjdHMsXG4gICAgICAgICAgdG9sZXJhbmNlLFxuICAgICAgICAgIGRvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHNcbiAgICAgICAgKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgcmV0dXJuIHJlY3RzO1xufVxuXG5mdW5jdGlvbiBnZXRDbGllbnRSZWN0c05vT3ZlcmxhcChyYW5nZSwgZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0cykge1xuICBjb25zdCByYW5nZUNsaWVudFJlY3RzID0gcmFuZ2UuZ2V0Q2xpZW50UmVjdHMoKTtcbiAgcmV0dXJuIGdldENsaWVudFJlY3RzTm9PdmVybGFwXyhcbiAgICByYW5nZUNsaWVudFJlY3RzLFxuICAgIGRvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHNcbiAgKTtcbn1cblxuZnVuY3Rpb24gZ2V0Q2xpZW50UmVjdHNOb092ZXJsYXBfKFxuICBjbGllbnRSZWN0cyxcbiAgZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0c1xuKSB7XG4gIGNvbnN0IHRvbGVyYW5jZSA9IDE7XG4gIGNvbnN0IG9yaWdpbmFsUmVjdHMgPSBbXTtcbiAgZm9yIChjb25zdCByYW5nZUNsaWVudFJlY3Qgb2YgY2xpZW50UmVjdHMpIHtcbiAgICBvcmlnaW5hbFJlY3RzLnB1c2goe1xuICAgICAgYm90dG9tOiByYW5nZUNsaWVudFJlY3QuYm90dG9tLFxuICAgICAgaGVpZ2h0OiByYW5nZUNsaWVudFJlY3QuaGVpZ2h0LFxuICAgICAgbGVmdDogcmFuZ2VDbGllbnRSZWN0LmxlZnQsXG4gICAgICByaWdodDogcmFuZ2VDbGllbnRSZWN0LnJpZ2h0LFxuICAgICAgdG9wOiByYW5nZUNsaWVudFJlY3QudG9wLFxuICAgICAgd2lkdGg6IHJhbmdlQ2xpZW50UmVjdC53aWR0aCxcbiAgICB9KTtcbiAgfVxuICBjb25zdCBtZXJnZWRSZWN0cyA9IG1lcmdlVG91Y2hpbmdSZWN0cyhcbiAgICBvcmlnaW5hbFJlY3RzLFxuICAgIHRvbGVyYW5jZSxcbiAgICBkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzXG4gICk7XG4gIGNvbnN0IG5vQ29udGFpbmVkUmVjdHMgPSByZW1vdmVDb250YWluZWRSZWN0cyhtZXJnZWRSZWN0cywgdG9sZXJhbmNlKTtcbiAgY29uc3QgbmV3UmVjdHMgPSByZXBsYWNlT3ZlcmxhcGluZ1JlY3RzKG5vQ29udGFpbmVkUmVjdHMpO1xuICBjb25zdCBtaW5BcmVhID0gMiAqIDI7XG4gIGZvciAobGV0IGogPSBuZXdSZWN0cy5sZW5ndGggLSAxOyBqID49IDA7IGotLSkge1xuICAgIGNvbnN0IHJlY3QgPSBuZXdSZWN0c1tqXTtcbiAgICBjb25zdCBiaWdFbm91Z2ggPSByZWN0LndpZHRoICogcmVjdC5oZWlnaHQgPiBtaW5BcmVhO1xuICAgIGlmICghYmlnRW5vdWdoKSB7XG4gICAgICBpZiAobmV3UmVjdHMubGVuZ3RoID4gMSkge1xuICAgICAgICBpZiAoSVNfREVWKSB7XG4gICAgICAgICAgY29uc29sZS5sb2coXCJDTElFTlQgUkVDVDogcmVtb3ZlIHNtYWxsXCIpO1xuICAgICAgICB9XG4gICAgICAgIG5ld1JlY3RzLnNwbGljZShqLCAxKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGlmIChJU19ERVYpIHtcbiAgICAgICAgICBjb25zb2xlLmxvZyhcIkNMSUVOVCBSRUNUOiByZW1vdmUgc21hbGwsIGJ1dCBrZWVwIG90aGVyd2lzZSBlbXB0eSFcIik7XG4gICAgICAgIH1cbiAgICAgICAgYnJlYWs7XG4gICAgICB9XG4gICAgfVxuICB9XG4gIGlmIChJU19ERVYpIHtcbiAgICBjaGVja092ZXJsYXBzKG5ld1JlY3RzKTtcbiAgfVxuICBpZiAoSVNfREVWKSB7XG4gICAgY29uc29sZS5sb2coXG4gICAgICBgQ0xJRU5UIFJFQ1Q6IHJlZHVjZWQgJHtvcmlnaW5hbFJlY3RzLmxlbmd0aH0gLS0+ICR7bmV3UmVjdHMubGVuZ3RofWBcbiAgICApO1xuICB9XG4gIHJldHVybiBuZXdSZWN0cztcbn1cblxuZnVuY3Rpb24gaXNQYWdpbmF0ZWQoZG9jdW1lbnQpIHtcbiAgcmV0dXJuIChcbiAgICBkb2N1bWVudCAmJlxuICAgIGRvY3VtZW50LmRvY3VtZW50RWxlbWVudCAmJlxuICAgIGRvY3VtZW50LmRvY3VtZW50RWxlbWVudC5jbGFzc0xpc3QuY29udGFpbnMoQ0xBU1NfUEFHSU5BVEVEKVxuICApO1xufVxuXG5mdW5jdGlvbiBnZXRTY3JvbGxpbmdFbGVtZW50KGRvY3VtZW50KSB7XG4gIGlmIChkb2N1bWVudC5zY3JvbGxpbmdFbGVtZW50KSB7XG4gICAgcmV0dXJuIGRvY3VtZW50LnNjcm9sbGluZ0VsZW1lbnQ7XG4gIH1cbiAgcmV0dXJuIGRvY3VtZW50LmJvZHk7XG59XG5cbmZ1bmN0aW9uIGVuc3VyZUNvbnRhaW5lcih3aW4sIGFubm90YXRpb25GbGFnKSB7XG4gIGNvbnN0IGRvY3VtZW50ID0gd2luLmRvY3VtZW50O1xuXG4gIGlmICghX2hpZ2hsaWdodHNDb250YWluZXIpIHtcbiAgICBpZiAoIWJvZHlFdmVudExpc3RlbmVyc1NldCkge1xuICAgICAgYm9keUV2ZW50TGlzdGVuZXJzU2V0ID0gdHJ1ZTtcbiAgICAgIGRvY3VtZW50LmJvZHkuYWRkRXZlbnRMaXN0ZW5lcihcbiAgICAgICAgXCJtb3VzZWRvd25cIixcbiAgICAgICAgKGV2KSA9PiB7XG4gICAgICAgICAgbGFzdE1vdXNlRG93blggPSBldi5jbGllbnRYO1xuICAgICAgICAgIGxhc3RNb3VzZURvd25ZID0gZXYuY2xpZW50WTtcbiAgICAgICAgfSxcbiAgICAgICAgZmFsc2VcbiAgICAgICk7XG4gICAgICBkb2N1bWVudC5ib2R5LmFkZEV2ZW50TGlzdGVuZXIoXG4gICAgICAgIFwibW91c2V1cFwiLFxuICAgICAgICAoZXYpID0+IHtcbiAgICAgICAgICBpZiAoXG4gICAgICAgICAgICBNYXRoLmFicyhsYXN0TW91c2VEb3duWCAtIGV2LmNsaWVudFgpIDwgMyAmJlxuICAgICAgICAgICAgTWF0aC5hYnMobGFzdE1vdXNlRG93blkgLSBldi5jbGllbnRZKSA8IDNcbiAgICAgICAgICApIHtcbiAgICAgICAgICAgIHByb2Nlc3NNb3VzZUV2ZW50KHdpbiwgZXYpO1xuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAgZmFsc2VcbiAgICAgICk7XG4gICAgICBkb2N1bWVudC5ib2R5LmFkZEV2ZW50TGlzdGVuZXIoXG4gICAgICAgIFwibW91c2Vtb3ZlXCIsXG4gICAgICAgIChldikgPT4ge1xuICAgICAgICAgIHByb2Nlc3NNb3VzZUV2ZW50KHdpbiwgZXYpO1xuICAgICAgICB9LFxuICAgICAgICBmYWxzZVxuICAgICAgKTtcblxuICAgICAgZG9jdW1lbnQuYm9keS5hZGRFdmVudExpc3RlbmVyKFxuICAgICAgICBcInRvdWNoZW5kXCIsXG4gICAgICAgIGZ1bmN0aW9uIHRvdWNoRW5kKGUpIHtcbiAgICAgICAgICBwcm9jZXNzVG91Y2hFdmVudCh3aW4sIGUpO1xuICAgICAgICB9LFxuICAgICAgICBmYWxzZVxuICAgICAgKTtcbiAgICB9XG4gICAgX2hpZ2hsaWdodHNDb250YWluZXIgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KFwiZGl2XCIpO1xuICAgIF9oaWdobGlnaHRzQ29udGFpbmVyLnNldEF0dHJpYnV0ZShcImlkXCIsIElEX0hJR0hMSUdIVFNfQ09OVEFJTkVSKTtcblxuICAgIF9oaWdobGlnaHRzQ29udGFpbmVyLnN0eWxlLnNldFByb3BlcnR5KFwicG9pbnRlci1ldmVudHNcIiwgXCJub25lXCIpO1xuICAgIGRvY3VtZW50LmJvZHkuYXBwZW5kKF9oaWdobGlnaHRzQ29udGFpbmVyKTtcbiAgfVxuXG4gIHJldHVybiBfaGlnaGxpZ2h0c0NvbnRhaW5lcjtcbn1cblxuZnVuY3Rpb24gaGlkZUFsbGhpZ2hsaWdodHMoKSB7XG4gIGlmIChfaGlnaGxpZ2h0c0NvbnRhaW5lcikge1xuICAgIF9oaWdobGlnaHRzQ29udGFpbmVyLnJlbW92ZSgpO1xuICAgIF9oaWdobGlnaHRzQ29udGFpbmVyID0gbnVsbDtcbiAgfVxufVxuXG5mdW5jdGlvbiBkZXN0cm95QWxsaGlnaGxpZ2h0cygpIHtcbiAgaGlkZUFsbGhpZ2hsaWdodHMoKTtcbiAgX2hpZ2hsaWdodHMuc3BsaWNlKDAsIF9oaWdobGlnaHRzLmxlbmd0aCk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBkZXN0cm95SGlnaGxpZ2h0KGlkKSB7XG4gIGxldCBpID0gLTE7XG4gIGxldCBfZG9jdW1lbnQgPSB3aW5kb3cuZG9jdW1lbnQ7XG4gIGNvbnN0IGhpZ2hsaWdodCA9IF9oaWdobGlnaHRzLmZpbmQoKGgsIGopID0+IHtcbiAgICBpID0gajtcbiAgICByZXR1cm4gaC5pZCA9PT0gaWQ7XG4gIH0pO1xuICBpZiAoaGlnaGxpZ2h0ICYmIGkgPj0gMCAmJiBpIDwgX2hpZ2hsaWdodHMubGVuZ3RoKSB7XG4gICAgX2hpZ2hsaWdodHMuc3BsaWNlKGksIDEpO1xuICB9XG4gIGNvbnN0IGhpZ2hsaWdodENvbnRhaW5lciA9IF9kb2N1bWVudC5nZXRFbGVtZW50QnlJZChpZCk7XG4gIGlmIChoaWdobGlnaHRDb250YWluZXIpIHtcbiAgICBoaWdobGlnaHRDb250YWluZXIucmVtb3ZlKCk7XG4gIH1cbn1cblxuZnVuY3Rpb24gaXNDZmlUZXh0Tm9kZShub2RlKSB7XG4gIHJldHVybiBub2RlLm5vZGVUeXBlICE9PSBOb2RlLkVMRU1FTlRfTk9ERTtcbn1cblxuZnVuY3Rpb24gZ2V0Q2hpbGRUZXh0Tm9kZUNmaUluZGV4KGVsZW1lbnQsIGNoaWxkKSB7XG4gIGxldCBmb3VuZCA9IC0xO1xuICBsZXQgdGV4dE5vZGVJbmRleCA9IC0xO1xuICBsZXQgcHJldmlvdXNXYXNFbGVtZW50ID0gZmFsc2U7XG4gIGZvciAobGV0IGkgPSAwOyBpIDwgZWxlbWVudC5jaGlsZE5vZGVzLmxlbmd0aDsgaSsrKSB7XG4gICAgY29uc3QgY2hpbGROb2RlID0gZWxlbWVudC5jaGlsZE5vZGVzW2ldO1xuICAgIGNvbnN0IGlzVGV4dCA9IGlzQ2ZpVGV4dE5vZGUoY2hpbGROb2RlKTtcbiAgICBpZiAoaXNUZXh0IHx8IHByZXZpb3VzV2FzRWxlbWVudCkge1xuICAgICAgdGV4dE5vZGVJbmRleCArPSAyO1xuICAgIH1cbiAgICBpZiAoaXNUZXh0KSB7XG4gICAgICBpZiAoY2hpbGROb2RlID09PSBjaGlsZCkge1xuICAgICAgICBmb3VuZCA9IHRleHROb2RlSW5kZXg7XG4gICAgICAgIGJyZWFrO1xuICAgICAgfVxuICAgIH1cbiAgICBwcmV2aW91c1dhc0VsZW1lbnQgPSBjaGlsZE5vZGUubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFO1xuICB9XG4gIHJldHVybiBmb3VuZDtcbn1cblxuZnVuY3Rpb24gZ2V0Q29tbW9uQW5jZXN0b3JFbGVtZW50KG5vZGUxLCBub2RlMikge1xuICBpZiAobm9kZTEubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFICYmIG5vZGUxID09PSBub2RlMikge1xuICAgIHJldHVybiBub2RlMTtcbiAgfVxuICBpZiAobm9kZTEubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFICYmIG5vZGUxLmNvbnRhaW5zKG5vZGUyKSkge1xuICAgIHJldHVybiBub2RlMTtcbiAgfVxuICBpZiAobm9kZTIubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFICYmIG5vZGUyLmNvbnRhaW5zKG5vZGUxKSkge1xuICAgIHJldHVybiBub2RlMjtcbiAgfVxuICBjb25zdCBub2RlMUVsZW1lbnRBbmNlc3RvckNoYWluID0gW107XG4gIGxldCBwYXJlbnQgPSBub2RlMS5wYXJlbnROb2RlO1xuICB3aGlsZSAocGFyZW50ICYmIHBhcmVudC5ub2RlVHlwZSA9PT0gTm9kZS5FTEVNRU5UX05PREUpIHtcbiAgICBub2RlMUVsZW1lbnRBbmNlc3RvckNoYWluLnB1c2gocGFyZW50KTtcbiAgICBwYXJlbnQgPSBwYXJlbnQucGFyZW50Tm9kZTtcbiAgfVxuICBjb25zdCBub2RlMkVsZW1lbnRBbmNlc3RvckNoYWluID0gW107XG4gIHBhcmVudCA9IG5vZGUyLnBhcmVudE5vZGU7XG4gIHdoaWxlIChwYXJlbnQgJiYgcGFyZW50Lm5vZGVUeXBlID09PSBOb2RlLkVMRU1FTlRfTk9ERSkge1xuICAgIG5vZGUyRWxlbWVudEFuY2VzdG9yQ2hhaW4ucHVzaChwYXJlbnQpO1xuICAgIHBhcmVudCA9IHBhcmVudC5wYXJlbnROb2RlO1xuICB9XG4gIGxldCBjb21tb25BbmNlc3RvciA9IG5vZGUxRWxlbWVudEFuY2VzdG9yQ2hhaW4uZmluZChcbiAgICAobm9kZTFFbGVtZW50QW5jZXN0b3IpID0+IHtcbiAgICAgIHJldHVybiBub2RlMkVsZW1lbnRBbmNlc3RvckNoYWluLmluZGV4T2Yobm9kZTFFbGVtZW50QW5jZXN0b3IpID49IDA7XG4gICAgfVxuICApO1xuICBpZiAoIWNvbW1vbkFuY2VzdG9yKSB7XG4gICAgY29tbW9uQW5jZXN0b3IgPSBub2RlMkVsZW1lbnRBbmNlc3RvckNoYWluLmZpbmQoKG5vZGUyRWxlbWVudEFuY2VzdG9yKSA9PiB7XG4gICAgICByZXR1cm4gbm9kZTFFbGVtZW50QW5jZXN0b3JDaGFpbi5pbmRleE9mKG5vZGUyRWxlbWVudEFuY2VzdG9yKSA+PSAwO1xuICAgIH0pO1xuICB9XG4gIHJldHVybiBjb21tb25BbmNlc3Rvcjtcbn1cblxuZnVuY3Rpb24gZnVsbFF1YWxpZmllZFNlbGVjdG9yKG5vZGUpIHtcbiAgaWYgKG5vZGUubm9kZVR5cGUgIT09IE5vZGUuRUxFTUVOVF9OT0RFKSB7XG4gICAgY29uc3QgbG93ZXJDYXNlTmFtZSA9XG4gICAgICAobm9kZS5sb2NhbE5hbWUgJiYgbm9kZS5sb2NhbE5hbWUudG9Mb3dlckNhc2UoKSkgfHxcbiAgICAgIG5vZGUubm9kZU5hbWUudG9Mb3dlckNhc2UoKTtcbiAgICByZXR1cm4gbG93ZXJDYXNlTmFtZTtcbiAgfVxuICAvL3JldHVybiBjc3NQYXRoKG5vZGUsIGp1c3RTZWxlY3Rvcik7XG4gIHJldHVybiBjc3NQYXRoKG5vZGUsIHRydWUpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gZ2V0Q3VycmVudFNlbGVjdGlvbkluZm8oKSB7XG4gIGNvbnN0IHNlbGVjdGlvbiA9IHdpbmRvdy5nZXRTZWxlY3Rpb24oKTtcbiAgaWYgKCFzZWxlY3Rpb24pIHtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG4gIGlmIChzZWxlY3Rpb24uaXNDb2xsYXBzZWQpIHtcbiAgICBjb25zb2xlLmxvZyhcIl5eXiBTRUxFQ1RJT04gQ09MTEFQU0VELlwiKTtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG4gIGNvbnN0IHJhd1RleHQgPSBzZWxlY3Rpb24udG9TdHJpbmcoKTtcbiAgY29uc3QgY2xlYW5UZXh0ID0gcmF3VGV4dC50cmltKCkucmVwbGFjZSgvXFxuL2csIFwiIFwiKS5yZXBsYWNlKC9cXHNcXHMrL2csIFwiIFwiKTtcbiAgaWYgKGNsZWFuVGV4dC5sZW5ndGggPT09IDApIHtcbiAgICBjb25zb2xlLmxvZyhcIl5eXiBTRUxFQ1RJT04gVEVYVCBFTVBUWS5cIik7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuICBpZiAoIXNlbGVjdGlvbi5hbmNob3JOb2RlIHx8ICFzZWxlY3Rpb24uZm9jdXNOb2RlKSB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuICBjb25zdCByYW5nZSA9XG4gICAgc2VsZWN0aW9uLnJhbmdlQ291bnQgPT09IDFcbiAgICAgID8gc2VsZWN0aW9uLmdldFJhbmdlQXQoMClcbiAgICAgIDogY3JlYXRlT3JkZXJlZFJhbmdlKFxuICAgICAgICAgIHNlbGVjdGlvbi5hbmNob3JOb2RlLFxuICAgICAgICAgIHNlbGVjdGlvbi5hbmNob3JPZmZzZXQsXG4gICAgICAgICAgc2VsZWN0aW9uLmZvY3VzTm9kZSxcbiAgICAgICAgICBzZWxlY3Rpb24uZm9jdXNPZmZzZXRcbiAgICAgICAgKTtcbiAgaWYgKCFyYW5nZSB8fCByYW5nZS5jb2xsYXBzZWQpIHtcbiAgICBjb25zb2xlLmxvZyhcIiQkJCQkJCQkJCQkJCQkJCQkIENBTk5PVCBHRVQgTk9OLUNPTExBUFNFRCBTRUxFQ1RJT04gUkFOR0U/IVwiKTtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG4gIGNvbnN0IHJhbmdlSW5mbyA9IGNvbnZlcnRSYW5nZShyYW5nZSwgZnVsbFF1YWxpZmllZFNlbGVjdG9yLCBjb21wdXRlQ0ZJKTtcbiAgaWYgKCFyYW5nZUluZm8pIHtcbiAgICBjb25zb2xlLmxvZyhcIl5eXiBTRUxFQ1RJT04gUkFOR0UgSU5GTyBGQUlMPyFcIik7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuXG4gIGlmIChJU19ERVYgJiYgREVCVUdfVklTVUFMUykge1xuICAgIGNvbnN0IHJlc3RvcmVkUmFuZ2UgPSBjb252ZXJ0UmFuZ2VJbmZvKHdpbi5kb2N1bWVudCwgcmFuZ2VJbmZvKTtcbiAgICBpZiAocmVzdG9yZWRSYW5nZSkge1xuICAgICAgaWYgKFxuICAgICAgICByZXN0b3JlZFJhbmdlLnN0YXJ0T2Zmc2V0ID09PSByYW5nZS5zdGFydE9mZnNldCAmJlxuICAgICAgICByZXN0b3JlZFJhbmdlLmVuZE9mZnNldCA9PT0gcmFuZ2UuZW5kT2Zmc2V0ICYmXG4gICAgICAgIHJlc3RvcmVkUmFuZ2Uuc3RhcnRDb250YWluZXIgPT09IHJhbmdlLnN0YXJ0Q29udGFpbmVyICYmXG4gICAgICAgIHJlc3RvcmVkUmFuZ2UuZW5kQ29udGFpbmVyID09PSByYW5nZS5lbmRDb250YWluZXJcbiAgICAgICkge1xuICAgICAgICBjb25zb2xlLmxvZyhcIlNFTEVDVElPTiBSQU5HRSBSRVNUT1JFRCBPS0FZIChkZXYgY2hlY2spLlwiKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGNvbnNvbGUubG9nKFwiU0VMRUNUSU9OIFJBTkdFIFJFU1RPUkUgRkFJTCAoZGV2IGNoZWNrKS5cIik7XG4gICAgICAgIGR1bXBEZWJ1ZyhcbiAgICAgICAgICBcIlNFTEVDVElPTlwiLFxuICAgICAgICAgIHNlbGVjdGlvbi5hbmNob3JOb2RlLFxuICAgICAgICAgIHNlbGVjdGlvbi5hbmNob3JPZmZzZXQsXG4gICAgICAgICAgc2VsZWN0aW9uLmZvY3VzTm9kZSxcbiAgICAgICAgICBzZWxlY3Rpb24uZm9jdXNPZmZzZXQsXG4gICAgICAgICAgZ2V0Q3NzU2VsZWN0b3JcbiAgICAgICAgKTtcbiAgICAgICAgZHVtcERlYnVnKFxuICAgICAgICAgIFwiT1JERVJFRCBSQU5HRSBGUk9NIFNFTEVDVElPTlwiLFxuICAgICAgICAgIHJhbmdlLnN0YXJ0Q29udGFpbmVyLFxuICAgICAgICAgIHJhbmdlLnN0YXJ0T2Zmc2V0LFxuICAgICAgICAgIHJhbmdlLmVuZENvbnRhaW5lcixcbiAgICAgICAgICByYW5nZS5lbmRPZmZzZXQsXG4gICAgICAgICAgZ2V0Q3NzU2VsZWN0b3JcbiAgICAgICAgKTtcbiAgICAgICAgZHVtcERlYnVnKFxuICAgICAgICAgIFwiUkVTVE9SRUQgUkFOR0VcIixcbiAgICAgICAgICByZXN0b3JlZFJhbmdlLnN0YXJ0Q29udGFpbmVyLFxuICAgICAgICAgIHJlc3RvcmVkUmFuZ2Uuc3RhcnRPZmZzZXQsXG4gICAgICAgICAgcmVzdG9yZWRSYW5nZS5lbmRDb250YWluZXIsXG4gICAgICAgICAgcmVzdG9yZWRSYW5nZS5lbmRPZmZzZXQsXG4gICAgICAgICAgZ2V0Q3NzU2VsZWN0b3JcbiAgICAgICAgKTtcbiAgICAgIH1cbiAgICB9IGVsc2Uge1xuICAgICAgY29uc29sZS5sb2coXCJDQU5OT1QgUkVTVE9SRSBTRUxFQ1RJT04gUkFOR0UgPz8hXCIpO1xuICAgIH1cbiAgfSBlbHNlIHtcbiAgfVxuXG4gIHJldHVybiB7XG4gICAgbG9jYXRpb25zOiByYW5nZUluZm8yTG9jYXRpb24ocmFuZ2VJbmZvKSxcbiAgICB0ZXh0OiB7XG4gICAgICBoaWdobGlnaHQ6IHJhd1RleHQsXG4gICAgfSxcbiAgfTtcbn1cblxuZnVuY3Rpb24gY2hlY2tCbGFja2xpc3RlZChlbCkge1xuICBsZXQgYmxhY2tsaXN0ZWRJZDtcbiAgY29uc3QgaWQgPSBlbC5nZXRBdHRyaWJ1dGUoXCJpZFwiKTtcbiAgaWYgKGlkICYmIF9ibGFja2xpc3RJZENsYXNzRm9yQ0ZJLmluZGV4T2YoaWQpID49IDApIHtcbiAgICBjb25zb2xlLmxvZyhcImNoZWNrQmxhY2tsaXN0ZWQgSUQ6IFwiICsgaWQpO1xuICAgIGJsYWNrbGlzdGVkSWQgPSBpZDtcbiAgfVxuICBsZXQgYmxhY2tsaXN0ZWRDbGFzcztcbiAgZm9yIChjb25zdCBpdGVtIG9mIF9ibGFja2xpc3RJZENsYXNzRm9yQ0ZJKSB7XG4gICAgaWYgKGVsLmNsYXNzTGlzdC5jb250YWlucyhpdGVtKSkge1xuICAgICAgY29uc29sZS5sb2coXCJjaGVja0JsYWNrbGlzdGVkIENMQVNTOiBcIiArIGl0ZW0pO1xuICAgICAgYmxhY2tsaXN0ZWRDbGFzcyA9IGl0ZW07XG4gICAgICBicmVhaztcbiAgICB9XG4gIH1cbiAgaWYgKGJsYWNrbGlzdGVkSWQgfHwgYmxhY2tsaXN0ZWRDbGFzcykge1xuICAgIHJldHVybiB0cnVlO1xuICB9XG5cbiAgcmV0dXJuIGZhbHNlO1xufVxuXG5mdW5jdGlvbiBjc3NQYXRoKG5vZGUsIG9wdGltaXplZCkge1xuICBpZiAobm9kZS5ub2RlVHlwZSAhPT0gTm9kZS5FTEVNRU5UX05PREUpIHtcbiAgICByZXR1cm4gXCJcIjtcbiAgfVxuXG4gIGNvbnN0IHN0ZXBzID0gW107XG4gIGxldCBjb250ZXh0Tm9kZSA9IG5vZGU7XG4gIHdoaWxlIChjb250ZXh0Tm9kZSkge1xuICAgIGNvbnN0IHN0ZXAgPSBfY3NzUGF0aFN0ZXAoY29udGV4dE5vZGUsICEhb3B0aW1pemVkLCBjb250ZXh0Tm9kZSA9PT0gbm9kZSk7XG4gICAgaWYgKCFzdGVwKSB7XG4gICAgICBicmVhazsgLy8gRXJyb3IgLSBiYWlsIG91dCBlYXJseS5cbiAgICB9XG4gICAgc3RlcHMucHVzaChzdGVwLnZhbHVlKTtcbiAgICBpZiAoc3RlcC5vcHRpbWl6ZWQpIHtcbiAgICAgIGJyZWFrO1xuICAgIH1cbiAgICBjb250ZXh0Tm9kZSA9IGNvbnRleHROb2RlLnBhcmVudE5vZGU7XG4gIH1cbiAgc3RlcHMucmV2ZXJzZSgpO1xuICByZXR1cm4gc3RlcHMuam9pbihcIiA+IFwiKTtcbn1cbi8vIHRzbGludDpkaXNhYmxlLW5leHQtbGluZTptYXgtbGluZS1sZW5ndGhcbi8vIGh0dHBzOi8vY2hyb21pdW0uZ29vZ2xlc291cmNlLmNvbS9jaHJvbWl1bS9ibGluay8rL21hc3Rlci9Tb3VyY2UvZGV2dG9vbHMvZnJvbnRfZW5kL2NvbXBvbmVudHMvRE9NUHJlc2VudGF0aW9uVXRpbHMuanMjMzE2XG5mdW5jdGlvbiBfY3NzUGF0aFN0ZXAobm9kZSwgb3B0aW1pemVkLCBpc1RhcmdldE5vZGUpIHtcbiAgZnVuY3Rpb24gcHJlZml4ZWRFbGVtZW50Q2xhc3NOYW1lcyhuZCkge1xuICAgIGNvbnN0IGNsYXNzQXR0cmlidXRlID0gbmQuZ2V0QXR0cmlidXRlKFwiY2xhc3NcIik7XG4gICAgaWYgKCFjbGFzc0F0dHJpYnV0ZSkge1xuICAgICAgcmV0dXJuIFtdO1xuICAgIH1cblxuICAgIHJldHVybiBjbGFzc0F0dHJpYnV0ZVxuICAgICAgLnNwbGl0KC9cXHMrL2cpXG4gICAgICAuZmlsdGVyKEJvb2xlYW4pXG4gICAgICAubWFwKChubSkgPT4ge1xuICAgICAgICAvLyBUaGUgcHJlZml4IGlzIHJlcXVpcmVkIHRvIHN0b3JlIFwiX19wcm90b19fXCIgaW4gYSBvYmplY3QtYmFzZWQgbWFwLlxuICAgICAgICByZXR1cm4gXCIkXCIgKyBubTtcbiAgICAgIH0pO1xuICB9XG5cbiAgZnVuY3Rpb24gaWRTZWxlY3RvcihpZGQpIHtcbiAgICByZXR1cm4gXCIjXCIgKyBlc2NhcGVJZGVudGlmaWVySWZOZWVkZWQoaWRkKTtcbiAgfVxuXG4gIGZ1bmN0aW9uIGVzY2FwZUlkZW50aWZpZXJJZk5lZWRlZChpZGVudCkge1xuICAgIGlmIChpc0NTU0lkZW50aWZpZXIoaWRlbnQpKSB7XG4gICAgICByZXR1cm4gaWRlbnQ7XG4gICAgfVxuXG4gICAgY29uc3Qgc2hvdWxkRXNjYXBlRmlyc3QgPSAvXig/OlswLTldfC1bMC05LV0/KS8udGVzdChpZGVudCk7XG4gICAgY29uc3QgbGFzdEluZGV4ID0gaWRlbnQubGVuZ3RoIC0gMTtcbiAgICByZXR1cm4gaWRlbnQucmVwbGFjZSgvLi9nLCBmdW5jdGlvbiAoYywgaWkpIHtcbiAgICAgIHJldHVybiAoc2hvdWxkRXNjYXBlRmlyc3QgJiYgaWkgPT09IDApIHx8ICFpc0NTU0lkZW50Q2hhcihjKVxuICAgICAgICA/IGVzY2FwZUFzY2lpQ2hhcihjLCBpaSA9PT0gbGFzdEluZGV4KVxuICAgICAgICA6IGM7XG4gICAgfSk7XG4gIH1cblxuICBmdW5jdGlvbiBlc2NhcGVBc2NpaUNoYXIoYywgaXNMYXN0KSB7XG4gICAgcmV0dXJuIFwiXFxcXFwiICsgdG9IZXhCeXRlKGMpICsgKGlzTGFzdCA/IFwiXCIgOiBcIiBcIik7XG4gIH1cblxuICBmdW5jdGlvbiB0b0hleEJ5dGUoYykge1xuICAgIGxldCBoZXhCeXRlID0gYy5jaGFyQ29kZUF0KDApLnRvU3RyaW5nKDE2KTtcbiAgICBpZiAoaGV4Qnl0ZS5sZW5ndGggPT09IDEpIHtcbiAgICAgIGhleEJ5dGUgPSBcIjBcIiArIGhleEJ5dGU7XG4gICAgfVxuICAgIHJldHVybiBoZXhCeXRlO1xuICB9XG5cbiAgZnVuY3Rpb24gaXNDU1NJZGVudENoYXIoYykge1xuICAgIGlmICgvW2EtekEtWjAtOV8tXS8udGVzdChjKSkge1xuICAgICAgcmV0dXJuIHRydWU7XG4gICAgfVxuICAgIHJldHVybiBjLmNoYXJDb2RlQXQoMCkgPj0gMHhhMDtcbiAgfVxuXG4gIGZ1bmN0aW9uIGlzQ1NTSWRlbnRpZmllcih2YWx1ZSkge1xuICAgIHJldHVybiAvXi0/W2EtekEtWl9dW2EtekEtWjAtOV8tXSokLy50ZXN0KHZhbHVlKTtcbiAgfVxuXG4gIGlmIChub2RlLm5vZGVUeXBlICE9PSBOb2RlLkVMRU1FTlRfTk9ERSkge1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgY29uc3QgbG93ZXJDYXNlTmFtZSA9XG4gICAgKG5vZGUubG9jYWxOYW1lICYmIG5vZGUubG9jYWxOYW1lLnRvTG93ZXJDYXNlKCkpIHx8XG4gICAgbm9kZS5ub2RlTmFtZS50b0xvd2VyQ2FzZSgpO1xuXG4gIGNvbnN0IGVsZW1lbnQgPSBub2RlO1xuXG4gIGNvbnN0IGlkID0gZWxlbWVudC5nZXRBdHRyaWJ1dGUoXCJpZFwiKTtcblxuICBpZiAob3B0aW1pemVkKSB7XG4gICAgaWYgKGlkKSB7XG4gICAgICByZXR1cm4ge1xuICAgICAgICBvcHRpbWl6ZWQ6IHRydWUsXG4gICAgICAgIHZhbHVlOiBpZFNlbGVjdG9yKGlkKSxcbiAgICAgIH07XG4gICAgfVxuICAgIGlmIChcbiAgICAgIGxvd2VyQ2FzZU5hbWUgPT09IFwiYm9keVwiIHx8XG4gICAgICBsb3dlckNhc2VOYW1lID09PSBcImhlYWRcIiB8fFxuICAgICAgbG93ZXJDYXNlTmFtZSA9PT0gXCJodG1sXCJcbiAgICApIHtcbiAgICAgIHJldHVybiB7XG4gICAgICAgIG9wdGltaXplZDogdHJ1ZSxcbiAgICAgICAgdmFsdWU6IGxvd2VyQ2FzZU5hbWUsIC8vIG5vZGUubm9kZU5hbWVJbkNvcnJlY3RDYXNlKCksXG4gICAgICB9O1xuICAgIH1cbiAgfVxuXG4gIGNvbnN0IG5vZGVOYW1lID0gbG93ZXJDYXNlTmFtZTsgLy8gbm9kZS5ub2RlTmFtZUluQ29ycmVjdENhc2UoKTtcbiAgaWYgKGlkKSB7XG4gICAgcmV0dXJuIHtcbiAgICAgIG9wdGltaXplZDogdHJ1ZSxcbiAgICAgIHZhbHVlOiBub2RlTmFtZSArIGlkU2VsZWN0b3IoaWQpLFxuICAgIH07XG4gIH1cblxuICBjb25zdCBwYXJlbnQgPSBub2RlLnBhcmVudE5vZGU7XG5cbiAgaWYgKCFwYXJlbnQgfHwgcGFyZW50Lm5vZGVUeXBlID09PSBOb2RlLkRPQ1VNRU5UX05PREUpIHtcbiAgICByZXR1cm4ge1xuICAgICAgb3B0aW1pemVkOiB0cnVlLFxuICAgICAgdmFsdWU6IG5vZGVOYW1lLFxuICAgIH07XG4gIH1cblxuICBjb25zdCBwcmVmaXhlZE93bkNsYXNzTmFtZXNBcnJheV8gPSBwcmVmaXhlZEVsZW1lbnRDbGFzc05hbWVzKGVsZW1lbnQpO1xuXG4gIGNvbnN0IHByZWZpeGVkT3duQ2xhc3NOYW1lc0FycmF5ID0gW107IC8vIC5rZXlTZXQoKVxuICBwcmVmaXhlZE93bkNsYXNzTmFtZXNBcnJheV8uZm9yRWFjaCgoYXJySXRlbSkgPT4ge1xuICAgIGlmIChwcmVmaXhlZE93bkNsYXNzTmFtZXNBcnJheS5pbmRleE9mKGFyckl0ZW0pIDwgMCkge1xuICAgICAgcHJlZml4ZWRPd25DbGFzc05hbWVzQXJyYXkucHVzaChhcnJJdGVtKTtcbiAgICB9XG4gIH0pO1xuXG4gIGxldCBuZWVkc0NsYXNzTmFtZXMgPSBmYWxzZTtcbiAgbGV0IG5lZWRzTnRoQ2hpbGQgPSBmYWxzZTtcbiAgbGV0IG93bkluZGV4ID0gLTE7XG4gIGxldCBlbGVtZW50SW5kZXggPSAtMTtcbiAgY29uc3Qgc2libGluZ3MgPSBwYXJlbnQuY2hpbGRyZW47XG5cbiAgZm9yIChcbiAgICBsZXQgaSA9IDA7XG4gICAgKG93bkluZGV4ID09PSAtMSB8fCAhbmVlZHNOdGhDaGlsZCkgJiYgaSA8IHNpYmxpbmdzLmxlbmd0aDtcbiAgICArK2lcbiAgKSB7XG4gICAgY29uc3Qgc2libGluZyA9IHNpYmxpbmdzW2ldO1xuICAgIGlmIChzaWJsaW5nLm5vZGVUeXBlICE9PSBOb2RlLkVMRU1FTlRfTk9ERSkge1xuICAgICAgY29udGludWU7XG4gICAgfVxuICAgIGVsZW1lbnRJbmRleCArPSAxO1xuICAgIGlmIChzaWJsaW5nID09PSBub2RlKSB7XG4gICAgICBvd25JbmRleCA9IGVsZW1lbnRJbmRleDtcbiAgICAgIGNvbnRpbnVlO1xuICAgIH1cbiAgICBpZiAobmVlZHNOdGhDaGlsZCkge1xuICAgICAgY29udGludWU7XG4gICAgfVxuXG4gICAgLy8gc2libGluZy5ub2RlTmFtZUluQ29ycmVjdENhc2UoKVxuICAgIGNvbnN0IHNpYmxpbmdOYW1lID1cbiAgICAgIChzaWJsaW5nLmxvY2FsTmFtZSAmJiBzaWJsaW5nLmxvY2FsTmFtZS50b0xvd2VyQ2FzZSgpKSB8fFxuICAgICAgc2libGluZy5ub2RlTmFtZS50b0xvd2VyQ2FzZSgpO1xuICAgIGlmIChzaWJsaW5nTmFtZSAhPT0gbm9kZU5hbWUpIHtcbiAgICAgIGNvbnRpbnVlO1xuICAgIH1cbiAgICBuZWVkc0NsYXNzTmFtZXMgPSB0cnVlO1xuXG4gICAgY29uc3Qgb3duQ2xhc3NOYW1lcyA9IFtdO1xuICAgIHByZWZpeGVkT3duQ2xhc3NOYW1lc0FycmF5LmZvckVhY2goKGFyckl0ZW0pID0+IHtcbiAgICAgIG93bkNsYXNzTmFtZXMucHVzaChhcnJJdGVtKTtcbiAgICB9KTtcbiAgICBsZXQgb3duQ2xhc3NOYW1lQ291bnQgPSBvd25DbGFzc05hbWVzLmxlbmd0aDtcblxuICAgIGlmIChvd25DbGFzc05hbWVDb3VudCA9PT0gMCkge1xuICAgICAgbmVlZHNOdGhDaGlsZCA9IHRydWU7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG4gICAgY29uc3Qgc2libGluZ0NsYXNzTmFtZXNBcnJheV8gPSBwcmVmaXhlZEVsZW1lbnRDbGFzc05hbWVzKHNpYmxpbmcpO1xuICAgIGNvbnN0IHNpYmxpbmdDbGFzc05hbWVzQXJyYXkgPSBbXTsgLy8gLmtleVNldCgpXG4gICAgc2libGluZ0NsYXNzTmFtZXNBcnJheV8uZm9yRWFjaCgoYXJySXRlbSkgPT4ge1xuICAgICAgaWYgKHNpYmxpbmdDbGFzc05hbWVzQXJyYXkuaW5kZXhPZihhcnJJdGVtKSA8IDApIHtcbiAgICAgICAgc2libGluZ0NsYXNzTmFtZXNBcnJheS5wdXNoKGFyckl0ZW0pO1xuICAgICAgfVxuICAgIH0pO1xuXG4gICAgZm9yIChjb25zdCBzaWJsaW5nQ2xhc3Mgb2Ygc2libGluZ0NsYXNzTmFtZXNBcnJheSkge1xuICAgICAgY29uc3QgaW5kID0gb3duQ2xhc3NOYW1lcy5pbmRleE9mKHNpYmxpbmdDbGFzcyk7XG4gICAgICBpZiAoaW5kIDwgMCkge1xuICAgICAgICBjb250aW51ZTtcbiAgICAgIH1cblxuICAgICAgb3duQ2xhc3NOYW1lcy5zcGxpY2UoaW5kLCAxKTsgLy8gZGVsZXRlIG93bkNsYXNzTmFtZXNbc2libGluZ0NsYXNzXTtcblxuICAgICAgaWYgKCEtLW93bkNsYXNzTmFtZUNvdW50KSB7XG4gICAgICAgIG5lZWRzTnRoQ2hpbGQgPSB0cnVlO1xuICAgICAgICBicmVhaztcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICBsZXQgcmVzdWx0ID0gbm9kZU5hbWU7XG4gIGlmIChcbiAgICBpc1RhcmdldE5vZGUgJiZcbiAgICBub2RlTmFtZSA9PT0gXCJpbnB1dFwiICYmXG4gICAgZWxlbWVudC5nZXRBdHRyaWJ1dGUoXCJ0eXBlXCIpICYmXG4gICAgIWVsZW1lbnQuZ2V0QXR0cmlidXRlKFwiaWRcIikgJiZcbiAgICAhZWxlbWVudC5nZXRBdHRyaWJ1dGUoXCJjbGFzc1wiKVxuICApIHtcbiAgICByZXN1bHQgKz0gJ1t0eXBlPVwiJyArIGVsZW1lbnQuZ2V0QXR0cmlidXRlKFwidHlwZVwiKSArICdcIl0nO1xuICB9XG4gIGlmIChuZWVkc050aENoaWxkKSB7XG4gICAgcmVzdWx0ICs9IFwiOm50aC1jaGlsZChcIiArIChvd25JbmRleCArIDEpICsgXCIpXCI7XG4gIH0gZWxzZSBpZiAobmVlZHNDbGFzc05hbWVzKSB7XG4gICAgZm9yIChjb25zdCBwcmVmaXhlZE5hbWUgb2YgcHJlZml4ZWRPd25DbGFzc05hbWVzQXJyYXkpIHtcbiAgICAgIHJlc3VsdCArPSBcIi5cIiArIGVzY2FwZUlkZW50aWZpZXJJZk5lZWRlZChwcmVmaXhlZE5hbWUuc3Vic3RyKDEpKTtcbiAgICB9XG4gIH1cblxuICByZXR1cm4ge1xuICAgIG9wdGltaXplZDogZmFsc2UsXG4gICAgdmFsdWU6IHJlc3VsdCxcbiAgfTtcbn1cblxuZnVuY3Rpb24gY29tcHV0ZUNGSShub2RlKSB7XG4gIC8vIFRPRE86IGhhbmRsZSBjaGFyYWN0ZXIgcG9zaXRpb24gaW5zaWRlIHRleHQgbm9kZVxuICBpZiAobm9kZS5ub2RlVHlwZSAhPT0gTm9kZS5FTEVNRU5UX05PREUpIHtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG5cbiAgbGV0IGNmaSA9IFwiXCI7XG5cbiAgbGV0IGN1cnJlbnRFbGVtZW50ID0gbm9kZTtcbiAgd2hpbGUgKFxuICAgIGN1cnJlbnRFbGVtZW50LnBhcmVudE5vZGUgJiZcbiAgICBjdXJyZW50RWxlbWVudC5wYXJlbnROb2RlLm5vZGVUeXBlID09PSBOb2RlLkVMRU1FTlRfTk9ERVxuICApIHtcbiAgICBjb25zdCBibGFja2xpc3RlZCA9IGNoZWNrQmxhY2tsaXN0ZWQoY3VycmVudEVsZW1lbnQpO1xuICAgIGlmICghYmxhY2tsaXN0ZWQpIHtcbiAgICAgIGNvbnN0IGN1cnJlbnRFbGVtZW50UGFyZW50Q2hpbGRyZW4gPSBjdXJyZW50RWxlbWVudC5wYXJlbnROb2RlLmNoaWxkcmVuO1xuICAgICAgbGV0IGN1cnJlbnRFbGVtZW50SW5kZXggPSAtMTtcbiAgICAgIGZvciAobGV0IGkgPSAwOyBpIDwgY3VycmVudEVsZW1lbnRQYXJlbnRDaGlsZHJlbi5sZW5ndGg7IGkrKykge1xuICAgICAgICBpZiAoY3VycmVudEVsZW1lbnQgPT09IGN1cnJlbnRFbGVtZW50UGFyZW50Q2hpbGRyZW5baV0pIHtcbiAgICAgICAgICBjdXJyZW50RWxlbWVudEluZGV4ID0gaTtcbiAgICAgICAgICBicmVhaztcbiAgICAgICAgfVxuICAgICAgfVxuICAgICAgaWYgKGN1cnJlbnRFbGVtZW50SW5kZXggPj0gMCkge1xuICAgICAgICBjb25zdCBjZmlJbmRleCA9IChjdXJyZW50RWxlbWVudEluZGV4ICsgMSkgKiAyO1xuICAgICAgICBjZmkgPVxuICAgICAgICAgIGNmaUluZGV4ICtcbiAgICAgICAgICAoY3VycmVudEVsZW1lbnQuaWQgPyBcIltcIiArIGN1cnJlbnRFbGVtZW50LmlkICsgXCJdXCIgOiBcIlwiKSArXG4gICAgICAgICAgKGNmaS5sZW5ndGggPyBcIi9cIiArIGNmaSA6IFwiXCIpO1xuICAgICAgfVxuICAgIH1cbiAgICBjdXJyZW50RWxlbWVudCA9IGN1cnJlbnRFbGVtZW50LnBhcmVudE5vZGU7XG4gIH1cblxuICByZXR1cm4gXCIvXCIgKyBjZmk7XG59XG5cbmZ1bmN0aW9uIF9jcmVhdGVIaWdobGlnaHQobG9jYXRpb25zLCBjb2xvciwgcG9pbnRlckludGVyYWN0aW9uLCB0eXBlKSB7XG4gIGNvbnN0IHJhbmdlSW5mbyA9IGxvY2F0aW9uMlJhbmdlSW5mbyhsb2NhdGlvbnMpO1xuICBjb25zdCB1bmlxdWVTdHIgPSBgJHtyYW5nZUluZm8uY2ZpfSR7cmFuZ2VJbmZvLnN0YXJ0Q29udGFpbmVyRWxlbWVudENzc1NlbGVjdG9yfSR7cmFuZ2VJbmZvLnN0YXJ0Q29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4fSR7cmFuZ2VJbmZvLnN0YXJ0T2Zmc2V0fSR7cmFuZ2VJbmZvLmVuZENvbnRhaW5lckVsZW1lbnRDc3NTZWxlY3Rvcn0ke3JhbmdlSW5mby5lbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXh9JHtyYW5nZUluZm8uZW5kT2Zmc2V0fWA7XG5cbiAgY29uc3QgaGFzaCA9IHJlcXVpcmUoXCJoYXNoLmpzXCIpO1xuICBjb25zdCBzaGEyNTZIZXggPSBoYXNoLnNoYTI1NigpLnVwZGF0ZSh1bmlxdWVTdHIpLmRpZ2VzdChcImhleFwiKTtcblxuICB2YXIgaWQ7XG4gIGlmICh0eXBlID09IElEX0hJR0hMSUdIVFNfQ09OVEFJTkVSKSB7XG4gICAgaWQgPSBcIlIyX0hJR0hMSUdIVF9cIiArIHNoYTI1NkhleDtcbiAgfSBlbHNlIHtcbiAgICBpZCA9IFwiUjJfQU5OT1RBVElPTl9cIiArIHNoYTI1NkhleDtcbiAgfVxuXG4gIGRlc3Ryb3lIaWdobGlnaHQoaWQpO1xuXG4gIGNvbnN0IGhpZ2hsaWdodCA9IHtcbiAgICBjb2xvcjogY29sb3IgPyBjb2xvciA6IERFRkFVTFRfQkFDS0dST1VORF9DT0xPUixcbiAgICBpZCxcbiAgICBwb2ludGVySW50ZXJhY3Rpb24sXG4gICAgcmFuZ2VJbmZvLFxuICB9O1xuICBfaGlnaGxpZ2h0cy5wdXNoKGhpZ2hsaWdodCk7XG4gIGNyZWF0ZUhpZ2hsaWdodERvbShcbiAgICB3aW5kb3csXG4gICAgaGlnaGxpZ2h0LFxuICAgIHR5cGUgPT0gSURfQU5OT1RBVElPTl9DT05UQUlORVIgPyB0cnVlIDogZmFsc2VcbiAgKTtcblxuICByZXR1cm4gaGlnaGxpZ2h0O1xufVxuXG5leHBvcnQgZnVuY3Rpb24gY3JlYXRlSGlnaGxpZ2h0KHNlbGVjdGlvbkluZm8sIGNvbG9yLCBwb2ludGVySW50ZXJhY3Rpb24pIHtcbiAgcmV0dXJuIF9jcmVhdGVIaWdobGlnaHQoXG4gICAgc2VsZWN0aW9uSW5mbyxcbiAgICBjb2xvcixcbiAgICBwb2ludGVySW50ZXJhY3Rpb24sXG4gICAgSURfSElHSExJR0hUU19DT05UQUlORVJcbiAgKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZUFubm90YXRpb24oaWQpIHtcbiAgbGV0IGkgPSAtMTtcblxuICBjb25zdCBoaWdobGlnaHQgPSBfaGlnaGxpZ2h0cy5maW5kKChoLCBqKSA9PiB7XG4gICAgaSA9IGo7XG4gICAgcmV0dXJuIGguaWQgPT09IGlkO1xuICB9KTtcbiAgaWYgKGkgPT0gX2hpZ2hsaWdodHMubGVuZ3RoKSByZXR1cm47XG5cbiAgdmFyIGxvY2F0aW9ucyA9IHtcbiAgICBsb2NhdGlvbnM6IHJhbmdlSW5mbzJMb2NhdGlvbihoaWdobGlnaHQucmFuZ2VJbmZvKSxcbiAgfTtcblxuICByZXR1cm4gX2NyZWF0ZUhpZ2hsaWdodChcbiAgICBsb2NhdGlvbnMsXG4gICAgaGlnaGxpZ2h0LmNvbG9yLFxuICAgIHRydWUsXG4gICAgSURfQU5OT1RBVElPTl9DT05UQUlORVJcbiAgKTtcbn1cblxuZnVuY3Rpb24gY3JlYXRlSGlnaGxpZ2h0RG9tKHdpbiwgaGlnaGxpZ2h0LCBhbm5vdGF0aW9uRmxhZykge1xuICBjb25zdCBkb2N1bWVudCA9IHdpbi5kb2N1bWVudDtcblxuICBjb25zdCBzY2FsZSA9XG4gICAgMSAvXG4gICAgKHdpbi5SRUFESVVNMiAmJiB3aW4uUkVBRElVTTIuaXNGaXhlZExheW91dFxuICAgICAgPyB3aW4uUkVBRElVTTIuZnhsVmlld3BvcnRTY2FsZVxuICAgICAgOiAxKTtcblxuICBjb25zdCBzY3JvbGxFbGVtZW50ID0gZ2V0U2Nyb2xsaW5nRWxlbWVudChkb2N1bWVudCk7XG5cbiAgY29uc3QgcmFuZ2UgPSBjb252ZXJ0UmFuZ2VJbmZvKGRvY3VtZW50LCBoaWdobGlnaHQucmFuZ2VJbmZvKTtcbiAgaWYgKCFyYW5nZSkge1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cblxuICBjb25zdCBwYWdpbmF0ZWQgPSBpc1BhZ2luYXRlZChkb2N1bWVudCk7XG4gIGNvbnN0IGhpZ2hsaWdodHNDb250YWluZXIgPSBlbnN1cmVDb250YWluZXIod2luLCBhbm5vdGF0aW9uRmxhZyk7XG4gIGNvbnN0IGhpZ2hsaWdodFBhcmVudCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoXCJkaXZcIik7XG5cbiAgaGlnaGxpZ2h0UGFyZW50LnNldEF0dHJpYnV0ZShcImlkXCIsIGhpZ2hsaWdodC5pZCk7XG4gIGhpZ2hsaWdodFBhcmVudC5zZXRBdHRyaWJ1dGUoXCJjbGFzc1wiLCBDTEFTU19ISUdITElHSFRfQ09OVEFJTkVSKTtcblxuICBkb2N1bWVudC5ib2R5LnN0eWxlLnBvc2l0aW9uID0gXCJyZWxhdGl2ZVwiO1xuICBoaWdobGlnaHRQYXJlbnQuc3R5bGUuc2V0UHJvcGVydHkoXCJwb2ludGVyLWV2ZW50c1wiLCBcIm5vbmVcIik7XG4gIGlmIChoaWdobGlnaHQucG9pbnRlckludGVyYWN0aW9uKSB7XG4gICAgaGlnaGxpZ2h0UGFyZW50LnNldEF0dHJpYnV0ZShcImRhdGEtY2xpY2tcIiwgXCIxXCIpO1xuICB9XG5cbiAgY29uc3QgYm9keVJlY3QgPSBkb2N1bWVudC5ib2R5LmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpO1xuICBjb25zdCB1c2VTVkcgPSAhREVCVUdfVklTVUFMUyAmJiBVU0VfU1ZHO1xuICAvL2NvbnN0IHVzZVNWRyA9IFVTRV9TVkc7XG4gIGNvbnN0IGRyYXdVbmRlcmxpbmUgPSBmYWxzZTtcbiAgY29uc3QgZHJhd1N0cmlrZVRocm91Z2ggPSBmYWxzZTtcbiAgY29uc3QgZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0cyA9IGRyYXdVbmRlcmxpbmUgfHwgZHJhd1N0cmlrZVRocm91Z2g7XG4gIC8vY29uc3QgY2xpZW50UmVjdHMgPSBERUJVR19WSVNVQUxTID8gcmFuZ2UuZ2V0Q2xpZW50UmVjdHMoKSA6XG4gIGNvbnN0IGNsaWVudFJlY3RzID0gZ2V0Q2xpZW50UmVjdHNOb092ZXJsYXAoXG4gICAgcmFuZ2UsXG4gICAgZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0c1xuICApO1xuICBsZXQgaGlnaGxpZ2h0QXJlYVNWR0RvY0ZyYWc7XG4gIGNvbnN0IHJvdW5kZWRDb3JuZXIgPSAzO1xuICBjb25zdCB1bmRlcmxpbmVUaGlja25lc3MgPSAyO1xuICBjb25zdCBzdHJpa2VUaHJvdWdoTGluZVRoaWNrbmVzcyA9IDM7XG4gIGNvbnN0IG9wYWNpdHkgPSBERUZBVUxUX0JBQ0tHUk9VTkRfQ09MT1JfT1BBQ0lUWTtcbiAgbGV0IGV4dHJhID0gXCJcIjtcbiAgY29uc3QgcmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0ID1cbiAgICBmcmFtZUZvckhpZ2hsaWdodEFubm90YXRpb25NYXJrV2l0aElEKHdpbiwgaGlnaGxpZ2h0LmlkKTtcblxuICBsZXQgeE9mZnNldDtcbiAgbGV0IHlPZmZzZXQ7XG4gIGxldCBhbm5vdGF0aW9uT2Zmc2V0O1xuXG4gIGlmIChuYXZpZ2F0b3IudXNlckFnZW50Lm1hdGNoKC9BbmRyb2lkL2kpKSB7XG4gICAgeE9mZnNldCA9IHBhZ2luYXRlZCA/IC1zY3JvbGxFbGVtZW50LnNjcm9sbExlZnQgOiBib2R5UmVjdC5sZWZ0O1xuICAgIHlPZmZzZXQgPSBwYWdpbmF0ZWQgPyAtc2Nyb2xsRWxlbWVudC5zY3JvbGxUb3AgOiBib2R5UmVjdC50b3A7XG4gICAgYW5ub3RhdGlvbk9mZnNldCA9XG4gICAgICBwYXJzZUludChcbiAgICAgICAgKHJhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdC5yaWdodCAtIHhPZmZzZXQpIC8gd2luZG93LmlubmVyV2lkdGhcbiAgICAgICkgKyAxO1xuICB9IGVsc2UgaWYgKG5hdmlnYXRvci51c2VyQWdlbnQubWF0Y2goL2lQaG9uZXxpUGFkfGlQb2QvaSkpIHtcbiAgICB4T2Zmc2V0ID0gcGFnaW5hdGVkID8gMCA6IC1zY3JvbGxFbGVtZW50LnNjcm9sbExlZnQ7XG4gICAgeU9mZnNldCA9IHBhZ2luYXRlZCA/IDAgOiBib2R5UmVjdC50b3A7XG4gICAgYW5ub3RhdGlvbk9mZnNldCA9IHBhcnNlSW50KFxuICAgICAgcmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0LnJpZ2h0IC8gd2luZG93LmlubmVyV2lkdGggKyAxXG4gICAgKTtcbiAgfVxuXG4gIGZvciAoY29uc3QgY2xpZW50UmVjdCBvZiBjbGllbnRSZWN0cykge1xuICAgIGlmICh1c2VTVkcpIHtcbiAgICAgIGNvbnN0IGJvcmRlclRoaWNrbmVzcyA9IDA7XG4gICAgICBpZiAoIWhpZ2hsaWdodEFyZWFTVkdEb2NGcmFnKSB7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdEb2NGcmFnID0gZG9jdW1lbnQuY3JlYXRlRG9jdW1lbnRGcmFnbWVudCgpO1xuICAgICAgfVxuICAgICAgY29uc3QgaGlnaGxpZ2h0QXJlYVNWR1JlY3QgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50TlMoXG4gICAgICAgIFNWR19YTUxfTkFNRVNQQUNFLFxuICAgICAgICBcInJlY3RcIlxuICAgICAgKTtcblxuICAgICAgaGlnaGxpZ2h0QXJlYVNWR1JlY3Quc2V0QXR0cmlidXRlKFwiY2xhc3NcIiwgQ0xBU1NfSElHSExJR0hUX0FSRUEpO1xuICAgICAgaGlnaGxpZ2h0QXJlYVNWR1JlY3Quc2V0QXR0cmlidXRlKFxuICAgICAgICBcInN0eWxlXCIsXG4gICAgICAgIGBmaWxsOiByZ2IoJHtoaWdobGlnaHQuY29sb3IucmVkfSwgJHtoaWdobGlnaHQuY29sb3IuZ3JlZW59LCAke2hpZ2hsaWdodC5jb2xvci5ibHVlfSkgIWltcG9ydGFudDsgZmlsbC1vcGFjaXR5OiAke29wYWNpdHl9ICFpbXBvcnRhbnQ7IHN0cm9rZS13aWR0aDogMDtgXG4gICAgICApO1xuICAgICAgaGlnaGxpZ2h0QXJlYVNWR1JlY3Quc2NhbGUgPSBzY2FsZTtcblxuICAgICAgLypcbiAgICAgICAgICAgICBoaWdobGlnaHRBcmVhU1ZHUmVjdC5yZWN0ID0ge1xuICAgICAgICAgICAgIGhlaWdodDogY2xpZW50UmVjdC5oZWlnaHQsXG4gICAgICAgICAgICAgbGVmdDogY2xpZW50UmVjdC5sZWZ0IC0geE9mZnNldCxcbiAgICAgICAgICAgICB0b3A6IGNsaWVudFJlY3QudG9wIC0geU9mZnNldCxcbiAgICAgICAgICAgICB3aWR0aDogY2xpZW50UmVjdC53aWR0aCxcbiAgICAgICAgICAgICB9O1xuICAgICAgICAgICAgICovXG5cbiAgICAgIGlmIChhbm5vdGF0aW9uRmxhZykge1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHUmVjdC5yZWN0ID0ge1xuICAgICAgICAgIGhlaWdodDogQU5OT1RBVElPTl9XSURUSCwgLy9yYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QuaGVpZ2h0IC0gcmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0LmhlaWdodC80LFxuICAgICAgICAgIGxlZnQ6IHdpbmRvdy5pbm5lcldpZHRoICogYW5ub3RhdGlvbk9mZnNldCAtIEFOTk9UQVRJT05fV0lEVEgsXG4gICAgICAgICAgdG9wOiByYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QudG9wIC0geU9mZnNldCxcbiAgICAgICAgICB3aWR0aDogQU5OT1RBVElPTl9XSURUSCxcbiAgICAgICAgfTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdSZWN0LnJlY3QgPSB7XG4gICAgICAgICAgaGVpZ2h0OiBjbGllbnRSZWN0LmhlaWdodCxcbiAgICAgICAgICBsZWZ0OiBjbGllbnRSZWN0LmxlZnQgLSB4T2Zmc2V0LFxuICAgICAgICAgIHRvcDogY2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgICAgIHdpZHRoOiBjbGllbnRSZWN0LndpZHRoLFxuICAgICAgICB9O1xuICAgICAgfVxuXG4gICAgICBoaWdobGlnaHRBcmVhU1ZHUmVjdC5zZXRBdHRyaWJ1dGUoXCJyeFwiLCBgJHtyb3VuZGVkQ29ybmVyICogc2NhbGV9YCk7XG4gICAgICBoaWdobGlnaHRBcmVhU1ZHUmVjdC5zZXRBdHRyaWJ1dGUoXCJyeVwiLCBgJHtyb3VuZGVkQ29ybmVyICogc2NhbGV9YCk7XG4gICAgICBoaWdobGlnaHRBcmVhU1ZHUmVjdC5zZXRBdHRyaWJ1dGUoXG4gICAgICAgIFwieFwiLFxuICAgICAgICBgJHsoaGlnaGxpZ2h0QXJlYVNWR1JlY3QucmVjdC5sZWZ0IC0gYm9yZGVyVGhpY2tuZXNzKSAqIHNjYWxlfWBcbiAgICAgICk7XG4gICAgICBoaWdobGlnaHRBcmVhU1ZHUmVjdC5zZXRBdHRyaWJ1dGUoXG4gICAgICAgIFwieVwiLFxuICAgICAgICBgJHsoaGlnaGxpZ2h0QXJlYVNWR1JlY3QucmVjdC50b3AgLSBib3JkZXJUaGlja25lc3MpICogc2NhbGV9YFxuICAgICAgKTtcbiAgICAgIGhpZ2hsaWdodEFyZWFTVkdSZWN0LnNldEF0dHJpYnV0ZShcbiAgICAgICAgXCJoZWlnaHRcIixcbiAgICAgICAgYCR7KGhpZ2hsaWdodEFyZWFTVkdSZWN0LnJlY3QuaGVpZ2h0ICsgYm9yZGVyVGhpY2tuZXNzICogMikgKiBzY2FsZX1gXG4gICAgICApO1xuICAgICAgaGlnaGxpZ2h0QXJlYVNWR1JlY3Quc2V0QXR0cmlidXRlKFxuICAgICAgICBcIndpZHRoXCIsXG4gICAgICAgIGAkeyhoaWdobGlnaHRBcmVhU1ZHUmVjdC5yZWN0LndpZHRoICsgYm9yZGVyVGhpY2tuZXNzICogMikgKiBzY2FsZX1gXG4gICAgICApO1xuICAgICAgaGlnaGxpZ2h0QXJlYVNWR0RvY0ZyYWcuYXBwZW5kQ2hpbGQoaGlnaGxpZ2h0QXJlYVNWR1JlY3QpO1xuICAgICAgaWYgKGRyYXdVbmRlcmxpbmUpIHtcbiAgICAgICAgY29uc3QgaGlnaGxpZ2h0QXJlYVNWR0xpbmUgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50TlMoXG4gICAgICAgICAgU1ZHX1hNTF9OQU1FU1BBQ0UsXG4gICAgICAgICAgXCJsaW5lXCJcbiAgICAgICAgKTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR1JlY3Quc2V0QXR0cmlidXRlKFwiY2xhc3NcIiwgQ0xBU1NfSElHSExJR0hUX0FSRUEpO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5zZXRBdHRyaWJ1dGUoXG4gICAgICAgICAgXCJzdHlsZVwiLFxuICAgICAgICAgIGBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS13aWR0aDogJHtcbiAgICAgICAgICAgIHVuZGVybGluZVRoaWNrbmVzcyAqIHNjYWxlXG4gICAgICAgICAgfTsgc3Ryb2tlOiByZ2IoJHtoaWdobGlnaHQuY29sb3IucmVkfSwgJHtoaWdobGlnaHQuY29sb3IuZ3JlZW59LCAke1xuICAgICAgICAgICAgaGlnaGxpZ2h0LmNvbG9yLmJsdWVcbiAgICAgICAgICB9KSAhaW1wb3J0YW50OyBzdHJva2Utb3BhY2l0eTogJHtvcGFjaXR5fSAhaW1wb3J0YW50YFxuICAgICAgICApO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5zY2FsZSA9IHNjYWxlO1xuICAgICAgICAvKlxuICAgICAgICAgICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0ID0ge1xuICAgICAgICAgICAgICAgICBoZWlnaHQ6IGNsaWVudFJlY3QuaGVpZ2h0LFxuICAgICAgICAgICAgICAgICBsZWZ0OiBjbGllbnRSZWN0LmxlZnQgLSB4T2Zmc2V0LFxuICAgICAgICAgICAgICAgICB0b3A6IGNsaWVudFJlY3QudG9wIC0geU9mZnNldCxcbiAgICAgICAgICAgICAgICAgd2lkdGg6IGNsaWVudFJlY3Qud2lkdGgsXG4gICAgICAgICAgICAgICAgIH07XG4gICAgICAgICAgICAgICAgICovXG4gICAgICAgIGlmIChhbm5vdGF0aW9uRmxhZykge1xuICAgICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnJlY3QgPSB7XG4gICAgICAgICAgICBoZWlnaHQ6IEFOTk9UQVRJT05fV0lEVEgsIC8vcmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0LmhlaWdodCAtIHJhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdC5oZWlnaHQvNCxcbiAgICAgICAgICAgIGxlZnQ6IHdpbmRvdy5pbm5lcldpZHRoICogYW5ub3RhdGlvbk9mZnNldCAtIEFOTk9UQVRJT05fV0lEVEgsXG4gICAgICAgICAgICB0b3A6IHJhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgICAgICAgd2lkdGg6IEFOTk9UQVRJT05fV0lEVEgsXG4gICAgICAgICAgfTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0ID0ge1xuICAgICAgICAgICAgaGVpZ2h0OiBjbGllbnRSZWN0LmhlaWdodCxcbiAgICAgICAgICAgIGxlZnQ6IGNsaWVudFJlY3QubGVmdCAtIHhPZmZzZXQsXG4gICAgICAgICAgICB0b3A6IGNsaWVudFJlY3QudG9wIC0geU9mZnNldCxcbiAgICAgICAgICAgIHdpZHRoOiBjbGllbnRSZWN0LndpZHRoLFxuICAgICAgICAgIH07XG4gICAgICAgIH1cblxuICAgICAgICBjb25zdCBsaW5lT2Zmc2V0ID1cbiAgICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LndpZHRoID4gcm91bmRlZENvcm5lciA/IHJvdW5kZWRDb3JuZXIgOiAwO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5zZXRBdHRyaWJ1dGUoXG4gICAgICAgICAgXCJ4MVwiLFxuICAgICAgICAgIGAkeyhoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LmxlZnQgKyBsaW5lT2Zmc2V0KSAqIHNjYWxlfWBcbiAgICAgICAgKTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0xpbmUuc2V0QXR0cmlidXRlKFxuICAgICAgICAgIFwieDJcIixcbiAgICAgICAgICBgJHtcbiAgICAgICAgICAgIChoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LmxlZnQgK1xuICAgICAgICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LndpZHRoIC1cbiAgICAgICAgICAgICAgbGluZU9mZnNldCkgKlxuICAgICAgICAgICAgc2NhbGVcbiAgICAgICAgICB9YFxuICAgICAgICApO1xuICAgICAgICBjb25zdCB5ID1cbiAgICAgICAgICAoaGlnaGxpZ2h0QXJlYVNWR0xpbmUucmVjdC50b3AgK1xuICAgICAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0xpbmUucmVjdC5oZWlnaHQgLVxuICAgICAgICAgICAgdW5kZXJsaW5lVGhpY2tuZXNzIC8gMikgKlxuICAgICAgICAgIHNjYWxlO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5zZXRBdHRyaWJ1dGUoXCJ5MVwiLCBgJHt5fWApO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5zZXRBdHRyaWJ1dGUoXCJ5MlwiLCBgJHt5fWApO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5zZXRBdHRyaWJ1dGUoXG4gICAgICAgICAgXCJoZWlnaHRcIixcbiAgICAgICAgICBgJHtoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LmhlaWdodCAqIHNjYWxlfWBcbiAgICAgICAgKTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0xpbmUuc2V0QXR0cmlidXRlKFxuICAgICAgICAgIFwid2lkdGhcIixcbiAgICAgICAgICBgJHtoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LndpZHRoICogc2NhbGV9YFxuICAgICAgICApO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHRG9jRnJhZy5hcHBlbmRDaGlsZChoaWdobGlnaHRBcmVhU1ZHTGluZSk7XG4gICAgICB9XG4gICAgICBpZiAoZHJhd1N0cmlrZVRocm91Z2gpIHtcbiAgICAgICAgY29uc3QgaGlnaGxpZ2h0QXJlYVNWR0xpbmUgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50TlMoXG4gICAgICAgICAgU1ZHX1hNTF9OQU1FU1BBQ0UsXG4gICAgICAgICAgXCJsaW5lXCJcbiAgICAgICAgKTtcblxuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHUmVjdC5zZXRBdHRyaWJ1dGUoXCJjbGFzc1wiLCBDTEFTU19ISUdITElHSFRfQVJFQSk7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnNldEF0dHJpYnV0ZShcbiAgICAgICAgICBcInN0eWxlXCIsXG4gICAgICAgICAgYHN0cm9rZS1saW5lY2FwOiBidXR0OyBzdHJva2Utd2lkdGg6ICR7XG4gICAgICAgICAgICBzdHJpa2VUaHJvdWdoTGluZVRoaWNrbmVzcyAqIHNjYWxlXG4gICAgICAgICAgfTsgc3Ryb2tlOiByZ2IoJHtoaWdobGlnaHQuY29sb3IucmVkfSwgJHtoaWdobGlnaHQuY29sb3IuZ3JlZW59LCAke1xuICAgICAgICAgICAgaGlnaGxpZ2h0LmNvbG9yLmJsdWVcbiAgICAgICAgICB9KSAhaW1wb3J0YW50OyBzdHJva2Utb3BhY2l0eTogJHtvcGFjaXR5fSAhaW1wb3J0YW50YFxuICAgICAgICApO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5zY2FsZSA9IHNjYWxlO1xuXG4gICAgICAgIC8qXG4gICAgICAgICAgICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnJlY3QgPSB7XG4gICAgICAgICAgICAgICAgIGhlaWdodDogY2xpZW50UmVjdC5oZWlnaHQsXG4gICAgICAgICAgICAgICAgIGxlZnQ6IGNsaWVudFJlY3QubGVmdCAtIHhPZmZzZXQsXG4gICAgICAgICAgICAgICAgIHRvcDogY2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgICAgICAgICAgICB3aWR0aDogY2xpZW50UmVjdC53aWR0aCxcbiAgICAgICAgICAgICAgICAgfTtcbiAgICAgICAgICAgICAgICAgKi9cblxuICAgICAgICBpZiAoYW5ub3RhdGlvbkZsYWcpIHtcbiAgICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0ID0ge1xuICAgICAgICAgICAgaGVpZ2h0OiBBTk5PVEFUSU9OX1dJRFRILCAvL3JhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdC5oZWlnaHQgLSByYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QuaGVpZ2h0LzQsXG4gICAgICAgICAgICBsZWZ0OiB3aW5kb3cuaW5uZXJXaWR0aCAqIGFubm90YXRpb25PZmZzZXQgLSBBTk5PVEFUSU9OX1dJRFRILFxuICAgICAgICAgICAgdG9wOiByYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QudG9wIC0geU9mZnNldCxcbiAgICAgICAgICAgIHdpZHRoOiBBTk5PVEFUSU9OX1dJRFRILFxuICAgICAgICAgIH07XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0xpbmUucmVjdCA9IHtcbiAgICAgICAgICAgIGhlaWdodDogY2xpZW50UmVjdC5oZWlnaHQsXG4gICAgICAgICAgICBsZWZ0OiBjbGllbnRSZWN0LmxlZnQgLSB4T2Zmc2V0LFxuICAgICAgICAgICAgdG9wOiBjbGllbnRSZWN0LnRvcCAtIHlPZmZzZXQsXG4gICAgICAgICAgICB3aWR0aDogY2xpZW50UmVjdC53aWR0aCxcbiAgICAgICAgICB9O1xuICAgICAgICB9XG5cbiAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0xpbmUuc2V0QXR0cmlidXRlKFxuICAgICAgICAgIFwieDFcIixcbiAgICAgICAgICBgJHtoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LmxlZnQgKiBzY2FsZX1gXG4gICAgICAgICk7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnNldEF0dHJpYnV0ZShcbiAgICAgICAgICBcIngyXCIsXG4gICAgICAgICAgYCR7XG4gICAgICAgICAgICAoaGlnaGxpZ2h0QXJlYVNWR0xpbmUucmVjdC5sZWZ0ICsgaGlnaGxpZ2h0QXJlYVNWR0xpbmUucmVjdC53aWR0aCkgKlxuICAgICAgICAgICAgc2NhbGVcbiAgICAgICAgICB9YFxuICAgICAgICApO1xuICAgICAgICBjb25zdCBsaW5lT2Zmc2V0ID0gaGlnaGxpZ2h0QXJlYVNWR0xpbmUucmVjdC5oZWlnaHQgLyAyO1xuICAgICAgICBjb25zdCB5ID0gKGhpZ2hsaWdodEFyZWFTVkdMaW5lLnJlY3QudG9wICsgbGluZU9mZnNldCkgKiBzY2FsZTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0xpbmUuc2V0QXR0cmlidXRlKFwieTFcIiwgYCR7eX1gKTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0xpbmUuc2V0QXR0cmlidXRlKFwieTJcIiwgYCR7eX1gKTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0xpbmUuc2V0QXR0cmlidXRlKFxuICAgICAgICAgIFwiaGVpZ2h0XCIsXG4gICAgICAgICAgYCR7aGlnaGxpZ2h0QXJlYVNWR0xpbmUucmVjdC5oZWlnaHQgKiBzY2FsZX1gXG4gICAgICAgICk7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnNldEF0dHJpYnV0ZShcbiAgICAgICAgICBcIndpZHRoXCIsXG4gICAgICAgICAgYCR7aGlnaGxpZ2h0QXJlYVNWR0xpbmUucmVjdC53aWR0aCAqIHNjYWxlfWBcbiAgICAgICAgKTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0RvY0ZyYWcuYXBwZW5kQ2hpbGQoaGlnaGxpZ2h0QXJlYVNWR0xpbmUpO1xuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICBjb25zdCBoaWdobGlnaHRBcmVhID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudChcImRpdlwiKTtcblxuICAgICAgaGlnaGxpZ2h0QXJlYS5zZXRBdHRyaWJ1dGUoXCJjbGFzc1wiLCBDTEFTU19ISUdITElHSFRfQVJFQSk7XG5cbiAgICAgIGlmIChERUJVR19WSVNVQUxTKSB7XG4gICAgICAgIGNvbnN0IHJnYiA9IE1hdGgucm91bmQoMHhmZmZmZmYgKiBNYXRoLnJhbmRvbSgpKTtcbiAgICAgICAgY29uc3QgciA9IHJnYiA+PiAxNjtcbiAgICAgICAgY29uc3QgZyA9IChyZ2IgPj4gOCkgJiAyNTU7XG4gICAgICAgIGNvbnN0IGIgPSByZ2IgJiAyNTU7XG4gICAgICAgIGV4dHJhID0gYG91dGxpbmUtY29sb3I6IHJnYigke3J9LCAke2d9LCAke2J9KTsgb3V0bGluZS1zdHlsZTogc29saWQ7IG91dGxpbmUtd2lkdGg6IDFweDsgb3V0bGluZS1vZmZzZXQ6IC0xcHg7YDtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGlmIChkcmF3VW5kZXJsaW5lKSB7XG4gICAgICAgICAgZXh0cmEgKz0gYGJvcmRlci1ib3R0b206ICR7dW5kZXJsaW5lVGhpY2tuZXNzICogc2NhbGV9cHggc29saWQgcmdiYSgke1xuICAgICAgICAgICAgaGlnaGxpZ2h0LmNvbG9yLnJlZFxuICAgICAgICAgIH0sICR7aGlnaGxpZ2h0LmNvbG9yLmdyZWVufSwgJHtcbiAgICAgICAgICAgIGhpZ2hsaWdodC5jb2xvci5ibHVlXG4gICAgICAgICAgfSwgJHtvcGFjaXR5fSkgIWltcG9ydGFudGA7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICAgIGhpZ2hsaWdodEFyZWEuc2V0QXR0cmlidXRlKFxuICAgICAgICBcInN0eWxlXCIsXG4gICAgICAgIGBib3JkZXItcmFkaXVzOiAke3JvdW5kZWRDb3JuZXJ9cHggIWltcG9ydGFudDsgYmFja2dyb3VuZC1jb2xvcjogcmdiYSgke2hpZ2hsaWdodC5jb2xvci5yZWR9LCAke2hpZ2hsaWdodC5jb2xvci5ncmVlbn0sICR7aGlnaGxpZ2h0LmNvbG9yLmJsdWV9LCAke29wYWNpdHl9KSAhaW1wb3J0YW50OyAke2V4dHJhfWBcbiAgICAgICk7XG4gICAgICBoaWdobGlnaHRBcmVhLnN0eWxlLnNldFByb3BlcnR5KFwicG9pbnRlci1ldmVudHNcIiwgXCJub25lXCIpO1xuICAgICAgaGlnaGxpZ2h0QXJlYS5zdHlsZS5wb3NpdGlvbiA9IHBhZ2luYXRlZCA/IFwiZml4ZWRcIiA6IFwiYWJzb2x1dGVcIjtcbiAgICAgIGhpZ2hsaWdodEFyZWEuc2NhbGUgPSBzY2FsZTtcbiAgICAgIC8qXG4gICAgICAgICAgICAgaGlnaGxpZ2h0QXJlYS5yZWN0ID0ge1xuICAgICAgICAgICAgIGhlaWdodDogY2xpZW50UmVjdC5oZWlnaHQsXG4gICAgICAgICAgICAgbGVmdDogY2xpZW50UmVjdC5sZWZ0IC0geE9mZnNldCxcbiAgICAgICAgICAgICB0b3A6IGNsaWVudFJlY3QudG9wIC0geU9mZnNldCxcbiAgICAgICAgICAgICB3aWR0aDogY2xpZW50UmVjdC53aWR0aCxcbiAgICAgICAgICAgICB9O1xuICAgICAgICAgICAgICovXG4gICAgICBpZiAoYW5ub3RhdGlvbkZsYWcpIHtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYS5yZWN0ID0ge1xuICAgICAgICAgIGhlaWdodDogQU5OT1RBVElPTl9XSURUSCwgLy9yYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QuaGVpZ2h0IC0gcmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0LmhlaWdodC80LFxuICAgICAgICAgIGxlZnQ6IHdpbmRvdy5pbm5lcldpZHRoICogYW5ub3RhdGlvbk9mZnNldCAtIEFOTk9UQVRJT05fV0lEVEgsXG4gICAgICAgICAgdG9wOiByYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QudG9wIC0geU9mZnNldCxcbiAgICAgICAgICB3aWR0aDogQU5OT1RBVElPTl9XSURUSCxcbiAgICAgICAgfTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGhpZ2hsaWdodEFyZWEucmVjdCA9IHtcbiAgICAgICAgICBoZWlnaHQ6IGNsaWVudFJlY3QuaGVpZ2h0LFxuICAgICAgICAgIGxlZnQ6IGNsaWVudFJlY3QubGVmdCAtIHhPZmZzZXQsXG4gICAgICAgICAgdG9wOiBjbGllbnRSZWN0LnRvcCAtIHlPZmZzZXQsXG4gICAgICAgICAgd2lkdGg6IGNsaWVudFJlY3Qud2lkdGgsXG4gICAgICAgIH07XG4gICAgICB9XG5cbiAgICAgIGhpZ2hsaWdodEFyZWEuc3R5bGUud2lkdGggPSBgJHtoaWdobGlnaHRBcmVhLnJlY3Qud2lkdGggKiBzY2FsZX1weGA7XG4gICAgICBoaWdobGlnaHRBcmVhLnN0eWxlLmhlaWdodCA9IGAke2hpZ2hsaWdodEFyZWEucmVjdC5oZWlnaHQgKiBzY2FsZX1weGA7XG4gICAgICBoaWdobGlnaHRBcmVhLnN0eWxlLmxlZnQgPSBgJHtoaWdobGlnaHRBcmVhLnJlY3QubGVmdCAqIHNjYWxlfXB4YDtcbiAgICAgIGhpZ2hsaWdodEFyZWEuc3R5bGUudG9wID0gYCR7aGlnaGxpZ2h0QXJlYS5yZWN0LnRvcCAqIHNjYWxlfXB4YDtcbiAgICAgIGhpZ2hsaWdodFBhcmVudC5hcHBlbmQoaGlnaGxpZ2h0QXJlYSk7XG4gICAgICBpZiAoIURFQlVHX1ZJU1VBTFMgJiYgZHJhd1N0cmlrZVRocm91Z2gpIHtcbiAgICAgICAgLy9pZiAoZHJhd1N0cmlrZVRocm91Z2gpIHtcbiAgICAgICAgY29uc3QgaGlnaGxpZ2h0QXJlYUxpbmUgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KFwiZGl2XCIpO1xuICAgICAgICBoaWdobGlnaHRBcmVhTGluZS5zZXRBdHRyaWJ1dGUoXCJjbGFzc1wiLCBDTEFTU19ISUdITElHSFRfQVJFQSk7XG5cbiAgICAgICAgaGlnaGxpZ2h0QXJlYUxpbmUuc2V0QXR0cmlidXRlKFxuICAgICAgICAgIFwic3R5bGVcIixcbiAgICAgICAgICBgYmFja2dyb3VuZC1jb2xvcjogcmdiYSgke2hpZ2hsaWdodC5jb2xvci5yZWR9LCAke2hpZ2hsaWdodC5jb2xvci5ncmVlbn0sICR7aGlnaGxpZ2h0LmNvbG9yLmJsdWV9LCAke29wYWNpdHl9KSAhaW1wb3J0YW50O2BcbiAgICAgICAgKTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYUxpbmUuc3R5bGUuc2V0UHJvcGVydHkoXCJwb2ludGVyLWV2ZW50c1wiLCBcIm5vbmVcIik7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFMaW5lLnN0eWxlLnBvc2l0aW9uID0gcGFnaW5hdGVkID8gXCJmaXhlZFwiIDogXCJhYnNvbHV0ZVwiO1xuICAgICAgICBoaWdobGlnaHRBcmVhTGluZS5zY2FsZSA9IHNjYWxlO1xuICAgICAgICAvKlxuICAgICAgICAgICAgICAgICBoaWdobGlnaHRBcmVhTGluZS5yZWN0ID0ge1xuICAgICAgICAgICAgICAgICBoZWlnaHQ6IGNsaWVudFJlY3QuaGVpZ2h0LFxuICAgICAgICAgICAgICAgICBsZWZ0OiBjbGllbnRSZWN0LmxlZnQgLSB4T2Zmc2V0LFxuICAgICAgICAgICAgICAgICB0b3A6IGNsaWVudFJlY3QudG9wIC0geU9mZnNldCxcbiAgICAgICAgICAgICAgICAgd2lkdGg6IGNsaWVudFJlY3Qud2lkdGgsXG4gICAgICAgICAgICAgICAgIH07XG4gICAgICAgICAgICAgICAgICovXG5cbiAgICAgICAgaWYgKGFubm90YXRpb25GbGFnKSB7XG4gICAgICAgICAgaGlnaGxpZ2h0QXJlYUxpbmUucmVjdCA9IHtcbiAgICAgICAgICAgIGhlaWdodDogQU5OT1RBVElPTl9XSURUSCwgLy9yYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QuaGVpZ2h0IC0gcmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0LmhlaWdodC80LFxuICAgICAgICAgICAgbGVmdDogd2luZG93LmlubmVyV2lkdGggKiBhbm5vdGF0aW9uT2Zmc2V0IC0gQU5OT1RBVElPTl9XSURUSCxcbiAgICAgICAgICAgIHRvcDogcmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0LnRvcCAtIHlPZmZzZXQsXG4gICAgICAgICAgICB3aWR0aDogQU5OT1RBVElPTl9XSURUSCxcbiAgICAgICAgICB9O1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIGhpZ2hsaWdodEFyZWFMaW5lLnJlY3QgPSB7XG4gICAgICAgICAgICBoZWlnaHQ6IGNsaWVudFJlY3QuaGVpZ2h0LFxuICAgICAgICAgICAgbGVmdDogY2xpZW50UmVjdC5sZWZ0IC0geE9mZnNldCxcbiAgICAgICAgICAgIHRvcDogY2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgICAgICAgd2lkdGg6IGNsaWVudFJlY3Qud2lkdGgsXG4gICAgICAgICAgfTtcbiAgICAgICAgfVxuXG4gICAgICAgIGhpZ2hsaWdodEFyZWFMaW5lLnN0eWxlLndpZHRoID0gYCR7XG4gICAgICAgICAgaGlnaGxpZ2h0QXJlYUxpbmUucmVjdC53aWR0aCAqIHNjYWxlXG4gICAgICAgIH1weGA7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFMaW5lLnN0eWxlLmhlaWdodCA9IGAke1xuICAgICAgICAgIHN0cmlrZVRocm91Z2hMaW5lVGhpY2tuZXNzICogc2NhbGVcbiAgICAgICAgfXB4YDtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYUxpbmUuc3R5bGUubGVmdCA9IGAke1xuICAgICAgICAgIGhpZ2hsaWdodEFyZWFMaW5lLnJlY3QubGVmdCAqIHNjYWxlXG4gICAgICAgIH1weGA7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFMaW5lLnN0eWxlLnRvcCA9IGAke1xuICAgICAgICAgIChoaWdobGlnaHRBcmVhTGluZS5yZWN0LnRvcCArXG4gICAgICAgICAgICBoaWdobGlnaHRBcmVhTGluZS5yZWN0LmhlaWdodCAvIDIgLVxuICAgICAgICAgICAgc3RyaWtlVGhyb3VnaExpbmVUaGlja25lc3MgLyAyKSAqXG4gICAgICAgICAgc2NhbGVcbiAgICAgICAgfXB4YDtcbiAgICAgICAgaGlnaGxpZ2h0UGFyZW50LmFwcGVuZChoaWdobGlnaHRBcmVhTGluZSk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgaWYgKGFubm90YXRpb25GbGFnKSB7XG4gICAgICBicmVhaztcbiAgICB9XG4gIH1cblxuICBpZiAodXNlU1ZHICYmIGhpZ2hsaWdodEFyZWFTVkdEb2NGcmFnKSB7XG4gICAgY29uc3QgaGlnaGxpZ2h0QXJlYVNWRyA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnROUyhTVkdfWE1MX05BTUVTUEFDRSwgXCJzdmdcIik7XG4gICAgaGlnaGxpZ2h0QXJlYVNWRy5zZXRBdHRyaWJ1dGUoXCJwb2ludGVyLWV2ZW50c1wiLCBcIm5vbmVcIik7XG4gICAgaGlnaGxpZ2h0QXJlYVNWRy5zdHlsZS5wb3NpdGlvbiA9IHBhZ2luYXRlZCA/IFwiZml4ZWRcIiA6IFwiYWJzb2x1dGVcIjtcbiAgICBoaWdobGlnaHRBcmVhU1ZHLnN0eWxlLm92ZXJmbG93ID0gXCJ2aXNpYmxlXCI7XG4gICAgaGlnaGxpZ2h0QXJlYVNWRy5zdHlsZS5sZWZ0ID0gXCIwXCI7XG4gICAgaGlnaGxpZ2h0QXJlYVNWRy5zdHlsZS50b3AgPSBcIjBcIjtcbiAgICBoaWdobGlnaHRBcmVhU1ZHLmFwcGVuZChoaWdobGlnaHRBcmVhU1ZHRG9jRnJhZyk7XG4gICAgaGlnaGxpZ2h0UGFyZW50LmFwcGVuZChoaWdobGlnaHRBcmVhU1ZHKTtcbiAgfVxuXG4gIGNvbnN0IGhpZ2hsaWdodEJvdW5kaW5nID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudChcImRpdlwiKTtcblxuICBpZiAoYW5ub3RhdGlvbkZsYWcpIHtcbiAgICBoaWdobGlnaHRCb3VuZGluZy5zZXRBdHRyaWJ1dGUoXCJjbGFzc1wiLCBDTEFTU19BTk5PVEFUSU9OX0JPVU5ESU5HX0FSRUEpO1xuICAgIGhpZ2hsaWdodEJvdW5kaW5nLnNldEF0dHJpYnV0ZShcbiAgICAgIFwic3R5bGVcIixcbiAgICAgIGBib3JkZXItcmFkaXVzOiAke3JvdW5kZWRDb3JuZXJ9cHggIWltcG9ydGFudDsgYmFja2dyb3VuZC1jb2xvcjogcmdiYSgke2hpZ2hsaWdodC5jb2xvci5yZWR9LCAke2hpZ2hsaWdodC5jb2xvci5ncmVlbn0sICR7aGlnaGxpZ2h0LmNvbG9yLmJsdWV9LCAke29wYWNpdHl9KSAhaW1wb3J0YW50OyAke2V4dHJhfWBcbiAgICApO1xuICB9IGVsc2Uge1xuICAgIGhpZ2hsaWdodEJvdW5kaW5nLnNldEF0dHJpYnV0ZShcImNsYXNzXCIsIENMQVNTX0hJR0hMSUdIVF9CT1VORElOR19BUkVBKTtcbiAgfVxuXG4gIGhpZ2hsaWdodEJvdW5kaW5nLnN0eWxlLnNldFByb3BlcnR5KFwicG9pbnRlci1ldmVudHNcIiwgXCJub25lXCIpO1xuICBoaWdobGlnaHRCb3VuZGluZy5zdHlsZS5wb3NpdGlvbiA9IHBhZ2luYXRlZCA/IFwiZml4ZWRcIiA6IFwiYWJzb2x1dGVcIjtcbiAgaGlnaGxpZ2h0Qm91bmRpbmcuc2NhbGUgPSBzY2FsZTtcblxuICBpZiAoREVCVUdfVklTVUFMUykge1xuICAgIGhpZ2hsaWdodEJvdW5kaW5nLnNldEF0dHJpYnV0ZShcbiAgICAgIFwic3R5bGVcIixcbiAgICAgIGBvdXRsaW5lLWNvbG9yOiBtYWdlbnRhOyBvdXRsaW5lLXN0eWxlOiBzb2xpZDsgb3V0bGluZS13aWR0aDogMXB4OyBvdXRsaW5lLW9mZnNldDogLTFweDtgXG4gICAgKTtcbiAgfVxuXG4gIGlmIChhbm5vdGF0aW9uRmxhZykge1xuICAgIGhpZ2hsaWdodEJvdW5kaW5nLnJlY3QgPSB7XG4gICAgICBoZWlnaHQ6IEFOTk9UQVRJT05fV0lEVEgsIC8vcmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0LmhlaWdodCAtIHJhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdC5oZWlnaHQvNCxcbiAgICAgIGxlZnQ6IHdpbmRvdy5pbm5lcldpZHRoICogYW5ub3RhdGlvbk9mZnNldCAtIEFOTk9UQVRJT05fV0lEVEgsXG4gICAgICB0b3A6IHJhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgd2lkdGg6IEFOTk9UQVRJT05fV0lEVEgsXG4gICAgfTtcbiAgfSBlbHNlIHtcbiAgICBjb25zdCByYW5nZUJvdW5kaW5nQ2xpZW50UmVjdCA9IHJhbmdlLmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpO1xuICAgIGhpZ2hsaWdodEJvdW5kaW5nLnJlY3QgPSB7XG4gICAgICBoZWlnaHQ6IHJhbmdlQm91bmRpbmdDbGllbnRSZWN0LmhlaWdodCxcbiAgICAgIGxlZnQ6IHJhbmdlQm91bmRpbmdDbGllbnRSZWN0LmxlZnQgLSB4T2Zmc2V0LFxuICAgICAgdG9wOiByYW5nZUJvdW5kaW5nQ2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgd2lkdGg6IHJhbmdlQm91bmRpbmdDbGllbnRSZWN0LndpZHRoLFxuICAgIH07XG4gIH1cblxuICBoaWdobGlnaHRCb3VuZGluZy5zdHlsZS53aWR0aCA9IGAke2hpZ2hsaWdodEJvdW5kaW5nLnJlY3Qud2lkdGggKiBzY2FsZX1weGA7XG4gIGhpZ2hsaWdodEJvdW5kaW5nLnN0eWxlLmhlaWdodCA9IGAke2hpZ2hsaWdodEJvdW5kaW5nLnJlY3QuaGVpZ2h0ICogc2NhbGV9cHhgO1xuICBoaWdobGlnaHRCb3VuZGluZy5zdHlsZS5sZWZ0ID0gYCR7aGlnaGxpZ2h0Qm91bmRpbmcucmVjdC5sZWZ0ICogc2NhbGV9cHhgO1xuICBoaWdobGlnaHRCb3VuZGluZy5zdHlsZS50b3AgPSBgJHtoaWdobGlnaHRCb3VuZGluZy5yZWN0LnRvcCAqIHNjYWxlfXB4YDtcblxuICBoaWdobGlnaHRQYXJlbnQuYXBwZW5kKGhpZ2hsaWdodEJvdW5kaW5nKTtcbiAgaGlnaGxpZ2h0c0NvbnRhaW5lci5hcHBlbmQoaGlnaGxpZ2h0UGFyZW50KTtcblxuICByZXR1cm4gaGlnaGxpZ2h0UGFyZW50O1xufVxuXG5mdW5jdGlvbiBjcmVhdGVPcmRlcmVkUmFuZ2Uoc3RhcnROb2RlLCBzdGFydE9mZnNldCwgZW5kTm9kZSwgZW5kT2Zmc2V0KSB7XG4gIGNvbnN0IHJhbmdlID0gbmV3IFJhbmdlKCk7XG4gIHJhbmdlLnNldFN0YXJ0KHN0YXJ0Tm9kZSwgc3RhcnRPZmZzZXQpO1xuICByYW5nZS5zZXRFbmQoZW5kTm9kZSwgZW5kT2Zmc2V0KTtcbiAgaWYgKCFyYW5nZS5jb2xsYXBzZWQpIHtcbiAgICByZXR1cm4gcmFuZ2U7XG4gIH1cbiAgY29uc29sZS5sb2coXCI+Pj4gY3JlYXRlT3JkZXJlZFJhbmdlIENPTExBUFNFRCAuLi4gUkFOR0UgUkVWRVJTRT9cIik7XG4gIGNvbnN0IHJhbmdlUmV2ZXJzZSA9IG5ldyBSYW5nZSgpO1xuICByYW5nZVJldmVyc2Uuc2V0U3RhcnQoZW5kTm9kZSwgZW5kT2Zmc2V0KTtcbiAgcmFuZ2VSZXZlcnNlLnNldEVuZChzdGFydE5vZGUsIHN0YXJ0T2Zmc2V0KTtcbiAgaWYgKCFyYW5nZVJldmVyc2UuY29sbGFwc2VkKSB7XG4gICAgY29uc29sZS5sb2coXCI+Pj4gY3JlYXRlT3JkZXJlZFJhbmdlIFJBTkdFIFJFVkVSU0UgT0suXCIpO1xuICAgIHJldHVybiByYW5nZTtcbiAgfVxuICBjb25zb2xlLmxvZyhcIj4+PiBjcmVhdGVPcmRlcmVkUmFuZ2UgUkFOR0UgUkVWRVJTRSBBTFNPIENPTExBUFNFRD8hXCIpO1xuICByZXR1cm4gdW5kZWZpbmVkO1xufVxuXG5mdW5jdGlvbiBjb252ZXJ0UmFuZ2UocmFuZ2UsIGdldENzc1NlbGVjdG9yLCBjb21wdXRlRWxlbWVudENGSSkge1xuICBjb25zdCBzdGFydElzRWxlbWVudCA9IHJhbmdlLnN0YXJ0Q29udGFpbmVyLm5vZGVUeXBlID09PSBOb2RlLkVMRU1FTlRfTk9ERTtcbiAgY29uc3Qgc3RhcnRDb250YWluZXJFbGVtZW50ID0gc3RhcnRJc0VsZW1lbnRcbiAgICA/IHJhbmdlLnN0YXJ0Q29udGFpbmVyXG4gICAgOiByYW5nZS5zdGFydENvbnRhaW5lci5wYXJlbnROb2RlICYmXG4gICAgICByYW5nZS5zdGFydENvbnRhaW5lci5wYXJlbnROb2RlLm5vZGVUeXBlID09PSBOb2RlLkVMRU1FTlRfTk9ERVxuICAgID8gcmFuZ2Uuc3RhcnRDb250YWluZXIucGFyZW50Tm9kZVxuICAgIDogdW5kZWZpbmVkO1xuICBpZiAoIXN0YXJ0Q29udGFpbmVyRWxlbWVudCkge1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgY29uc3Qgc3RhcnRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXggPSBzdGFydElzRWxlbWVudFxuICAgID8gLTFcbiAgICA6IEFycmF5LmZyb20oc3RhcnRDb250YWluZXJFbGVtZW50LmNoaWxkTm9kZXMpLmluZGV4T2YoXG4gICAgICAgIHJhbmdlLnN0YXJ0Q29udGFpbmVyXG4gICAgICApO1xuICBpZiAoc3RhcnRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXggPCAtMSkge1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgY29uc3Qgc3RhcnRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3IgPSBnZXRDc3NTZWxlY3RvcihcbiAgICBzdGFydENvbnRhaW5lckVsZW1lbnRcbiAgKTtcbiAgY29uc3QgZW5kSXNFbGVtZW50ID0gcmFuZ2UuZW5kQ29udGFpbmVyLm5vZGVUeXBlID09PSBOb2RlLkVMRU1FTlRfTk9ERTtcbiAgY29uc3QgZW5kQ29udGFpbmVyRWxlbWVudCA9IGVuZElzRWxlbWVudFxuICAgID8gcmFuZ2UuZW5kQ29udGFpbmVyXG4gICAgOiByYW5nZS5lbmRDb250YWluZXIucGFyZW50Tm9kZSAmJlxuICAgICAgcmFuZ2UuZW5kQ29udGFpbmVyLnBhcmVudE5vZGUubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFXG4gICAgPyByYW5nZS5lbmRDb250YWluZXIucGFyZW50Tm9kZVxuICAgIDogdW5kZWZpbmVkO1xuICBpZiAoIWVuZENvbnRhaW5lckVsZW1lbnQpIHtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG4gIGNvbnN0IGVuZENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCA9IGVuZElzRWxlbWVudFxuICAgID8gLTFcbiAgICA6IEFycmF5LmZyb20oZW5kQ29udGFpbmVyRWxlbWVudC5jaGlsZE5vZGVzKS5pbmRleE9mKHJhbmdlLmVuZENvbnRhaW5lcik7XG4gIGlmIChlbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXggPCAtMSkge1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgY29uc3QgZW5kQ29udGFpbmVyRWxlbWVudENzc1NlbGVjdG9yID0gZ2V0Q3NzU2VsZWN0b3IoZW5kQ29udGFpbmVyRWxlbWVudCk7XG4gIGNvbnN0IGNvbW1vbkVsZW1lbnRBbmNlc3RvciA9IGdldENvbW1vbkFuY2VzdG9yRWxlbWVudChcbiAgICByYW5nZS5zdGFydENvbnRhaW5lcixcbiAgICByYW5nZS5lbmRDb250YWluZXJcbiAgKTtcbiAgaWYgKCFjb21tb25FbGVtZW50QW5jZXN0b3IpIHtcbiAgICBjb25zb2xlLmxvZyhcIl5eXiBOTyBSQU5HRSBDT01NT04gQU5DRVNUT1I/IVwiKTtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG4gIGlmIChyYW5nZS5jb21tb25BbmNlc3RvckNvbnRhaW5lcikge1xuICAgIGNvbnN0IHJhbmdlQ29tbW9uQW5jZXN0b3JFbGVtZW50ID1cbiAgICAgIHJhbmdlLmNvbW1vbkFuY2VzdG9yQ29udGFpbmVyLm5vZGVUeXBlID09PSBOb2RlLkVMRU1FTlRfTk9ERVxuICAgICAgICA/IHJhbmdlLmNvbW1vbkFuY2VzdG9yQ29udGFpbmVyXG4gICAgICAgIDogcmFuZ2UuY29tbW9uQW5jZXN0b3JDb250YWluZXIucGFyZW50Tm9kZTtcbiAgICBpZiAoXG4gICAgICByYW5nZUNvbW1vbkFuY2VzdG9yRWxlbWVudCAmJlxuICAgICAgcmFuZ2VDb21tb25BbmNlc3RvckVsZW1lbnQubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFXG4gICAgKSB7XG4gICAgICBpZiAoY29tbW9uRWxlbWVudEFuY2VzdG9yICE9PSByYW5nZUNvbW1vbkFuY2VzdG9yRWxlbWVudCkge1xuICAgICAgICBjb25zb2xlLmxvZyhcIj4+Pj4+PiBDT01NT04gQU5DRVNUT1IgQ09OVEFJTkVSIERJRkY/PyFcIik7XG4gICAgICAgIGNvbnNvbGUubG9nKGdldENzc1NlbGVjdG9yKGNvbW1vbkVsZW1lbnRBbmNlc3RvcikpO1xuICAgICAgICBjb25zb2xlLmxvZyhnZXRDc3NTZWxlY3RvcihyYW5nZUNvbW1vbkFuY2VzdG9yRWxlbWVudCkpO1xuICAgICAgfVxuICAgIH1cbiAgfVxuICBjb25zdCByb290RWxlbWVudENmaSA9IGNvbXB1dGVFbGVtZW50Q0ZJKGNvbW1vbkVsZW1lbnRBbmNlc3Rvcik7XG4gIGNvbnN0IHN0YXJ0RWxlbWVudENmaSA9IGNvbXB1dGVFbGVtZW50Q0ZJKHN0YXJ0Q29udGFpbmVyRWxlbWVudCk7XG4gIGNvbnN0IGVuZEVsZW1lbnRDZmkgPSBjb21wdXRlRWxlbWVudENGSShlbmRDb250YWluZXJFbGVtZW50KTtcbiAgbGV0IGNmaTtcbiAgaWYgKHJvb3RFbGVtZW50Q2ZpICYmIHN0YXJ0RWxlbWVudENmaSAmJiBlbmRFbGVtZW50Q2ZpKSB7XG4gICAgbGV0IHN0YXJ0RWxlbWVudE9yVGV4dENmaSA9IHN0YXJ0RWxlbWVudENmaTtcbiAgICBpZiAoIXN0YXJ0SXNFbGVtZW50KSB7XG4gICAgICBjb25zdCBzdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleEZvckNmaSA9IGdldENoaWxkVGV4dE5vZGVDZmlJbmRleChcbiAgICAgICAgc3RhcnRDb250YWluZXJFbGVtZW50LFxuICAgICAgICByYW5nZS5zdGFydENvbnRhaW5lclxuICAgICAgKTtcbiAgICAgIHN0YXJ0RWxlbWVudE9yVGV4dENmaSA9XG4gICAgICAgIHN0YXJ0RWxlbWVudENmaSArXG4gICAgICAgIFwiL1wiICtcbiAgICAgICAgc3RhcnRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXhGb3JDZmkgK1xuICAgICAgICBcIjpcIiArXG4gICAgICAgIHJhbmdlLnN0YXJ0T2Zmc2V0O1xuICAgIH0gZWxzZSB7XG4gICAgICBpZiAoXG4gICAgICAgIHJhbmdlLnN0YXJ0T2Zmc2V0ID49IDAgJiZcbiAgICAgICAgcmFuZ2Uuc3RhcnRPZmZzZXQgPCBzdGFydENvbnRhaW5lckVsZW1lbnQuY2hpbGROb2Rlcy5sZW5ndGhcbiAgICAgICkge1xuICAgICAgICBjb25zdCBjaGlsZE5vZGUgPSBzdGFydENvbnRhaW5lckVsZW1lbnQuY2hpbGROb2Rlc1tyYW5nZS5zdGFydE9mZnNldF07XG4gICAgICAgIGlmIChjaGlsZE5vZGUubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFKSB7XG4gICAgICAgICAgc3RhcnRFbGVtZW50T3JUZXh0Q2ZpID1cbiAgICAgICAgICAgIHN0YXJ0RWxlbWVudENmaSArIFwiL1wiICsgKHJhbmdlLnN0YXJ0T2Zmc2V0ICsgMSkgKiAyO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIGNvbnN0IGNmaVRleHROb2RlSW5kZXggPSBnZXRDaGlsZFRleHROb2RlQ2ZpSW5kZXgoXG4gICAgICAgICAgICBzdGFydENvbnRhaW5lckVsZW1lbnQsXG4gICAgICAgICAgICBjaGlsZE5vZGVcbiAgICAgICAgICApO1xuICAgICAgICAgIHN0YXJ0RWxlbWVudE9yVGV4dENmaSA9IHN0YXJ0RWxlbWVudENmaSArIFwiL1wiICsgY2ZpVGV4dE5vZGVJbmRleDtcbiAgICAgICAgfVxuICAgICAgfSBlbHNlIHtcbiAgICAgICAgY29uc3QgY2ZpSW5kZXhPZkxhc3RFbGVtZW50ID1cbiAgICAgICAgICBzdGFydENvbnRhaW5lckVsZW1lbnQuY2hpbGRFbGVtZW50Q291bnQgKiAyO1xuICAgICAgICBjb25zdCBsYXN0Q2hpbGROb2RlID1cbiAgICAgICAgICBzdGFydENvbnRhaW5lckVsZW1lbnQuY2hpbGROb2Rlc1tcbiAgICAgICAgICAgIHN0YXJ0Q29udGFpbmVyRWxlbWVudC5jaGlsZE5vZGVzLmxlbmd0aCAtIDFcbiAgICAgICAgICBdO1xuICAgICAgICBpZiAobGFzdENoaWxkTm9kZS5ub2RlVHlwZSA9PT0gTm9kZS5FTEVNRU5UX05PREUpIHtcbiAgICAgICAgICBzdGFydEVsZW1lbnRPclRleHRDZmkgPVxuICAgICAgICAgICAgc3RhcnRFbGVtZW50Q2ZpICsgXCIvXCIgKyAoY2ZpSW5kZXhPZkxhc3RFbGVtZW50ICsgMSk7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgc3RhcnRFbGVtZW50T3JUZXh0Q2ZpID1cbiAgICAgICAgICAgIHN0YXJ0RWxlbWVudENmaSArIFwiL1wiICsgKGNmaUluZGV4T2ZMYXN0RWxlbWVudCArIDIpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuICAgIGxldCBlbmRFbGVtZW50T3JUZXh0Q2ZpID0gZW5kRWxlbWVudENmaTtcbiAgICBpZiAoIWVuZElzRWxlbWVudCkge1xuICAgICAgY29uc3QgZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4Rm9yQ2ZpID0gZ2V0Q2hpbGRUZXh0Tm9kZUNmaUluZGV4KFxuICAgICAgICBlbmRDb250YWluZXJFbGVtZW50LFxuICAgICAgICByYW5nZS5lbmRDb250YWluZXJcbiAgICAgICk7XG4gICAgICBlbmRFbGVtZW50T3JUZXh0Q2ZpID1cbiAgICAgICAgZW5kRWxlbWVudENmaSArXG4gICAgICAgIFwiL1wiICtcbiAgICAgICAgZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4Rm9yQ2ZpICtcbiAgICAgICAgXCI6XCIgK1xuICAgICAgICByYW5nZS5lbmRPZmZzZXQ7XG4gICAgfSBlbHNlIHtcbiAgICAgIGlmIChcbiAgICAgICAgcmFuZ2UuZW5kT2Zmc2V0ID49IDAgJiZcbiAgICAgICAgcmFuZ2UuZW5kT2Zmc2V0IDwgZW5kQ29udGFpbmVyRWxlbWVudC5jaGlsZE5vZGVzLmxlbmd0aFxuICAgICAgKSB7XG4gICAgICAgIGNvbnN0IGNoaWxkTm9kZSA9IGVuZENvbnRhaW5lckVsZW1lbnQuY2hpbGROb2Rlc1tyYW5nZS5lbmRPZmZzZXRdO1xuICAgICAgICBpZiAoY2hpbGROb2RlLm5vZGVUeXBlID09PSBOb2RlLkVMRU1FTlRfTk9ERSkge1xuICAgICAgICAgIGVuZEVsZW1lbnRPclRleHRDZmkgPSBlbmRFbGVtZW50Q2ZpICsgXCIvXCIgKyAocmFuZ2UuZW5kT2Zmc2V0ICsgMSkgKiAyO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIGNvbnN0IGNmaVRleHROb2RlSW5kZXggPSBnZXRDaGlsZFRleHROb2RlQ2ZpSW5kZXgoXG4gICAgICAgICAgICBlbmRDb250YWluZXJFbGVtZW50LFxuICAgICAgICAgICAgY2hpbGROb2RlXG4gICAgICAgICAgKTtcbiAgICAgICAgICBlbmRFbGVtZW50T3JUZXh0Q2ZpID0gZW5kRWxlbWVudENmaSArIFwiL1wiICsgY2ZpVGV4dE5vZGVJbmRleDtcbiAgICAgICAgfVxuICAgICAgfSBlbHNlIHtcbiAgICAgICAgY29uc3QgY2ZpSW5kZXhPZkxhc3RFbGVtZW50ID0gZW5kQ29udGFpbmVyRWxlbWVudC5jaGlsZEVsZW1lbnRDb3VudCAqIDI7XG4gICAgICAgIGNvbnN0IGxhc3RDaGlsZE5vZGUgPVxuICAgICAgICAgIGVuZENvbnRhaW5lckVsZW1lbnQuY2hpbGROb2Rlc1tcbiAgICAgICAgICAgIGVuZENvbnRhaW5lckVsZW1lbnQuY2hpbGROb2Rlcy5sZW5ndGggLSAxXG4gICAgICAgICAgXTtcbiAgICAgICAgaWYgKGxhc3RDaGlsZE5vZGUubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFKSB7XG4gICAgICAgICAgZW5kRWxlbWVudE9yVGV4dENmaSA9XG4gICAgICAgICAgICBlbmRFbGVtZW50Q2ZpICsgXCIvXCIgKyAoY2ZpSW5kZXhPZkxhc3RFbGVtZW50ICsgMSk7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgZW5kRWxlbWVudE9yVGV4dENmaSA9XG4gICAgICAgICAgICBlbmRFbGVtZW50Q2ZpICsgXCIvXCIgKyAoY2ZpSW5kZXhPZkxhc3RFbGVtZW50ICsgMik7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG4gICAgY2ZpID1cbiAgICAgIHJvb3RFbGVtZW50Q2ZpICtcbiAgICAgIFwiLFwiICtcbiAgICAgIHN0YXJ0RWxlbWVudE9yVGV4dENmaS5yZXBsYWNlKHJvb3RFbGVtZW50Q2ZpLCBcIlwiKSArXG4gICAgICBcIixcIiArXG4gICAgICBlbmRFbGVtZW50T3JUZXh0Q2ZpLnJlcGxhY2Uocm9vdEVsZW1lbnRDZmksIFwiXCIpO1xuICB9XG4gIHJldHVybiB7XG4gICAgY2ZpLFxuICAgIGVuZENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCxcbiAgICBlbmRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3IsXG4gICAgZW5kT2Zmc2V0OiByYW5nZS5lbmRPZmZzZXQsXG4gICAgc3RhcnRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXgsXG4gICAgc3RhcnRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3IsXG4gICAgc3RhcnRPZmZzZXQ6IHJhbmdlLnN0YXJ0T2Zmc2V0LFxuICB9O1xufVxuXG5mdW5jdGlvbiBjb252ZXJ0UmFuZ2VJbmZvKGRvY3VtZW50LCByYW5nZUluZm8pIHtcbiAgY29uc3Qgc3RhcnRFbGVtZW50ID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvcihcbiAgICByYW5nZUluZm8uc3RhcnRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3JcbiAgKTtcbiAgaWYgKCFzdGFydEVsZW1lbnQpIHtcbiAgICBjb25zb2xlLmxvZyhcIl5eXiBjb252ZXJ0UmFuZ2VJbmZvIE5PIFNUQVJUIEVMRU1FTlQgQ1NTIFNFTEVDVE9SPyFcIik7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuICBsZXQgc3RhcnRDb250YWluZXIgPSBzdGFydEVsZW1lbnQ7XG4gIGlmIChyYW5nZUluZm8uc3RhcnRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXggPj0gMCkge1xuICAgIGlmIChcbiAgICAgIHJhbmdlSW5mby5zdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCA+PVxuICAgICAgc3RhcnRFbGVtZW50LmNoaWxkTm9kZXMubGVuZ3RoXG4gICAgKSB7XG4gICAgICBjb25zb2xlLmxvZyhcbiAgICAgICAgXCJeXl4gY29udmVydFJhbmdlSW5mbyByYW5nZUluZm8uc3RhcnRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXggPj0gc3RhcnRFbGVtZW50LmNoaWxkTm9kZXMubGVuZ3RoPyFcIlxuICAgICAgKTtcbiAgICAgIHJldHVybiB1bmRlZmluZWQ7XG4gICAgfVxuICAgIHN0YXJ0Q29udGFpbmVyID1cbiAgICAgIHN0YXJ0RWxlbWVudC5jaGlsZE5vZGVzW3JhbmdlSW5mby5zdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleF07XG4gICAgaWYgKHN0YXJ0Q29udGFpbmVyLm5vZGVUeXBlICE9PSBOb2RlLlRFWFRfTk9ERSkge1xuICAgICAgY29uc29sZS5sb2coXG4gICAgICAgIFwiXl5eIGNvbnZlcnRSYW5nZUluZm8gc3RhcnRDb250YWluZXIubm9kZVR5cGUgIT09IE5vZGUuVEVYVF9OT0RFPyFcIlxuICAgICAgKTtcbiAgICAgIHJldHVybiB1bmRlZmluZWQ7XG4gICAgfVxuICB9XG4gIGNvbnN0IGVuZEVsZW1lbnQgPSBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKFxuICAgIHJhbmdlSW5mby5lbmRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3JcbiAgKTtcbiAgaWYgKCFlbmRFbGVtZW50KSB7XG4gICAgY29uc29sZS5sb2coXCJeXl4gY29udmVydFJhbmdlSW5mbyBOTyBFTkQgRUxFTUVOVCBDU1MgU0VMRUNUT1I/IVwiKTtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG4gIGxldCBlbmRDb250YWluZXIgPSBlbmRFbGVtZW50O1xuICBpZiAocmFuZ2VJbmZvLmVuZENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCA+PSAwKSB7XG4gICAgaWYgKFxuICAgICAgcmFuZ2VJbmZvLmVuZENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCA+PSBlbmRFbGVtZW50LmNoaWxkTm9kZXMubGVuZ3RoXG4gICAgKSB7XG4gICAgICBjb25zb2xlLmxvZyhcbiAgICAgICAgXCJeXl4gY29udmVydFJhbmdlSW5mbyByYW5nZUluZm8uZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4ID49IGVuZEVsZW1lbnQuY2hpbGROb2Rlcy5sZW5ndGg/IVwiXG4gICAgICApO1xuICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICB9XG4gICAgZW5kQ29udGFpbmVyID1cbiAgICAgIGVuZEVsZW1lbnQuY2hpbGROb2Rlc1tyYW5nZUluZm8uZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4XTtcbiAgICBpZiAoZW5kQ29udGFpbmVyLm5vZGVUeXBlICE9PSBOb2RlLlRFWFRfTk9ERSkge1xuICAgICAgY29uc29sZS5sb2coXG4gICAgICAgIFwiXl5eIGNvbnZlcnRSYW5nZUluZm8gZW5kQ29udGFpbmVyLm5vZGVUeXBlICE9PSBOb2RlLlRFWFRfTk9ERT8hXCJcbiAgICAgICk7XG4gICAgICByZXR1cm4gdW5kZWZpbmVkO1xuICAgIH1cbiAgfVxuICByZXR1cm4gY3JlYXRlT3JkZXJlZFJhbmdlKFxuICAgIHN0YXJ0Q29udGFpbmVyLFxuICAgIHJhbmdlSW5mby5zdGFydE9mZnNldCxcbiAgICBlbmRDb250YWluZXIsXG4gICAgcmFuZ2VJbmZvLmVuZE9mZnNldFxuICApO1xufVxuXG5mdW5jdGlvbiBmcmFtZUZvckhpZ2hsaWdodEFubm90YXRpb25NYXJrV2l0aElEKHdpbiwgaWQpIHtcbiAgbGV0IGNsaWVudFJlY3RzID0gZnJhbWVGb3JIaWdobGlnaHRXaXRoSUQoaWQpO1xuICBpZiAoIWNsaWVudFJlY3RzKSByZXR1cm47XG5cbiAgdmFyIHRvcENsaWVudFJlY3QgPSBjbGllbnRSZWN0c1swXTtcbiAgdmFyIG1heEhlaWdodCA9IHRvcENsaWVudFJlY3QuaGVpZ2h0O1xuICBmb3IgKGNvbnN0IGNsaWVudFJlY3Qgb2YgY2xpZW50UmVjdHMpIHtcbiAgICBpZiAoY2xpZW50UmVjdC50b3AgPCB0b3BDbGllbnRSZWN0LnRvcCkgdG9wQ2xpZW50UmVjdCA9IGNsaWVudFJlY3Q7XG4gICAgaWYgKGNsaWVudFJlY3QuaGVpZ2h0ID4gbWF4SGVpZ2h0KSBtYXhIZWlnaHQgPSBjbGllbnRSZWN0LmhlaWdodDtcbiAgfVxuXG4gIGNvbnN0IGRvY3VtZW50ID0gd2luLmRvY3VtZW50O1xuXG4gIGNvbnN0IHNjcm9sbEVsZW1lbnQgPSBnZXRTY3JvbGxpbmdFbGVtZW50KGRvY3VtZW50KTtcbiAgY29uc3QgcGFnaW5hdGVkID0gaXNQYWdpbmF0ZWQoZG9jdW1lbnQpO1xuICBjb25zdCBib2R5UmVjdCA9IGRvY3VtZW50LmJvZHkuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCk7XG4gIGxldCB5T2Zmc2V0O1xuICBpZiAobmF2aWdhdG9yLnVzZXJBZ2VudC5tYXRjaCgvQW5kcm9pZC9pKSkge1xuICAgIHlPZmZzZXQgPSBwYWdpbmF0ZWQgPyAtc2Nyb2xsRWxlbWVudC5zY3JvbGxUb3AgOiBib2R5UmVjdC50b3A7XG4gIH0gZWxzZSBpZiAobmF2aWdhdG9yLnVzZXJBZ2VudC5tYXRjaCgvaVBob25lfGlQYWR8aVBvZC9pKSkge1xuICAgIHlPZmZzZXQgPSBwYWdpbmF0ZWQgPyAwIDogYm9keVJlY3QudG9wO1xuICB9XG4gIHZhciBuZXdUb3AgPSB0b3BDbGllbnRSZWN0LnRvcDtcblxuICBpZiAoX2hpZ2hsaWdodHNDb250YWluZXIpIHtcbiAgICBkbyB7XG4gICAgICB2YXIgYm91bmRpbmdBcmVhcyA9IGRvY3VtZW50LmdldEVsZW1lbnRzQnlDbGFzc05hbWUoXG4gICAgICAgIENMQVNTX0FOTk9UQVRJT05fQk9VTkRJTkdfQVJFQVxuICAgICAgKTtcbiAgICAgIHZhciBmb3VuZCA9IGZhbHNlO1xuICAgICAgLy9mb3IgKGxldCBpID0gMCwgbGVuZ3RoID0gYm91bmRpbmdBcmVhcy5zbmFwc2hvdExlbmd0aDsgaSA8IGxlbmd0aDsgKytpKSB7XG4gICAgICBmb3IgKFxuICAgICAgICB2YXIgaSA9IDAsIGxlbiA9IGJvdW5kaW5nQXJlYXMubGVuZ3RoIHwgMDtcbiAgICAgICAgaSA8IGxlbjtcbiAgICAgICAgaSA9IChpICsgMSkgfCAwXG4gICAgICApIHtcbiAgICAgICAgdmFyIGJvdW5kaW5nQXJlYSA9IGJvdW5kaW5nQXJlYXNbaV07XG4gICAgICAgIGlmIChNYXRoLmFicyhib3VuZGluZ0FyZWEucmVjdC50b3AgLSAobmV3VG9wIC0geU9mZnNldCkpIDwgMykge1xuICAgICAgICAgIG5ld1RvcCArPSBib3VuZGluZ0FyZWEucmVjdC5oZWlnaHQ7XG4gICAgICAgICAgZm91bmQgPSB0cnVlO1xuICAgICAgICAgIGJyZWFrO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfSB3aGlsZSAoZm91bmQpO1xuICB9XG5cbiAgdG9wQ2xpZW50UmVjdC50b3AgPSBuZXdUb3A7XG4gIHRvcENsaWVudFJlY3QuaGVpZ2h0ID0gbWF4SGVpZ2h0O1xuXG4gIHJldHVybiB0b3BDbGllbnRSZWN0O1xufVxuXG5mdW5jdGlvbiBoaWdobGlnaHRXaXRoSUQoaWQpIHtcbiAgbGV0IGkgPSAtMTtcbiAgY29uc3QgaGlnaGxpZ2h0ID0gX2hpZ2hsaWdodHMuZmluZCgoaCwgaikgPT4ge1xuICAgIGkgPSBqO1xuICAgIHJldHVybiBoLmlkID09PSBpZDtcbiAgfSk7XG4gIHJldHVybiBoaWdobGlnaHQ7XG59XG5cbmZ1bmN0aW9uIGZyYW1lRm9ySGlnaGxpZ2h0V2l0aElEKGlkKSB7XG4gIGNvbnN0IGhpZ2hsaWdodCA9IGhpZ2hsaWdodFdpdGhJRChpZCk7XG4gIGlmICghaGlnaGxpZ2h0KSByZXR1cm47XG5cbiAgY29uc3QgZG9jdW1lbnQgPSB3aW5kb3cuZG9jdW1lbnQ7XG4gIGNvbnN0IHNjcm9sbEVsZW1lbnQgPSBnZXRTY3JvbGxpbmdFbGVtZW50KGRvY3VtZW50KTtcbiAgY29uc3QgcmFuZ2UgPSBjb252ZXJ0UmFuZ2VJbmZvKGRvY3VtZW50LCBoaWdobGlnaHQucmFuZ2VJbmZvKTtcbiAgaWYgKCFyYW5nZSkge1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cblxuICBjb25zdCBkcmF3VW5kZXJsaW5lID0gZmFsc2U7XG4gIGNvbnN0IGRyYXdTdHJpa2VUaHJvdWdoID0gZmFsc2U7XG4gIGNvbnN0IGRvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHMgPSBkcmF3VW5kZXJsaW5lIHx8IGRyYXdTdHJpa2VUaHJvdWdoO1xuICAvL2NvbnN0IGNsaWVudFJlY3RzID0gREVCVUdfVklTVUFMUyA/IHJhbmdlLmdldENsaWVudFJlY3RzKCkgOlxuICBjb25zdCBjbGllbnRSZWN0cyA9IGdldENsaWVudFJlY3RzTm9PdmVybGFwKFxuICAgIHJhbmdlLFxuICAgIGRvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHNcbiAgKTtcblxuICByZXR1cm4gY2xpZW50UmVjdHM7XG59XG5cbmZ1bmN0aW9uIHJhbmdlSW5mbzJMb2NhdGlvbihyYW5nZUluZm8pIHtcbiAgcmV0dXJuIHtcbiAgICBjc3NTZWxlY3RvcjogcmFuZ2VJbmZvLnN0YXJ0Q29udGFpbmVyRWxlbWVudENzc1NlbGVjdG9yLFxuICAgIHBhcnRpYWxDZmk6IHJhbmdlSW5mby5jZmksXG4gICAgZG9tUmFuZ2U6IHtcbiAgICAgIHN0YXJ0OiB7XG4gICAgICAgIGNzc1NlbGVjdG9yOiByYW5nZUluZm8uc3RhcnRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3IsXG4gICAgICAgIHRleHROb2RlSW5kZXg6IHJhbmdlSW5mby5zdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCxcbiAgICAgICAgb2Zmc2V0OiByYW5nZUluZm8uc3RhcnRPZmZzZXQsXG4gICAgICB9LFxuICAgICAgZW5kOiB7XG4gICAgICAgIGNzc1NlbGVjdG9yOiByYW5nZUluZm8uZW5kQ29udGFpbmVyRWxlbWVudENzc1NlbGVjdG9yLFxuICAgICAgICB0ZXh0Tm9kZUluZGV4OiByYW5nZUluZm8uZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4LFxuICAgICAgICBvZmZzZXQ6IHJhbmdlSW5mby5lbmRPZmZzZXQsXG4gICAgICB9LFxuICAgIH0sXG4gIH07XG59XG5cbmZ1bmN0aW9uIGxvY2F0aW9uMlJhbmdlSW5mbyhsb2NhdGlvbikge1xuICBjb25zdCBsb2NhdGlvbnMgPSBsb2NhdGlvbi5sb2NhdGlvbnM7XG4gIGNvbnN0IGRvbVJhbmdlID0gbG9jYXRpb25zLmRvbVJhbmdlO1xuICBjb25zdCBzdGFydCA9IGRvbVJhbmdlLnN0YXJ0O1xuICBjb25zdCBlbmQgPSBkb21SYW5nZS5lbmQ7XG5cbiAgcmV0dXJuIHtcbiAgICBjZmk6IGxvY2F0aW9uLnBhcnRpYWxDZmksXG4gICAgZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4OiBlbmQudGV4dE5vZGVJbmRleCxcbiAgICBlbmRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3I6IGVuZC5jc3NTZWxlY3RvcixcbiAgICBlbmRPZmZzZXQ6IGVuZC5vZmZzZXQsXG4gICAgc3RhcnRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXg6IHN0YXJ0LnRleHROb2RlSW5kZXgsXG4gICAgc3RhcnRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3I6IHN0YXJ0LmNzc1NlbGVjdG9yLFxuICAgIHN0YXJ0T2Zmc2V0OiBzdGFydC5vZmZzZXQsXG4gIH07XG59XG5cbmV4cG9ydCBmdW5jdGlvbiByZWN0YW5nbGVGb3JIaWdobGlnaHRXaXRoSUQoaWQpIHtcbiAgY29uc3QgaGlnaGxpZ2h0ID0gaGlnaGxpZ2h0V2l0aElEKGlkKTtcbiAgaWYgKCFoaWdobGlnaHQpIHJldHVybjtcblxuICBjb25zdCBkb2N1bWVudCA9IHdpbmRvdy5kb2N1bWVudDtcbiAgY29uc3Qgc2Nyb2xsRWxlbWVudCA9IGdldFNjcm9sbGluZ0VsZW1lbnQoZG9jdW1lbnQpO1xuICBjb25zdCByYW5nZSA9IGNvbnZlcnRSYW5nZUluZm8oZG9jdW1lbnQsIGhpZ2hsaWdodC5yYW5nZUluZm8pO1xuICBpZiAoIXJhbmdlKSB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuXG4gIGNvbnN0IGRyYXdVbmRlcmxpbmUgPSBmYWxzZTtcbiAgY29uc3QgZHJhd1N0cmlrZVRocm91Z2ggPSBmYWxzZTtcbiAgY29uc3QgZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0cyA9IGRyYXdVbmRlcmxpbmUgfHwgZHJhd1N0cmlrZVRocm91Z2g7XG4gIC8vY29uc3QgY2xpZW50UmVjdHMgPSBERUJVR19WSVNVQUxTID8gcmFuZ2UuZ2V0Q2xpZW50UmVjdHMoKSA6XG4gIGNvbnN0IGNsaWVudFJlY3RzID0gZ2V0Q2xpZW50UmVjdHNOb092ZXJsYXAoXG4gICAgcmFuZ2UsXG4gICAgZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0c1xuICApO1xuICB2YXIgc2l6ZSA9IHtcbiAgICBzY3JlZW5XaWR0aDogd2luZG93Lm91dGVyV2lkdGgsXG4gICAgc2NyZWVuSGVpZ2h0OiB3aW5kb3cub3V0ZXJIZWlnaHQsXG4gICAgbGVmdDogY2xpZW50UmVjdHNbMF0ubGVmdCxcbiAgICB3aWR0aDogY2xpZW50UmVjdHNbMF0ud2lkdGgsXG4gICAgdG9wOiBjbGllbnRSZWN0c1swXS50b3AsXG4gICAgaGVpZ2h0OiBjbGllbnRSZWN0c1swXS5oZWlnaHQsXG4gIH07XG5cbiAgcmV0dXJuIHNpemU7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBnZXRTZWxlY3Rpb25SZWN0KCkge1xuICB0cnkge1xuICAgIHZhciBzZWwgPSB3aW5kb3cuZ2V0U2VsZWN0aW9uKCk7XG4gICAgaWYgKCFzZWwpIHtcbiAgICAgIHJldHVybjtcbiAgICB9XG4gICAgdmFyIHJhbmdlID0gc2VsLmdldFJhbmdlQXQoMCk7XG5cbiAgICBjb25zdCBjbGllbnRSZWN0ID0gcmFuZ2UuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCk7XG5cbiAgICB2YXIgaGFuZGxlQm91bmRzID0ge1xuICAgICAgc2NyZWVuV2lkdGg6IHdpbmRvdy5vdXRlcldpZHRoLFxuICAgICAgc2NyZWVuSGVpZ2h0OiB3aW5kb3cub3V0ZXJIZWlnaHQsXG4gICAgICBsZWZ0OiBjbGllbnRSZWN0LmxlZnQsXG4gICAgICB3aWR0aDogY2xpZW50UmVjdC53aWR0aCxcbiAgICAgIHRvcDogY2xpZW50UmVjdC50b3AsXG4gICAgICBoZWlnaHQ6IGNsaWVudFJlY3QuaGVpZ2h0LFxuICAgIH07XG4gICAgcmV0dXJuIGhhbmRsZUJvdW5kcztcbiAgfSBjYXRjaCAoZSkge1xuICAgIHJldHVybiBudWxsO1xuICB9XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBzZXRTY3JvbGxNb2RlKGZsYWcpIHtcbiAgaWYgKCFmbGFnKSB7XG4gICAgZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LmNsYXNzTGlzdC5hZGQoQ0xBU1NfUEFHSU5BVEVEKTtcbiAgfSBlbHNlIHtcbiAgICBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuY2xhc3NMaXN0LnJlbW92ZShDTEFTU19QQUdJTkFURUQpO1xuICB9XG59XG5cbi8qXG4gaWYgKGRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIpIHsgLy8gSUUgPj0gOTsgb3RoZXIgYnJvd3NlcnNcbiAgICAgICAgZG9jdW1lbnQuYWRkRXZlbnRMaXN0ZW5lcignY29udGV4dG1lbnUnLCBmdW5jdGlvbihlKSB7XG4gICAgICAgICAgICAvL2FsZXJ0KFwiWW91J3ZlIHRyaWVkIHRvIG9wZW4gY29udGV4dCBtZW51XCIpOyAvL2hlcmUgeW91IGRyYXcgeW91ciBvd24gbWVudVxuICAgICAgICAgICAgLy9lLnByZXZlbnREZWZhdWx0KCk7XG4gICAgICAgICAgICAvL2xldCBnZXRDc3NTZWxlY3RvciA9IGZ1bGxRdWFsaWZpZWRTZWxlY3RvcjtcbiAgICAgICAgICAgIFxuXHRcdFx0bGV0IHN0ciA9IHdpbmRvdy5nZXRTZWxlY3Rpb24oKTtcblx0XHRcdGxldCBzZWxlY3Rpb25JbmZvID0gZ2V0Q3VycmVudFNlbGVjdGlvbkluZm8oKTtcblx0XHRcdGxldCBwb3MgPSBjcmVhdGVIaWdobGlnaHQoc2VsZWN0aW9uSW5mbyx7cmVkOjEwLGdyZWVuOjUwLGJsdWU6MjMwfSx0cnVlKTtcblx0XHRcdGxldCByZXQyID0gY3JlYXRlQW5ub3RhdGlvbihwb3MuaWQpO1xuXHRcdFx0XG4gIH0sIGZhbHNlKTtcbiAgICB9IGVsc2UgeyAvLyBJRSA8IDlcbiAgICAgICAgZG9jdW1lbnQuYXR0YWNoRXZlbnQoJ29uY29udGV4dG1lbnUnLCBmdW5jdGlvbigpIHtcbiAgICAgICAgICAgIGFsZXJ0KFwiWW91J3ZlIHRyaWVkIHRvIG9wZW4gY29udGV4dCBtZW51XCIpO1xuICAgICAgICAgICAgd2luZG93LmV2ZW50LnJldHVyblZhbHVlID0gZmFsc2U7XG4gICAgICAgIH0pO1xuICAgIH1cbiovXG4iLCIvL1xuLy8gIENvcHlyaWdodCAyMDIyIFJlYWRpdW0gRm91bmRhdGlvbi4gQWxsIHJpZ2h0cyByZXNlcnZlZC5cbi8vICBVc2Ugb2YgdGhpcyBzb3VyY2UgY29kZSBpcyBnb3Zlcm5lZCBieSB0aGUgQlNELXN0eWxlIGxpY2Vuc2Vcbi8vICBhdmFpbGFibGUgaW4gdGhlIHRvcC1sZXZlbCBMSUNFTlNFIGZpbGUgb2YgdGhlIHByb2plY3QuXG4vL1xuXG5pbXBvcnQgeyBpc1Njcm9sbE1vZGVFbmFibGVkIH0gZnJvbSBcIi4vdXRpbHNcIjtcbmltcG9ydCB7IGdldENzc1NlbGVjdG9yIH0gZnJvbSBcImNzcy1zZWxlY3Rvci1nZW5lcmF0b3JcIjtcblxuZXhwb3J0IGZ1bmN0aW9uIGZpbmRGaXJzdFZpc2libGVMb2NhdG9yKCkge1xuICBjb25zdCBlbGVtZW50ID0gZmluZEVsZW1lbnQoZG9jdW1lbnQuYm9keSk7XG4gIHJldHVybiB7XG4gICAgaHJlZjogXCIjXCIsXG4gICAgdHlwZTogXCJhcHBsaWNhdGlvbi94aHRtbCt4bWxcIixcbiAgICBsb2NhdGlvbnM6IHtcbiAgICAgIGNzc1NlbGVjdG9yOiBnZXRDc3NTZWxlY3RvcihlbGVtZW50KSxcbiAgICB9LFxuICAgIHRleHQ6IHtcbiAgICAgIGhpZ2hsaWdodDogZWxlbWVudC50ZXh0Q29udGVudCxcbiAgICB9LFxuICB9O1xufVxuXG5mdW5jdGlvbiBmaW5kRWxlbWVudChyb290RWxlbWVudCkge1xuICBmb3IgKHZhciBpID0gMDsgaSA8IHJvb3RFbGVtZW50LmNoaWxkcmVuLmxlbmd0aDsgaSsrKSB7XG4gICAgY29uc3QgY2hpbGQgPSByb290RWxlbWVudC5jaGlsZHJlbltpXTtcbiAgICBpZiAoIXNob3VsZElnbm9yZUVsZW1lbnQoY2hpbGQpICYmIGlzRWxlbWVudFZpc2libGUoY2hpbGQpKSB7XG4gICAgICByZXR1cm4gZmluZEVsZW1lbnQoY2hpbGQpO1xuICAgIH1cbiAgfVxuICByZXR1cm4gcm9vdEVsZW1lbnQ7XG59XG5cbmZ1bmN0aW9uIGlzRWxlbWVudFZpc2libGUoZWxlbWVudCkge1xuICBpZiAocmVhZGl1bS5pc0ZpeGVkTGF5b3V0KSByZXR1cm4gdHJ1ZTtcblxuICBpZiAoZWxlbWVudCA9PT0gZG9jdW1lbnQuYm9keSB8fCBlbGVtZW50ID09PSBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQpIHtcbiAgICByZXR1cm4gdHJ1ZTtcbiAgfVxuICBpZiAoIWRvY3VtZW50IHx8ICFkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQgfHwgIWRvY3VtZW50LmJvZHkpIHtcbiAgICByZXR1cm4gZmFsc2U7XG4gIH1cblxuICBjb25zdCByZWN0ID0gZWxlbWVudC5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKTtcbiAgaWYgKGlzU2Nyb2xsTW9kZUVuYWJsZWQoKSkge1xuICAgIHJldHVybiByZWN0LmJvdHRvbSA+IDAgJiYgcmVjdC50b3AgPCB3aW5kb3cuaW5uZXJIZWlnaHQ7XG4gIH0gZWxzZSB7XG4gICAgcmV0dXJuIHJlY3QucmlnaHQgPiAwICYmIHJlY3QubGVmdCA8IHdpbmRvdy5pbm5lcldpZHRoO1xuICB9XG59XG5cbmZ1bmN0aW9uIHNob3VsZElnbm9yZUVsZW1lbnQoZWxlbWVudCkge1xuICBjb25zdCBlbFN0eWxlID0gZ2V0Q29tcHV0ZWRTdHlsZShlbGVtZW50KTtcbiAgaWYgKGVsU3R5bGUpIHtcbiAgICBjb25zdCBkaXNwbGF5ID0gZWxTdHlsZS5nZXRQcm9wZXJ0eVZhbHVlKFwiZGlzcGxheVwiKTtcbiAgICBpZiAoZGlzcGxheSAhPSBcImJsb2NrXCIpIHtcbiAgICAgIHJldHVybiB0cnVlO1xuICAgIH1cbiAgICAvLyBDYW5ub3QgYmUgcmVsaWVkIHVwb24sIGJlY2F1c2Ugd2ViIGJyb3dzZXIgZW5naW5lIHJlcG9ydHMgaW52aXNpYmxlIHdoZW4gb3V0IG9mIHZpZXcgaW5cbiAgICAvLyBzY3JvbGxlZCBjb2x1bW5zIVxuICAgIC8vIGNvbnN0IHZpc2liaWxpdHkgPSBlbFN0eWxlLmdldFByb3BlcnR5VmFsdWUoXCJ2aXNpYmlsaXR5XCIpO1xuICAgIC8vIGlmICh2aXNpYmlsaXR5ID09PSBcImhpZGRlblwiKSB7XG4gICAgLy8gICAgIHJldHVybiBmYWxzZTtcbiAgICAvLyB9XG4gICAgY29uc3Qgb3BhY2l0eSA9IGVsU3R5bGUuZ2V0UHJvcGVydHlWYWx1ZShcIm9wYWNpdHlcIik7XG4gICAgaWYgKG9wYWNpdHkgPT09IFwiMFwiKSB7XG4gICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gIH1cblxuICByZXR1cm4gZmFsc2U7XG59XG4iLCIvL1xuLy8gIENvcHlyaWdodCAyMDIxIFJlYWRpdW0gRm91bmRhdGlvbi4gQWxsIHJpZ2h0cyByZXNlcnZlZC5cbi8vICBVc2Ugb2YgdGhpcyBzb3VyY2UgY29kZSBpcyBnb3Zlcm5lZCBieSB0aGUgQlNELXN0eWxlIGxpY2Vuc2Vcbi8vICBhdmFpbGFibGUgaW4gdGhlIHRvcC1sZXZlbCBMSUNFTlNFIGZpbGUgb2YgdGhlIHByb2plY3QuXG4vL1xuXG5pbXBvcnQgeyBsb2cgYXMgbG9nTmF0aXZlLCBsb2dFcnJvciwgc25hcEN1cnJlbnRPZmZzZXQgfSBmcm9tIFwiLi91dGlsc1wiO1xuaW1wb3J0IHsgdG9OYXRpdmVSZWN0IH0gZnJvbSBcIi4vcmVjdFwiO1xuaW1wb3J0IHsgVGV4dFJhbmdlIH0gZnJvbSBcIi4vdmVuZG9yL2h5cG90aGVzaXMvYW5jaG9yaW5nL3RleHQtcmFuZ2VcIjtcblxuLy8gUG9seWZpbGwgZm9yIEFuZHJvaWQgQVBJIDI2XG5pbXBvcnQgbWF0Y2hBbGwgZnJvbSBcInN0cmluZy5wcm90b3R5cGUubWF0Y2hhbGxcIjtcbm1hdGNoQWxsLnNoaW0oKTtcblxuY29uc3QgZGVidWcgPSB0cnVlO1xuXG4vLyBOb3RpZnkgbmF0aXZlIGNvZGUgdGhhdCB0aGUgc2VsZWN0aW9uIGNoYW5nZXMuXG53aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcihcbiAgXCJsb2FkXCIsXG4gIGZ1bmN0aW9uICgpIHtcbiAgICB2YXIgaXNTZWxlY3RpbmcgPSBmYWxzZTtcbiAgICBkb2N1bWVudC5hZGRFdmVudExpc3RlbmVyKFwic2VsZWN0aW9uY2hhbmdlXCIsIGZ1bmN0aW9uICgpIHtcbiAgICAgIGNvbnN0IGNvbGxhcHNlZCA9IHdpbmRvdy5nZXRTZWxlY3Rpb24oKS5pc0NvbGxhcHNlZDtcblxuICAgICAgaWYgKGNvbGxhcHNlZCAmJiBpc1NlbGVjdGluZykge1xuICAgICAgICBpc1NlbGVjdGluZyA9IGZhbHNlO1xuICAgICAgICBBbmRyb2lkLm9uU2VsZWN0aW9uRW5kKCk7XG4gICAgICAgIC8vIFNuYXBzIHRoZSBjdXJyZW50IGNvbHVtbiBpbiBjYXNlIHRoZSB1c2VyIHNoaWZ0ZWQgdGhlIHNjcm9sbCBieSBkcmFnZ2luZyB0aGUgdGV4dCBzZWxlY3Rpb24uXG4gICAgICAgIHNuYXBDdXJyZW50T2Zmc2V0KCk7XG4gICAgICB9IGVsc2UgaWYgKCFjb2xsYXBzZWQgJiYgIWlzU2VsZWN0aW5nKSB7XG4gICAgICAgIGlzU2VsZWN0aW5nID0gdHJ1ZTtcbiAgICAgICAgQW5kcm9pZC5vblNlbGVjdGlvblN0YXJ0KCk7XG4gICAgICB9XG4gICAgfSk7XG4gIH0sXG4gIGZhbHNlXG4pO1xuXG5leHBvcnQgZnVuY3Rpb24gZ2V0Q3VycmVudFNlbGVjdGlvbigpIHtcbiAgY29uc3QgdGV4dCA9IGdldEN1cnJlbnRTZWxlY3Rpb25UZXh0KCk7XG4gIGlmICghdGV4dCkge1xuICAgIHJldHVybiBudWxsO1xuICB9XG4gIGNvbnN0IHJlY3QgPSBnZXRTZWxlY3Rpb25SZWN0KCk7XG4gIHJldHVybiB7IHRleHQsIHJlY3QgfTtcbn1cblxuZnVuY3Rpb24gZ2V0U2VsZWN0aW9uUmVjdCgpIHtcbiAgdHJ5IHtcbiAgICBsZXQgc2VsID0gd2luZG93LmdldFNlbGVjdGlvbigpO1xuICAgIGlmICghc2VsKSB7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIGxldCByYW5nZSA9IHNlbC5nZXRSYW5nZUF0KDApO1xuXG4gICAgcmV0dXJuIHRvTmF0aXZlUmVjdChyYW5nZS5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKSk7XG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBsb2dFcnJvcihlKTtcbiAgICByZXR1cm4gbnVsbDtcbiAgfVxufVxuXG5mdW5jdGlvbiBnZXRDdXJyZW50U2VsZWN0aW9uVGV4dCgpIHtcbiAgY29uc3Qgc2VsZWN0aW9uID0gd2luZG93LmdldFNlbGVjdGlvbigpO1xuICBpZiAoIXNlbGVjdGlvbikge1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgaWYgKHNlbGVjdGlvbi5pc0NvbGxhcHNlZCkge1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgY29uc3QgaGlnaGxpZ2h0ID0gc2VsZWN0aW9uLnRvU3RyaW5nKCk7XG4gIGNvbnN0IGNsZWFuSGlnaGxpZ2h0ID0gaGlnaGxpZ2h0XG4gICAgLnRyaW0oKVxuICAgIC5yZXBsYWNlKC9cXG4vZywgXCIgXCIpXG4gICAgLnJlcGxhY2UoL1xcc1xccysvZywgXCIgXCIpO1xuICBpZiAoY2xlYW5IaWdobGlnaHQubGVuZ3RoID09PSAwKSB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuICBpZiAoIXNlbGVjdGlvbi5hbmNob3JOb2RlIHx8ICFzZWxlY3Rpb24uZm9jdXNOb2RlKSB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuICBjb25zdCByYW5nZSA9XG4gICAgc2VsZWN0aW9uLnJhbmdlQ291bnQgPT09IDFcbiAgICAgID8gc2VsZWN0aW9uLmdldFJhbmdlQXQoMClcbiAgICAgIDogY3JlYXRlT3JkZXJlZFJhbmdlKFxuICAgICAgICAgIHNlbGVjdGlvbi5hbmNob3JOb2RlLFxuICAgICAgICAgIHNlbGVjdGlvbi5hbmNob3JPZmZzZXQsXG4gICAgICAgICAgc2VsZWN0aW9uLmZvY3VzTm9kZSxcbiAgICAgICAgICBzZWxlY3Rpb24uZm9jdXNPZmZzZXRcbiAgICAgICAgKTtcbiAgaWYgKCFyYW5nZSB8fCByYW5nZS5jb2xsYXBzZWQpIHtcbiAgICBsb2coXCIkJCQkJCQkJCQkJCQkJCQkJCBDQU5OT1QgR0VUIE5PTi1DT0xMQVBTRUQgU0VMRUNUSU9OIFJBTkdFPyFcIik7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuXG4gIGNvbnN0IHRleHQgPSBkb2N1bWVudC5ib2R5LnRleHRDb250ZW50O1xuICBjb25zdCB0ZXh0UmFuZ2UgPSBUZXh0UmFuZ2UuZnJvbVJhbmdlKHJhbmdlKS5yZWxhdGl2ZVRvKGRvY3VtZW50LmJvZHkpO1xuICBjb25zdCBzdGFydCA9IHRleHRSYW5nZS5zdGFydC5vZmZzZXQ7XG4gIGNvbnN0IGVuZCA9IHRleHRSYW5nZS5lbmQub2Zmc2V0O1xuXG4gIGNvbnN0IHNuaXBwZXRMZW5ndGggPSAyMDA7XG5cbiAgLy8gQ29tcHV0ZSB0aGUgdGV4dCBiZWZvcmUgdGhlIGhpZ2hsaWdodCwgaWdub3JpbmcgdGhlIGZpcnN0IFwid29yZFwiLCB3aGljaCBtaWdodCBiZSBjdXQuXG4gIGxldCBiZWZvcmUgPSB0ZXh0LnNsaWNlKE1hdGgubWF4KDAsIHN0YXJ0IC0gc25pcHBldExlbmd0aCksIHN0YXJ0KTtcbiAgbGV0IGZpcnN0V29yZFN0YXJ0ID0gYmVmb3JlLnNlYXJjaCgvXFxQe0x9XFxwe0x9L2d1KTtcbiAgaWYgKGZpcnN0V29yZFN0YXJ0ICE9PSAtMSkge1xuICAgIGJlZm9yZSA9IGJlZm9yZS5zbGljZShmaXJzdFdvcmRTdGFydCArIDEpO1xuICB9XG5cbiAgLy8gQ29tcHV0ZSB0aGUgdGV4dCBhZnRlciB0aGUgaGlnaGxpZ2h0LCBpZ25vcmluZyB0aGUgbGFzdCBcIndvcmRcIiwgd2hpY2ggbWlnaHQgYmUgY3V0LlxuICBsZXQgYWZ0ZXIgPSB0ZXh0LnNsaWNlKGVuZCwgTWF0aC5taW4odGV4dC5sZW5ndGgsIGVuZCArIHNuaXBwZXRMZW5ndGgpKTtcbiAgbGV0IGxhc3RXb3JkRW5kID0gQXJyYXkuZnJvbShhZnRlci5tYXRjaEFsbCgvXFxwe0x9XFxQe0x9L2d1KSkucG9wKCk7XG4gIGlmIChsYXN0V29yZEVuZCAhPT0gdW5kZWZpbmVkICYmIGxhc3RXb3JkRW5kLmluZGV4ID4gMSkge1xuICAgIGFmdGVyID0gYWZ0ZXIuc2xpY2UoMCwgbGFzdFdvcmRFbmQuaW5kZXggKyAxKTtcbiAgfVxuXG4gIHJldHVybiB7IGhpZ2hsaWdodCwgYmVmb3JlLCBhZnRlciB9O1xufVxuXG5mdW5jdGlvbiBjcmVhdGVPcmRlcmVkUmFuZ2Uoc3RhcnROb2RlLCBzdGFydE9mZnNldCwgZW5kTm9kZSwgZW5kT2Zmc2V0KSB7XG4gIGNvbnN0IHJhbmdlID0gbmV3IFJhbmdlKCk7XG4gIHJhbmdlLnNldFN0YXJ0KHN0YXJ0Tm9kZSwgc3RhcnRPZmZzZXQpO1xuICByYW5nZS5zZXRFbmQoZW5kTm9kZSwgZW5kT2Zmc2V0KTtcbiAgaWYgKCFyYW5nZS5jb2xsYXBzZWQpIHtcbiAgICByZXR1cm4gcmFuZ2U7XG4gIH1cbiAgbG9nKFwiPj4+IGNyZWF0ZU9yZGVyZWRSYW5nZSBDT0xMQVBTRUQgLi4uIFJBTkdFIFJFVkVSU0U/XCIpO1xuICBjb25zdCByYW5nZVJldmVyc2UgPSBuZXcgUmFuZ2UoKTtcbiAgcmFuZ2VSZXZlcnNlLnNldFN0YXJ0KGVuZE5vZGUsIGVuZE9mZnNldCk7XG4gIHJhbmdlUmV2ZXJzZS5zZXRFbmQoc3RhcnROb2RlLCBzdGFydE9mZnNldCk7XG4gIGlmICghcmFuZ2VSZXZlcnNlLmNvbGxhcHNlZCkge1xuICAgIGxvZyhcIj4+PiBjcmVhdGVPcmRlcmVkUmFuZ2UgUkFOR0UgUkVWRVJTRSBPSy5cIik7XG4gICAgcmV0dXJuIHJhbmdlO1xuICB9XG4gIGxvZyhcIj4+PiBjcmVhdGVPcmRlcmVkUmFuZ2UgUkFOR0UgUkVWRVJTRSBBTFNPIENPTExBUFNFRD8hXCIpO1xuICByZXR1cm4gdW5kZWZpbmVkO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gY29udmVydFJhbmdlSW5mbyhkb2N1bWVudCwgcmFuZ2VJbmZvKSB7XG4gIGNvbnN0IHN0YXJ0RWxlbWVudCA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoXG4gICAgcmFuZ2VJbmZvLnN0YXJ0Q29udGFpbmVyRWxlbWVudENzc1NlbGVjdG9yXG4gICk7XG4gIGlmICghc3RhcnRFbGVtZW50KSB7XG4gICAgbG9nKFwiXl5eIGNvbnZlcnRSYW5nZUluZm8gTk8gU1RBUlQgRUxFTUVOVCBDU1MgU0VMRUNUT1I/IVwiKTtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG4gIGxldCBzdGFydENvbnRhaW5lciA9IHN0YXJ0RWxlbWVudDtcbiAgaWYgKHJhbmdlSW5mby5zdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCA+PSAwKSB7XG4gICAgaWYgKFxuICAgICAgcmFuZ2VJbmZvLnN0YXJ0Q29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4ID49XG4gICAgICBzdGFydEVsZW1lbnQuY2hpbGROb2Rlcy5sZW5ndGhcbiAgICApIHtcbiAgICAgIGxvZyhcbiAgICAgICAgXCJeXl4gY29udmVydFJhbmdlSW5mbyByYW5nZUluZm8uc3RhcnRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXggPj0gc3RhcnRFbGVtZW50LmNoaWxkTm9kZXMubGVuZ3RoPyFcIlxuICAgICAgKTtcbiAgICAgIHJldHVybiB1bmRlZmluZWQ7XG4gICAgfVxuICAgIHN0YXJ0Q29udGFpbmVyID1cbiAgICAgIHN0YXJ0RWxlbWVudC5jaGlsZE5vZGVzW3JhbmdlSW5mby5zdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleF07XG4gICAgaWYgKHN0YXJ0Q29udGFpbmVyLm5vZGVUeXBlICE9PSBOb2RlLlRFWFRfTk9ERSkge1xuICAgICAgbG9nKFwiXl5eIGNvbnZlcnRSYW5nZUluZm8gc3RhcnRDb250YWluZXIubm9kZVR5cGUgIT09IE5vZGUuVEVYVF9OT0RFPyFcIik7XG4gICAgICByZXR1cm4gdW5kZWZpbmVkO1xuICAgIH1cbiAgfVxuICBjb25zdCBlbmRFbGVtZW50ID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvcihcbiAgICByYW5nZUluZm8uZW5kQ29udGFpbmVyRWxlbWVudENzc1NlbGVjdG9yXG4gICk7XG4gIGlmICghZW5kRWxlbWVudCkge1xuICAgIGxvZyhcIl5eXiBjb252ZXJ0UmFuZ2VJbmZvIE5PIEVORCBFTEVNRU5UIENTUyBTRUxFQ1RPUj8hXCIpO1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgbGV0IGVuZENvbnRhaW5lciA9IGVuZEVsZW1lbnQ7XG4gIGlmIChyYW5nZUluZm8uZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4ID49IDApIHtcbiAgICBpZiAoXG4gICAgICByYW5nZUluZm8uZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4ID49IGVuZEVsZW1lbnQuY2hpbGROb2Rlcy5sZW5ndGhcbiAgICApIHtcbiAgICAgIGxvZyhcbiAgICAgICAgXCJeXl4gY29udmVydFJhbmdlSW5mbyByYW5nZUluZm8uZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4ID49IGVuZEVsZW1lbnQuY2hpbGROb2Rlcy5sZW5ndGg/IVwiXG4gICAgICApO1xuICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICB9XG4gICAgZW5kQ29udGFpbmVyID1cbiAgICAgIGVuZEVsZW1lbnQuY2hpbGROb2Rlc1tyYW5nZUluZm8uZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4XTtcbiAgICBpZiAoZW5kQ29udGFpbmVyLm5vZGVUeXBlICE9PSBOb2RlLlRFWFRfTk9ERSkge1xuICAgICAgbG9nKFwiXl5eIGNvbnZlcnRSYW5nZUluZm8gZW5kQ29udGFpbmVyLm5vZGVUeXBlICE9PSBOb2RlLlRFWFRfTk9ERT8hXCIpO1xuICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICB9XG4gIH1cbiAgcmV0dXJuIGNyZWF0ZU9yZGVyZWRSYW5nZShcbiAgICBzdGFydENvbnRhaW5lcixcbiAgICByYW5nZUluZm8uc3RhcnRPZmZzZXQsXG4gICAgZW5kQ29udGFpbmVyLFxuICAgIHJhbmdlSW5mby5lbmRPZmZzZXRcbiAgKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGxvY2F0aW9uMlJhbmdlSW5mbyhsb2NhdGlvbikge1xuICBjb25zdCBsb2NhdGlvbnMgPSBsb2NhdGlvbi5sb2NhdGlvbnM7XG4gIGNvbnN0IGRvbVJhbmdlID0gbG9jYXRpb25zLmRvbVJhbmdlO1xuICBjb25zdCBzdGFydCA9IGRvbVJhbmdlLnN0YXJ0O1xuICBjb25zdCBlbmQgPSBkb21SYW5nZS5lbmQ7XG5cbiAgcmV0dXJuIHtcbiAgICBlbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXg6IGVuZC50ZXh0Tm9kZUluZGV4LFxuICAgIGVuZENvbnRhaW5lckVsZW1lbnRDc3NTZWxlY3RvcjogZW5kLmNzc1NlbGVjdG9yLFxuICAgIGVuZE9mZnNldDogZW5kLm9mZnNldCxcbiAgICBzdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleDogc3RhcnQudGV4dE5vZGVJbmRleCxcbiAgICBzdGFydENvbnRhaW5lckVsZW1lbnRDc3NTZWxlY3Rvcjogc3RhcnQuY3NzU2VsZWN0b3IsXG4gICAgc3RhcnRPZmZzZXQ6IHN0YXJ0Lm9mZnNldCxcbiAgfTtcbn1cblxuZnVuY3Rpb24gbG9nKCkge1xuICBpZiAoZGVidWcpIHtcbiAgICBsb2dOYXRpdmUuYXBwbHkobnVsbCwgYXJndW1lbnRzKTtcbiAgfVxufVxuIiwiLy9cbi8vICBDb3B5cmlnaHQgMjAyMSBSZWFkaXVtIEZvdW5kYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuXG4vLyAgVXNlIG9mIHRoaXMgc291cmNlIGNvZGUgaXMgZ292ZXJuZWQgYnkgdGhlIEJTRC1zdHlsZSBsaWNlbnNlXG4vLyAgYXZhaWxhYmxlIGluIHRoZSB0b3AtbGV2ZWwgTElDRU5TRSBmaWxlIG9mIHRoZSBwcm9qZWN0LlxuLy9cblxuLy8gQmFzZSBzY3JpcHQgdXNlZCBieSBib3RoIHJlZmxvd2FibGUgYW5kIGZpeGVkIGxheW91dCByZXNvdXJjZXMuXG5cbmltcG9ydCBcIi4vZ2VzdHVyZXNcIjtcbmltcG9ydCB7XG4gIHJlbW92ZVByb3BlcnR5LFxuICBzY3JvbGxMZWZ0LFxuICBzY3JvbGxSaWdodCxcbiAgc2Nyb2xsVG9FbmQsXG4gIHNjcm9sbFRvSWQsXG4gIHNjcm9sbFRvUG9zaXRpb24sXG4gIHNjcm9sbFRvU3RhcnQsXG4gIHNjcm9sbFRvVGV4dCxcbiAgc2V0UHJvcGVydHksXG4gIHNldENTU1Byb3BlcnRpZXMsXG59IGZyb20gXCIuL3V0aWxzXCI7XG5pbXBvcnQge1xuICBjcmVhdGVBbm5vdGF0aW9uLFxuICBjcmVhdGVIaWdobGlnaHQsXG4gIGRlc3Ryb3lIaWdobGlnaHQsXG4gIGdldEN1cnJlbnRTZWxlY3Rpb25JbmZvLFxuICBnZXRTZWxlY3Rpb25SZWN0LFxuICByZWN0YW5nbGVGb3JIaWdobGlnaHRXaXRoSUQsXG4gIHNldFNjcm9sbE1vZGUsXG59IGZyb20gXCIuL2hpZ2hsaWdodFwiO1xuaW1wb3J0IHsgZmluZEZpcnN0VmlzaWJsZUxvY2F0b3IgfSBmcm9tIFwiLi9kb21cIjtcbmltcG9ydCB7IGdldEN1cnJlbnRTZWxlY3Rpb24gfSBmcm9tIFwiLi9zZWxlY3Rpb25cIjtcbmltcG9ydCB7IGdldERlY29yYXRpb25zLCByZWdpc3RlclRlbXBsYXRlcyB9IGZyb20gXCIuL2RlY29yYXRvclwiO1xuXG4vLyBQdWJsaWMgQVBJIHVzZWQgYnkgdGhlIG5hdmlnYXRvci5cbndpbmRvdy5yZWFkaXVtID0ge1xuICAvLyB1dGlsc1xuICBzY3JvbGxUb0lkOiBzY3JvbGxUb0lkLFxuICBzY3JvbGxUb1Bvc2l0aW9uOiBzY3JvbGxUb1Bvc2l0aW9uLFxuICBzY3JvbGxUb1RleHQ6IHNjcm9sbFRvVGV4dCxcbiAgc2Nyb2xsTGVmdDogc2Nyb2xsTGVmdCxcbiAgc2Nyb2xsUmlnaHQ6IHNjcm9sbFJpZ2h0LFxuICBzY3JvbGxUb1N0YXJ0OiBzY3JvbGxUb1N0YXJ0LFxuICBzY3JvbGxUb0VuZDogc2Nyb2xsVG9FbmQsXG4gIHNldENTU1Byb3BlcnRpZXM6IHNldENTU1Byb3BlcnRpZXMsXG4gIHNldFByb3BlcnR5OiBzZXRQcm9wZXJ0eSxcbiAgcmVtb3ZlUHJvcGVydHk6IHJlbW92ZVByb3BlcnR5LFxuXG4gIC8vIHNlbGVjdGlvblxuICBnZXRDdXJyZW50U2VsZWN0aW9uOiBnZXRDdXJyZW50U2VsZWN0aW9uLFxuXG4gIC8vIGRlY29yYXRpb25cbiAgcmVnaXN0ZXJEZWNvcmF0aW9uVGVtcGxhdGVzOiByZWdpc3RlclRlbXBsYXRlcyxcbiAgZ2V0RGVjb3JhdGlvbnM6IGdldERlY29yYXRpb25zLFxuXG4gIC8vIERPTVxuICBmaW5kRmlyc3RWaXNpYmxlTG9jYXRvcjogZmluZEZpcnN0VmlzaWJsZUxvY2F0b3IsXG59O1xuXG4vLyBMZWdhY3kgaGlnaGxpZ2h0cyBBUEkuXG53aW5kb3cuY3JlYXRlQW5ub3RhdGlvbiA9IGNyZWF0ZUFubm90YXRpb247XG53aW5kb3cuY3JlYXRlSGlnaGxpZ2h0ID0gY3JlYXRlSGlnaGxpZ2h0O1xud2luZG93LmRlc3Ryb3lIaWdobGlnaHQgPSBkZXN0cm95SGlnaGxpZ2h0O1xud2luZG93LmdldEN1cnJlbnRTZWxlY3Rpb25JbmZvID0gZ2V0Q3VycmVudFNlbGVjdGlvbkluZm87XG53aW5kb3cuZ2V0U2VsZWN0aW9uUmVjdCA9IGdldFNlbGVjdGlvblJlY3Q7XG53aW5kb3cucmVjdGFuZ2xlRm9ySGlnaGxpZ2h0V2l0aElEID0gcmVjdGFuZ2xlRm9ySGlnaGxpZ2h0V2l0aElEO1xud2luZG93LnNldFNjcm9sbE1vZGUgPSBzZXRTY3JvbGxNb2RlO1xuIiwiLy9cbi8vICBDb3B5cmlnaHQgMjAyMSBSZWFkaXVtIEZvdW5kYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuXG4vLyAgVXNlIG9mIHRoaXMgc291cmNlIGNvZGUgaXMgZ292ZXJuZWQgYnkgdGhlIEJTRC1zdHlsZSBsaWNlbnNlXG4vLyAgYXZhaWxhYmxlIGluIHRoZSB0b3AtbGV2ZWwgTElDRU5TRSBmaWxlIG9mIHRoZSBwcm9qZWN0LlxuLy9cblxuLy8gU2NyaXB0IHVzZWQgZm9yIGZpeGVkIGxheW91dHMgcmVzb3VyY2VzLlxuXG5pbXBvcnQgXCIuL2luZGV4XCI7XG5cbndpbmRvdy5yZWFkaXVtLmlzRml4ZWRMYXlvdXQgPSB0cnVlO1xuIl0sIm5hbWVzIjpbImFwcHJveFNlYXJjaCIsInNlYXJjaCIsInRleHQiLCJzdHIiLCJtYXhFcnJvcnMiLCJtYXRjaFBvcyIsImV4YWN0TWF0Y2hlcyIsImluZGV4T2YiLCJwdXNoIiwic3RhcnQiLCJlbmQiLCJsZW5ndGgiLCJlcnJvcnMiLCJ0ZXh0TWF0Y2hTY29yZSIsIm1hdGNoZXMiLCJtYXRjaFF1b3RlIiwicXVvdGUiLCJjb250ZXh0IiwiTWF0aCIsIm1pbiIsInNjb3JlTWF0Y2giLCJtYXRjaCIsInF1b3RlV2VpZ2h0IiwicHJlZml4V2VpZ2h0Iiwic3VmZml4V2VpZ2h0IiwicG9zV2VpZ2h0IiwicXVvdGVTY29yZSIsInByZWZpeFNjb3JlIiwicHJlZml4Iiwic2xpY2UiLCJtYXgiLCJzdWZmaXhTY29yZSIsInN1ZmZpeCIsInBvc1Njb3JlIiwiaGludCIsIm9mZnNldCIsImFicyIsInJhd1Njb3JlIiwibWF4U2NvcmUiLCJub3JtYWxpemVkU2NvcmUiLCJzY29yZWRNYXRjaGVzIiwibWFwIiwibSIsInNjb3JlIiwic29ydCIsImEiLCJiIiwibm9kZVRleHRMZW5ndGgiLCJub2RlIiwibm9kZVR5cGUiLCJOb2RlIiwiRUxFTUVOVF9OT0RFIiwiVEVYVF9OT0RFIiwidGV4dENvbnRlbnQiLCJwcmV2aW91c1NpYmxpbmdzVGV4dExlbmd0aCIsInNpYmxpbmciLCJwcmV2aW91c1NpYmxpbmciLCJyZXNvbHZlT2Zmc2V0cyIsImVsZW1lbnQiLCJvZmZzZXRzIiwibmV4dE9mZnNldCIsInNoaWZ0Iiwibm9kZUl0ZXIiLCJvd25lckRvY3VtZW50IiwiY3JlYXRlTm9kZUl0ZXJhdG9yIiwiTm9kZUZpbHRlciIsIlNIT1dfVEVYVCIsInJlc3VsdHMiLCJjdXJyZW50Tm9kZSIsIm5leHROb2RlIiwidGV4dE5vZGUiLCJ1bmRlZmluZWQiLCJkYXRhIiwiUmFuZ2VFcnJvciIsIlJFU09MVkVfRk9SV0FSRFMiLCJSRVNPTFZFX0JBQ0tXQVJEUyIsIlRleHRQb3NpdGlvbiIsIkVycm9yIiwicGFyZW50IiwiY29udGFpbnMiLCJlbCIsInBhcmVudEVsZW1lbnQiLCJvcHRpb25zIiwiZXJyIiwiZGlyZWN0aW9uIiwidHciLCJkb2N1bWVudCIsImNyZWF0ZVRyZWVXYWxrZXIiLCJnZXRSb290Tm9kZSIsImZvcndhcmRzIiwicHJldmlvdXNOb2RlIiwiZnJvbVBvaW50IiwidGV4dE9mZnNldCIsImNoaWxkTm9kZXMiLCJpIiwiVGV4dFJhbmdlIiwicmVsYXRpdmVUbyIsInJlc29sdmUiLCJyYW5nZSIsIlJhbmdlIiwic2V0U3RhcnQiLCJzZXRFbmQiLCJzdGFydENvbnRhaW5lciIsInN0YXJ0T2Zmc2V0IiwiZW5kQ29udGFpbmVyIiwiZW5kT2Zmc2V0Iiwicm9vdCIsIm5vZGVGcm9tWFBhdGgiLCJ4cGF0aEZyb21Ob2RlIiwiUmFuZ2VBbmNob3IiLCJub3JtYWxpemVkUmFuZ2UiLCJmcm9tUmFuZ2UiLCJ0b1JhbmdlIiwidGV4dFJhbmdlIiwidHlwZSIsInNlbGVjdG9yIiwic3RhcnRQb3MiLCJmcm9tQ2hhck9mZnNldCIsImVuZFBvcyIsIlRleHRQb3NpdGlvbkFuY2hvciIsImZyb21PZmZzZXRzIiwiVGV4dFF1b3RlQW5jaG9yIiwiZXhhY3QiLCJ0b1Bvc2l0aW9uQW5jaG9yIiwiY29udGV4dExlbiIsIndpbmRvdyIsImFkZEV2ZW50TGlzdGVuZXIiLCJldmVudCIsIkFuZHJvaWQiLCJsb2dFcnJvciIsIm1lc3NhZ2UiLCJmaWxlbmFtZSIsImxpbmVubyIsIm9ic2VydmVyIiwiUmVzaXplT2JzZXJ2ZXIiLCJvblZpZXdwb3J0V2lkdGhDaGFuZ2VkIiwic25hcEN1cnJlbnRPZmZzZXQiLCJvYnNlcnZlIiwiYm9keSIsImFwcGVuZFZpcnR1YWxDb2x1bW5JZk5lZWRlZCIsImlkIiwidmlydHVhbENvbCIsImdldEVsZW1lbnRCeUlkIiwiaXNTY3JvbGxNb2RlRW5hYmxlZCIsImdldENvbHVtbkNvdW50UGVyU2NyZWVuIiwicmVtb3ZlIiwiZG9jdW1lbnRXaWR0aCIsInNjcm9sbGluZ0VsZW1lbnQiLCJzY3JvbGxXaWR0aCIsImNvbENvdW50IiwicGFnZVdpZHRoIiwiaGFzT2RkQ29sQ291bnQiLCJyb3VuZCIsImNyZWF0ZUVsZW1lbnQiLCJzZXRBdHRyaWJ1dGUiLCJzdHlsZSIsImJyZWFrQmVmb3JlIiwiaW5uZXJIVE1MIiwiYXBwZW5kQ2hpbGQiLCJ3aWR0aCIsImdldFZpZXdwb3J0V2lkdGgiLCJkZXZpY2VQaXhlbFJhdGlvIiwic2V0UHJvcGVydHkiLCJwYXJzZUludCIsImdldENvbXB1dGVkU3R5bGUiLCJkb2N1bWVudEVsZW1lbnQiLCJnZXRQcm9wZXJ0eVZhbHVlIiwidHJpbSIsImlzUlRMIiwiZGlyIiwidG9Mb3dlckNhc2UiLCJzY3JvbGxUb0lkIiwic2Nyb2xsVG9SZWN0IiwiZ2V0Qm91bmRpbmdDbGllbnRSZWN0Iiwic2Nyb2xsVG9Qb3NpdGlvbiIsInBvc2l0aW9uIiwic2Nyb2xsSGVpZ2h0Iiwic2Nyb2xsVG9wIiwiZmFjdG9yIiwic2Nyb2xsTGVmdCIsInNuYXBPZmZzZXQiLCJzY3JvbGxUb1RleHQiLCJyYW5nZUZyb21Mb2NhdG9yIiwic2Nyb2xsVG9SYW5nZSIsInJlY3QiLCJ0b3AiLCJzY3JvbGxZIiwibGVmdCIsInNjcm9sbFgiLCJzY3JvbGxUb1N0YXJ0Iiwic2Nyb2xsVG8iLCJzY3JvbGxUb0VuZCIsIm1pbk9mZnNldCIsInNjcm9sbFRvT2Zmc2V0Iiwic2Nyb2xsUmlnaHQiLCJtYXhPZmZzZXQiLCJjdXJyZW50T2Zmc2V0IiwiZGlmZiIsInZhbHVlIiwiZGVsdGEiLCJsb2NhdG9yIiwibG9jYXRpb25zIiwiaGlnaGxpZ2h0IiwiY3NzU2VsZWN0b3IiLCJxdWVyeVNlbGVjdG9yIiwiYW5jaG9yIiwiYmVmb3JlIiwiYWZ0ZXIiLCJmcmFnbWVudHMiLCJodG1sSWQiLCJjcmVhdGVSYW5nZSIsInNldFN0YXJ0QmVmb3JlIiwic2V0RW5kQWZ0ZXIiLCJlIiwic2V0Q1NTUHJvcGVydGllcyIsInByb3BlcnRpZXMiLCJuYW1lIiwia2V5IiwicmVtb3ZlUHJvcGVydHkiLCJsb2ciLCJBcnJheSIsInByb3RvdHlwZSIsImNhbGwiLCJhcmd1bWVudHMiLCJqb2luIiwibG9nTmF0aXZlIiwiZGVidWciLCJ0b05hdGl2ZVJlY3QiLCJwaXhlbFJhdGlvIiwiaGVpZ2h0IiwicmlnaHQiLCJib3R0b20iLCJnZXRDbGllbnRSZWN0c05vT3ZlcmxhcCIsImRvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHMiLCJjbGllbnRSZWN0cyIsImdldENsaWVudFJlY3RzIiwidG9sZXJhbmNlIiwib3JpZ2luYWxSZWN0cyIsInJhbmdlQ2xpZW50UmVjdCIsIm1lcmdlZFJlY3RzIiwibWVyZ2VUb3VjaGluZ1JlY3RzIiwibm9Db250YWluZWRSZWN0cyIsInJlbW92ZUNvbnRhaW5lZFJlY3RzIiwibmV3UmVjdHMiLCJyZXBsYWNlT3ZlcmxhcGluZ1JlY3RzIiwibWluQXJlYSIsImoiLCJiaWdFbm91Z2giLCJzcGxpY2UiLCJyZWN0cyIsInJlY3QxIiwicmVjdDIiLCJyZWN0c0xpbmVVcFZlcnRpY2FsbHkiLCJhbG1vc3RFcXVhbCIsInJlY3RzTGluZVVwSG9yaXpvbnRhbGx5IiwiaG9yaXpvbnRhbEFsbG93ZWQiLCJhbGlnbmVkIiwiY2FuTWVyZ2UiLCJyZWN0c1RvdWNoT3JPdmVybGFwIiwiZmlsdGVyIiwicmVwbGFjZW1lbnRDbGllbnRSZWN0IiwiZ2V0Qm91bmRpbmdSZWN0IiwicmVjdHNUb0tlZXAiLCJTZXQiLCJkZWxldGUiLCJwb3NzaWJseUNvbnRhaW5pbmdSZWN0IiwiaGFzIiwicmVjdENvbnRhaW5zIiwiZnJvbSIsInJlY3RDb250YWluc1BvaW50IiwieCIsInkiLCJ0b0FkZCIsInRvUmVtb3ZlIiwic3VidHJhY3RSZWN0czEiLCJyZWN0U3VidHJhY3QiLCJzdWJ0cmFjdFJlY3RzMiIsImFwcGx5IiwicmVjdEludGVyc2VjdGVkIiwicmVjdEludGVyc2VjdCIsInJlY3RBIiwicmVjdEIiLCJyZWN0QyIsInJlY3REIiwibWF4TGVmdCIsIm1pblJpZ2h0IiwibWF4VG9wIiwibWluQm90dG9tIiwic3R5bGVzIiwiTWFwIiwiZ3JvdXBzIiwibGFzdEdyb3VwSWQiLCJyZWdpc3RlclRlbXBsYXRlcyIsIm5ld1N0eWxlcyIsInN0eWxlc2hlZXQiLCJPYmplY3QiLCJlbnRyaWVzIiwic2V0Iiwic3R5bGVFbGVtZW50IiwiZ2V0RWxlbWVudHNCeVRhZ05hbWUiLCJnZXREZWNvcmF0aW9ucyIsImdyb3VwTmFtZSIsImdyb3VwIiwiZ2V0IiwiRGVjb3JhdGlvbkdyb3VwIiwiaGFuZGxlRGVjb3JhdGlvbkNsaWNrRXZlbnQiLCJjbGlja0V2ZW50Iiwic2l6ZSIsImZpbmRUYXJnZXQiLCJncm91cENvbnRlbnQiLCJpdGVtcyIsInJldmVyc2UiLCJpdGVtIiwiY2xpY2thYmxlRWxlbWVudHMiLCJ0b0pTT04iLCJjbGllbnRYIiwiY2xpZW50WSIsInRhcmdldCIsIm9uRGVjb3JhdGlvbkFjdGl2YXRlZCIsIkpTT04iLCJzdHJpbmdpZnkiLCJkZWNvcmF0aW9uIiwiY2xpY2siLCJncm91cElkIiwibGFzdEl0ZW1JZCIsImNvbnRhaW5lciIsImFkZCIsImxheW91dCIsImRlY29yYXRpb25JZCIsImluZGV4IiwiZmluZEluZGV4IiwidXBkYXRlIiwiY2xlYXIiLCJjbGVhckNvbnRhaW5lciIsInJlcXVlc3RMYXlvdXQiLCJmb3JFYWNoIiwiZ3JvdXBDb250YWluZXIiLCJyZXF1aXJlQ29udGFpbmVyIiwiaXRlbUNvbnRhaW5lciIsInZpZXdwb3J0V2lkdGgiLCJpbm5lcldpZHRoIiwiY29sdW1uQ291bnQiLCJ4T2Zmc2V0IiwieU9mZnNldCIsInBvc2l0aW9uRWxlbWVudCIsImJvdW5kaW5nUmVjdCIsImZsb29yIiwiZWxlbWVudFRlbXBsYXRlIiwidGVtcGxhdGUiLCJjb250ZW50IiwiZmlyc3RFbGVtZW50Q2hpbGQiLCJlcnJvciIsInIxIiwicjIiLCJjbGllbnRSZWN0IiwibGluZSIsImNsb25lTm9kZSIsImFwcGVuZCIsImJvdW5kcyIsInF1ZXJ5U2VsZWN0b3JBbGwiLCJjaGlsZHJlbiIsImxhc3RTaXplIiwiY2xpZW50V2lkdGgiLCJjbGllbnRIZWlnaHQiLCJvbkNsaWNrIiwiYmluZERyYWdHZXN0dXJlIiwiZ2V0U2VsZWN0aW9uIiwiaXNDb2xsYXBzZWQiLCJkZWZhdWx0UHJldmVudGVkIiwidGFyZ2V0RWxlbWVudCIsIm91dGVySFRNTCIsImludGVyYWN0aXZlRWxlbWVudCIsIm5lYXJlc3RJbnRlcmFjdGl2ZUVsZW1lbnQiLCJzaG91bGRQcmV2ZW50RGVmYXVsdCIsIm9uVGFwIiwic3RvcFByb3BhZ2F0aW9uIiwicHJldmVudERlZmF1bHQiLCJvblN0YXJ0IiwicGFzc2l2ZSIsIm9uRW5kIiwib25Nb3ZlIiwic3RhdGUiLCJpc1N0YXJ0aW5nRHJhZyIsInN0YXJ0WCIsInRvdWNoZXMiLCJzdGFydFkiLCJjdXJyZW50WCIsImN1cnJlbnRZIiwib2Zmc2V0WCIsIm9mZnNldFkiLCJvbkRyYWdTdGFydCIsIm9uRHJhZ01vdmUiLCJvbkRyYWdFbmQiLCJpbnRlcmFjdGl2ZVRhZ3MiLCJub2RlTmFtZSIsImhhc0F0dHJpYnV0ZSIsImdldEF0dHJpYnV0ZSIsIlJPT1RfQ0xBU1NfUkVEVUNFX01PVElPTiIsIlJPT1RfQ0xBU1NfTk9fRk9PVE5PVEVTIiwiUE9QVVBfRElBTE9HX0NMQVNTIiwiRk9PVE5PVEVTX0NPTlRBSU5FUl9DTEFTUyIsIkZPT1ROT1RFU19DTE9TRV9CVVRUT05fQ0xBU1MiLCJGT09UTk9URV9GT1JDRV9TSE9XIiwiVFRTX0lEX1BSRVZJT1VTIiwiVFRTX0lEX05FWFQiLCJUVFNfSURfU0xJREVSIiwiVFRTX0lEX0FDVElWRV9XT1JEIiwiVFRTX0lEX0NPTlRBSU5FUiIsIlRUU19JRF9JTkZPIiwiVFRTX05BVl9CVVRUT05fQ0xBU1MiLCJUVFNfSURfU1BFQUtJTkdfRE9DX0VMRU1FTlQiLCJUVFNfQ0xBU1NfSU5KRUNURURfU1BBTiIsIlRUU19DTEFTU19JTkpFQ1RFRF9TVUJTUEFOIiwiVFRTX0lEX0lOSkVDVEVEX1BBUkVOVCIsIklEX0hJR0hMSUdIVFNfQ09OVEFJTkVSIiwiSURfQU5OT1RBVElPTl9DT05UQUlORVIiLCJDTEFTU19ISUdITElHSFRfQ09OVEFJTkVSIiwiQ0xBU1NfQU5OT1RBVElPTl9DT05UQUlORVIiLCJDTEFTU19ISUdITElHSFRfQVJFQSIsIkNMQVNTX0FOTk9UQVRJT05fQVJFQSIsIkNMQVNTX0hJR0hMSUdIVF9CT1VORElOR19BUkVBIiwiQ0xBU1NfQU5OT1RBVElPTl9CT1VORElOR19BUkVBIiwiX2JsYWNrbGlzdElkQ2xhc3NGb3JDRkkiLCJDTEFTU19QQUdJTkFURUQiLCJJU19ERVYiLCJfaGlnaGxpZ2h0cyIsIl9oaWdobGlnaHRzQ29udGFpbmVyIiwiX2Fubm90YXRpb25Db250YWluZXIiLCJsYXN0TW91c2VEb3duWCIsImxhc3RNb3VzZURvd25ZIiwiYm9keUV2ZW50TGlzdGVuZXJzU2V0IiwiVVNFX1NWRyIsIkRFRkFVTFRfQkFDS0dST1VORF9DT0xPUl9PUEFDSVRZIiwiQUxUX0JBQ0tHUk9VTkRfQ09MT1JfT1BBQ0lUWSIsIkRFQlVHX1ZJU1VBTFMiLCJERUZBVUxUX0JBQ0tHUk9VTkRfQ09MT1IiLCJibHVlIiwiZ3JlZW4iLCJyZWQiLCJBTk5PVEFUSU9OX1dJRFRIIiwicmVzZXRIaWdobGlnaHRCb3VuZGluZ1N0eWxlIiwiX3dpbiIsImhpZ2hsaWdodEJvdW5kaW5nIiwib3V0bGluZSIsInNldEhpZ2hsaWdodEFyZWFTdHlsZSIsIndpbiIsImhpZ2hsaWdodEFyZWFzIiwidXNlU1ZHIiwiaGlnaGxpZ2h0QXJlYSIsImlzU1ZHIiwibmFtZXNwYWNlVVJJIiwiU1ZHX1hNTF9OQU1FU1BBQ0UiLCJvcGFjaXR5IiwiY29sb3IiLCJyZXNldEhpZ2hsaWdodEFyZWFTdHlsZSIsInBhcmVudE5vZGUiLCJmaW5kIiwiaCIsInByb2Nlc3NUb3VjaEV2ZW50IiwiZXYiLCJzY3JvbGxFbGVtZW50IiwiZ2V0U2Nyb2xsaW5nRWxlbWVudCIsImNoYW5nZWRUb3VjaGVzIiwicGFnaW5hdGVkIiwiaXNQYWdpbmF0ZWQiLCJib2R5UmVjdCIsIm5hdmlnYXRvciIsInVzZXJBZ2VudCIsImZvdW5kSGlnaGxpZ2h0IiwiZm91bmRFbGVtZW50IiwiZm91bmRSZWN0IiwiaGlnaGxpZ2h0UGFyZW50IiwiaGl0IiwiaGlnaGxpZ2h0RnJhZ21lbnRzIiwiaGlnaGxpZ2h0RnJhZ21lbnQiLCJ3aXRoUmVjdCIsImhpZ2hsaWdodEJvdW5kaW5ncyIsImFsbEhpZ2hsaWdodEFyZWFzIiwiZm91bmRFbGVtZW50SGlnaGxpZ2h0QXJlYXMiLCJmb3VuZEVsZW1lbnRIaWdobGlnaHRCb3VuZGluZyIsImFsbEhpZ2hsaWdodEJvdW5kaW5ncyIsInNldEhpZ2hsaWdodEJvdW5kaW5nU3R5bGUiLCJzY3JlZW5XaWR0aCIsIm91dGVyV2lkdGgiLCJzY3JlZW5IZWlnaHQiLCJvdXRlckhlaWdodCIsInBheWxvYWQiLCJwcm9jZXNzIiwiZWxlY3Ryb25fMSIsImlwY1JlbmRlcmVyIiwic2VuZFRvSG9zdCIsIlIyX0VWRU5UX0hJR0hMSUdIVF9DTElDSyIsIndlYmtpdFVSTCIsImNvbnNvbGUiLCJpbmNsdWRlcyIsImhpZ2hsaWdodEFubm90YXRpb25NYXJrQWN0aXZhdGVkIiwid2Via2l0IiwibWVzc2FnZUhhbmRsZXJzIiwicG9zdE1lc3NhZ2UiLCJoaWdobGlnaHRBY3RpdmF0ZWQiLCJwcm9jZXNzTW91c2VFdmVudCIsInRvdWNoZWRQb3NpdGlvbiIsImlubmVySGVpZ2h0IiwidG9QcmVzZXJ2ZSIsInRvQ2hlY2siLCJjaGVja092ZXJsYXBzIiwic3RpbGxPdmVybGFwaW5nUmVjdHMiLCJoYXMxIiwiaGFzMiIsInhPdmVybGFwIiwiZ2V0UmVjdE92ZXJsYXBYIiwieU92ZXJsYXAiLCJnZXRSZWN0T3ZlcmxhcFkiLCJyYW5nZUNsaWVudFJlY3RzIiwiZ2V0Q2xpZW50UmVjdHNOb092ZXJsYXBfIiwiY2xhc3NMaXN0IiwiZW5zdXJlQ29udGFpbmVyIiwiYW5ub3RhdGlvbkZsYWciLCJ0b3VjaEVuZCIsImhpZGVBbGxoaWdobGlnaHRzIiwiZGVzdHJveUFsbGhpZ2hsaWdodHMiLCJkZXN0cm95SGlnaGxpZ2h0IiwiX2RvY3VtZW50IiwiaGlnaGxpZ2h0Q29udGFpbmVyIiwiaXNDZmlUZXh0Tm9kZSIsImdldENoaWxkVGV4dE5vZGVDZmlJbmRleCIsImNoaWxkIiwiZm91bmQiLCJ0ZXh0Tm9kZUluZGV4IiwicHJldmlvdXNXYXNFbGVtZW50IiwiY2hpbGROb2RlIiwiaXNUZXh0IiwiZ2V0Q29tbW9uQW5jZXN0b3JFbGVtZW50Iiwibm9kZTEiLCJub2RlMiIsIm5vZGUxRWxlbWVudEFuY2VzdG9yQ2hhaW4iLCJub2RlMkVsZW1lbnRBbmNlc3RvckNoYWluIiwiY29tbW9uQW5jZXN0b3IiLCJub2RlMUVsZW1lbnRBbmNlc3RvciIsIm5vZGUyRWxlbWVudEFuY2VzdG9yIiwiZnVsbFF1YWxpZmllZFNlbGVjdG9yIiwibG93ZXJDYXNlTmFtZSIsImxvY2FsTmFtZSIsImNzc1BhdGgiLCJnZXRDdXJyZW50U2VsZWN0aW9uSW5mbyIsInNlbGVjdGlvbiIsInJhd1RleHQiLCJ0b1N0cmluZyIsImNsZWFuVGV4dCIsInJlcGxhY2UiLCJhbmNob3JOb2RlIiwiZm9jdXNOb2RlIiwicmFuZ2VDb3VudCIsImdldFJhbmdlQXQiLCJjcmVhdGVPcmRlcmVkUmFuZ2UiLCJhbmNob3JPZmZzZXQiLCJmb2N1c09mZnNldCIsImNvbGxhcHNlZCIsInJhbmdlSW5mbyIsImNvbnZlcnRSYW5nZSIsImNvbXB1dGVDRkkiLCJyZXN0b3JlZFJhbmdlIiwiY29udmVydFJhbmdlSW5mbyIsImR1bXBEZWJ1ZyIsImdldENzc1NlbGVjdG9yIiwicmFuZ2VJbmZvMkxvY2F0aW9uIiwiY2hlY2tCbGFja2xpc3RlZCIsImJsYWNrbGlzdGVkSWQiLCJibGFja2xpc3RlZENsYXNzIiwib3B0aW1pemVkIiwic3RlcHMiLCJjb250ZXh0Tm9kZSIsInN0ZXAiLCJfY3NzUGF0aFN0ZXAiLCJpc1RhcmdldE5vZGUiLCJwcmVmaXhlZEVsZW1lbnRDbGFzc05hbWVzIiwibmQiLCJjbGFzc0F0dHJpYnV0ZSIsInNwbGl0IiwiQm9vbGVhbiIsIm5tIiwiaWRTZWxlY3RvciIsImlkZCIsImVzY2FwZUlkZW50aWZpZXJJZk5lZWRlZCIsImlkZW50IiwiaXNDU1NJZGVudGlmaWVyIiwic2hvdWxkRXNjYXBlRmlyc3QiLCJ0ZXN0IiwibGFzdEluZGV4IiwiYyIsImlpIiwiaXNDU1NJZGVudENoYXIiLCJlc2NhcGVBc2NpaUNoYXIiLCJpc0xhc3QiLCJ0b0hleEJ5dGUiLCJoZXhCeXRlIiwiY2hhckNvZGVBdCIsIkRPQ1VNRU5UX05PREUiLCJwcmVmaXhlZE93bkNsYXNzTmFtZXNBcnJheV8iLCJwcmVmaXhlZE93bkNsYXNzTmFtZXNBcnJheSIsImFyckl0ZW0iLCJuZWVkc0NsYXNzTmFtZXMiLCJuZWVkc050aENoaWxkIiwib3duSW5kZXgiLCJlbGVtZW50SW5kZXgiLCJzaWJsaW5ncyIsInNpYmxpbmdOYW1lIiwib3duQ2xhc3NOYW1lcyIsIm93bkNsYXNzTmFtZUNvdW50Iiwic2libGluZ0NsYXNzTmFtZXNBcnJheV8iLCJzaWJsaW5nQ2xhc3NOYW1lc0FycmF5Iiwic2libGluZ0NsYXNzIiwiaW5kIiwicmVzdWx0IiwicHJlZml4ZWROYW1lIiwic3Vic3RyIiwiY2ZpIiwiY3VycmVudEVsZW1lbnQiLCJibGFja2xpc3RlZCIsImN1cnJlbnRFbGVtZW50UGFyZW50Q2hpbGRyZW4iLCJjdXJyZW50RWxlbWVudEluZGV4IiwiY2ZpSW5kZXgiLCJfY3JlYXRlSGlnaGxpZ2h0IiwicG9pbnRlckludGVyYWN0aW9uIiwibG9jYXRpb24yUmFuZ2VJbmZvIiwidW5pcXVlU3RyIiwic3RhcnRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3IiLCJzdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCIsImVuZENvbnRhaW5lckVsZW1lbnRDc3NTZWxlY3RvciIsImVuZENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCIsImhhc2giLCJyZXF1aXJlIiwic2hhMjU2SGV4Iiwic2hhMjU2IiwiZGlnZXN0IiwiY3JlYXRlSGlnaGxpZ2h0RG9tIiwiY3JlYXRlSGlnaGxpZ2h0Iiwic2VsZWN0aW9uSW5mbyIsImNyZWF0ZUFubm90YXRpb24iLCJzY2FsZSIsIlJFQURJVU0yIiwiaXNGaXhlZExheW91dCIsImZ4bFZpZXdwb3J0U2NhbGUiLCJoaWdobGlnaHRzQ29udGFpbmVyIiwiZHJhd1VuZGVybGluZSIsImRyYXdTdHJpa2VUaHJvdWdoIiwiaGlnaGxpZ2h0QXJlYVNWR0RvY0ZyYWciLCJyb3VuZGVkQ29ybmVyIiwidW5kZXJsaW5lVGhpY2tuZXNzIiwic3RyaWtlVGhyb3VnaExpbmVUaGlja25lc3MiLCJleHRyYSIsInJhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdCIsImZyYW1lRm9ySGlnaGxpZ2h0QW5ub3RhdGlvbk1hcmtXaXRoSUQiLCJhbm5vdGF0aW9uT2Zmc2V0IiwiYm9yZGVyVGhpY2tuZXNzIiwiY3JlYXRlRG9jdW1lbnRGcmFnbWVudCIsImhpZ2hsaWdodEFyZWFTVkdSZWN0IiwiY3JlYXRlRWxlbWVudE5TIiwiaGlnaGxpZ2h0QXJlYVNWR0xpbmUiLCJsaW5lT2Zmc2V0IiwicmdiIiwicmFuZG9tIiwiciIsImciLCJoaWdobGlnaHRBcmVhTGluZSIsImhpZ2hsaWdodEFyZWFTVkciLCJvdmVyZmxvdyIsInJhbmdlQm91bmRpbmdDbGllbnRSZWN0Iiwic3RhcnROb2RlIiwiZW5kTm9kZSIsInJhbmdlUmV2ZXJzZSIsImNvbXB1dGVFbGVtZW50Q0ZJIiwic3RhcnRJc0VsZW1lbnQiLCJzdGFydENvbnRhaW5lckVsZW1lbnQiLCJlbmRJc0VsZW1lbnQiLCJlbmRDb250YWluZXJFbGVtZW50IiwiY29tbW9uRWxlbWVudEFuY2VzdG9yIiwiY29tbW9uQW5jZXN0b3JDb250YWluZXIiLCJyYW5nZUNvbW1vbkFuY2VzdG9yRWxlbWVudCIsInJvb3RFbGVtZW50Q2ZpIiwic3RhcnRFbGVtZW50Q2ZpIiwiZW5kRWxlbWVudENmaSIsInN0YXJ0RWxlbWVudE9yVGV4dENmaSIsInN0YXJ0Q29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4Rm9yQ2ZpIiwiY2ZpVGV4dE5vZGVJbmRleCIsImNmaUluZGV4T2ZMYXN0RWxlbWVudCIsImNoaWxkRWxlbWVudENvdW50IiwibGFzdENoaWxkTm9kZSIsImVuZEVsZW1lbnRPclRleHRDZmkiLCJlbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXhGb3JDZmkiLCJzdGFydEVsZW1lbnQiLCJlbmRFbGVtZW50IiwiZnJhbWVGb3JIaWdobGlnaHRXaXRoSUQiLCJ0b3BDbGllbnRSZWN0IiwibWF4SGVpZ2h0IiwibmV3VG9wIiwiYm91bmRpbmdBcmVhcyIsImdldEVsZW1lbnRzQnlDbGFzc05hbWUiLCJsZW4iLCJib3VuZGluZ0FyZWEiLCJoaWdobGlnaHRXaXRoSUQiLCJwYXJ0aWFsQ2ZpIiwiZG9tUmFuZ2UiLCJsb2NhdGlvbiIsInJlY3RhbmdsZUZvckhpZ2hsaWdodFdpdGhJRCIsImdldFNlbGVjdGlvblJlY3QiLCJzZWwiLCJoYW5kbGVCb3VuZHMiLCJzZXRTY3JvbGxNb2RlIiwiZmxhZyIsImZpbmRGaXJzdFZpc2libGVMb2NhdG9yIiwiZmluZEVsZW1lbnQiLCJocmVmIiwicm9vdEVsZW1lbnQiLCJzaG91bGRJZ25vcmVFbGVtZW50IiwiaXNFbGVtZW50VmlzaWJsZSIsInJlYWRpdW0iLCJlbFN0eWxlIiwiZGlzcGxheSIsIm1hdGNoQWxsIiwic2hpbSIsImlzU2VsZWN0aW5nIiwib25TZWxlY3Rpb25FbmQiLCJvblNlbGVjdGlvblN0YXJ0IiwiZ2V0Q3VycmVudFNlbGVjdGlvbiIsImdldEN1cnJlbnRTZWxlY3Rpb25UZXh0IiwiY2xlYW5IaWdobGlnaHQiLCJzbmlwcGV0TGVuZ3RoIiwiZmlyc3RXb3JkU3RhcnQiLCJsYXN0V29yZEVuZCIsInBvcCIsInJlZ2lzdGVyRGVjb3JhdGlvblRlbXBsYXRlcyJdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///6396\n')},1924:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar callBind = __webpack_require__(5559);\n\nvar $indexOf = callBind(GetIntrinsic('String.prototype.indexOf'));\n\nmodule.exports = function callBoundIntrinsic(name, allowMissing) {\n\tvar intrinsic = GetIntrinsic(name, !!allowMissing);\n\tif (typeof intrinsic === 'function' && $indexOf(name, '.prototype.') > -1) {\n\t\treturn callBind(intrinsic);\n\t}\n\treturn intrinsic;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTkyNC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQyxlQUFlLG1CQUFPLENBQUMsSUFBSTs7QUFFM0I7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2NhbGwtYmluZC9jYWxsQm91bmQuanM/NTQ1ZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciBjYWxsQmluZCA9IHJlcXVpcmUoJy4vJyk7XG5cbnZhciAkaW5kZXhPZiA9IGNhbGxCaW5kKEdldEludHJpbnNpYygnU3RyaW5nLnByb3RvdHlwZS5pbmRleE9mJykpO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIGNhbGxCb3VuZEludHJpbnNpYyhuYW1lLCBhbGxvd01pc3NpbmcpIHtcblx0dmFyIGludHJpbnNpYyA9IEdldEludHJpbnNpYyhuYW1lLCAhIWFsbG93TWlzc2luZyk7XG5cdGlmICh0eXBlb2YgaW50cmluc2ljID09PSAnZnVuY3Rpb24nICYmICRpbmRleE9mKG5hbWUsICcucHJvdG90eXBlLicpID4gLTEpIHtcblx0XHRyZXR1cm4gY2FsbEJpbmQoaW50cmluc2ljKTtcblx0fVxuXHRyZXR1cm4gaW50cmluc2ljO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///1924\n")},5559:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar bind = __webpack_require__(8612);\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $apply = GetIntrinsic('%Function.prototype.apply%');\nvar $call = GetIntrinsic('%Function.prototype.call%');\nvar $reflectApply = GetIntrinsic('%Reflect.apply%', true) || bind.call($call, $apply);\n\nvar $gOPD = GetIntrinsic('%Object.getOwnPropertyDescriptor%', true);\nvar $defineProperty = GetIntrinsic('%Object.defineProperty%', true);\nvar $max = GetIntrinsic('%Math.max%');\n\nif ($defineProperty) {\n\ttry {\n\t\t$defineProperty({}, 'a', { value: 1 });\n\t} catch (e) {\n\t\t// IE 8 has a broken defineProperty\n\t\t$defineProperty = null;\n\t}\n}\n\nmodule.exports = function callBind(originalFunction) {\n\tvar func = $reflectApply(bind, $call, arguments);\n\tif ($gOPD && $defineProperty) {\n\t\tvar desc = $gOPD(func, 'length');\n\t\tif (desc.configurable) {\n\t\t\t// original length, plus the receiver, minus any additional arguments (after the receiver)\n\t\t\t$defineProperty(\n\t\t\t\tfunc,\n\t\t\t\t'length',\n\t\t\t\t{ value: 1 + $max(0, originalFunction.length - (arguments.length - 1)) }\n\t\t\t);\n\t\t}\n\t}\n\treturn func;\n};\n\nvar applyBind = function applyBind() {\n\treturn $reflectApply(bind, $apply, arguments);\n};\n\nif ($defineProperty) {\n\t$defineProperty(module.exports, 'apply', { value: applyBind });\n} else {\n\tmodule.exports.apply = applyBind;\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTU1OS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixXQUFXLG1CQUFPLENBQUMsSUFBZTtBQUNsQyxtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxvQkFBb0IsU0FBUyxVQUFVO0FBQ3ZDLEdBQUc7QUFDSDtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxNQUFNO0FBQ047QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQSw0Q0FBNEMsa0JBQWtCO0FBQzlELEVBQUU7QUFDRixDQUFDLG9CQUFvQjtBQUNyQiIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvY2FsbC1iaW5kL2luZGV4LmpzPzNlYjEiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgYmluZCA9IHJlcXVpcmUoJ2Z1bmN0aW9uLWJpbmQnKTtcbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkYXBwbHkgPSBHZXRJbnRyaW5zaWMoJyVGdW5jdGlvbi5wcm90b3R5cGUuYXBwbHklJyk7XG52YXIgJGNhbGwgPSBHZXRJbnRyaW5zaWMoJyVGdW5jdGlvbi5wcm90b3R5cGUuY2FsbCUnKTtcbnZhciAkcmVmbGVjdEFwcGx5ID0gR2V0SW50cmluc2ljKCclUmVmbGVjdC5hcHBseSUnLCB0cnVlKSB8fCBiaW5kLmNhbGwoJGNhbGwsICRhcHBseSk7XG5cbnZhciAkZ09QRCA9IEdldEludHJpbnNpYygnJU9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3IlJywgdHJ1ZSk7XG52YXIgJGRlZmluZVByb3BlcnR5ID0gR2V0SW50cmluc2ljKCclT2JqZWN0LmRlZmluZVByb3BlcnR5JScsIHRydWUpO1xudmFyICRtYXggPSBHZXRJbnRyaW5zaWMoJyVNYXRoLm1heCUnKTtcblxuaWYgKCRkZWZpbmVQcm9wZXJ0eSkge1xuXHR0cnkge1xuXHRcdCRkZWZpbmVQcm9wZXJ0eSh7fSwgJ2EnLCB7IHZhbHVlOiAxIH0pO1xuXHR9IGNhdGNoIChlKSB7XG5cdFx0Ly8gSUUgOCBoYXMgYSBicm9rZW4gZGVmaW5lUHJvcGVydHlcblx0XHQkZGVmaW5lUHJvcGVydHkgPSBudWxsO1xuXHR9XG59XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gY2FsbEJpbmQob3JpZ2luYWxGdW5jdGlvbikge1xuXHR2YXIgZnVuYyA9ICRyZWZsZWN0QXBwbHkoYmluZCwgJGNhbGwsIGFyZ3VtZW50cyk7XG5cdGlmICgkZ09QRCAmJiAkZGVmaW5lUHJvcGVydHkpIHtcblx0XHR2YXIgZGVzYyA9ICRnT1BEKGZ1bmMsICdsZW5ndGgnKTtcblx0XHRpZiAoZGVzYy5jb25maWd1cmFibGUpIHtcblx0XHRcdC8vIG9yaWdpbmFsIGxlbmd0aCwgcGx1cyB0aGUgcmVjZWl2ZXIsIG1pbnVzIGFueSBhZGRpdGlvbmFsIGFyZ3VtZW50cyAoYWZ0ZXIgdGhlIHJlY2VpdmVyKVxuXHRcdFx0JGRlZmluZVByb3BlcnR5KFxuXHRcdFx0XHRmdW5jLFxuXHRcdFx0XHQnbGVuZ3RoJyxcblx0XHRcdFx0eyB2YWx1ZTogMSArICRtYXgoMCwgb3JpZ2luYWxGdW5jdGlvbi5sZW5ndGggLSAoYXJndW1lbnRzLmxlbmd0aCAtIDEpKSB9XG5cdFx0XHQpO1xuXHRcdH1cblx0fVxuXHRyZXR1cm4gZnVuYztcbn07XG5cbnZhciBhcHBseUJpbmQgPSBmdW5jdGlvbiBhcHBseUJpbmQoKSB7XG5cdHJldHVybiAkcmVmbGVjdEFwcGx5KGJpbmQsICRhcHBseSwgYXJndW1lbnRzKTtcbn07XG5cbmlmICgkZGVmaW5lUHJvcGVydHkpIHtcblx0JGRlZmluZVByb3BlcnR5KG1vZHVsZS5leHBvcnRzLCAnYXBwbHknLCB7IHZhbHVlOiBhcHBseUJpbmQgfSk7XG59IGVsc2Uge1xuXHRtb2R1bGUuZXhwb3J0cy5hcHBseSA9IGFwcGx5QmluZDtcbn1cbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///5559\n")},4289:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar keys = __webpack_require__(2215);\nvar hasSymbols = typeof Symbol === 'function' && typeof Symbol('foo') === 'symbol';\n\nvar toStr = Object.prototype.toString;\nvar concat = Array.prototype.concat;\nvar origDefineProperty = Object.defineProperty;\n\nvar isFunction = function (fn) {\n\treturn typeof fn === 'function' && toStr.call(fn) === '[object Function]';\n};\n\nvar arePropertyDescriptorsSupported = function () {\n\tvar obj = {};\n\ttry {\n\t\torigDefineProperty(obj, 'x', { enumerable: false, value: obj });\n\t\t// eslint-disable-next-line no-unused-vars, no-restricted-syntax\n\t\tfor (var _ in obj) { // jscs:ignore disallowUnusedVariables\n\t\t\treturn false;\n\t\t}\n\t\treturn obj.x === obj;\n\t} catch (e) { /* this is IE 8. */\n\t\treturn false;\n\t}\n};\nvar supportsDescriptors = origDefineProperty && arePropertyDescriptorsSupported();\n\nvar defineProperty = function (object, name, value, predicate) {\n\tif (name in object && (!isFunction(predicate) || !predicate())) {\n\t\treturn;\n\t}\n\tif (supportsDescriptors) {\n\t\torigDefineProperty(object, name, {\n\t\t\tconfigurable: true,\n\t\t\tenumerable: false,\n\t\t\tvalue: value,\n\t\t\twritable: true\n\t\t});\n\t} else {\n\t\tobject[name] = value;\n\t}\n};\n\nvar defineProperties = function (object, map) {\n\tvar predicates = arguments.length > 2 ? arguments[2] : {};\n\tvar props = keys(map);\n\tif (hasSymbols) {\n\t\tprops = concat.call(props, Object.getOwnPropertySymbols(map));\n\t}\n\tfor (var i = 0; i < props.length; i += 1) {\n\t\tdefineProperty(object, props[i], map[props[i]], predicates[props[i]]);\n\t}\n};\n\ndefineProperties.supportsDescriptors = !!supportsDescriptors;\n\nmodule.exports = defineProperties;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDI4OS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixXQUFXLG1CQUFPLENBQUMsSUFBYTtBQUNoQzs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBLGlDQUFpQywrQkFBK0I7QUFDaEU7QUFDQSx1QkFBdUI7QUFDdkI7QUFDQTtBQUNBO0FBQ0EsR0FBRyxZQUFZO0FBQ2Y7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0gsR0FBRztBQUNIO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxpQkFBaUIsa0JBQWtCO0FBQ25DO0FBQ0E7QUFDQTs7QUFFQTs7QUFFQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZGVmaW5lLXByb3BlcnRpZXMvaW5kZXguanM/ZjM2NyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBrZXlzID0gcmVxdWlyZSgnb2JqZWN0LWtleXMnKTtcbnZhciBoYXNTeW1ib2xzID0gdHlwZW9mIFN5bWJvbCA9PT0gJ2Z1bmN0aW9uJyAmJiB0eXBlb2YgU3ltYm9sKCdmb28nKSA9PT0gJ3N5bWJvbCc7XG5cbnZhciB0b1N0ciA9IE9iamVjdC5wcm90b3R5cGUudG9TdHJpbmc7XG52YXIgY29uY2F0ID0gQXJyYXkucHJvdG90eXBlLmNvbmNhdDtcbnZhciBvcmlnRGVmaW5lUHJvcGVydHkgPSBPYmplY3QuZGVmaW5lUHJvcGVydHk7XG5cbnZhciBpc0Z1bmN0aW9uID0gZnVuY3Rpb24gKGZuKSB7XG5cdHJldHVybiB0eXBlb2YgZm4gPT09ICdmdW5jdGlvbicgJiYgdG9TdHIuY2FsbChmbikgPT09ICdbb2JqZWN0IEZ1bmN0aW9uXSc7XG59O1xuXG52YXIgYXJlUHJvcGVydHlEZXNjcmlwdG9yc1N1cHBvcnRlZCA9IGZ1bmN0aW9uICgpIHtcblx0dmFyIG9iaiA9IHt9O1xuXHR0cnkge1xuXHRcdG9yaWdEZWZpbmVQcm9wZXJ0eShvYmosICd4JywgeyBlbnVtZXJhYmxlOiBmYWxzZSwgdmFsdWU6IG9iaiB9KTtcblx0XHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tdW51c2VkLXZhcnMsIG5vLXJlc3RyaWN0ZWQtc3ludGF4XG5cdFx0Zm9yICh2YXIgXyBpbiBvYmopIHsgLy8ganNjczppZ25vcmUgZGlzYWxsb3dVbnVzZWRWYXJpYWJsZXNcblx0XHRcdHJldHVybiBmYWxzZTtcblx0XHR9XG5cdFx0cmV0dXJuIG9iai54ID09PSBvYmo7XG5cdH0gY2F0Y2ggKGUpIHsgLyogdGhpcyBpcyBJRSA4LiAqL1xuXHRcdHJldHVybiBmYWxzZTtcblx0fVxufTtcbnZhciBzdXBwb3J0c0Rlc2NyaXB0b3JzID0gb3JpZ0RlZmluZVByb3BlcnR5ICYmIGFyZVByb3BlcnR5RGVzY3JpcHRvcnNTdXBwb3J0ZWQoKTtcblxudmFyIGRlZmluZVByb3BlcnR5ID0gZnVuY3Rpb24gKG9iamVjdCwgbmFtZSwgdmFsdWUsIHByZWRpY2F0ZSkge1xuXHRpZiAobmFtZSBpbiBvYmplY3QgJiYgKCFpc0Z1bmN0aW9uKHByZWRpY2F0ZSkgfHwgIXByZWRpY2F0ZSgpKSkge1xuXHRcdHJldHVybjtcblx0fVxuXHRpZiAoc3VwcG9ydHNEZXNjcmlwdG9ycykge1xuXHRcdG9yaWdEZWZpbmVQcm9wZXJ0eShvYmplY3QsIG5hbWUsIHtcblx0XHRcdGNvbmZpZ3VyYWJsZTogdHJ1ZSxcblx0XHRcdGVudW1lcmFibGU6IGZhbHNlLFxuXHRcdFx0dmFsdWU6IHZhbHVlLFxuXHRcdFx0d3JpdGFibGU6IHRydWVcblx0XHR9KTtcblx0fSBlbHNlIHtcblx0XHRvYmplY3RbbmFtZV0gPSB2YWx1ZTtcblx0fVxufTtcblxudmFyIGRlZmluZVByb3BlcnRpZXMgPSBmdW5jdGlvbiAob2JqZWN0LCBtYXApIHtcblx0dmFyIHByZWRpY2F0ZXMgPSBhcmd1bWVudHMubGVuZ3RoID4gMiA/IGFyZ3VtZW50c1syXSA6IHt9O1xuXHR2YXIgcHJvcHMgPSBrZXlzKG1hcCk7XG5cdGlmIChoYXNTeW1ib2xzKSB7XG5cdFx0cHJvcHMgPSBjb25jYXQuY2FsbChwcm9wcywgT2JqZWN0LmdldE93blByb3BlcnR5U3ltYm9scyhtYXApKTtcblx0fVxuXHRmb3IgKHZhciBpID0gMDsgaSA8IHByb3BzLmxlbmd0aDsgaSArPSAxKSB7XG5cdFx0ZGVmaW5lUHJvcGVydHkob2JqZWN0LCBwcm9wc1tpXSwgbWFwW3Byb3BzW2ldXSwgcHJlZGljYXRlc1twcm9wc1tpXV0pO1xuXHR9XG59O1xuXG5kZWZpbmVQcm9wZXJ0aWVzLnN1cHBvcnRzRGVzY3JpcHRvcnMgPSAhIXN1cHBvcnRzRGVzY3JpcHRvcnM7XG5cbm1vZHVsZS5leHBvcnRzID0gZGVmaW5lUHJvcGVydGllcztcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///4289\n")},1503:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar hasSymbols = typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol';\n\nvar isPrimitive = __webpack_require__(4149);\nvar isCallable = __webpack_require__(5320);\nvar isDate = __webpack_require__(8923);\nvar isSymbol = __webpack_require__(2636);\n\nvar ordinaryToPrimitive = function OrdinaryToPrimitive(O, hint) {\n\tif (typeof O === 'undefined' || O === null) {\n\t\tthrow new TypeError('Cannot call method on ' + O);\n\t}\n\tif (typeof hint !== 'string' || (hint !== 'number' && hint !== 'string')) {\n\t\tthrow new TypeError('hint must be \"string\" or \"number\"');\n\t}\n\tvar methodNames = hint === 'string' ? ['toString', 'valueOf'] : ['valueOf', 'toString'];\n\tvar method, result, i;\n\tfor (i = 0; i < methodNames.length; ++i) {\n\t\tmethod = O[methodNames[i]];\n\t\tif (isCallable(method)) {\n\t\t\tresult = method.call(O);\n\t\t\tif (isPrimitive(result)) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\tthrow new TypeError('No default value');\n};\n\nvar GetMethod = function GetMethod(O, P) {\n\tvar func = O[P];\n\tif (func !== null && typeof func !== 'undefined') {\n\t\tif (!isCallable(func)) {\n\t\t\tthrow new TypeError(func + ' returned for property ' + P + ' of object ' + O + ' is not a function');\n\t\t}\n\t\treturn func;\n\t}\n\treturn void 0;\n};\n\n// http://www.ecma-international.org/ecma-262/6.0/#sec-toprimitive\nmodule.exports = function ToPrimitive(input) {\n\tif (isPrimitive(input)) {\n\t\treturn input;\n\t}\n\tvar hint = 'default';\n\tif (arguments.length > 1) {\n\t\tif (arguments[1] === String) {\n\t\t\thint = 'string';\n\t\t} else if (arguments[1] === Number) {\n\t\t\thint = 'number';\n\t\t}\n\t}\n\n\tvar exoticToPrim;\n\tif (hasSymbols) {\n\t\tif (Symbol.toPrimitive) {\n\t\t\texoticToPrim = GetMethod(input, Symbol.toPrimitive);\n\t\t} else if (isSymbol(input)) {\n\t\t\texoticToPrim = Symbol.prototype.valueOf;\n\t\t}\n\t}\n\tif (typeof exoticToPrim !== 'undefined') {\n\t\tvar result = exoticToPrim.call(input, hint);\n\t\tif (isPrimitive(result)) {\n\t\t\treturn result;\n\t\t}\n\t\tthrow new TypeError('unable to convert exotic object to primitive');\n\t}\n\tif (hint === 'default' && (isDate(input) || isSymbol(input))) {\n\t\thint = 'string';\n\t}\n\treturn ordinaryToPrimitive(input, hint === 'default' ? 'number' : hint);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTUwMy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQSxrQkFBa0IsbUJBQU8sQ0FBQyxJQUF1QjtBQUNqRCxpQkFBaUIsbUJBQU8sQ0FBQyxJQUFhO0FBQ3RDLGFBQWEsbUJBQU8sQ0FBQyxJQUFnQjtBQUNyQyxlQUFlLG1CQUFPLENBQUMsSUFBVzs7QUFFbEM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsYUFBYSx3QkFBd0I7QUFDckM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSTtBQUNKO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLElBQUk7QUFDSjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLXRvLXByaW1pdGl2ZS9lczIwMTUuanM/NTk5NyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBoYXNTeW1ib2xzID0gdHlwZW9mIFN5bWJvbCA9PT0gJ2Z1bmN0aW9uJyAmJiB0eXBlb2YgU3ltYm9sLml0ZXJhdG9yID09PSAnc3ltYm9sJztcblxudmFyIGlzUHJpbWl0aXZlID0gcmVxdWlyZSgnLi9oZWxwZXJzL2lzUHJpbWl0aXZlJyk7XG52YXIgaXNDYWxsYWJsZSA9IHJlcXVpcmUoJ2lzLWNhbGxhYmxlJyk7XG52YXIgaXNEYXRlID0gcmVxdWlyZSgnaXMtZGF0ZS1vYmplY3QnKTtcbnZhciBpc1N5bWJvbCA9IHJlcXVpcmUoJ2lzLXN5bWJvbCcpO1xuXG52YXIgb3JkaW5hcnlUb1ByaW1pdGl2ZSA9IGZ1bmN0aW9uIE9yZGluYXJ5VG9QcmltaXRpdmUoTywgaGludCkge1xuXHRpZiAodHlwZW9mIE8gPT09ICd1bmRlZmluZWQnIHx8IE8gPT09IG51bGwpIHtcblx0XHR0aHJvdyBuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FsbCBtZXRob2Qgb24gJyArIE8pO1xuXHR9XG5cdGlmICh0eXBlb2YgaGludCAhPT0gJ3N0cmluZycgfHwgKGhpbnQgIT09ICdudW1iZXInICYmIGhpbnQgIT09ICdzdHJpbmcnKSkge1xuXHRcdHRocm93IG5ldyBUeXBlRXJyb3IoJ2hpbnQgbXVzdCBiZSBcInN0cmluZ1wiIG9yIFwibnVtYmVyXCInKTtcblx0fVxuXHR2YXIgbWV0aG9kTmFtZXMgPSBoaW50ID09PSAnc3RyaW5nJyA/IFsndG9TdHJpbmcnLCAndmFsdWVPZiddIDogWyd2YWx1ZU9mJywgJ3RvU3RyaW5nJ107XG5cdHZhciBtZXRob2QsIHJlc3VsdCwgaTtcblx0Zm9yIChpID0gMDsgaSA8IG1ldGhvZE5hbWVzLmxlbmd0aDsgKytpKSB7XG5cdFx0bWV0aG9kID0gT1ttZXRob2ROYW1lc1tpXV07XG5cdFx0aWYgKGlzQ2FsbGFibGUobWV0aG9kKSkge1xuXHRcdFx0cmVzdWx0ID0gbWV0aG9kLmNhbGwoTyk7XG5cdFx0XHRpZiAoaXNQcmltaXRpdmUocmVzdWx0KSkge1xuXHRcdFx0XHRyZXR1cm4gcmVzdWx0O1xuXHRcdFx0fVxuXHRcdH1cblx0fVxuXHR0aHJvdyBuZXcgVHlwZUVycm9yKCdObyBkZWZhdWx0IHZhbHVlJyk7XG59O1xuXG52YXIgR2V0TWV0aG9kID0gZnVuY3Rpb24gR2V0TWV0aG9kKE8sIFApIHtcblx0dmFyIGZ1bmMgPSBPW1BdO1xuXHRpZiAoZnVuYyAhPT0gbnVsbCAmJiB0eXBlb2YgZnVuYyAhPT0gJ3VuZGVmaW5lZCcpIHtcblx0XHRpZiAoIWlzQ2FsbGFibGUoZnVuYykpIHtcblx0XHRcdHRocm93IG5ldyBUeXBlRXJyb3IoZnVuYyArICcgcmV0dXJuZWQgZm9yIHByb3BlcnR5ICcgKyBQICsgJyBvZiBvYmplY3QgJyArIE8gKyAnIGlzIG5vdCBhIGZ1bmN0aW9uJyk7XG5cdFx0fVxuXHRcdHJldHVybiBmdW5jO1xuXHR9XG5cdHJldHVybiB2b2lkIDA7XG59O1xuXG4vLyBodHRwOi8vd3d3LmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtdG9wcmltaXRpdmVcbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVG9QcmltaXRpdmUoaW5wdXQpIHtcblx0aWYgKGlzUHJpbWl0aXZlKGlucHV0KSkge1xuXHRcdHJldHVybiBpbnB1dDtcblx0fVxuXHR2YXIgaGludCA9ICdkZWZhdWx0Jztcblx0aWYgKGFyZ3VtZW50cy5sZW5ndGggPiAxKSB7XG5cdFx0aWYgKGFyZ3VtZW50c1sxXSA9PT0gU3RyaW5nKSB7XG5cdFx0XHRoaW50ID0gJ3N0cmluZyc7XG5cdFx0fSBlbHNlIGlmIChhcmd1bWVudHNbMV0gPT09IE51bWJlcikge1xuXHRcdFx0aGludCA9ICdudW1iZXInO1xuXHRcdH1cblx0fVxuXG5cdHZhciBleG90aWNUb1ByaW07XG5cdGlmIChoYXNTeW1ib2xzKSB7XG5cdFx0aWYgKFN5bWJvbC50b1ByaW1pdGl2ZSkge1xuXHRcdFx0ZXhvdGljVG9QcmltID0gR2V0TWV0aG9kKGlucHV0LCBTeW1ib2wudG9QcmltaXRpdmUpO1xuXHRcdH0gZWxzZSBpZiAoaXNTeW1ib2woaW5wdXQpKSB7XG5cdFx0XHRleG90aWNUb1ByaW0gPSBTeW1ib2wucHJvdG90eXBlLnZhbHVlT2Y7XG5cdFx0fVxuXHR9XG5cdGlmICh0eXBlb2YgZXhvdGljVG9QcmltICE9PSAndW5kZWZpbmVkJykge1xuXHRcdHZhciByZXN1bHQgPSBleG90aWNUb1ByaW0uY2FsbChpbnB1dCwgaGludCk7XG5cdFx0aWYgKGlzUHJpbWl0aXZlKHJlc3VsdCkpIHtcblx0XHRcdHJldHVybiByZXN1bHQ7XG5cdFx0fVxuXHRcdHRocm93IG5ldyBUeXBlRXJyb3IoJ3VuYWJsZSB0byBjb252ZXJ0IGV4b3RpYyBvYmplY3QgdG8gcHJpbWl0aXZlJyk7XG5cdH1cblx0aWYgKGhpbnQgPT09ICdkZWZhdWx0JyAmJiAoaXNEYXRlKGlucHV0KSB8fCBpc1N5bWJvbChpbnB1dCkpKSB7XG5cdFx0aGludCA9ICdzdHJpbmcnO1xuXHR9XG5cdHJldHVybiBvcmRpbmFyeVRvUHJpbWl0aXZlKGlucHV0LCBoaW50ID09PSAnZGVmYXVsdCcgPyAnbnVtYmVyJyA6IGhpbnQpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///1503\n")},2116:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar toStr = Object.prototype.toString;\n\nvar isPrimitive = __webpack_require__(4149);\n\nvar isCallable = __webpack_require__(5320);\n\n// http://ecma-international.org/ecma-262/5.1/#sec-8.12.8\nvar ES5internalSlots = {\n\t'[[DefaultValue]]': function (O) {\n\t\tvar actualHint;\n\t\tif (arguments.length > 1) {\n\t\t\tactualHint = arguments[1];\n\t\t} else {\n\t\t\tactualHint = toStr.call(O) === '[object Date]' ? String : Number;\n\t\t}\n\n\t\tif (actualHint === String || actualHint === Number) {\n\t\t\tvar methods = actualHint === String ? ['toString', 'valueOf'] : ['valueOf', 'toString'];\n\t\t\tvar value, i;\n\t\t\tfor (i = 0; i < methods.length; ++i) {\n\t\t\t\tif (isCallable(O[methods[i]])) {\n\t\t\t\t\tvalue = O[methods[i]]();\n\t\t\t\t\tif (isPrimitive(value)) {\n\t\t\t\t\t\treturn value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new TypeError('No default value');\n\t\t}\n\t\tthrow new TypeError('invalid [[DefaultValue]] hint supplied');\n\t}\n};\n\n// http://ecma-international.org/ecma-262/5.1/#sec-9.1\nmodule.exports = function ToPrimitive(input) {\n\tif (isPrimitive(input)) {\n\t\treturn input;\n\t}\n\tif (arguments.length > 1) {\n\t\treturn ES5internalSlots['[[DefaultValue]]'](input, arguments[1]);\n\t}\n\treturn ES5internalSlots['[[DefaultValue]]'](input);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjExNi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQSxrQkFBa0IsbUJBQU8sQ0FBQyxJQUF1Qjs7QUFFakQsaUJBQWlCLG1CQUFPLENBQUMsSUFBYTs7QUFFdEM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSTtBQUNKO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0EsZUFBZSxvQkFBb0I7QUFDbkM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy10by1wcmltaXRpdmUvZXM1LmpzPzJmMTciXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgdG9TdHIgPSBPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nO1xuXG52YXIgaXNQcmltaXRpdmUgPSByZXF1aXJlKCcuL2hlbHBlcnMvaXNQcmltaXRpdmUnKTtcblxudmFyIGlzQ2FsbGFibGUgPSByZXF1aXJlKCdpcy1jYWxsYWJsZScpO1xuXG4vLyBodHRwOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi81LjEvI3NlYy04LjEyLjhcbnZhciBFUzVpbnRlcm5hbFNsb3RzID0ge1xuXHQnW1tEZWZhdWx0VmFsdWVdXSc6IGZ1bmN0aW9uIChPKSB7XG5cdFx0dmFyIGFjdHVhbEhpbnQ7XG5cdFx0aWYgKGFyZ3VtZW50cy5sZW5ndGggPiAxKSB7XG5cdFx0XHRhY3R1YWxIaW50ID0gYXJndW1lbnRzWzFdO1xuXHRcdH0gZWxzZSB7XG5cdFx0XHRhY3R1YWxIaW50ID0gdG9TdHIuY2FsbChPKSA9PT0gJ1tvYmplY3QgRGF0ZV0nID8gU3RyaW5nIDogTnVtYmVyO1xuXHRcdH1cblxuXHRcdGlmIChhY3R1YWxIaW50ID09PSBTdHJpbmcgfHwgYWN0dWFsSGludCA9PT0gTnVtYmVyKSB7XG5cdFx0XHR2YXIgbWV0aG9kcyA9IGFjdHVhbEhpbnQgPT09IFN0cmluZyA/IFsndG9TdHJpbmcnLCAndmFsdWVPZiddIDogWyd2YWx1ZU9mJywgJ3RvU3RyaW5nJ107XG5cdFx0XHR2YXIgdmFsdWUsIGk7XG5cdFx0XHRmb3IgKGkgPSAwOyBpIDwgbWV0aG9kcy5sZW5ndGg7ICsraSkge1xuXHRcdFx0XHRpZiAoaXNDYWxsYWJsZShPW21ldGhvZHNbaV1dKSkge1xuXHRcdFx0XHRcdHZhbHVlID0gT1ttZXRob2RzW2ldXSgpO1xuXHRcdFx0XHRcdGlmIChpc1ByaW1pdGl2ZSh2YWx1ZSkpIHtcblx0XHRcdFx0XHRcdHJldHVybiB2YWx1ZTtcblx0XHRcdFx0XHR9XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHRcdHRocm93IG5ldyBUeXBlRXJyb3IoJ05vIGRlZmF1bHQgdmFsdWUnKTtcblx0XHR9XG5cdFx0dGhyb3cgbmV3IFR5cGVFcnJvcignaW52YWxpZCBbW0RlZmF1bHRWYWx1ZV1dIGhpbnQgc3VwcGxpZWQnKTtcblx0fVxufTtcblxuLy8gaHR0cDovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNS4xLyNzZWMtOS4xXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFRvUHJpbWl0aXZlKGlucHV0KSB7XG5cdGlmIChpc1ByaW1pdGl2ZShpbnB1dCkpIHtcblx0XHRyZXR1cm4gaW5wdXQ7XG5cdH1cblx0aWYgKGFyZ3VtZW50cy5sZW5ndGggPiAxKSB7XG5cdFx0cmV0dXJuIEVTNWludGVybmFsU2xvdHNbJ1tbRGVmYXVsdFZhbHVlXV0nXShpbnB1dCwgYXJndW1lbnRzWzFdKTtcblx0fVxuXHRyZXR1cm4gRVM1aW50ZXJuYWxTbG90c1snW1tEZWZhdWx0VmFsdWVdXSddKGlucHV0KTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///2116\n")},4149:function(module){"use strict";eval("\n\nmodule.exports = function isPrimitive(value) {\n\treturn value === null || (typeof value !== 'function' && typeof value !== 'object');\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDE0OS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLXRvLXByaW1pdGl2ZS9oZWxwZXJzL2lzUHJpbWl0aXZlLmpzPzRkZTgiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIGlzUHJpbWl0aXZlKHZhbHVlKSB7XG5cdHJldHVybiB2YWx1ZSA9PT0gbnVsbCB8fCAodHlwZW9mIHZhbHVlICE9PSAnZnVuY3Rpb24nICYmIHR5cGVvZiB2YWx1ZSAhPT0gJ29iamVjdCcpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///4149\n")},7648:function(module){"use strict";eval("\n\n/* eslint no-invalid-this: 1 */\n\nvar ERROR_MESSAGE = 'Function.prototype.bind called on incompatible ';\nvar slice = Array.prototype.slice;\nvar toStr = Object.prototype.toString;\nvar funcType = '[object Function]';\n\nmodule.exports = function bind(that) {\n var target = this;\n if (typeof target !== 'function' || toStr.call(target) !== funcType) {\n throw new TypeError(ERROR_MESSAGE + target);\n }\n var args = slice.call(arguments, 1);\n\n var bound;\n var binder = function () {\n if (this instanceof bound) {\n var result = target.apply(\n this,\n args.concat(slice.call(arguments))\n );\n if (Object(result) === result) {\n return result;\n }\n return this;\n } else {\n return target.apply(\n that,\n args.concat(slice.call(arguments))\n );\n }\n };\n\n var boundLength = Math.max(0, target.length - args.length);\n var boundArgs = [];\n for (var i = 0; i < boundLength; i++) {\n boundArgs.push('$' + i);\n }\n\n bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this,arguments); }')(binder);\n\n if (target.prototype) {\n var Empty = function Empty() {};\n Empty.prototype = target.prototype;\n bound.prototype = new Empty();\n Empty.prototype = null;\n }\n\n return bound;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzY0OC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLFVBQVU7QUFDVjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBLG9CQUFvQixpQkFBaUI7QUFDckM7QUFDQTs7QUFFQSwrRUFBK0Usc0NBQXNDOztBQUVySDtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZnVuY3Rpb24tYmluZC9pbXBsZW1lbnRhdGlvbi5qcz82ODhlIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxuLyogZXNsaW50IG5vLWludmFsaWQtdGhpczogMSAqL1xuXG52YXIgRVJST1JfTUVTU0FHRSA9ICdGdW5jdGlvbi5wcm90b3R5cGUuYmluZCBjYWxsZWQgb24gaW5jb21wYXRpYmxlICc7XG52YXIgc2xpY2UgPSBBcnJheS5wcm90b3R5cGUuc2xpY2U7XG52YXIgdG9TdHIgPSBPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nO1xudmFyIGZ1bmNUeXBlID0gJ1tvYmplY3QgRnVuY3Rpb25dJztcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBiaW5kKHRoYXQpIHtcbiAgICB2YXIgdGFyZ2V0ID0gdGhpcztcbiAgICBpZiAodHlwZW9mIHRhcmdldCAhPT0gJ2Z1bmN0aW9uJyB8fCB0b1N0ci5jYWxsKHRhcmdldCkgIT09IGZ1bmNUeXBlKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoRVJST1JfTUVTU0FHRSArIHRhcmdldCk7XG4gICAgfVxuICAgIHZhciBhcmdzID0gc2xpY2UuY2FsbChhcmd1bWVudHMsIDEpO1xuXG4gICAgdmFyIGJvdW5kO1xuICAgIHZhciBiaW5kZXIgPSBmdW5jdGlvbiAoKSB7XG4gICAgICAgIGlmICh0aGlzIGluc3RhbmNlb2YgYm91bmQpIHtcbiAgICAgICAgICAgIHZhciByZXN1bHQgPSB0YXJnZXQuYXBwbHkoXG4gICAgICAgICAgICAgICAgdGhpcyxcbiAgICAgICAgICAgICAgICBhcmdzLmNvbmNhdChzbGljZS5jYWxsKGFyZ3VtZW50cykpXG4gICAgICAgICAgICApO1xuICAgICAgICAgICAgaWYgKE9iamVjdChyZXN1bHQpID09PSByZXN1bHQpIHtcbiAgICAgICAgICAgICAgICByZXR1cm4gcmVzdWx0O1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgcmV0dXJuIHRoaXM7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICByZXR1cm4gdGFyZ2V0LmFwcGx5KFxuICAgICAgICAgICAgICAgIHRoYXQsXG4gICAgICAgICAgICAgICAgYXJncy5jb25jYXQoc2xpY2UuY2FsbChhcmd1bWVudHMpKVxuICAgICAgICAgICAgKTtcbiAgICAgICAgfVxuICAgIH07XG5cbiAgICB2YXIgYm91bmRMZW5ndGggPSBNYXRoLm1heCgwLCB0YXJnZXQubGVuZ3RoIC0gYXJncy5sZW5ndGgpO1xuICAgIHZhciBib3VuZEFyZ3MgPSBbXTtcbiAgICBmb3IgKHZhciBpID0gMDsgaSA8IGJvdW5kTGVuZ3RoOyBpKyspIHtcbiAgICAgICAgYm91bmRBcmdzLnB1c2goJyQnICsgaSk7XG4gICAgfVxuXG4gICAgYm91bmQgPSBGdW5jdGlvbignYmluZGVyJywgJ3JldHVybiBmdW5jdGlvbiAoJyArIGJvdW5kQXJncy5qb2luKCcsJykgKyAnKXsgcmV0dXJuIGJpbmRlci5hcHBseSh0aGlzLGFyZ3VtZW50cyk7IH0nKShiaW5kZXIpO1xuXG4gICAgaWYgKHRhcmdldC5wcm90b3R5cGUpIHtcbiAgICAgICAgdmFyIEVtcHR5ID0gZnVuY3Rpb24gRW1wdHkoKSB7fTtcbiAgICAgICAgRW1wdHkucHJvdG90eXBlID0gdGFyZ2V0LnByb3RvdHlwZTtcbiAgICAgICAgYm91bmQucHJvdG90eXBlID0gbmV3IEVtcHR5KCk7XG4gICAgICAgIEVtcHR5LnByb3RvdHlwZSA9IG51bGw7XG4gICAgfVxuXG4gICAgcmV0dXJuIGJvdW5kO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///7648\n")},8612:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar implementation = __webpack_require__(7648);\n\nmodule.exports = Function.prototype.bind || implementation;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODYxMi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixxQkFBcUIsbUJBQU8sQ0FBQyxJQUFrQjs7QUFFL0MiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2Z1bmN0aW9uLWJpbmQvaW5kZXguanM/MGY3YyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBpbXBsZW1lbnRhdGlvbiA9IHJlcXVpcmUoJy4vaW1wbGVtZW50YXRpb24nKTtcblxubW9kdWxlLmV4cG9ydHMgPSBGdW5jdGlvbi5wcm90b3R5cGUuYmluZCB8fCBpbXBsZW1lbnRhdGlvbjtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///8612\n")},210:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar undefined;\n\nvar $SyntaxError = SyntaxError;\nvar $Function = Function;\nvar $TypeError = TypeError;\n\n// eslint-disable-next-line consistent-return\nvar getEvalledConstructor = function (expressionSyntax) {\n\ttry {\n\t\treturn $Function('\"use strict\"; return (' + expressionSyntax + ').constructor;')();\n\t} catch (e) {}\n};\n\nvar $gOPD = Object.getOwnPropertyDescriptor;\nif ($gOPD) {\n\ttry {\n\t\t$gOPD({}, '');\n\t} catch (e) {\n\t\t$gOPD = null; // this is IE 8, which has a broken gOPD\n\t}\n}\n\nvar throwTypeError = function () {\n\tthrow new $TypeError();\n};\nvar ThrowTypeError = $gOPD\n\t? (function () {\n\t\ttry {\n\t\t\t// eslint-disable-next-line no-unused-expressions, no-caller, no-restricted-properties\n\t\t\targuments.callee; // IE 8 does not throw here\n\t\t\treturn throwTypeError;\n\t\t} catch (calleeThrows) {\n\t\t\ttry {\n\t\t\t\t// IE 8 throws on Object.getOwnPropertyDescriptor(arguments, '')\n\t\t\t\treturn $gOPD(arguments, 'callee').get;\n\t\t\t} catch (gOPDthrows) {\n\t\t\t\treturn throwTypeError;\n\t\t\t}\n\t\t}\n\t}())\n\t: throwTypeError;\n\nvar hasSymbols = __webpack_require__(1405)();\n\nvar getProto = Object.getPrototypeOf || function (x) { return x.__proto__; }; // eslint-disable-line no-proto\n\nvar needsEval = {};\n\nvar TypedArray = typeof Uint8Array === 'undefined' ? undefined : getProto(Uint8Array);\n\nvar INTRINSICS = {\n\t'%AggregateError%': typeof AggregateError === 'undefined' ? undefined : AggregateError,\n\t'%Array%': Array,\n\t'%ArrayBuffer%': typeof ArrayBuffer === 'undefined' ? undefined : ArrayBuffer,\n\t'%ArrayIteratorPrototype%': hasSymbols ? getProto([][Symbol.iterator]()) : undefined,\n\t'%AsyncFromSyncIteratorPrototype%': undefined,\n\t'%AsyncFunction%': needsEval,\n\t'%AsyncGenerator%': needsEval,\n\t'%AsyncGeneratorFunction%': needsEval,\n\t'%AsyncIteratorPrototype%': needsEval,\n\t'%Atomics%': typeof Atomics === 'undefined' ? undefined : Atomics,\n\t'%BigInt%': typeof BigInt === 'undefined' ? undefined : BigInt,\n\t'%Boolean%': Boolean,\n\t'%DataView%': typeof DataView === 'undefined' ? undefined : DataView,\n\t'%Date%': Date,\n\t'%decodeURI%': decodeURI,\n\t'%decodeURIComponent%': decodeURIComponent,\n\t'%encodeURI%': encodeURI,\n\t'%encodeURIComponent%': encodeURIComponent,\n\t'%Error%': Error,\n\t'%eval%': eval, // eslint-disable-line no-eval\n\t'%EvalError%': EvalError,\n\t'%Float32Array%': typeof Float32Array === 'undefined' ? undefined : Float32Array,\n\t'%Float64Array%': typeof Float64Array === 'undefined' ? undefined : Float64Array,\n\t'%FinalizationRegistry%': typeof FinalizationRegistry === 'undefined' ? undefined : FinalizationRegistry,\n\t'%Function%': $Function,\n\t'%GeneratorFunction%': needsEval,\n\t'%Int8Array%': typeof Int8Array === 'undefined' ? undefined : Int8Array,\n\t'%Int16Array%': typeof Int16Array === 'undefined' ? undefined : Int16Array,\n\t'%Int32Array%': typeof Int32Array === 'undefined' ? undefined : Int32Array,\n\t'%isFinite%': isFinite,\n\t'%isNaN%': isNaN,\n\t'%IteratorPrototype%': hasSymbols ? getProto(getProto([][Symbol.iterator]())) : undefined,\n\t'%JSON%': typeof JSON === 'object' ? JSON : undefined,\n\t'%Map%': typeof Map === 'undefined' ? undefined : Map,\n\t'%MapIteratorPrototype%': typeof Map === 'undefined' || !hasSymbols ? undefined : getProto(new Map()[Symbol.iterator]()),\n\t'%Math%': Math,\n\t'%Number%': Number,\n\t'%Object%': Object,\n\t'%parseFloat%': parseFloat,\n\t'%parseInt%': parseInt,\n\t'%Promise%': typeof Promise === 'undefined' ? undefined : Promise,\n\t'%Proxy%': typeof Proxy === 'undefined' ? undefined : Proxy,\n\t'%RangeError%': RangeError,\n\t'%ReferenceError%': ReferenceError,\n\t'%Reflect%': typeof Reflect === 'undefined' ? undefined : Reflect,\n\t'%RegExp%': RegExp,\n\t'%Set%': typeof Set === 'undefined' ? undefined : Set,\n\t'%SetIteratorPrototype%': typeof Set === 'undefined' || !hasSymbols ? undefined : getProto(new Set()[Symbol.iterator]()),\n\t'%SharedArrayBuffer%': typeof SharedArrayBuffer === 'undefined' ? undefined : SharedArrayBuffer,\n\t'%String%': String,\n\t'%StringIteratorPrototype%': hasSymbols ? getProto(''[Symbol.iterator]()) : undefined,\n\t'%Symbol%': hasSymbols ? Symbol : undefined,\n\t'%SyntaxError%': $SyntaxError,\n\t'%ThrowTypeError%': ThrowTypeError,\n\t'%TypedArray%': TypedArray,\n\t'%TypeError%': $TypeError,\n\t'%Uint8Array%': typeof Uint8Array === 'undefined' ? undefined : Uint8Array,\n\t'%Uint8ClampedArray%': typeof Uint8ClampedArray === 'undefined' ? undefined : Uint8ClampedArray,\n\t'%Uint16Array%': typeof Uint16Array === 'undefined' ? undefined : Uint16Array,\n\t'%Uint32Array%': typeof Uint32Array === 'undefined' ? undefined : Uint32Array,\n\t'%URIError%': URIError,\n\t'%WeakMap%': typeof WeakMap === 'undefined' ? undefined : WeakMap,\n\t'%WeakRef%': typeof WeakRef === 'undefined' ? undefined : WeakRef,\n\t'%WeakSet%': typeof WeakSet === 'undefined' ? undefined : WeakSet\n};\n\nvar doEval = function doEval(name) {\n\tvar value;\n\tif (name === '%AsyncFunction%') {\n\t\tvalue = getEvalledConstructor('async function () {}');\n\t} else if (name === '%GeneratorFunction%') {\n\t\tvalue = getEvalledConstructor('function* () {}');\n\t} else if (name === '%AsyncGeneratorFunction%') {\n\t\tvalue = getEvalledConstructor('async function* () {}');\n\t} else if (name === '%AsyncGenerator%') {\n\t\tvar fn = doEval('%AsyncGeneratorFunction%');\n\t\tif (fn) {\n\t\t\tvalue = fn.prototype;\n\t\t}\n\t} else if (name === '%AsyncIteratorPrototype%') {\n\t\tvar gen = doEval('%AsyncGenerator%');\n\t\tif (gen) {\n\t\t\tvalue = getProto(gen.prototype);\n\t\t}\n\t}\n\n\tINTRINSICS[name] = value;\n\n\treturn value;\n};\n\nvar LEGACY_ALIASES = {\n\t'%ArrayBufferPrototype%': ['ArrayBuffer', 'prototype'],\n\t'%ArrayPrototype%': ['Array', 'prototype'],\n\t'%ArrayProto_entries%': ['Array', 'prototype', 'entries'],\n\t'%ArrayProto_forEach%': ['Array', 'prototype', 'forEach'],\n\t'%ArrayProto_keys%': ['Array', 'prototype', 'keys'],\n\t'%ArrayProto_values%': ['Array', 'prototype', 'values'],\n\t'%AsyncFunctionPrototype%': ['AsyncFunction', 'prototype'],\n\t'%AsyncGenerator%': ['AsyncGeneratorFunction', 'prototype'],\n\t'%AsyncGeneratorPrototype%': ['AsyncGeneratorFunction', 'prototype', 'prototype'],\n\t'%BooleanPrototype%': ['Boolean', 'prototype'],\n\t'%DataViewPrototype%': ['DataView', 'prototype'],\n\t'%DatePrototype%': ['Date', 'prototype'],\n\t'%ErrorPrototype%': ['Error', 'prototype'],\n\t'%EvalErrorPrototype%': ['EvalError', 'prototype'],\n\t'%Float32ArrayPrototype%': ['Float32Array', 'prototype'],\n\t'%Float64ArrayPrototype%': ['Float64Array', 'prototype'],\n\t'%FunctionPrototype%': ['Function', 'prototype'],\n\t'%Generator%': ['GeneratorFunction', 'prototype'],\n\t'%GeneratorPrototype%': ['GeneratorFunction', 'prototype', 'prototype'],\n\t'%Int8ArrayPrototype%': ['Int8Array', 'prototype'],\n\t'%Int16ArrayPrototype%': ['Int16Array', 'prototype'],\n\t'%Int32ArrayPrototype%': ['Int32Array', 'prototype'],\n\t'%JSONParse%': ['JSON', 'parse'],\n\t'%JSONStringify%': ['JSON', 'stringify'],\n\t'%MapPrototype%': ['Map', 'prototype'],\n\t'%NumberPrototype%': ['Number', 'prototype'],\n\t'%ObjectPrototype%': ['Object', 'prototype'],\n\t'%ObjProto_toString%': ['Object', 'prototype', 'toString'],\n\t'%ObjProto_valueOf%': ['Object', 'prototype', 'valueOf'],\n\t'%PromisePrototype%': ['Promise', 'prototype'],\n\t'%PromiseProto_then%': ['Promise', 'prototype', 'then'],\n\t'%Promise_all%': ['Promise', 'all'],\n\t'%Promise_reject%': ['Promise', 'reject'],\n\t'%Promise_resolve%': ['Promise', 'resolve'],\n\t'%RangeErrorPrototype%': ['RangeError', 'prototype'],\n\t'%ReferenceErrorPrototype%': ['ReferenceError', 'prototype'],\n\t'%RegExpPrototype%': ['RegExp', 'prototype'],\n\t'%SetPrototype%': ['Set', 'prototype'],\n\t'%SharedArrayBufferPrototype%': ['SharedArrayBuffer', 'prototype'],\n\t'%StringPrototype%': ['String', 'prototype'],\n\t'%SymbolPrototype%': ['Symbol', 'prototype'],\n\t'%SyntaxErrorPrototype%': ['SyntaxError', 'prototype'],\n\t'%TypedArrayPrototype%': ['TypedArray', 'prototype'],\n\t'%TypeErrorPrototype%': ['TypeError', 'prototype'],\n\t'%Uint8ArrayPrototype%': ['Uint8Array', 'prototype'],\n\t'%Uint8ClampedArrayPrototype%': ['Uint8ClampedArray', 'prototype'],\n\t'%Uint16ArrayPrototype%': ['Uint16Array', 'prototype'],\n\t'%Uint32ArrayPrototype%': ['Uint32Array', 'prototype'],\n\t'%URIErrorPrototype%': ['URIError', 'prototype'],\n\t'%WeakMapPrototype%': ['WeakMap', 'prototype'],\n\t'%WeakSetPrototype%': ['WeakSet', 'prototype']\n};\n\nvar bind = __webpack_require__(8612);\nvar hasOwn = __webpack_require__(7642);\nvar $concat = bind.call(Function.call, Array.prototype.concat);\nvar $spliceApply = bind.call(Function.apply, Array.prototype.splice);\nvar $replace = bind.call(Function.call, String.prototype.replace);\nvar $strSlice = bind.call(Function.call, String.prototype.slice);\n\n/* adapted from https://github.com/lodash/lodash/blob/4.17.15/dist/lodash.js#L6735-L6744 */\nvar rePropName = /[^%.[\\]]+|\\[(?:(-?\\d+(?:\\.\\d+)?)|([\"'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|%$))/g;\nvar reEscapeChar = /\\\\(\\\\)?/g; /** Used to match backslashes in property paths. */\nvar stringToPath = function stringToPath(string) {\n\tvar first = $strSlice(string, 0, 1);\n\tvar last = $strSlice(string, -1);\n\tif (first === '%' && last !== '%') {\n\t\tthrow new $SyntaxError('invalid intrinsic syntax, expected closing `%`');\n\t} else if (last === '%' && first !== '%') {\n\t\tthrow new $SyntaxError('invalid intrinsic syntax, expected opening `%`');\n\t}\n\tvar result = [];\n\t$replace(string, rePropName, function (match, number, quote, subString) {\n\t\tresult[result.length] = quote ? $replace(subString, reEscapeChar, '$1') : number || match;\n\t});\n\treturn result;\n};\n/* end adaptation */\n\nvar getBaseIntrinsic = function getBaseIntrinsic(name, allowMissing) {\n\tvar intrinsicName = name;\n\tvar alias;\n\tif (hasOwn(LEGACY_ALIASES, intrinsicName)) {\n\t\talias = LEGACY_ALIASES[intrinsicName];\n\t\tintrinsicName = '%' + alias[0] + '%';\n\t}\n\n\tif (hasOwn(INTRINSICS, intrinsicName)) {\n\t\tvar value = INTRINSICS[intrinsicName];\n\t\tif (value === needsEval) {\n\t\t\tvalue = doEval(intrinsicName);\n\t\t}\n\t\tif (typeof value === 'undefined' && !allowMissing) {\n\t\t\tthrow new $TypeError('intrinsic ' + name + ' exists, but is not available. Please file an issue!');\n\t\t}\n\n\t\treturn {\n\t\t\talias: alias,\n\t\t\tname: intrinsicName,\n\t\t\tvalue: value\n\t\t};\n\t}\n\n\tthrow new $SyntaxError('intrinsic ' + name + ' does not exist!');\n};\n\nmodule.exports = function GetIntrinsic(name, allowMissing) {\n\tif (typeof name !== 'string' || name.length === 0) {\n\t\tthrow new $TypeError('intrinsic name must be a non-empty string');\n\t}\n\tif (arguments.length > 1 && typeof allowMissing !== 'boolean') {\n\t\tthrow new $TypeError('\"allowMissing\" argument must be a boolean');\n\t}\n\n\tvar parts = stringToPath(name);\n\tvar intrinsicBaseName = parts.length > 0 ? parts[0] : '';\n\n\tvar intrinsic = getBaseIntrinsic('%' + intrinsicBaseName + '%', allowMissing);\n\tvar intrinsicRealName = intrinsic.name;\n\tvar value = intrinsic.value;\n\tvar skipFurtherCaching = false;\n\n\tvar alias = intrinsic.alias;\n\tif (alias) {\n\t\tintrinsicBaseName = alias[0];\n\t\t$spliceApply(parts, $concat([0, 1], alias));\n\t}\n\n\tfor (var i = 1, isOwn = true; i < parts.length; i += 1) {\n\t\tvar part = parts[i];\n\t\tvar first = $strSlice(part, 0, 1);\n\t\tvar last = $strSlice(part, -1);\n\t\tif (\n\t\t\t(\n\t\t\t\t(first === '\"' || first === \"'\" || first === '`')\n\t\t\t\t|| (last === '\"' || last === \"'\" || last === '`')\n\t\t\t)\n\t\t\t&& first !== last\n\t\t) {\n\t\t\tthrow new $SyntaxError('property names with quotes must have matching quotes');\n\t\t}\n\t\tif (part === 'constructor' || !isOwn) {\n\t\t\tskipFurtherCaching = true;\n\t\t}\n\n\t\tintrinsicBaseName += '.' + part;\n\t\tintrinsicRealName = '%' + intrinsicBaseName + '%';\n\n\t\tif (hasOwn(INTRINSICS, intrinsicRealName)) {\n\t\t\tvalue = INTRINSICS[intrinsicRealName];\n\t\t} else if (value != null) {\n\t\t\tif (!(part in value)) {\n\t\t\t\tif (!allowMissing) {\n\t\t\t\t\tthrow new $TypeError('base intrinsic for ' + name + ' exists, but the property is not available.');\n\t\t\t\t}\n\t\t\t\treturn void undefined;\n\t\t\t}\n\t\t\tif ($gOPD && (i + 1) >= parts.length) {\n\t\t\t\tvar desc = $gOPD(value, part);\n\t\t\t\tisOwn = !!desc;\n\n\t\t\t\t// By convention, when a data property is converted to an accessor\n\t\t\t\t// property to emulate a data property that does not suffer from\n\t\t\t\t// the override mistake, that accessor's getter is marked with\n\t\t\t\t// an `originalValue` property. Here, when we detect this, we\n\t\t\t\t// uphold the illusion by pretending to see that original data\n\t\t\t\t// property, i.e., returning the value rather than the getter\n\t\t\t\t// itself.\n\t\t\t\tif (isOwn && 'get' in desc && !('originalValue' in desc.get)) {\n\t\t\t\t\tvalue = desc.get;\n\t\t\t\t} else {\n\t\t\t\t\tvalue = value[part];\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tisOwn = hasOwn(value, part);\n\t\t\t\tvalue = value[part];\n\t\t\t}\n\n\t\t\tif (isOwn && !skipFurtherCaching) {\n\t\t\t\tINTRINSICS[intrinsicRealName] = value;\n\t\t\t}\n\t\t}\n\t}\n\treturn value;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,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O0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZ2V0LWludHJpbnNpYy9pbmRleC5qcz8wMGNlIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIHVuZGVmaW5lZDtcblxudmFyICRTeW50YXhFcnJvciA9IFN5bnRheEVycm9yO1xudmFyICRGdW5jdGlvbiA9IEZ1bmN0aW9uO1xudmFyICRUeXBlRXJyb3IgPSBUeXBlRXJyb3I7XG5cbi8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBjb25zaXN0ZW50LXJldHVyblxudmFyIGdldEV2YWxsZWRDb25zdHJ1Y3RvciA9IGZ1bmN0aW9uIChleHByZXNzaW9uU3ludGF4KSB7XG5cdHRyeSB7XG5cdFx0cmV0dXJuICRGdW5jdGlvbignXCJ1c2Ugc3RyaWN0XCI7IHJldHVybiAoJyArIGV4cHJlc3Npb25TeW50YXggKyAnKS5jb25zdHJ1Y3RvcjsnKSgpO1xuXHR9IGNhdGNoIChlKSB7fVxufTtcblxudmFyICRnT1BEID0gT2JqZWN0LmdldE93blByb3BlcnR5RGVzY3JpcHRvcjtcbmlmICgkZ09QRCkge1xuXHR0cnkge1xuXHRcdCRnT1BEKHt9LCAnJyk7XG5cdH0gY2F0Y2ggKGUpIHtcblx0XHQkZ09QRCA9IG51bGw7IC8vIHRoaXMgaXMgSUUgOCwgd2hpY2ggaGFzIGEgYnJva2VuIGdPUERcblx0fVxufVxuXG52YXIgdGhyb3dUeXBlRXJyb3IgPSBmdW5jdGlvbiAoKSB7XG5cdHRocm93IG5ldyAkVHlwZUVycm9yKCk7XG59O1xudmFyIFRocm93VHlwZUVycm9yID0gJGdPUERcblx0PyAoZnVuY3Rpb24gKCkge1xuXHRcdHRyeSB7XG5cdFx0XHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tdW51c2VkLWV4cHJlc3Npb25zLCBuby1jYWxsZXIsIG5vLXJlc3RyaWN0ZWQtcHJvcGVydGllc1xuXHRcdFx0YXJndW1lbnRzLmNhbGxlZTsgLy8gSUUgOCBkb2VzIG5vdCB0aHJvdyBoZXJlXG5cdFx0XHRyZXR1cm4gdGhyb3dUeXBlRXJyb3I7XG5cdFx0fSBjYXRjaCAoY2FsbGVlVGhyb3dzKSB7XG5cdFx0XHR0cnkge1xuXHRcdFx0XHQvLyBJRSA4IHRocm93cyBvbiBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yKGFyZ3VtZW50cywgJycpXG5cdFx0XHRcdHJldHVybiAkZ09QRChhcmd1bWVudHMsICdjYWxsZWUnKS5nZXQ7XG5cdFx0XHR9IGNhdGNoIChnT1BEdGhyb3dzKSB7XG5cdFx0XHRcdHJldHVybiB0aHJvd1R5cGVFcnJvcjtcblx0XHRcdH1cblx0XHR9XG5cdH0oKSlcblx0OiB0aHJvd1R5cGVFcnJvcjtcblxudmFyIGhhc1N5bWJvbHMgPSByZXF1aXJlKCdoYXMtc3ltYm9scycpKCk7XG5cbnZhciBnZXRQcm90byA9IE9iamVjdC5nZXRQcm90b3R5cGVPZiB8fCBmdW5jdGlvbiAoeCkgeyByZXR1cm4geC5fX3Byb3RvX187IH07IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tcHJvdG9cblxudmFyIG5lZWRzRXZhbCA9IHt9O1xuXG52YXIgVHlwZWRBcnJheSA9IHR5cGVvZiBVaW50OEFycmF5ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IGdldFByb3RvKFVpbnQ4QXJyYXkpO1xuXG52YXIgSU5UUklOU0lDUyA9IHtcblx0JyVBZ2dyZWdhdGVFcnJvciUnOiB0eXBlb2YgQWdncmVnYXRlRXJyb3IgPT09ICd1bmRlZmluZWQnID8gdW5kZWZpbmVkIDogQWdncmVnYXRlRXJyb3IsXG5cdCclQXJyYXklJzogQXJyYXksXG5cdCclQXJyYXlCdWZmZXIlJzogdHlwZW9mIEFycmF5QnVmZmVyID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IEFycmF5QnVmZmVyLFxuXHQnJUFycmF5SXRlcmF0b3JQcm90b3R5cGUlJzogaGFzU3ltYm9scyA/IGdldFByb3RvKFtdW1N5bWJvbC5pdGVyYXRvcl0oKSkgOiB1bmRlZmluZWQsXG5cdCclQXN5bmNGcm9tU3luY0l0ZXJhdG9yUHJvdG90eXBlJSc6IHVuZGVmaW5lZCxcblx0JyVBc3luY0Z1bmN0aW9uJSc6IG5lZWRzRXZhbCxcblx0JyVBc3luY0dlbmVyYXRvciUnOiBuZWVkc0V2YWwsXG5cdCclQXN5bmNHZW5lcmF0b3JGdW5jdGlvbiUnOiBuZWVkc0V2YWwsXG5cdCclQXN5bmNJdGVyYXRvclByb3RvdHlwZSUnOiBuZWVkc0V2YWwsXG5cdCclQXRvbWljcyUnOiB0eXBlb2YgQXRvbWljcyA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBBdG9taWNzLFxuXHQnJUJpZ0ludCUnOiB0eXBlb2YgQmlnSW50ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IEJpZ0ludCxcblx0JyVCb29sZWFuJSc6IEJvb2xlYW4sXG5cdCclRGF0YVZpZXclJzogdHlwZW9mIERhdGFWaWV3ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IERhdGFWaWV3LFxuXHQnJURhdGUlJzogRGF0ZSxcblx0JyVkZWNvZGVVUkklJzogZGVjb2RlVVJJLFxuXHQnJWRlY29kZVVSSUNvbXBvbmVudCUnOiBkZWNvZGVVUklDb21wb25lbnQsXG5cdCclZW5jb2RlVVJJJSc6IGVuY29kZVVSSSxcblx0JyVlbmNvZGVVUklDb21wb25lbnQlJzogZW5jb2RlVVJJQ29tcG9uZW50LFxuXHQnJUVycm9yJSc6IEVycm9yLFxuXHQnJWV2YWwlJzogZXZhbCwgLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1ldmFsXG5cdCclRXZhbEVycm9yJSc6IEV2YWxFcnJvcixcblx0JyVGbG9hdDMyQXJyYXklJzogdHlwZW9mIEZsb2F0MzJBcnJheSA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBGbG9hdDMyQXJyYXksXG5cdCclRmxvYXQ2NEFycmF5JSc6IHR5cGVvZiBGbG9hdDY0QXJyYXkgPT09ICd1bmRlZmluZWQnID8gdW5kZWZpbmVkIDogRmxvYXQ2NEFycmF5LFxuXHQnJUZpbmFsaXphdGlvblJlZ2lzdHJ5JSc6IHR5cGVvZiBGaW5hbGl6YXRpb25SZWdpc3RyeSA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBGaW5hbGl6YXRpb25SZWdpc3RyeSxcblx0JyVGdW5jdGlvbiUnOiAkRnVuY3Rpb24sXG5cdCclR2VuZXJhdG9yRnVuY3Rpb24lJzogbmVlZHNFdmFsLFxuXHQnJUludDhBcnJheSUnOiB0eXBlb2YgSW50OEFycmF5ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IEludDhBcnJheSxcblx0JyVJbnQxNkFycmF5JSc6IHR5cGVvZiBJbnQxNkFycmF5ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IEludDE2QXJyYXksXG5cdCclSW50MzJBcnJheSUnOiB0eXBlb2YgSW50MzJBcnJheSA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBJbnQzMkFycmF5LFxuXHQnJWlzRmluaXRlJSc6IGlzRmluaXRlLFxuXHQnJWlzTmFOJSc6IGlzTmFOLFxuXHQnJUl0ZXJhdG9yUHJvdG90eXBlJSc6IGhhc1N5bWJvbHMgPyBnZXRQcm90byhnZXRQcm90byhbXVtTeW1ib2wuaXRlcmF0b3JdKCkpKSA6IHVuZGVmaW5lZCxcblx0JyVKU09OJSc6IHR5cGVvZiBKU09OID09PSAnb2JqZWN0JyA/IEpTT04gOiB1bmRlZmluZWQsXG5cdCclTWFwJSc6IHR5cGVvZiBNYXAgPT09ICd1bmRlZmluZWQnID8gdW5kZWZpbmVkIDogTWFwLFxuXHQnJU1hcEl0ZXJhdG9yUHJvdG90eXBlJSc6IHR5cGVvZiBNYXAgPT09ICd1bmRlZmluZWQnIHx8ICFoYXNTeW1ib2xzID8gdW5kZWZpbmVkIDogZ2V0UHJvdG8obmV3IE1hcCgpW1N5bWJvbC5pdGVyYXRvcl0oKSksXG5cdCclTWF0aCUnOiBNYXRoLFxuXHQnJU51bWJlciUnOiBOdW1iZXIsXG5cdCclT2JqZWN0JSc6IE9iamVjdCxcblx0JyVwYXJzZUZsb2F0JSc6IHBhcnNlRmxvYXQsXG5cdCclcGFyc2VJbnQlJzogcGFyc2VJbnQsXG5cdCclUHJvbWlzZSUnOiB0eXBlb2YgUHJvbWlzZSA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBQcm9taXNlLFxuXHQnJVByb3h5JSc6IHR5cGVvZiBQcm94eSA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBQcm94eSxcblx0JyVSYW5nZUVycm9yJSc6IFJhbmdlRXJyb3IsXG5cdCclUmVmZXJlbmNlRXJyb3IlJzogUmVmZXJlbmNlRXJyb3IsXG5cdCclUmVmbGVjdCUnOiB0eXBlb2YgUmVmbGVjdCA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBSZWZsZWN0LFxuXHQnJVJlZ0V4cCUnOiBSZWdFeHAsXG5cdCclU2V0JSc6IHR5cGVvZiBTZXQgPT09ICd1bmRlZmluZWQnID8gdW5kZWZpbmVkIDogU2V0LFxuXHQnJVNldEl0ZXJhdG9yUHJvdG90eXBlJSc6IHR5cGVvZiBTZXQgPT09ICd1bmRlZmluZWQnIHx8ICFoYXNTeW1ib2xzID8gdW5kZWZpbmVkIDogZ2V0UHJvdG8obmV3IFNldCgpW1N5bWJvbC5pdGVyYXRvcl0oKSksXG5cdCclU2hhcmVkQXJyYXlCdWZmZXIlJzogdHlwZW9mIFNoYXJlZEFycmF5QnVmZmVyID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IFNoYXJlZEFycmF5QnVmZmVyLFxuXHQnJVN0cmluZyUnOiBTdHJpbmcsXG5cdCclU3RyaW5nSXRlcmF0b3JQcm90b3R5cGUlJzogaGFzU3ltYm9scyA/IGdldFByb3RvKCcnW1N5bWJvbC5pdGVyYXRvcl0oKSkgOiB1bmRlZmluZWQsXG5cdCclU3ltYm9sJSc6IGhhc1N5bWJvbHMgPyBTeW1ib2wgOiB1bmRlZmluZWQsXG5cdCclU3ludGF4RXJyb3IlJzogJFN5bnRheEVycm9yLFxuXHQnJVRocm93VHlwZUVycm9yJSc6IFRocm93VHlwZUVycm9yLFxuXHQnJVR5cGVkQXJyYXklJzogVHlwZWRBcnJheSxcblx0JyVUeXBlRXJyb3IlJzogJFR5cGVFcnJvcixcblx0JyVVaW50OEFycmF5JSc6IHR5cGVvZiBVaW50OEFycmF5ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IFVpbnQ4QXJyYXksXG5cdCclVWludDhDbGFtcGVkQXJyYXklJzogdHlwZW9mIFVpbnQ4Q2xhbXBlZEFycmF5ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IFVpbnQ4Q2xhbXBlZEFycmF5LFxuXHQnJVVpbnQxNkFycmF5JSc6IHR5cGVvZiBVaW50MTZBcnJheSA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBVaW50MTZBcnJheSxcblx0JyVVaW50MzJBcnJheSUnOiB0eXBlb2YgVWludDMyQXJyYXkgPT09ICd1bmRlZmluZWQnID8gdW5kZWZpbmVkIDogVWludDMyQXJyYXksXG5cdCclVVJJRXJyb3IlJzogVVJJRXJyb3IsXG5cdCclV2Vha01hcCUnOiB0eXBlb2YgV2Vha01hcCA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBXZWFrTWFwLFxuXHQnJVdlYWtSZWYlJzogdHlwZW9mIFdlYWtSZWYgPT09ICd1bmRlZmluZWQnID8gdW5kZWZpbmVkIDogV2Vha1JlZixcblx0JyVXZWFrU2V0JSc6IHR5cGVvZiBXZWFrU2V0ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IFdlYWtTZXRcbn07XG5cbnZhciBkb0V2YWwgPSBmdW5jdGlvbiBkb0V2YWwobmFtZSkge1xuXHR2YXIgdmFsdWU7XG5cdGlmIChuYW1lID09PSAnJUFzeW5jRnVuY3Rpb24lJykge1xuXHRcdHZhbHVlID0gZ2V0RXZhbGxlZENvbnN0cnVjdG9yKCdhc3luYyBmdW5jdGlvbiAoKSB7fScpO1xuXHR9IGVsc2UgaWYgKG5hbWUgPT09ICclR2VuZXJhdG9yRnVuY3Rpb24lJykge1xuXHRcdHZhbHVlID0gZ2V0RXZhbGxlZENvbnN0cnVjdG9yKCdmdW5jdGlvbiogKCkge30nKTtcblx0fSBlbHNlIGlmIChuYW1lID09PSAnJUFzeW5jR2VuZXJhdG9yRnVuY3Rpb24lJykge1xuXHRcdHZhbHVlID0gZ2V0RXZhbGxlZENvbnN0cnVjdG9yKCdhc3luYyBmdW5jdGlvbiogKCkge30nKTtcblx0fSBlbHNlIGlmIChuYW1lID09PSAnJUFzeW5jR2VuZXJhdG9yJScpIHtcblx0XHR2YXIgZm4gPSBkb0V2YWwoJyVBc3luY0dlbmVyYXRvckZ1bmN0aW9uJScpO1xuXHRcdGlmIChmbikge1xuXHRcdFx0dmFsdWUgPSBmbi5wcm90b3R5cGU7XG5cdFx0fVxuXHR9IGVsc2UgaWYgKG5hbWUgPT09ICclQXN5bmNJdGVyYXRvclByb3RvdHlwZSUnKSB7XG5cdFx0dmFyIGdlbiA9IGRvRXZhbCgnJUFzeW5jR2VuZXJhdG9yJScpO1xuXHRcdGlmIChnZW4pIHtcblx0XHRcdHZhbHVlID0gZ2V0UHJvdG8oZ2VuLnByb3RvdHlwZSk7XG5cdFx0fVxuXHR9XG5cblx0SU5UUklOU0lDU1tuYW1lXSA9IHZhbHVlO1xuXG5cdHJldHVybiB2YWx1ZTtcbn07XG5cbnZhciBMRUdBQ1lfQUxJQVNFUyA9IHtcblx0JyVBcnJheUJ1ZmZlclByb3RvdHlwZSUnOiBbJ0FycmF5QnVmZmVyJywgJ3Byb3RvdHlwZSddLFxuXHQnJUFycmF5UHJvdG90eXBlJSc6IFsnQXJyYXknLCAncHJvdG90eXBlJ10sXG5cdCclQXJyYXlQcm90b19lbnRyaWVzJSc6IFsnQXJyYXknLCAncHJvdG90eXBlJywgJ2VudHJpZXMnXSxcblx0JyVBcnJheVByb3RvX2ZvckVhY2glJzogWydBcnJheScsICdwcm90b3R5cGUnLCAnZm9yRWFjaCddLFxuXHQnJUFycmF5UHJvdG9fa2V5cyUnOiBbJ0FycmF5JywgJ3Byb3RvdHlwZScsICdrZXlzJ10sXG5cdCclQXJyYXlQcm90b192YWx1ZXMlJzogWydBcnJheScsICdwcm90b3R5cGUnLCAndmFsdWVzJ10sXG5cdCclQXN5bmNGdW5jdGlvblByb3RvdHlwZSUnOiBbJ0FzeW5jRnVuY3Rpb24nLCAncHJvdG90eXBlJ10sXG5cdCclQXN5bmNHZW5lcmF0b3IlJzogWydBc3luY0dlbmVyYXRvckZ1bmN0aW9uJywgJ3Byb3RvdHlwZSddLFxuXHQnJUFzeW5jR2VuZXJhdG9yUHJvdG90eXBlJSc6IFsnQXN5bmNHZW5lcmF0b3JGdW5jdGlvbicsICdwcm90b3R5cGUnLCAncHJvdG90eXBlJ10sXG5cdCclQm9vbGVhblByb3RvdHlwZSUnOiBbJ0Jvb2xlYW4nLCAncHJvdG90eXBlJ10sXG5cdCclRGF0YVZpZXdQcm90b3R5cGUlJzogWydEYXRhVmlldycsICdwcm90b3R5cGUnXSxcblx0JyVEYXRlUHJvdG90eXBlJSc6IFsnRGF0ZScsICdwcm90b3R5cGUnXSxcblx0JyVFcnJvclByb3RvdHlwZSUnOiBbJ0Vycm9yJywgJ3Byb3RvdHlwZSddLFxuXHQnJUV2YWxFcnJvclByb3RvdHlwZSUnOiBbJ0V2YWxFcnJvcicsICdwcm90b3R5cGUnXSxcblx0JyVGbG9hdDMyQXJyYXlQcm90b3R5cGUlJzogWydGbG9hdDMyQXJyYXknLCAncHJvdG90eXBlJ10sXG5cdCclRmxvYXQ2NEFycmF5UHJvdG90eXBlJSc6IFsnRmxvYXQ2NEFycmF5JywgJ3Byb3RvdHlwZSddLFxuXHQnJUZ1bmN0aW9uUHJvdG90eXBlJSc6IFsnRnVuY3Rpb24nLCAncHJvdG90eXBlJ10sXG5cdCclR2VuZXJhdG9yJSc6IFsnR2VuZXJhdG9yRnVuY3Rpb24nLCAncHJvdG90eXBlJ10sXG5cdCclR2VuZXJhdG9yUHJvdG90eXBlJSc6IFsnR2VuZXJhdG9yRnVuY3Rpb24nLCAncHJvdG90eXBlJywgJ3Byb3RvdHlwZSddLFxuXHQnJUludDhBcnJheVByb3RvdHlwZSUnOiBbJ0ludDhBcnJheScsICdwcm90b3R5cGUnXSxcblx0JyVJbnQxNkFycmF5UHJvdG90eXBlJSc6IFsnSW50MTZBcnJheScsICdwcm90b3R5cGUnXSxcblx0JyVJbnQzMkFycmF5UHJvdG90eXBlJSc6IFsnSW50MzJBcnJheScsICdwcm90b3R5cGUnXSxcblx0JyVKU09OUGFyc2UlJzogWydKU09OJywgJ3BhcnNlJ10sXG5cdCclSlNPTlN0cmluZ2lmeSUnOiBbJ0pTT04nLCAnc3RyaW5naWZ5J10sXG5cdCclTWFwUHJvdG90eXBlJSc6IFsnTWFwJywgJ3Byb3RvdHlwZSddLFxuXHQnJU51bWJlclByb3RvdHlwZSUnOiBbJ051bWJlcicsICdwcm90b3R5cGUnXSxcblx0JyVPYmplY3RQcm90b3R5cGUlJzogWydPYmplY3QnLCAncHJvdG90eXBlJ10sXG5cdCclT2JqUHJvdG9fdG9TdHJpbmclJzogWydPYmplY3QnLCAncHJvdG90eXBlJywgJ3RvU3RyaW5nJ10sXG5cdCclT2JqUHJvdG9fdmFsdWVPZiUnOiBbJ09iamVjdCcsICdwcm90b3R5cGUnLCAndmFsdWVPZiddLFxuXHQnJVByb21pc2VQcm90b3R5cGUlJzogWydQcm9taXNlJywgJ3Byb3RvdHlwZSddLFxuXHQnJVByb21pc2VQcm90b190aGVuJSc6IFsnUHJvbWlzZScsICdwcm90b3R5cGUnLCAndGhlbiddLFxuXHQnJVByb21pc2VfYWxsJSc6IFsnUHJvbWlzZScsICdhbGwnXSxcblx0JyVQcm9taXNlX3JlamVjdCUnOiBbJ1Byb21pc2UnLCAncmVqZWN0J10sXG5cdCclUHJvbWlzZV9yZXNvbHZlJSc6IFsnUHJvbWlzZScsICdyZXNvbHZlJ10sXG5cdCclUmFuZ2VFcnJvclByb3RvdHlwZSUnOiBbJ1JhbmdlRXJyb3InLCAncHJvdG90eXBlJ10sXG5cdCclUmVmZXJlbmNlRXJyb3JQcm90b3R5cGUlJzogWydSZWZlcmVuY2VFcnJvcicsICdwcm90b3R5cGUnXSxcblx0JyVSZWdFeHBQcm90b3R5cGUlJzogWydSZWdFeHAnLCAncHJvdG90eXBlJ10sXG5cdCclU2V0UHJvdG90eXBlJSc6IFsnU2V0JywgJ3Byb3RvdHlwZSddLFxuXHQnJVNoYXJlZEFycmF5QnVmZmVyUHJvdG90eXBlJSc6IFsnU2hhcmVkQXJyYXlCdWZmZXInLCAncHJvdG90eXBlJ10sXG5cdCclU3RyaW5nUHJvdG90eXBlJSc6IFsnU3RyaW5nJywgJ3Byb3RvdHlwZSddLFxuXHQnJVN5bWJvbFByb3RvdHlwZSUnOiBbJ1N5bWJvbCcsICdwcm90b3R5cGUnXSxcblx0JyVTeW50YXhFcnJvclByb3RvdHlwZSUnOiBbJ1N5bnRheEVycm9yJywgJ3Byb3RvdHlwZSddLFxuXHQnJVR5cGVkQXJyYXlQcm90b3R5cGUlJzogWydUeXBlZEFycmF5JywgJ3Byb3RvdHlwZSddLFxuXHQnJVR5cGVFcnJvclByb3RvdHlwZSUnOiBbJ1R5cGVFcnJvcicsICdwcm90b3R5cGUnXSxcblx0JyVVaW50OEFycmF5UHJvdG90eXBlJSc6IFsnVWludDhBcnJheScsICdwcm90b3R5cGUnXSxcblx0JyVVaW50OENsYW1wZWRBcnJheVByb3RvdHlwZSUnOiBbJ1VpbnQ4Q2xhbXBlZEFycmF5JywgJ3Byb3RvdHlwZSddLFxuXHQnJVVpbnQxNkFycmF5UHJvdG90eXBlJSc6IFsnVWludDE2QXJyYXknLCAncHJvdG90eXBlJ10sXG5cdCclVWludDMyQXJyYXlQcm90b3R5cGUlJzogWydVaW50MzJBcnJheScsICdwcm90b3R5cGUnXSxcblx0JyVVUklFcnJvclByb3RvdHlwZSUnOiBbJ1VSSUVycm9yJywgJ3Byb3RvdHlwZSddLFxuXHQnJVdlYWtNYXBQcm90b3R5cGUlJzogWydXZWFrTWFwJywgJ3Byb3RvdHlwZSddLFxuXHQnJVdlYWtTZXRQcm90b3R5cGUlJzogWydXZWFrU2V0JywgJ3Byb3RvdHlwZSddXG59O1xuXG52YXIgYmluZCA9IHJlcXVpcmUoJ2Z1bmN0aW9uLWJpbmQnKTtcbnZhciBoYXNPd24gPSByZXF1aXJlKCdoYXMnKTtcbnZhciAkY29uY2F0ID0gYmluZC5jYWxsKEZ1bmN0aW9uLmNhbGwsIEFycmF5LnByb3RvdHlwZS5jb25jYXQpO1xudmFyICRzcGxpY2VBcHBseSA9IGJpbmQuY2FsbChGdW5jdGlvbi5hcHBseSwgQXJyYXkucHJvdG90eXBlLnNwbGljZSk7XG52YXIgJHJlcGxhY2UgPSBiaW5kLmNhbGwoRnVuY3Rpb24uY2FsbCwgU3RyaW5nLnByb3RvdHlwZS5yZXBsYWNlKTtcbnZhciAkc3RyU2xpY2UgPSBiaW5kLmNhbGwoRnVuY3Rpb24uY2FsbCwgU3RyaW5nLnByb3RvdHlwZS5zbGljZSk7XG5cbi8qIGFkYXB0ZWQgZnJvbSBodHRwczovL2dpdGh1Yi5jb20vbG9kYXNoL2xvZGFzaC9ibG9iLzQuMTcuMTUvZGlzdC9sb2Rhc2guanMjTDY3MzUtTDY3NDQgKi9cbnZhciByZVByb3BOYW1lID0gL1teJS5bXFxdXSt8XFxbKD86KC0/XFxkKyg/OlxcLlxcZCspPyl8KFtcIiddKSgoPzooPyFcXDIpW15cXFxcXXxcXFxcLikqPylcXDIpXFxdfCg/PSg/OlxcLnxcXFtcXF0pKD86XFwufFxcW1xcXXwlJCkpL2c7XG52YXIgcmVFc2NhcGVDaGFyID0gL1xcXFwoXFxcXCk/L2c7IC8qKiBVc2VkIHRvIG1hdGNoIGJhY2tzbGFzaGVzIGluIHByb3BlcnR5IHBhdGhzLiAqL1xudmFyIHN0cmluZ1RvUGF0aCA9IGZ1bmN0aW9uIHN0cmluZ1RvUGF0aChzdHJpbmcpIHtcblx0dmFyIGZpcnN0ID0gJHN0clNsaWNlKHN0cmluZywgMCwgMSk7XG5cdHZhciBsYXN0ID0gJHN0clNsaWNlKHN0cmluZywgLTEpO1xuXHRpZiAoZmlyc3QgPT09ICclJyAmJiBsYXN0ICE9PSAnJScpIHtcblx0XHR0aHJvdyBuZXcgJFN5bnRheEVycm9yKCdpbnZhbGlkIGludHJpbnNpYyBzeW50YXgsIGV4cGVjdGVkIGNsb3NpbmcgYCVgJyk7XG5cdH0gZWxzZSBpZiAobGFzdCA9PT0gJyUnICYmIGZpcnN0ICE9PSAnJScpIHtcblx0XHR0aHJvdyBuZXcgJFN5bnRheEVycm9yKCdpbnZhbGlkIGludHJpbnNpYyBzeW50YXgsIGV4cGVjdGVkIG9wZW5pbmcgYCVgJyk7XG5cdH1cblx0dmFyIHJlc3VsdCA9IFtdO1xuXHQkcmVwbGFjZShzdHJpbmcsIHJlUHJvcE5hbWUsIGZ1bmN0aW9uIChtYXRjaCwgbnVtYmVyLCBxdW90ZSwgc3ViU3RyaW5nKSB7XG5cdFx0cmVzdWx0W3Jlc3VsdC5sZW5ndGhdID0gcXVvdGUgPyAkcmVwbGFjZShzdWJTdHJpbmcsIHJlRXNjYXBlQ2hhciwgJyQxJykgOiBudW1iZXIgfHwgbWF0Y2g7XG5cdH0pO1xuXHRyZXR1cm4gcmVzdWx0O1xufTtcbi8qIGVuZCBhZGFwdGF0aW9uICovXG5cbnZhciBnZXRCYXNlSW50cmluc2ljID0gZnVuY3Rpb24gZ2V0QmFzZUludHJpbnNpYyhuYW1lLCBhbGxvd01pc3NpbmcpIHtcblx0dmFyIGludHJpbnNpY05hbWUgPSBuYW1lO1xuXHR2YXIgYWxpYXM7XG5cdGlmIChoYXNPd24oTEVHQUNZX0FMSUFTRVMsIGludHJpbnNpY05hbWUpKSB7XG5cdFx0YWxpYXMgPSBMRUdBQ1lfQUxJQVNFU1tpbnRyaW5zaWNOYW1lXTtcblx0XHRpbnRyaW5zaWNOYW1lID0gJyUnICsgYWxpYXNbMF0gKyAnJSc7XG5cdH1cblxuXHRpZiAoaGFzT3duKElOVFJJTlNJQ1MsIGludHJpbnNpY05hbWUpKSB7XG5cdFx0dmFyIHZhbHVlID0gSU5UUklOU0lDU1tpbnRyaW5zaWNOYW1lXTtcblx0XHRpZiAodmFsdWUgPT09IG5lZWRzRXZhbCkge1xuXHRcdFx0dmFsdWUgPSBkb0V2YWwoaW50cmluc2ljTmFtZSk7XG5cdFx0fVxuXHRcdGlmICh0eXBlb2YgdmFsdWUgPT09ICd1bmRlZmluZWQnICYmICFhbGxvd01pc3NpbmcpIHtcblx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdpbnRyaW5zaWMgJyArIG5hbWUgKyAnIGV4aXN0cywgYnV0IGlzIG5vdCBhdmFpbGFibGUuIFBsZWFzZSBmaWxlIGFuIGlzc3VlIScpO1xuXHRcdH1cblxuXHRcdHJldHVybiB7XG5cdFx0XHRhbGlhczogYWxpYXMsXG5cdFx0XHRuYW1lOiBpbnRyaW5zaWNOYW1lLFxuXHRcdFx0dmFsdWU6IHZhbHVlXG5cdFx0fTtcblx0fVxuXG5cdHRocm93IG5ldyAkU3ludGF4RXJyb3IoJ2ludHJpbnNpYyAnICsgbmFtZSArICcgZG9lcyBub3QgZXhpc3QhJyk7XG59O1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIEdldEludHJpbnNpYyhuYW1lLCBhbGxvd01pc3NpbmcpIHtcblx0aWYgKHR5cGVvZiBuYW1lICE9PSAnc3RyaW5nJyB8fCBuYW1lLmxlbmd0aCA9PT0gMCkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdpbnRyaW5zaWMgbmFtZSBtdXN0IGJlIGEgbm9uLWVtcHR5IHN0cmluZycpO1xuXHR9XG5cdGlmIChhcmd1bWVudHMubGVuZ3RoID4gMSAmJiB0eXBlb2YgYWxsb3dNaXNzaW5nICE9PSAnYm9vbGVhbicpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignXCJhbGxvd01pc3NpbmdcIiBhcmd1bWVudCBtdXN0IGJlIGEgYm9vbGVhbicpO1xuXHR9XG5cblx0dmFyIHBhcnRzID0gc3RyaW5nVG9QYXRoKG5hbWUpO1xuXHR2YXIgaW50cmluc2ljQmFzZU5hbWUgPSBwYXJ0cy5sZW5ndGggPiAwID8gcGFydHNbMF0gOiAnJztcblxuXHR2YXIgaW50cmluc2ljID0gZ2V0QmFzZUludHJpbnNpYygnJScgKyBpbnRyaW5zaWNCYXNlTmFtZSArICclJywgYWxsb3dNaXNzaW5nKTtcblx0dmFyIGludHJpbnNpY1JlYWxOYW1lID0gaW50cmluc2ljLm5hbWU7XG5cdHZhciB2YWx1ZSA9IGludHJpbnNpYy52YWx1ZTtcblx0dmFyIHNraXBGdXJ0aGVyQ2FjaGluZyA9IGZhbHNlO1xuXG5cdHZhciBhbGlhcyA9IGludHJpbnNpYy5hbGlhcztcblx0aWYgKGFsaWFzKSB7XG5cdFx0aW50cmluc2ljQmFzZU5hbWUgPSBhbGlhc1swXTtcblx0XHQkc3BsaWNlQXBwbHkocGFydHMsICRjb25jYXQoWzAsIDFdLCBhbGlhcykpO1xuXHR9XG5cblx0Zm9yICh2YXIgaSA9IDEsIGlzT3duID0gdHJ1ZTsgaSA8IHBhcnRzLmxlbmd0aDsgaSArPSAxKSB7XG5cdFx0dmFyIHBhcnQgPSBwYXJ0c1tpXTtcblx0XHR2YXIgZmlyc3QgPSAkc3RyU2xpY2UocGFydCwgMCwgMSk7XG5cdFx0dmFyIGxhc3QgPSAkc3RyU2xpY2UocGFydCwgLTEpO1xuXHRcdGlmIChcblx0XHRcdChcblx0XHRcdFx0KGZpcnN0ID09PSAnXCInIHx8IGZpcnN0ID09PSBcIidcIiB8fCBmaXJzdCA9PT0gJ2AnKVxuXHRcdFx0XHR8fCAobGFzdCA9PT0gJ1wiJyB8fCBsYXN0ID09PSBcIidcIiB8fCBsYXN0ID09PSAnYCcpXG5cdFx0XHQpXG5cdFx0XHQmJiBmaXJzdCAhPT0gbGFzdFxuXHRcdCkge1xuXHRcdFx0dGhyb3cgbmV3ICRTeW50YXhFcnJvcigncHJvcGVydHkgbmFtZXMgd2l0aCBxdW90ZXMgbXVzdCBoYXZlIG1hdGNoaW5nIHF1b3RlcycpO1xuXHRcdH1cblx0XHRpZiAocGFydCA9PT0gJ2NvbnN0cnVjdG9yJyB8fCAhaXNPd24pIHtcblx0XHRcdHNraXBGdXJ0aGVyQ2FjaGluZyA9IHRydWU7XG5cdFx0fVxuXG5cdFx0aW50cmluc2ljQmFzZU5hbWUgKz0gJy4nICsgcGFydDtcblx0XHRpbnRyaW5zaWNSZWFsTmFtZSA9ICclJyArIGludHJpbnNpY0Jhc2VOYW1lICsgJyUnO1xuXG5cdFx0aWYgKGhhc093bihJTlRSSU5TSUNTLCBpbnRyaW5zaWNSZWFsTmFtZSkpIHtcblx0XHRcdHZhbHVlID0gSU5UUklOU0lDU1tpbnRyaW5zaWNSZWFsTmFtZV07XG5cdFx0fSBlbHNlIGlmICh2YWx1ZSAhPSBudWxsKSB7XG5cdFx0XHRpZiAoIShwYXJ0IGluIHZhbHVlKSkge1xuXHRcdFx0XHRpZiAoIWFsbG93TWlzc2luZykge1xuXHRcdFx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdiYXNlIGludHJpbnNpYyBmb3IgJyArIG5hbWUgKyAnIGV4aXN0cywgYnV0IHRoZSBwcm9wZXJ0eSBpcyBub3QgYXZhaWxhYmxlLicpO1xuXHRcdFx0XHR9XG5cdFx0XHRcdHJldHVybiB2b2lkIHVuZGVmaW5lZDtcblx0XHRcdH1cblx0XHRcdGlmICgkZ09QRCAmJiAoaSArIDEpID49IHBhcnRzLmxlbmd0aCkge1xuXHRcdFx0XHR2YXIgZGVzYyA9ICRnT1BEKHZhbHVlLCBwYXJ0KTtcblx0XHRcdFx0aXNPd24gPSAhIWRlc2M7XG5cblx0XHRcdFx0Ly8gQnkgY29udmVudGlvbiwgd2hlbiBhIGRhdGEgcHJvcGVydHkgaXMgY29udmVydGVkIHRvIGFuIGFjY2Vzc29yXG5cdFx0XHRcdC8vIHByb3BlcnR5IHRvIGVtdWxhdGUgYSBkYXRhIHByb3BlcnR5IHRoYXQgZG9lcyBub3Qgc3VmZmVyIGZyb21cblx0XHRcdFx0Ly8gdGhlIG92ZXJyaWRlIG1pc3Rha2UsIHRoYXQgYWNjZXNzb3IncyBnZXR0ZXIgaXMgbWFya2VkIHdpdGhcblx0XHRcdFx0Ly8gYW4gYG9yaWdpbmFsVmFsdWVgIHByb3BlcnR5LiBIZXJlLCB3aGVuIHdlIGRldGVjdCB0aGlzLCB3ZVxuXHRcdFx0XHQvLyB1cGhvbGQgdGhlIGlsbHVzaW9uIGJ5IHByZXRlbmRpbmcgdG8gc2VlIHRoYXQgb3JpZ2luYWwgZGF0YVxuXHRcdFx0XHQvLyBwcm9wZXJ0eSwgaS5lLiwgcmV0dXJuaW5nIHRoZSB2YWx1ZSByYXRoZXIgdGhhbiB0aGUgZ2V0dGVyXG5cdFx0XHRcdC8vIGl0c2VsZi5cblx0XHRcdFx0aWYgKGlzT3duICYmICdnZXQnIGluIGRlc2MgJiYgISgnb3JpZ2luYWxWYWx1ZScgaW4gZGVzYy5nZXQpKSB7XG5cdFx0XHRcdFx0dmFsdWUgPSBkZXNjLmdldDtcblx0XHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0XHR2YWx1ZSA9IHZhbHVlW3BhcnRdO1xuXHRcdFx0XHR9XG5cdFx0XHR9IGVsc2Uge1xuXHRcdFx0XHRpc093biA9IGhhc093bih2YWx1ZSwgcGFydCk7XG5cdFx0XHRcdHZhbHVlID0gdmFsdWVbcGFydF07XG5cdFx0XHR9XG5cblx0XHRcdGlmIChpc093biAmJiAhc2tpcEZ1cnRoZXJDYWNoaW5nKSB7XG5cdFx0XHRcdElOVFJJTlNJQ1NbaW50cmluc2ljUmVhbE5hbWVdID0gdmFsdWU7XG5cdFx0XHR9XG5cdFx0fVxuXHR9XG5cdHJldHVybiB2YWx1ZTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///210\n")},1405:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar origSymbol = typeof Symbol !== 'undefined' && Symbol;\nvar hasSymbolSham = __webpack_require__(5419);\n\nmodule.exports = function hasNativeSymbols() {\n\tif (typeof origSymbol !== 'function') { return false; }\n\tif (typeof Symbol !== 'function') { return false; }\n\tif (typeof origSymbol('foo') !== 'symbol') { return false; }\n\tif (typeof Symbol('bar') !== 'symbol') { return false; }\n\n\treturn hasSymbolSham();\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTQwNS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBLG9CQUFvQixtQkFBTyxDQUFDLElBQVM7O0FBRXJDO0FBQ0EseUNBQXlDO0FBQ3pDLHFDQUFxQztBQUNyQyw4Q0FBOEM7QUFDOUMsMENBQTBDOztBQUUxQztBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9oYXMtc3ltYm9scy9pbmRleC5qcz81MTU2Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIG9yaWdTeW1ib2wgPSB0eXBlb2YgU3ltYm9sICE9PSAndW5kZWZpbmVkJyAmJiBTeW1ib2w7XG52YXIgaGFzU3ltYm9sU2hhbSA9IHJlcXVpcmUoJy4vc2hhbXMnKTtcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBoYXNOYXRpdmVTeW1ib2xzKCkge1xuXHRpZiAodHlwZW9mIG9yaWdTeW1ib2wgIT09ICdmdW5jdGlvbicpIHsgcmV0dXJuIGZhbHNlOyB9XG5cdGlmICh0eXBlb2YgU3ltYm9sICE9PSAnZnVuY3Rpb24nKSB7IHJldHVybiBmYWxzZTsgfVxuXHRpZiAodHlwZW9mIG9yaWdTeW1ib2woJ2ZvbycpICE9PSAnc3ltYm9sJykgeyByZXR1cm4gZmFsc2U7IH1cblx0aWYgKHR5cGVvZiBTeW1ib2woJ2JhcicpICE9PSAnc3ltYm9sJykgeyByZXR1cm4gZmFsc2U7IH1cblxuXHRyZXR1cm4gaGFzU3ltYm9sU2hhbSgpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///1405\n")},5419:function(module){"use strict";eval("\n\n/* eslint complexity: [2, 18], max-statements: [2, 33] */\nmodule.exports = function hasSymbols() {\n\tif (typeof Symbol !== 'function' || typeof Object.getOwnPropertySymbols !== 'function') { return false; }\n\tif (typeof Symbol.iterator === 'symbol') { return true; }\n\n\tvar obj = {};\n\tvar sym = Symbol('test');\n\tvar symObj = Object(sym);\n\tif (typeof sym === 'string') { return false; }\n\n\tif (Object.prototype.toString.call(sym) !== '[object Symbol]') { return false; }\n\tif (Object.prototype.toString.call(symObj) !== '[object Symbol]') { return false; }\n\n\t// temp disabled per https://github.com/ljharb/object.assign/issues/17\n\t// if (sym instanceof Symbol) { return false; }\n\t// temp disabled per https://github.com/WebReflection/get-own-property-symbols/issues/4\n\t// if (!(symObj instanceof Symbol)) { return false; }\n\n\t// if (typeof Symbol.prototype.toString !== 'function') { return false; }\n\t// if (String(sym) !== Symbol.prototype.toString.call(sym)) { return false; }\n\n\tvar symVal = 42;\n\tobj[sym] = symVal;\n\tfor (sym in obj) { return false; } // eslint-disable-line no-restricted-syntax, no-unreachable-loop\n\tif (typeof Object.keys === 'function' && Object.keys(obj).length !== 0) { return false; }\n\n\tif (typeof Object.getOwnPropertyNames === 'function' && Object.getOwnPropertyNames(obj).length !== 0) { return false; }\n\n\tvar syms = Object.getOwnPropertySymbols(obj);\n\tif (syms.length !== 1 || syms[0] !== sym) { return false; }\n\n\tif (!Object.prototype.propertyIsEnumerable.call(obj, sym)) { return false; }\n\n\tif (typeof Object.getOwnPropertyDescriptor === 'function') {\n\t\tvar descriptor = Object.getOwnPropertyDescriptor(obj, sym);\n\t\tif (descriptor.value !== symVal || descriptor.enumerable !== true) { return false; }\n\t}\n\n\treturn true;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTQxOS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EsMkZBQTJGO0FBQzNGLDRDQUE0Qzs7QUFFNUM7QUFDQTtBQUNBO0FBQ0EsZ0NBQWdDOztBQUVoQyxrRUFBa0U7QUFDbEUscUVBQXFFOztBQUVyRTtBQUNBLGlDQUFpQztBQUNqQztBQUNBLHVDQUF1Qzs7QUFFdkMsMkRBQTJEO0FBQzNELCtEQUErRDs7QUFFL0Q7QUFDQTtBQUNBLG9CQUFvQixnQkFBZ0I7QUFDcEMsMkVBQTJFOztBQUUzRSx5R0FBeUc7O0FBRXpHO0FBQ0EsNkNBQTZDOztBQUU3Qyw4REFBOEQ7O0FBRTlEO0FBQ0E7QUFDQSx1RUFBdUU7QUFDdkU7O0FBRUE7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvaGFzLXN5bWJvbHMvc2hhbXMuanM/MTY5NiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbi8qIGVzbGludCBjb21wbGV4aXR5OiBbMiwgMThdLCBtYXgtc3RhdGVtZW50czogWzIsIDMzXSAqL1xubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBoYXNTeW1ib2xzKCkge1xuXHRpZiAodHlwZW9mIFN5bWJvbCAhPT0gJ2Z1bmN0aW9uJyB8fCB0eXBlb2YgT2JqZWN0LmdldE93blByb3BlcnR5U3ltYm9scyAhPT0gJ2Z1bmN0aW9uJykgeyByZXR1cm4gZmFsc2U7IH1cblx0aWYgKHR5cGVvZiBTeW1ib2wuaXRlcmF0b3IgPT09ICdzeW1ib2wnKSB7IHJldHVybiB0cnVlOyB9XG5cblx0dmFyIG9iaiA9IHt9O1xuXHR2YXIgc3ltID0gU3ltYm9sKCd0ZXN0Jyk7XG5cdHZhciBzeW1PYmogPSBPYmplY3Qoc3ltKTtcblx0aWYgKHR5cGVvZiBzeW0gPT09ICdzdHJpbmcnKSB7IHJldHVybiBmYWxzZTsgfVxuXG5cdGlmIChPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nLmNhbGwoc3ltKSAhPT0gJ1tvYmplY3QgU3ltYm9sXScpIHsgcmV0dXJuIGZhbHNlOyB9XG5cdGlmIChPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nLmNhbGwoc3ltT2JqKSAhPT0gJ1tvYmplY3QgU3ltYm9sXScpIHsgcmV0dXJuIGZhbHNlOyB9XG5cblx0Ly8gdGVtcCBkaXNhYmxlZCBwZXIgaHR0cHM6Ly9naXRodWIuY29tL2xqaGFyYi9vYmplY3QuYXNzaWduL2lzc3Vlcy8xN1xuXHQvLyBpZiAoc3ltIGluc3RhbmNlb2YgU3ltYm9sKSB7IHJldHVybiBmYWxzZTsgfVxuXHQvLyB0ZW1wIGRpc2FibGVkIHBlciBodHRwczovL2dpdGh1Yi5jb20vV2ViUmVmbGVjdGlvbi9nZXQtb3duLXByb3BlcnR5LXN5bWJvbHMvaXNzdWVzLzRcblx0Ly8gaWYgKCEoc3ltT2JqIGluc3RhbmNlb2YgU3ltYm9sKSkgeyByZXR1cm4gZmFsc2U7IH1cblxuXHQvLyBpZiAodHlwZW9mIFN5bWJvbC5wcm90b3R5cGUudG9TdHJpbmcgIT09ICdmdW5jdGlvbicpIHsgcmV0dXJuIGZhbHNlOyB9XG5cdC8vIGlmIChTdHJpbmcoc3ltKSAhPT0gU3ltYm9sLnByb3RvdHlwZS50b1N0cmluZy5jYWxsKHN5bSkpIHsgcmV0dXJuIGZhbHNlOyB9XG5cblx0dmFyIHN5bVZhbCA9IDQyO1xuXHRvYmpbc3ltXSA9IHN5bVZhbDtcblx0Zm9yIChzeW0gaW4gb2JqKSB7IHJldHVybiBmYWxzZTsgfSAvLyBlc2xpbnQtZGlzYWJsZS1saW5lIG5vLXJlc3RyaWN0ZWQtc3ludGF4LCBuby11bnJlYWNoYWJsZS1sb29wXG5cdGlmICh0eXBlb2YgT2JqZWN0LmtleXMgPT09ICdmdW5jdGlvbicgJiYgT2JqZWN0LmtleXMob2JqKS5sZW5ndGggIT09IDApIHsgcmV0dXJuIGZhbHNlOyB9XG5cblx0aWYgKHR5cGVvZiBPYmplY3QuZ2V0T3duUHJvcGVydHlOYW1lcyA9PT0gJ2Z1bmN0aW9uJyAmJiBPYmplY3QuZ2V0T3duUHJvcGVydHlOYW1lcyhvYmopLmxlbmd0aCAhPT0gMCkgeyByZXR1cm4gZmFsc2U7IH1cblxuXHR2YXIgc3ltcyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eVN5bWJvbHMob2JqKTtcblx0aWYgKHN5bXMubGVuZ3RoICE9PSAxIHx8IHN5bXNbMF0gIT09IHN5bSkgeyByZXR1cm4gZmFsc2U7IH1cblxuXHRpZiAoIU9iamVjdC5wcm90b3R5cGUucHJvcGVydHlJc0VudW1lcmFibGUuY2FsbChvYmosIHN5bSkpIHsgcmV0dXJuIGZhbHNlOyB9XG5cblx0aWYgKHR5cGVvZiBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yID09PSAnZnVuY3Rpb24nKSB7XG5cdFx0dmFyIGRlc2NyaXB0b3IgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yKG9iaiwgc3ltKTtcblx0XHRpZiAoZGVzY3JpcHRvci52YWx1ZSAhPT0gc3ltVmFsIHx8IGRlc2NyaXB0b3IuZW51bWVyYWJsZSAhPT0gdHJ1ZSkgeyByZXR1cm4gZmFsc2U7IH1cblx0fVxuXG5cdHJldHVybiB0cnVlO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///5419\n")},6410:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar hasSymbols = __webpack_require__(5419);\n\nmodule.exports = function hasToStringTagShams() {\n\treturn hasSymbols() && !!Symbol.toStringTag;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNjQxMC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixpQkFBaUIsbUJBQU8sQ0FBQyxJQUFtQjs7QUFFNUM7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9oYXMtdG9zdHJpbmd0YWcvc2hhbXMuanM/MDdhNCJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBoYXNTeW1ib2xzID0gcmVxdWlyZSgnaGFzLXN5bWJvbHMvc2hhbXMnKTtcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBoYXNUb1N0cmluZ1RhZ1NoYW1zKCkge1xuXHRyZXR1cm4gaGFzU3ltYm9scygpICYmICEhU3ltYm9sLnRvU3RyaW5nVGFnO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///6410\n")},7642:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar bind = __webpack_require__(8612);\n\nmodule.exports = bind.call(Function.call, Object.prototype.hasOwnProperty);\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzY0Mi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixXQUFXLG1CQUFPLENBQUMsSUFBZTs7QUFFbEMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhcy9zcmMvaW5kZXguanM/YTBkMyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBiaW5kID0gcmVxdWlyZSgnZnVuY3Rpb24tYmluZCcpO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGJpbmQuY2FsbChGdW5jdGlvbi5jYWxsLCBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5KTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///7642\n")},3715:function(__unused_webpack_module,exports,__webpack_require__){eval("var hash = exports;\n\nhash.utils = __webpack_require__(6436);\nhash.common = __webpack_require__(5772);\nhash.sha = __webpack_require__(9041);\nhash.ripemd = __webpack_require__(2949);\nhash.hmac = __webpack_require__(2344);\n\n// Proxy hash functions to the main object\nhash.sha1 = hash.sha.sha1;\nhash.sha256 = hash.sha.sha256;\nhash.sha224 = hash.sha.sha224;\nhash.sha384 = hash.sha.sha384;\nhash.sha512 = hash.sha.sha512;\nhash.ripemd160 = hash.ripemd.ripemd160;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzcxNS5qcyIsIm1hcHBpbmdzIjoiQUFBQTs7QUFFQSxhQUFhLG1CQUFPLENBQUMsSUFBYztBQUNuQyxjQUFjLG1CQUFPLENBQUMsSUFBZTtBQUNyQyxXQUFXLG1CQUFPLENBQUMsSUFBWTtBQUMvQixjQUFjLG1CQUFPLENBQUMsSUFBZTtBQUNyQyxZQUFZLG1CQUFPLENBQUMsSUFBYTs7QUFFakM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2guanM/N2Q5MiJdLCJzb3VyY2VzQ29udGVudCI6WyJ2YXIgaGFzaCA9IGV4cG9ydHM7XG5cbmhhc2gudXRpbHMgPSByZXF1aXJlKCcuL2hhc2gvdXRpbHMnKTtcbmhhc2guY29tbW9uID0gcmVxdWlyZSgnLi9oYXNoL2NvbW1vbicpO1xuaGFzaC5zaGEgPSByZXF1aXJlKCcuL2hhc2gvc2hhJyk7XG5oYXNoLnJpcGVtZCA9IHJlcXVpcmUoJy4vaGFzaC9yaXBlbWQnKTtcbmhhc2guaG1hYyA9IHJlcXVpcmUoJy4vaGFzaC9obWFjJyk7XG5cbi8vIFByb3h5IGhhc2ggZnVuY3Rpb25zIHRvIHRoZSBtYWluIG9iamVjdFxuaGFzaC5zaGExID0gaGFzaC5zaGEuc2hhMTtcbmhhc2guc2hhMjU2ID0gaGFzaC5zaGEuc2hhMjU2O1xuaGFzaC5zaGEyMjQgPSBoYXNoLnNoYS5zaGEyMjQ7XG5oYXNoLnNoYTM4NCA9IGhhc2guc2hhLnNoYTM4NDtcbmhhc2guc2hhNTEyID0gaGFzaC5zaGEuc2hhNTEyO1xuaGFzaC5yaXBlbWQxNjAgPSBoYXNoLnJpcGVtZC5yaXBlbWQxNjA7XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3715\n")},5772:function(__unused_webpack_module,exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar assert = __webpack_require__(9746);\n\nfunction BlockHash() {\n this.pending = null;\n this.pendingTotal = 0;\n this.blockSize = this.constructor.blockSize;\n this.outSize = this.constructor.outSize;\n this.hmacStrength = this.constructor.hmacStrength;\n this.padLength = this.constructor.padLength / 8;\n this.endian = 'big';\n\n this._delta8 = this.blockSize / 8;\n this._delta32 = this.blockSize / 32;\n}\nexports.BlockHash = BlockHash;\n\nBlockHash.prototype.update = function update(msg, enc) {\n // Convert message to array, pad it, and join into 32bit blocks\n msg = utils.toArray(msg, enc);\n if (!this.pending)\n this.pending = msg;\n else\n this.pending = this.pending.concat(msg);\n this.pendingTotal += msg.length;\n\n // Enough data, try updating\n if (this.pending.length >= this._delta8) {\n msg = this.pending;\n\n // Process pending data in blocks\n var r = msg.length % this._delta8;\n this.pending = msg.slice(msg.length - r, msg.length);\n if (this.pending.length === 0)\n this.pending = null;\n\n msg = utils.join32(msg, 0, msg.length - r, this.endian);\n for (var i = 0; i < msg.length; i += this._delta32)\n this._update(msg, i, i + this._delta32);\n }\n\n return this;\n};\n\nBlockHash.prototype.digest = function digest(enc) {\n this.update(this._pad());\n assert(this.pending === null);\n\n return this._digest(enc);\n};\n\nBlockHash.prototype._pad = function pad() {\n var len = this.pendingTotal;\n var bytes = this._delta8;\n var k = bytes - ((len + this.padLength) % bytes);\n var res = new Array(k + this.padLength);\n res[0] = 0x80;\n for (var i = 1; i < k; i++)\n res[i] = 0;\n\n // Append length\n len <<= 3;\n if (this.endian === 'big') {\n for (var t = 8; t < this.padLength; t++)\n res[i++] = 0;\n\n res[i++] = 0;\n res[i++] = 0;\n res[i++] = 0;\n res[i++] = 0;\n res[i++] = (len >>> 24) & 0xff;\n res[i++] = (len >>> 16) & 0xff;\n res[i++] = (len >>> 8) & 0xff;\n res[i++] = len & 0xff;\n } else {\n res[i++] = len & 0xff;\n res[i++] = (len >>> 8) & 0xff;\n res[i++] = (len >>> 16) & 0xff;\n res[i++] = (len >>> 24) & 0xff;\n res[i++] = 0;\n res[i++] = 0;\n res[i++] = 0;\n res[i++] = 0;\n\n for (t = 8; t < this.padLength; t++)\n res[i++] = 0;\n }\n\n return res;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTc3Mi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBUztBQUM3QixhQUFhLG1CQUFPLENBQUMsSUFBcUI7O0FBRTFDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0EsaUJBQWlCOztBQUVqQjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0Esb0JBQW9CLGdCQUFnQjtBQUNwQztBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esa0JBQWtCLE9BQU87QUFDekI7O0FBRUE7QUFDQTtBQUNBO0FBQ0Esb0JBQW9CLG9CQUFvQjtBQUN4Qzs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSTtBQUNKO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUEsZ0JBQWdCLG9CQUFvQjtBQUNwQztBQUNBOztBQUVBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2gvY29tbW9uLmpzP2VkYzkiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgdXRpbHMgPSByZXF1aXJlKCcuL3V0aWxzJyk7XG52YXIgYXNzZXJ0ID0gcmVxdWlyZSgnbWluaW1hbGlzdGljLWFzc2VydCcpO1xuXG5mdW5jdGlvbiBCbG9ja0hhc2goKSB7XG4gIHRoaXMucGVuZGluZyA9IG51bGw7XG4gIHRoaXMucGVuZGluZ1RvdGFsID0gMDtcbiAgdGhpcy5ibG9ja1NpemUgPSB0aGlzLmNvbnN0cnVjdG9yLmJsb2NrU2l6ZTtcbiAgdGhpcy5vdXRTaXplID0gdGhpcy5jb25zdHJ1Y3Rvci5vdXRTaXplO1xuICB0aGlzLmhtYWNTdHJlbmd0aCA9IHRoaXMuY29uc3RydWN0b3IuaG1hY1N0cmVuZ3RoO1xuICB0aGlzLnBhZExlbmd0aCA9IHRoaXMuY29uc3RydWN0b3IucGFkTGVuZ3RoIC8gODtcbiAgdGhpcy5lbmRpYW4gPSAnYmlnJztcblxuICB0aGlzLl9kZWx0YTggPSB0aGlzLmJsb2NrU2l6ZSAvIDg7XG4gIHRoaXMuX2RlbHRhMzIgPSB0aGlzLmJsb2NrU2l6ZSAvIDMyO1xufVxuZXhwb3J0cy5CbG9ja0hhc2ggPSBCbG9ja0hhc2g7XG5cbkJsb2NrSGFzaC5wcm90b3R5cGUudXBkYXRlID0gZnVuY3Rpb24gdXBkYXRlKG1zZywgZW5jKSB7XG4gIC8vIENvbnZlcnQgbWVzc2FnZSB0byBhcnJheSwgcGFkIGl0LCBhbmQgam9pbiBpbnRvIDMyYml0IGJsb2Nrc1xuICBtc2cgPSB1dGlscy50b0FycmF5KG1zZywgZW5jKTtcbiAgaWYgKCF0aGlzLnBlbmRpbmcpXG4gICAgdGhpcy5wZW5kaW5nID0gbXNnO1xuICBlbHNlXG4gICAgdGhpcy5wZW5kaW5nID0gdGhpcy5wZW5kaW5nLmNvbmNhdChtc2cpO1xuICB0aGlzLnBlbmRpbmdUb3RhbCArPSBtc2cubGVuZ3RoO1xuXG4gIC8vIEVub3VnaCBkYXRhLCB0cnkgdXBkYXRpbmdcbiAgaWYgKHRoaXMucGVuZGluZy5sZW5ndGggPj0gdGhpcy5fZGVsdGE4KSB7XG4gICAgbXNnID0gdGhpcy5wZW5kaW5nO1xuXG4gICAgLy8gUHJvY2VzcyBwZW5kaW5nIGRhdGEgaW4gYmxvY2tzXG4gICAgdmFyIHIgPSBtc2cubGVuZ3RoICUgdGhpcy5fZGVsdGE4O1xuICAgIHRoaXMucGVuZGluZyA9IG1zZy5zbGljZShtc2cubGVuZ3RoIC0gciwgbXNnLmxlbmd0aCk7XG4gICAgaWYgKHRoaXMucGVuZGluZy5sZW5ndGggPT09IDApXG4gICAgICB0aGlzLnBlbmRpbmcgPSBudWxsO1xuXG4gICAgbXNnID0gdXRpbHMuam9pbjMyKG1zZywgMCwgbXNnLmxlbmd0aCAtIHIsIHRoaXMuZW5kaWFuKTtcbiAgICBmb3IgKHZhciBpID0gMDsgaSA8IG1zZy5sZW5ndGg7IGkgKz0gdGhpcy5fZGVsdGEzMilcbiAgICAgIHRoaXMuX3VwZGF0ZShtc2csIGksIGkgKyB0aGlzLl9kZWx0YTMyKTtcbiAgfVxuXG4gIHJldHVybiB0aGlzO1xufTtcblxuQmxvY2tIYXNoLnByb3RvdHlwZS5kaWdlc3QgPSBmdW5jdGlvbiBkaWdlc3QoZW5jKSB7XG4gIHRoaXMudXBkYXRlKHRoaXMuX3BhZCgpKTtcbiAgYXNzZXJ0KHRoaXMucGVuZGluZyA9PT0gbnVsbCk7XG5cbiAgcmV0dXJuIHRoaXMuX2RpZ2VzdChlbmMpO1xufTtcblxuQmxvY2tIYXNoLnByb3RvdHlwZS5fcGFkID0gZnVuY3Rpb24gcGFkKCkge1xuICB2YXIgbGVuID0gdGhpcy5wZW5kaW5nVG90YWw7XG4gIHZhciBieXRlcyA9IHRoaXMuX2RlbHRhODtcbiAgdmFyIGsgPSBieXRlcyAtICgobGVuICsgdGhpcy5wYWRMZW5ndGgpICUgYnl0ZXMpO1xuICB2YXIgcmVzID0gbmV3IEFycmF5KGsgKyB0aGlzLnBhZExlbmd0aCk7XG4gIHJlc1swXSA9IDB4ODA7XG4gIGZvciAodmFyIGkgPSAxOyBpIDwgazsgaSsrKVxuICAgIHJlc1tpXSA9IDA7XG5cbiAgLy8gQXBwZW5kIGxlbmd0aFxuICBsZW4gPDw9IDM7XG4gIGlmICh0aGlzLmVuZGlhbiA9PT0gJ2JpZycpIHtcbiAgICBmb3IgKHZhciB0ID0gODsgdCA8IHRoaXMucGFkTGVuZ3RoOyB0KyspXG4gICAgICByZXNbaSsrXSA9IDA7XG5cbiAgICByZXNbaSsrXSA9IDA7XG4gICAgcmVzW2krK10gPSAwO1xuICAgIHJlc1tpKytdID0gMDtcbiAgICByZXNbaSsrXSA9IDA7XG4gICAgcmVzW2krK10gPSAobGVuID4+PiAyNCkgJiAweGZmO1xuICAgIHJlc1tpKytdID0gKGxlbiA+Pj4gMTYpICYgMHhmZjtcbiAgICByZXNbaSsrXSA9IChsZW4gPj4+IDgpICYgMHhmZjtcbiAgICByZXNbaSsrXSA9IGxlbiAmIDB4ZmY7XG4gIH0gZWxzZSB7XG4gICAgcmVzW2krK10gPSBsZW4gJiAweGZmO1xuICAgIHJlc1tpKytdID0gKGxlbiA+Pj4gOCkgJiAweGZmO1xuICAgIHJlc1tpKytdID0gKGxlbiA+Pj4gMTYpICYgMHhmZjtcbiAgICByZXNbaSsrXSA9IChsZW4gPj4+IDI0KSAmIDB4ZmY7XG4gICAgcmVzW2krK10gPSAwO1xuICAgIHJlc1tpKytdID0gMDtcbiAgICByZXNbaSsrXSA9IDA7XG4gICAgcmVzW2krK10gPSAwO1xuXG4gICAgZm9yICh0ID0gODsgdCA8IHRoaXMucGFkTGVuZ3RoOyB0KyspXG4gICAgICByZXNbaSsrXSA9IDA7XG4gIH1cblxuICByZXR1cm4gcmVzO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///5772\n")},2344:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar assert = __webpack_require__(9746);\n\nfunction Hmac(hash, key, enc) {\n if (!(this instanceof Hmac))\n return new Hmac(hash, key, enc);\n this.Hash = hash;\n this.blockSize = hash.blockSize / 8;\n this.outSize = hash.outSize / 8;\n this.inner = null;\n this.outer = null;\n\n this._init(utils.toArray(key, enc));\n}\nmodule.exports = Hmac;\n\nHmac.prototype._init = function init(key) {\n // Shorten key, if needed\n if (key.length > this.blockSize)\n key = new this.Hash().update(key).digest();\n assert(key.length <= this.blockSize);\n\n // Add padding to key\n for (var i = key.length; i < this.blockSize; i++)\n key.push(0);\n\n for (i = 0; i < key.length; i++)\n key[i] ^= 0x36;\n this.inner = new this.Hash().update(key);\n\n // 0x36 ^ 0x5c = 0x6a\n for (i = 0; i < key.length; i++)\n key[i] ^= 0x6a;\n this.outer = new this.Hash().update(key);\n};\n\nHmac.prototype.update = function update(msg, enc) {\n this.inner.update(msg, enc);\n return this;\n};\n\nHmac.prototype.digest = function digest(enc) {\n this.outer.update(this.inner.digest());\n return this.outer.digest(enc);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjM0NC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBUztBQUM3QixhQUFhLG1CQUFPLENBQUMsSUFBcUI7O0FBRTFDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQSwyQkFBMkIsb0JBQW9CO0FBQy9DOztBQUVBLGNBQWMsZ0JBQWdCO0FBQzlCO0FBQ0E7O0FBRUE7QUFDQSxjQUFjLGdCQUFnQjtBQUM5QjtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2gvaG1hYy5qcz8yMTM3Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIHV0aWxzID0gcmVxdWlyZSgnLi91dGlscycpO1xudmFyIGFzc2VydCA9IHJlcXVpcmUoJ21pbmltYWxpc3RpYy1hc3NlcnQnKTtcblxuZnVuY3Rpb24gSG1hYyhoYXNoLCBrZXksIGVuYykge1xuICBpZiAoISh0aGlzIGluc3RhbmNlb2YgSG1hYykpXG4gICAgcmV0dXJuIG5ldyBIbWFjKGhhc2gsIGtleSwgZW5jKTtcbiAgdGhpcy5IYXNoID0gaGFzaDtcbiAgdGhpcy5ibG9ja1NpemUgPSBoYXNoLmJsb2NrU2l6ZSAvIDg7XG4gIHRoaXMub3V0U2l6ZSA9IGhhc2gub3V0U2l6ZSAvIDg7XG4gIHRoaXMuaW5uZXIgPSBudWxsO1xuICB0aGlzLm91dGVyID0gbnVsbDtcblxuICB0aGlzLl9pbml0KHV0aWxzLnRvQXJyYXkoa2V5LCBlbmMpKTtcbn1cbm1vZHVsZS5leHBvcnRzID0gSG1hYztcblxuSG1hYy5wcm90b3R5cGUuX2luaXQgPSBmdW5jdGlvbiBpbml0KGtleSkge1xuICAvLyBTaG9ydGVuIGtleSwgaWYgbmVlZGVkXG4gIGlmIChrZXkubGVuZ3RoID4gdGhpcy5ibG9ja1NpemUpXG4gICAga2V5ID0gbmV3IHRoaXMuSGFzaCgpLnVwZGF0ZShrZXkpLmRpZ2VzdCgpO1xuICBhc3NlcnQoa2V5Lmxlbmd0aCA8PSB0aGlzLmJsb2NrU2l6ZSk7XG5cbiAgLy8gQWRkIHBhZGRpbmcgdG8ga2V5XG4gIGZvciAodmFyIGkgPSBrZXkubGVuZ3RoOyBpIDwgdGhpcy5ibG9ja1NpemU7IGkrKylcbiAgICBrZXkucHVzaCgwKTtcblxuICBmb3IgKGkgPSAwOyBpIDwga2V5Lmxlbmd0aDsgaSsrKVxuICAgIGtleVtpXSBePSAweDM2O1xuICB0aGlzLmlubmVyID0gbmV3IHRoaXMuSGFzaCgpLnVwZGF0ZShrZXkpO1xuXG4gIC8vIDB4MzYgXiAweDVjID0gMHg2YVxuICBmb3IgKGkgPSAwOyBpIDwga2V5Lmxlbmd0aDsgaSsrKVxuICAgIGtleVtpXSBePSAweDZhO1xuICB0aGlzLm91dGVyID0gbmV3IHRoaXMuSGFzaCgpLnVwZGF0ZShrZXkpO1xufTtcblxuSG1hYy5wcm90b3R5cGUudXBkYXRlID0gZnVuY3Rpb24gdXBkYXRlKG1zZywgZW5jKSB7XG4gIHRoaXMuaW5uZXIudXBkYXRlKG1zZywgZW5jKTtcbiAgcmV0dXJuIHRoaXM7XG59O1xuXG5IbWFjLnByb3RvdHlwZS5kaWdlc3QgPSBmdW5jdGlvbiBkaWdlc3QoZW5jKSB7XG4gIHRoaXMub3V0ZXIudXBkYXRlKHRoaXMuaW5uZXIuZGlnZXN0KCkpO1xuICByZXR1cm4gdGhpcy5vdXRlci5kaWdlc3QoZW5jKTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///2344\n")},2949:function(__unused_webpack_module,exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar common = __webpack_require__(5772);\n\nvar rotl32 = utils.rotl32;\nvar sum32 = utils.sum32;\nvar sum32_3 = utils.sum32_3;\nvar sum32_4 = utils.sum32_4;\nvar BlockHash = common.BlockHash;\n\nfunction RIPEMD160() {\n if (!(this instanceof RIPEMD160))\n return new RIPEMD160();\n\n BlockHash.call(this);\n\n this.h = [ 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0 ];\n this.endian = 'little';\n}\nutils.inherits(RIPEMD160, BlockHash);\nexports.ripemd160 = RIPEMD160;\n\nRIPEMD160.blockSize = 512;\nRIPEMD160.outSize = 160;\nRIPEMD160.hmacStrength = 192;\nRIPEMD160.padLength = 64;\n\nRIPEMD160.prototype._update = function update(msg, start) {\n var A = this.h[0];\n var B = this.h[1];\n var C = this.h[2];\n var D = this.h[3];\n var E = this.h[4];\n var Ah = A;\n var Bh = B;\n var Ch = C;\n var Dh = D;\n var Eh = E;\n for (var j = 0; j < 80; j++) {\n var T = sum32(\n rotl32(\n sum32_4(A, f(j, B, C, D), msg[r[j] + start], K(j)),\n s[j]),\n E);\n A = E;\n E = D;\n D = rotl32(C, 10);\n C = B;\n B = T;\n T = sum32(\n rotl32(\n sum32_4(Ah, f(79 - j, Bh, Ch, Dh), msg[rh[j] + start], Kh(j)),\n sh[j]),\n Eh);\n Ah = Eh;\n Eh = Dh;\n Dh = rotl32(Ch, 10);\n Ch = Bh;\n Bh = T;\n }\n T = sum32_3(this.h[1], C, Dh);\n this.h[1] = sum32_3(this.h[2], D, Eh);\n this.h[2] = sum32_3(this.h[3], E, Ah);\n this.h[3] = sum32_3(this.h[4], A, Bh);\n this.h[4] = sum32_3(this.h[0], B, Ch);\n this.h[0] = T;\n};\n\nRIPEMD160.prototype._digest = function digest(enc) {\n if (enc === 'hex')\n return utils.toHex32(this.h, 'little');\n else\n return utils.split32(this.h, 'little');\n};\n\nfunction f(j, x, y, z) {\n if (j <= 15)\n return x ^ y ^ z;\n else if (j <= 31)\n return (x & y) | ((~x) & z);\n else if (j <= 47)\n return (x | (~y)) ^ z;\n else if (j <= 63)\n return (x & z) | (y & (~z));\n else\n return x ^ (y | (~z));\n}\n\nfunction K(j) {\n if (j <= 15)\n return 0x00000000;\n else if (j <= 31)\n return 0x5a827999;\n else if (j <= 47)\n return 0x6ed9eba1;\n else if (j <= 63)\n return 0x8f1bbcdc;\n else\n return 0xa953fd4e;\n}\n\nfunction Kh(j) {\n if (j <= 15)\n return 0x50a28be6;\n else if (j <= 31)\n return 0x5c4dd124;\n else if (j <= 47)\n return 0x6d703ef3;\n else if (j <= 63)\n return 0x7a6d76e9;\n else\n return 0x00000000;\n}\n\nvar r = [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,\n 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,\n 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,\n 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,\n 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13\n];\n\nvar rh = [\n 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,\n 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,\n 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,\n 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,\n 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11\n];\n\nvar s = [\n 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,\n 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,\n 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,\n 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,\n 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6\n];\n\nvar sh = [\n 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,\n 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,\n 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,\n 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,\n 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11\n];\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjk0OS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBUztBQUM3QixhQUFhLG1CQUFPLENBQUMsSUFBVTs7QUFFL0I7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQSxpQkFBaUI7O0FBRWpCO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxrQkFBa0IsUUFBUTtBQUMxQjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2gvcmlwZW1kLmpzP2JiNDQiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgdXRpbHMgPSByZXF1aXJlKCcuL3V0aWxzJyk7XG52YXIgY29tbW9uID0gcmVxdWlyZSgnLi9jb21tb24nKTtcblxudmFyIHJvdGwzMiA9IHV0aWxzLnJvdGwzMjtcbnZhciBzdW0zMiA9IHV0aWxzLnN1bTMyO1xudmFyIHN1bTMyXzMgPSB1dGlscy5zdW0zMl8zO1xudmFyIHN1bTMyXzQgPSB1dGlscy5zdW0zMl80O1xudmFyIEJsb2NrSGFzaCA9IGNvbW1vbi5CbG9ja0hhc2g7XG5cbmZ1bmN0aW9uIFJJUEVNRDE2MCgpIHtcbiAgaWYgKCEodGhpcyBpbnN0YW5jZW9mIFJJUEVNRDE2MCkpXG4gICAgcmV0dXJuIG5ldyBSSVBFTUQxNjAoKTtcblxuICBCbG9ja0hhc2guY2FsbCh0aGlzKTtcblxuICB0aGlzLmggPSBbIDB4Njc0NTIzMDEsIDB4ZWZjZGFiODksIDB4OThiYWRjZmUsIDB4MTAzMjU0NzYsIDB4YzNkMmUxZjAgXTtcbiAgdGhpcy5lbmRpYW4gPSAnbGl0dGxlJztcbn1cbnV0aWxzLmluaGVyaXRzKFJJUEVNRDE2MCwgQmxvY2tIYXNoKTtcbmV4cG9ydHMucmlwZW1kMTYwID0gUklQRU1EMTYwO1xuXG5SSVBFTUQxNjAuYmxvY2tTaXplID0gNTEyO1xuUklQRU1EMTYwLm91dFNpemUgPSAxNjA7XG5SSVBFTUQxNjAuaG1hY1N0cmVuZ3RoID0gMTkyO1xuUklQRU1EMTYwLnBhZExlbmd0aCA9IDY0O1xuXG5SSVBFTUQxNjAucHJvdG90eXBlLl91cGRhdGUgPSBmdW5jdGlvbiB1cGRhdGUobXNnLCBzdGFydCkge1xuICB2YXIgQSA9IHRoaXMuaFswXTtcbiAgdmFyIEIgPSB0aGlzLmhbMV07XG4gIHZhciBDID0gdGhpcy5oWzJdO1xuICB2YXIgRCA9IHRoaXMuaFszXTtcbiAgdmFyIEUgPSB0aGlzLmhbNF07XG4gIHZhciBBaCA9IEE7XG4gIHZhciBCaCA9IEI7XG4gIHZhciBDaCA9IEM7XG4gIHZhciBEaCA9IEQ7XG4gIHZhciBFaCA9IEU7XG4gIGZvciAodmFyIGogPSAwOyBqIDwgODA7IGorKykge1xuICAgIHZhciBUID0gc3VtMzIoXG4gICAgICByb3RsMzIoXG4gICAgICAgIHN1bTMyXzQoQSwgZihqLCBCLCBDLCBEKSwgbXNnW3Jbal0gKyBzdGFydF0sIEsoaikpLFxuICAgICAgICBzW2pdKSxcbiAgICAgIEUpO1xuICAgIEEgPSBFO1xuICAgIEUgPSBEO1xuICAgIEQgPSByb3RsMzIoQywgMTApO1xuICAgIEMgPSBCO1xuICAgIEIgPSBUO1xuICAgIFQgPSBzdW0zMihcbiAgICAgIHJvdGwzMihcbiAgICAgICAgc3VtMzJfNChBaCwgZig3OSAtIGosIEJoLCBDaCwgRGgpLCBtc2dbcmhbal0gKyBzdGFydF0sIEtoKGopKSxcbiAgICAgICAgc2hbal0pLFxuICAgICAgRWgpO1xuICAgIEFoID0gRWg7XG4gICAgRWggPSBEaDtcbiAgICBEaCA9IHJvdGwzMihDaCwgMTApO1xuICAgIENoID0gQmg7XG4gICAgQmggPSBUO1xuICB9XG4gIFQgPSBzdW0zMl8zKHRoaXMuaFsxXSwgQywgRGgpO1xuICB0aGlzLmhbMV0gPSBzdW0zMl8zKHRoaXMuaFsyXSwgRCwgRWgpO1xuICB0aGlzLmhbMl0gPSBzdW0zMl8zKHRoaXMuaFszXSwgRSwgQWgpO1xuICB0aGlzLmhbM10gPSBzdW0zMl8zKHRoaXMuaFs0XSwgQSwgQmgpO1xuICB0aGlzLmhbNF0gPSBzdW0zMl8zKHRoaXMuaFswXSwgQiwgQ2gpO1xuICB0aGlzLmhbMF0gPSBUO1xufTtcblxuUklQRU1EMTYwLnByb3RvdHlwZS5fZGlnZXN0ID0gZnVuY3Rpb24gZGlnZXN0KGVuYykge1xuICBpZiAoZW5jID09PSAnaGV4JylcbiAgICByZXR1cm4gdXRpbHMudG9IZXgzMih0aGlzLmgsICdsaXR0bGUnKTtcbiAgZWxzZVxuICAgIHJldHVybiB1dGlscy5zcGxpdDMyKHRoaXMuaCwgJ2xpdHRsZScpO1xufTtcblxuZnVuY3Rpb24gZihqLCB4LCB5LCB6KSB7XG4gIGlmIChqIDw9IDE1KVxuICAgIHJldHVybiB4IF4geSBeIHo7XG4gIGVsc2UgaWYgKGogPD0gMzEpXG4gICAgcmV0dXJuICh4ICYgeSkgfCAoKH54KSAmIHopO1xuICBlbHNlIGlmIChqIDw9IDQ3KVxuICAgIHJldHVybiAoeCB8ICh+eSkpIF4gejtcbiAgZWxzZSBpZiAoaiA8PSA2MylcbiAgICByZXR1cm4gKHggJiB6KSB8ICh5ICYgKH56KSk7XG4gIGVsc2VcbiAgICByZXR1cm4geCBeICh5IHwgKH56KSk7XG59XG5cbmZ1bmN0aW9uIEsoaikge1xuICBpZiAoaiA8PSAxNSlcbiAgICByZXR1cm4gMHgwMDAwMDAwMDtcbiAgZWxzZSBpZiAoaiA8PSAzMSlcbiAgICByZXR1cm4gMHg1YTgyNzk5OTtcbiAgZWxzZSBpZiAoaiA8PSA0NylcbiAgICByZXR1cm4gMHg2ZWQ5ZWJhMTtcbiAgZWxzZSBpZiAoaiA8PSA2MylcbiAgICByZXR1cm4gMHg4ZjFiYmNkYztcbiAgZWxzZVxuICAgIHJldHVybiAweGE5NTNmZDRlO1xufVxuXG5mdW5jdGlvbiBLaChqKSB7XG4gIGlmIChqIDw9IDE1KVxuICAgIHJldHVybiAweDUwYTI4YmU2O1xuICBlbHNlIGlmIChqIDw9IDMxKVxuICAgIHJldHVybiAweDVjNGRkMTI0O1xuICBlbHNlIGlmIChqIDw9IDQ3KVxuICAgIHJldHVybiAweDZkNzAzZWYzO1xuICBlbHNlIGlmIChqIDw9IDYzKVxuICAgIHJldHVybiAweDdhNmQ3NmU5O1xuICBlbHNlXG4gICAgcmV0dXJuIDB4MDAwMDAwMDA7XG59XG5cbnZhciByID0gW1xuICAwLCAxLCAyLCAzLCA0LCA1LCA2LCA3LCA4LCA5LCAxMCwgMTEsIDEyLCAxMywgMTQsIDE1LFxuICA3LCA0LCAxMywgMSwgMTAsIDYsIDE1LCAzLCAxMiwgMCwgOSwgNSwgMiwgMTQsIDExLCA4LFxuICAzLCAxMCwgMTQsIDQsIDksIDE1LCA4LCAxLCAyLCA3LCAwLCA2LCAxMywgMTEsIDUsIDEyLFxuICAxLCA5LCAxMSwgMTAsIDAsIDgsIDEyLCA0LCAxMywgMywgNywgMTUsIDE0LCA1LCA2LCAyLFxuICA0LCAwLCA1LCA5LCA3LCAxMiwgMiwgMTAsIDE0LCAxLCAzLCA4LCAxMSwgNiwgMTUsIDEzXG5dO1xuXG52YXIgcmggPSBbXG4gIDUsIDE0LCA3LCAwLCA5LCAyLCAxMSwgNCwgMTMsIDYsIDE1LCA4LCAxLCAxMCwgMywgMTIsXG4gIDYsIDExLCAzLCA3LCAwLCAxMywgNSwgMTAsIDE0LCAxNSwgOCwgMTIsIDQsIDksIDEsIDIsXG4gIDE1LCA1LCAxLCAzLCA3LCAxNCwgNiwgOSwgMTEsIDgsIDEyLCAyLCAxMCwgMCwgNCwgMTMsXG4gIDgsIDYsIDQsIDEsIDMsIDExLCAxNSwgMCwgNSwgMTIsIDIsIDEzLCA5LCA3LCAxMCwgMTQsXG4gIDEyLCAxNSwgMTAsIDQsIDEsIDUsIDgsIDcsIDYsIDIsIDEzLCAxNCwgMCwgMywgOSwgMTFcbl07XG5cbnZhciBzID0gW1xuICAxMSwgMTQsIDE1LCAxMiwgNSwgOCwgNywgOSwgMTEsIDEzLCAxNCwgMTUsIDYsIDcsIDksIDgsXG4gIDcsIDYsIDgsIDEzLCAxMSwgOSwgNywgMTUsIDcsIDEyLCAxNSwgOSwgMTEsIDcsIDEzLCAxMixcbiAgMTEsIDEzLCA2LCA3LCAxNCwgOSwgMTMsIDE1LCAxNCwgOCwgMTMsIDYsIDUsIDEyLCA3LCA1LFxuICAxMSwgMTIsIDE0LCAxNSwgMTQsIDE1LCA5LCA4LCA5LCAxNCwgNSwgNiwgOCwgNiwgNSwgMTIsXG4gIDksIDE1LCA1LCAxMSwgNiwgOCwgMTMsIDEyLCA1LCAxMiwgMTMsIDE0LCAxMSwgOCwgNSwgNlxuXTtcblxudmFyIHNoID0gW1xuICA4LCA5LCA5LCAxMSwgMTMsIDE1LCAxNSwgNSwgNywgNywgOCwgMTEsIDE0LCAxNCwgMTIsIDYsXG4gIDksIDEzLCAxNSwgNywgMTIsIDgsIDksIDExLCA3LCA3LCAxMiwgNywgNiwgMTUsIDEzLCAxMSxcbiAgOSwgNywgMTUsIDExLCA4LCA2LCA2LCAxNCwgMTIsIDEzLCA1LCAxNCwgMTMsIDEzLCA3LCA1LFxuICAxNSwgNSwgOCwgMTEsIDE0LCAxNCwgNiwgMTQsIDYsIDksIDEyLCA5LCAxMiwgNSwgMTUsIDgsXG4gIDgsIDUsIDEyLCA5LCAxMiwgNSwgMTQsIDYsIDgsIDEzLCA2LCA1LCAxNSwgMTMsIDExLCAxMVxuXTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2949\n")},9041:function(__unused_webpack_module,exports,__webpack_require__){"use strict";eval("\n\nexports.sha1 = __webpack_require__(4761);\nexports.sha224 = __webpack_require__(799);\nexports.sha256 = __webpack_require__(9344);\nexports.sha384 = __webpack_require__(772);\nexports.sha512 = __webpack_require__(5900);\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTA0MS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYix3Q0FBaUM7QUFDakMseUNBQXFDO0FBQ3JDLDBDQUFxQztBQUNyQyx5Q0FBcUM7QUFDckMsMENBQXFDIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9oYXNoLmpzL2xpYi9oYXNoL3NoYS5qcz81OTE5Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxuZXhwb3J0cy5zaGExID0gcmVxdWlyZSgnLi9zaGEvMScpO1xuZXhwb3J0cy5zaGEyMjQgPSByZXF1aXJlKCcuL3NoYS8yMjQnKTtcbmV4cG9ydHMuc2hhMjU2ID0gcmVxdWlyZSgnLi9zaGEvMjU2Jyk7XG5leHBvcnRzLnNoYTM4NCA9IHJlcXVpcmUoJy4vc2hhLzM4NCcpO1xuZXhwb3J0cy5zaGE1MTIgPSByZXF1aXJlKCcuL3NoYS81MTInKTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///9041\n")},4761:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar common = __webpack_require__(5772);\nvar shaCommon = __webpack_require__(7038);\n\nvar rotl32 = utils.rotl32;\nvar sum32 = utils.sum32;\nvar sum32_5 = utils.sum32_5;\nvar ft_1 = shaCommon.ft_1;\nvar BlockHash = common.BlockHash;\n\nvar sha1_K = [\n 0x5A827999, 0x6ED9EBA1,\n 0x8F1BBCDC, 0xCA62C1D6\n];\n\nfunction SHA1() {\n if (!(this instanceof SHA1))\n return new SHA1();\n\n BlockHash.call(this);\n this.h = [\n 0x67452301, 0xefcdab89, 0x98badcfe,\n 0x10325476, 0xc3d2e1f0 ];\n this.W = new Array(80);\n}\n\nutils.inherits(SHA1, BlockHash);\nmodule.exports = SHA1;\n\nSHA1.blockSize = 512;\nSHA1.outSize = 160;\nSHA1.hmacStrength = 80;\nSHA1.padLength = 64;\n\nSHA1.prototype._update = function _update(msg, start) {\n var W = this.W;\n\n for (var i = 0; i < 16; i++)\n W[i] = msg[start + i];\n\n for(; i < W.length; i++)\n W[i] = rotl32(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1);\n\n var a = this.h[0];\n var b = this.h[1];\n var c = this.h[2];\n var d = this.h[3];\n var e = this.h[4];\n\n for (i = 0; i < W.length; i++) {\n var s = ~~(i / 20);\n var t = sum32_5(rotl32(a, 5), ft_1(s, b, c, d), e, W[i], sha1_K[s]);\n e = d;\n d = c;\n c = rotl32(b, 30);\n b = a;\n a = t;\n }\n\n this.h[0] = sum32(this.h[0], a);\n this.h[1] = sum32(this.h[1], b);\n this.h[2] = sum32(this.h[2], c);\n this.h[3] = sum32(this.h[3], d);\n this.h[4] = sum32(this.h[4], e);\n};\n\nSHA1.prototype._digest = function digest(enc) {\n if (enc === 'hex')\n return utils.toHex32(this.h, 'big');\n else\n return utils.split32(this.h, 'big');\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDc2MS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBVTtBQUM5QixhQUFhLG1CQUFPLENBQUMsSUFBVztBQUNoQyxnQkFBZ0IsbUJBQU8sQ0FBQyxJQUFVOztBQUVsQztBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBLGtCQUFrQixRQUFRO0FBQzFCOztBQUVBLFFBQVEsY0FBYztBQUN0Qjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBLGNBQWMsY0FBYztBQUM1QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2gvc2hhLzEuanM/MTNlMiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciB1dGlscyA9IHJlcXVpcmUoJy4uL3V0aWxzJyk7XG52YXIgY29tbW9uID0gcmVxdWlyZSgnLi4vY29tbW9uJyk7XG52YXIgc2hhQ29tbW9uID0gcmVxdWlyZSgnLi9jb21tb24nKTtcblxudmFyIHJvdGwzMiA9IHV0aWxzLnJvdGwzMjtcbnZhciBzdW0zMiA9IHV0aWxzLnN1bTMyO1xudmFyIHN1bTMyXzUgPSB1dGlscy5zdW0zMl81O1xudmFyIGZ0XzEgPSBzaGFDb21tb24uZnRfMTtcbnZhciBCbG9ja0hhc2ggPSBjb21tb24uQmxvY2tIYXNoO1xuXG52YXIgc2hhMV9LID0gW1xuICAweDVBODI3OTk5LCAweDZFRDlFQkExLFxuICAweDhGMUJCQ0RDLCAweENBNjJDMUQ2XG5dO1xuXG5mdW5jdGlvbiBTSEExKCkge1xuICBpZiAoISh0aGlzIGluc3RhbmNlb2YgU0hBMSkpXG4gICAgcmV0dXJuIG5ldyBTSEExKCk7XG5cbiAgQmxvY2tIYXNoLmNhbGwodGhpcyk7XG4gIHRoaXMuaCA9IFtcbiAgICAweDY3NDUyMzAxLCAweGVmY2RhYjg5LCAweDk4YmFkY2ZlLFxuICAgIDB4MTAzMjU0NzYsIDB4YzNkMmUxZjAgXTtcbiAgdGhpcy5XID0gbmV3IEFycmF5KDgwKTtcbn1cblxudXRpbHMuaW5oZXJpdHMoU0hBMSwgQmxvY2tIYXNoKTtcbm1vZHVsZS5leHBvcnRzID0gU0hBMTtcblxuU0hBMS5ibG9ja1NpemUgPSA1MTI7XG5TSEExLm91dFNpemUgPSAxNjA7XG5TSEExLmhtYWNTdHJlbmd0aCA9IDgwO1xuU0hBMS5wYWRMZW5ndGggPSA2NDtcblxuU0hBMS5wcm90b3R5cGUuX3VwZGF0ZSA9IGZ1bmN0aW9uIF91cGRhdGUobXNnLCBzdGFydCkge1xuICB2YXIgVyA9IHRoaXMuVztcblxuICBmb3IgKHZhciBpID0gMDsgaSA8IDE2OyBpKyspXG4gICAgV1tpXSA9IG1zZ1tzdGFydCArIGldO1xuXG4gIGZvcig7IGkgPCBXLmxlbmd0aDsgaSsrKVxuICAgIFdbaV0gPSByb3RsMzIoV1tpIC0gM10gXiBXW2kgLSA4XSBeIFdbaSAtIDE0XSBeIFdbaSAtIDE2XSwgMSk7XG5cbiAgdmFyIGEgPSB0aGlzLmhbMF07XG4gIHZhciBiID0gdGhpcy5oWzFdO1xuICB2YXIgYyA9IHRoaXMuaFsyXTtcbiAgdmFyIGQgPSB0aGlzLmhbM107XG4gIHZhciBlID0gdGhpcy5oWzRdO1xuXG4gIGZvciAoaSA9IDA7IGkgPCBXLmxlbmd0aDsgaSsrKSB7XG4gICAgdmFyIHMgPSB+fihpIC8gMjApO1xuICAgIHZhciB0ID0gc3VtMzJfNShyb3RsMzIoYSwgNSksIGZ0XzEocywgYiwgYywgZCksIGUsIFdbaV0sIHNoYTFfS1tzXSk7XG4gICAgZSA9IGQ7XG4gICAgZCA9IGM7XG4gICAgYyA9IHJvdGwzMihiLCAzMCk7XG4gICAgYiA9IGE7XG4gICAgYSA9IHQ7XG4gIH1cblxuICB0aGlzLmhbMF0gPSBzdW0zMih0aGlzLmhbMF0sIGEpO1xuICB0aGlzLmhbMV0gPSBzdW0zMih0aGlzLmhbMV0sIGIpO1xuICB0aGlzLmhbMl0gPSBzdW0zMih0aGlzLmhbMl0sIGMpO1xuICB0aGlzLmhbM10gPSBzdW0zMih0aGlzLmhbM10sIGQpO1xuICB0aGlzLmhbNF0gPSBzdW0zMih0aGlzLmhbNF0sIGUpO1xufTtcblxuU0hBMS5wcm90b3R5cGUuX2RpZ2VzdCA9IGZ1bmN0aW9uIGRpZ2VzdChlbmMpIHtcbiAgaWYgKGVuYyA9PT0gJ2hleCcpXG4gICAgcmV0dXJuIHV0aWxzLnRvSGV4MzIodGhpcy5oLCAnYmlnJyk7XG4gIGVsc2VcbiAgICByZXR1cm4gdXRpbHMuc3BsaXQzMih0aGlzLmgsICdiaWcnKTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///4761\n")},799:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar SHA256 = __webpack_require__(9344);\n\nfunction SHA224() {\n if (!(this instanceof SHA224))\n return new SHA224();\n\n SHA256.call(this);\n this.h = [\n 0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939,\n 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4 ];\n}\nutils.inherits(SHA224, SHA256);\nmodule.exports = SHA224;\n\nSHA224.blockSize = 512;\nSHA224.outSize = 224;\nSHA224.hmacStrength = 192;\nSHA224.padLength = 64;\n\nSHA224.prototype._digest = function digest(enc) {\n // Just truncate output\n if (enc === 'hex')\n return utils.toHex32(this.h.slice(0, 7), 'big');\n else\n return utils.split32(this.h.slice(0, 7), 'big');\n};\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzk5LmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLFlBQVksbUJBQU8sQ0FBQyxJQUFVO0FBQzlCLGFBQWEsbUJBQU8sQ0FBQyxJQUFPOztBQUU1QjtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2gvc2hhLzIyNC5qcz8wN2YyIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIHV0aWxzID0gcmVxdWlyZSgnLi4vdXRpbHMnKTtcbnZhciBTSEEyNTYgPSByZXF1aXJlKCcuLzI1NicpO1xuXG5mdW5jdGlvbiBTSEEyMjQoKSB7XG4gIGlmICghKHRoaXMgaW5zdGFuY2VvZiBTSEEyMjQpKVxuICAgIHJldHVybiBuZXcgU0hBMjI0KCk7XG5cbiAgU0hBMjU2LmNhbGwodGhpcyk7XG4gIHRoaXMuaCA9IFtcbiAgICAweGMxMDU5ZWQ4LCAweDM2N2NkNTA3LCAweDMwNzBkZDE3LCAweGY3MGU1OTM5LFxuICAgIDB4ZmZjMDBiMzEsIDB4Njg1ODE1MTEsIDB4NjRmOThmYTcsIDB4YmVmYTRmYTQgXTtcbn1cbnV0aWxzLmluaGVyaXRzKFNIQTIyNCwgU0hBMjU2KTtcbm1vZHVsZS5leHBvcnRzID0gU0hBMjI0O1xuXG5TSEEyMjQuYmxvY2tTaXplID0gNTEyO1xuU0hBMjI0Lm91dFNpemUgPSAyMjQ7XG5TSEEyMjQuaG1hY1N0cmVuZ3RoID0gMTkyO1xuU0hBMjI0LnBhZExlbmd0aCA9IDY0O1xuXG5TSEEyMjQucHJvdG90eXBlLl9kaWdlc3QgPSBmdW5jdGlvbiBkaWdlc3QoZW5jKSB7XG4gIC8vIEp1c3QgdHJ1bmNhdGUgb3V0cHV0XG4gIGlmIChlbmMgPT09ICdoZXgnKVxuICAgIHJldHVybiB1dGlscy50b0hleDMyKHRoaXMuaC5zbGljZSgwLCA3KSwgJ2JpZycpO1xuICBlbHNlXG4gICAgcmV0dXJuIHV0aWxzLnNwbGl0MzIodGhpcy5oLnNsaWNlKDAsIDcpLCAnYmlnJyk7XG59O1xuXG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///799\n")},9344:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar common = __webpack_require__(5772);\nvar shaCommon = __webpack_require__(7038);\nvar assert = __webpack_require__(9746);\n\nvar sum32 = utils.sum32;\nvar sum32_4 = utils.sum32_4;\nvar sum32_5 = utils.sum32_5;\nvar ch32 = shaCommon.ch32;\nvar maj32 = shaCommon.maj32;\nvar s0_256 = shaCommon.s0_256;\nvar s1_256 = shaCommon.s1_256;\nvar g0_256 = shaCommon.g0_256;\nvar g1_256 = shaCommon.g1_256;\n\nvar BlockHash = common.BlockHash;\n\nvar sha256_K = [\n 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,\n 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,\n 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,\n 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,\n 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,\n 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,\n 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,\n 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,\n 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,\n 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,\n 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,\n 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,\n 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,\n 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,\n 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,\n 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2\n];\n\nfunction SHA256() {\n if (!(this instanceof SHA256))\n return new SHA256();\n\n BlockHash.call(this);\n this.h = [\n 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,\n 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19\n ];\n this.k = sha256_K;\n this.W = new Array(64);\n}\nutils.inherits(SHA256, BlockHash);\nmodule.exports = SHA256;\n\nSHA256.blockSize = 512;\nSHA256.outSize = 256;\nSHA256.hmacStrength = 192;\nSHA256.padLength = 64;\n\nSHA256.prototype._update = function _update(msg, start) {\n var W = this.W;\n\n for (var i = 0; i < 16; i++)\n W[i] = msg[start + i];\n for (; i < W.length; i++)\n W[i] = sum32_4(g1_256(W[i - 2]), W[i - 7], g0_256(W[i - 15]), W[i - 16]);\n\n var a = this.h[0];\n var b = this.h[1];\n var c = this.h[2];\n var d = this.h[3];\n var e = this.h[4];\n var f = this.h[5];\n var g = this.h[6];\n var h = this.h[7];\n\n assert(this.k.length === W.length);\n for (i = 0; i < W.length; i++) {\n var T1 = sum32_5(h, s1_256(e), ch32(e, f, g), this.k[i], W[i]);\n var T2 = sum32(s0_256(a), maj32(a, b, c));\n h = g;\n g = f;\n f = e;\n e = sum32(d, T1);\n d = c;\n c = b;\n b = a;\n a = sum32(T1, T2);\n }\n\n this.h[0] = sum32(this.h[0], a);\n this.h[1] = sum32(this.h[1], b);\n this.h[2] = sum32(this.h[2], c);\n this.h[3] = sum32(this.h[3], d);\n this.h[4] = sum32(this.h[4], e);\n this.h[5] = sum32(this.h[5], f);\n this.h[6] = sum32(this.h[6], g);\n this.h[7] = sum32(this.h[7], h);\n};\n\nSHA256.prototype._digest = function digest(enc) {\n if (enc === 'hex')\n return utils.toHex32(this.h, 'big');\n else\n return utils.split32(this.h, 'big');\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTM0NC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBVTtBQUM5QixhQUFhLG1CQUFPLENBQUMsSUFBVztBQUNoQyxnQkFBZ0IsbUJBQU8sQ0FBQyxJQUFVO0FBQ2xDLGFBQWEsbUJBQU8sQ0FBQyxJQUFxQjs7QUFFMUM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQSxrQkFBa0IsUUFBUTtBQUMxQjtBQUNBLFNBQVMsY0FBYztBQUN2Qjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0EsY0FBYyxjQUFjO0FBQzVCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvaGFzaC5qcy9saWIvaGFzaC9zaGEvMjU2LmpzPzZlZWQiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgdXRpbHMgPSByZXF1aXJlKCcuLi91dGlscycpO1xudmFyIGNvbW1vbiA9IHJlcXVpcmUoJy4uL2NvbW1vbicpO1xudmFyIHNoYUNvbW1vbiA9IHJlcXVpcmUoJy4vY29tbW9uJyk7XG52YXIgYXNzZXJ0ID0gcmVxdWlyZSgnbWluaW1hbGlzdGljLWFzc2VydCcpO1xuXG52YXIgc3VtMzIgPSB1dGlscy5zdW0zMjtcbnZhciBzdW0zMl80ID0gdXRpbHMuc3VtMzJfNDtcbnZhciBzdW0zMl81ID0gdXRpbHMuc3VtMzJfNTtcbnZhciBjaDMyID0gc2hhQ29tbW9uLmNoMzI7XG52YXIgbWFqMzIgPSBzaGFDb21tb24ubWFqMzI7XG52YXIgczBfMjU2ID0gc2hhQ29tbW9uLnMwXzI1NjtcbnZhciBzMV8yNTYgPSBzaGFDb21tb24uczFfMjU2O1xudmFyIGcwXzI1NiA9IHNoYUNvbW1vbi5nMF8yNTY7XG52YXIgZzFfMjU2ID0gc2hhQ29tbW9uLmcxXzI1NjtcblxudmFyIEJsb2NrSGFzaCA9IGNvbW1vbi5CbG9ja0hhc2g7XG5cbnZhciBzaGEyNTZfSyA9IFtcbiAgMHg0MjhhMmY5OCwgMHg3MTM3NDQ5MSwgMHhiNWMwZmJjZiwgMHhlOWI1ZGJhNSxcbiAgMHgzOTU2YzI1YiwgMHg1OWYxMTFmMSwgMHg5MjNmODJhNCwgMHhhYjFjNWVkNSxcbiAgMHhkODA3YWE5OCwgMHgxMjgzNWIwMSwgMHgyNDMxODViZSwgMHg1NTBjN2RjMyxcbiAgMHg3MmJlNWQ3NCwgMHg4MGRlYjFmZSwgMHg5YmRjMDZhNywgMHhjMTliZjE3NCxcbiAgMHhlNDliNjljMSwgMHhlZmJlNDc4NiwgMHgwZmMxOWRjNiwgMHgyNDBjYTFjYyxcbiAgMHgyZGU5MmM2ZiwgMHg0YTc0ODRhYSwgMHg1Y2IwYTlkYywgMHg3NmY5ODhkYSxcbiAgMHg5ODNlNTE1MiwgMHhhODMxYzY2ZCwgMHhiMDAzMjdjOCwgMHhiZjU5N2ZjNyxcbiAgMHhjNmUwMGJmMywgMHhkNWE3OTE0NywgMHgwNmNhNjM1MSwgMHgxNDI5Mjk2NyxcbiAgMHgyN2I3MGE4NSwgMHgyZTFiMjEzOCwgMHg0ZDJjNmRmYywgMHg1MzM4MGQxMyxcbiAgMHg2NTBhNzM1NCwgMHg3NjZhMGFiYiwgMHg4MWMyYzkyZSwgMHg5MjcyMmM4NSxcbiAgMHhhMmJmZThhMSwgMHhhODFhNjY0YiwgMHhjMjRiOGI3MCwgMHhjNzZjNTFhMyxcbiAgMHhkMTkyZTgxOSwgMHhkNjk5MDYyNCwgMHhmNDBlMzU4NSwgMHgxMDZhYTA3MCxcbiAgMHgxOWE0YzExNiwgMHgxZTM3NmMwOCwgMHgyNzQ4Nzc0YywgMHgzNGIwYmNiNSxcbiAgMHgzOTFjMGNiMywgMHg0ZWQ4YWE0YSwgMHg1YjljY2E0ZiwgMHg2ODJlNmZmMyxcbiAgMHg3NDhmODJlZSwgMHg3OGE1NjM2ZiwgMHg4NGM4NzgxNCwgMHg4Y2M3MDIwOCxcbiAgMHg5MGJlZmZmYSwgMHhhNDUwNmNlYiwgMHhiZWY5YTNmNywgMHhjNjcxNzhmMlxuXTtcblxuZnVuY3Rpb24gU0hBMjU2KCkge1xuICBpZiAoISh0aGlzIGluc3RhbmNlb2YgU0hBMjU2KSlcbiAgICByZXR1cm4gbmV3IFNIQTI1NigpO1xuXG4gIEJsb2NrSGFzaC5jYWxsKHRoaXMpO1xuICB0aGlzLmggPSBbXG4gICAgMHg2YTA5ZTY2NywgMHhiYjY3YWU4NSwgMHgzYzZlZjM3MiwgMHhhNTRmZjUzYSxcbiAgICAweDUxMGU1MjdmLCAweDliMDU2ODhjLCAweDFmODNkOWFiLCAweDViZTBjZDE5XG4gIF07XG4gIHRoaXMuayA9IHNoYTI1Nl9LO1xuICB0aGlzLlcgPSBuZXcgQXJyYXkoNjQpO1xufVxudXRpbHMuaW5oZXJpdHMoU0hBMjU2LCBCbG9ja0hhc2gpO1xubW9kdWxlLmV4cG9ydHMgPSBTSEEyNTY7XG5cblNIQTI1Ni5ibG9ja1NpemUgPSA1MTI7XG5TSEEyNTYub3V0U2l6ZSA9IDI1NjtcblNIQTI1Ni5obWFjU3RyZW5ndGggPSAxOTI7XG5TSEEyNTYucGFkTGVuZ3RoID0gNjQ7XG5cblNIQTI1Ni5wcm90b3R5cGUuX3VwZGF0ZSA9IGZ1bmN0aW9uIF91cGRhdGUobXNnLCBzdGFydCkge1xuICB2YXIgVyA9IHRoaXMuVztcblxuICBmb3IgKHZhciBpID0gMDsgaSA8IDE2OyBpKyspXG4gICAgV1tpXSA9IG1zZ1tzdGFydCArIGldO1xuICBmb3IgKDsgaSA8IFcubGVuZ3RoOyBpKyspXG4gICAgV1tpXSA9IHN1bTMyXzQoZzFfMjU2KFdbaSAtIDJdKSwgV1tpIC0gN10sIGcwXzI1NihXW2kgLSAxNV0pLCBXW2kgLSAxNl0pO1xuXG4gIHZhciBhID0gdGhpcy5oWzBdO1xuICB2YXIgYiA9IHRoaXMuaFsxXTtcbiAgdmFyIGMgPSB0aGlzLmhbMl07XG4gIHZhciBkID0gdGhpcy5oWzNdO1xuICB2YXIgZSA9IHRoaXMuaFs0XTtcbiAgdmFyIGYgPSB0aGlzLmhbNV07XG4gIHZhciBnID0gdGhpcy5oWzZdO1xuICB2YXIgaCA9IHRoaXMuaFs3XTtcblxuICBhc3NlcnQodGhpcy5rLmxlbmd0aCA9PT0gVy5sZW5ndGgpO1xuICBmb3IgKGkgPSAwOyBpIDwgVy5sZW5ndGg7IGkrKykge1xuICAgIHZhciBUMSA9IHN1bTMyXzUoaCwgczFfMjU2KGUpLCBjaDMyKGUsIGYsIGcpLCB0aGlzLmtbaV0sIFdbaV0pO1xuICAgIHZhciBUMiA9IHN1bTMyKHMwXzI1NihhKSwgbWFqMzIoYSwgYiwgYykpO1xuICAgIGggPSBnO1xuICAgIGcgPSBmO1xuICAgIGYgPSBlO1xuICAgIGUgPSBzdW0zMihkLCBUMSk7XG4gICAgZCA9IGM7XG4gICAgYyA9IGI7XG4gICAgYiA9IGE7XG4gICAgYSA9IHN1bTMyKFQxLCBUMik7XG4gIH1cblxuICB0aGlzLmhbMF0gPSBzdW0zMih0aGlzLmhbMF0sIGEpO1xuICB0aGlzLmhbMV0gPSBzdW0zMih0aGlzLmhbMV0sIGIpO1xuICB0aGlzLmhbMl0gPSBzdW0zMih0aGlzLmhbMl0sIGMpO1xuICB0aGlzLmhbM10gPSBzdW0zMih0aGlzLmhbM10sIGQpO1xuICB0aGlzLmhbNF0gPSBzdW0zMih0aGlzLmhbNF0sIGUpO1xuICB0aGlzLmhbNV0gPSBzdW0zMih0aGlzLmhbNV0sIGYpO1xuICB0aGlzLmhbNl0gPSBzdW0zMih0aGlzLmhbNl0sIGcpO1xuICB0aGlzLmhbN10gPSBzdW0zMih0aGlzLmhbN10sIGgpO1xufTtcblxuU0hBMjU2LnByb3RvdHlwZS5fZGlnZXN0ID0gZnVuY3Rpb24gZGlnZXN0KGVuYykge1xuICBpZiAoZW5jID09PSAnaGV4JylcbiAgICByZXR1cm4gdXRpbHMudG9IZXgzMih0aGlzLmgsICdiaWcnKTtcbiAgZWxzZVxuICAgIHJldHVybiB1dGlscy5zcGxpdDMyKHRoaXMuaCwgJ2JpZycpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///9344\n")},772:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\n\nvar SHA512 = __webpack_require__(5900);\n\nfunction SHA384() {\n if (!(this instanceof SHA384))\n return new SHA384();\n\n SHA512.call(this);\n this.h = [\n 0xcbbb9d5d, 0xc1059ed8,\n 0x629a292a, 0x367cd507,\n 0x9159015a, 0x3070dd17,\n 0x152fecd8, 0xf70e5939,\n 0x67332667, 0xffc00b31,\n 0x8eb44a87, 0x68581511,\n 0xdb0c2e0d, 0x64f98fa7,\n 0x47b5481d, 0xbefa4fa4 ];\n}\nutils.inherits(SHA384, SHA512);\nmodule.exports = SHA384;\n\nSHA384.blockSize = 1024;\nSHA384.outSize = 384;\nSHA384.hmacStrength = 192;\nSHA384.padLength = 128;\n\nSHA384.prototype._digest = function digest(enc) {\n if (enc === 'hex')\n return utils.toHex32(this.h.slice(0, 12), 'big');\n else\n return utils.split32(this.h.slice(0, 12), 'big');\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzcyLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLFlBQVksbUJBQU8sQ0FBQyxJQUFVOztBQUU5QixhQUFhLG1CQUFPLENBQUMsSUFBTzs7QUFFNUI7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvaGFzaC5qcy9saWIvaGFzaC9zaGEvMzg0LmpzPzhiOTUiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgdXRpbHMgPSByZXF1aXJlKCcuLi91dGlscycpO1xuXG52YXIgU0hBNTEyID0gcmVxdWlyZSgnLi81MTInKTtcblxuZnVuY3Rpb24gU0hBMzg0KCkge1xuICBpZiAoISh0aGlzIGluc3RhbmNlb2YgU0hBMzg0KSlcbiAgICByZXR1cm4gbmV3IFNIQTM4NCgpO1xuXG4gIFNIQTUxMi5jYWxsKHRoaXMpO1xuICB0aGlzLmggPSBbXG4gICAgMHhjYmJiOWQ1ZCwgMHhjMTA1OWVkOCxcbiAgICAweDYyOWEyOTJhLCAweDM2N2NkNTA3LFxuICAgIDB4OTE1OTAxNWEsIDB4MzA3MGRkMTcsXG4gICAgMHgxNTJmZWNkOCwgMHhmNzBlNTkzOSxcbiAgICAweDY3MzMyNjY3LCAweGZmYzAwYjMxLFxuICAgIDB4OGViNDRhODcsIDB4Njg1ODE1MTEsXG4gICAgMHhkYjBjMmUwZCwgMHg2NGY5OGZhNyxcbiAgICAweDQ3YjU0ODFkLCAweGJlZmE0ZmE0IF07XG59XG51dGlscy5pbmhlcml0cyhTSEEzODQsIFNIQTUxMik7XG5tb2R1bGUuZXhwb3J0cyA9IFNIQTM4NDtcblxuU0hBMzg0LmJsb2NrU2l6ZSA9IDEwMjQ7XG5TSEEzODQub3V0U2l6ZSA9IDM4NDtcblNIQTM4NC5obWFjU3RyZW5ndGggPSAxOTI7XG5TSEEzODQucGFkTGVuZ3RoID0gMTI4O1xuXG5TSEEzODQucHJvdG90eXBlLl9kaWdlc3QgPSBmdW5jdGlvbiBkaWdlc3QoZW5jKSB7XG4gIGlmIChlbmMgPT09ICdoZXgnKVxuICAgIHJldHVybiB1dGlscy50b0hleDMyKHRoaXMuaC5zbGljZSgwLCAxMiksICdiaWcnKTtcbiAgZWxzZVxuICAgIHJldHVybiB1dGlscy5zcGxpdDMyKHRoaXMuaC5zbGljZSgwLCAxMiksICdiaWcnKTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///772\n")},5900:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar common = __webpack_require__(5772);\nvar assert = __webpack_require__(9746);\n\nvar rotr64_hi = utils.rotr64_hi;\nvar rotr64_lo = utils.rotr64_lo;\nvar shr64_hi = utils.shr64_hi;\nvar shr64_lo = utils.shr64_lo;\nvar sum64 = utils.sum64;\nvar sum64_hi = utils.sum64_hi;\nvar sum64_lo = utils.sum64_lo;\nvar sum64_4_hi = utils.sum64_4_hi;\nvar sum64_4_lo = utils.sum64_4_lo;\nvar sum64_5_hi = utils.sum64_5_hi;\nvar sum64_5_lo = utils.sum64_5_lo;\n\nvar BlockHash = common.BlockHash;\n\nvar sha512_K = [\n 0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd,\n 0xb5c0fbcf, 0xec4d3b2f, 0xe9b5dba5, 0x8189dbbc,\n 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019,\n 0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118,\n 0xd807aa98, 0xa3030242, 0x12835b01, 0x45706fbe,\n 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2,\n 0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1,\n 0x9bdc06a7, 0x25c71235, 0xc19bf174, 0xcf692694,\n 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3,\n 0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65,\n 0x2de92c6f, 0x592b0275, 0x4a7484aa, 0x6ea6e483,\n 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5,\n 0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210,\n 0xb00327c8, 0x98fb213f, 0xbf597fc7, 0xbeef0ee4,\n 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725,\n 0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70,\n 0x27b70a85, 0x46d22ffc, 0x2e1b2138, 0x5c26c926,\n 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df,\n 0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8,\n 0x81c2c92e, 0x47edaee6, 0x92722c85, 0x1482353b,\n 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001,\n 0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30,\n 0xd192e819, 0xd6ef5218, 0xd6990624, 0x5565a910,\n 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8,\n 0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53,\n 0x2748774c, 0xdf8eeb99, 0x34b0bcb5, 0xe19b48a8,\n 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb,\n 0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3,\n 0x748f82ee, 0x5defb2fc, 0x78a5636f, 0x43172f60,\n 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec,\n 0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9,\n 0xbef9a3f7, 0xb2c67915, 0xc67178f2, 0xe372532b,\n 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207,\n 0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178,\n 0x06f067aa, 0x72176fba, 0x0a637dc5, 0xa2c898a6,\n 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b,\n 0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493,\n 0x3c9ebe0a, 0x15c9bebc, 0x431d67c4, 0x9c100d4c,\n 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a,\n 0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817\n];\n\nfunction SHA512() {\n if (!(this instanceof SHA512))\n return new SHA512();\n\n BlockHash.call(this);\n this.h = [\n 0x6a09e667, 0xf3bcc908,\n 0xbb67ae85, 0x84caa73b,\n 0x3c6ef372, 0xfe94f82b,\n 0xa54ff53a, 0x5f1d36f1,\n 0x510e527f, 0xade682d1,\n 0x9b05688c, 0x2b3e6c1f,\n 0x1f83d9ab, 0xfb41bd6b,\n 0x5be0cd19, 0x137e2179 ];\n this.k = sha512_K;\n this.W = new Array(160);\n}\nutils.inherits(SHA512, BlockHash);\nmodule.exports = SHA512;\n\nSHA512.blockSize = 1024;\nSHA512.outSize = 512;\nSHA512.hmacStrength = 192;\nSHA512.padLength = 128;\n\nSHA512.prototype._prepareBlock = function _prepareBlock(msg, start) {\n var W = this.W;\n\n // 32 x 32bit words\n for (var i = 0; i < 32; i++)\n W[i] = msg[start + i];\n for (; i < W.length; i += 2) {\n var c0_hi = g1_512_hi(W[i - 4], W[i - 3]); // i - 2\n var c0_lo = g1_512_lo(W[i - 4], W[i - 3]);\n var c1_hi = W[i - 14]; // i - 7\n var c1_lo = W[i - 13];\n var c2_hi = g0_512_hi(W[i - 30], W[i - 29]); // i - 15\n var c2_lo = g0_512_lo(W[i - 30], W[i - 29]);\n var c3_hi = W[i - 32]; // i - 16\n var c3_lo = W[i - 31];\n\n W[i] = sum64_4_hi(\n c0_hi, c0_lo,\n c1_hi, c1_lo,\n c2_hi, c2_lo,\n c3_hi, c3_lo);\n W[i + 1] = sum64_4_lo(\n c0_hi, c0_lo,\n c1_hi, c1_lo,\n c2_hi, c2_lo,\n c3_hi, c3_lo);\n }\n};\n\nSHA512.prototype._update = function _update(msg, start) {\n this._prepareBlock(msg, start);\n\n var W = this.W;\n\n var ah = this.h[0];\n var al = this.h[1];\n var bh = this.h[2];\n var bl = this.h[3];\n var ch = this.h[4];\n var cl = this.h[5];\n var dh = this.h[6];\n var dl = this.h[7];\n var eh = this.h[8];\n var el = this.h[9];\n var fh = this.h[10];\n var fl = this.h[11];\n var gh = this.h[12];\n var gl = this.h[13];\n var hh = this.h[14];\n var hl = this.h[15];\n\n assert(this.k.length === W.length);\n for (var i = 0; i < W.length; i += 2) {\n var c0_hi = hh;\n var c0_lo = hl;\n var c1_hi = s1_512_hi(eh, el);\n var c1_lo = s1_512_lo(eh, el);\n var c2_hi = ch64_hi(eh, el, fh, fl, gh, gl);\n var c2_lo = ch64_lo(eh, el, fh, fl, gh, gl);\n var c3_hi = this.k[i];\n var c3_lo = this.k[i + 1];\n var c4_hi = W[i];\n var c4_lo = W[i + 1];\n\n var T1_hi = sum64_5_hi(\n c0_hi, c0_lo,\n c1_hi, c1_lo,\n c2_hi, c2_lo,\n c3_hi, c3_lo,\n c4_hi, c4_lo);\n var T1_lo = sum64_5_lo(\n c0_hi, c0_lo,\n c1_hi, c1_lo,\n c2_hi, c2_lo,\n c3_hi, c3_lo,\n c4_hi, c4_lo);\n\n c0_hi = s0_512_hi(ah, al);\n c0_lo = s0_512_lo(ah, al);\n c1_hi = maj64_hi(ah, al, bh, bl, ch, cl);\n c1_lo = maj64_lo(ah, al, bh, bl, ch, cl);\n\n var T2_hi = sum64_hi(c0_hi, c0_lo, c1_hi, c1_lo);\n var T2_lo = sum64_lo(c0_hi, c0_lo, c1_hi, c1_lo);\n\n hh = gh;\n hl = gl;\n\n gh = fh;\n gl = fl;\n\n fh = eh;\n fl = el;\n\n eh = sum64_hi(dh, dl, T1_hi, T1_lo);\n el = sum64_lo(dl, dl, T1_hi, T1_lo);\n\n dh = ch;\n dl = cl;\n\n ch = bh;\n cl = bl;\n\n bh = ah;\n bl = al;\n\n ah = sum64_hi(T1_hi, T1_lo, T2_hi, T2_lo);\n al = sum64_lo(T1_hi, T1_lo, T2_hi, T2_lo);\n }\n\n sum64(this.h, 0, ah, al);\n sum64(this.h, 2, bh, bl);\n sum64(this.h, 4, ch, cl);\n sum64(this.h, 6, dh, dl);\n sum64(this.h, 8, eh, el);\n sum64(this.h, 10, fh, fl);\n sum64(this.h, 12, gh, gl);\n sum64(this.h, 14, hh, hl);\n};\n\nSHA512.prototype._digest = function digest(enc) {\n if (enc === 'hex')\n return utils.toHex32(this.h, 'big');\n else\n return utils.split32(this.h, 'big');\n};\n\nfunction ch64_hi(xh, xl, yh, yl, zh) {\n var r = (xh & yh) ^ ((~xh) & zh);\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction ch64_lo(xh, xl, yh, yl, zh, zl) {\n var r = (xl & yl) ^ ((~xl) & zl);\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction maj64_hi(xh, xl, yh, yl, zh) {\n var r = (xh & yh) ^ (xh & zh) ^ (yh & zh);\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction maj64_lo(xh, xl, yh, yl, zh, zl) {\n var r = (xl & yl) ^ (xl & zl) ^ (yl & zl);\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction s0_512_hi(xh, xl) {\n var c0_hi = rotr64_hi(xh, xl, 28);\n var c1_hi = rotr64_hi(xl, xh, 2); // 34\n var c2_hi = rotr64_hi(xl, xh, 7); // 39\n\n var r = c0_hi ^ c1_hi ^ c2_hi;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction s0_512_lo(xh, xl) {\n var c0_lo = rotr64_lo(xh, xl, 28);\n var c1_lo = rotr64_lo(xl, xh, 2); // 34\n var c2_lo = rotr64_lo(xl, xh, 7); // 39\n\n var r = c0_lo ^ c1_lo ^ c2_lo;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction s1_512_hi(xh, xl) {\n var c0_hi = rotr64_hi(xh, xl, 14);\n var c1_hi = rotr64_hi(xh, xl, 18);\n var c2_hi = rotr64_hi(xl, xh, 9); // 41\n\n var r = c0_hi ^ c1_hi ^ c2_hi;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction s1_512_lo(xh, xl) {\n var c0_lo = rotr64_lo(xh, xl, 14);\n var c1_lo = rotr64_lo(xh, xl, 18);\n var c2_lo = rotr64_lo(xl, xh, 9); // 41\n\n var r = c0_lo ^ c1_lo ^ c2_lo;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction g0_512_hi(xh, xl) {\n var c0_hi = rotr64_hi(xh, xl, 1);\n var c1_hi = rotr64_hi(xh, xl, 8);\n var c2_hi = shr64_hi(xh, xl, 7);\n\n var r = c0_hi ^ c1_hi ^ c2_hi;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction g0_512_lo(xh, xl) {\n var c0_lo = rotr64_lo(xh, xl, 1);\n var c1_lo = rotr64_lo(xh, xl, 8);\n var c2_lo = shr64_lo(xh, xl, 7);\n\n var r = c0_lo ^ c1_lo ^ c2_lo;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction g1_512_hi(xh, xl) {\n var c0_hi = rotr64_hi(xh, xl, 19);\n var c1_hi = rotr64_hi(xl, xh, 29); // 61\n var c2_hi = shr64_hi(xh, xl, 6);\n\n var r = c0_hi ^ c1_hi ^ c2_hi;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction g1_512_lo(xh, xl) {\n var c0_lo = rotr64_lo(xh, xl, 19);\n var c1_lo = rotr64_lo(xl, xh, 29); // 61\n var c2_lo = shr64_lo(xh, xl, 6);\n\n var r = c0_lo ^ c1_lo ^ c2_lo;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTkwMC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBVTtBQUM5QixhQUFhLG1CQUFPLENBQUMsSUFBVztBQUNoQyxhQUFhLG1CQUFPLENBQUMsSUFBcUI7O0FBRTFDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBLGtCQUFrQixRQUFRO0FBQzFCO0FBQ0EsU0FBUyxjQUFjO0FBQ3ZCLGdEQUFnRDtBQUNoRDtBQUNBLDRCQUE0QjtBQUM1QjtBQUNBLGtEQUFrRDtBQUNsRDtBQUNBLDRCQUE0QjtBQUM1Qjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBLGtCQUFrQixjQUFjO0FBQ2hDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxxQ0FBcUM7QUFDckMscUNBQXFDOztBQUVyQztBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxxQ0FBcUM7QUFDckMscUNBQXFDOztBQUVyQztBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBLHFDQUFxQzs7QUFFckM7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxxQ0FBcUM7O0FBRXJDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxzQ0FBc0M7QUFDdEM7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0Esc0NBQXNDO0FBQ3RDOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2gvc2hhLzUxMi5qcz9iNTI1Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIHV0aWxzID0gcmVxdWlyZSgnLi4vdXRpbHMnKTtcbnZhciBjb21tb24gPSByZXF1aXJlKCcuLi9jb21tb24nKTtcbnZhciBhc3NlcnQgPSByZXF1aXJlKCdtaW5pbWFsaXN0aWMtYXNzZXJ0Jyk7XG5cbnZhciByb3RyNjRfaGkgPSB1dGlscy5yb3RyNjRfaGk7XG52YXIgcm90cjY0X2xvID0gdXRpbHMucm90cjY0X2xvO1xudmFyIHNocjY0X2hpID0gdXRpbHMuc2hyNjRfaGk7XG52YXIgc2hyNjRfbG8gPSB1dGlscy5zaHI2NF9sbztcbnZhciBzdW02NCA9IHV0aWxzLnN1bTY0O1xudmFyIHN1bTY0X2hpID0gdXRpbHMuc3VtNjRfaGk7XG52YXIgc3VtNjRfbG8gPSB1dGlscy5zdW02NF9sbztcbnZhciBzdW02NF80X2hpID0gdXRpbHMuc3VtNjRfNF9oaTtcbnZhciBzdW02NF80X2xvID0gdXRpbHMuc3VtNjRfNF9sbztcbnZhciBzdW02NF81X2hpID0gdXRpbHMuc3VtNjRfNV9oaTtcbnZhciBzdW02NF81X2xvID0gdXRpbHMuc3VtNjRfNV9sbztcblxudmFyIEJsb2NrSGFzaCA9IGNvbW1vbi5CbG9ja0hhc2g7XG5cbnZhciBzaGE1MTJfSyA9IFtcbiAgMHg0MjhhMmY5OCwgMHhkNzI4YWUyMiwgMHg3MTM3NDQ5MSwgMHgyM2VmNjVjZCxcbiAgMHhiNWMwZmJjZiwgMHhlYzRkM2IyZiwgMHhlOWI1ZGJhNSwgMHg4MTg5ZGJiYyxcbiAgMHgzOTU2YzI1YiwgMHhmMzQ4YjUzOCwgMHg1OWYxMTFmMSwgMHhiNjA1ZDAxOSxcbiAgMHg5MjNmODJhNCwgMHhhZjE5NGY5YiwgMHhhYjFjNWVkNSwgMHhkYTZkODExOCxcbiAgMHhkODA3YWE5OCwgMHhhMzAzMDI0MiwgMHgxMjgzNWIwMSwgMHg0NTcwNmZiZSxcbiAgMHgyNDMxODViZSwgMHg0ZWU0YjI4YywgMHg1NTBjN2RjMywgMHhkNWZmYjRlMixcbiAgMHg3MmJlNWQ3NCwgMHhmMjdiODk2ZiwgMHg4MGRlYjFmZSwgMHgzYjE2OTZiMSxcbiAgMHg5YmRjMDZhNywgMHgyNWM3MTIzNSwgMHhjMTliZjE3NCwgMHhjZjY5MjY5NCxcbiAgMHhlNDliNjljMSwgMHg5ZWYxNGFkMiwgMHhlZmJlNDc4NiwgMHgzODRmMjVlMyxcbiAgMHgwZmMxOWRjNiwgMHg4YjhjZDViNSwgMHgyNDBjYTFjYywgMHg3N2FjOWM2NSxcbiAgMHgyZGU5MmM2ZiwgMHg1OTJiMDI3NSwgMHg0YTc0ODRhYSwgMHg2ZWE2ZTQ4MyxcbiAgMHg1Y2IwYTlkYywgMHhiZDQxZmJkNCwgMHg3NmY5ODhkYSwgMHg4MzExNTNiNSxcbiAgMHg5ODNlNTE1MiwgMHhlZTY2ZGZhYiwgMHhhODMxYzY2ZCwgMHgyZGI0MzIxMCxcbiAgMHhiMDAzMjdjOCwgMHg5OGZiMjEzZiwgMHhiZjU5N2ZjNywgMHhiZWVmMGVlNCxcbiAgMHhjNmUwMGJmMywgMHgzZGE4OGZjMiwgMHhkNWE3OTE0NywgMHg5MzBhYTcyNSxcbiAgMHgwNmNhNjM1MSwgMHhlMDAzODI2ZiwgMHgxNDI5Mjk2NywgMHgwYTBlNmU3MCxcbiAgMHgyN2I3MGE4NSwgMHg0NmQyMmZmYywgMHgyZTFiMjEzOCwgMHg1YzI2YzkyNixcbiAgMHg0ZDJjNmRmYywgMHg1YWM0MmFlZCwgMHg1MzM4MGQxMywgMHg5ZDk1YjNkZixcbiAgMHg2NTBhNzM1NCwgMHg4YmFmNjNkZSwgMHg3NjZhMGFiYiwgMHgzYzc3YjJhOCxcbiAgMHg4MWMyYzkyZSwgMHg0N2VkYWVlNiwgMHg5MjcyMmM4NSwgMHgxNDgyMzUzYixcbiAgMHhhMmJmZThhMSwgMHg0Y2YxMDM2NCwgMHhhODFhNjY0YiwgMHhiYzQyMzAwMSxcbiAgMHhjMjRiOGI3MCwgMHhkMGY4OTc5MSwgMHhjNzZjNTFhMywgMHgwNjU0YmUzMCxcbiAgMHhkMTkyZTgxOSwgMHhkNmVmNTIxOCwgMHhkNjk5MDYyNCwgMHg1NTY1YTkxMCxcbiAgMHhmNDBlMzU4NSwgMHg1NzcxMjAyYSwgMHgxMDZhYTA3MCwgMHgzMmJiZDFiOCxcbiAgMHgxOWE0YzExNiwgMHhiOGQyZDBjOCwgMHgxZTM3NmMwOCwgMHg1MTQxYWI1MyxcbiAgMHgyNzQ4Nzc0YywgMHhkZjhlZWI5OSwgMHgzNGIwYmNiNSwgMHhlMTliNDhhOCxcbiAgMHgzOTFjMGNiMywgMHhjNWM5NWE2MywgMHg0ZWQ4YWE0YSwgMHhlMzQxOGFjYixcbiAgMHg1YjljY2E0ZiwgMHg3NzYzZTM3MywgMHg2ODJlNmZmMywgMHhkNmIyYjhhMyxcbiAgMHg3NDhmODJlZSwgMHg1ZGVmYjJmYywgMHg3OGE1NjM2ZiwgMHg0MzE3MmY2MCxcbiAgMHg4NGM4NzgxNCwgMHhhMWYwYWI3MiwgMHg4Y2M3MDIwOCwgMHgxYTY0MzllYyxcbiAgMHg5MGJlZmZmYSwgMHgyMzYzMWUyOCwgMHhhNDUwNmNlYiwgMHhkZTgyYmRlOSxcbiAgMHhiZWY5YTNmNywgMHhiMmM2NzkxNSwgMHhjNjcxNzhmMiwgMHhlMzcyNTMyYixcbiAgMHhjYTI3M2VjZSwgMHhlYTI2NjE5YywgMHhkMTg2YjhjNywgMHgyMWMwYzIwNyxcbiAgMHhlYWRhN2RkNiwgMHhjZGUwZWIxZSwgMHhmNTdkNGY3ZiwgMHhlZTZlZDE3OCxcbiAgMHgwNmYwNjdhYSwgMHg3MjE3NmZiYSwgMHgwYTYzN2RjNSwgMHhhMmM4OThhNixcbiAgMHgxMTNmOTgwNCwgMHhiZWY5MGRhZSwgMHgxYjcxMGIzNSwgMHgxMzFjNDcxYixcbiAgMHgyOGRiNzdmNSwgMHgyMzA0N2Q4NCwgMHgzMmNhYWI3YiwgMHg0MGM3MjQ5MyxcbiAgMHgzYzllYmUwYSwgMHgxNWM5YmViYywgMHg0MzFkNjdjNCwgMHg5YzEwMGQ0YyxcbiAgMHg0Y2M1ZDRiZSwgMHhjYjNlNDJiNiwgMHg1OTdmMjk5YywgMHhmYzY1N2UyYSxcbiAgMHg1ZmNiNmZhYiwgMHgzYWQ2ZmFlYywgMHg2YzQ0MTk4YywgMHg0YTQ3NTgxN1xuXTtcblxuZnVuY3Rpb24gU0hBNTEyKCkge1xuICBpZiAoISh0aGlzIGluc3RhbmNlb2YgU0hBNTEyKSlcbiAgICByZXR1cm4gbmV3IFNIQTUxMigpO1xuXG4gIEJsb2NrSGFzaC5jYWxsKHRoaXMpO1xuICB0aGlzLmggPSBbXG4gICAgMHg2YTA5ZTY2NywgMHhmM2JjYzkwOCxcbiAgICAweGJiNjdhZTg1LCAweDg0Y2FhNzNiLFxuICAgIDB4M2M2ZWYzNzIsIDB4ZmU5NGY4MmIsXG4gICAgMHhhNTRmZjUzYSwgMHg1ZjFkMzZmMSxcbiAgICAweDUxMGU1MjdmLCAweGFkZTY4MmQxLFxuICAgIDB4OWIwNTY4OGMsIDB4MmIzZTZjMWYsXG4gICAgMHgxZjgzZDlhYiwgMHhmYjQxYmQ2YixcbiAgICAweDViZTBjZDE5LCAweDEzN2UyMTc5IF07XG4gIHRoaXMuayA9IHNoYTUxMl9LO1xuICB0aGlzLlcgPSBuZXcgQXJyYXkoMTYwKTtcbn1cbnV0aWxzLmluaGVyaXRzKFNIQTUxMiwgQmxvY2tIYXNoKTtcbm1vZHVsZS5leHBvcnRzID0gU0hBNTEyO1xuXG5TSEE1MTIuYmxvY2tTaXplID0gMTAyNDtcblNIQTUxMi5vdXRTaXplID0gNTEyO1xuU0hBNTEyLmhtYWNTdHJlbmd0aCA9IDE5MjtcblNIQTUxMi5wYWRMZW5ndGggPSAxMjg7XG5cblNIQTUxMi5wcm90b3R5cGUuX3ByZXBhcmVCbG9jayA9IGZ1bmN0aW9uIF9wcmVwYXJlQmxvY2sobXNnLCBzdGFydCkge1xuICB2YXIgVyA9IHRoaXMuVztcblxuICAvLyAzMiB4IDMyYml0IHdvcmRzXG4gIGZvciAodmFyIGkgPSAwOyBpIDwgMzI7IGkrKylcbiAgICBXW2ldID0gbXNnW3N0YXJ0ICsgaV07XG4gIGZvciAoOyBpIDwgVy5sZW5ndGg7IGkgKz0gMikge1xuICAgIHZhciBjMF9oaSA9IGcxXzUxMl9oaShXW2kgLSA0XSwgV1tpIC0gM10pOyAgLy8gaSAtIDJcbiAgICB2YXIgYzBfbG8gPSBnMV81MTJfbG8oV1tpIC0gNF0sIFdbaSAtIDNdKTtcbiAgICB2YXIgYzFfaGkgPSBXW2kgLSAxNF07ICAvLyBpIC0gN1xuICAgIHZhciBjMV9sbyA9IFdbaSAtIDEzXTtcbiAgICB2YXIgYzJfaGkgPSBnMF81MTJfaGkoV1tpIC0gMzBdLCBXW2kgLSAyOV0pOyAgLy8gaSAtIDE1XG4gICAgdmFyIGMyX2xvID0gZzBfNTEyX2xvKFdbaSAtIDMwXSwgV1tpIC0gMjldKTtcbiAgICB2YXIgYzNfaGkgPSBXW2kgLSAzMl07ICAvLyBpIC0gMTZcbiAgICB2YXIgYzNfbG8gPSBXW2kgLSAzMV07XG5cbiAgICBXW2ldID0gc3VtNjRfNF9oaShcbiAgICAgIGMwX2hpLCBjMF9sbyxcbiAgICAgIGMxX2hpLCBjMV9sbyxcbiAgICAgIGMyX2hpLCBjMl9sbyxcbiAgICAgIGMzX2hpLCBjM19sbyk7XG4gICAgV1tpICsgMV0gPSBzdW02NF80X2xvKFxuICAgICAgYzBfaGksIGMwX2xvLFxuICAgICAgYzFfaGksIGMxX2xvLFxuICAgICAgYzJfaGksIGMyX2xvLFxuICAgICAgYzNfaGksIGMzX2xvKTtcbiAgfVxufTtcblxuU0hBNTEyLnByb3RvdHlwZS5fdXBkYXRlID0gZnVuY3Rpb24gX3VwZGF0ZShtc2csIHN0YXJ0KSB7XG4gIHRoaXMuX3ByZXBhcmVCbG9jayhtc2csIHN0YXJ0KTtcblxuICB2YXIgVyA9IHRoaXMuVztcblxuICB2YXIgYWggPSB0aGlzLmhbMF07XG4gIHZhciBhbCA9IHRoaXMuaFsxXTtcbiAgdmFyIGJoID0gdGhpcy5oWzJdO1xuICB2YXIgYmwgPSB0aGlzLmhbM107XG4gIHZhciBjaCA9IHRoaXMuaFs0XTtcbiAgdmFyIGNsID0gdGhpcy5oWzVdO1xuICB2YXIgZGggPSB0aGlzLmhbNl07XG4gIHZhciBkbCA9IHRoaXMuaFs3XTtcbiAgdmFyIGVoID0gdGhpcy5oWzhdO1xuICB2YXIgZWwgPSB0aGlzLmhbOV07XG4gIHZhciBmaCA9IHRoaXMuaFsxMF07XG4gIHZhciBmbCA9IHRoaXMuaFsxMV07XG4gIHZhciBnaCA9IHRoaXMuaFsxMl07XG4gIHZhciBnbCA9IHRoaXMuaFsxM107XG4gIHZhciBoaCA9IHRoaXMuaFsxNF07XG4gIHZhciBobCA9IHRoaXMuaFsxNV07XG5cbiAgYXNzZXJ0KHRoaXMuay5sZW5ndGggPT09IFcubGVuZ3RoKTtcbiAgZm9yICh2YXIgaSA9IDA7IGkgPCBXLmxlbmd0aDsgaSArPSAyKSB7XG4gICAgdmFyIGMwX2hpID0gaGg7XG4gICAgdmFyIGMwX2xvID0gaGw7XG4gICAgdmFyIGMxX2hpID0gczFfNTEyX2hpKGVoLCBlbCk7XG4gICAgdmFyIGMxX2xvID0gczFfNTEyX2xvKGVoLCBlbCk7XG4gICAgdmFyIGMyX2hpID0gY2g2NF9oaShlaCwgZWwsIGZoLCBmbCwgZ2gsIGdsKTtcbiAgICB2YXIgYzJfbG8gPSBjaDY0X2xvKGVoLCBlbCwgZmgsIGZsLCBnaCwgZ2wpO1xuICAgIHZhciBjM19oaSA9IHRoaXMua1tpXTtcbiAgICB2YXIgYzNfbG8gPSB0aGlzLmtbaSArIDFdO1xuICAgIHZhciBjNF9oaSA9IFdbaV07XG4gICAgdmFyIGM0X2xvID0gV1tpICsgMV07XG5cbiAgICB2YXIgVDFfaGkgPSBzdW02NF81X2hpKFxuICAgICAgYzBfaGksIGMwX2xvLFxuICAgICAgYzFfaGksIGMxX2xvLFxuICAgICAgYzJfaGksIGMyX2xvLFxuICAgICAgYzNfaGksIGMzX2xvLFxuICAgICAgYzRfaGksIGM0X2xvKTtcbiAgICB2YXIgVDFfbG8gPSBzdW02NF81X2xvKFxuICAgICAgYzBfaGksIGMwX2xvLFxuICAgICAgYzFfaGksIGMxX2xvLFxuICAgICAgYzJfaGksIGMyX2xvLFxuICAgICAgYzNfaGksIGMzX2xvLFxuICAgICAgYzRfaGksIGM0X2xvKTtcblxuICAgIGMwX2hpID0gczBfNTEyX2hpKGFoLCBhbCk7XG4gICAgYzBfbG8gPSBzMF81MTJfbG8oYWgsIGFsKTtcbiAgICBjMV9oaSA9IG1hajY0X2hpKGFoLCBhbCwgYmgsIGJsLCBjaCwgY2wpO1xuICAgIGMxX2xvID0gbWFqNjRfbG8oYWgsIGFsLCBiaCwgYmwsIGNoLCBjbCk7XG5cbiAgICB2YXIgVDJfaGkgPSBzdW02NF9oaShjMF9oaSwgYzBfbG8sIGMxX2hpLCBjMV9sbyk7XG4gICAgdmFyIFQyX2xvID0gc3VtNjRfbG8oYzBfaGksIGMwX2xvLCBjMV9oaSwgYzFfbG8pO1xuXG4gICAgaGggPSBnaDtcbiAgICBobCA9IGdsO1xuXG4gICAgZ2ggPSBmaDtcbiAgICBnbCA9IGZsO1xuXG4gICAgZmggPSBlaDtcbiAgICBmbCA9IGVsO1xuXG4gICAgZWggPSBzdW02NF9oaShkaCwgZGwsIFQxX2hpLCBUMV9sbyk7XG4gICAgZWwgPSBzdW02NF9sbyhkbCwgZGwsIFQxX2hpLCBUMV9sbyk7XG5cbiAgICBkaCA9IGNoO1xuICAgIGRsID0gY2w7XG5cbiAgICBjaCA9IGJoO1xuICAgIGNsID0gYmw7XG5cbiAgICBiaCA9IGFoO1xuICAgIGJsID0gYWw7XG5cbiAgICBhaCA9IHN1bTY0X2hpKFQxX2hpLCBUMV9sbywgVDJfaGksIFQyX2xvKTtcbiAgICBhbCA9IHN1bTY0X2xvKFQxX2hpLCBUMV9sbywgVDJfaGksIFQyX2xvKTtcbiAgfVxuXG4gIHN1bTY0KHRoaXMuaCwgMCwgYWgsIGFsKTtcbiAgc3VtNjQodGhpcy5oLCAyLCBiaCwgYmwpO1xuICBzdW02NCh0aGlzLmgsIDQsIGNoLCBjbCk7XG4gIHN1bTY0KHRoaXMuaCwgNiwgZGgsIGRsKTtcbiAgc3VtNjQodGhpcy5oLCA4LCBlaCwgZWwpO1xuICBzdW02NCh0aGlzLmgsIDEwLCBmaCwgZmwpO1xuICBzdW02NCh0aGlzLmgsIDEyLCBnaCwgZ2wpO1xuICBzdW02NCh0aGlzLmgsIDE0LCBoaCwgaGwpO1xufTtcblxuU0hBNTEyLnByb3RvdHlwZS5fZGlnZXN0ID0gZnVuY3Rpb24gZGlnZXN0KGVuYykge1xuICBpZiAoZW5jID09PSAnaGV4JylcbiAgICByZXR1cm4gdXRpbHMudG9IZXgzMih0aGlzLmgsICdiaWcnKTtcbiAgZWxzZVxuICAgIHJldHVybiB1dGlscy5zcGxpdDMyKHRoaXMuaCwgJ2JpZycpO1xufTtcblxuZnVuY3Rpb24gY2g2NF9oaSh4aCwgeGwsIHloLCB5bCwgemgpIHtcbiAgdmFyIHIgPSAoeGggJiB5aCkgXiAoKH54aCkgJiB6aCk7XG4gIGlmIChyIDwgMClcbiAgICByICs9IDB4MTAwMDAwMDAwO1xuICByZXR1cm4gcjtcbn1cblxuZnVuY3Rpb24gY2g2NF9sbyh4aCwgeGwsIHloLCB5bCwgemgsIHpsKSB7XG4gIHZhciByID0gKHhsICYgeWwpIF4gKCh+eGwpICYgemwpO1xuICBpZiAociA8IDApXG4gICAgciArPSAweDEwMDAwMDAwMDtcbiAgcmV0dXJuIHI7XG59XG5cbmZ1bmN0aW9uIG1hajY0X2hpKHhoLCB4bCwgeWgsIHlsLCB6aCkge1xuICB2YXIgciA9ICh4aCAmIHloKSBeICh4aCAmIHpoKSBeICh5aCAmIHpoKTtcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuXG5mdW5jdGlvbiBtYWo2NF9sbyh4aCwgeGwsIHloLCB5bCwgemgsIHpsKSB7XG4gIHZhciByID0gKHhsICYgeWwpIF4gKHhsICYgemwpIF4gKHlsICYgemwpO1xuICBpZiAociA8IDApXG4gICAgciArPSAweDEwMDAwMDAwMDtcbiAgcmV0dXJuIHI7XG59XG5cbmZ1bmN0aW9uIHMwXzUxMl9oaSh4aCwgeGwpIHtcbiAgdmFyIGMwX2hpID0gcm90cjY0X2hpKHhoLCB4bCwgMjgpO1xuICB2YXIgYzFfaGkgPSByb3RyNjRfaGkoeGwsIHhoLCAyKTsgIC8vIDM0XG4gIHZhciBjMl9oaSA9IHJvdHI2NF9oaSh4bCwgeGgsIDcpOyAgLy8gMzlcblxuICB2YXIgciA9IGMwX2hpIF4gYzFfaGkgXiBjMl9oaTtcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuXG5mdW5jdGlvbiBzMF81MTJfbG8oeGgsIHhsKSB7XG4gIHZhciBjMF9sbyA9IHJvdHI2NF9sbyh4aCwgeGwsIDI4KTtcbiAgdmFyIGMxX2xvID0gcm90cjY0X2xvKHhsLCB4aCwgMik7ICAvLyAzNFxuICB2YXIgYzJfbG8gPSByb3RyNjRfbG8oeGwsIHhoLCA3KTsgIC8vIDM5XG5cbiAgdmFyIHIgPSBjMF9sbyBeIGMxX2xvIF4gYzJfbG87XG4gIGlmIChyIDwgMClcbiAgICByICs9IDB4MTAwMDAwMDAwO1xuICByZXR1cm4gcjtcbn1cblxuZnVuY3Rpb24gczFfNTEyX2hpKHhoLCB4bCkge1xuICB2YXIgYzBfaGkgPSByb3RyNjRfaGkoeGgsIHhsLCAxNCk7XG4gIHZhciBjMV9oaSA9IHJvdHI2NF9oaSh4aCwgeGwsIDE4KTtcbiAgdmFyIGMyX2hpID0gcm90cjY0X2hpKHhsLCB4aCwgOSk7ICAvLyA0MVxuXG4gIHZhciByID0gYzBfaGkgXiBjMV9oaSBeIGMyX2hpO1xuICBpZiAociA8IDApXG4gICAgciArPSAweDEwMDAwMDAwMDtcbiAgcmV0dXJuIHI7XG59XG5cbmZ1bmN0aW9uIHMxXzUxMl9sbyh4aCwgeGwpIHtcbiAgdmFyIGMwX2xvID0gcm90cjY0X2xvKHhoLCB4bCwgMTQpO1xuICB2YXIgYzFfbG8gPSByb3RyNjRfbG8oeGgsIHhsLCAxOCk7XG4gIHZhciBjMl9sbyA9IHJvdHI2NF9sbyh4bCwgeGgsIDkpOyAgLy8gNDFcblxuICB2YXIgciA9IGMwX2xvIF4gYzFfbG8gXiBjMl9sbztcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuXG5mdW5jdGlvbiBnMF81MTJfaGkoeGgsIHhsKSB7XG4gIHZhciBjMF9oaSA9IHJvdHI2NF9oaSh4aCwgeGwsIDEpO1xuICB2YXIgYzFfaGkgPSByb3RyNjRfaGkoeGgsIHhsLCA4KTtcbiAgdmFyIGMyX2hpID0gc2hyNjRfaGkoeGgsIHhsLCA3KTtcblxuICB2YXIgciA9IGMwX2hpIF4gYzFfaGkgXiBjMl9oaTtcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuXG5mdW5jdGlvbiBnMF81MTJfbG8oeGgsIHhsKSB7XG4gIHZhciBjMF9sbyA9IHJvdHI2NF9sbyh4aCwgeGwsIDEpO1xuICB2YXIgYzFfbG8gPSByb3RyNjRfbG8oeGgsIHhsLCA4KTtcbiAgdmFyIGMyX2xvID0gc2hyNjRfbG8oeGgsIHhsLCA3KTtcblxuICB2YXIgciA9IGMwX2xvIF4gYzFfbG8gXiBjMl9sbztcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuXG5mdW5jdGlvbiBnMV81MTJfaGkoeGgsIHhsKSB7XG4gIHZhciBjMF9oaSA9IHJvdHI2NF9oaSh4aCwgeGwsIDE5KTtcbiAgdmFyIGMxX2hpID0gcm90cjY0X2hpKHhsLCB4aCwgMjkpOyAgLy8gNjFcbiAgdmFyIGMyX2hpID0gc2hyNjRfaGkoeGgsIHhsLCA2KTtcblxuICB2YXIgciA9IGMwX2hpIF4gYzFfaGkgXiBjMl9oaTtcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuXG5mdW5jdGlvbiBnMV81MTJfbG8oeGgsIHhsKSB7XG4gIHZhciBjMF9sbyA9IHJvdHI2NF9sbyh4aCwgeGwsIDE5KTtcbiAgdmFyIGMxX2xvID0gcm90cjY0X2xvKHhsLCB4aCwgMjkpOyAgLy8gNjFcbiAgdmFyIGMyX2xvID0gc2hyNjRfbG8oeGgsIHhsLCA2KTtcblxuICB2YXIgciA9IGMwX2xvIF4gYzFfbG8gXiBjMl9sbztcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///5900\n")},7038:function(__unused_webpack_module,exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar rotr32 = utils.rotr32;\n\nfunction ft_1(s, x, y, z) {\n if (s === 0)\n return ch32(x, y, z);\n if (s === 1 || s === 3)\n return p32(x, y, z);\n if (s === 2)\n return maj32(x, y, z);\n}\nexports.ft_1 = ft_1;\n\nfunction ch32(x, y, z) {\n return (x & y) ^ ((~x) & z);\n}\nexports.ch32 = ch32;\n\nfunction maj32(x, y, z) {\n return (x & y) ^ (x & z) ^ (y & z);\n}\nexports.maj32 = maj32;\n\nfunction p32(x, y, z) {\n return x ^ y ^ z;\n}\nexports.p32 = p32;\n\nfunction s0_256(x) {\n return rotr32(x, 2) ^ rotr32(x, 13) ^ rotr32(x, 22);\n}\nexports.s0_256 = s0_256;\n\nfunction s1_256(x) {\n return rotr32(x, 6) ^ rotr32(x, 11) ^ rotr32(x, 25);\n}\nexports.s1_256 = s1_256;\n\nfunction g0_256(x) {\n return rotr32(x, 7) ^ rotr32(x, 18) ^ (x >>> 3);\n}\nexports.g0_256 = g0_256;\n\nfunction g1_256(x) {\n return rotr32(x, 17) ^ rotr32(x, 19) ^ (x >>> 10);\n}\nexports.g1_256 = g1_256;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzAzOC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBVTtBQUM5Qjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsWUFBWTs7QUFFWjtBQUNBO0FBQ0E7QUFDQSxZQUFZOztBQUVaO0FBQ0E7QUFDQTtBQUNBLGFBQWE7O0FBRWI7QUFDQTtBQUNBO0FBQ0EsV0FBVzs7QUFFWDtBQUNBO0FBQ0E7QUFDQSxjQUFjOztBQUVkO0FBQ0E7QUFDQTtBQUNBLGNBQWM7O0FBRWQ7QUFDQTtBQUNBO0FBQ0EsY0FBYzs7QUFFZDtBQUNBO0FBQ0E7QUFDQSxjQUFjIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9oYXNoLmpzL2xpYi9oYXNoL3NoYS9jb21tb24uanM/YWE1NiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciB1dGlscyA9IHJlcXVpcmUoJy4uL3V0aWxzJyk7XG52YXIgcm90cjMyID0gdXRpbHMucm90cjMyO1xuXG5mdW5jdGlvbiBmdF8xKHMsIHgsIHksIHopIHtcbiAgaWYgKHMgPT09IDApXG4gICAgcmV0dXJuIGNoMzIoeCwgeSwgeik7XG4gIGlmIChzID09PSAxIHx8IHMgPT09IDMpXG4gICAgcmV0dXJuIHAzMih4LCB5LCB6KTtcbiAgaWYgKHMgPT09IDIpXG4gICAgcmV0dXJuIG1hajMyKHgsIHksIHopO1xufVxuZXhwb3J0cy5mdF8xID0gZnRfMTtcblxuZnVuY3Rpb24gY2gzMih4LCB5LCB6KSB7XG4gIHJldHVybiAoeCAmIHkpIF4gKCh+eCkgJiB6KTtcbn1cbmV4cG9ydHMuY2gzMiA9IGNoMzI7XG5cbmZ1bmN0aW9uIG1hajMyKHgsIHksIHopIHtcbiAgcmV0dXJuICh4ICYgeSkgXiAoeCAmIHopIF4gKHkgJiB6KTtcbn1cbmV4cG9ydHMubWFqMzIgPSBtYWozMjtcblxuZnVuY3Rpb24gcDMyKHgsIHksIHopIHtcbiAgcmV0dXJuIHggXiB5IF4gejtcbn1cbmV4cG9ydHMucDMyID0gcDMyO1xuXG5mdW5jdGlvbiBzMF8yNTYoeCkge1xuICByZXR1cm4gcm90cjMyKHgsIDIpIF4gcm90cjMyKHgsIDEzKSBeIHJvdHIzMih4LCAyMik7XG59XG5leHBvcnRzLnMwXzI1NiA9IHMwXzI1NjtcblxuZnVuY3Rpb24gczFfMjU2KHgpIHtcbiAgcmV0dXJuIHJvdHIzMih4LCA2KSBeIHJvdHIzMih4LCAxMSkgXiByb3RyMzIoeCwgMjUpO1xufVxuZXhwb3J0cy5zMV8yNTYgPSBzMV8yNTY7XG5cbmZ1bmN0aW9uIGcwXzI1Nih4KSB7XG4gIHJldHVybiByb3RyMzIoeCwgNykgXiByb3RyMzIoeCwgMTgpIF4gKHggPj4+IDMpO1xufVxuZXhwb3J0cy5nMF8yNTYgPSBnMF8yNTY7XG5cbmZ1bmN0aW9uIGcxXzI1Nih4KSB7XG4gIHJldHVybiByb3RyMzIoeCwgMTcpIF4gcm90cjMyKHgsIDE5KSBeICh4ID4+PiAxMCk7XG59XG5leHBvcnRzLmcxXzI1NiA9IGcxXzI1NjtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///7038\n")},6436:function(__unused_webpack_module,exports,__webpack_require__){"use strict";eval("\n\nvar assert = __webpack_require__(9746);\nvar inherits = __webpack_require__(5717);\n\nexports.inherits = inherits;\n\nfunction isSurrogatePair(msg, i) {\n if ((msg.charCodeAt(i) & 0xFC00) !== 0xD800) {\n return false;\n }\n if (i < 0 || i + 1 >= msg.length) {\n return false;\n }\n return (msg.charCodeAt(i + 1) & 0xFC00) === 0xDC00;\n}\n\nfunction toArray(msg, enc) {\n if (Array.isArray(msg))\n return msg.slice();\n if (!msg)\n return [];\n var res = [];\n if (typeof msg === 'string') {\n if (!enc) {\n // Inspired by stringToUtf8ByteArray() in closure-library by Google\n // https://github.com/google/closure-library/blob/8598d87242af59aac233270742c8984e2b2bdbe0/closure/goog/crypt/crypt.js#L117-L143\n // Apache License 2.0\n // https://github.com/google/closure-library/blob/master/LICENSE\n var p = 0;\n for (var i = 0; i < msg.length; i++) {\n var c = msg.charCodeAt(i);\n if (c < 128) {\n res[p++] = c;\n } else if (c < 2048) {\n res[p++] = (c >> 6) | 192;\n res[p++] = (c & 63) | 128;\n } else if (isSurrogatePair(msg, i)) {\n c = 0x10000 + ((c & 0x03FF) << 10) + (msg.charCodeAt(++i) & 0x03FF);\n res[p++] = (c >> 18) | 240;\n res[p++] = ((c >> 12) & 63) | 128;\n res[p++] = ((c >> 6) & 63) | 128;\n res[p++] = (c & 63) | 128;\n } else {\n res[p++] = (c >> 12) | 224;\n res[p++] = ((c >> 6) & 63) | 128;\n res[p++] = (c & 63) | 128;\n }\n }\n } else if (enc === 'hex') {\n msg = msg.replace(/[^a-z0-9]+/ig, '');\n if (msg.length % 2 !== 0)\n msg = '0' + msg;\n for (i = 0; i < msg.length; i += 2)\n res.push(parseInt(msg[i] + msg[i + 1], 16));\n }\n } else {\n for (i = 0; i < msg.length; i++)\n res[i] = msg[i] | 0;\n }\n return res;\n}\nexports.toArray = toArray;\n\nfunction toHex(msg) {\n var res = '';\n for (var i = 0; i < msg.length; i++)\n res += zero2(msg[i].toString(16));\n return res;\n}\nexports.toHex = toHex;\n\nfunction htonl(w) {\n var res = (w >>> 24) |\n ((w >>> 8) & 0xff00) |\n ((w << 8) & 0xff0000) |\n ((w & 0xff) << 24);\n return res >>> 0;\n}\nexports.htonl = htonl;\n\nfunction toHex32(msg, endian) {\n var res = '';\n for (var i = 0; i < msg.length; i++) {\n var w = msg[i];\n if (endian === 'little')\n w = htonl(w);\n res += zero8(w.toString(16));\n }\n return res;\n}\nexports.toHex32 = toHex32;\n\nfunction zero2(word) {\n if (word.length === 1)\n return '0' + word;\n else\n return word;\n}\nexports.zero2 = zero2;\n\nfunction zero8(word) {\n if (word.length === 7)\n return '0' + word;\n else if (word.length === 6)\n return '00' + word;\n else if (word.length === 5)\n return '000' + word;\n else if (word.length === 4)\n return '0000' + word;\n else if (word.length === 3)\n return '00000' + word;\n else if (word.length === 2)\n return '000000' + word;\n else if (word.length === 1)\n return '0000000' + word;\n else\n return word;\n}\nexports.zero8 = zero8;\n\nfunction join32(msg, start, end, endian) {\n var len = end - start;\n assert(len % 4 === 0);\n var res = new Array(len / 4);\n for (var i = 0, k = start; i < res.length; i++, k += 4) {\n var w;\n if (endian === 'big')\n w = (msg[k] << 24) | (msg[k + 1] << 16) | (msg[k + 2] << 8) | msg[k + 3];\n else\n w = (msg[k + 3] << 24) | (msg[k + 2] << 16) | (msg[k + 1] << 8) | msg[k];\n res[i] = w >>> 0;\n }\n return res;\n}\nexports.join32 = join32;\n\nfunction split32(msg, endian) {\n var res = new Array(msg.length * 4);\n for (var i = 0, k = 0; i < msg.length; i++, k += 4) {\n var m = msg[i];\n if (endian === 'big') {\n res[k] = m >>> 24;\n res[k + 1] = (m >>> 16) & 0xff;\n res[k + 2] = (m >>> 8) & 0xff;\n res[k + 3] = m & 0xff;\n } else {\n res[k + 3] = m >>> 24;\n res[k + 2] = (m >>> 16) & 0xff;\n res[k + 1] = (m >>> 8) & 0xff;\n res[k] = m & 0xff;\n }\n }\n return res;\n}\nexports.split32 = split32;\n\nfunction rotr32(w, b) {\n return (w >>> b) | (w << (32 - b));\n}\nexports.rotr32 = rotr32;\n\nfunction rotl32(w, b) {\n return (w << b) | (w >>> (32 - b));\n}\nexports.rotl32 = rotl32;\n\nfunction sum32(a, b) {\n return (a + b) >>> 0;\n}\nexports.sum32 = sum32;\n\nfunction sum32_3(a, b, c) {\n return (a + b + c) >>> 0;\n}\nexports.sum32_3 = sum32_3;\n\nfunction sum32_4(a, b, c, d) {\n return (a + b + c + d) >>> 0;\n}\nexports.sum32_4 = sum32_4;\n\nfunction sum32_5(a, b, c, d, e) {\n return (a + b + c + d + e) >>> 0;\n}\nexports.sum32_5 = sum32_5;\n\nfunction sum64(buf, pos, ah, al) {\n var bh = buf[pos];\n var bl = buf[pos + 1];\n\n var lo = (al + bl) >>> 0;\n var hi = (lo < al ? 1 : 0) + ah + bh;\n buf[pos] = hi >>> 0;\n buf[pos + 1] = lo;\n}\nexports.sum64 = sum64;\n\nfunction sum64_hi(ah, al, bh, bl) {\n var lo = (al + bl) >>> 0;\n var hi = (lo < al ? 1 : 0) + ah + bh;\n return hi >>> 0;\n}\nexports.sum64_hi = sum64_hi;\n\nfunction sum64_lo(ah, al, bh, bl) {\n var lo = al + bl;\n return lo >>> 0;\n}\nexports.sum64_lo = sum64_lo;\n\nfunction sum64_4_hi(ah, al, bh, bl, ch, cl, dh, dl) {\n var carry = 0;\n var lo = al;\n lo = (lo + bl) >>> 0;\n carry += lo < al ? 1 : 0;\n lo = (lo + cl) >>> 0;\n carry += lo < cl ? 1 : 0;\n lo = (lo + dl) >>> 0;\n carry += lo < dl ? 1 : 0;\n\n var hi = ah + bh + ch + dh + carry;\n return hi >>> 0;\n}\nexports.sum64_4_hi = sum64_4_hi;\n\nfunction sum64_4_lo(ah, al, bh, bl, ch, cl, dh, dl) {\n var lo = al + bl + cl + dl;\n return lo >>> 0;\n}\nexports.sum64_4_lo = sum64_4_lo;\n\nfunction sum64_5_hi(ah, al, bh, bl, ch, cl, dh, dl, eh, el) {\n var carry = 0;\n var lo = al;\n lo = (lo + bl) >>> 0;\n carry += lo < al ? 1 : 0;\n lo = (lo + cl) >>> 0;\n carry += lo < cl ? 1 : 0;\n lo = (lo + dl) >>> 0;\n carry += lo < dl ? 1 : 0;\n lo = (lo + el) >>> 0;\n carry += lo < el ? 1 : 0;\n\n var hi = ah + bh + ch + dh + eh + carry;\n return hi >>> 0;\n}\nexports.sum64_5_hi = sum64_5_hi;\n\nfunction sum64_5_lo(ah, al, bh, bl, ch, cl, dh, dl, eh, el) {\n var lo = al + bl + cl + dl + el;\n\n return lo >>> 0;\n}\nexports.sum64_5_lo = sum64_5_lo;\n\nfunction rotr64_hi(ah, al, num) {\n var r = (al << (32 - num)) | (ah >>> num);\n return r >>> 0;\n}\nexports.rotr64_hi = rotr64_hi;\n\nfunction rotr64_lo(ah, al, num) {\n var r = (ah << (32 - num)) | (al >>> num);\n return r >>> 0;\n}\nexports.rotr64_lo = rotr64_lo;\n\nfunction shr64_hi(ah, al, num) {\n return ah >>> num;\n}\nexports.shr64_hi = shr64_hi;\n\nfunction shr64_lo(ah, al, num) {\n var r = (ah << (32 - num)) | (al >>> num);\n return r >>> 0;\n}\nexports.shr64_lo = shr64_lo;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNjQzNi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixhQUFhLG1CQUFPLENBQUMsSUFBcUI7QUFDMUMsZUFBZSxtQkFBTyxDQUFDLElBQVU7O0FBRWpDLGdCQUFnQjs7QUFFaEI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esc0JBQXNCLGdCQUFnQjtBQUN0QztBQUNBO0FBQ0E7QUFDQSxVQUFVO0FBQ1Y7QUFDQTtBQUNBLFVBQVU7QUFDVjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsVUFBVTtBQUNWO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxNQUFNO0FBQ047QUFDQTtBQUNBO0FBQ0Esa0JBQWtCLGdCQUFnQjtBQUNsQztBQUNBO0FBQ0EsSUFBSTtBQUNKLGdCQUFnQixnQkFBZ0I7QUFDaEM7QUFDQTtBQUNBO0FBQ0E7QUFDQSxlQUFlOztBQUVmO0FBQ0E7QUFDQSxrQkFBa0IsZ0JBQWdCO0FBQ2xDO0FBQ0E7QUFDQTtBQUNBLGFBQWE7O0FBRWI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxhQUFhOztBQUViO0FBQ0E7QUFDQSxrQkFBa0IsZ0JBQWdCO0FBQ2xDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsZUFBZTs7QUFFZjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxhQUFhOztBQUViO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLGFBQWE7O0FBRWI7QUFDQTtBQUNBO0FBQ0E7QUFDQSw2QkFBNkIsZ0JBQWdCO0FBQzdDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLGNBQWM7O0FBRWQ7QUFDQTtBQUNBLHlCQUF5QixnQkFBZ0I7QUFDekM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsTUFBTTtBQUNOO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxlQUFlOztBQUVmO0FBQ0E7QUFDQTtBQUNBLGNBQWM7O0FBRWQ7QUFDQTtBQUNBO0FBQ0EsY0FBYzs7QUFFZDtBQUNBO0FBQ0E7QUFDQSxhQUFhOztBQUViO0FBQ0E7QUFDQTtBQUNBLGVBQWU7O0FBRWY7QUFDQTtBQUNBO0FBQ0EsZUFBZTs7QUFFZjtBQUNBO0FBQ0E7QUFDQSxlQUFlOztBQUVmO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsYUFBYTs7QUFFYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsZ0JBQWdCOztBQUVoQjtBQUNBO0FBQ0E7QUFDQTtBQUNBLGdCQUFnQjs7QUFFaEI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBLGtCQUFrQjs7QUFFbEI7QUFDQTtBQUNBO0FBQ0E7QUFDQSxrQkFBa0I7O0FBRWxCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0Esa0JBQWtCOztBQUVsQjtBQUNBOztBQUVBO0FBQ0E7QUFDQSxrQkFBa0I7O0FBRWxCO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsaUJBQWlCOztBQUVqQjtBQUNBO0FBQ0E7QUFDQTtBQUNBLGlCQUFpQjs7QUFFakI7QUFDQTtBQUNBO0FBQ0EsZ0JBQWdCOztBQUVoQjtBQUNBO0FBQ0E7QUFDQTtBQUNBLGdCQUFnQiIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvaGFzaC5qcy9saWIvaGFzaC91dGlscy5qcz9jM2MwIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGFzc2VydCA9IHJlcXVpcmUoJ21pbmltYWxpc3RpYy1hc3NlcnQnKTtcbnZhciBpbmhlcml0cyA9IHJlcXVpcmUoJ2luaGVyaXRzJyk7XG5cbmV4cG9ydHMuaW5oZXJpdHMgPSBpbmhlcml0cztcblxuZnVuY3Rpb24gaXNTdXJyb2dhdGVQYWlyKG1zZywgaSkge1xuICBpZiAoKG1zZy5jaGFyQ29kZUF0KGkpICYgMHhGQzAwKSAhPT0gMHhEODAwKSB7XG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG4gIGlmIChpIDwgMCB8fCBpICsgMSA+PSBtc2cubGVuZ3RoKSB7XG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG4gIHJldHVybiAobXNnLmNoYXJDb2RlQXQoaSArIDEpICYgMHhGQzAwKSA9PT0gMHhEQzAwO1xufVxuXG5mdW5jdGlvbiB0b0FycmF5KG1zZywgZW5jKSB7XG4gIGlmIChBcnJheS5pc0FycmF5KG1zZykpXG4gICAgcmV0dXJuIG1zZy5zbGljZSgpO1xuICBpZiAoIW1zZylcbiAgICByZXR1cm4gW107XG4gIHZhciByZXMgPSBbXTtcbiAgaWYgKHR5cGVvZiBtc2cgPT09ICdzdHJpbmcnKSB7XG4gICAgaWYgKCFlbmMpIHtcbiAgICAgIC8vIEluc3BpcmVkIGJ5IHN0cmluZ1RvVXRmOEJ5dGVBcnJheSgpIGluIGNsb3N1cmUtbGlicmFyeSBieSBHb29nbGVcbiAgICAgIC8vIGh0dHBzOi8vZ2l0aHViLmNvbS9nb29nbGUvY2xvc3VyZS1saWJyYXJ5L2Jsb2IvODU5OGQ4NzI0MmFmNTlhYWMyMzMyNzA3NDJjODk4NGUyYjJiZGJlMC9jbG9zdXJlL2dvb2cvY3J5cHQvY3J5cHQuanMjTDExNy1MMTQzXG4gICAgICAvLyBBcGFjaGUgTGljZW5zZSAyLjBcbiAgICAgIC8vIGh0dHBzOi8vZ2l0aHViLmNvbS9nb29nbGUvY2xvc3VyZS1saWJyYXJ5L2Jsb2IvbWFzdGVyL0xJQ0VOU0VcbiAgICAgIHZhciBwID0gMDtcbiAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwgbXNnLmxlbmd0aDsgaSsrKSB7XG4gICAgICAgIHZhciBjID0gbXNnLmNoYXJDb2RlQXQoaSk7XG4gICAgICAgIGlmIChjIDwgMTI4KSB7XG4gICAgICAgICAgcmVzW3ArK10gPSBjO1xuICAgICAgICB9IGVsc2UgaWYgKGMgPCAyMDQ4KSB7XG4gICAgICAgICAgcmVzW3ArK10gPSAoYyA+PiA2KSB8IDE5MjtcbiAgICAgICAgICByZXNbcCsrXSA9IChjICYgNjMpIHwgMTI4O1xuICAgICAgICB9IGVsc2UgaWYgKGlzU3Vycm9nYXRlUGFpcihtc2csIGkpKSB7XG4gICAgICAgICAgYyA9IDB4MTAwMDAgKyAoKGMgJiAweDAzRkYpIDw8IDEwKSArIChtc2cuY2hhckNvZGVBdCgrK2kpICYgMHgwM0ZGKTtcbiAgICAgICAgICByZXNbcCsrXSA9IChjID4+IDE4KSB8IDI0MDtcbiAgICAgICAgICByZXNbcCsrXSA9ICgoYyA+PiAxMikgJiA2MykgfCAxMjg7XG4gICAgICAgICAgcmVzW3ArK10gPSAoKGMgPj4gNikgJiA2MykgfCAxMjg7XG4gICAgICAgICAgcmVzW3ArK10gPSAoYyAmIDYzKSB8IDEyODtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICByZXNbcCsrXSA9IChjID4+IDEyKSB8IDIyNDtcbiAgICAgICAgICByZXNbcCsrXSA9ICgoYyA+PiA2KSAmIDYzKSB8IDEyODtcbiAgICAgICAgICByZXNbcCsrXSA9IChjICYgNjMpIHwgMTI4O1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfSBlbHNlIGlmIChlbmMgPT09ICdoZXgnKSB7XG4gICAgICBtc2cgPSBtc2cucmVwbGFjZSgvW15hLXowLTldKy9pZywgJycpO1xuICAgICAgaWYgKG1zZy5sZW5ndGggJSAyICE9PSAwKVxuICAgICAgICBtc2cgPSAnMCcgKyBtc2c7XG4gICAgICBmb3IgKGkgPSAwOyBpIDwgbXNnLmxlbmd0aDsgaSArPSAyKVxuICAgICAgICByZXMucHVzaChwYXJzZUludChtc2dbaV0gKyBtc2dbaSArIDFdLCAxNikpO1xuICAgIH1cbiAgfSBlbHNlIHtcbiAgICBmb3IgKGkgPSAwOyBpIDwgbXNnLmxlbmd0aDsgaSsrKVxuICAgICAgcmVzW2ldID0gbXNnW2ldIHwgMDtcbiAgfVxuICByZXR1cm4gcmVzO1xufVxuZXhwb3J0cy50b0FycmF5ID0gdG9BcnJheTtcblxuZnVuY3Rpb24gdG9IZXgobXNnKSB7XG4gIHZhciByZXMgPSAnJztcbiAgZm9yICh2YXIgaSA9IDA7IGkgPCBtc2cubGVuZ3RoOyBpKyspXG4gICAgcmVzICs9IHplcm8yKG1zZ1tpXS50b1N0cmluZygxNikpO1xuICByZXR1cm4gcmVzO1xufVxuZXhwb3J0cy50b0hleCA9IHRvSGV4O1xuXG5mdW5jdGlvbiBodG9ubCh3KSB7XG4gIHZhciByZXMgPSAodyA+Pj4gMjQpIHxcbiAgICAgICAgICAgICgodyA+Pj4gOCkgJiAweGZmMDApIHxcbiAgICAgICAgICAgICgodyA8PCA4KSAmIDB4ZmYwMDAwKSB8XG4gICAgICAgICAgICAoKHcgJiAweGZmKSA8PCAyNCk7XG4gIHJldHVybiByZXMgPj4+IDA7XG59XG5leHBvcnRzLmh0b25sID0gaHRvbmw7XG5cbmZ1bmN0aW9uIHRvSGV4MzIobXNnLCBlbmRpYW4pIHtcbiAgdmFyIHJlcyA9ICcnO1xuICBmb3IgKHZhciBpID0gMDsgaSA8IG1zZy5sZW5ndGg7IGkrKykge1xuICAgIHZhciB3ID0gbXNnW2ldO1xuICAgIGlmIChlbmRpYW4gPT09ICdsaXR0bGUnKVxuICAgICAgdyA9IGh0b25sKHcpO1xuICAgIHJlcyArPSB6ZXJvOCh3LnRvU3RyaW5nKDE2KSk7XG4gIH1cbiAgcmV0dXJuIHJlcztcbn1cbmV4cG9ydHMudG9IZXgzMiA9IHRvSGV4MzI7XG5cbmZ1bmN0aW9uIHplcm8yKHdvcmQpIHtcbiAgaWYgKHdvcmQubGVuZ3RoID09PSAxKVxuICAgIHJldHVybiAnMCcgKyB3b3JkO1xuICBlbHNlXG4gICAgcmV0dXJuIHdvcmQ7XG59XG5leHBvcnRzLnplcm8yID0gemVybzI7XG5cbmZ1bmN0aW9uIHplcm84KHdvcmQpIHtcbiAgaWYgKHdvcmQubGVuZ3RoID09PSA3KVxuICAgIHJldHVybiAnMCcgKyB3b3JkO1xuICBlbHNlIGlmICh3b3JkLmxlbmd0aCA9PT0gNilcbiAgICByZXR1cm4gJzAwJyArIHdvcmQ7XG4gIGVsc2UgaWYgKHdvcmQubGVuZ3RoID09PSA1KVxuICAgIHJldHVybiAnMDAwJyArIHdvcmQ7XG4gIGVsc2UgaWYgKHdvcmQubGVuZ3RoID09PSA0KVxuICAgIHJldHVybiAnMDAwMCcgKyB3b3JkO1xuICBlbHNlIGlmICh3b3JkLmxlbmd0aCA9PT0gMylcbiAgICByZXR1cm4gJzAwMDAwJyArIHdvcmQ7XG4gIGVsc2UgaWYgKHdvcmQubGVuZ3RoID09PSAyKVxuICAgIHJldHVybiAnMDAwMDAwJyArIHdvcmQ7XG4gIGVsc2UgaWYgKHdvcmQubGVuZ3RoID09PSAxKVxuICAgIHJldHVybiAnMDAwMDAwMCcgKyB3b3JkO1xuICBlbHNlXG4gICAgcmV0dXJuIHdvcmQ7XG59XG5leHBvcnRzLnplcm84ID0gemVybzg7XG5cbmZ1bmN0aW9uIGpvaW4zMihtc2csIHN0YXJ0LCBlbmQsIGVuZGlhbikge1xuICB2YXIgbGVuID0gZW5kIC0gc3RhcnQ7XG4gIGFzc2VydChsZW4gJSA0ID09PSAwKTtcbiAgdmFyIHJlcyA9IG5ldyBBcnJheShsZW4gLyA0KTtcbiAgZm9yICh2YXIgaSA9IDAsIGsgPSBzdGFydDsgaSA8IHJlcy5sZW5ndGg7IGkrKywgayArPSA0KSB7XG4gICAgdmFyIHc7XG4gICAgaWYgKGVuZGlhbiA9PT0gJ2JpZycpXG4gICAgICB3ID0gKG1zZ1trXSA8PCAyNCkgfCAobXNnW2sgKyAxXSA8PCAxNikgfCAobXNnW2sgKyAyXSA8PCA4KSB8IG1zZ1trICsgM107XG4gICAgZWxzZVxuICAgICAgdyA9IChtc2dbayArIDNdIDw8IDI0KSB8IChtc2dbayArIDJdIDw8IDE2KSB8IChtc2dbayArIDFdIDw8IDgpIHwgbXNnW2tdO1xuICAgIHJlc1tpXSA9IHcgPj4+IDA7XG4gIH1cbiAgcmV0dXJuIHJlcztcbn1cbmV4cG9ydHMuam9pbjMyID0gam9pbjMyO1xuXG5mdW5jdGlvbiBzcGxpdDMyKG1zZywgZW5kaWFuKSB7XG4gIHZhciByZXMgPSBuZXcgQXJyYXkobXNnLmxlbmd0aCAqIDQpO1xuICBmb3IgKHZhciBpID0gMCwgayA9IDA7IGkgPCBtc2cubGVuZ3RoOyBpKyssIGsgKz0gNCkge1xuICAgIHZhciBtID0gbXNnW2ldO1xuICAgIGlmIChlbmRpYW4gPT09ICdiaWcnKSB7XG4gICAgICByZXNba10gPSBtID4+PiAyNDtcbiAgICAgIHJlc1trICsgMV0gPSAobSA+Pj4gMTYpICYgMHhmZjtcbiAgICAgIHJlc1trICsgMl0gPSAobSA+Pj4gOCkgJiAweGZmO1xuICAgICAgcmVzW2sgKyAzXSA9IG0gJiAweGZmO1xuICAgIH0gZWxzZSB7XG4gICAgICByZXNbayArIDNdID0gbSA+Pj4gMjQ7XG4gICAgICByZXNbayArIDJdID0gKG0gPj4+IDE2KSAmIDB4ZmY7XG4gICAgICByZXNbayArIDFdID0gKG0gPj4+IDgpICYgMHhmZjtcbiAgICAgIHJlc1trXSA9IG0gJiAweGZmO1xuICAgIH1cbiAgfVxuICByZXR1cm4gcmVzO1xufVxuZXhwb3J0cy5zcGxpdDMyID0gc3BsaXQzMjtcblxuZnVuY3Rpb24gcm90cjMyKHcsIGIpIHtcbiAgcmV0dXJuICh3ID4+PiBiKSB8ICh3IDw8ICgzMiAtIGIpKTtcbn1cbmV4cG9ydHMucm90cjMyID0gcm90cjMyO1xuXG5mdW5jdGlvbiByb3RsMzIodywgYikge1xuICByZXR1cm4gKHcgPDwgYikgfCAodyA+Pj4gKDMyIC0gYikpO1xufVxuZXhwb3J0cy5yb3RsMzIgPSByb3RsMzI7XG5cbmZ1bmN0aW9uIHN1bTMyKGEsIGIpIHtcbiAgcmV0dXJuIChhICsgYikgPj4+IDA7XG59XG5leHBvcnRzLnN1bTMyID0gc3VtMzI7XG5cbmZ1bmN0aW9uIHN1bTMyXzMoYSwgYiwgYykge1xuICByZXR1cm4gKGEgKyBiICsgYykgPj4+IDA7XG59XG5leHBvcnRzLnN1bTMyXzMgPSBzdW0zMl8zO1xuXG5mdW5jdGlvbiBzdW0zMl80KGEsIGIsIGMsIGQpIHtcbiAgcmV0dXJuIChhICsgYiArIGMgKyBkKSA+Pj4gMDtcbn1cbmV4cG9ydHMuc3VtMzJfNCA9IHN1bTMyXzQ7XG5cbmZ1bmN0aW9uIHN1bTMyXzUoYSwgYiwgYywgZCwgZSkge1xuICByZXR1cm4gKGEgKyBiICsgYyArIGQgKyBlKSA+Pj4gMDtcbn1cbmV4cG9ydHMuc3VtMzJfNSA9IHN1bTMyXzU7XG5cbmZ1bmN0aW9uIHN1bTY0KGJ1ZiwgcG9zLCBhaCwgYWwpIHtcbiAgdmFyIGJoID0gYnVmW3Bvc107XG4gIHZhciBibCA9IGJ1Zltwb3MgKyAxXTtcblxuICB2YXIgbG8gPSAoYWwgKyBibCkgPj4+IDA7XG4gIHZhciBoaSA9IChsbyA8IGFsID8gMSA6IDApICsgYWggKyBiaDtcbiAgYnVmW3Bvc10gPSBoaSA+Pj4gMDtcbiAgYnVmW3BvcyArIDFdID0gbG87XG59XG5leHBvcnRzLnN1bTY0ID0gc3VtNjQ7XG5cbmZ1bmN0aW9uIHN1bTY0X2hpKGFoLCBhbCwgYmgsIGJsKSB7XG4gIHZhciBsbyA9IChhbCArIGJsKSA+Pj4gMDtcbiAgdmFyIGhpID0gKGxvIDwgYWwgPyAxIDogMCkgKyBhaCArIGJoO1xuICByZXR1cm4gaGkgPj4+IDA7XG59XG5leHBvcnRzLnN1bTY0X2hpID0gc3VtNjRfaGk7XG5cbmZ1bmN0aW9uIHN1bTY0X2xvKGFoLCBhbCwgYmgsIGJsKSB7XG4gIHZhciBsbyA9IGFsICsgYmw7XG4gIHJldHVybiBsbyA+Pj4gMDtcbn1cbmV4cG9ydHMuc3VtNjRfbG8gPSBzdW02NF9sbztcblxuZnVuY3Rpb24gc3VtNjRfNF9oaShhaCwgYWwsIGJoLCBibCwgY2gsIGNsLCBkaCwgZGwpIHtcbiAgdmFyIGNhcnJ5ID0gMDtcbiAgdmFyIGxvID0gYWw7XG4gIGxvID0gKGxvICsgYmwpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGFsID8gMSA6IDA7XG4gIGxvID0gKGxvICsgY2wpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGNsID8gMSA6IDA7XG4gIGxvID0gKGxvICsgZGwpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGRsID8gMSA6IDA7XG5cbiAgdmFyIGhpID0gYWggKyBiaCArIGNoICsgZGggKyBjYXJyeTtcbiAgcmV0dXJuIGhpID4+PiAwO1xufVxuZXhwb3J0cy5zdW02NF80X2hpID0gc3VtNjRfNF9oaTtcblxuZnVuY3Rpb24gc3VtNjRfNF9sbyhhaCwgYWwsIGJoLCBibCwgY2gsIGNsLCBkaCwgZGwpIHtcbiAgdmFyIGxvID0gYWwgKyBibCArIGNsICsgZGw7XG4gIHJldHVybiBsbyA+Pj4gMDtcbn1cbmV4cG9ydHMuc3VtNjRfNF9sbyA9IHN1bTY0XzRfbG87XG5cbmZ1bmN0aW9uIHN1bTY0XzVfaGkoYWgsIGFsLCBiaCwgYmwsIGNoLCBjbCwgZGgsIGRsLCBlaCwgZWwpIHtcbiAgdmFyIGNhcnJ5ID0gMDtcbiAgdmFyIGxvID0gYWw7XG4gIGxvID0gKGxvICsgYmwpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGFsID8gMSA6IDA7XG4gIGxvID0gKGxvICsgY2wpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGNsID8gMSA6IDA7XG4gIGxvID0gKGxvICsgZGwpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGRsID8gMSA6IDA7XG4gIGxvID0gKGxvICsgZWwpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGVsID8gMSA6IDA7XG5cbiAgdmFyIGhpID0gYWggKyBiaCArIGNoICsgZGggKyBlaCArIGNhcnJ5O1xuICByZXR1cm4gaGkgPj4+IDA7XG59XG5leHBvcnRzLnN1bTY0XzVfaGkgPSBzdW02NF81X2hpO1xuXG5mdW5jdGlvbiBzdW02NF81X2xvKGFoLCBhbCwgYmgsIGJsLCBjaCwgY2wsIGRoLCBkbCwgZWgsIGVsKSB7XG4gIHZhciBsbyA9IGFsICsgYmwgKyBjbCArIGRsICsgZWw7XG5cbiAgcmV0dXJuIGxvID4+PiAwO1xufVxuZXhwb3J0cy5zdW02NF81X2xvID0gc3VtNjRfNV9sbztcblxuZnVuY3Rpb24gcm90cjY0X2hpKGFoLCBhbCwgbnVtKSB7XG4gIHZhciByID0gKGFsIDw8ICgzMiAtIG51bSkpIHwgKGFoID4+PiBudW0pO1xuICByZXR1cm4gciA+Pj4gMDtcbn1cbmV4cG9ydHMucm90cjY0X2hpID0gcm90cjY0X2hpO1xuXG5mdW5jdGlvbiByb3RyNjRfbG8oYWgsIGFsLCBudW0pIHtcbiAgdmFyIHIgPSAoYWggPDwgKDMyIC0gbnVtKSkgfCAoYWwgPj4+IG51bSk7XG4gIHJldHVybiByID4+PiAwO1xufVxuZXhwb3J0cy5yb3RyNjRfbG8gPSByb3RyNjRfbG87XG5cbmZ1bmN0aW9uIHNocjY0X2hpKGFoLCBhbCwgbnVtKSB7XG4gIHJldHVybiBhaCA+Pj4gbnVtO1xufVxuZXhwb3J0cy5zaHI2NF9oaSA9IHNocjY0X2hpO1xuXG5mdW5jdGlvbiBzaHI2NF9sbyhhaCwgYWwsIG51bSkge1xuICB2YXIgciA9IChhaCA8PCAoMzIgLSBudW0pKSB8IChhbCA+Pj4gbnVtKTtcbiAgcmV0dXJuIHIgPj4+IDA7XG59XG5leHBvcnRzLnNocjY0X2xvID0gc2hyNjRfbG87XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///6436\n")},5717:function(module){eval("if (typeof Object.create === 'function') {\n // implementation from standard node.js 'util' module\n module.exports = function inherits(ctor, superCtor) {\n if (superCtor) {\n ctor.super_ = superCtor\n ctor.prototype = Object.create(superCtor.prototype, {\n constructor: {\n value: ctor,\n enumerable: false,\n writable: true,\n configurable: true\n }\n })\n }\n };\n} else {\n // old school shim for old browsers\n module.exports = function inherits(ctor, superCtor) {\n if (superCtor) {\n ctor.super_ = superCtor\n var TempCtor = function () {}\n TempCtor.prototype = superCtor.prototype\n ctor.prototype = new TempCtor()\n ctor.prototype.constructor = ctor\n }\n }\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTcxNy5qcyIsIm1hcHBpbmdzIjoiQUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxPQUFPO0FBQ1A7QUFDQTtBQUNBLEVBQUU7QUFDRjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9pbmhlcml0cy9pbmhlcml0c19icm93c2VyLmpzPzNmYjUiXSwic291cmNlc0NvbnRlbnQiOlsiaWYgKHR5cGVvZiBPYmplY3QuY3JlYXRlID09PSAnZnVuY3Rpb24nKSB7XG4gIC8vIGltcGxlbWVudGF0aW9uIGZyb20gc3RhbmRhcmQgbm9kZS5qcyAndXRpbCcgbW9kdWxlXG4gIG1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaW5oZXJpdHMoY3Rvciwgc3VwZXJDdG9yKSB7XG4gICAgaWYgKHN1cGVyQ3Rvcikge1xuICAgICAgY3Rvci5zdXBlcl8gPSBzdXBlckN0b3JcbiAgICAgIGN0b3IucHJvdG90eXBlID0gT2JqZWN0LmNyZWF0ZShzdXBlckN0b3IucHJvdG90eXBlLCB7XG4gICAgICAgIGNvbnN0cnVjdG9yOiB7XG4gICAgICAgICAgdmFsdWU6IGN0b3IsXG4gICAgICAgICAgZW51bWVyYWJsZTogZmFsc2UsXG4gICAgICAgICAgd3JpdGFibGU6IHRydWUsXG4gICAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgICAgIH1cbiAgICAgIH0pXG4gICAgfVxuICB9O1xufSBlbHNlIHtcbiAgLy8gb2xkIHNjaG9vbCBzaGltIGZvciBvbGQgYnJvd3NlcnNcbiAgbW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBpbmhlcml0cyhjdG9yLCBzdXBlckN0b3IpIHtcbiAgICBpZiAoc3VwZXJDdG9yKSB7XG4gICAgICBjdG9yLnN1cGVyXyA9IHN1cGVyQ3RvclxuICAgICAgdmFyIFRlbXBDdG9yID0gZnVuY3Rpb24gKCkge31cbiAgICAgIFRlbXBDdG9yLnByb3RvdHlwZSA9IHN1cGVyQ3Rvci5wcm90b3R5cGVcbiAgICAgIGN0b3IucHJvdG90eXBlID0gbmV3IFRlbXBDdG9yKClcbiAgICAgIGN0b3IucHJvdG90eXBlLmNvbnN0cnVjdG9yID0gY3RvclxuICAgIH1cbiAgfVxufVxuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///5717\n")},9496:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\nvar has = __webpack_require__(7642);\nvar channel = __webpack_require__(7478)();\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar SLOT = {\n\tassert: function (O, slot) {\n\t\tif (!O || (typeof O !== 'object' && typeof O !== 'function')) {\n\t\t\tthrow new $TypeError('`O` is not an object');\n\t\t}\n\t\tif (typeof slot !== 'string') {\n\t\t\tthrow new $TypeError('`slot` must be a string');\n\t\t}\n\t\tchannel.assert(O);\n\t},\n\tget: function (O, slot) {\n\t\tif (!O || (typeof O !== 'object' && typeof O !== 'function')) {\n\t\t\tthrow new $TypeError('`O` is not an object');\n\t\t}\n\t\tif (typeof slot !== 'string') {\n\t\t\tthrow new $TypeError('`slot` must be a string');\n\t\t}\n\t\tvar slots = channel.get(O);\n\t\treturn slots && slots['$' + slot];\n\t},\n\thas: function (O, slot) {\n\t\tif (!O || (typeof O !== 'object' && typeof O !== 'function')) {\n\t\t\tthrow new $TypeError('`O` is not an object');\n\t\t}\n\t\tif (typeof slot !== 'string') {\n\t\t\tthrow new $TypeError('`slot` must be a string');\n\t\t}\n\t\tvar slots = channel.get(O);\n\t\treturn !!slots && has(slots, '$' + slot);\n\t},\n\tset: function (O, slot, V) {\n\t\tif (!O || (typeof O !== 'object' && typeof O !== 'function')) {\n\t\t\tthrow new $TypeError('`O` is not an object');\n\t\t}\n\t\tif (typeof slot !== 'string') {\n\t\t\tthrow new $TypeError('`slot` must be a string');\n\t\t}\n\t\tvar slots = channel.get(O);\n\t\tif (!slots) {\n\t\t\tslots = {};\n\t\t\tchannel.set(O, slots);\n\t\t}\n\t\tslots['$' + slot] = V;\n\t}\n};\n\nif (Object.freeze) {\n\tObject.freeze(SLOT);\n}\n\nmodule.exports = SLOT;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTQ5Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlO0FBQzFDLFVBQVUsbUJBQU8sQ0FBQyxJQUFLO0FBQ3ZCLGNBQWMsbUJBQU8sQ0FBQyxJQUFjOztBQUVwQzs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxFQUFFO0FBQ0Y7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsRUFBRTtBQUNGO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEVBQUU7QUFDRjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9pbnRlcm5hbC1zbG90L2luZGV4LmpzPzY1ZWIiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xudmFyIGhhcyA9IHJlcXVpcmUoJ2hhcycpO1xudmFyIGNoYW5uZWwgPSByZXF1aXJlKCdzaWRlLWNoYW5uZWwnKSgpO1xuXG52YXIgJFR5cGVFcnJvciA9IEdldEludHJpbnNpYygnJVR5cGVFcnJvciUnKTtcblxudmFyIFNMT1QgPSB7XG5cdGFzc2VydDogZnVuY3Rpb24gKE8sIHNsb3QpIHtcblx0XHRpZiAoIU8gfHwgKHR5cGVvZiBPICE9PSAnb2JqZWN0JyAmJiB0eXBlb2YgTyAhPT0gJ2Z1bmN0aW9uJykpIHtcblx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdgT2AgaXMgbm90IGFuIG9iamVjdCcpO1xuXHRcdH1cblx0XHRpZiAodHlwZW9mIHNsb3QgIT09ICdzdHJpbmcnKSB7XG5cdFx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignYHNsb3RgIG11c3QgYmUgYSBzdHJpbmcnKTtcblx0XHR9XG5cdFx0Y2hhbm5lbC5hc3NlcnQoTyk7XG5cdH0sXG5cdGdldDogZnVuY3Rpb24gKE8sIHNsb3QpIHtcblx0XHRpZiAoIU8gfHwgKHR5cGVvZiBPICE9PSAnb2JqZWN0JyAmJiB0eXBlb2YgTyAhPT0gJ2Z1bmN0aW9uJykpIHtcblx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdgT2AgaXMgbm90IGFuIG9iamVjdCcpO1xuXHRcdH1cblx0XHRpZiAodHlwZW9mIHNsb3QgIT09ICdzdHJpbmcnKSB7XG5cdFx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignYHNsb3RgIG11c3QgYmUgYSBzdHJpbmcnKTtcblx0XHR9XG5cdFx0dmFyIHNsb3RzID0gY2hhbm5lbC5nZXQoTyk7XG5cdFx0cmV0dXJuIHNsb3RzICYmIHNsb3RzWyckJyArIHNsb3RdO1xuXHR9LFxuXHRoYXM6IGZ1bmN0aW9uIChPLCBzbG90KSB7XG5cdFx0aWYgKCFPIHx8ICh0eXBlb2YgTyAhPT0gJ29iamVjdCcgJiYgdHlwZW9mIE8gIT09ICdmdW5jdGlvbicpKSB7XG5cdFx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignYE9gIGlzIG5vdCBhbiBvYmplY3QnKTtcblx0XHR9XG5cdFx0aWYgKHR5cGVvZiBzbG90ICE9PSAnc3RyaW5nJykge1xuXHRcdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ2BzbG90YCBtdXN0IGJlIGEgc3RyaW5nJyk7XG5cdFx0fVxuXHRcdHZhciBzbG90cyA9IGNoYW5uZWwuZ2V0KE8pO1xuXHRcdHJldHVybiAhIXNsb3RzICYmIGhhcyhzbG90cywgJyQnICsgc2xvdCk7XG5cdH0sXG5cdHNldDogZnVuY3Rpb24gKE8sIHNsb3QsIFYpIHtcblx0XHRpZiAoIU8gfHwgKHR5cGVvZiBPICE9PSAnb2JqZWN0JyAmJiB0eXBlb2YgTyAhPT0gJ2Z1bmN0aW9uJykpIHtcblx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdgT2AgaXMgbm90IGFuIG9iamVjdCcpO1xuXHRcdH1cblx0XHRpZiAodHlwZW9mIHNsb3QgIT09ICdzdHJpbmcnKSB7XG5cdFx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignYHNsb3RgIG11c3QgYmUgYSBzdHJpbmcnKTtcblx0XHR9XG5cdFx0dmFyIHNsb3RzID0gY2hhbm5lbC5nZXQoTyk7XG5cdFx0aWYgKCFzbG90cykge1xuXHRcdFx0c2xvdHMgPSB7fTtcblx0XHRcdGNoYW5uZWwuc2V0KE8sIHNsb3RzKTtcblx0XHR9XG5cdFx0c2xvdHNbJyQnICsgc2xvdF0gPSBWO1xuXHR9XG59O1xuXG5pZiAoT2JqZWN0LmZyZWV6ZSkge1xuXHRPYmplY3QuZnJlZXplKFNMT1QpO1xufVxuXG5tb2R1bGUuZXhwb3J0cyA9IFNMT1Q7XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///9496\n")},5320:function(module){"use strict";eval("\n\nvar fnToStr = Function.prototype.toString;\nvar reflectApply = typeof Reflect === 'object' && Reflect !== null && Reflect.apply;\nvar badArrayLike;\nvar isCallableMarker;\nif (typeof reflectApply === 'function' && typeof Object.defineProperty === 'function') {\n\ttry {\n\t\tbadArrayLike = Object.defineProperty({}, 'length', {\n\t\t\tget: function () {\n\t\t\t\tthrow isCallableMarker;\n\t\t\t}\n\t\t});\n\t\tisCallableMarker = {};\n\t\t// eslint-disable-next-line no-throw-literal\n\t\treflectApply(function () { throw 42; }, null, badArrayLike);\n\t} catch (_) {\n\t\tif (_ !== isCallableMarker) {\n\t\t\treflectApply = null;\n\t\t}\n\t}\n} else {\n\treflectApply = null;\n}\n\nvar constructorRegex = /^\\s*class\\b/;\nvar isES6ClassFn = function isES6ClassFunction(value) {\n\ttry {\n\t\tvar fnStr = fnToStr.call(value);\n\t\treturn constructorRegex.test(fnStr);\n\t} catch (e) {\n\t\treturn false; // not a function\n\t}\n};\n\nvar tryFunctionObject = function tryFunctionToStr(value) {\n\ttry {\n\t\tif (isES6ClassFn(value)) { return false; }\n\t\tfnToStr.call(value);\n\t\treturn true;\n\t} catch (e) {\n\t\treturn false;\n\t}\n};\nvar toStr = Object.prototype.toString;\nvar fnClass = '[object Function]';\nvar genClass = '[object GeneratorFunction]';\nvar hasToStringTag = typeof Symbol === 'function' && !!Symbol.toStringTag; // better: use `has-tostringtag`\n/* globals document: false */\nvar documentDotAll = typeof document === 'object' && typeof document.all === 'undefined' && document.all !== undefined ? document.all : {};\n\nmodule.exports = reflectApply\n\t? function isCallable(value) {\n\t\tif (value === documentDotAll) { return true; }\n\t\tif (!value) { return false; }\n\t\tif (typeof value !== 'function' && typeof value !== 'object') { return false; }\n\t\tif (typeof value === 'function' && !value.prototype) { return true; }\n\t\ttry {\n\t\t\treflectApply(value, null, badArrayLike);\n\t\t} catch (e) {\n\t\t\tif (e !== isCallableMarker) { return false; }\n\t\t}\n\t\treturn !isES6ClassFn(value);\n\t}\n\t: function isCallable(value) {\n\t\tif (value === documentDotAll) { return true; }\n\t\tif (!value) { return false; }\n\t\tif (typeof value !== 'function' && typeof value !== 'object') { return false; }\n\t\tif (typeof value === 'function' && !value.prototype) { return true; }\n\t\tif (hasToStringTag) { return tryFunctionObject(value); }\n\t\tif (isES6ClassFn(value)) { return false; }\n\t\tvar strClass = toStr.call(value);\n\t\treturn strClass === fnClass || strClass === genClass;\n\t};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTMyMC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSx5Q0FBeUM7QUFDekM7QUFDQTtBQUNBO0FBQ0EsR0FBRztBQUNIO0FBQ0E7QUFDQSw2QkFBNkIsV0FBVztBQUN4QyxHQUFHO0FBQ0g7QUFDQTtBQUNBO0FBQ0E7QUFDQSxFQUFFO0FBQ0Y7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsR0FBRztBQUNILGdCQUFnQjtBQUNoQjtBQUNBOztBQUVBO0FBQ0E7QUFDQSw2QkFBNkI7QUFDN0I7QUFDQTtBQUNBLEdBQUc7QUFDSDtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSwyRUFBMkU7QUFDM0U7QUFDQTs7QUFFQTtBQUNBO0FBQ0Esa0NBQWtDO0FBQ2xDLGdCQUFnQjtBQUNoQixrRUFBa0U7QUFDbEUseURBQXlEO0FBQ3pEO0FBQ0E7QUFDQSxJQUFJO0FBQ0osaUNBQWlDO0FBQ2pDO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esa0NBQWtDO0FBQ2xDLGdCQUFnQjtBQUNoQixrRUFBa0U7QUFDbEUseURBQXlEO0FBQ3pELHdCQUF3QjtBQUN4Qiw2QkFBNkI7QUFDN0I7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9pcy1jYWxsYWJsZS9pbmRleC5qcz8yMWQwIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGZuVG9TdHIgPSBGdW5jdGlvbi5wcm90b3R5cGUudG9TdHJpbmc7XG52YXIgcmVmbGVjdEFwcGx5ID0gdHlwZW9mIFJlZmxlY3QgPT09ICdvYmplY3QnICYmIFJlZmxlY3QgIT09IG51bGwgJiYgUmVmbGVjdC5hcHBseTtcbnZhciBiYWRBcnJheUxpa2U7XG52YXIgaXNDYWxsYWJsZU1hcmtlcjtcbmlmICh0eXBlb2YgcmVmbGVjdEFwcGx5ID09PSAnZnVuY3Rpb24nICYmIHR5cGVvZiBPYmplY3QuZGVmaW5lUHJvcGVydHkgPT09ICdmdW5jdGlvbicpIHtcblx0dHJ5IHtcblx0XHRiYWRBcnJheUxpa2UgPSBPYmplY3QuZGVmaW5lUHJvcGVydHkoe30sICdsZW5ndGgnLCB7XG5cdFx0XHRnZXQ6IGZ1bmN0aW9uICgpIHtcblx0XHRcdFx0dGhyb3cgaXNDYWxsYWJsZU1hcmtlcjtcblx0XHRcdH1cblx0XHR9KTtcblx0XHRpc0NhbGxhYmxlTWFya2VyID0ge307XG5cdFx0Ly8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLXRocm93LWxpdGVyYWxcblx0XHRyZWZsZWN0QXBwbHkoZnVuY3Rpb24gKCkgeyB0aHJvdyA0MjsgfSwgbnVsbCwgYmFkQXJyYXlMaWtlKTtcblx0fSBjYXRjaCAoXykge1xuXHRcdGlmIChfICE9PSBpc0NhbGxhYmxlTWFya2VyKSB7XG5cdFx0XHRyZWZsZWN0QXBwbHkgPSBudWxsO1xuXHRcdH1cblx0fVxufSBlbHNlIHtcblx0cmVmbGVjdEFwcGx5ID0gbnVsbDtcbn1cblxudmFyIGNvbnN0cnVjdG9yUmVnZXggPSAvXlxccypjbGFzc1xcYi87XG52YXIgaXNFUzZDbGFzc0ZuID0gZnVuY3Rpb24gaXNFUzZDbGFzc0Z1bmN0aW9uKHZhbHVlKSB7XG5cdHRyeSB7XG5cdFx0dmFyIGZuU3RyID0gZm5Ub1N0ci5jYWxsKHZhbHVlKTtcblx0XHRyZXR1cm4gY29uc3RydWN0b3JSZWdleC50ZXN0KGZuU3RyKTtcblx0fSBjYXRjaCAoZSkge1xuXHRcdHJldHVybiBmYWxzZTsgLy8gbm90IGEgZnVuY3Rpb25cblx0fVxufTtcblxudmFyIHRyeUZ1bmN0aW9uT2JqZWN0ID0gZnVuY3Rpb24gdHJ5RnVuY3Rpb25Ub1N0cih2YWx1ZSkge1xuXHR0cnkge1xuXHRcdGlmIChpc0VTNkNsYXNzRm4odmFsdWUpKSB7IHJldHVybiBmYWxzZTsgfVxuXHRcdGZuVG9TdHIuY2FsbCh2YWx1ZSk7XG5cdFx0cmV0dXJuIHRydWU7XG5cdH0gY2F0Y2ggKGUpIHtcblx0XHRyZXR1cm4gZmFsc2U7XG5cdH1cbn07XG52YXIgdG9TdHIgPSBPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nO1xudmFyIGZuQ2xhc3MgPSAnW29iamVjdCBGdW5jdGlvbl0nO1xudmFyIGdlbkNsYXNzID0gJ1tvYmplY3QgR2VuZXJhdG9yRnVuY3Rpb25dJztcbnZhciBoYXNUb1N0cmluZ1RhZyA9IHR5cGVvZiBTeW1ib2wgPT09ICdmdW5jdGlvbicgJiYgISFTeW1ib2wudG9TdHJpbmdUYWc7IC8vIGJldHRlcjogdXNlIGBoYXMtdG9zdHJpbmd0YWdgXG4vKiBnbG9iYWxzIGRvY3VtZW50OiBmYWxzZSAqL1xudmFyIGRvY3VtZW50RG90QWxsID0gdHlwZW9mIGRvY3VtZW50ID09PSAnb2JqZWN0JyAmJiB0eXBlb2YgZG9jdW1lbnQuYWxsID09PSAndW5kZWZpbmVkJyAmJiBkb2N1bWVudC5hbGwgIT09IHVuZGVmaW5lZCA/IGRvY3VtZW50LmFsbCA6IHt9O1xuXG5tb2R1bGUuZXhwb3J0cyA9IHJlZmxlY3RBcHBseVxuXHQ/IGZ1bmN0aW9uIGlzQ2FsbGFibGUodmFsdWUpIHtcblx0XHRpZiAodmFsdWUgPT09IGRvY3VtZW50RG90QWxsKSB7IHJldHVybiB0cnVlOyB9XG5cdFx0aWYgKCF2YWx1ZSkgeyByZXR1cm4gZmFsc2U7IH1cblx0XHRpZiAodHlwZW9mIHZhbHVlICE9PSAnZnVuY3Rpb24nICYmIHR5cGVvZiB2YWx1ZSAhPT0gJ29iamVjdCcpIHsgcmV0dXJuIGZhbHNlOyB9XG5cdFx0aWYgKHR5cGVvZiB2YWx1ZSA9PT0gJ2Z1bmN0aW9uJyAmJiAhdmFsdWUucHJvdG90eXBlKSB7IHJldHVybiB0cnVlOyB9XG5cdFx0dHJ5IHtcblx0XHRcdHJlZmxlY3RBcHBseSh2YWx1ZSwgbnVsbCwgYmFkQXJyYXlMaWtlKTtcblx0XHR9IGNhdGNoIChlKSB7XG5cdFx0XHRpZiAoZSAhPT0gaXNDYWxsYWJsZU1hcmtlcikgeyByZXR1cm4gZmFsc2U7IH1cblx0XHR9XG5cdFx0cmV0dXJuICFpc0VTNkNsYXNzRm4odmFsdWUpO1xuXHR9XG5cdDogZnVuY3Rpb24gaXNDYWxsYWJsZSh2YWx1ZSkge1xuXHRcdGlmICh2YWx1ZSA9PT0gZG9jdW1lbnREb3RBbGwpIHsgcmV0dXJuIHRydWU7IH1cblx0XHRpZiAoIXZhbHVlKSB7IHJldHVybiBmYWxzZTsgfVxuXHRcdGlmICh0eXBlb2YgdmFsdWUgIT09ICdmdW5jdGlvbicgJiYgdHlwZW9mIHZhbHVlICE9PSAnb2JqZWN0JykgeyByZXR1cm4gZmFsc2U7IH1cblx0XHRpZiAodHlwZW9mIHZhbHVlID09PSAnZnVuY3Rpb24nICYmICF2YWx1ZS5wcm90b3R5cGUpIHsgcmV0dXJuIHRydWU7IH1cblx0XHRpZiAoaGFzVG9TdHJpbmdUYWcpIHsgcmV0dXJuIHRyeUZ1bmN0aW9uT2JqZWN0KHZhbHVlKTsgfVxuXHRcdGlmIChpc0VTNkNsYXNzRm4odmFsdWUpKSB7IHJldHVybiBmYWxzZTsgfVxuXHRcdHZhciBzdHJDbGFzcyA9IHRvU3RyLmNhbGwodmFsdWUpO1xuXHRcdHJldHVybiBzdHJDbGFzcyA9PT0gZm5DbGFzcyB8fCBzdHJDbGFzcyA9PT0gZ2VuQ2xhc3M7XG5cdH07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///5320\n")},8923:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar getDay = Date.prototype.getDay;\nvar tryDateObject = function tryDateGetDayCall(value) {\n\ttry {\n\t\tgetDay.call(value);\n\t\treturn true;\n\t} catch (e) {\n\t\treturn false;\n\t}\n};\n\nvar toStr = Object.prototype.toString;\nvar dateClass = '[object Date]';\nvar hasToStringTag = __webpack_require__(6410)();\n\nmodule.exports = function isDateObject(value) {\n\tif (typeof value !== 'object' || value === null) {\n\t\treturn false;\n\t}\n\treturn hasToStringTag ? tryDateObject(value) : toStr.call(value) === dateClass;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODkyMy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsR0FBRztBQUNIO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0EscUJBQXFCLG1CQUFPLENBQUMsSUFBdUI7O0FBRXBEO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvaXMtZGF0ZS1vYmplY3QvaW5kZXguanM/MGU2NSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBnZXREYXkgPSBEYXRlLnByb3RvdHlwZS5nZXREYXk7XG52YXIgdHJ5RGF0ZU9iamVjdCA9IGZ1bmN0aW9uIHRyeURhdGVHZXREYXlDYWxsKHZhbHVlKSB7XG5cdHRyeSB7XG5cdFx0Z2V0RGF5LmNhbGwodmFsdWUpO1xuXHRcdHJldHVybiB0cnVlO1xuXHR9IGNhdGNoIChlKSB7XG5cdFx0cmV0dXJuIGZhbHNlO1xuXHR9XG59O1xuXG52YXIgdG9TdHIgPSBPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nO1xudmFyIGRhdGVDbGFzcyA9ICdbb2JqZWN0IERhdGVdJztcbnZhciBoYXNUb1N0cmluZ1RhZyA9IHJlcXVpcmUoJ2hhcy10b3N0cmluZ3RhZy9zaGFtcycpKCk7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaXNEYXRlT2JqZWN0KHZhbHVlKSB7XG5cdGlmICh0eXBlb2YgdmFsdWUgIT09ICdvYmplY3QnIHx8IHZhbHVlID09PSBudWxsKSB7XG5cdFx0cmV0dXJuIGZhbHNlO1xuXHR9XG5cdHJldHVybiBoYXNUb1N0cmluZ1RhZyA/IHRyeURhdGVPYmplY3QodmFsdWUpIDogdG9TdHIuY2FsbCh2YWx1ZSkgPT09IGRhdGVDbGFzcztcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///8923\n")},8420:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar callBound = __webpack_require__(1924);\nvar hasToStringTag = __webpack_require__(6410)();\nvar has;\nvar $exec;\nvar isRegexMarker;\nvar badStringifier;\n\nif (hasToStringTag) {\n\thas = callBound('Object.prototype.hasOwnProperty');\n\t$exec = callBound('RegExp.prototype.exec');\n\tisRegexMarker = {};\n\n\tvar throwRegexMarker = function () {\n\t\tthrow isRegexMarker;\n\t};\n\tbadStringifier = {\n\t\ttoString: throwRegexMarker,\n\t\tvalueOf: throwRegexMarker\n\t};\n\n\tif (typeof Symbol.toPrimitive === 'symbol') {\n\t\tbadStringifier[Symbol.toPrimitive] = throwRegexMarker;\n\t}\n}\n\nvar $toString = callBound('Object.prototype.toString');\nvar gOPD = Object.getOwnPropertyDescriptor;\nvar regexClass = '[object RegExp]';\n\nmodule.exports = hasToStringTag\n\t// eslint-disable-next-line consistent-return\n\t? function isRegex(value) {\n\t\tif (!value || typeof value !== 'object') {\n\t\t\treturn false;\n\t\t}\n\n\t\tvar descriptor = gOPD(value, 'lastIndex');\n\t\tvar hasLastIndexDataProperty = descriptor && has(descriptor, 'value');\n\t\tif (!hasLastIndexDataProperty) {\n\t\t\treturn false;\n\t\t}\n\n\t\ttry {\n\t\t\t$exec(value, badStringifier);\n\t\t} catch (e) {\n\t\t\treturn e === isRegexMarker;\n\t\t}\n\t}\n\t: function isRegex(value) {\n\t\t// In older browsers, typeof regex incorrectly returns 'function'\n\t\tif (!value || (typeof value !== 'object' && typeof value !== 'function')) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $toString(value) === regexClass;\n\t};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODQyMC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixnQkFBZ0IsbUJBQU8sQ0FBQyxJQUFxQjtBQUM3QyxxQkFBcUIsbUJBQU8sQ0FBQyxJQUF1QjtBQUNwRDtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBLElBQUk7QUFDSjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2lzLXJlZ2V4L2luZGV4LmpzP2Q4ZDgiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgY2FsbEJvdW5kID0gcmVxdWlyZSgnY2FsbC1iaW5kL2NhbGxCb3VuZCcpO1xudmFyIGhhc1RvU3RyaW5nVGFnID0gcmVxdWlyZSgnaGFzLXRvc3RyaW5ndGFnL3NoYW1zJykoKTtcbnZhciBoYXM7XG52YXIgJGV4ZWM7XG52YXIgaXNSZWdleE1hcmtlcjtcbnZhciBiYWRTdHJpbmdpZmllcjtcblxuaWYgKGhhc1RvU3RyaW5nVGFnKSB7XG5cdGhhcyA9IGNhbGxCb3VuZCgnT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eScpO1xuXHQkZXhlYyA9IGNhbGxCb3VuZCgnUmVnRXhwLnByb3RvdHlwZS5leGVjJyk7XG5cdGlzUmVnZXhNYXJrZXIgPSB7fTtcblxuXHR2YXIgdGhyb3dSZWdleE1hcmtlciA9IGZ1bmN0aW9uICgpIHtcblx0XHR0aHJvdyBpc1JlZ2V4TWFya2VyO1xuXHR9O1xuXHRiYWRTdHJpbmdpZmllciA9IHtcblx0XHR0b1N0cmluZzogdGhyb3dSZWdleE1hcmtlcixcblx0XHR2YWx1ZU9mOiB0aHJvd1JlZ2V4TWFya2VyXG5cdH07XG5cblx0aWYgKHR5cGVvZiBTeW1ib2wudG9QcmltaXRpdmUgPT09ICdzeW1ib2wnKSB7XG5cdFx0YmFkU3RyaW5naWZpZXJbU3ltYm9sLnRvUHJpbWl0aXZlXSA9IHRocm93UmVnZXhNYXJrZXI7XG5cdH1cbn1cblxudmFyICR0b1N0cmluZyA9IGNhbGxCb3VuZCgnT2JqZWN0LnByb3RvdHlwZS50b1N0cmluZycpO1xudmFyIGdPUEQgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yO1xudmFyIHJlZ2V4Q2xhc3MgPSAnW29iamVjdCBSZWdFeHBdJztcblxubW9kdWxlLmV4cG9ydHMgPSBoYXNUb1N0cmluZ1RhZ1xuXHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgY29uc2lzdGVudC1yZXR1cm5cblx0PyBmdW5jdGlvbiBpc1JlZ2V4KHZhbHVlKSB7XG5cdFx0aWYgKCF2YWx1ZSB8fCB0eXBlb2YgdmFsdWUgIT09ICdvYmplY3QnKSB7XG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0fVxuXG5cdFx0dmFyIGRlc2NyaXB0b3IgPSBnT1BEKHZhbHVlLCAnbGFzdEluZGV4Jyk7XG5cdFx0dmFyIGhhc0xhc3RJbmRleERhdGFQcm9wZXJ0eSA9IGRlc2NyaXB0b3IgJiYgaGFzKGRlc2NyaXB0b3IsICd2YWx1ZScpO1xuXHRcdGlmICghaGFzTGFzdEluZGV4RGF0YVByb3BlcnR5KSB7XG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0fVxuXG5cdFx0dHJ5IHtcblx0XHRcdCRleGVjKHZhbHVlLCBiYWRTdHJpbmdpZmllcik7XG5cdFx0fSBjYXRjaCAoZSkge1xuXHRcdFx0cmV0dXJuIGUgPT09IGlzUmVnZXhNYXJrZXI7XG5cdFx0fVxuXHR9XG5cdDogZnVuY3Rpb24gaXNSZWdleCh2YWx1ZSkge1xuXHRcdC8vIEluIG9sZGVyIGJyb3dzZXJzLCB0eXBlb2YgcmVnZXggaW5jb3JyZWN0bHkgcmV0dXJucyAnZnVuY3Rpb24nXG5cdFx0aWYgKCF2YWx1ZSB8fCAodHlwZW9mIHZhbHVlICE9PSAnb2JqZWN0JyAmJiB0eXBlb2YgdmFsdWUgIT09ICdmdW5jdGlvbicpKSB7XG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0fVxuXG5cdFx0cmV0dXJuICR0b1N0cmluZyh2YWx1ZSkgPT09IHJlZ2V4Q2xhc3M7XG5cdH07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///8420\n")},2636:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar toStr = Object.prototype.toString;\nvar hasSymbols = __webpack_require__(1405)();\n\nif (hasSymbols) {\n\tvar symToStr = Symbol.prototype.toString;\n\tvar symStringRegex = /^Symbol\\(.*\\)$/;\n\tvar isSymbolObject = function isRealSymbolObject(value) {\n\t\tif (typeof value.valueOf() !== 'symbol') {\n\t\t\treturn false;\n\t\t}\n\t\treturn symStringRegex.test(symToStr.call(value));\n\t};\n\n\tmodule.exports = function isSymbol(value) {\n\t\tif (typeof value === 'symbol') {\n\t\t\treturn true;\n\t\t}\n\t\tif (toStr.call(value) !== '[object Symbol]') {\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\treturn isSymbolObject(value);\n\t\t} catch (e) {\n\t\t\treturn false;\n\t\t}\n\t};\n} else {\n\n\tmodule.exports = function isSymbol(value) {\n\t\t// this environment does not support Symbols.\n\t\treturn false && 0;\n\t};\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjYzNi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBLGlCQUFpQixtQkFBTyxDQUFDLElBQWE7O0FBRXRDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxJQUFJO0FBQ0o7QUFDQTtBQUNBO0FBQ0EsRUFBRTs7QUFFRjtBQUNBO0FBQ0EsU0FBUyxNQUFLLElBQUksQ0FBSztBQUN2QjtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9pcy1zeW1ib2wvaW5kZXguanM/ZmVjNSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciB0b1N0ciA9IE9iamVjdC5wcm90b3R5cGUudG9TdHJpbmc7XG52YXIgaGFzU3ltYm9scyA9IHJlcXVpcmUoJ2hhcy1zeW1ib2xzJykoKTtcblxuaWYgKGhhc1N5bWJvbHMpIHtcblx0dmFyIHN5bVRvU3RyID0gU3ltYm9sLnByb3RvdHlwZS50b1N0cmluZztcblx0dmFyIHN5bVN0cmluZ1JlZ2V4ID0gL15TeW1ib2xcXCguKlxcKSQvO1xuXHR2YXIgaXNTeW1ib2xPYmplY3QgPSBmdW5jdGlvbiBpc1JlYWxTeW1ib2xPYmplY3QodmFsdWUpIHtcblx0XHRpZiAodHlwZW9mIHZhbHVlLnZhbHVlT2YoKSAhPT0gJ3N5bWJvbCcpIHtcblx0XHRcdHJldHVybiBmYWxzZTtcblx0XHR9XG5cdFx0cmV0dXJuIHN5bVN0cmluZ1JlZ2V4LnRlc3Qoc3ltVG9TdHIuY2FsbCh2YWx1ZSkpO1xuXHR9O1xuXG5cdG1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaXNTeW1ib2wodmFsdWUpIHtcblx0XHRpZiAodHlwZW9mIHZhbHVlID09PSAnc3ltYm9sJykge1xuXHRcdFx0cmV0dXJuIHRydWU7XG5cdFx0fVxuXHRcdGlmICh0b1N0ci5jYWxsKHZhbHVlKSAhPT0gJ1tvYmplY3QgU3ltYm9sXScpIHtcblx0XHRcdHJldHVybiBmYWxzZTtcblx0XHR9XG5cdFx0dHJ5IHtcblx0XHRcdHJldHVybiBpc1N5bWJvbE9iamVjdCh2YWx1ZSk7XG5cdFx0fSBjYXRjaCAoZSkge1xuXHRcdFx0cmV0dXJuIGZhbHNlO1xuXHRcdH1cblx0fTtcbn0gZWxzZSB7XG5cblx0bW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBpc1N5bWJvbCh2YWx1ZSkge1xuXHRcdC8vIHRoaXMgZW52aXJvbm1lbnQgZG9lcyBub3Qgc3VwcG9ydCBTeW1ib2xzLlxuXHRcdHJldHVybiBmYWxzZSAmJiB2YWx1ZTtcblx0fTtcbn1cbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2636\n")},9746:function(module){eval("module.exports = assert;\n\nfunction assert(val, msg) {\n if (!val)\n throw new Error(msg || 'Assertion failed');\n}\n\nassert.equal = function assertEqual(l, r, msg) {\n if (l != r)\n throw new Error(msg || ('Assertion failed: ' + l + ' != ' + r));\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTc0Ni5qcyIsIm1hcHBpbmdzIjoiQUFBQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvbWluaW1hbGlzdGljLWFzc2VydC9pbmRleC5qcz9kYTNlIl0sInNvdXJjZXNDb250ZW50IjpbIm1vZHVsZS5leHBvcnRzID0gYXNzZXJ0O1xuXG5mdW5jdGlvbiBhc3NlcnQodmFsLCBtc2cpIHtcbiAgaWYgKCF2YWwpXG4gICAgdGhyb3cgbmV3IEVycm9yKG1zZyB8fCAnQXNzZXJ0aW9uIGZhaWxlZCcpO1xufVxuXG5hc3NlcnQuZXF1YWwgPSBmdW5jdGlvbiBhc3NlcnRFcXVhbChsLCByLCBtc2cpIHtcbiAgaWYgKGwgIT0gcilcbiAgICB0aHJvdyBuZXcgRXJyb3IobXNnIHx8ICgnQXNzZXJ0aW9uIGZhaWxlZDogJyArIGwgKyAnICE9ICcgKyByKSk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///9746\n")},631:function(module,__unused_webpack_exports,__webpack_require__){eval("var hasMap = typeof Map === 'function' && Map.prototype;\nvar mapSizeDescriptor = Object.getOwnPropertyDescriptor && hasMap ? Object.getOwnPropertyDescriptor(Map.prototype, 'size') : null;\nvar mapSize = hasMap && mapSizeDescriptor && typeof mapSizeDescriptor.get === 'function' ? mapSizeDescriptor.get : null;\nvar mapForEach = hasMap && Map.prototype.forEach;\nvar hasSet = typeof Set === 'function' && Set.prototype;\nvar setSizeDescriptor = Object.getOwnPropertyDescriptor && hasSet ? Object.getOwnPropertyDescriptor(Set.prototype, 'size') : null;\nvar setSize = hasSet && setSizeDescriptor && typeof setSizeDescriptor.get === 'function' ? setSizeDescriptor.get : null;\nvar setForEach = hasSet && Set.prototype.forEach;\nvar hasWeakMap = typeof WeakMap === 'function' && WeakMap.prototype;\nvar weakMapHas = hasWeakMap ? WeakMap.prototype.has : null;\nvar hasWeakSet = typeof WeakSet === 'function' && WeakSet.prototype;\nvar weakSetHas = hasWeakSet ? WeakSet.prototype.has : null;\nvar hasWeakRef = typeof WeakRef === 'function' && WeakRef.prototype;\nvar weakRefDeref = hasWeakRef ? WeakRef.prototype.deref : null;\nvar booleanValueOf = Boolean.prototype.valueOf;\nvar objectToString = Object.prototype.toString;\nvar functionToString = Function.prototype.toString;\nvar match = String.prototype.match;\nvar bigIntValueOf = typeof BigInt === 'function' ? BigInt.prototype.valueOf : null;\nvar gOPS = Object.getOwnPropertySymbols;\nvar symToString = typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' ? Symbol.prototype.toString : null;\nvar hasShammedSymbols = typeof Symbol === 'function' && typeof Symbol.iterator === 'object';\nvar isEnumerable = Object.prototype.propertyIsEnumerable;\n\nvar gPO = (typeof Reflect === 'function' ? Reflect.getPrototypeOf : Object.getPrototypeOf) || (\n [].__proto__ === Array.prototype // eslint-disable-line no-proto\n ? function (O) {\n return O.__proto__; // eslint-disable-line no-proto\n }\n : null\n);\n\nvar inspectCustom = __webpack_require__(4654).custom;\nvar inspectSymbol = inspectCustom && isSymbol(inspectCustom) ? inspectCustom : null;\nvar toStringTag = typeof Symbol === 'function' && typeof Symbol.toStringTag !== 'undefined' ? Symbol.toStringTag : null;\n\nmodule.exports = function inspect_(obj, options, depth, seen) {\n var opts = options || {};\n\n if (has(opts, 'quoteStyle') && (opts.quoteStyle !== 'single' && opts.quoteStyle !== 'double')) {\n throw new TypeError('option \"quoteStyle\" must be \"single\" or \"double\"');\n }\n if (\n has(opts, 'maxStringLength') && (typeof opts.maxStringLength === 'number'\n ? opts.maxStringLength < 0 && opts.maxStringLength !== Infinity\n : opts.maxStringLength !== null\n )\n ) {\n throw new TypeError('option \"maxStringLength\", if provided, must be a positive integer, Infinity, or `null`');\n }\n var customInspect = has(opts, 'customInspect') ? opts.customInspect : true;\n if (typeof customInspect !== 'boolean' && customInspect !== 'symbol') {\n throw new TypeError('option \"customInspect\", if provided, must be `true`, `false`, or `\\'symbol\\'`');\n }\n\n if (\n has(opts, 'indent')\n && opts.indent !== null\n && opts.indent !== '\\t'\n && !(parseInt(opts.indent, 10) === opts.indent && opts.indent > 0)\n ) {\n throw new TypeError('options \"indent\" must be \"\\\\t\", an integer > 0, or `null`');\n }\n\n if (typeof obj === 'undefined') {\n return 'undefined';\n }\n if (obj === null) {\n return 'null';\n }\n if (typeof obj === 'boolean') {\n return obj ? 'true' : 'false';\n }\n\n if (typeof obj === 'string') {\n return inspectString(obj, opts);\n }\n if (typeof obj === 'number') {\n if (obj === 0) {\n return Infinity / obj > 0 ? '0' : '-0';\n }\n return String(obj);\n }\n if (typeof obj === 'bigint') {\n return String(obj) + 'n';\n }\n\n var maxDepth = typeof opts.depth === 'undefined' ? 5 : opts.depth;\n if (typeof depth === 'undefined') { depth = 0; }\n if (depth >= maxDepth && maxDepth > 0 && typeof obj === 'object') {\n return isArray(obj) ? '[Array]' : '[Object]';\n }\n\n var indent = getIndent(opts, depth);\n\n if (typeof seen === 'undefined') {\n seen = [];\n } else if (indexOf(seen, obj) >= 0) {\n return '[Circular]';\n }\n\n function inspect(value, from, noIndent) {\n if (from) {\n seen = seen.slice();\n seen.push(from);\n }\n if (noIndent) {\n var newOpts = {\n depth: opts.depth\n };\n if (has(opts, 'quoteStyle')) {\n newOpts.quoteStyle = opts.quoteStyle;\n }\n return inspect_(value, newOpts, depth + 1, seen);\n }\n return inspect_(value, opts, depth + 1, seen);\n }\n\n if (typeof obj === 'function') {\n var name = nameOf(obj);\n var keys = arrObjKeys(obj, inspect);\n return '[Function' + (name ? ': ' + name : ' (anonymous)') + ']' + (keys.length > 0 ? ' { ' + keys.join(', ') + ' }' : '');\n }\n if (isSymbol(obj)) {\n var symString = hasShammedSymbols ? String(obj).replace(/^(Symbol\\(.*\\))_[^)]*$/, '$1') : symToString.call(obj);\n return typeof obj === 'object' && !hasShammedSymbols ? markBoxed(symString) : symString;\n }\n if (isElement(obj)) {\n var s = '<' + String(obj.nodeName).toLowerCase();\n var attrs = obj.attributes || [];\n for (var i = 0; i < attrs.length; i++) {\n s += ' ' + attrs[i].name + '=' + wrapQuotes(quote(attrs[i].value), 'double', opts);\n }\n s += '>';\n if (obj.childNodes && obj.childNodes.length) { s += '...'; }\n s += '</' + String(obj.nodeName).toLowerCase() + '>';\n return s;\n }\n if (isArray(obj)) {\n if (obj.length === 0) { return '[]'; }\n var xs = arrObjKeys(obj, inspect);\n if (indent && !singleLineValues(xs)) {\n return '[' + indentedJoin(xs, indent) + ']';\n }\n return '[ ' + xs.join(', ') + ' ]';\n }\n if (isError(obj)) {\n var parts = arrObjKeys(obj, inspect);\n if (parts.length === 0) { return '[' + String(obj) + ']'; }\n return '{ [' + String(obj) + '] ' + parts.join(', ') + ' }';\n }\n if (typeof obj === 'object' && customInspect) {\n if (inspectSymbol && typeof obj[inspectSymbol] === 'function') {\n return obj[inspectSymbol]();\n } else if (customInspect !== 'symbol' && typeof obj.inspect === 'function') {\n return obj.inspect();\n }\n }\n if (isMap(obj)) {\n var mapParts = [];\n mapForEach.call(obj, function (value, key) {\n mapParts.push(inspect(key, obj, true) + ' => ' + inspect(value, obj));\n });\n return collectionOf('Map', mapSize.call(obj), mapParts, indent);\n }\n if (isSet(obj)) {\n var setParts = [];\n setForEach.call(obj, function (value) {\n setParts.push(inspect(value, obj));\n });\n return collectionOf('Set', setSize.call(obj), setParts, indent);\n }\n if (isWeakMap(obj)) {\n return weakCollectionOf('WeakMap');\n }\n if (isWeakSet(obj)) {\n return weakCollectionOf('WeakSet');\n }\n if (isWeakRef(obj)) {\n return weakCollectionOf('WeakRef');\n }\n if (isNumber(obj)) {\n return markBoxed(inspect(Number(obj)));\n }\n if (isBigInt(obj)) {\n return markBoxed(inspect(bigIntValueOf.call(obj)));\n }\n if (isBoolean(obj)) {\n return markBoxed(booleanValueOf.call(obj));\n }\n if (isString(obj)) {\n return markBoxed(inspect(String(obj)));\n }\n if (!isDate(obj) && !isRegExp(obj)) {\n var ys = arrObjKeys(obj, inspect);\n var isPlainObject = gPO ? gPO(obj) === Object.prototype : obj instanceof Object || obj.constructor === Object;\n var protoTag = obj instanceof Object ? '' : 'null prototype';\n var stringTag = !isPlainObject && toStringTag && Object(obj) === obj && toStringTag in obj ? toStr(obj).slice(8, -1) : protoTag ? 'Object' : '';\n var constructorTag = isPlainObject || typeof obj.constructor !== 'function' ? '' : obj.constructor.name ? obj.constructor.name + ' ' : '';\n var tag = constructorTag + (stringTag || protoTag ? '[' + [].concat(stringTag || [], protoTag || []).join(': ') + '] ' : '');\n if (ys.length === 0) { return tag + '{}'; }\n if (indent) {\n return tag + '{' + indentedJoin(ys, indent) + '}';\n }\n return tag + '{ ' + ys.join(', ') + ' }';\n }\n return String(obj);\n};\n\nfunction wrapQuotes(s, defaultStyle, opts) {\n var quoteChar = (opts.quoteStyle || defaultStyle) === 'double' ? '\"' : \"'\";\n return quoteChar + s + quoteChar;\n}\n\nfunction quote(s) {\n return String(s).replace(/\"/g, '"');\n}\n\nfunction isArray(obj) { return toStr(obj) === '[object Array]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\nfunction isDate(obj) { return toStr(obj) === '[object Date]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\nfunction isRegExp(obj) { return toStr(obj) === '[object RegExp]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\nfunction isError(obj) { return toStr(obj) === '[object Error]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\nfunction isString(obj) { return toStr(obj) === '[object String]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\nfunction isNumber(obj) { return toStr(obj) === '[object Number]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\nfunction isBoolean(obj) { return toStr(obj) === '[object Boolean]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\n\n// Symbol and BigInt do have Symbol.toStringTag by spec, so that can't be used to eliminate false positives\nfunction isSymbol(obj) {\n if (hasShammedSymbols) {\n return obj && typeof obj === 'object' && obj instanceof Symbol;\n }\n if (typeof obj === 'symbol') {\n return true;\n }\n if (!obj || typeof obj !== 'object' || !symToString) {\n return false;\n }\n try {\n symToString.call(obj);\n return true;\n } catch (e) {}\n return false;\n}\n\nfunction isBigInt(obj) {\n if (!obj || typeof obj !== 'object' || !bigIntValueOf) {\n return false;\n }\n try {\n bigIntValueOf.call(obj);\n return true;\n } catch (e) {}\n return false;\n}\n\nvar hasOwn = Object.prototype.hasOwnProperty || function (key) { return key in this; };\nfunction has(obj, key) {\n return hasOwn.call(obj, key);\n}\n\nfunction toStr(obj) {\n return objectToString.call(obj);\n}\n\nfunction nameOf(f) {\n if (f.name) { return f.name; }\n var m = match.call(functionToString.call(f), /^function\\s*([\\w$]+)/);\n if (m) { return m[1]; }\n return null;\n}\n\nfunction indexOf(xs, x) {\n if (xs.indexOf) { return xs.indexOf(x); }\n for (var i = 0, l = xs.length; i < l; i++) {\n if (xs[i] === x) { return i; }\n }\n return -1;\n}\n\nfunction isMap(x) {\n if (!mapSize || !x || typeof x !== 'object') {\n return false;\n }\n try {\n mapSize.call(x);\n try {\n setSize.call(x);\n } catch (s) {\n return true;\n }\n return x instanceof Map; // core-js workaround, pre-v2.5.0\n } catch (e) {}\n return false;\n}\n\nfunction isWeakMap(x) {\n if (!weakMapHas || !x || typeof x !== 'object') {\n return false;\n }\n try {\n weakMapHas.call(x, weakMapHas);\n try {\n weakSetHas.call(x, weakSetHas);\n } catch (s) {\n return true;\n }\n return x instanceof WeakMap; // core-js workaround, pre-v2.5.0\n } catch (e) {}\n return false;\n}\n\nfunction isWeakRef(x) {\n if (!weakRefDeref || !x || typeof x !== 'object') {\n return false;\n }\n try {\n weakRefDeref.call(x);\n return true;\n } catch (e) {}\n return false;\n}\n\nfunction isSet(x) {\n if (!setSize || !x || typeof x !== 'object') {\n return false;\n }\n try {\n setSize.call(x);\n try {\n mapSize.call(x);\n } catch (m) {\n return true;\n }\n return x instanceof Set; // core-js workaround, pre-v2.5.0\n } catch (e) {}\n return false;\n}\n\nfunction isWeakSet(x) {\n if (!weakSetHas || !x || typeof x !== 'object') {\n return false;\n }\n try {\n weakSetHas.call(x, weakSetHas);\n try {\n weakMapHas.call(x, weakMapHas);\n } catch (s) {\n return true;\n }\n return x instanceof WeakSet; // core-js workaround, pre-v2.5.0\n } catch (e) {}\n return false;\n}\n\nfunction isElement(x) {\n if (!x || typeof x !== 'object') { return false; }\n if (typeof HTMLElement !== 'undefined' && x instanceof HTMLElement) {\n return true;\n }\n return typeof x.nodeName === 'string' && typeof x.getAttribute === 'function';\n}\n\nfunction inspectString(str, opts) {\n if (str.length > opts.maxStringLength) {\n var remaining = str.length - opts.maxStringLength;\n var trailer = '... ' + remaining + ' more character' + (remaining > 1 ? 's' : '');\n return inspectString(str.slice(0, opts.maxStringLength), opts) + trailer;\n }\n // eslint-disable-next-line no-control-regex\n var s = str.replace(/(['\\\\])/g, '\\\\$1').replace(/[\\x00-\\x1f]/g, lowbyte);\n return wrapQuotes(s, 'single', opts);\n}\n\nfunction lowbyte(c) {\n var n = c.charCodeAt(0);\n var x = {\n 8: 'b',\n 9: 't',\n 10: 'n',\n 12: 'f',\n 13: 'r'\n }[n];\n if (x) { return '\\\\' + x; }\n return '\\\\x' + (n < 0x10 ? '0' : '') + n.toString(16).toUpperCase();\n}\n\nfunction markBoxed(str) {\n return 'Object(' + str + ')';\n}\n\nfunction weakCollectionOf(type) {\n return type + ' { ? }';\n}\n\nfunction collectionOf(type, size, entries, indent) {\n var joinedEntries = indent ? indentedJoin(entries, indent) : entries.join(', ');\n return type + ' (' + size + ') {' + joinedEntries + '}';\n}\n\nfunction singleLineValues(xs) {\n for (var i = 0; i < xs.length; i++) {\n if (indexOf(xs[i], '\\n') >= 0) {\n return false;\n }\n }\n return true;\n}\n\nfunction getIndent(opts, depth) {\n var baseIndent;\n if (opts.indent === '\\t') {\n baseIndent = '\\t';\n } else if (typeof opts.indent === 'number' && opts.indent > 0) {\n baseIndent = Array(opts.indent + 1).join(' ');\n } else {\n return null;\n }\n return {\n base: baseIndent,\n prev: Array(depth + 1).join(baseIndent)\n };\n}\n\nfunction indentedJoin(xs, indent) {\n if (xs.length === 0) { return ''; }\n var lineJoiner = '\\n' + indent.prev + indent.base;\n return lineJoiner + xs.join(',' + lineJoiner) + '\\n' + indent.prev;\n}\n\nfunction arrObjKeys(obj, inspect) {\n var isArr = isArray(obj);\n var xs = [];\n if (isArr) {\n xs.length = obj.length;\n for (var i = 0; i < obj.length; i++) {\n xs[i] = has(obj, i) ? inspect(obj[i], obj) : '';\n }\n }\n var syms = typeof gOPS === 'function' ? gOPS(obj) : [];\n var symMap;\n if (hasShammedSymbols) {\n symMap = {};\n for (var k = 0; k < syms.length; k++) {\n symMap['$' + syms[k]] = syms[k];\n }\n }\n\n for (var key in obj) { // eslint-disable-line no-restricted-syntax\n if (!has(obj, key)) { continue; } // eslint-disable-line no-restricted-syntax, no-continue\n if (isArr && String(Number(key)) === key && key < obj.length) { continue; } // eslint-disable-line no-restricted-syntax, no-continue\n if (hasShammedSymbols && symMap['$' + key] instanceof Symbol) {\n // this is to prevent shammed Symbols, which are stored as strings, from being included in the string key section\n continue; // eslint-disable-line no-restricted-syntax, no-continue\n } else if ((/[^\\w$]/).test(key)) {\n xs.push(inspect(key, obj) + ': ' + inspect(obj[key], obj));\n } else {\n xs.push(key + ': ' + inspect(obj[key], obj));\n }\n }\n if (typeof gOPS === 'function') {\n for (var j = 0; j < syms.length; j++) {\n if (isEnumerable.call(obj, syms[j])) {\n xs.push('[' + inspect(syms[j]) + ']: ' + inspect(obj[syms[j]], obj));\n }\n }\n }\n return xs;\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,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Q1Y7QUFDQTtBQUNBO0FBQ0E7QUFDQSx3QkFBd0IsaUJBQWlCO0FBQ3pDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9vYmplY3QtaW5zcGVjdC9pbmRleC5qcz8yNzE0Il0sInNvdXJjZXNDb250ZW50IjpbInZhciBoYXNNYXAgPSB0eXBlb2YgTWFwID09PSAnZnVuY3Rpb24nICYmIE1hcC5wcm90b3R5cGU7XG52YXIgbWFwU2l6ZURlc2NyaXB0b3IgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yICYmIGhhc01hcCA/IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3IoTWFwLnByb3RvdHlwZSwgJ3NpemUnKSA6IG51bGw7XG52YXIgbWFwU2l6ZSA9IGhhc01hcCAmJiBtYXBTaXplRGVzY3JpcHRvciAmJiB0eXBlb2YgbWFwU2l6ZURlc2NyaXB0b3IuZ2V0ID09PSAnZnVuY3Rpb24nID8gbWFwU2l6ZURlc2NyaXB0b3IuZ2V0IDogbnVsbDtcbnZhciBtYXBGb3JFYWNoID0gaGFzTWFwICYmIE1hcC5wcm90b3R5cGUuZm9yRWFjaDtcbnZhciBoYXNTZXQgPSB0eXBlb2YgU2V0ID09PSAnZnVuY3Rpb24nICYmIFNldC5wcm90b3R5cGU7XG52YXIgc2V0U2l6ZURlc2NyaXB0b3IgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yICYmIGhhc1NldCA/IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3IoU2V0LnByb3RvdHlwZSwgJ3NpemUnKSA6IG51bGw7XG52YXIgc2V0U2l6ZSA9IGhhc1NldCAmJiBzZXRTaXplRGVzY3JpcHRvciAmJiB0eXBlb2Ygc2V0U2l6ZURlc2NyaXB0b3IuZ2V0ID09PSAnZnVuY3Rpb24nID8gc2V0U2l6ZURlc2NyaXB0b3IuZ2V0IDogbnVsbDtcbnZhciBzZXRGb3JFYWNoID0gaGFzU2V0ICYmIFNldC5wcm90b3R5cGUuZm9yRWFjaDtcbnZhciBoYXNXZWFrTWFwID0gdHlwZW9mIFdlYWtNYXAgPT09ICdmdW5jdGlvbicgJiYgV2Vha01hcC5wcm90b3R5cGU7XG52YXIgd2Vha01hcEhhcyA9IGhhc1dlYWtNYXAgPyBXZWFrTWFwLnByb3RvdHlwZS5oYXMgOiBudWxsO1xudmFyIGhhc1dlYWtTZXQgPSB0eXBlb2YgV2Vha1NldCA9PT0gJ2Z1bmN0aW9uJyAmJiBXZWFrU2V0LnByb3RvdHlwZTtcbnZhciB3ZWFrU2V0SGFzID0gaGFzV2Vha1NldCA/IFdlYWtTZXQucHJvdG90eXBlLmhhcyA6IG51bGw7XG52YXIgaGFzV2Vha1JlZiA9IHR5cGVvZiBXZWFrUmVmID09PSAnZnVuY3Rpb24nICYmIFdlYWtSZWYucHJvdG90eXBlO1xudmFyIHdlYWtSZWZEZXJlZiA9IGhhc1dlYWtSZWYgPyBXZWFrUmVmLnByb3RvdHlwZS5kZXJlZiA6IG51bGw7XG52YXIgYm9vbGVhblZhbHVlT2YgPSBCb29sZWFuLnByb3RvdHlwZS52YWx1ZU9mO1xudmFyIG9iamVjdFRvU3RyaW5nID0gT2JqZWN0LnByb3RvdHlwZS50b1N0cmluZztcbnZhciBmdW5jdGlvblRvU3RyaW5nID0gRnVuY3Rpb24ucHJvdG90eXBlLnRvU3RyaW5nO1xudmFyIG1hdGNoID0gU3RyaW5nLnByb3RvdHlwZS5tYXRjaDtcbnZhciBiaWdJbnRWYWx1ZU9mID0gdHlwZW9mIEJpZ0ludCA9PT0gJ2Z1bmN0aW9uJyA/IEJpZ0ludC5wcm90b3R5cGUudmFsdWVPZiA6IG51bGw7XG52YXIgZ09QUyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eVN5bWJvbHM7XG52YXIgc3ltVG9TdHJpbmcgPSB0eXBlb2YgU3ltYm9sID09PSAnZnVuY3Rpb24nICYmIHR5cGVvZiBTeW1ib2wuaXRlcmF0b3IgPT09ICdzeW1ib2wnID8gU3ltYm9sLnByb3RvdHlwZS50b1N0cmluZyA6IG51bGw7XG52YXIgaGFzU2hhbW1lZFN5bWJvbHMgPSB0eXBlb2YgU3ltYm9sID09PSAnZnVuY3Rpb24nICYmIHR5cGVvZiBTeW1ib2wuaXRlcmF0b3IgPT09ICdvYmplY3QnO1xudmFyIGlzRW51bWVyYWJsZSA9IE9iamVjdC5wcm90b3R5cGUucHJvcGVydHlJc0VudW1lcmFibGU7XG5cbnZhciBnUE8gPSAodHlwZW9mIFJlZmxlY3QgPT09ICdmdW5jdGlvbicgPyBSZWZsZWN0LmdldFByb3RvdHlwZU9mIDogT2JqZWN0LmdldFByb3RvdHlwZU9mKSB8fCAoXG4gICAgW10uX19wcm90b19fID09PSBBcnJheS5wcm90b3R5cGUgLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1wcm90b1xuICAgICAgICA/IGZ1bmN0aW9uIChPKSB7XG4gICAgICAgICAgICByZXR1cm4gTy5fX3Byb3RvX187IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tcHJvdG9cbiAgICAgICAgfVxuICAgICAgICA6IG51bGxcbik7XG5cbnZhciBpbnNwZWN0Q3VzdG9tID0gcmVxdWlyZSgnLi91dGlsLmluc3BlY3QnKS5jdXN0b207XG52YXIgaW5zcGVjdFN5bWJvbCA9IGluc3BlY3RDdXN0b20gJiYgaXNTeW1ib2woaW5zcGVjdEN1c3RvbSkgPyBpbnNwZWN0Q3VzdG9tIDogbnVsbDtcbnZhciB0b1N0cmluZ1RhZyA9IHR5cGVvZiBTeW1ib2wgPT09ICdmdW5jdGlvbicgJiYgdHlwZW9mIFN5bWJvbC50b1N0cmluZ1RhZyAhPT0gJ3VuZGVmaW5lZCcgPyBTeW1ib2wudG9TdHJpbmdUYWcgOiBudWxsO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIGluc3BlY3RfKG9iaiwgb3B0aW9ucywgZGVwdGgsIHNlZW4pIHtcbiAgICB2YXIgb3B0cyA9IG9wdGlvbnMgfHwge307XG5cbiAgICBpZiAoaGFzKG9wdHMsICdxdW90ZVN0eWxlJykgJiYgKG9wdHMucXVvdGVTdHlsZSAhPT0gJ3NpbmdsZScgJiYgb3B0cy5xdW90ZVN0eWxlICE9PSAnZG91YmxlJykpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignb3B0aW9uIFwicXVvdGVTdHlsZVwiIG11c3QgYmUgXCJzaW5nbGVcIiBvciBcImRvdWJsZVwiJyk7XG4gICAgfVxuICAgIGlmIChcbiAgICAgICAgaGFzKG9wdHMsICdtYXhTdHJpbmdMZW5ndGgnKSAmJiAodHlwZW9mIG9wdHMubWF4U3RyaW5nTGVuZ3RoID09PSAnbnVtYmVyJ1xuICAgICAgICAgICAgPyBvcHRzLm1heFN0cmluZ0xlbmd0aCA8IDAgJiYgb3B0cy5tYXhTdHJpbmdMZW5ndGggIT09IEluZmluaXR5XG4gICAgICAgICAgICA6IG9wdHMubWF4U3RyaW5nTGVuZ3RoICE9PSBudWxsXG4gICAgICAgIClcbiAgICApIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignb3B0aW9uIFwibWF4U3RyaW5nTGVuZ3RoXCIsIGlmIHByb3ZpZGVkLCBtdXN0IGJlIGEgcG9zaXRpdmUgaW50ZWdlciwgSW5maW5pdHksIG9yIGBudWxsYCcpO1xuICAgIH1cbiAgICB2YXIgY3VzdG9tSW5zcGVjdCA9IGhhcyhvcHRzLCAnY3VzdG9tSW5zcGVjdCcpID8gb3B0cy5jdXN0b21JbnNwZWN0IDogdHJ1ZTtcbiAgICBpZiAodHlwZW9mIGN1c3RvbUluc3BlY3QgIT09ICdib29sZWFuJyAmJiBjdXN0b21JbnNwZWN0ICE9PSAnc3ltYm9sJykge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdvcHRpb24gXCJjdXN0b21JbnNwZWN0XCIsIGlmIHByb3ZpZGVkLCBtdXN0IGJlIGB0cnVlYCwgYGZhbHNlYCwgb3IgYFxcJ3N5bWJvbFxcJ2AnKTtcbiAgICB9XG5cbiAgICBpZiAoXG4gICAgICAgIGhhcyhvcHRzLCAnaW5kZW50JylcbiAgICAgICAgJiYgb3B0cy5pbmRlbnQgIT09IG51bGxcbiAgICAgICAgJiYgb3B0cy5pbmRlbnQgIT09ICdcXHQnXG4gICAgICAgICYmICEocGFyc2VJbnQob3B0cy5pbmRlbnQsIDEwKSA9PT0gb3B0cy5pbmRlbnQgJiYgb3B0cy5pbmRlbnQgPiAwKVxuICAgICkge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdvcHRpb25zIFwiaW5kZW50XCIgbXVzdCBiZSBcIlxcXFx0XCIsIGFuIGludGVnZXIgPiAwLCBvciBgbnVsbGAnKTtcbiAgICB9XG5cbiAgICBpZiAodHlwZW9mIG9iaiA9PT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgICAgcmV0dXJuICd1bmRlZmluZWQnO1xuICAgIH1cbiAgICBpZiAob2JqID09PSBudWxsKSB7XG4gICAgICAgIHJldHVybiAnbnVsbCc7XG4gICAgfVxuICAgIGlmICh0eXBlb2Ygb2JqID09PSAnYm9vbGVhbicpIHtcbiAgICAgICAgcmV0dXJuIG9iaiA/ICd0cnVlJyA6ICdmYWxzZSc7XG4gICAgfVxuXG4gICAgaWYgKHR5cGVvZiBvYmogPT09ICdzdHJpbmcnKSB7XG4gICAgICAgIHJldHVybiBpbnNwZWN0U3RyaW5nKG9iaiwgb3B0cyk7XG4gICAgfVxuICAgIGlmICh0eXBlb2Ygb2JqID09PSAnbnVtYmVyJykge1xuICAgICAgICBpZiAob2JqID09PSAwKSB7XG4gICAgICAgICAgICByZXR1cm4gSW5maW5pdHkgLyBvYmogPiAwID8gJzAnIDogJy0wJztcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gU3RyaW5nKG9iaik7XG4gICAgfVxuICAgIGlmICh0eXBlb2Ygb2JqID09PSAnYmlnaW50Jykge1xuICAgICAgICByZXR1cm4gU3RyaW5nKG9iaikgKyAnbic7XG4gICAgfVxuXG4gICAgdmFyIG1heERlcHRoID0gdHlwZW9mIG9wdHMuZGVwdGggPT09ICd1bmRlZmluZWQnID8gNSA6IG9wdHMuZGVwdGg7XG4gICAgaWYgKHR5cGVvZiBkZXB0aCA9PT0gJ3VuZGVmaW5lZCcpIHsgZGVwdGggPSAwOyB9XG4gICAgaWYgKGRlcHRoID49IG1heERlcHRoICYmIG1heERlcHRoID4gMCAmJiB0eXBlb2Ygb2JqID09PSAnb2JqZWN0Jykge1xuICAgICAgICByZXR1cm4gaXNBcnJheShvYmopID8gJ1tBcnJheV0nIDogJ1tPYmplY3RdJztcbiAgICB9XG5cbiAgICB2YXIgaW5kZW50ID0gZ2V0SW5kZW50KG9wdHMsIGRlcHRoKTtcblxuICAgIGlmICh0eXBlb2Ygc2VlbiA9PT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgICAgc2VlbiA9IFtdO1xuICAgIH0gZWxzZSBpZiAoaW5kZXhPZihzZWVuLCBvYmopID49IDApIHtcbiAgICAgICAgcmV0dXJuICdbQ2lyY3VsYXJdJztcbiAgICB9XG5cbiAgICBmdW5jdGlvbiBpbnNwZWN0KHZhbHVlLCBmcm9tLCBub0luZGVudCkge1xuICAgICAgICBpZiAoZnJvbSkge1xuICAgICAgICAgICAgc2VlbiA9IHNlZW4uc2xpY2UoKTtcbiAgICAgICAgICAgIHNlZW4ucHVzaChmcm9tKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAobm9JbmRlbnQpIHtcbiAgICAgICAgICAgIHZhciBuZXdPcHRzID0ge1xuICAgICAgICAgICAgICAgIGRlcHRoOiBvcHRzLmRlcHRoXG4gICAgICAgICAgICB9O1xuICAgICAgICAgICAgaWYgKGhhcyhvcHRzLCAncXVvdGVTdHlsZScpKSB7XG4gICAgICAgICAgICAgICAgbmV3T3B0cy5xdW90ZVN0eWxlID0gb3B0cy5xdW90ZVN0eWxlO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgcmV0dXJuIGluc3BlY3RfKHZhbHVlLCBuZXdPcHRzLCBkZXB0aCArIDEsIHNlZW4pO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBpbnNwZWN0Xyh2YWx1ZSwgb3B0cywgZGVwdGggKyAxLCBzZWVuKTtcbiAgICB9XG5cbiAgICBpZiAodHlwZW9mIG9iaiA9PT0gJ2Z1bmN0aW9uJykge1xuICAgICAgICB2YXIgbmFtZSA9IG5hbWVPZihvYmopO1xuICAgICAgICB2YXIga2V5cyA9IGFyck9iaktleXMob2JqLCBpbnNwZWN0KTtcbiAgICAgICAgcmV0dXJuICdbRnVuY3Rpb24nICsgKG5hbWUgPyAnOiAnICsgbmFtZSA6ICcgKGFub255bW91cyknKSArICddJyArIChrZXlzLmxlbmd0aCA+IDAgPyAnIHsgJyArIGtleXMuam9pbignLCAnKSArICcgfScgOiAnJyk7XG4gICAgfVxuICAgIGlmIChpc1N5bWJvbChvYmopKSB7XG4gICAgICAgIHZhciBzeW1TdHJpbmcgPSBoYXNTaGFtbWVkU3ltYm9scyA/IFN0cmluZyhvYmopLnJlcGxhY2UoL14oU3ltYm9sXFwoLipcXCkpX1teKV0qJC8sICckMScpIDogc3ltVG9TdHJpbmcuY2FsbChvYmopO1xuICAgICAgICByZXR1cm4gdHlwZW9mIG9iaiA9PT0gJ29iamVjdCcgJiYgIWhhc1NoYW1tZWRTeW1ib2xzID8gbWFya0JveGVkKHN5bVN0cmluZykgOiBzeW1TdHJpbmc7XG4gICAgfVxuICAgIGlmIChpc0VsZW1lbnQob2JqKSkge1xuICAgICAgICB2YXIgcyA9ICc8JyArIFN0cmluZyhvYmoubm9kZU5hbWUpLnRvTG93ZXJDYXNlKCk7XG4gICAgICAgIHZhciBhdHRycyA9IG9iai5hdHRyaWJ1dGVzIHx8IFtdO1xuICAgICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGF0dHJzLmxlbmd0aDsgaSsrKSB7XG4gICAgICAgICAgICBzICs9ICcgJyArIGF0dHJzW2ldLm5hbWUgKyAnPScgKyB3cmFwUXVvdGVzKHF1b3RlKGF0dHJzW2ldLnZhbHVlKSwgJ2RvdWJsZScsIG9wdHMpO1xuICAgICAgICB9XG4gICAgICAgIHMgKz0gJz4nO1xuICAgICAgICBpZiAob2JqLmNoaWxkTm9kZXMgJiYgb2JqLmNoaWxkTm9kZXMubGVuZ3RoKSB7IHMgKz0gJy4uLic7IH1cbiAgICAgICAgcyArPSAnPC8nICsgU3RyaW5nKG9iai5ub2RlTmFtZSkudG9Mb3dlckNhc2UoKSArICc+JztcbiAgICAgICAgcmV0dXJuIHM7XG4gICAgfVxuICAgIGlmIChpc0FycmF5KG9iaikpIHtcbiAgICAgICAgaWYgKG9iai5sZW5ndGggPT09IDApIHsgcmV0dXJuICdbXSc7IH1cbiAgICAgICAgdmFyIHhzID0gYXJyT2JqS2V5cyhvYmosIGluc3BlY3QpO1xuICAgICAgICBpZiAoaW5kZW50ICYmICFzaW5nbGVMaW5lVmFsdWVzKHhzKSkge1xuICAgICAgICAgICAgcmV0dXJuICdbJyArIGluZGVudGVkSm9pbih4cywgaW5kZW50KSArICddJztcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gJ1sgJyArIHhzLmpvaW4oJywgJykgKyAnIF0nO1xuICAgIH1cbiAgICBpZiAoaXNFcnJvcihvYmopKSB7XG4gICAgICAgIHZhciBwYXJ0cyA9IGFyck9iaktleXMob2JqLCBpbnNwZWN0KTtcbiAgICAgICAgaWYgKHBhcnRzLmxlbmd0aCA9PT0gMCkgeyByZXR1cm4gJ1snICsgU3RyaW5nKG9iaikgKyAnXSc7IH1cbiAgICAgICAgcmV0dXJuICd7IFsnICsgU3RyaW5nKG9iaikgKyAnXSAnICsgcGFydHMuam9pbignLCAnKSArICcgfSc7XG4gICAgfVxuICAgIGlmICh0eXBlb2Ygb2JqID09PSAnb2JqZWN0JyAmJiBjdXN0b21JbnNwZWN0KSB7XG4gICAgICAgIGlmIChpbnNwZWN0U3ltYm9sICYmIHR5cGVvZiBvYmpbaW5zcGVjdFN5bWJvbF0gPT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgICAgIHJldHVybiBvYmpbaW5zcGVjdFN5bWJvbF0oKTtcbiAgICAgICAgfSBlbHNlIGlmIChjdXN0b21JbnNwZWN0ICE9PSAnc3ltYm9sJyAmJiB0eXBlb2Ygb2JqLmluc3BlY3QgPT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgICAgIHJldHVybiBvYmouaW5zcGVjdCgpO1xuICAgICAgICB9XG4gICAgfVxuICAgIGlmIChpc01hcChvYmopKSB7XG4gICAgICAgIHZhciBtYXBQYXJ0cyA9IFtdO1xuICAgICAgICBtYXBGb3JFYWNoLmNhbGwob2JqLCBmdW5jdGlvbiAodmFsdWUsIGtleSkge1xuICAgICAgICAgICAgbWFwUGFydHMucHVzaChpbnNwZWN0KGtleSwgb2JqLCB0cnVlKSArICcgPT4gJyArIGluc3BlY3QodmFsdWUsIG9iaikpO1xuICAgICAgICB9KTtcbiAgICAgICAgcmV0dXJuIGNvbGxlY3Rpb25PZignTWFwJywgbWFwU2l6ZS5jYWxsKG9iaiksIG1hcFBhcnRzLCBpbmRlbnQpO1xuICAgIH1cbiAgICBpZiAoaXNTZXQob2JqKSkge1xuICAgICAgICB2YXIgc2V0UGFydHMgPSBbXTtcbiAgICAgICAgc2V0Rm9yRWFjaC5jYWxsKG9iaiwgZnVuY3Rpb24gKHZhbHVlKSB7XG4gICAgICAgICAgICBzZXRQYXJ0cy5wdXNoKGluc3BlY3QodmFsdWUsIG9iaikpO1xuICAgICAgICB9KTtcbiAgICAgICAgcmV0dXJuIGNvbGxlY3Rpb25PZignU2V0Jywgc2V0U2l6ZS5jYWxsKG9iaiksIHNldFBhcnRzLCBpbmRlbnQpO1xuICAgIH1cbiAgICBpZiAoaXNXZWFrTWFwKG9iaikpIHtcbiAgICAgICAgcmV0dXJuIHdlYWtDb2xsZWN0aW9uT2YoJ1dlYWtNYXAnKTtcbiAgICB9XG4gICAgaWYgKGlzV2Vha1NldChvYmopKSB7XG4gICAgICAgIHJldHVybiB3ZWFrQ29sbGVjdGlvbk9mKCdXZWFrU2V0Jyk7XG4gICAgfVxuICAgIGlmIChpc1dlYWtSZWYob2JqKSkge1xuICAgICAgICByZXR1cm4gd2Vha0NvbGxlY3Rpb25PZignV2Vha1JlZicpO1xuICAgIH1cbiAgICBpZiAoaXNOdW1iZXIob2JqKSkge1xuICAgICAgICByZXR1cm4gbWFya0JveGVkKGluc3BlY3QoTnVtYmVyKG9iaikpKTtcbiAgICB9XG4gICAgaWYgKGlzQmlnSW50KG9iaikpIHtcbiAgICAgICAgcmV0dXJuIG1hcmtCb3hlZChpbnNwZWN0KGJpZ0ludFZhbHVlT2YuY2FsbChvYmopKSk7XG4gICAgfVxuICAgIGlmIChpc0Jvb2xlYW4ob2JqKSkge1xuICAgICAgICByZXR1cm4gbWFya0JveGVkKGJvb2xlYW5WYWx1ZU9mLmNhbGwob2JqKSk7XG4gICAgfVxuICAgIGlmIChpc1N0cmluZyhvYmopKSB7XG4gICAgICAgIHJldHVybiBtYXJrQm94ZWQoaW5zcGVjdChTdHJpbmcob2JqKSkpO1xuICAgIH1cbiAgICBpZiAoIWlzRGF0ZShvYmopICYmICFpc1JlZ0V4cChvYmopKSB7XG4gICAgICAgIHZhciB5cyA9IGFyck9iaktleXMob2JqLCBpbnNwZWN0KTtcbiAgICAgICAgdmFyIGlzUGxhaW5PYmplY3QgPSBnUE8gPyBnUE8ob2JqKSA9PT0gT2JqZWN0LnByb3RvdHlwZSA6IG9iaiBpbnN0YW5jZW9mIE9iamVjdCB8fCBvYmouY29uc3RydWN0b3IgPT09IE9iamVjdDtcbiAgICAgICAgdmFyIHByb3RvVGFnID0gb2JqIGluc3RhbmNlb2YgT2JqZWN0ID8gJycgOiAnbnVsbCBwcm90b3R5cGUnO1xuICAgICAgICB2YXIgc3RyaW5nVGFnID0gIWlzUGxhaW5PYmplY3QgJiYgdG9TdHJpbmdUYWcgJiYgT2JqZWN0KG9iaikgPT09IG9iaiAmJiB0b1N0cmluZ1RhZyBpbiBvYmogPyB0b1N0cihvYmopLnNsaWNlKDgsIC0xKSA6IHByb3RvVGFnID8gJ09iamVjdCcgOiAnJztcbiAgICAgICAgdmFyIGNvbnN0cnVjdG9yVGFnID0gaXNQbGFpbk9iamVjdCB8fCB0eXBlb2Ygb2JqLmNvbnN0cnVjdG9yICE9PSAnZnVuY3Rpb24nID8gJycgOiBvYmouY29uc3RydWN0b3IubmFtZSA/IG9iai5jb25zdHJ1Y3Rvci5uYW1lICsgJyAnIDogJyc7XG4gICAgICAgIHZhciB0YWcgPSBjb25zdHJ1Y3RvclRhZyArIChzdHJpbmdUYWcgfHwgcHJvdG9UYWcgPyAnWycgKyBbXS5jb25jYXQoc3RyaW5nVGFnIHx8IFtdLCBwcm90b1RhZyB8fCBbXSkuam9pbignOiAnKSArICddICcgOiAnJyk7XG4gICAgICAgIGlmICh5cy5sZW5ndGggPT09IDApIHsgcmV0dXJuIHRhZyArICd7fSc7IH1cbiAgICAgICAgaWYgKGluZGVudCkge1xuICAgICAgICAgICAgcmV0dXJuIHRhZyArICd7JyArIGluZGVudGVkSm9pbih5cywgaW5kZW50KSArICd9JztcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gdGFnICsgJ3sgJyArIHlzLmpvaW4oJywgJykgKyAnIH0nO1xuICAgIH1cbiAgICByZXR1cm4gU3RyaW5nKG9iaik7XG59O1xuXG5mdW5jdGlvbiB3cmFwUXVvdGVzKHMsIGRlZmF1bHRTdHlsZSwgb3B0cykge1xuICAgIHZhciBxdW90ZUNoYXIgPSAob3B0cy5xdW90ZVN0eWxlIHx8IGRlZmF1bHRTdHlsZSkgPT09ICdkb3VibGUnID8gJ1wiJyA6IFwiJ1wiO1xuICAgIHJldHVybiBxdW90ZUNoYXIgKyBzICsgcXVvdGVDaGFyO1xufVxuXG5mdW5jdGlvbiBxdW90ZShzKSB7XG4gICAgcmV0dXJuIFN0cmluZyhzKS5yZXBsYWNlKC9cIi9nLCAnJnF1b3Q7Jyk7XG59XG5cbmZ1bmN0aW9uIGlzQXJyYXkob2JqKSB7IHJldHVybiB0b1N0cihvYmopID09PSAnW29iamVjdCBBcnJheV0nICYmICghdG9TdHJpbmdUYWcgfHwgISh0eXBlb2Ygb2JqID09PSAnb2JqZWN0JyAmJiB0b1N0cmluZ1RhZyBpbiBvYmopKTsgfVxuZnVuY3Rpb24gaXNEYXRlKG9iaikgeyByZXR1cm4gdG9TdHIob2JqKSA9PT0gJ1tvYmplY3QgRGF0ZV0nICYmICghdG9TdHJpbmdUYWcgfHwgISh0eXBlb2Ygb2JqID09PSAnb2JqZWN0JyAmJiB0b1N0cmluZ1RhZyBpbiBvYmopKTsgfVxuZnVuY3Rpb24gaXNSZWdFeHAob2JqKSB7IHJldHVybiB0b1N0cihvYmopID09PSAnW29iamVjdCBSZWdFeHBdJyAmJiAoIXRvU3RyaW5nVGFnIHx8ICEodHlwZW9mIG9iaiA9PT0gJ29iamVjdCcgJiYgdG9TdHJpbmdUYWcgaW4gb2JqKSk7IH1cbmZ1bmN0aW9uIGlzRXJyb3Iob2JqKSB7IHJldHVybiB0b1N0cihvYmopID09PSAnW29iamVjdCBFcnJvcl0nICYmICghdG9TdHJpbmdUYWcgfHwgISh0eXBlb2Ygb2JqID09PSAnb2JqZWN0JyAmJiB0b1N0cmluZ1RhZyBpbiBvYmopKTsgfVxuZnVuY3Rpb24gaXNTdHJpbmcob2JqKSB7IHJldHVybiB0b1N0cihvYmopID09PSAnW29iamVjdCBTdHJpbmddJyAmJiAoIXRvU3RyaW5nVGFnIHx8ICEodHlwZW9mIG9iaiA9PT0gJ29iamVjdCcgJiYgdG9TdHJpbmdUYWcgaW4gb2JqKSk7IH1cbmZ1bmN0aW9uIGlzTnVtYmVyKG9iaikgeyByZXR1cm4gdG9TdHIob2JqKSA9PT0gJ1tvYmplY3QgTnVtYmVyXScgJiYgKCF0b1N0cmluZ1RhZyB8fCAhKHR5cGVvZiBvYmogPT09ICdvYmplY3QnICYmIHRvU3RyaW5nVGFnIGluIG9iaikpOyB9XG5mdW5jdGlvbiBpc0Jvb2xlYW4ob2JqKSB7IHJldHVybiB0b1N0cihvYmopID09PSAnW29iamVjdCBCb29sZWFuXScgJiYgKCF0b1N0cmluZ1RhZyB8fCAhKHR5cGVvZiBvYmogPT09ICdvYmplY3QnICYmIHRvU3RyaW5nVGFnIGluIG9iaikpOyB9XG5cbi8vIFN5bWJvbCBhbmQgQmlnSW50IGRvIGhhdmUgU3ltYm9sLnRvU3RyaW5nVGFnIGJ5IHNwZWMsIHNvIHRoYXQgY2FuJ3QgYmUgdXNlZCB0byBlbGltaW5hdGUgZmFsc2UgcG9zaXRpdmVzXG5mdW5jdGlvbiBpc1N5bWJvbChvYmopIHtcbiAgICBpZiAoaGFzU2hhbW1lZFN5bWJvbHMpIHtcbiAgICAgICAgcmV0dXJuIG9iaiAmJiB0eXBlb2Ygb2JqID09PSAnb2JqZWN0JyAmJiBvYmogaW5zdGFuY2VvZiBTeW1ib2w7XG4gICAgfVxuICAgIGlmICh0eXBlb2Ygb2JqID09PSAnc3ltYm9sJykge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gICAgaWYgKCFvYmogfHwgdHlwZW9mIG9iaiAhPT0gJ29iamVjdCcgfHwgIXN5bVRvU3RyaW5nKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgdHJ5IHtcbiAgICAgICAgc3ltVG9TdHJpbmcuY2FsbChvYmopO1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9IGNhdGNoIChlKSB7fVxuICAgIHJldHVybiBmYWxzZTtcbn1cblxuZnVuY3Rpb24gaXNCaWdJbnQob2JqKSB7XG4gICAgaWYgKCFvYmogfHwgdHlwZW9mIG9iaiAhPT0gJ29iamVjdCcgfHwgIWJpZ0ludFZhbHVlT2YpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICB0cnkge1xuICAgICAgICBiaWdJbnRWYWx1ZU9mLmNhbGwob2JqKTtcbiAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgfSBjYXRjaCAoZSkge31cbiAgICByZXR1cm4gZmFsc2U7XG59XG5cbnZhciBoYXNPd24gPSBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5IHx8IGZ1bmN0aW9uIChrZXkpIHsgcmV0dXJuIGtleSBpbiB0aGlzOyB9O1xuZnVuY3Rpb24gaGFzKG9iaiwga2V5KSB7XG4gICAgcmV0dXJuIGhhc093bi5jYWxsKG9iaiwga2V5KTtcbn1cblxuZnVuY3Rpb24gdG9TdHIob2JqKSB7XG4gICAgcmV0dXJuIG9iamVjdFRvU3RyaW5nLmNhbGwob2JqKTtcbn1cblxuZnVuY3Rpb24gbmFtZU9mKGYpIHtcbiAgICBpZiAoZi5uYW1lKSB7IHJldHVybiBmLm5hbWU7IH1cbiAgICB2YXIgbSA9IG1hdGNoLmNhbGwoZnVuY3Rpb25Ub1N0cmluZy5jYWxsKGYpLCAvXmZ1bmN0aW9uXFxzKihbXFx3JF0rKS8pO1xuICAgIGlmIChtKSB7IHJldHVybiBtWzFdOyB9XG4gICAgcmV0dXJuIG51bGw7XG59XG5cbmZ1bmN0aW9uIGluZGV4T2YoeHMsIHgpIHtcbiAgICBpZiAoeHMuaW5kZXhPZikgeyByZXR1cm4geHMuaW5kZXhPZih4KTsgfVxuICAgIGZvciAodmFyIGkgPSAwLCBsID0geHMubGVuZ3RoOyBpIDwgbDsgaSsrKSB7XG4gICAgICAgIGlmICh4c1tpXSA9PT0geCkgeyByZXR1cm4gaTsgfVxuICAgIH1cbiAgICByZXR1cm4gLTE7XG59XG5cbmZ1bmN0aW9uIGlzTWFwKHgpIHtcbiAgICBpZiAoIW1hcFNpemUgfHwgIXggfHwgdHlwZW9mIHggIT09ICdvYmplY3QnKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgdHJ5IHtcbiAgICAgICAgbWFwU2l6ZS5jYWxsKHgpO1xuICAgICAgICB0cnkge1xuICAgICAgICAgICAgc2V0U2l6ZS5jYWxsKHgpO1xuICAgICAgICB9IGNhdGNoIChzKSB7XG4gICAgICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4geCBpbnN0YW5jZW9mIE1hcDsgLy8gY29yZS1qcyB3b3JrYXJvdW5kLCBwcmUtdjIuNS4wXG4gICAgfSBjYXRjaCAoZSkge31cbiAgICByZXR1cm4gZmFsc2U7XG59XG5cbmZ1bmN0aW9uIGlzV2Vha01hcCh4KSB7XG4gICAgaWYgKCF3ZWFrTWFwSGFzIHx8ICF4IHx8IHR5cGVvZiB4ICE9PSAnb2JqZWN0Jykge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHRyeSB7XG4gICAgICAgIHdlYWtNYXBIYXMuY2FsbCh4LCB3ZWFrTWFwSGFzKTtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIHdlYWtTZXRIYXMuY2FsbCh4LCB3ZWFrU2V0SGFzKTtcbiAgICAgICAgfSBjYXRjaCAocykge1xuICAgICAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIHggaW5zdGFuY2VvZiBXZWFrTWFwOyAvLyBjb3JlLWpzIHdvcmthcm91bmQsIHByZS12Mi41LjBcbiAgICB9IGNhdGNoIChlKSB7fVxuICAgIHJldHVybiBmYWxzZTtcbn1cblxuZnVuY3Rpb24gaXNXZWFrUmVmKHgpIHtcbiAgICBpZiAoIXdlYWtSZWZEZXJlZiB8fCAheCB8fCB0eXBlb2YgeCAhPT0gJ29iamVjdCcpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICB0cnkge1xuICAgICAgICB3ZWFrUmVmRGVyZWYuY2FsbCh4KTtcbiAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgfSBjYXRjaCAoZSkge31cbiAgICByZXR1cm4gZmFsc2U7XG59XG5cbmZ1bmN0aW9uIGlzU2V0KHgpIHtcbiAgICBpZiAoIXNldFNpemUgfHwgIXggfHwgdHlwZW9mIHggIT09ICdvYmplY3QnKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgdHJ5IHtcbiAgICAgICAgc2V0U2l6ZS5jYWxsKHgpO1xuICAgICAgICB0cnkge1xuICAgICAgICAgICAgbWFwU2l6ZS5jYWxsKHgpO1xuICAgICAgICB9IGNhdGNoIChtKSB7XG4gICAgICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4geCBpbnN0YW5jZW9mIFNldDsgLy8gY29yZS1qcyB3b3JrYXJvdW5kLCBwcmUtdjIuNS4wXG4gICAgfSBjYXRjaCAoZSkge31cbiAgICByZXR1cm4gZmFsc2U7XG59XG5cbmZ1bmN0aW9uIGlzV2Vha1NldCh4KSB7XG4gICAgaWYgKCF3ZWFrU2V0SGFzIHx8ICF4IHx8IHR5cGVvZiB4ICE9PSAnb2JqZWN0Jykge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHRyeSB7XG4gICAgICAgIHdlYWtTZXRIYXMuY2FsbCh4LCB3ZWFrU2V0SGFzKTtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIHdlYWtNYXBIYXMuY2FsbCh4LCB3ZWFrTWFwSGFzKTtcbiAgICAgICAgfSBjYXRjaCAocykge1xuICAgICAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIHggaW5zdGFuY2VvZiBXZWFrU2V0OyAvLyBjb3JlLWpzIHdvcmthcm91bmQsIHByZS12Mi41LjBcbiAgICB9IGNhdGNoIChlKSB7fVxuICAgIHJldHVybiBmYWxzZTtcbn1cblxuZnVuY3Rpb24gaXNFbGVtZW50KHgpIHtcbiAgICBpZiAoIXggfHwgdHlwZW9mIHggIT09ICdvYmplY3QnKSB7IHJldHVybiBmYWxzZTsgfVxuICAgIGlmICh0eXBlb2YgSFRNTEVsZW1lbnQgIT09ICd1bmRlZmluZWQnICYmIHggaW5zdGFuY2VvZiBIVE1MRWxlbWVudCkge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gICAgcmV0dXJuIHR5cGVvZiB4Lm5vZGVOYW1lID09PSAnc3RyaW5nJyAmJiB0eXBlb2YgeC5nZXRBdHRyaWJ1dGUgPT09ICdmdW5jdGlvbic7XG59XG5cbmZ1bmN0aW9uIGluc3BlY3RTdHJpbmcoc3RyLCBvcHRzKSB7XG4gICAgaWYgKHN0ci5sZW5ndGggPiBvcHRzLm1heFN0cmluZ0xlbmd0aCkge1xuICAgICAgICB2YXIgcmVtYWluaW5nID0gc3RyLmxlbmd0aCAtIG9wdHMubWF4U3RyaW5nTGVuZ3RoO1xuICAgICAgICB2YXIgdHJhaWxlciA9ICcuLi4gJyArIHJlbWFpbmluZyArICcgbW9yZSBjaGFyYWN0ZXInICsgKHJlbWFpbmluZyA+IDEgPyAncycgOiAnJyk7XG4gICAgICAgIHJldHVybiBpbnNwZWN0U3RyaW5nKHN0ci5zbGljZSgwLCBvcHRzLm1heFN0cmluZ0xlbmd0aCksIG9wdHMpICsgdHJhaWxlcjtcbiAgICB9XG4gICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLWNvbnRyb2wtcmVnZXhcbiAgICB2YXIgcyA9IHN0ci5yZXBsYWNlKC8oWydcXFxcXSkvZywgJ1xcXFwkMScpLnJlcGxhY2UoL1tcXHgwMC1cXHgxZl0vZywgbG93Ynl0ZSk7XG4gICAgcmV0dXJuIHdyYXBRdW90ZXMocywgJ3NpbmdsZScsIG9wdHMpO1xufVxuXG5mdW5jdGlvbiBsb3dieXRlKGMpIHtcbiAgICB2YXIgbiA9IGMuY2hhckNvZGVBdCgwKTtcbiAgICB2YXIgeCA9IHtcbiAgICAgICAgODogJ2InLFxuICAgICAgICA5OiAndCcsXG4gICAgICAgIDEwOiAnbicsXG4gICAgICAgIDEyOiAnZicsXG4gICAgICAgIDEzOiAncidcbiAgICB9W25dO1xuICAgIGlmICh4KSB7IHJldHVybiAnXFxcXCcgKyB4OyB9XG4gICAgcmV0dXJuICdcXFxceCcgKyAobiA8IDB4MTAgPyAnMCcgOiAnJykgKyBuLnRvU3RyaW5nKDE2KS50b1VwcGVyQ2FzZSgpO1xufVxuXG5mdW5jdGlvbiBtYXJrQm94ZWQoc3RyKSB7XG4gICAgcmV0dXJuICdPYmplY3QoJyArIHN0ciArICcpJztcbn1cblxuZnVuY3Rpb24gd2Vha0NvbGxlY3Rpb25PZih0eXBlKSB7XG4gICAgcmV0dXJuIHR5cGUgKyAnIHsgPyB9Jztcbn1cblxuZnVuY3Rpb24gY29sbGVjdGlvbk9mKHR5cGUsIHNpemUsIGVudHJpZXMsIGluZGVudCkge1xuICAgIHZhciBqb2luZWRFbnRyaWVzID0gaW5kZW50ID8gaW5kZW50ZWRKb2luKGVudHJpZXMsIGluZGVudCkgOiBlbnRyaWVzLmpvaW4oJywgJyk7XG4gICAgcmV0dXJuIHR5cGUgKyAnICgnICsgc2l6ZSArICcpIHsnICsgam9pbmVkRW50cmllcyArICd9Jztcbn1cblxuZnVuY3Rpb24gc2luZ2xlTGluZVZhbHVlcyh4cykge1xuICAgIGZvciAodmFyIGkgPSAwOyBpIDwgeHMubGVuZ3RoOyBpKyspIHtcbiAgICAgICAgaWYgKGluZGV4T2YoeHNbaV0sICdcXG4nKSA+PSAwKSB7XG4gICAgICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG5cbmZ1bmN0aW9uIGdldEluZGVudChvcHRzLCBkZXB0aCkge1xuICAgIHZhciBiYXNlSW5kZW50O1xuICAgIGlmIChvcHRzLmluZGVudCA9PT0gJ1xcdCcpIHtcbiAgICAgICAgYmFzZUluZGVudCA9ICdcXHQnO1xuICAgIH0gZWxzZSBpZiAodHlwZW9mIG9wdHMuaW5kZW50ID09PSAnbnVtYmVyJyAmJiBvcHRzLmluZGVudCA+IDApIHtcbiAgICAgICAgYmFzZUluZGVudCA9IEFycmF5KG9wdHMuaW5kZW50ICsgMSkuam9pbignICcpO1xuICAgIH0gZWxzZSB7XG4gICAgICAgIHJldHVybiBudWxsO1xuICAgIH1cbiAgICByZXR1cm4ge1xuICAgICAgICBiYXNlOiBiYXNlSW5kZW50LFxuICAgICAgICBwcmV2OiBBcnJheShkZXB0aCArIDEpLmpvaW4oYmFzZUluZGVudClcbiAgICB9O1xufVxuXG5mdW5jdGlvbiBpbmRlbnRlZEpvaW4oeHMsIGluZGVudCkge1xuICAgIGlmICh4cy5sZW5ndGggPT09IDApIHsgcmV0dXJuICcnOyB9XG4gICAgdmFyIGxpbmVKb2luZXIgPSAnXFxuJyArIGluZGVudC5wcmV2ICsgaW5kZW50LmJhc2U7XG4gICAgcmV0dXJuIGxpbmVKb2luZXIgKyB4cy5qb2luKCcsJyArIGxpbmVKb2luZXIpICsgJ1xcbicgKyBpbmRlbnQucHJldjtcbn1cblxuZnVuY3Rpb24gYXJyT2JqS2V5cyhvYmosIGluc3BlY3QpIHtcbiAgICB2YXIgaXNBcnIgPSBpc0FycmF5KG9iaik7XG4gICAgdmFyIHhzID0gW107XG4gICAgaWYgKGlzQXJyKSB7XG4gICAgICAgIHhzLmxlbmd0aCA9IG9iai5sZW5ndGg7XG4gICAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwgb2JqLmxlbmd0aDsgaSsrKSB7XG4gICAgICAgICAgICB4c1tpXSA9IGhhcyhvYmosIGkpID8gaW5zcGVjdChvYmpbaV0sIG9iaikgOiAnJztcbiAgICAgICAgfVxuICAgIH1cbiAgICB2YXIgc3ltcyA9IHR5cGVvZiBnT1BTID09PSAnZnVuY3Rpb24nID8gZ09QUyhvYmopIDogW107XG4gICAgdmFyIHN5bU1hcDtcbiAgICBpZiAoaGFzU2hhbW1lZFN5bWJvbHMpIHtcbiAgICAgICAgc3ltTWFwID0ge307XG4gICAgICAgIGZvciAodmFyIGsgPSAwOyBrIDwgc3ltcy5sZW5ndGg7IGsrKykge1xuICAgICAgICAgICAgc3ltTWFwWyckJyArIHN5bXNba11dID0gc3ltc1trXTtcbiAgICAgICAgfVxuICAgIH1cblxuICAgIGZvciAodmFyIGtleSBpbiBvYmopIHsgLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1yZXN0cmljdGVkLXN5bnRheFxuICAgICAgICBpZiAoIWhhcyhvYmosIGtleSkpIHsgY29udGludWU7IH0gLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1yZXN0cmljdGVkLXN5bnRheCwgbm8tY29udGludWVcbiAgICAgICAgaWYgKGlzQXJyICYmIFN0cmluZyhOdW1iZXIoa2V5KSkgPT09IGtleSAmJiBrZXkgPCBvYmoubGVuZ3RoKSB7IGNvbnRpbnVlOyB9IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tcmVzdHJpY3RlZC1zeW50YXgsIG5vLWNvbnRpbnVlXG4gICAgICAgIGlmIChoYXNTaGFtbWVkU3ltYm9scyAmJiBzeW1NYXBbJyQnICsga2V5XSBpbnN0YW5jZW9mIFN5bWJvbCkge1xuICAgICAgICAgICAgLy8gdGhpcyBpcyB0byBwcmV2ZW50IHNoYW1tZWQgU3ltYm9scywgd2hpY2ggYXJlIHN0b3JlZCBhcyBzdHJpbmdzLCBmcm9tIGJlaW5nIGluY2x1ZGVkIGluIHRoZSBzdHJpbmcga2V5IHNlY3Rpb25cbiAgICAgICAgICAgIGNvbnRpbnVlOyAvLyBlc2xpbnQtZGlzYWJsZS1saW5lIG5vLXJlc3RyaWN0ZWQtc3ludGF4LCBuby1jb250aW51ZVxuICAgICAgICB9IGVsc2UgaWYgKCgvW15cXHckXS8pLnRlc3Qoa2V5KSkge1xuICAgICAgICAgICAgeHMucHVzaChpbnNwZWN0KGtleSwgb2JqKSArICc6ICcgKyBpbnNwZWN0KG9ialtrZXldLCBvYmopKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIHhzLnB1c2goa2V5ICsgJzogJyArIGluc3BlY3Qob2JqW2tleV0sIG9iaikpO1xuICAgICAgICB9XG4gICAgfVxuICAgIGlmICh0eXBlb2YgZ09QUyA9PT0gJ2Z1bmN0aW9uJykge1xuICAgICAgICBmb3IgKHZhciBqID0gMDsgaiA8IHN5bXMubGVuZ3RoOyBqKyspIHtcbiAgICAgICAgICAgIGlmIChpc0VudW1lcmFibGUuY2FsbChvYmosIHN5bXNbal0pKSB7XG4gICAgICAgICAgICAgICAgeHMucHVzaCgnWycgKyBpbnNwZWN0KHN5bXNbal0pICsgJ106ICcgKyBpbnNwZWN0KG9ialtzeW1zW2pdXSwgb2JqKSk7XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIHhzO1xufVxuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///631\n")},8987:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar keysShim;\nif (!Object.keys) {\n\t// modified from https://github.com/es-shims/es5-shim\n\tvar has = Object.prototype.hasOwnProperty;\n\tvar toStr = Object.prototype.toString;\n\tvar isArgs = __webpack_require__(1414); // eslint-disable-line global-require\n\tvar isEnumerable = Object.prototype.propertyIsEnumerable;\n\tvar hasDontEnumBug = !isEnumerable.call({ toString: null }, 'toString');\n\tvar hasProtoEnumBug = isEnumerable.call(function () {}, 'prototype');\n\tvar dontEnums = [\n\t\t'toString',\n\t\t'toLocaleString',\n\t\t'valueOf',\n\t\t'hasOwnProperty',\n\t\t'isPrototypeOf',\n\t\t'propertyIsEnumerable',\n\t\t'constructor'\n\t];\n\tvar equalsConstructorPrototype = function (o) {\n\t\tvar ctor = o.constructor;\n\t\treturn ctor && ctor.prototype === o;\n\t};\n\tvar excludedKeys = {\n\t\t$applicationCache: true,\n\t\t$console: true,\n\t\t$external: true,\n\t\t$frame: true,\n\t\t$frameElement: true,\n\t\t$frames: true,\n\t\t$innerHeight: true,\n\t\t$innerWidth: true,\n\t\t$onmozfullscreenchange: true,\n\t\t$onmozfullscreenerror: true,\n\t\t$outerHeight: true,\n\t\t$outerWidth: true,\n\t\t$pageXOffset: true,\n\t\t$pageYOffset: true,\n\t\t$parent: true,\n\t\t$scrollLeft: true,\n\t\t$scrollTop: true,\n\t\t$scrollX: true,\n\t\t$scrollY: true,\n\t\t$self: true,\n\t\t$webkitIndexedDB: true,\n\t\t$webkitStorageInfo: true,\n\t\t$window: true\n\t};\n\tvar hasAutomationEqualityBug = (function () {\n\t\t/* global window */\n\t\tif (typeof window === 'undefined') { return false; }\n\t\tfor (var k in window) {\n\t\t\ttry {\n\t\t\t\tif (!excludedKeys['$' + k] && has.call(window, k) && window[k] !== null && typeof window[k] === 'object') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tequalsConstructorPrototype(window[k]);\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}());\n\tvar equalsConstructorPrototypeIfNotBuggy = function (o) {\n\t\t/* global window */\n\t\tif (typeof window === 'undefined' || !hasAutomationEqualityBug) {\n\t\t\treturn equalsConstructorPrototype(o);\n\t\t}\n\t\ttry {\n\t\t\treturn equalsConstructorPrototype(o);\n\t\t} catch (e) {\n\t\t\treturn false;\n\t\t}\n\t};\n\n\tkeysShim = function keys(object) {\n\t\tvar isObject = object !== null && typeof object === 'object';\n\t\tvar isFunction = toStr.call(object) === '[object Function]';\n\t\tvar isArguments = isArgs(object);\n\t\tvar isString = isObject && toStr.call(object) === '[object String]';\n\t\tvar theKeys = [];\n\n\t\tif (!isObject && !isFunction && !isArguments) {\n\t\t\tthrow new TypeError('Object.keys called on a non-object');\n\t\t}\n\n\t\tvar skipProto = hasProtoEnumBug && isFunction;\n\t\tif (isString && object.length > 0 && !has.call(object, 0)) {\n\t\t\tfor (var i = 0; i < object.length; ++i) {\n\t\t\t\ttheKeys.push(String(i));\n\t\t\t}\n\t\t}\n\n\t\tif (isArguments && object.length > 0) {\n\t\t\tfor (var j = 0; j < object.length; ++j) {\n\t\t\t\ttheKeys.push(String(j));\n\t\t\t}\n\t\t} else {\n\t\t\tfor (var name in object) {\n\t\t\t\tif (!(skipProto && name === 'prototype') && has.call(object, name)) {\n\t\t\t\t\ttheKeys.push(String(name));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (hasDontEnumBug) {\n\t\t\tvar skipConstructor = equalsConstructorPrototypeIfNotBuggy(object);\n\n\t\t\tfor (var k = 0; k < dontEnums.length; ++k) {\n\t\t\t\tif (!(skipConstructor && dontEnums[k] === 'constructor') && has.call(object, dontEnums[k])) {\n\t\t\t\t\ttheKeys.push(dontEnums[k]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn theKeys;\n\t};\n}\nmodule.exports = keysShim;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODk4Ny5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsY0FBYyxtQkFBTyxDQUFDLElBQWUsR0FBRztBQUN4QztBQUNBLDJDQUEyQyxnQkFBZ0I7QUFDM0QsdURBQXVEO0FBQ3ZEO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsdUNBQXVDO0FBQ3ZDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxPQUFPO0FBQ1A7QUFDQTtBQUNBO0FBQ0EsS0FBSztBQUNMO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsRUFBRTtBQUNGO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSTtBQUNKO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxtQkFBbUIsbUJBQW1CO0FBQ3RDO0FBQ0E7QUFDQTs7QUFFQTtBQUNBLG1CQUFtQixtQkFBbUI7QUFDdEM7QUFDQTtBQUNBLElBQUk7QUFDSjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQSxtQkFBbUIsc0JBQXNCO0FBQ3pDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvb2JqZWN0LWtleXMvaW1wbGVtZW50YXRpb24uanM/YjE4OSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBrZXlzU2hpbTtcbmlmICghT2JqZWN0LmtleXMpIHtcblx0Ly8gbW9kaWZpZWQgZnJvbSBodHRwczovL2dpdGh1Yi5jb20vZXMtc2hpbXMvZXM1LXNoaW1cblx0dmFyIGhhcyA9IE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHk7XG5cdHZhciB0b1N0ciA9IE9iamVjdC5wcm90b3R5cGUudG9TdHJpbmc7XG5cdHZhciBpc0FyZ3MgPSByZXF1aXJlKCcuL2lzQXJndW1lbnRzJyk7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgZ2xvYmFsLXJlcXVpcmVcblx0dmFyIGlzRW51bWVyYWJsZSA9IE9iamVjdC5wcm90b3R5cGUucHJvcGVydHlJc0VudW1lcmFibGU7XG5cdHZhciBoYXNEb250RW51bUJ1ZyA9ICFpc0VudW1lcmFibGUuY2FsbCh7IHRvU3RyaW5nOiBudWxsIH0sICd0b1N0cmluZycpO1xuXHR2YXIgaGFzUHJvdG9FbnVtQnVnID0gaXNFbnVtZXJhYmxlLmNhbGwoZnVuY3Rpb24gKCkge30sICdwcm90b3R5cGUnKTtcblx0dmFyIGRvbnRFbnVtcyA9IFtcblx0XHQndG9TdHJpbmcnLFxuXHRcdCd0b0xvY2FsZVN0cmluZycsXG5cdFx0J3ZhbHVlT2YnLFxuXHRcdCdoYXNPd25Qcm9wZXJ0eScsXG5cdFx0J2lzUHJvdG90eXBlT2YnLFxuXHRcdCdwcm9wZXJ0eUlzRW51bWVyYWJsZScsXG5cdFx0J2NvbnN0cnVjdG9yJ1xuXHRdO1xuXHR2YXIgZXF1YWxzQ29uc3RydWN0b3JQcm90b3R5cGUgPSBmdW5jdGlvbiAobykge1xuXHRcdHZhciBjdG9yID0gby5jb25zdHJ1Y3Rvcjtcblx0XHRyZXR1cm4gY3RvciAmJiBjdG9yLnByb3RvdHlwZSA9PT0gbztcblx0fTtcblx0dmFyIGV4Y2x1ZGVkS2V5cyA9IHtcblx0XHQkYXBwbGljYXRpb25DYWNoZTogdHJ1ZSxcblx0XHQkY29uc29sZTogdHJ1ZSxcblx0XHQkZXh0ZXJuYWw6IHRydWUsXG5cdFx0JGZyYW1lOiB0cnVlLFxuXHRcdCRmcmFtZUVsZW1lbnQ6IHRydWUsXG5cdFx0JGZyYW1lczogdHJ1ZSxcblx0XHQkaW5uZXJIZWlnaHQ6IHRydWUsXG5cdFx0JGlubmVyV2lkdGg6IHRydWUsXG5cdFx0JG9ubW96ZnVsbHNjcmVlbmNoYW5nZTogdHJ1ZSxcblx0XHQkb25tb3pmdWxsc2NyZWVuZXJyb3I6IHRydWUsXG5cdFx0JG91dGVySGVpZ2h0OiB0cnVlLFxuXHRcdCRvdXRlcldpZHRoOiB0cnVlLFxuXHRcdCRwYWdlWE9mZnNldDogdHJ1ZSxcblx0XHQkcGFnZVlPZmZzZXQ6IHRydWUsXG5cdFx0JHBhcmVudDogdHJ1ZSxcblx0XHQkc2Nyb2xsTGVmdDogdHJ1ZSxcblx0XHQkc2Nyb2xsVG9wOiB0cnVlLFxuXHRcdCRzY3JvbGxYOiB0cnVlLFxuXHRcdCRzY3JvbGxZOiB0cnVlLFxuXHRcdCRzZWxmOiB0cnVlLFxuXHRcdCR3ZWJraXRJbmRleGVkREI6IHRydWUsXG5cdFx0JHdlYmtpdFN0b3JhZ2VJbmZvOiB0cnVlLFxuXHRcdCR3aW5kb3c6IHRydWVcblx0fTtcblx0dmFyIGhhc0F1dG9tYXRpb25FcXVhbGl0eUJ1ZyA9IChmdW5jdGlvbiAoKSB7XG5cdFx0LyogZ2xvYmFsIHdpbmRvdyAqL1xuXHRcdGlmICh0eXBlb2Ygd2luZG93ID09PSAndW5kZWZpbmVkJykgeyByZXR1cm4gZmFsc2U7IH1cblx0XHRmb3IgKHZhciBrIGluIHdpbmRvdykge1xuXHRcdFx0dHJ5IHtcblx0XHRcdFx0aWYgKCFleGNsdWRlZEtleXNbJyQnICsga10gJiYgaGFzLmNhbGwod2luZG93LCBrKSAmJiB3aW5kb3dba10gIT09IG51bGwgJiYgdHlwZW9mIHdpbmRvd1trXSA9PT0gJ29iamVjdCcpIHtcblx0XHRcdFx0XHR0cnkge1xuXHRcdFx0XHRcdFx0ZXF1YWxzQ29uc3RydWN0b3JQcm90b3R5cGUod2luZG93W2tdKTtcblx0XHRcdFx0XHR9IGNhdGNoIChlKSB7XG5cdFx0XHRcdFx0XHRyZXR1cm4gdHJ1ZTtcblx0XHRcdFx0XHR9XG5cdFx0XHRcdH1cblx0XHRcdH0gY2F0Y2ggKGUpIHtcblx0XHRcdFx0cmV0dXJuIHRydWU7XG5cdFx0XHR9XG5cdFx0fVxuXHRcdHJldHVybiBmYWxzZTtcblx0fSgpKTtcblx0dmFyIGVxdWFsc0NvbnN0cnVjdG9yUHJvdG90eXBlSWZOb3RCdWdneSA9IGZ1bmN0aW9uIChvKSB7XG5cdFx0LyogZ2xvYmFsIHdpbmRvdyAqL1xuXHRcdGlmICh0eXBlb2Ygd2luZG93ID09PSAndW5kZWZpbmVkJyB8fCAhaGFzQXV0b21hdGlvbkVxdWFsaXR5QnVnKSB7XG5cdFx0XHRyZXR1cm4gZXF1YWxzQ29uc3RydWN0b3JQcm90b3R5cGUobyk7XG5cdFx0fVxuXHRcdHRyeSB7XG5cdFx0XHRyZXR1cm4gZXF1YWxzQ29uc3RydWN0b3JQcm90b3R5cGUobyk7XG5cdFx0fSBjYXRjaCAoZSkge1xuXHRcdFx0cmV0dXJuIGZhbHNlO1xuXHRcdH1cblx0fTtcblxuXHRrZXlzU2hpbSA9IGZ1bmN0aW9uIGtleXMob2JqZWN0KSB7XG5cdFx0dmFyIGlzT2JqZWN0ID0gb2JqZWN0ICE9PSBudWxsICYmIHR5cGVvZiBvYmplY3QgPT09ICdvYmplY3QnO1xuXHRcdHZhciBpc0Z1bmN0aW9uID0gdG9TdHIuY2FsbChvYmplY3QpID09PSAnW29iamVjdCBGdW5jdGlvbl0nO1xuXHRcdHZhciBpc0FyZ3VtZW50cyA9IGlzQXJncyhvYmplY3QpO1xuXHRcdHZhciBpc1N0cmluZyA9IGlzT2JqZWN0ICYmIHRvU3RyLmNhbGwob2JqZWN0KSA9PT0gJ1tvYmplY3QgU3RyaW5nXSc7XG5cdFx0dmFyIHRoZUtleXMgPSBbXTtcblxuXHRcdGlmICghaXNPYmplY3QgJiYgIWlzRnVuY3Rpb24gJiYgIWlzQXJndW1lbnRzKSB7XG5cdFx0XHR0aHJvdyBuZXcgVHlwZUVycm9yKCdPYmplY3Qua2V5cyBjYWxsZWQgb24gYSBub24tb2JqZWN0Jyk7XG5cdFx0fVxuXG5cdFx0dmFyIHNraXBQcm90byA9IGhhc1Byb3RvRW51bUJ1ZyAmJiBpc0Z1bmN0aW9uO1xuXHRcdGlmIChpc1N0cmluZyAmJiBvYmplY3QubGVuZ3RoID4gMCAmJiAhaGFzLmNhbGwob2JqZWN0LCAwKSkge1xuXHRcdFx0Zm9yICh2YXIgaSA9IDA7IGkgPCBvYmplY3QubGVuZ3RoOyArK2kpIHtcblx0XHRcdFx0dGhlS2V5cy5wdXNoKFN0cmluZyhpKSk7XG5cdFx0XHR9XG5cdFx0fVxuXG5cdFx0aWYgKGlzQXJndW1lbnRzICYmIG9iamVjdC5sZW5ndGggPiAwKSB7XG5cdFx0XHRmb3IgKHZhciBqID0gMDsgaiA8IG9iamVjdC5sZW5ndGg7ICsraikge1xuXHRcdFx0XHR0aGVLZXlzLnB1c2goU3RyaW5nKGopKTtcblx0XHRcdH1cblx0XHR9IGVsc2Uge1xuXHRcdFx0Zm9yICh2YXIgbmFtZSBpbiBvYmplY3QpIHtcblx0XHRcdFx0aWYgKCEoc2tpcFByb3RvICYmIG5hbWUgPT09ICdwcm90b3R5cGUnKSAmJiBoYXMuY2FsbChvYmplY3QsIG5hbWUpKSB7XG5cdFx0XHRcdFx0dGhlS2V5cy5wdXNoKFN0cmluZyhuYW1lKSk7XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHR9XG5cblx0XHRpZiAoaGFzRG9udEVudW1CdWcpIHtcblx0XHRcdHZhciBza2lwQ29uc3RydWN0b3IgPSBlcXVhbHNDb25zdHJ1Y3RvclByb3RvdHlwZUlmTm90QnVnZ3kob2JqZWN0KTtcblxuXHRcdFx0Zm9yICh2YXIgayA9IDA7IGsgPCBkb250RW51bXMubGVuZ3RoOyArK2spIHtcblx0XHRcdFx0aWYgKCEoc2tpcENvbnN0cnVjdG9yICYmIGRvbnRFbnVtc1trXSA9PT0gJ2NvbnN0cnVjdG9yJykgJiYgaGFzLmNhbGwob2JqZWN0LCBkb250RW51bXNba10pKSB7XG5cdFx0XHRcdFx0dGhlS2V5cy5wdXNoKGRvbnRFbnVtc1trXSk7XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHR9XG5cdFx0cmV0dXJuIHRoZUtleXM7XG5cdH07XG59XG5tb2R1bGUuZXhwb3J0cyA9IGtleXNTaGltO1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///8987\n")},2215:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar slice = Array.prototype.slice;\nvar isArgs = __webpack_require__(1414);\n\nvar origKeys = Object.keys;\nvar keysShim = origKeys ? function keys(o) { return origKeys(o); } : __webpack_require__(8987);\n\nvar originalKeys = Object.keys;\n\nkeysShim.shim = function shimObjectKeys() {\n\tif (Object.keys) {\n\t\tvar keysWorksWithArguments = (function () {\n\t\t\t// Safari 5.0 bug\n\t\t\tvar args = Object.keys(arguments);\n\t\t\treturn args && args.length === arguments.length;\n\t\t}(1, 2));\n\t\tif (!keysWorksWithArguments) {\n\t\t\tObject.keys = function keys(object) { // eslint-disable-line func-name-matching\n\t\t\t\tif (isArgs(object)) {\n\t\t\t\t\treturn originalKeys(slice.call(object));\n\t\t\t\t}\n\t\t\t\treturn originalKeys(object);\n\t\t\t};\n\t\t}\n\t} else {\n\t\tObject.keys = keysShim;\n\t}\n\treturn Object.keys || keysShim;\n};\n\nmodule.exports = keysShim;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjIxNS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBLGFBQWEsbUJBQU8sQ0FBQyxJQUFlOztBQUVwQztBQUNBLDZDQUE2QyxzQkFBc0IsRUFBRSxtQkFBTyxDQUFDLElBQWtCOztBQUUvRjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0g7QUFDQSx5Q0FBeUM7QUFDekM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsR0FBRztBQUNIO0FBQ0E7QUFDQTtBQUNBOztBQUVBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9vYmplY3Qta2V5cy9pbmRleC5qcz9kNmM3Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIHNsaWNlID0gQXJyYXkucHJvdG90eXBlLnNsaWNlO1xudmFyIGlzQXJncyA9IHJlcXVpcmUoJy4vaXNBcmd1bWVudHMnKTtcblxudmFyIG9yaWdLZXlzID0gT2JqZWN0LmtleXM7XG52YXIga2V5c1NoaW0gPSBvcmlnS2V5cyA/IGZ1bmN0aW9uIGtleXMobykgeyByZXR1cm4gb3JpZ0tleXMobyk7IH0gOiByZXF1aXJlKCcuL2ltcGxlbWVudGF0aW9uJyk7XG5cbnZhciBvcmlnaW5hbEtleXMgPSBPYmplY3Qua2V5cztcblxua2V5c1NoaW0uc2hpbSA9IGZ1bmN0aW9uIHNoaW1PYmplY3RLZXlzKCkge1xuXHRpZiAoT2JqZWN0LmtleXMpIHtcblx0XHR2YXIga2V5c1dvcmtzV2l0aEFyZ3VtZW50cyA9IChmdW5jdGlvbiAoKSB7XG5cdFx0XHQvLyBTYWZhcmkgNS4wIGJ1Z1xuXHRcdFx0dmFyIGFyZ3MgPSBPYmplY3Qua2V5cyhhcmd1bWVudHMpO1xuXHRcdFx0cmV0dXJuIGFyZ3MgJiYgYXJncy5sZW5ndGggPT09IGFyZ3VtZW50cy5sZW5ndGg7XG5cdFx0fSgxLCAyKSk7XG5cdFx0aWYgKCFrZXlzV29ya3NXaXRoQXJndW1lbnRzKSB7XG5cdFx0XHRPYmplY3Qua2V5cyA9IGZ1bmN0aW9uIGtleXMob2JqZWN0KSB7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgZnVuYy1uYW1lLW1hdGNoaW5nXG5cdFx0XHRcdGlmIChpc0FyZ3Mob2JqZWN0KSkge1xuXHRcdFx0XHRcdHJldHVybiBvcmlnaW5hbEtleXMoc2xpY2UuY2FsbChvYmplY3QpKTtcblx0XHRcdFx0fVxuXHRcdFx0XHRyZXR1cm4gb3JpZ2luYWxLZXlzKG9iamVjdCk7XG5cdFx0XHR9O1xuXHRcdH1cblx0fSBlbHNlIHtcblx0XHRPYmplY3Qua2V5cyA9IGtleXNTaGltO1xuXHR9XG5cdHJldHVybiBPYmplY3Qua2V5cyB8fCBrZXlzU2hpbTtcbn07XG5cbm1vZHVsZS5leHBvcnRzID0ga2V5c1NoaW07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///2215\n")},1414:function(module){"use strict";eval("\n\nvar toStr = Object.prototype.toString;\n\nmodule.exports = function isArguments(value) {\n\tvar str = toStr.call(value);\n\tvar isArgs = str === '[object Arguments]';\n\tif (!isArgs) {\n\t\tisArgs = str !== '[object Array]' &&\n\t\t\tvalue !== null &&\n\t\t\ttypeof value === 'object' &&\n\t\t\ttypeof value.length === 'number' &&\n\t\t\tvalue.length >= 0 &&\n\t\t\ttoStr.call(value.callee) === '[object Function]';\n\t}\n\treturn isArgs;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTQxNC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvb2JqZWN0LWtleXMvaXNBcmd1bWVudHMuanM/ZDRhYiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciB0b1N0ciA9IE9iamVjdC5wcm90b3R5cGUudG9TdHJpbmc7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaXNBcmd1bWVudHModmFsdWUpIHtcblx0dmFyIHN0ciA9IHRvU3RyLmNhbGwodmFsdWUpO1xuXHR2YXIgaXNBcmdzID0gc3RyID09PSAnW29iamVjdCBBcmd1bWVudHNdJztcblx0aWYgKCFpc0FyZ3MpIHtcblx0XHRpc0FyZ3MgPSBzdHIgIT09ICdbb2JqZWN0IEFycmF5XScgJiZcblx0XHRcdHZhbHVlICE9PSBudWxsICYmXG5cdFx0XHR0eXBlb2YgdmFsdWUgPT09ICdvYmplY3QnICYmXG5cdFx0XHR0eXBlb2YgdmFsdWUubGVuZ3RoID09PSAnbnVtYmVyJyAmJlxuXHRcdFx0dmFsdWUubGVuZ3RoID49IDAgJiZcblx0XHRcdHRvU3RyLmNhbGwodmFsdWUuY2FsbGVlKSA9PT0gJ1tvYmplY3QgRnVuY3Rpb25dJztcblx0fVxuXHRyZXR1cm4gaXNBcmdzO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///1414\n")},3697:function(module){"use strict";eval("\n\nvar $Object = Object;\nvar $TypeError = TypeError;\n\nmodule.exports = function flags() {\n\tif (this != null && this !== $Object(this)) {\n\t\tthrow new $TypeError('RegExp.prototype.flags getter called on non-object');\n\t}\n\tvar result = '';\n\tif (this.global) {\n\t\tresult += 'g';\n\t}\n\tif (this.ignoreCase) {\n\t\tresult += 'i';\n\t}\n\tif (this.multiline) {\n\t\tresult += 'm';\n\t}\n\tif (this.dotAll) {\n\t\tresult += 's';\n\t}\n\tif (this.unicode) {\n\t\tresult += 'u';\n\t}\n\tif (this.sticky) {\n\t\tresult += 'y';\n\t}\n\treturn result;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzY5Ny5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9yZWdleHAucHJvdG90eXBlLmZsYWdzL2ltcGxlbWVudGF0aW9uLmpzPzU3MDgiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgJE9iamVjdCA9IE9iamVjdDtcbnZhciAkVHlwZUVycm9yID0gVHlwZUVycm9yO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIGZsYWdzKCkge1xuXHRpZiAodGhpcyAhPSBudWxsICYmIHRoaXMgIT09ICRPYmplY3QodGhpcykpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignUmVnRXhwLnByb3RvdHlwZS5mbGFncyBnZXR0ZXIgY2FsbGVkIG9uIG5vbi1vYmplY3QnKTtcblx0fVxuXHR2YXIgcmVzdWx0ID0gJyc7XG5cdGlmICh0aGlzLmdsb2JhbCkge1xuXHRcdHJlc3VsdCArPSAnZyc7XG5cdH1cblx0aWYgKHRoaXMuaWdub3JlQ2FzZSkge1xuXHRcdHJlc3VsdCArPSAnaSc7XG5cdH1cblx0aWYgKHRoaXMubXVsdGlsaW5lKSB7XG5cdFx0cmVzdWx0ICs9ICdtJztcblx0fVxuXHRpZiAodGhpcy5kb3RBbGwpIHtcblx0XHRyZXN1bHQgKz0gJ3MnO1xuXHR9XG5cdGlmICh0aGlzLnVuaWNvZGUpIHtcblx0XHRyZXN1bHQgKz0gJ3UnO1xuXHR9XG5cdGlmICh0aGlzLnN0aWNreSkge1xuXHRcdHJlc3VsdCArPSAneSc7XG5cdH1cblx0cmV0dXJuIHJlc3VsdDtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3697\n")},2847:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar define = __webpack_require__(4289);\nvar callBind = __webpack_require__(5559);\n\nvar implementation = __webpack_require__(3697);\nvar getPolyfill = __webpack_require__(1721);\nvar shim = __webpack_require__(2753);\n\nvar flagsBound = callBind(implementation);\n\ndefine(flagsBound, {\n\tgetPolyfill: getPolyfill,\n\timplementation: implementation,\n\tshim: shim\n});\n\nmodule.exports = flagsBound;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjg0Ny5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixhQUFhLG1CQUFPLENBQUMsSUFBbUI7QUFDeEMsZUFBZSxtQkFBTyxDQUFDLElBQVc7O0FBRWxDLHFCQUFxQixtQkFBTyxDQUFDLElBQWtCO0FBQy9DLGtCQUFrQixtQkFBTyxDQUFDLElBQVk7QUFDdEMsV0FBVyxtQkFBTyxDQUFDLElBQVE7O0FBRTNCOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsQ0FBQzs7QUFFRCIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvcmVnZXhwLnByb3RvdHlwZS5mbGFncy9pbmRleC5qcz9lNzEwIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGRlZmluZSA9IHJlcXVpcmUoJ2RlZmluZS1wcm9wZXJ0aWVzJyk7XG52YXIgY2FsbEJpbmQgPSByZXF1aXJlKCdjYWxsLWJpbmQnKTtcblxudmFyIGltcGxlbWVudGF0aW9uID0gcmVxdWlyZSgnLi9pbXBsZW1lbnRhdGlvbicpO1xudmFyIGdldFBvbHlmaWxsID0gcmVxdWlyZSgnLi9wb2x5ZmlsbCcpO1xudmFyIHNoaW0gPSByZXF1aXJlKCcuL3NoaW0nKTtcblxudmFyIGZsYWdzQm91bmQgPSBjYWxsQmluZChpbXBsZW1lbnRhdGlvbik7XG5cbmRlZmluZShmbGFnc0JvdW5kLCB7XG5cdGdldFBvbHlmaWxsOiBnZXRQb2x5ZmlsbCxcblx0aW1wbGVtZW50YXRpb246IGltcGxlbWVudGF0aW9uLFxuXHRzaGltOiBzaGltXG59KTtcblxubW9kdWxlLmV4cG9ydHMgPSBmbGFnc0JvdW5kO1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///2847\n")},1721:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar implementation = __webpack_require__(3697);\n\nvar supportsDescriptors = __webpack_require__(4289).supportsDescriptors;\nvar $gOPD = Object.getOwnPropertyDescriptor;\nvar $TypeError = TypeError;\n\nmodule.exports = function getPolyfill() {\n\tif (!supportsDescriptors) {\n\t\tthrow new $TypeError('RegExp.prototype.flags requires a true ES5 environment that supports property descriptors');\n\t}\n\tif ((/a/mig).flags === 'gim') {\n\t\tvar descriptor = $gOPD(RegExp.prototype, 'flags');\n\t\tif (descriptor && typeof descriptor.get === 'function' && typeof (/a/).dotAll === 'boolean') {\n\t\t\treturn descriptor.get;\n\t\t}\n\t}\n\treturn implementation;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTcyMS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixxQkFBcUIsbUJBQU8sQ0FBQyxJQUFrQjs7QUFFL0MsMEJBQTBCLDZDQUFnRDtBQUMxRTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvcmVnZXhwLnByb3RvdHlwZS5mbGFncy9wb2x5ZmlsbC5qcz81N2VjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGltcGxlbWVudGF0aW9uID0gcmVxdWlyZSgnLi9pbXBsZW1lbnRhdGlvbicpO1xuXG52YXIgc3VwcG9ydHNEZXNjcmlwdG9ycyA9IHJlcXVpcmUoJ2RlZmluZS1wcm9wZXJ0aWVzJykuc3VwcG9ydHNEZXNjcmlwdG9ycztcbnZhciAkZ09QRCA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3I7XG52YXIgJFR5cGVFcnJvciA9IFR5cGVFcnJvcjtcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBnZXRQb2x5ZmlsbCgpIHtcblx0aWYgKCFzdXBwb3J0c0Rlc2NyaXB0b3JzKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ1JlZ0V4cC5wcm90b3R5cGUuZmxhZ3MgcmVxdWlyZXMgYSB0cnVlIEVTNSBlbnZpcm9ubWVudCB0aGF0IHN1cHBvcnRzIHByb3BlcnR5IGRlc2NyaXB0b3JzJyk7XG5cdH1cblx0aWYgKCgvYS9taWcpLmZsYWdzID09PSAnZ2ltJykge1xuXHRcdHZhciBkZXNjcmlwdG9yID0gJGdPUEQoUmVnRXhwLnByb3RvdHlwZSwgJ2ZsYWdzJyk7XG5cdFx0aWYgKGRlc2NyaXB0b3IgJiYgdHlwZW9mIGRlc2NyaXB0b3IuZ2V0ID09PSAnZnVuY3Rpb24nICYmIHR5cGVvZiAoL2EvKS5kb3RBbGwgPT09ICdib29sZWFuJykge1xuXHRcdFx0cmV0dXJuIGRlc2NyaXB0b3IuZ2V0O1xuXHRcdH1cblx0fVxuXHRyZXR1cm4gaW1wbGVtZW50YXRpb247XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///1721\n")},2753:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar supportsDescriptors = __webpack_require__(4289).supportsDescriptors;\nvar getPolyfill = __webpack_require__(1721);\nvar gOPD = Object.getOwnPropertyDescriptor;\nvar defineProperty = Object.defineProperty;\nvar TypeErr = TypeError;\nvar getProto = Object.getPrototypeOf;\nvar regex = /a/;\n\nmodule.exports = function shimFlags() {\n\tif (!supportsDescriptors || !getProto) {\n\t\tthrow new TypeErr('RegExp.prototype.flags requires a true ES5 environment that supports property descriptors');\n\t}\n\tvar polyfill = getPolyfill();\n\tvar proto = getProto(regex);\n\tvar descriptor = gOPD(proto, 'flags');\n\tif (!descriptor || descriptor.get !== polyfill) {\n\t\tdefineProperty(proto, 'flags', {\n\t\t\tconfigurable: true,\n\t\t\tenumerable: false,\n\t\t\tget: polyfill\n\t\t});\n\t}\n\treturn polyfill;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjc1My5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYiwwQkFBMEIsNkNBQWdEO0FBQzFFLGtCQUFrQixtQkFBTyxDQUFDLElBQVk7QUFDdEM7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0g7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9yZWdleHAucHJvdG90eXBlLmZsYWdzL3NoaW0uanM/MWM3ZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBzdXBwb3J0c0Rlc2NyaXB0b3JzID0gcmVxdWlyZSgnZGVmaW5lLXByb3BlcnRpZXMnKS5zdXBwb3J0c0Rlc2NyaXB0b3JzO1xudmFyIGdldFBvbHlmaWxsID0gcmVxdWlyZSgnLi9wb2x5ZmlsbCcpO1xudmFyIGdPUEQgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yO1xudmFyIGRlZmluZVByb3BlcnR5ID0gT2JqZWN0LmRlZmluZVByb3BlcnR5O1xudmFyIFR5cGVFcnIgPSBUeXBlRXJyb3I7XG52YXIgZ2V0UHJvdG8gPSBPYmplY3QuZ2V0UHJvdG90eXBlT2Y7XG52YXIgcmVnZXggPSAvYS87XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gc2hpbUZsYWdzKCkge1xuXHRpZiAoIXN1cHBvcnRzRGVzY3JpcHRvcnMgfHwgIWdldFByb3RvKSB7XG5cdFx0dGhyb3cgbmV3IFR5cGVFcnIoJ1JlZ0V4cC5wcm90b3R5cGUuZmxhZ3MgcmVxdWlyZXMgYSB0cnVlIEVTNSBlbnZpcm9ubWVudCB0aGF0IHN1cHBvcnRzIHByb3BlcnR5IGRlc2NyaXB0b3JzJyk7XG5cdH1cblx0dmFyIHBvbHlmaWxsID0gZ2V0UG9seWZpbGwoKTtcblx0dmFyIHByb3RvID0gZ2V0UHJvdG8ocmVnZXgpO1xuXHR2YXIgZGVzY3JpcHRvciA9IGdPUEQocHJvdG8sICdmbGFncycpO1xuXHRpZiAoIWRlc2NyaXB0b3IgfHwgZGVzY3JpcHRvci5nZXQgIT09IHBvbHlmaWxsKSB7XG5cdFx0ZGVmaW5lUHJvcGVydHkocHJvdG8sICdmbGFncycsIHtcblx0XHRcdGNvbmZpZ3VyYWJsZTogdHJ1ZSxcblx0XHRcdGVudW1lcmFibGU6IGZhbHNlLFxuXHRcdFx0Z2V0OiBwb2x5ZmlsbFxuXHRcdH0pO1xuXHR9XG5cdHJldHVybiBwb2x5ZmlsbDtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///2753\n")},7478:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\nvar callBound = __webpack_require__(1924);\nvar inspect = __webpack_require__(631);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar $WeakMap = GetIntrinsic('%WeakMap%', true);\nvar $Map = GetIntrinsic('%Map%', true);\n\nvar $weakMapGet = callBound('WeakMap.prototype.get', true);\nvar $weakMapSet = callBound('WeakMap.prototype.set', true);\nvar $weakMapHas = callBound('WeakMap.prototype.has', true);\nvar $mapGet = callBound('Map.prototype.get', true);\nvar $mapSet = callBound('Map.prototype.set', true);\nvar $mapHas = callBound('Map.prototype.has', true);\n\n/*\n * This function traverses the list returning the node corresponding to the\n * given key.\n *\n * That node is also moved to the head of the list, so that if it's accessed\n * again we don't need to traverse the whole list. By doing so, all the recently\n * used nodes can be accessed relatively quickly.\n */\nvar listGetNode = function (list, key) { // eslint-disable-line consistent-return\n\tfor (var prev = list, curr; (curr = prev.next) !== null; prev = curr) {\n\t\tif (curr.key === key) {\n\t\t\tprev.next = curr.next;\n\t\t\tcurr.next = list.next;\n\t\t\tlist.next = curr; // eslint-disable-line no-param-reassign\n\t\t\treturn curr;\n\t\t}\n\t}\n};\n\nvar listGet = function (objects, key) {\n\tvar node = listGetNode(objects, key);\n\treturn node && node.value;\n};\nvar listSet = function (objects, key, value) {\n\tvar node = listGetNode(objects, key);\n\tif (node) {\n\t\tnode.value = value;\n\t} else {\n\t\t// Prepend the new node to the beginning of the list\n\t\tobjects.next = { // eslint-disable-line no-param-reassign\n\t\t\tkey: key,\n\t\t\tnext: objects.next,\n\t\t\tvalue: value\n\t\t};\n\t}\n};\nvar listHas = function (objects, key) {\n\treturn !!listGetNode(objects, key);\n};\n\nmodule.exports = function getSideChannel() {\n\tvar $wm;\n\tvar $m;\n\tvar $o;\n\tvar channel = {\n\t\tassert: function (key) {\n\t\t\tif (!channel.has(key)) {\n\t\t\t\tthrow new $TypeError('Side channel does not contain ' + inspect(key));\n\t\t\t}\n\t\t},\n\t\tget: function (key) { // eslint-disable-line consistent-return\n\t\t\tif ($WeakMap && key && (typeof key === 'object' || typeof key === 'function')) {\n\t\t\t\tif ($wm) {\n\t\t\t\t\treturn $weakMapGet($wm, key);\n\t\t\t\t}\n\t\t\t} else if ($Map) {\n\t\t\t\tif ($m) {\n\t\t\t\t\treturn $mapGet($m, key);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif ($o) { // eslint-disable-line no-lonely-if\n\t\t\t\t\treturn listGet($o, key);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\thas: function (key) {\n\t\t\tif ($WeakMap && key && (typeof key === 'object' || typeof key === 'function')) {\n\t\t\t\tif ($wm) {\n\t\t\t\t\treturn $weakMapHas($wm, key);\n\t\t\t\t}\n\t\t\t} else if ($Map) {\n\t\t\t\tif ($m) {\n\t\t\t\t\treturn $mapHas($m, key);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif ($o) { // eslint-disable-line no-lonely-if\n\t\t\t\t\treturn listHas($o, key);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t},\n\t\tset: function (key, value) {\n\t\t\tif ($WeakMap && key && (typeof key === 'object' || typeof key === 'function')) {\n\t\t\t\tif (!$wm) {\n\t\t\t\t\t$wm = new $WeakMap();\n\t\t\t\t}\n\t\t\t\t$weakMapSet($wm, key, value);\n\t\t\t} else if ($Map) {\n\t\t\t\tif (!$m) {\n\t\t\t\t\t$m = new $Map();\n\t\t\t\t}\n\t\t\t\t$mapSet($m, key, value);\n\t\t\t} else {\n\t\t\t\tif (!$o) {\n\t\t\t\t\t/*\n\t\t\t\t\t * Initialize the linked list as an empty node, so that we don't have\n\t\t\t\t\t * to special-case handling of the first node: we can always refer to\n\t\t\t\t\t * it as (previous node).next, instead of something like (list).head\n\t\t\t\t\t */\n\t\t\t\t\t$o = { key: {}, next: null };\n\t\t\t\t}\n\t\t\t\tlistSet($o, key, value);\n\t\t\t}\n\t\t}\n\t};\n\treturn channel;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzQ3OC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlO0FBQzFDLGdCQUFnQixtQkFBTyxDQUFDLElBQXFCO0FBQzdDLGNBQWMsbUJBQU8sQ0FBQyxHQUFnQjs7QUFFdEM7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EseUNBQXlDO0FBQ3pDLDZCQUE2Qiw2QkFBNkI7QUFDMUQ7QUFDQTtBQUNBO0FBQ0EscUJBQXFCO0FBQ3JCO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0g7QUFDQSxtQkFBbUI7QUFDbkI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEdBQUc7QUFDSCx3QkFBd0I7QUFDeEI7QUFDQTtBQUNBO0FBQ0E7QUFDQSxLQUFLO0FBQ0w7QUFDQTtBQUNBO0FBQ0EsS0FBSztBQUNMLGNBQWM7QUFDZDtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0g7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEtBQUs7QUFDTDtBQUNBO0FBQ0E7QUFDQSxLQUFLO0FBQ0wsY0FBYztBQUNkO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsR0FBRztBQUNIO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEtBQUs7QUFDTDtBQUNBO0FBQ0E7QUFDQTtBQUNBLEtBQUs7QUFDTDtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxZQUFZLE9BQU87QUFDbkI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL3NpZGUtY2hhbm5lbC9pbmRleC5qcz81NDAyIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcbnZhciBjYWxsQm91bmQgPSByZXF1aXJlKCdjYWxsLWJpbmQvY2FsbEJvdW5kJyk7XG52YXIgaW5zcGVjdCA9IHJlcXVpcmUoJ29iamVjdC1pbnNwZWN0Jyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xudmFyICRXZWFrTWFwID0gR2V0SW50cmluc2ljKCclV2Vha01hcCUnLCB0cnVlKTtcbnZhciAkTWFwID0gR2V0SW50cmluc2ljKCclTWFwJScsIHRydWUpO1xuXG52YXIgJHdlYWtNYXBHZXQgPSBjYWxsQm91bmQoJ1dlYWtNYXAucHJvdG90eXBlLmdldCcsIHRydWUpO1xudmFyICR3ZWFrTWFwU2V0ID0gY2FsbEJvdW5kKCdXZWFrTWFwLnByb3RvdHlwZS5zZXQnLCB0cnVlKTtcbnZhciAkd2Vha01hcEhhcyA9IGNhbGxCb3VuZCgnV2Vha01hcC5wcm90b3R5cGUuaGFzJywgdHJ1ZSk7XG52YXIgJG1hcEdldCA9IGNhbGxCb3VuZCgnTWFwLnByb3RvdHlwZS5nZXQnLCB0cnVlKTtcbnZhciAkbWFwU2V0ID0gY2FsbEJvdW5kKCdNYXAucHJvdG90eXBlLnNldCcsIHRydWUpO1xudmFyICRtYXBIYXMgPSBjYWxsQm91bmQoJ01hcC5wcm90b3R5cGUuaGFzJywgdHJ1ZSk7XG5cbi8qXG4gKiBUaGlzIGZ1bmN0aW9uIHRyYXZlcnNlcyB0aGUgbGlzdCByZXR1cm5pbmcgdGhlIG5vZGUgY29ycmVzcG9uZGluZyB0byB0aGVcbiAqIGdpdmVuIGtleS5cbiAqXG4gKiBUaGF0IG5vZGUgaXMgYWxzbyBtb3ZlZCB0byB0aGUgaGVhZCBvZiB0aGUgbGlzdCwgc28gdGhhdCBpZiBpdCdzIGFjY2Vzc2VkXG4gKiBhZ2FpbiB3ZSBkb24ndCBuZWVkIHRvIHRyYXZlcnNlIHRoZSB3aG9sZSBsaXN0LiBCeSBkb2luZyBzbywgYWxsIHRoZSByZWNlbnRseVxuICogdXNlZCBub2RlcyBjYW4gYmUgYWNjZXNzZWQgcmVsYXRpdmVseSBxdWlja2x5LlxuICovXG52YXIgbGlzdEdldE5vZGUgPSBmdW5jdGlvbiAobGlzdCwga2V5KSB7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgY29uc2lzdGVudC1yZXR1cm5cblx0Zm9yICh2YXIgcHJldiA9IGxpc3QsIGN1cnI7IChjdXJyID0gcHJldi5uZXh0KSAhPT0gbnVsbDsgcHJldiA9IGN1cnIpIHtcblx0XHRpZiAoY3Vyci5rZXkgPT09IGtleSkge1xuXHRcdFx0cHJldi5uZXh0ID0gY3Vyci5uZXh0O1xuXHRcdFx0Y3Vyci5uZXh0ID0gbGlzdC5uZXh0O1xuXHRcdFx0bGlzdC5uZXh0ID0gY3VycjsgLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1wYXJhbS1yZWFzc2lnblxuXHRcdFx0cmV0dXJuIGN1cnI7XG5cdFx0fVxuXHR9XG59O1xuXG52YXIgbGlzdEdldCA9IGZ1bmN0aW9uIChvYmplY3RzLCBrZXkpIHtcblx0dmFyIG5vZGUgPSBsaXN0R2V0Tm9kZShvYmplY3RzLCBrZXkpO1xuXHRyZXR1cm4gbm9kZSAmJiBub2RlLnZhbHVlO1xufTtcbnZhciBsaXN0U2V0ID0gZnVuY3Rpb24gKG9iamVjdHMsIGtleSwgdmFsdWUpIHtcblx0dmFyIG5vZGUgPSBsaXN0R2V0Tm9kZShvYmplY3RzLCBrZXkpO1xuXHRpZiAobm9kZSkge1xuXHRcdG5vZGUudmFsdWUgPSB2YWx1ZTtcblx0fSBlbHNlIHtcblx0XHQvLyBQcmVwZW5kIHRoZSBuZXcgbm9kZSB0byB0aGUgYmVnaW5uaW5nIG9mIHRoZSBsaXN0XG5cdFx0b2JqZWN0cy5uZXh0ID0geyAvLyBlc2xpbnQtZGlzYWJsZS1saW5lIG5vLXBhcmFtLXJlYXNzaWduXG5cdFx0XHRrZXk6IGtleSxcblx0XHRcdG5leHQ6IG9iamVjdHMubmV4dCxcblx0XHRcdHZhbHVlOiB2YWx1ZVxuXHRcdH07XG5cdH1cbn07XG52YXIgbGlzdEhhcyA9IGZ1bmN0aW9uIChvYmplY3RzLCBrZXkpIHtcblx0cmV0dXJuICEhbGlzdEdldE5vZGUob2JqZWN0cywga2V5KTtcbn07XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gZ2V0U2lkZUNoYW5uZWwoKSB7XG5cdHZhciAkd207XG5cdHZhciAkbTtcblx0dmFyICRvO1xuXHR2YXIgY2hhbm5lbCA9IHtcblx0XHRhc3NlcnQ6IGZ1bmN0aW9uIChrZXkpIHtcblx0XHRcdGlmICghY2hhbm5lbC5oYXMoa2V5KSkge1xuXHRcdFx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignU2lkZSBjaGFubmVsIGRvZXMgbm90IGNvbnRhaW4gJyArIGluc3BlY3Qoa2V5KSk7XG5cdFx0XHR9XG5cdFx0fSxcblx0XHRnZXQ6IGZ1bmN0aW9uIChrZXkpIHsgLy8gZXNsaW50LWRpc2FibGUtbGluZSBjb25zaXN0ZW50LXJldHVyblxuXHRcdFx0aWYgKCRXZWFrTWFwICYmIGtleSAmJiAodHlwZW9mIGtleSA9PT0gJ29iamVjdCcgfHwgdHlwZW9mIGtleSA9PT0gJ2Z1bmN0aW9uJykpIHtcblx0XHRcdFx0aWYgKCR3bSkge1xuXHRcdFx0XHRcdHJldHVybiAkd2Vha01hcEdldCgkd20sIGtleSk7XG5cdFx0XHRcdH1cblx0XHRcdH0gZWxzZSBpZiAoJE1hcCkge1xuXHRcdFx0XHRpZiAoJG0pIHtcblx0XHRcdFx0XHRyZXR1cm4gJG1hcEdldCgkbSwga2V5KTtcblx0XHRcdFx0fVxuXHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0aWYgKCRvKSB7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tbG9uZWx5LWlmXG5cdFx0XHRcdFx0cmV0dXJuIGxpc3RHZXQoJG8sIGtleSk7XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHR9LFxuXHRcdGhhczogZnVuY3Rpb24gKGtleSkge1xuXHRcdFx0aWYgKCRXZWFrTWFwICYmIGtleSAmJiAodHlwZW9mIGtleSA9PT0gJ29iamVjdCcgfHwgdHlwZW9mIGtleSA9PT0gJ2Z1bmN0aW9uJykpIHtcblx0XHRcdFx0aWYgKCR3bSkge1xuXHRcdFx0XHRcdHJldHVybiAkd2Vha01hcEhhcygkd20sIGtleSk7XG5cdFx0XHRcdH1cblx0XHRcdH0gZWxzZSBpZiAoJE1hcCkge1xuXHRcdFx0XHRpZiAoJG0pIHtcblx0XHRcdFx0XHRyZXR1cm4gJG1hcEhhcygkbSwga2V5KTtcblx0XHRcdFx0fVxuXHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0aWYgKCRvKSB7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tbG9uZWx5LWlmXG5cdFx0XHRcdFx0cmV0dXJuIGxpc3RIYXMoJG8sIGtleSk7XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHRcdHJldHVybiBmYWxzZTtcblx0XHR9LFxuXHRcdHNldDogZnVuY3Rpb24gKGtleSwgdmFsdWUpIHtcblx0XHRcdGlmICgkV2Vha01hcCAmJiBrZXkgJiYgKHR5cGVvZiBrZXkgPT09ICdvYmplY3QnIHx8IHR5cGVvZiBrZXkgPT09ICdmdW5jdGlvbicpKSB7XG5cdFx0XHRcdGlmICghJHdtKSB7XG5cdFx0XHRcdFx0JHdtID0gbmV3ICRXZWFrTWFwKCk7XG5cdFx0XHRcdH1cblx0XHRcdFx0JHdlYWtNYXBTZXQoJHdtLCBrZXksIHZhbHVlKTtcblx0XHRcdH0gZWxzZSBpZiAoJE1hcCkge1xuXHRcdFx0XHRpZiAoISRtKSB7XG5cdFx0XHRcdFx0JG0gPSBuZXcgJE1hcCgpO1xuXHRcdFx0XHR9XG5cdFx0XHRcdCRtYXBTZXQoJG0sIGtleSwgdmFsdWUpO1xuXHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0aWYgKCEkbykge1xuXHRcdFx0XHRcdC8qXG5cdFx0XHRcdFx0ICogSW5pdGlhbGl6ZSB0aGUgbGlua2VkIGxpc3QgYXMgYW4gZW1wdHkgbm9kZSwgc28gdGhhdCB3ZSBkb24ndCBoYXZlXG5cdFx0XHRcdFx0ICogdG8gc3BlY2lhbC1jYXNlIGhhbmRsaW5nIG9mIHRoZSBmaXJzdCBub2RlOiB3ZSBjYW4gYWx3YXlzIHJlZmVyIHRvXG5cdFx0XHRcdFx0ICogaXQgYXMgKHByZXZpb3VzIG5vZGUpLm5leHQsIGluc3RlYWQgb2Ygc29tZXRoaW5nIGxpa2UgKGxpc3QpLmhlYWRcblx0XHRcdFx0XHQgKi9cblx0XHRcdFx0XHQkbyA9IHsga2V5OiB7fSwgbmV4dDogbnVsbCB9O1xuXHRcdFx0XHR9XG5cdFx0XHRcdGxpc3RTZXQoJG8sIGtleSwgdmFsdWUpO1xuXHRcdFx0fVxuXHRcdH1cblx0fTtcblx0cmV0dXJuIGNoYW5uZWw7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///7478\n")},9505:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar Call = __webpack_require__(581);\nvar Get = __webpack_require__(1391);\nvar GetMethod = __webpack_require__(7364);\nvar IsRegExp = __webpack_require__(840);\nvar ToString = __webpack_require__(6846);\nvar RequireObjectCoercible = __webpack_require__(9619);\nvar callBound = __webpack_require__(1924);\nvar hasSymbols = __webpack_require__(1405)();\nvar flagsGetter = __webpack_require__(2847);\n\nvar $indexOf = callBound('String.prototype.indexOf');\n\nvar regexpMatchAllPolyfill = __webpack_require__(6966);\n\nvar getMatcher = function getMatcher(regexp) { // eslint-disable-line consistent-return\n\tvar matcherPolyfill = regexpMatchAllPolyfill();\n\tif (hasSymbols && typeof Symbol.matchAll === 'symbol') {\n\t\tvar matcher = GetMethod(regexp, Symbol.matchAll);\n\t\tif (matcher === RegExp.prototype[Symbol.matchAll] && matcher !== matcherPolyfill) {\n\t\t\treturn matcherPolyfill;\n\t\t}\n\t\treturn matcher;\n\t}\n\t// fallback for pre-Symbol.matchAll environments\n\tif (IsRegExp(regexp)) {\n\t\treturn matcherPolyfill;\n\t}\n};\n\nmodule.exports = function matchAll(regexp) {\n\tvar O = RequireObjectCoercible(this);\n\n\tif (typeof regexp !== 'undefined' && regexp !== null) {\n\t\tvar isRegExp = IsRegExp(regexp);\n\t\tif (isRegExp) {\n\t\t\t// workaround for older engines that lack RegExp.prototype.flags\n\t\t\tvar flags = 'flags' in regexp ? Get(regexp, 'flags') : flagsGetter(regexp);\n\t\t\tRequireObjectCoercible(flags);\n\t\t\tif ($indexOf(ToString(flags), 'g') < 0) {\n\t\t\t\tthrow new TypeError('matchAll requires a global regular expression');\n\t\t\t}\n\t\t}\n\n\t\tvar matcher = getMatcher(regexp);\n\t\tif (typeof matcher !== 'undefined') {\n\t\t\treturn Call(matcher, regexp, [O]);\n\t\t}\n\t}\n\n\tvar S = ToString(O);\n\t// var rx = RegExpCreate(regexp, 'g');\n\tvar rx = new RegExp(regexp, 'g');\n\treturn Call(getMatcher(rx), rx, [S]);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTUwNS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixXQUFXLG1CQUFPLENBQUMsR0FBdUI7QUFDMUMsVUFBVSxtQkFBTyxDQUFDLElBQXNCO0FBQ3hDLGdCQUFnQixtQkFBTyxDQUFDLElBQTRCO0FBQ3BELGVBQWUsbUJBQU8sQ0FBQyxHQUEyQjtBQUNsRCxlQUFlLG1CQUFPLENBQUMsSUFBMkI7QUFDbEQsNkJBQTZCLG1CQUFPLENBQUMsSUFBeUM7QUFDOUUsZ0JBQWdCLG1CQUFPLENBQUMsSUFBcUI7QUFDN0MsaUJBQWlCLG1CQUFPLENBQUMsSUFBYTtBQUN0QyxrQkFBa0IsbUJBQU8sQ0FBQyxJQUF3Qjs7QUFFbEQ7O0FBRUEsNkJBQTZCLG1CQUFPLENBQUMsSUFBNEI7O0FBRWpFLCtDQUErQztBQUMvQztBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvc3RyaW5nLnByb3RvdHlwZS5tYXRjaGFsbC9pbXBsZW1lbnRhdGlvbi5qcz9jMTdkIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIENhbGwgPSByZXF1aXJlKCdlcy1hYnN0cmFjdC8yMDIxL0NhbGwnKTtcbnZhciBHZXQgPSByZXF1aXJlKCdlcy1hYnN0cmFjdC8yMDIxL0dldCcpO1xudmFyIEdldE1ldGhvZCA9IHJlcXVpcmUoJ2VzLWFic3RyYWN0LzIwMjEvR2V0TWV0aG9kJyk7XG52YXIgSXNSZWdFeHAgPSByZXF1aXJlKCdlcy1hYnN0cmFjdC8yMDIxL0lzUmVnRXhwJyk7XG52YXIgVG9TdHJpbmcgPSByZXF1aXJlKCdlcy1hYnN0cmFjdC8yMDIxL1RvU3RyaW5nJyk7XG52YXIgUmVxdWlyZU9iamVjdENvZXJjaWJsZSA9IHJlcXVpcmUoJ2VzLWFic3RyYWN0LzIwMjEvUmVxdWlyZU9iamVjdENvZXJjaWJsZScpO1xudmFyIGNhbGxCb3VuZCA9IHJlcXVpcmUoJ2NhbGwtYmluZC9jYWxsQm91bmQnKTtcbnZhciBoYXNTeW1ib2xzID0gcmVxdWlyZSgnaGFzLXN5bWJvbHMnKSgpO1xudmFyIGZsYWdzR2V0dGVyID0gcmVxdWlyZSgncmVnZXhwLnByb3RvdHlwZS5mbGFncycpO1xuXG52YXIgJGluZGV4T2YgPSBjYWxsQm91bmQoJ1N0cmluZy5wcm90b3R5cGUuaW5kZXhPZicpO1xuXG52YXIgcmVnZXhwTWF0Y2hBbGxQb2x5ZmlsbCA9IHJlcXVpcmUoJy4vcG9seWZpbGwtcmVnZXhwLW1hdGNoYWxsJyk7XG5cbnZhciBnZXRNYXRjaGVyID0gZnVuY3Rpb24gZ2V0TWF0Y2hlcihyZWdleHApIHsgLy8gZXNsaW50LWRpc2FibGUtbGluZSBjb25zaXN0ZW50LXJldHVyblxuXHR2YXIgbWF0Y2hlclBvbHlmaWxsID0gcmVnZXhwTWF0Y2hBbGxQb2x5ZmlsbCgpO1xuXHRpZiAoaGFzU3ltYm9scyAmJiB0eXBlb2YgU3ltYm9sLm1hdGNoQWxsID09PSAnc3ltYm9sJykge1xuXHRcdHZhciBtYXRjaGVyID0gR2V0TWV0aG9kKHJlZ2V4cCwgU3ltYm9sLm1hdGNoQWxsKTtcblx0XHRpZiAobWF0Y2hlciA9PT0gUmVnRXhwLnByb3RvdHlwZVtTeW1ib2wubWF0Y2hBbGxdICYmIG1hdGNoZXIgIT09IG1hdGNoZXJQb2x5ZmlsbCkge1xuXHRcdFx0cmV0dXJuIG1hdGNoZXJQb2x5ZmlsbDtcblx0XHR9XG5cdFx0cmV0dXJuIG1hdGNoZXI7XG5cdH1cblx0Ly8gZmFsbGJhY2sgZm9yIHByZS1TeW1ib2wubWF0Y2hBbGwgZW52aXJvbm1lbnRzXG5cdGlmIChJc1JlZ0V4cChyZWdleHApKSB7XG5cdFx0cmV0dXJuIG1hdGNoZXJQb2x5ZmlsbDtcblx0fVxufTtcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBtYXRjaEFsbChyZWdleHApIHtcblx0dmFyIE8gPSBSZXF1aXJlT2JqZWN0Q29lcmNpYmxlKHRoaXMpO1xuXG5cdGlmICh0eXBlb2YgcmVnZXhwICE9PSAndW5kZWZpbmVkJyAmJiByZWdleHAgIT09IG51bGwpIHtcblx0XHR2YXIgaXNSZWdFeHAgPSBJc1JlZ0V4cChyZWdleHApO1xuXHRcdGlmIChpc1JlZ0V4cCkge1xuXHRcdFx0Ly8gd29ya2Fyb3VuZCBmb3Igb2xkZXIgZW5naW5lcyB0aGF0IGxhY2sgUmVnRXhwLnByb3RvdHlwZS5mbGFnc1xuXHRcdFx0dmFyIGZsYWdzID0gJ2ZsYWdzJyBpbiByZWdleHAgPyBHZXQocmVnZXhwLCAnZmxhZ3MnKSA6IGZsYWdzR2V0dGVyKHJlZ2V4cCk7XG5cdFx0XHRSZXF1aXJlT2JqZWN0Q29lcmNpYmxlKGZsYWdzKTtcblx0XHRcdGlmICgkaW5kZXhPZihUb1N0cmluZyhmbGFncyksICdnJykgPCAwKSB7XG5cdFx0XHRcdHRocm93IG5ldyBUeXBlRXJyb3IoJ21hdGNoQWxsIHJlcXVpcmVzIGEgZ2xvYmFsIHJlZ3VsYXIgZXhwcmVzc2lvbicpO1xuXHRcdFx0fVxuXHRcdH1cblxuXHRcdHZhciBtYXRjaGVyID0gZ2V0TWF0Y2hlcihyZWdleHApO1xuXHRcdGlmICh0eXBlb2YgbWF0Y2hlciAhPT0gJ3VuZGVmaW5lZCcpIHtcblx0XHRcdHJldHVybiBDYWxsKG1hdGNoZXIsIHJlZ2V4cCwgW09dKTtcblx0XHR9XG5cdH1cblxuXHR2YXIgUyA9IFRvU3RyaW5nKE8pO1xuXHQvLyB2YXIgcnggPSBSZWdFeHBDcmVhdGUocmVnZXhwLCAnZycpO1xuXHR2YXIgcnggPSBuZXcgUmVnRXhwKHJlZ2V4cCwgJ2cnKTtcblx0cmV0dXJuIENhbGwoZ2V0TWF0Y2hlcihyeCksIHJ4LCBbU10pO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///9505\n")},4956:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar callBind = __webpack_require__(5559);\nvar define = __webpack_require__(4289);\n\nvar implementation = __webpack_require__(9505);\nvar getPolyfill = __webpack_require__(3447);\nvar shim = __webpack_require__(2376);\n\nvar boundMatchAll = callBind(implementation);\n\ndefine(boundMatchAll, {\n\tgetPolyfill: getPolyfill,\n\timplementation: implementation,\n\tshim: shim\n});\n\nmodule.exports = boundMatchAll;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDk1Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixlQUFlLG1CQUFPLENBQUMsSUFBVztBQUNsQyxhQUFhLG1CQUFPLENBQUMsSUFBbUI7O0FBRXhDLHFCQUFxQixtQkFBTyxDQUFDLElBQWtCO0FBQy9DLGtCQUFrQixtQkFBTyxDQUFDLElBQVk7QUFDdEMsV0FBVyxtQkFBTyxDQUFDLElBQVE7O0FBRTNCOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsQ0FBQzs7QUFFRCIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvc3RyaW5nLnByb3RvdHlwZS5tYXRjaGFsbC9pbmRleC5qcz9iMWNjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGNhbGxCaW5kID0gcmVxdWlyZSgnY2FsbC1iaW5kJyk7XG52YXIgZGVmaW5lID0gcmVxdWlyZSgnZGVmaW5lLXByb3BlcnRpZXMnKTtcblxudmFyIGltcGxlbWVudGF0aW9uID0gcmVxdWlyZSgnLi9pbXBsZW1lbnRhdGlvbicpO1xudmFyIGdldFBvbHlmaWxsID0gcmVxdWlyZSgnLi9wb2x5ZmlsbCcpO1xudmFyIHNoaW0gPSByZXF1aXJlKCcuL3NoaW0nKTtcblxudmFyIGJvdW5kTWF0Y2hBbGwgPSBjYWxsQmluZChpbXBsZW1lbnRhdGlvbik7XG5cbmRlZmluZShib3VuZE1hdGNoQWxsLCB7XG5cdGdldFBvbHlmaWxsOiBnZXRQb2x5ZmlsbCxcblx0aW1wbGVtZW50YXRpb246IGltcGxlbWVudGF0aW9uLFxuXHRzaGltOiBzaGltXG59KTtcblxubW9kdWxlLmV4cG9ydHMgPSBib3VuZE1hdGNoQWxsO1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///4956\n")},6966:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar hasSymbols = __webpack_require__(1405)();\nvar regexpMatchAll = __webpack_require__(7201);\n\nmodule.exports = function getRegExpMatchAllPolyfill() {\n\tif (!hasSymbols || typeof Symbol.matchAll !== 'symbol' || typeof RegExp.prototype[Symbol.matchAll] !== 'function') {\n\t\treturn regexpMatchAll;\n\t}\n\treturn RegExp.prototype[Symbol.matchAll];\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNjk2Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixpQkFBaUIsbUJBQU8sQ0FBQyxJQUFhO0FBQ3RDLHFCQUFxQixtQkFBTyxDQUFDLElBQW1COztBQUVoRDtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL3N0cmluZy5wcm90b3R5cGUubWF0Y2hhbGwvcG9seWZpbGwtcmVnZXhwLW1hdGNoYWxsLmpzPzZjMTgiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgaGFzU3ltYm9scyA9IHJlcXVpcmUoJ2hhcy1zeW1ib2xzJykoKTtcbnZhciByZWdleHBNYXRjaEFsbCA9IHJlcXVpcmUoJy4vcmVnZXhwLW1hdGNoYWxsJyk7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gZ2V0UmVnRXhwTWF0Y2hBbGxQb2x5ZmlsbCgpIHtcblx0aWYgKCFoYXNTeW1ib2xzIHx8IHR5cGVvZiBTeW1ib2wubWF0Y2hBbGwgIT09ICdzeW1ib2wnIHx8IHR5cGVvZiBSZWdFeHAucHJvdG90eXBlW1N5bWJvbC5tYXRjaEFsbF0gIT09ICdmdW5jdGlvbicpIHtcblx0XHRyZXR1cm4gcmVnZXhwTWF0Y2hBbGw7XG5cdH1cblx0cmV0dXJuIFJlZ0V4cC5wcm90b3R5cGVbU3ltYm9sLm1hdGNoQWxsXTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///6966\n")},3447:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar implementation = __webpack_require__(9505);\n\nmodule.exports = function getPolyfill() {\n\tif (String.prototype.matchAll) {\n\t\ttry {\n\t\t\t''.matchAll(RegExp.prototype);\n\t\t} catch (e) {\n\t\t\treturn String.prototype.matchAll;\n\t\t}\n\t}\n\treturn implementation;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzQ0Ny5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixxQkFBcUIsbUJBQU8sQ0FBQyxJQUFrQjs7QUFFL0M7QUFDQTtBQUNBO0FBQ0E7QUFDQSxJQUFJO0FBQ0o7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvc3RyaW5nLnByb3RvdHlwZS5tYXRjaGFsbC9wb2x5ZmlsbC5qcz9iOGExIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGltcGxlbWVudGF0aW9uID0gcmVxdWlyZSgnLi9pbXBsZW1lbnRhdGlvbicpO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIGdldFBvbHlmaWxsKCkge1xuXHRpZiAoU3RyaW5nLnByb3RvdHlwZS5tYXRjaEFsbCkge1xuXHRcdHRyeSB7XG5cdFx0XHQnJy5tYXRjaEFsbChSZWdFeHAucHJvdG90eXBlKTtcblx0XHR9IGNhdGNoIChlKSB7XG5cdFx0XHRyZXR1cm4gU3RyaW5nLnByb3RvdHlwZS5tYXRjaEFsbDtcblx0XHR9XG5cdH1cblx0cmV0dXJuIGltcGxlbWVudGF0aW9uO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///3447\n")},7201:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\n// var Construct = require('es-abstract/2021/Construct');\nvar CreateRegExpStringIterator = __webpack_require__(3937);\nvar Get = __webpack_require__(1391);\nvar Set = __webpack_require__(105);\nvar SpeciesConstructor = __webpack_require__(9655);\nvar ToLength = __webpack_require__(8305);\nvar ToString = __webpack_require__(6846);\nvar Type = __webpack_require__(3633);\nvar flagsGetter = __webpack_require__(2847);\n\nvar OrigRegExp = RegExp;\n\nvar supportsConstructingWithFlags = 'flags' in RegExp.prototype;\n\nvar constructRegexWithFlags = function constructRegex(C, R) {\n\tvar matcher;\n\t// workaround for older engines that lack RegExp.prototype.flags\n\tvar flags = 'flags' in R ? Get(R, 'flags') : ToString(flagsGetter(R));\n\tif (supportsConstructingWithFlags && typeof flags === 'string') {\n\t\tmatcher = new C(R, flags);\n\t} else if (C === OrigRegExp) {\n\t\t// workaround for older engines that can not construct a RegExp with flags\n\t\tmatcher = new C(R.source, flags);\n\t} else {\n\t\tmatcher = new C(R, flags);\n\t}\n\treturn { flags: flags, matcher: matcher };\n};\n\nvar regexMatchAll = function SymbolMatchAll(string) {\n\tvar R = this;\n\tif (Type(R) !== 'Object') {\n\t\tthrow new TypeError('\"this\" value must be an Object');\n\t}\n\tvar S = ToString(string);\n\tvar C = SpeciesConstructor(R, OrigRegExp);\n\n\tvar tmp = constructRegexWithFlags(C, R);\n\t// var flags = ToString(Get(R, 'flags'));\n\tvar flags = tmp.flags;\n\t// var matcher = Construct(C, [R, flags]);\n\tvar matcher = tmp.matcher;\n\n\tvar lastIndex = ToLength(Get(R, 'lastIndex'));\n\tSet(matcher, 'lastIndex', lastIndex, true);\n\tvar global = flags.indexOf('g') > -1;\n\tvar fullUnicode = flags.indexOf('u') > -1;\n\treturn CreateRegExpStringIterator(matcher, S, global, fullUnicode);\n};\n\nvar defineP = Object.defineProperty;\nvar gOPD = Object.getOwnPropertyDescriptor;\n\nif (defineP && gOPD) {\n\tvar desc = gOPD(regexMatchAll, 'name');\n\tif (desc && desc.configurable) {\n\t\tdefineP(regexMatchAll, 'name', { value: '[Symbol.matchAll]' });\n\t}\n}\n\nmodule.exports = regexMatchAll;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzIwMS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBLGlDQUFpQyxtQkFBTyxDQUFDLElBQTZDO0FBQ3RGLFVBQVUsbUJBQU8sQ0FBQyxJQUFzQjtBQUN4QyxVQUFVLG1CQUFPLENBQUMsR0FBc0I7QUFDeEMseUJBQXlCLG1CQUFPLENBQUMsSUFBcUM7QUFDdEUsZUFBZSxtQkFBTyxDQUFDLElBQTJCO0FBQ2xELGVBQWUsbUJBQU8sQ0FBQyxJQUEyQjtBQUNsRCxXQUFXLG1CQUFPLENBQUMsSUFBdUI7QUFDMUMsa0JBQWtCLG1CQUFPLENBQUMsSUFBd0I7O0FBRWxEOztBQUVBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEdBQUc7QUFDSDtBQUNBO0FBQ0EsR0FBRztBQUNIO0FBQ0E7QUFDQSxVQUFVO0FBQ1Y7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxtQ0FBbUMsNEJBQTRCO0FBQy9EO0FBQ0E7O0FBRUEiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL3N0cmluZy5wcm90b3R5cGUubWF0Y2hhbGwvcmVnZXhwLW1hdGNoYWxsLmpzP2ZhODkiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG4vLyB2YXIgQ29uc3RydWN0ID0gcmVxdWlyZSgnZXMtYWJzdHJhY3QvMjAyMS9Db25zdHJ1Y3QnKTtcbnZhciBDcmVhdGVSZWdFeHBTdHJpbmdJdGVyYXRvciA9IHJlcXVpcmUoJ2VzLWFic3RyYWN0LzIwMjEvQ3JlYXRlUmVnRXhwU3RyaW5nSXRlcmF0b3InKTtcbnZhciBHZXQgPSByZXF1aXJlKCdlcy1hYnN0cmFjdC8yMDIxL0dldCcpO1xudmFyIFNldCA9IHJlcXVpcmUoJ2VzLWFic3RyYWN0LzIwMjEvU2V0Jyk7XG52YXIgU3BlY2llc0NvbnN0cnVjdG9yID0gcmVxdWlyZSgnZXMtYWJzdHJhY3QvMjAyMS9TcGVjaWVzQ29uc3RydWN0b3InKTtcbnZhciBUb0xlbmd0aCA9IHJlcXVpcmUoJ2VzLWFic3RyYWN0LzIwMjEvVG9MZW5ndGgnKTtcbnZhciBUb1N0cmluZyA9IHJlcXVpcmUoJ2VzLWFic3RyYWN0LzIwMjEvVG9TdHJpbmcnKTtcbnZhciBUeXBlID0gcmVxdWlyZSgnZXMtYWJzdHJhY3QvMjAyMS9UeXBlJyk7XG52YXIgZmxhZ3NHZXR0ZXIgPSByZXF1aXJlKCdyZWdleHAucHJvdG90eXBlLmZsYWdzJyk7XG5cbnZhciBPcmlnUmVnRXhwID0gUmVnRXhwO1xuXG52YXIgc3VwcG9ydHNDb25zdHJ1Y3RpbmdXaXRoRmxhZ3MgPSAnZmxhZ3MnIGluIFJlZ0V4cC5wcm90b3R5cGU7XG5cbnZhciBjb25zdHJ1Y3RSZWdleFdpdGhGbGFncyA9IGZ1bmN0aW9uIGNvbnN0cnVjdFJlZ2V4KEMsIFIpIHtcblx0dmFyIG1hdGNoZXI7XG5cdC8vIHdvcmthcm91bmQgZm9yIG9sZGVyIGVuZ2luZXMgdGhhdCBsYWNrIFJlZ0V4cC5wcm90b3R5cGUuZmxhZ3Ncblx0dmFyIGZsYWdzID0gJ2ZsYWdzJyBpbiBSID8gR2V0KFIsICdmbGFncycpIDogVG9TdHJpbmcoZmxhZ3NHZXR0ZXIoUikpO1xuXHRpZiAoc3VwcG9ydHNDb25zdHJ1Y3RpbmdXaXRoRmxhZ3MgJiYgdHlwZW9mIGZsYWdzID09PSAnc3RyaW5nJykge1xuXHRcdG1hdGNoZXIgPSBuZXcgQyhSLCBmbGFncyk7XG5cdH0gZWxzZSBpZiAoQyA9PT0gT3JpZ1JlZ0V4cCkge1xuXHRcdC8vIHdvcmthcm91bmQgZm9yIG9sZGVyIGVuZ2luZXMgdGhhdCBjYW4gbm90IGNvbnN0cnVjdCBhIFJlZ0V4cCB3aXRoIGZsYWdzXG5cdFx0bWF0Y2hlciA9IG5ldyBDKFIuc291cmNlLCBmbGFncyk7XG5cdH0gZWxzZSB7XG5cdFx0bWF0Y2hlciA9IG5ldyBDKFIsIGZsYWdzKTtcblx0fVxuXHRyZXR1cm4geyBmbGFnczogZmxhZ3MsIG1hdGNoZXI6IG1hdGNoZXIgfTtcbn07XG5cbnZhciByZWdleE1hdGNoQWxsID0gZnVuY3Rpb24gU3ltYm9sTWF0Y2hBbGwoc3RyaW5nKSB7XG5cdHZhciBSID0gdGhpcztcblx0aWYgKFR5cGUoUikgIT09ICdPYmplY3QnKSB7XG5cdFx0dGhyb3cgbmV3IFR5cGVFcnJvcignXCJ0aGlzXCIgdmFsdWUgbXVzdCBiZSBhbiBPYmplY3QnKTtcblx0fVxuXHR2YXIgUyA9IFRvU3RyaW5nKHN0cmluZyk7XG5cdHZhciBDID0gU3BlY2llc0NvbnN0cnVjdG9yKFIsIE9yaWdSZWdFeHApO1xuXG5cdHZhciB0bXAgPSBjb25zdHJ1Y3RSZWdleFdpdGhGbGFncyhDLCBSKTtcblx0Ly8gdmFyIGZsYWdzID0gVG9TdHJpbmcoR2V0KFIsICdmbGFncycpKTtcblx0dmFyIGZsYWdzID0gdG1wLmZsYWdzO1xuXHQvLyB2YXIgbWF0Y2hlciA9IENvbnN0cnVjdChDLCBbUiwgZmxhZ3NdKTtcblx0dmFyIG1hdGNoZXIgPSB0bXAubWF0Y2hlcjtcblxuXHR2YXIgbGFzdEluZGV4ID0gVG9MZW5ndGgoR2V0KFIsICdsYXN0SW5kZXgnKSk7XG5cdFNldChtYXRjaGVyLCAnbGFzdEluZGV4JywgbGFzdEluZGV4LCB0cnVlKTtcblx0dmFyIGdsb2JhbCA9IGZsYWdzLmluZGV4T2YoJ2cnKSA+IC0xO1xuXHR2YXIgZnVsbFVuaWNvZGUgPSBmbGFncy5pbmRleE9mKCd1JykgPiAtMTtcblx0cmV0dXJuIENyZWF0ZVJlZ0V4cFN0cmluZ0l0ZXJhdG9yKG1hdGNoZXIsIFMsIGdsb2JhbCwgZnVsbFVuaWNvZGUpO1xufTtcblxudmFyIGRlZmluZVAgPSBPYmplY3QuZGVmaW5lUHJvcGVydHk7XG52YXIgZ09QRCA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3I7XG5cbmlmIChkZWZpbmVQICYmIGdPUEQpIHtcblx0dmFyIGRlc2MgPSBnT1BEKHJlZ2V4TWF0Y2hBbGwsICduYW1lJyk7XG5cdGlmIChkZXNjICYmIGRlc2MuY29uZmlndXJhYmxlKSB7XG5cdFx0ZGVmaW5lUChyZWdleE1hdGNoQWxsLCAnbmFtZScsIHsgdmFsdWU6ICdbU3ltYm9sLm1hdGNoQWxsXScgfSk7XG5cdH1cbn1cblxubW9kdWxlLmV4cG9ydHMgPSByZWdleE1hdGNoQWxsO1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///7201\n")},2376:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar define = __webpack_require__(4289);\nvar hasSymbols = __webpack_require__(1405)();\nvar getPolyfill = __webpack_require__(3447);\nvar regexpMatchAllPolyfill = __webpack_require__(6966);\n\nvar defineP = Object.defineProperty;\nvar gOPD = Object.getOwnPropertyDescriptor;\n\nmodule.exports = function shimMatchAll() {\n\tvar polyfill = getPolyfill();\n\tdefine(\n\t\tString.prototype,\n\t\t{ matchAll: polyfill },\n\t\t{ matchAll: function () { return String.prototype.matchAll !== polyfill; } }\n\t);\n\tif (hasSymbols) {\n\t\t// eslint-disable-next-line no-restricted-properties\n\t\tvar symbol = Symbol.matchAll || (Symbol['for'] ? Symbol['for']('Symbol.matchAll') : Symbol('Symbol.matchAll'));\n\t\tdefine(\n\t\t\tSymbol,\n\t\t\t{ matchAll: symbol },\n\t\t\t{ matchAll: function () { return Symbol.matchAll !== symbol; } }\n\t\t);\n\n\t\tif (defineP && gOPD) {\n\t\t\tvar desc = gOPD(Symbol, symbol);\n\t\t\tif (!desc || desc.configurable) {\n\t\t\t\tdefineP(Symbol, symbol, {\n\t\t\t\t\tconfigurable: false,\n\t\t\t\t\tenumerable: false,\n\t\t\t\t\tvalue: symbol,\n\t\t\t\t\twritable: false\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tvar regexpMatchAll = regexpMatchAllPolyfill();\n\t\tvar func = {};\n\t\tfunc[symbol] = regexpMatchAll;\n\t\tvar predicate = {};\n\t\tpredicate[symbol] = function () {\n\t\t\treturn RegExp.prototype[symbol] !== regexpMatchAll;\n\t\t};\n\t\tdefine(RegExp.prototype, func, predicate);\n\t}\n\treturn polyfill;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjM3Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixhQUFhLG1CQUFPLENBQUMsSUFBbUI7QUFDeEMsaUJBQWlCLG1CQUFPLENBQUMsSUFBYTtBQUN0QyxrQkFBa0IsbUJBQU8sQ0FBQyxJQUFZO0FBQ3RDLDZCQUE2QixtQkFBTyxDQUFDLElBQTRCOztBQUVqRTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSSxvQkFBb0I7QUFDeEIsSUFBSSx3QkFBd0I7QUFDNUI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsS0FBSyxrQkFBa0I7QUFDdkIsS0FBSyx3QkFBd0I7QUFDN0I7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEtBQUs7QUFDTDtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL3N0cmluZy5wcm90b3R5cGUubWF0Y2hhbGwvc2hpbS5qcz85Yzg4Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGRlZmluZSA9IHJlcXVpcmUoJ2RlZmluZS1wcm9wZXJ0aWVzJyk7XG52YXIgaGFzU3ltYm9scyA9IHJlcXVpcmUoJ2hhcy1zeW1ib2xzJykoKTtcbnZhciBnZXRQb2x5ZmlsbCA9IHJlcXVpcmUoJy4vcG9seWZpbGwnKTtcbnZhciByZWdleHBNYXRjaEFsbFBvbHlmaWxsID0gcmVxdWlyZSgnLi9wb2x5ZmlsbC1yZWdleHAtbWF0Y2hhbGwnKTtcblxudmFyIGRlZmluZVAgPSBPYmplY3QuZGVmaW5lUHJvcGVydHk7XG52YXIgZ09QRCA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3I7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gc2hpbU1hdGNoQWxsKCkge1xuXHR2YXIgcG9seWZpbGwgPSBnZXRQb2x5ZmlsbCgpO1xuXHRkZWZpbmUoXG5cdFx0U3RyaW5nLnByb3RvdHlwZSxcblx0XHR7IG1hdGNoQWxsOiBwb2x5ZmlsbCB9LFxuXHRcdHsgbWF0Y2hBbGw6IGZ1bmN0aW9uICgpIHsgcmV0dXJuIFN0cmluZy5wcm90b3R5cGUubWF0Y2hBbGwgIT09IHBvbHlmaWxsOyB9IH1cblx0KTtcblx0aWYgKGhhc1N5bWJvbHMpIHtcblx0XHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tcmVzdHJpY3RlZC1wcm9wZXJ0aWVzXG5cdFx0dmFyIHN5bWJvbCA9IFN5bWJvbC5tYXRjaEFsbCB8fCAoU3ltYm9sWydmb3InXSA/IFN5bWJvbFsnZm9yJ10oJ1N5bWJvbC5tYXRjaEFsbCcpIDogU3ltYm9sKCdTeW1ib2wubWF0Y2hBbGwnKSk7XG5cdFx0ZGVmaW5lKFxuXHRcdFx0U3ltYm9sLFxuXHRcdFx0eyBtYXRjaEFsbDogc3ltYm9sIH0sXG5cdFx0XHR7IG1hdGNoQWxsOiBmdW5jdGlvbiAoKSB7IHJldHVybiBTeW1ib2wubWF0Y2hBbGwgIT09IHN5bWJvbDsgfSB9XG5cdFx0KTtcblxuXHRcdGlmIChkZWZpbmVQICYmIGdPUEQpIHtcblx0XHRcdHZhciBkZXNjID0gZ09QRChTeW1ib2wsIHN5bWJvbCk7XG5cdFx0XHRpZiAoIWRlc2MgfHwgZGVzYy5jb25maWd1cmFibGUpIHtcblx0XHRcdFx0ZGVmaW5lUChTeW1ib2wsIHN5bWJvbCwge1xuXHRcdFx0XHRcdGNvbmZpZ3VyYWJsZTogZmFsc2UsXG5cdFx0XHRcdFx0ZW51bWVyYWJsZTogZmFsc2UsXG5cdFx0XHRcdFx0dmFsdWU6IHN5bWJvbCxcblx0XHRcdFx0XHR3cml0YWJsZTogZmFsc2Vcblx0XHRcdFx0fSk7XG5cdFx0XHR9XG5cdFx0fVxuXG5cdFx0dmFyIHJlZ2V4cE1hdGNoQWxsID0gcmVnZXhwTWF0Y2hBbGxQb2x5ZmlsbCgpO1xuXHRcdHZhciBmdW5jID0ge307XG5cdFx0ZnVuY1tzeW1ib2xdID0gcmVnZXhwTWF0Y2hBbGw7XG5cdFx0dmFyIHByZWRpY2F0ZSA9IHt9O1xuXHRcdHByZWRpY2F0ZVtzeW1ib2xdID0gZnVuY3Rpb24gKCkge1xuXHRcdFx0cmV0dXJuIFJlZ0V4cC5wcm90b3R5cGVbc3ltYm9sXSAhPT0gcmVnZXhwTWF0Y2hBbGw7XG5cdFx0fTtcblx0XHRkZWZpbmUoUmVnRXhwLnByb3RvdHlwZSwgZnVuYywgcHJlZGljYXRlKTtcblx0fVxuXHRyZXR1cm4gcG9seWZpbGw7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///2376\n")},4654:function(){},4766:function(module){eval('!function(t,e){ true?module.exports=e():0}(self,(function(){return(()=>{var t={426:(t,e,n)=>{var r=n(529);function o(t,e,n){Array.isArray(t)?t.push(e):t[n]=e}t.exports=function(t){var e,n,i,u=[];if(Array.isArray(t))n=[],e=t.length-1;else{if("object"!=typeof t||null===t)throw new TypeError("Expecting an Array or an Object, but `"+(null===t?"null":typeof t)+"` provided.");n={},i=Object.keys(t),e=i.length-1}return function n(c,a){var l,s,f,d;for(s=i?i[a]:a,Array.isArray(t[s])||(void 0===t[s]?t[s]=[]:t[s]=[t[s]]),l=0;l<t[s].length;l++)o((d=c,f=Array.isArray(d)?[].concat(d):r(d)),t[s][l],s),a>=e?u.push(f):n(f,a+1)}(n,0),u}},529:t=>{t.exports=function(){for(var t={},n=0;n<arguments.length;n++){var r=arguments[n];for(var o in r)e.call(r,o)&&(t[o]=r[o])}return t};var e=Object.prototype.hasOwnProperty}},e={};function n(r){var o=e[r];if(void 0!==o)return o.exports;var i=e[r]={exports:{}};return t[r](i,i.exports,n),i.exports}n.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var r in e)n.o(e,r)&&!n.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),n.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var r={};return(()=>{"use strict";n.r(r),n.d(r,{default:()=>X,getCssSelector:()=>Q});var t,e,o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol?"symbol":typeof t};function i(t){return null!=t&&"object"===(void 0===t?"undefined":o(t))&&1===t.nodeType&&"object"===o(t.style)&&"object"===o(t.ownerDocument)}function u(t="unknown problem",...e){console.warn(`CssSelectorGenerator: ${t}`,...e)}!function(t){t.NONE="none",t.DESCENDANT="descendant",t.CHILD="child"}(t||(t={})),function(t){t.id="id",t.class="class",t.tag="tag",t.attribute="attribute",t.nthchild="nthchild",t.nthoftype="nthoftype"}(e||(e={}));const c={selectors:[e.id,e.class,e.tag,e.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function a(t){return t instanceof RegExp}function l(t){return["string","function"].includes(typeof t)||a(t)}function s(t){return Array.isArray(t)?t.filter(l):[]}function f(t){const e=[Node.DOCUMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE,Node.ELEMENT_NODE];return function(t){return t instanceof Node}(t)&&e.includes(t.nodeType)}function d(t,e){if(f(t))return t.contains(e)||u("element root mismatch","Provided root does not contain the element. This will most likely result in producing a fallback selector using element\'s real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will nto work as intended."),t;const n=e.getRootNode({composed:!1});return f(n)?(n!==document&&u("shadow root inferred","You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended."),n):e.ownerDocument.querySelector(":root")}function p(t){return"number"==typeof t?t:Number.POSITIVE_INFINITY}function m(t=[]){const[e=[],...n]=t;return 0===n.length?e:n.reduce(((t,e)=>t.filter((t=>e.includes(t)))),e)}function h(t){return[].concat(...t)}function y(t){const e=t.map((t=>{if(a(t))return e=>t.test(e);if("function"==typeof t)return e=>{const n=t(e);return"boolean"!=typeof n?(u("pattern matcher function invalid","Provided pattern matching function does not return boolean. It\'s result will be ignored.",t),!1):n};if("string"==typeof t){const e=new RegExp("^"+t.replace(/[|\\\\{}()[\\]^$+?.]/g,"\\\\$&").replace(/\\*/g,".+")+"$");return t=>e.test(t)}return u("pattern matcher invalid","Pattern matching only accepts strings, regular expressions and/or functions. This item is invalid and will be ignored.",t),()=>!1}));return t=>e.some((e=>e(t)))}function g(t,e,n){const r=Array.from(d(n,t[0]).querySelectorAll(e));return r.length===t.length&&t.every((t=>r.includes(t)))}function b(t,e){e=null!=e?e:function(t){return t.ownerDocument.querySelector(":root")}(t);const n=[];let r=t;for(;i(r)&&r!==e;)n.push(r),r=r.parentElement;return n}function v(t,e){return m(t.map((t=>b(t,e))))}const N={[t.NONE]:{type:t.NONE,value:""},[t.DESCENDANT]:{type:t.DESCENDANT,value:" > "},[t.CHILD]:{type:t.CHILD,value:" "}},S=new RegExp(["^$","\\\\s","^\\\\d"].join("|")),E=new RegExp(["^$","^\\\\d"].join("|")),w=[e.nthoftype,e.tag,e.id,e.class,e.attribute,e.nthchild];var x=n(426),A=n.n(x);const C=y(["class","id","ng-*"]);function O({nodeName:t}){return`[${t}]`}function T({nodeName:t,nodeValue:e}){return`[${t}=\'${Y(e)}\']`}function I({nodeName:t}){return!C(t)}function j(t){const e=Array.from(t.attributes).filter(I);return[...e.map(O),...e.map(T)]}function D(t){return(t.getAttribute("class")||"").trim().split(/\\s+/).filter((t=>!E.test(t))).map((t=>`.${Y(t)}`))}function $(t){const e=t.getAttribute("id")||"",n=`#${Y(e)}`,r=t.getRootNode({composed:!1});return!S.test(e)&&g([t],n,r)?[n]:[]}function P(t){const e=t.parentNode;if(e){const n=Array.from(e.childNodes).filter(i).indexOf(t);if(n>-1)return[`:nth-child(${n+1})`]}return[]}function R(t){return[Y(t.tagName.toLowerCase())]}function _(t){const e=[...new Set(h(t.map(R)))];return 0===e.length||e.length>1?[]:[e[0]]}function k(t){const e=_([t])[0],n=t.parentElement;if(n){const r=Array.from(n.children).filter((t=>t.tagName.toLowerCase()===e)).indexOf(t);if(r>-1)return[`${e}:nth-of-type(${r+1})`]}return[]}function M(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){const n=[];let r=0,o=q(1);for(;o.length<=t.length&&r<e;)r+=1,n.push(o.map((e=>t[e]))),o=L(o,t.length-1);return n}function L(t=[],e=0){const n=t.length;if(0===n)return[];const r=[...t];r[n-1]+=1;for(let t=n-1;t>=0;t--)if(r[t]>e){if(0===t)return q(n+1);r[t-1]++,r[t]=r[t-1]+1}return r[n-1]>e?q(n+1):r}function q(t=1){return Array.from(Array(t).keys())}const F=":".charCodeAt(0).toString(16).toUpperCase(),V=/[ !"#$%&\'()\\[\\]{|}<>*+,./;=?@^`~\\\\]/;function Y(t=""){var e,n;return null!==(n=null===(e=null===CSS||void 0===CSS?void 0:CSS.escape)||void 0===e?void 0:e.call(CSS,t))&&void 0!==n?n:function(t=""){return t.split("").map((t=>":"===t?`\\\\${F} `:V.test(t)?`\\\\${t}`:escape(t).replace(/%/g,"\\\\"))).join("")}(t)}const B={tag:_,id:function(t){return 0===t.length||t.length>1?[]:$(t[0])},class:function(t){return m(t.map(D))},attribute:function(t){return m(t.map(j))},nthchild:function(t){return m(t.map(P))},nthoftype:function(t){return m(t.map(k))}},G={tag:R,id:$,class:D,attribute:j,nthchild:P,nthoftype:k};function W(t){return t.includes(e.tag)||t.includes(e.nthoftype)?[...t]:[...t,e.tag]}function H(t={}){const n=[...w];return t[e.tag]&&t[e.nthoftype]&&n.splice(n.indexOf(e.tag),1),n.map((e=>{return(r=t)[n=e]?r[n].join(""):"";var n,r})).join("")}function U(t,e,n="",r){const o=function(t,e){return""===e?t:function(t,e){return[...t.map((t=>e+" "+t)),...t.map((t=>e+" > "+t))]}(t,e)}(function(t,e,n){const r=h(function(t,e){return function(t){const{selectors:e,combineBetweenSelectors:n,includeTag:r,maxCandidates:o}=t,i=n?M(e,{maxResults:o}):e.map((t=>[t]));return r?i.map(W):i}(e).map((e=>function(t,e){const n={};return t.forEach((t=>{const r=e[t];r.length>0&&(n[t]=r)})),A()(n).map(H)}(e,t))).filter((t=>t.length>0))}(function(t,e){const{blacklist:n,whitelist:r,combineWithinSelector:o,maxCombinations:i}=e,u=y(n),c=y(r);return function(t){const{selectors:e,includeTag:n}=t,r=[].concat(e);return n&&!r.includes("tag")&&r.push("tag"),r}(e).reduce(((e,n)=>{const r=function(t=[],e){return t.sort(((t,n)=>{const r=e(t),o=e(n);return r&&!o?-1:!r&&o?1:0}))}(function(t=[],e,n){return t.filter((t=>n(t)||!e(t)))}(function(t,e){var n;return(null!==(n=B[e])&&void 0!==n?n:()=>[])(t)}(t,n),u,c),c);return e[n]=o?M(r,{maxResults:i}):r.map((t=>[t])),e}),{})}(t,n),n));return[...new Set(r)]}(t,r.root,r),n);for(const e of o)if(g(t,e,r.root))return e;return null}function z(t){return{value:t,include:!1}}function J({selectors:t,operator:n}){let r=[...w];t[e.tag]&&t[e.nthoftype]&&(r=r.filter((t=>t!==e.tag)));let o="";return r.forEach((e=>{(t[e]||[]).forEach((({value:t,include:e})=>{e&&(o+=t)}))})),n.value+o}function K(n){return[":root",...b(n).reverse().map((n=>{const r=function(e,n,r=t.NONE){const o={};return n.forEach((t=>{Reflect.set(o,t,function(t,e){return G[e](t)}(e,t).map(z))})),{element:e,operator:N[r],selectors:o}}(n,[e.nthchild],t.DESCENDANT);return r.selectors.nthchild.forEach((t=>{t.include=!0})),r})).map(J)].join("")}function Q(t,n={}){const r=function(t){const e=(Array.isArray(t)?t:[t]).filter(i);return[...new Set(e)]}(t),o=function(t,n={}){const r=Object.assign(Object.assign({},c),n);return{selectors:(o=r.selectors,Array.isArray(o)?o.filter((t=>{return n=e,r=t,Object.values(n).includes(r);var n,r})):[]),whitelist:s(r.whitelist),blacklist:s(r.blacklist),root:d(r.root,t),combineWithinSelector:!!r.combineWithinSelector,combineBetweenSelectors:!!r.combineBetweenSelectors,includeTag:!!r.includeTag,maxCombinations:p(r.maxCombinations),maxCandidates:p(r.maxCandidates)};var o}(r[0],n);let u="",a=o.root;function l(){return function(t,e,n="",r){if(0===t.length)return null;const o=[t.length>1?t:[],...v(t,e).map((t=>[t]))];for(const t of o){const e=U(t,0,n,r);if(e)return{foundElements:t,selector:e}}return null}(r,a,u,o)}let f=l();for(;f;){const{foundElements:t,selector:e}=f;if(g(r,e,o.root))return e;a=t[0],u=e,f=l()}return r.length>1?r.map((t=>Q(t,o))).join(", "):function(t){return t.map(K).join(", ")}(r)}const X=Q})(),r})()}));//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDc2Ni5qcyIsIm1hcHBpbmdzIjoiQUFBQSxlQUFlLEtBQWlELG9CQUFvQixDQUF1SSxDQUFDLGtCQUFrQixZQUFZLE9BQU8sY0FBYyxhQUFhLGtCQUFrQixrQ0FBa0Msc0JBQXNCLGVBQWUsc0NBQXNDLEtBQUssdUlBQXVJLElBQUksK0JBQStCLHVCQUF1QixZQUFZLDRFQUE0RSxjQUFjLG9GQUFvRixTQUFTLFNBQVMscUJBQXFCLFlBQVksS0FBSyxtQkFBbUIsS0FBSyxtQkFBbUIsd0NBQXdDLFVBQVUsdUNBQXVDLE1BQU0sY0FBYyxXQUFXLCtCQUErQixZQUFZLFlBQVkscUNBQXFDLFFBQVEsMENBQTBDLGNBQWMsSUFBSSxJQUFJLGFBQWEsK0RBQStELHVCQUF1QixFQUFFLDhEQUE4RCw0RkFBNEYsZUFBZSx3Q0FBd0MsU0FBUyxHQUFHLFNBQVMsWUFBWSxhQUFhLGNBQWMsbUNBQW1DLEVBQUUsa0ZBQWtGLGdCQUFnQixhQUFhLCtFQUErRSxjQUFjLCtIQUErSCxxQ0FBcUMsc0NBQXNDLEVBQUUsUUFBUSxhQUFhLHdEQUF3RCxTQUFTLGVBQWUsNEdBQTRHLFNBQVMsR0FBRyxTQUFTLGtPQUFrTyxjQUFjLDJCQUEyQixjQUFjLHFEQUFxRCxjQUFjLHVDQUF1QyxjQUFjLDJFQUEyRSxtQkFBbUIseUJBQXlCLDRCQUE0QixnQkFBZ0Isc1RBQXNULHVCQUF1QixZQUFZLEVBQUUsdVZBQXVWLGNBQWMsb0RBQW9ELGlCQUFpQixtQkFBbUIsd0VBQXdFLGNBQWMsc0JBQXNCLGNBQWMsbUJBQW1CLDRCQUE0QixtQ0FBbUMsYUFBYSxxS0FBcUssdUJBQXVCLHdDQUF3QywrQ0FBK0Msb0JBQW9CLHNLQUFzSyxHQUFHLDRCQUE0QixrQkFBa0Isa0RBQWtELHdEQUF3RCxnQkFBZ0Isd0JBQXdCLDhDQUE4QyxJQUFJLFdBQVcsUUFBUSxLQUFLLFlBQVksNkJBQTZCLFNBQVMsZ0JBQWdCLDZCQUE2QixTQUFTLFVBQVUscUJBQXFCLGlCQUFpQiw4QkFBOEIsWUFBWSx3QkFBd0IsNklBQTZJLHNCQUFzQixpQ0FBaUMsWUFBWSxXQUFXLEVBQUUsVUFBVSxFQUFFLEdBQUcsWUFBWSx1QkFBdUIsRUFBRSxVQUFVLEVBQUUsSUFBSSxLQUFLLElBQUksWUFBWSxXQUFXLEVBQUUsWUFBWSxjQUFjLDJDQUEyQyxnQ0FBZ0MsY0FBYyw0RkFBNEYsS0FBSyxJQUFJLGNBQWMsdUNBQXVDLEtBQUssbUJBQW1CLFlBQVksRUFBRSxvQ0FBb0MsY0FBYyxxQkFBcUIsTUFBTSxzREFBc0QsNkJBQTZCLElBQUksSUFBSSxTQUFTLGNBQWMsbUNBQW1DLGNBQWMsa0NBQWtDLDBDQUEwQyxjQUFjLG9DQUFvQyxNQUFNLG1GQUFtRixrQkFBa0IsRUFBRSxlQUFlLElBQUksSUFBSSxTQUFTLGlCQUFpQixzQ0FBc0MsR0FBRyxFQUFFLFdBQVcsZUFBZSxLQUFLLHdCQUF3QixpREFBaUQsU0FBUyxxQkFBcUIsaUJBQWlCLGtCQUFrQixlQUFlLFVBQVUsY0FBYyxLQUFLLGVBQWUsdUJBQXVCLHVCQUF1Qix5QkFBeUIsZ0JBQWdCLG1DQUFtQyx3RUFBd0UsRUFBRSxRQUFRLFdBQVcsaUJBQWlCLFFBQVEsc0lBQXNJLHdDQUF3QyxHQUFHLGlCQUFpQixFQUFFLDBDQUEwQyxJQUFJLFNBQVMscUJBQXFCLDJDQUEyQyxtQkFBbUIsbUJBQW1CLHVCQUF1QixtQkFBbUIsc0JBQXNCLG1CQUFtQix1QkFBdUIsb0JBQW9CLElBQUksdURBQXVELGNBQWMsc0VBQXNFLGVBQWUsRUFBRSxlQUFlLHlFQUF5RSxrQ0FBa0MsUUFBUSxZQUFZLHVCQUF1QixzQkFBc0IsNkJBQTZCLHdEQUF3RCxNQUFNLGlCQUFpQix3QkFBd0IsbUJBQW1CLE1BQU0sbUVBQW1FLFlBQVksYUFBYSxrQkFBa0Isb0JBQW9CLDBCQUEwQixXQUFXLHNCQUFzQixhQUFhLHFCQUFxQixpQkFBaUIsZ0NBQWdDLGVBQWUsTUFBTSxrRUFBa0UsaUJBQWlCLG1CQUFtQixNQUFNLHlCQUF5QixrQkFBa0IsOENBQThDLG9CQUFvQix5QkFBeUIsdUJBQXVCLG9CQUFvQiwwQkFBMEIsR0FBRyxvQkFBb0Isa0NBQWtDLGVBQWUsTUFBTSxnREFBZ0QsY0FBYyxtQkFBbUIsYUFBYSxvQkFBb0IsSUFBSSxFQUFFLFVBQVUsc0JBQXNCLGdCQUFnQiwyQ0FBMkMsWUFBWSxjQUFjLE9BQU8sb0JBQW9CLFlBQVksdUJBQXVCLEVBQUUsYUFBYSx1REFBdUQsU0FBUyxzQkFBc0Isc0JBQXNCLGtCQUFrQixJQUFJLFVBQVUsR0FBRyxhQUFhLGNBQWMsMENBQTBDLCtCQUErQixXQUFXLHNCQUFzQiw4QkFBOEIsZUFBZSxjQUFjLElBQUkscUNBQXFDLDhCQUE4Qix5Q0FBeUMsYUFBYSxLQUFLLG9CQUFvQixpQkFBaUIsRUFBRSxvQkFBb0IsMkNBQTJDLHNCQUFzQixxQkFBcUIsRUFBRSxzQ0FBc0MsT0FBTyxPQUFPLHdEQUF3RCw0Q0FBNEMsUUFBUSwrUUFBK1EsTUFBTSxTQUFTLGtCQUFrQixhQUFhLDRCQUE0Qiw0QkFBNEIsa0RBQWtELGtCQUFrQixtQkFBbUIsWUFBWSw0QkFBNEIsWUFBWSxVQUFVLFVBQVUsS0FBSyxFQUFFLEVBQUUsTUFBTSwyQkFBMkIsR0FBRywwQkFBMEIsaUJBQWlCLDREQUE0RCwyQkFBMkIsSUFBSSxVQUFVLE1BQU0sSUFBSSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvY3NzLXNlbGVjdG9yLWdlbmVyYXRvci9idWlsZC9pbmRleC5qcz8xZTQyIl0sInNvdXJjZXNDb250ZW50IjpbIiFmdW5jdGlvbih0LGUpe1wib2JqZWN0XCI9PXR5cGVvZiBleHBvcnRzJiZcIm9iamVjdFwiPT10eXBlb2YgbW9kdWxlP21vZHVsZS5leHBvcnRzPWUoKTpcImZ1bmN0aW9uXCI9PXR5cGVvZiBkZWZpbmUmJmRlZmluZS5hbWQ/ZGVmaW5lKFtdLGUpOlwib2JqZWN0XCI9PXR5cGVvZiBleHBvcnRzP2V4cG9ydHMuQ3NzU2VsZWN0b3JHZW5lcmF0b3I9ZSgpOnQuQ3NzU2VsZWN0b3JHZW5lcmF0b3I9ZSgpfShzZWxmLChmdW5jdGlvbigpe3JldHVybigoKT0+e3ZhciB0PXs0MjY6KHQsZSxuKT0+e3ZhciByPW4oNTI5KTtmdW5jdGlvbiBvKHQsZSxuKXtBcnJheS5pc0FycmF5KHQpP3QucHVzaChlKTp0W25dPWV9dC5leHBvcnRzPWZ1bmN0aW9uKHQpe3ZhciBlLG4saSx1PVtdO2lmKEFycmF5LmlzQXJyYXkodCkpbj1bXSxlPXQubGVuZ3RoLTE7ZWxzZXtpZihcIm9iamVjdFwiIT10eXBlb2YgdHx8bnVsbD09PXQpdGhyb3cgbmV3IFR5cGVFcnJvcihcIkV4cGVjdGluZyBhbiBBcnJheSBvciBhbiBPYmplY3QsIGJ1dCBgXCIrKG51bGw9PT10P1wibnVsbFwiOnR5cGVvZiB0KStcImAgcHJvdmlkZWQuXCIpO249e30saT1PYmplY3Qua2V5cyh0KSxlPWkubGVuZ3RoLTF9cmV0dXJuIGZ1bmN0aW9uIG4oYyxhKXt2YXIgbCxzLGYsZDtmb3Iocz1pP2lbYV06YSxBcnJheS5pc0FycmF5KHRbc10pfHwodm9pZCAwPT09dFtzXT90W3NdPVtdOnRbc109W3Rbc11dKSxsPTA7bDx0W3NdLmxlbmd0aDtsKyspbygoZD1jLGY9QXJyYXkuaXNBcnJheShkKT9bXS5jb25jYXQoZCk6cihkKSksdFtzXVtsXSxzKSxhPj1lP3UucHVzaChmKTpuKGYsYSsxKX0obiwwKSx1fX0sNTI5OnQ9Pnt0LmV4cG9ydHM9ZnVuY3Rpb24oKXtmb3IodmFyIHQ9e30sbj0wO248YXJndW1lbnRzLmxlbmd0aDtuKyspe3ZhciByPWFyZ3VtZW50c1tuXTtmb3IodmFyIG8gaW4gcillLmNhbGwocixvKSYmKHRbb109cltvXSl9cmV0dXJuIHR9O3ZhciBlPU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHl9fSxlPXt9O2Z1bmN0aW9uIG4ocil7dmFyIG89ZVtyXTtpZih2b2lkIDAhPT1vKXJldHVybiBvLmV4cG9ydHM7dmFyIGk9ZVtyXT17ZXhwb3J0czp7fX07cmV0dXJuIHRbcl0oaSxpLmV4cG9ydHMsbiksaS5leHBvcnRzfW4ubj10PT57dmFyIGU9dCYmdC5fX2VzTW9kdWxlPygpPT50LmRlZmF1bHQ6KCk9PnQ7cmV0dXJuIG4uZChlLHthOmV9KSxlfSxuLmQ9KHQsZSk9Pntmb3IodmFyIHIgaW4gZSluLm8oZSxyKSYmIW4ubyh0LHIpJiZPYmplY3QuZGVmaW5lUHJvcGVydHkodCxyLHtlbnVtZXJhYmxlOiEwLGdldDplW3JdfSl9LG4ubz0odCxlKT0+T2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHQsZSksbi5yPXQ9PntcInVuZGVmaW5lZFwiIT10eXBlb2YgU3ltYm9sJiZTeW1ib2wudG9TdHJpbmdUYWcmJk9iamVjdC5kZWZpbmVQcm9wZXJ0eSh0LFN5bWJvbC50b1N0cmluZ1RhZyx7dmFsdWU6XCJNb2R1bGVcIn0pLE9iamVjdC5kZWZpbmVQcm9wZXJ0eSh0LFwiX19lc01vZHVsZVwiLHt2YWx1ZTohMH0pfTt2YXIgcj17fTtyZXR1cm4oKCk9PntcInVzZSBzdHJpY3RcIjtuLnIociksbi5kKHIse2RlZmF1bHQ6KCk9PlgsZ2V0Q3NzU2VsZWN0b3I6KCk9PlF9KTt2YXIgdCxlLG89XCJmdW5jdGlvblwiPT10eXBlb2YgU3ltYm9sJiZcInN5bWJvbFwiPT10eXBlb2YgU3ltYm9sLml0ZXJhdG9yP2Z1bmN0aW9uKHQpe3JldHVybiB0eXBlb2YgdH06ZnVuY3Rpb24odCl7cmV0dXJuIHQmJlwiZnVuY3Rpb25cIj09dHlwZW9mIFN5bWJvbCYmdC5jb25zdHJ1Y3Rvcj09PVN5bWJvbD9cInN5bWJvbFwiOnR5cGVvZiB0fTtmdW5jdGlvbiBpKHQpe3JldHVybiBudWxsIT10JiZcIm9iamVjdFwiPT09KHZvaWQgMD09PXQ/XCJ1bmRlZmluZWRcIjpvKHQpKSYmMT09PXQubm9kZVR5cGUmJlwib2JqZWN0XCI9PT1vKHQuc3R5bGUpJiZcIm9iamVjdFwiPT09byh0Lm93bmVyRG9jdW1lbnQpfWZ1bmN0aW9uIHUodD1cInVua25vd24gcHJvYmxlbVwiLC4uLmUpe2NvbnNvbGUud2FybihgQ3NzU2VsZWN0b3JHZW5lcmF0b3I6ICR7dH1gLC4uLmUpfSFmdW5jdGlvbih0KXt0Lk5PTkU9XCJub25lXCIsdC5ERVNDRU5EQU5UPVwiZGVzY2VuZGFudFwiLHQuQ0hJTEQ9XCJjaGlsZFwifSh0fHwodD17fSkpLGZ1bmN0aW9uKHQpe3QuaWQ9XCJpZFwiLHQuY2xhc3M9XCJjbGFzc1wiLHQudGFnPVwidGFnXCIsdC5hdHRyaWJ1dGU9XCJhdHRyaWJ1dGVcIix0Lm50aGNoaWxkPVwibnRoY2hpbGRcIix0Lm50aG9mdHlwZT1cIm50aG9mdHlwZVwifShlfHwoZT17fSkpO2NvbnN0IGM9e3NlbGVjdG9yczpbZS5pZCxlLmNsYXNzLGUudGFnLGUuYXR0cmlidXRlXSxpbmNsdWRlVGFnOiExLHdoaXRlbGlzdDpbXSxibGFja2xpc3Q6W10sY29tYmluZVdpdGhpblNlbGVjdG9yOiEwLGNvbWJpbmVCZXR3ZWVuU2VsZWN0b3JzOiEwLHJvb3Q6bnVsbCxtYXhDb21iaW5hdGlvbnM6TnVtYmVyLlBPU0lUSVZFX0lORklOSVRZLG1heENhbmRpZGF0ZXM6TnVtYmVyLlBPU0lUSVZFX0lORklOSVRZfTtmdW5jdGlvbiBhKHQpe3JldHVybiB0IGluc3RhbmNlb2YgUmVnRXhwfWZ1bmN0aW9uIGwodCl7cmV0dXJuW1wic3RyaW5nXCIsXCJmdW5jdGlvblwiXS5pbmNsdWRlcyh0eXBlb2YgdCl8fGEodCl9ZnVuY3Rpb24gcyh0KXtyZXR1cm4gQXJyYXkuaXNBcnJheSh0KT90LmZpbHRlcihsKTpbXX1mdW5jdGlvbiBmKHQpe2NvbnN0IGU9W05vZGUuRE9DVU1FTlRfTk9ERSxOb2RlLkRPQ1VNRU5UX0ZSQUdNRU5UX05PREUsTm9kZS5FTEVNRU5UX05PREVdO3JldHVybiBmdW5jdGlvbih0KXtyZXR1cm4gdCBpbnN0YW5jZW9mIE5vZGV9KHQpJiZlLmluY2x1ZGVzKHQubm9kZVR5cGUpfWZ1bmN0aW9uIGQodCxlKXtpZihmKHQpKXJldHVybiB0LmNvbnRhaW5zKGUpfHx1KFwiZWxlbWVudCByb290IG1pc21hdGNoXCIsXCJQcm92aWRlZCByb290IGRvZXMgbm90IGNvbnRhaW4gdGhlIGVsZW1lbnQuIFRoaXMgd2lsbCBtb3N0IGxpa2VseSByZXN1bHQgaW4gcHJvZHVjaW5nIGEgZmFsbGJhY2sgc2VsZWN0b3IgdXNpbmcgZWxlbWVudCdzIHJlYWwgcm9vdCBub2RlLiBJZiB5b3UgcGxhbiB0byB1c2UgdGhlIHNlbGVjdG9yIHVzaW5nIHByb3ZpZGVkIHJvb3QgKGUuZy4gYHJvb3QucXVlcnlTZWxlY3RvcmApLCBpdCB3aWxsIG50byB3b3JrIGFzIGludGVuZGVkLlwiKSx0O2NvbnN0IG49ZS5nZXRSb290Tm9kZSh7Y29tcG9zZWQ6ITF9KTtyZXR1cm4gZihuKT8obiE9PWRvY3VtZW50JiZ1KFwic2hhZG93IHJvb3QgaW5mZXJyZWRcIixcIllvdSBkaWQgbm90IHByb3ZpZGUgYSByb290IGFuZCB0aGUgZWxlbWVudCBpcyBhIGNoaWxkIG9mIFNoYWRvdyBET00uIFRoaXMgd2lsbCBwcm9kdWNlIGEgc2VsZWN0b3IgdXNpbmcgU2hhZG93Um9vdCBhcyBhIHJvb3QuIElmIHlvdSBwbGFuIHRvIHVzZSB0aGUgc2VsZWN0b3IgdXNpbmcgZG9jdW1lbnQgYXMgYSByb290IChlLmcuIGBkb2N1bWVudC5xdWVyeVNlbGVjdG9yYCksIGl0IHdpbGwgbm90IHdvcmsgYXMgaW50ZW5kZWQuXCIpLG4pOmUub3duZXJEb2N1bWVudC5xdWVyeVNlbGVjdG9yKFwiOnJvb3RcIil9ZnVuY3Rpb24gcCh0KXtyZXR1cm5cIm51bWJlclwiPT10eXBlb2YgdD90Ok51bWJlci5QT1NJVElWRV9JTkZJTklUWX1mdW5jdGlvbiBtKHQ9W10pe2NvbnN0W2U9W10sLi4ubl09dDtyZXR1cm4gMD09PW4ubGVuZ3RoP2U6bi5yZWR1Y2UoKCh0LGUpPT50LmZpbHRlcigodD0+ZS5pbmNsdWRlcyh0KSkpKSxlKX1mdW5jdGlvbiBoKHQpe3JldHVybltdLmNvbmNhdCguLi50KX1mdW5jdGlvbiB5KHQpe2NvbnN0IGU9dC5tYXAoKHQ9PntpZihhKHQpKXJldHVybiBlPT50LnRlc3QoZSk7aWYoXCJmdW5jdGlvblwiPT10eXBlb2YgdClyZXR1cm4gZT0+e2NvbnN0IG49dChlKTtyZXR1cm5cImJvb2xlYW5cIiE9dHlwZW9mIG4/KHUoXCJwYXR0ZXJuIG1hdGNoZXIgZnVuY3Rpb24gaW52YWxpZFwiLFwiUHJvdmlkZWQgcGF0dGVybiBtYXRjaGluZyBmdW5jdGlvbiBkb2VzIG5vdCByZXR1cm4gYm9vbGVhbi4gSXQncyByZXN1bHQgd2lsbCBiZSBpZ25vcmVkLlwiLHQpLCExKTpufTtpZihcInN0cmluZ1wiPT10eXBlb2YgdCl7Y29uc3QgZT1uZXcgUmVnRXhwKFwiXlwiK3QucmVwbGFjZSgvW3xcXFxce30oKVtcXF1eJCs/Ll0vZyxcIlxcXFwkJlwiKS5yZXBsYWNlKC9cXCovZyxcIi4rXCIpK1wiJFwiKTtyZXR1cm4gdD0+ZS50ZXN0KHQpfXJldHVybiB1KFwicGF0dGVybiBtYXRjaGVyIGludmFsaWRcIixcIlBhdHRlcm4gbWF0Y2hpbmcgb25seSBhY2NlcHRzIHN0cmluZ3MsIHJlZ3VsYXIgZXhwcmVzc2lvbnMgYW5kL29yIGZ1bmN0aW9ucy4gVGhpcyBpdGVtIGlzIGludmFsaWQgYW5kIHdpbGwgYmUgaWdub3JlZC5cIix0KSwoKT0+ITF9KSk7cmV0dXJuIHQ9PmUuc29tZSgoZT0+ZSh0KSkpfWZ1bmN0aW9uIGcodCxlLG4pe2NvbnN0IHI9QXJyYXkuZnJvbShkKG4sdFswXSkucXVlcnlTZWxlY3RvckFsbChlKSk7cmV0dXJuIHIubGVuZ3RoPT09dC5sZW5ndGgmJnQuZXZlcnkoKHQ9PnIuaW5jbHVkZXModCkpKX1mdW5jdGlvbiBiKHQsZSl7ZT1udWxsIT1lP2U6ZnVuY3Rpb24odCl7cmV0dXJuIHQub3duZXJEb2N1bWVudC5xdWVyeVNlbGVjdG9yKFwiOnJvb3RcIil9KHQpO2NvbnN0IG49W107bGV0IHI9dDtmb3IoO2kocikmJnIhPT1lOyluLnB1c2gocikscj1yLnBhcmVudEVsZW1lbnQ7cmV0dXJuIG59ZnVuY3Rpb24gdih0LGUpe3JldHVybiBtKHQubWFwKCh0PT5iKHQsZSkpKSl9Y29uc3QgTj17W3QuTk9ORV06e3R5cGU6dC5OT05FLHZhbHVlOlwiXCJ9LFt0LkRFU0NFTkRBTlRdOnt0eXBlOnQuREVTQ0VOREFOVCx2YWx1ZTpcIiA+IFwifSxbdC5DSElMRF06e3R5cGU6dC5DSElMRCx2YWx1ZTpcIiBcIn19LFM9bmV3IFJlZ0V4cChbXCJeJFwiLFwiXFxcXHNcIixcIl5cXFxcZFwiXS5qb2luKFwifFwiKSksRT1uZXcgUmVnRXhwKFtcIl4kXCIsXCJeXFxcXGRcIl0uam9pbihcInxcIikpLHc9W2UubnRob2Z0eXBlLGUudGFnLGUuaWQsZS5jbGFzcyxlLmF0dHJpYnV0ZSxlLm50aGNoaWxkXTt2YXIgeD1uKDQyNiksQT1uLm4oeCk7Y29uc3QgQz15KFtcImNsYXNzXCIsXCJpZFwiLFwibmctKlwiXSk7ZnVuY3Rpb24gTyh7bm9kZU5hbWU6dH0pe3JldHVybmBbJHt0fV1gfWZ1bmN0aW9uIFQoe25vZGVOYW1lOnQsbm9kZVZhbHVlOmV9KXtyZXR1cm5gWyR7dH09JyR7WShlKX0nXWB9ZnVuY3Rpb24gSSh7bm9kZU5hbWU6dH0pe3JldHVybiFDKHQpfWZ1bmN0aW9uIGoodCl7Y29uc3QgZT1BcnJheS5mcm9tKHQuYXR0cmlidXRlcykuZmlsdGVyKEkpO3JldHVyblsuLi5lLm1hcChPKSwuLi5lLm1hcChUKV19ZnVuY3Rpb24gRCh0KXtyZXR1cm4odC5nZXRBdHRyaWJ1dGUoXCJjbGFzc1wiKXx8XCJcIikudHJpbSgpLnNwbGl0KC9cXHMrLykuZmlsdGVyKCh0PT4hRS50ZXN0KHQpKSkubWFwKCh0PT5gLiR7WSh0KX1gKSl9ZnVuY3Rpb24gJCh0KXtjb25zdCBlPXQuZ2V0QXR0cmlidXRlKFwiaWRcIil8fFwiXCIsbj1gIyR7WShlKX1gLHI9dC5nZXRSb290Tm9kZSh7Y29tcG9zZWQ6ITF9KTtyZXR1cm4hUy50ZXN0KGUpJiZnKFt0XSxuLHIpP1tuXTpbXX1mdW5jdGlvbiBQKHQpe2NvbnN0IGU9dC5wYXJlbnROb2RlO2lmKGUpe2NvbnN0IG49QXJyYXkuZnJvbShlLmNoaWxkTm9kZXMpLmZpbHRlcihpKS5pbmRleE9mKHQpO2lmKG4+LTEpcmV0dXJuW2A6bnRoLWNoaWxkKCR7bisxfSlgXX1yZXR1cm5bXX1mdW5jdGlvbiBSKHQpe3JldHVybltZKHQudGFnTmFtZS50b0xvd2VyQ2FzZSgpKV19ZnVuY3Rpb24gXyh0KXtjb25zdCBlPVsuLi5uZXcgU2V0KGgodC5tYXAoUikpKV07cmV0dXJuIDA9PT1lLmxlbmd0aHx8ZS5sZW5ndGg+MT9bXTpbZVswXV19ZnVuY3Rpb24gayh0KXtjb25zdCBlPV8oW3RdKVswXSxuPXQucGFyZW50RWxlbWVudDtpZihuKXtjb25zdCByPUFycmF5LmZyb20obi5jaGlsZHJlbikuZmlsdGVyKCh0PT50LnRhZ05hbWUudG9Mb3dlckNhc2UoKT09PWUpKS5pbmRleE9mKHQpO2lmKHI+LTEpcmV0dXJuW2Ake2V9Om50aC1vZi10eXBlKCR7cisxfSlgXX1yZXR1cm5bXX1mdW5jdGlvbiBNKHQ9W10se21heFJlc3VsdHM6ZT1OdW1iZXIuUE9TSVRJVkVfSU5GSU5JVFl9PXt9KXtjb25zdCBuPVtdO2xldCByPTAsbz1xKDEpO2Zvcig7by5sZW5ndGg8PXQubGVuZ3RoJiZyPGU7KXIrPTEsbi5wdXNoKG8ubWFwKChlPT50W2VdKSkpLG89TChvLHQubGVuZ3RoLTEpO3JldHVybiBufWZ1bmN0aW9uIEwodD1bXSxlPTApe2NvbnN0IG49dC5sZW5ndGg7aWYoMD09PW4pcmV0dXJuW107Y29uc3Qgcj1bLi4udF07cltuLTFdKz0xO2ZvcihsZXQgdD1uLTE7dD49MDt0LS0paWYoclt0XT5lKXtpZigwPT09dClyZXR1cm4gcShuKzEpO3JbdC0xXSsrLHJbdF09clt0LTFdKzF9cmV0dXJuIHJbbi0xXT5lP3EobisxKTpyfWZ1bmN0aW9uIHEodD0xKXtyZXR1cm4gQXJyYXkuZnJvbShBcnJheSh0KS5rZXlzKCkpfWNvbnN0IEY9XCI6XCIuY2hhckNvZGVBdCgwKS50b1N0cmluZygxNikudG9VcHBlckNhc2UoKSxWPS9bICFcIiMkJSYnKClcXFtcXF17fH08PiorLC4vOz0/QF5gflxcXFxdLztmdW5jdGlvbiBZKHQ9XCJcIil7dmFyIGUsbjtyZXR1cm4gbnVsbCE9PShuPW51bGw9PT0oZT1udWxsPT09Q1NTfHx2b2lkIDA9PT1DU1M/dm9pZCAwOkNTUy5lc2NhcGUpfHx2b2lkIDA9PT1lP3ZvaWQgMDplLmNhbGwoQ1NTLHQpKSYmdm9pZCAwIT09bj9uOmZ1bmN0aW9uKHQ9XCJcIil7cmV0dXJuIHQuc3BsaXQoXCJcIikubWFwKCh0PT5cIjpcIj09PXQ/YFxcXFwke0Z9IGA6Vi50ZXN0KHQpP2BcXFxcJHt0fWA6ZXNjYXBlKHQpLnJlcGxhY2UoLyUvZyxcIlxcXFxcIikpKS5qb2luKFwiXCIpfSh0KX1jb25zdCBCPXt0YWc6XyxpZDpmdW5jdGlvbih0KXtyZXR1cm4gMD09PXQubGVuZ3RofHx0Lmxlbmd0aD4xP1tdOiQodFswXSl9LGNsYXNzOmZ1bmN0aW9uKHQpe3JldHVybiBtKHQubWFwKEQpKX0sYXR0cmlidXRlOmZ1bmN0aW9uKHQpe3JldHVybiBtKHQubWFwKGopKX0sbnRoY2hpbGQ6ZnVuY3Rpb24odCl7cmV0dXJuIG0odC5tYXAoUCkpfSxudGhvZnR5cGU6ZnVuY3Rpb24odCl7cmV0dXJuIG0odC5tYXAoaykpfX0sRz17dGFnOlIsaWQ6JCxjbGFzczpELGF0dHJpYnV0ZTpqLG50aGNoaWxkOlAsbnRob2Z0eXBlOmt9O2Z1bmN0aW9uIFcodCl7cmV0dXJuIHQuaW5jbHVkZXMoZS50YWcpfHx0LmluY2x1ZGVzKGUubnRob2Z0eXBlKT9bLi4udF06Wy4uLnQsZS50YWddfWZ1bmN0aW9uIEgodD17fSl7Y29uc3Qgbj1bLi4ud107cmV0dXJuIHRbZS50YWddJiZ0W2UubnRob2Z0eXBlXSYmbi5zcGxpY2Uobi5pbmRleE9mKGUudGFnKSwxKSxuLm1hcCgoZT0+e3JldHVybihyPXQpW249ZV0/cltuXS5qb2luKFwiXCIpOlwiXCI7dmFyIG4scn0pKS5qb2luKFwiXCIpfWZ1bmN0aW9uIFUodCxlLG49XCJcIixyKXtjb25zdCBvPWZ1bmN0aW9uKHQsZSl7cmV0dXJuXCJcIj09PWU/dDpmdW5jdGlvbih0LGUpe3JldHVyblsuLi50Lm1hcCgodD0+ZStcIiBcIit0KSksLi4udC5tYXAoKHQ9PmUrXCIgPiBcIit0KSldfSh0LGUpfShmdW5jdGlvbih0LGUsbil7Y29uc3Qgcj1oKGZ1bmN0aW9uKHQsZSl7cmV0dXJuIGZ1bmN0aW9uKHQpe2NvbnN0e3NlbGVjdG9yczplLGNvbWJpbmVCZXR3ZWVuU2VsZWN0b3JzOm4saW5jbHVkZVRhZzpyLG1heENhbmRpZGF0ZXM6b309dCxpPW4/TShlLHttYXhSZXN1bHRzOm99KTplLm1hcCgodD0+W3RdKSk7cmV0dXJuIHI/aS5tYXAoVyk6aX0oZSkubWFwKChlPT5mdW5jdGlvbih0LGUpe2NvbnN0IG49e307cmV0dXJuIHQuZm9yRWFjaCgodD0+e2NvbnN0IHI9ZVt0XTtyLmxlbmd0aD4wJiYoblt0XT1yKX0pKSxBKCkobikubWFwKEgpfShlLHQpKSkuZmlsdGVyKCh0PT50Lmxlbmd0aD4wKSl9KGZ1bmN0aW9uKHQsZSl7Y29uc3R7YmxhY2tsaXN0Om4sd2hpdGVsaXN0OnIsY29tYmluZVdpdGhpblNlbGVjdG9yOm8sbWF4Q29tYmluYXRpb25zOml9PWUsdT15KG4pLGM9eShyKTtyZXR1cm4gZnVuY3Rpb24odCl7Y29uc3R7c2VsZWN0b3JzOmUsaW5jbHVkZVRhZzpufT10LHI9W10uY29uY2F0KGUpO3JldHVybiBuJiYhci5pbmNsdWRlcyhcInRhZ1wiKSYmci5wdXNoKFwidGFnXCIpLHJ9KGUpLnJlZHVjZSgoKGUsbik9Pntjb25zdCByPWZ1bmN0aW9uKHQ9W10sZSl7cmV0dXJuIHQuc29ydCgoKHQsbik9Pntjb25zdCByPWUodCksbz1lKG4pO3JldHVybiByJiYhbz8tMTohciYmbz8xOjB9KSl9KGZ1bmN0aW9uKHQ9W10sZSxuKXtyZXR1cm4gdC5maWx0ZXIoKHQ9Pm4odCl8fCFlKHQpKSl9KGZ1bmN0aW9uKHQsZSl7dmFyIG47cmV0dXJuKG51bGwhPT0obj1CW2VdKSYmdm9pZCAwIT09bj9uOigpPT5bXSkodCl9KHQsbiksdSxjKSxjKTtyZXR1cm4gZVtuXT1vP00ocix7bWF4UmVzdWx0czppfSk6ci5tYXAoKHQ9Plt0XSkpLGV9KSx7fSl9KHQsbiksbikpO3JldHVyblsuLi5uZXcgU2V0KHIpXX0odCxyLnJvb3Qsciksbik7Zm9yKGNvbnN0IGUgb2YgbylpZihnKHQsZSxyLnJvb3QpKXJldHVybiBlO3JldHVybiBudWxsfWZ1bmN0aW9uIHoodCl7cmV0dXJue3ZhbHVlOnQsaW5jbHVkZTohMX19ZnVuY3Rpb24gSih7c2VsZWN0b3JzOnQsb3BlcmF0b3I6bn0pe2xldCByPVsuLi53XTt0W2UudGFnXSYmdFtlLm50aG9mdHlwZV0mJihyPXIuZmlsdGVyKCh0PT50IT09ZS50YWcpKSk7bGV0IG89XCJcIjtyZXR1cm4gci5mb3JFYWNoKChlPT57KHRbZV18fFtdKS5mb3JFYWNoKCgoe3ZhbHVlOnQsaW5jbHVkZTplfSk9PntlJiYobys9dCl9KSl9KSksbi52YWx1ZStvfWZ1bmN0aW9uIEsobil7cmV0dXJuW1wiOnJvb3RcIiwuLi5iKG4pLnJldmVyc2UoKS5tYXAoKG49Pntjb25zdCByPWZ1bmN0aW9uKGUsbixyPXQuTk9ORSl7Y29uc3Qgbz17fTtyZXR1cm4gbi5mb3JFYWNoKCh0PT57UmVmbGVjdC5zZXQobyx0LGZ1bmN0aW9uKHQsZSl7cmV0dXJuIEdbZV0odCl9KGUsdCkubWFwKHopKX0pKSx7ZWxlbWVudDplLG9wZXJhdG9yOk5bcl0sc2VsZWN0b3JzOm99fShuLFtlLm50aGNoaWxkXSx0LkRFU0NFTkRBTlQpO3JldHVybiByLnNlbGVjdG9ycy5udGhjaGlsZC5mb3JFYWNoKCh0PT57dC5pbmNsdWRlPSEwfSkpLHJ9KSkubWFwKEopXS5qb2luKFwiXCIpfWZ1bmN0aW9uIFEodCxuPXt9KXtjb25zdCByPWZ1bmN0aW9uKHQpe2NvbnN0IGU9KEFycmF5LmlzQXJyYXkodCk/dDpbdF0pLmZpbHRlcihpKTtyZXR1cm5bLi4ubmV3IFNldChlKV19KHQpLG89ZnVuY3Rpb24odCxuPXt9KXtjb25zdCByPU9iamVjdC5hc3NpZ24oT2JqZWN0LmFzc2lnbih7fSxjKSxuKTtyZXR1cm57c2VsZWN0b3JzOihvPXIuc2VsZWN0b3JzLEFycmF5LmlzQXJyYXkobyk/by5maWx0ZXIoKHQ9PntyZXR1cm4gbj1lLHI9dCxPYmplY3QudmFsdWVzKG4pLmluY2x1ZGVzKHIpO3ZhciBuLHJ9KSk6W10pLHdoaXRlbGlzdDpzKHIud2hpdGVsaXN0KSxibGFja2xpc3Q6cyhyLmJsYWNrbGlzdCkscm9vdDpkKHIucm9vdCx0KSxjb21iaW5lV2l0aGluU2VsZWN0b3I6ISFyLmNvbWJpbmVXaXRoaW5TZWxlY3Rvcixjb21iaW5lQmV0d2VlblNlbGVjdG9yczohIXIuY29tYmluZUJldHdlZW5TZWxlY3RvcnMsaW5jbHVkZVRhZzohIXIuaW5jbHVkZVRhZyxtYXhDb21iaW5hdGlvbnM6cChyLm1heENvbWJpbmF0aW9ucyksbWF4Q2FuZGlkYXRlczpwKHIubWF4Q2FuZGlkYXRlcyl9O3ZhciBvfShyWzBdLG4pO2xldCB1PVwiXCIsYT1vLnJvb3Q7ZnVuY3Rpb24gbCgpe3JldHVybiBmdW5jdGlvbih0LGUsbj1cIlwiLHIpe2lmKDA9PT10Lmxlbmd0aClyZXR1cm4gbnVsbDtjb25zdCBvPVt0Lmxlbmd0aD4xP3Q6W10sLi4udih0LGUpLm1hcCgodD0+W3RdKSldO2Zvcihjb25zdCB0IG9mIG8pe2NvbnN0IGU9VSh0LDAsbixyKTtpZihlKXJldHVybntmb3VuZEVsZW1lbnRzOnQsc2VsZWN0b3I6ZX19cmV0dXJuIG51bGx9KHIsYSx1LG8pfWxldCBmPWwoKTtmb3IoO2Y7KXtjb25zdHtmb3VuZEVsZW1lbnRzOnQsc2VsZWN0b3I6ZX09ZjtpZihnKHIsZSxvLnJvb3QpKXJldHVybiBlO2E9dFswXSx1PWUsZj1sKCl9cmV0dXJuIHIubGVuZ3RoPjE/ci5tYXAoKHQ9PlEodCxvKSkpLmpvaW4oXCIsIFwiKTpmdW5jdGlvbih0KXtyZXR1cm4gdC5tYXAoSykuam9pbihcIiwgXCIpfShyKX1jb25zdCBYPVF9KSgpLHJ9KSgpfSkpOyJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///4766\n')},7912:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $Array = GetIntrinsic('%Array%');\n\n// eslint-disable-next-line global-require\nvar toStr = !$Array.isArray && __webpack_require__(1924)('Object.prototype.toString');\n\n// https://ecma-international.org/ecma-262/6.0/#sec-isarray\n\nmodule.exports = $Array.isArray || function IsArray(argument) {\n\treturn toStr(argument) === '[object Array]';\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzkxMi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQTtBQUNBLCtCQUErQixtQkFBTyxDQUFDLElBQXFCOztBQUU1RDs7QUFFQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjAvSXNBcnJheS5qcz83MGQ4Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRBcnJheSA9IEdldEludHJpbnNpYygnJUFycmF5JScpO1xuXG4vLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgZ2xvYmFsLXJlcXVpcmVcbnZhciB0b1N0ciA9ICEkQXJyYXkuaXNBcnJheSAmJiByZXF1aXJlKCdjYWxsLWJpbmQvY2FsbEJvdW5kJykoJ09iamVjdC5wcm90b3R5cGUudG9TdHJpbmcnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWlzYXJyYXlcblxubW9kdWxlLmV4cG9ydHMgPSAkQXJyYXkuaXNBcnJheSB8fCBmdW5jdGlvbiBJc0FycmF5KGFyZ3VtZW50KSB7XG5cdHJldHVybiB0b1N0cihhcmd1bWVudCkgPT09ICdbb2JqZWN0IEFycmF5XSc7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///7912\n")},4200:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar CodePointAt = __webpack_require__(2432);\nvar IsIntegralNumber = __webpack_require__(7312);\nvar Type = __webpack_require__(3633);\n\nvar MAX_SAFE_INTEGER = __webpack_require__(1645);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\n// https://ecma-international.org/ecma-262/12.0/#sec-advancestringindex\n\nmodule.exports = function AdvanceStringIndex(S, index, unicode) {\n\tif (Type(S) !== 'String') {\n\t\tthrow new $TypeError('Assertion failed: `S` must be a String');\n\t}\n\tif (!IsIntegralNumber(index) || index < 0 || index > MAX_SAFE_INTEGER) {\n\t\tthrow new $TypeError('Assertion failed: `length` must be an integer >= 0 and <= 2**53');\n\t}\n\tif (Type(unicode) !== 'Boolean') {\n\t\tthrow new $TypeError('Assertion failed: `unicode` must be a Boolean');\n\t}\n\tif (!unicode) {\n\t\treturn index + 1;\n\t}\n\tvar length = S.length;\n\tif ((index + 1) >= length) {\n\t\treturn index + 1;\n\t}\n\tvar cp = CodePointAt(S, index);\n\treturn index + cp['[[CodeUnitCount]]'];\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDIwMC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQyxrQkFBa0IsbUJBQU8sQ0FBQyxJQUFlO0FBQ3pDLHVCQUF1QixtQkFBTyxDQUFDLElBQW9CO0FBQ25ELFdBQVcsbUJBQU8sQ0FBQyxJQUFROztBQUUzQix1QkFBdUIsbUJBQU8sQ0FBQyxJQUEyQjs7QUFFMUQ7O0FBRUE7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9BZHZhbmNlU3RyaW5nSW5kZXguanM/YTg1YiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciBDb2RlUG9pbnRBdCA9IHJlcXVpcmUoJy4vQ29kZVBvaW50QXQnKTtcbnZhciBJc0ludGVncmFsTnVtYmVyID0gcmVxdWlyZSgnLi9Jc0ludGVncmFsTnVtYmVyJyk7XG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG52YXIgTUFYX1NBRkVfSU5URUdFUiA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvbWF4U2FmZUludGVnZXInKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG5cbi8vIGh0dHBzOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi8xMi4wLyNzZWMtYWR2YW5jZXN0cmluZ2luZGV4XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gQWR2YW5jZVN0cmluZ0luZGV4KFMsIGluZGV4LCB1bmljb2RlKSB7XG5cdGlmIChUeXBlKFMpICE9PSAnU3RyaW5nJykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBgU2AgbXVzdCBiZSBhIFN0cmluZycpO1xuXHR9XG5cdGlmICghSXNJbnRlZ3JhbE51bWJlcihpbmRleCkgfHwgaW5kZXggPCAwIHx8IGluZGV4ID4gTUFYX1NBRkVfSU5URUdFUikge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBgbGVuZ3RoYCBtdXN0IGJlIGFuIGludGVnZXIgPj0gMCBhbmQgPD0gMioqNTMnKTtcblx0fVxuXHRpZiAoVHlwZSh1bmljb2RlKSAhPT0gJ0Jvb2xlYW4nKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IGB1bmljb2RlYCBtdXN0IGJlIGEgQm9vbGVhbicpO1xuXHR9XG5cdGlmICghdW5pY29kZSkge1xuXHRcdHJldHVybiBpbmRleCArIDE7XG5cdH1cblx0dmFyIGxlbmd0aCA9IFMubGVuZ3RoO1xuXHRpZiAoKGluZGV4ICsgMSkgPj0gbGVuZ3RoKSB7XG5cdFx0cmV0dXJuIGluZGV4ICsgMTtcblx0fVxuXHR2YXIgY3AgPSBDb2RlUG9pbnRBdChTLCBpbmRleCk7XG5cdHJldHVybiBpbmRleCArIGNwWydbW0NvZGVVbml0Q291bnRdXSddO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///4200\n")},581:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\nvar callBound = __webpack_require__(1924);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar IsArray = __webpack_require__(6975);\n\nvar $apply = GetIntrinsic('%Reflect.apply%', true) || callBound('%Function.prototype.apply%');\n\n// https://ecma-international.org/ecma-262/6.0/#sec-call\n\nmodule.exports = function Call(F, V) {\n\tvar argumentsList = arguments.length > 2 ? arguments[2] : [];\n\tif (!IsArray(argumentsList)) {\n\t\tthrow new $TypeError('Assertion failed: optional `argumentsList`, if provided, must be a List');\n\t}\n\treturn $apply(F, V, argumentsList);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTgxLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWU7QUFDMUMsZ0JBQWdCLG1CQUFPLENBQUMsSUFBcUI7O0FBRTdDOztBQUVBLGNBQWMsbUJBQU8sQ0FBQyxJQUFXOztBQUVqQzs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9DYWxsLmpzP2Y4M2YiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xudmFyIGNhbGxCb3VuZCA9IHJlcXVpcmUoJ2NhbGwtYmluZC9jYWxsQm91bmQnKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG5cbnZhciBJc0FycmF5ID0gcmVxdWlyZSgnLi9Jc0FycmF5Jyk7XG5cbnZhciAkYXBwbHkgPSBHZXRJbnRyaW5zaWMoJyVSZWZsZWN0LmFwcGx5JScsIHRydWUpIHx8IGNhbGxCb3VuZCgnJUZ1bmN0aW9uLnByb3RvdHlwZS5hcHBseSUnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWNhbGxcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBDYWxsKEYsIFYpIHtcblx0dmFyIGFyZ3VtZW50c0xpc3QgPSBhcmd1bWVudHMubGVuZ3RoID4gMiA/IGFyZ3VtZW50c1syXSA6IFtdO1xuXHRpZiAoIUlzQXJyYXkoYXJndW1lbnRzTGlzdCkpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogb3B0aW9uYWwgYGFyZ3VtZW50c0xpc3RgLCBpZiBwcm92aWRlZCwgbXVzdCBiZSBhIExpc3QnKTtcblx0fVxuXHRyZXR1cm4gJGFwcGx5KEYsIFYsIGFyZ3VtZW50c0xpc3QpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///581\n")},2432:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar callBound = __webpack_require__(1924);\nvar isLeadingSurrogate = __webpack_require__(9544);\nvar isTrailingSurrogate = __webpack_require__(5424);\n\nvar Type = __webpack_require__(3633);\nvar UTF16SurrogatePairToCodePoint = __webpack_require__(4857);\n\nvar $charAt = callBound('String.prototype.charAt');\nvar $charCodeAt = callBound('String.prototype.charCodeAt');\n\n// https://ecma-international.org/ecma-262/12.0/#sec-codepointat\n\nmodule.exports = function CodePointAt(string, position) {\n\tif (Type(string) !== 'String') {\n\t\tthrow new $TypeError('Assertion failed: `string` must be a String');\n\t}\n\tvar size = string.length;\n\tif (position < 0 || position >= size) {\n\t\tthrow new $TypeError('Assertion failed: `position` must be >= 0, and < the length of `string`');\n\t}\n\tvar first = $charCodeAt(string, position);\n\tvar cp = $charAt(string, position);\n\tvar firstIsLeading = isLeadingSurrogate(first);\n\tvar firstIsTrailing = isTrailingSurrogate(first);\n\tif (!firstIsLeading && !firstIsTrailing) {\n\t\treturn {\n\t\t\t'[[CodePoint]]': cp,\n\t\t\t'[[CodeUnitCount]]': 1,\n\t\t\t'[[IsUnpairedSurrogate]]': false\n\t\t};\n\t}\n\tif (firstIsTrailing || (position + 1 === size)) {\n\t\treturn {\n\t\t\t'[[CodePoint]]': cp,\n\t\t\t'[[CodeUnitCount]]': 1,\n\t\t\t'[[IsUnpairedSurrogate]]': true\n\t\t};\n\t}\n\tvar second = $charCodeAt(string, position + 1);\n\tif (!isTrailingSurrogate(second)) {\n\t\treturn {\n\t\t\t'[[CodePoint]]': cp,\n\t\t\t'[[CodeUnitCount]]': 1,\n\t\t\t'[[IsUnpairedSurrogate]]': true\n\t\t};\n\t}\n\n\treturn {\n\t\t'[[CodePoint]]': UTF16SurrogatePairToCodePoint(first, second),\n\t\t'[[CodeUnitCount]]': 2,\n\t\t'[[IsUnpairedSurrogate]]': false\n\t};\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjQzMi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBLGdCQUFnQixtQkFBTyxDQUFDLElBQXFCO0FBQzdDLHlCQUF5QixtQkFBTyxDQUFDLElBQStCO0FBQ2hFLDBCQUEwQixtQkFBTyxDQUFDLElBQWdDOztBQUVsRSxXQUFXLG1CQUFPLENBQUMsSUFBUTtBQUMzQixvQ0FBb0MsbUJBQU8sQ0FBQyxJQUFpQzs7QUFFN0U7QUFDQTs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvQ29kZVBvaW50QXQuanM/NTNmOCJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xudmFyIGNhbGxCb3VuZCA9IHJlcXVpcmUoJ2NhbGwtYmluZC9jYWxsQm91bmQnKTtcbnZhciBpc0xlYWRpbmdTdXJyb2dhdGUgPSByZXF1aXJlKCcuLi9oZWxwZXJzL2lzTGVhZGluZ1N1cnJvZ2F0ZScpO1xudmFyIGlzVHJhaWxpbmdTdXJyb2dhdGUgPSByZXF1aXJlKCcuLi9oZWxwZXJzL2lzVHJhaWxpbmdTdXJyb2dhdGUnKTtcblxudmFyIFR5cGUgPSByZXF1aXJlKCcuL1R5cGUnKTtcbnZhciBVVEYxNlN1cnJvZ2F0ZVBhaXJUb0NvZGVQb2ludCA9IHJlcXVpcmUoJy4vVVRGMTZTdXJyb2dhdGVQYWlyVG9Db2RlUG9pbnQnKTtcblxudmFyICRjaGFyQXQgPSBjYWxsQm91bmQoJ1N0cmluZy5wcm90b3R5cGUuY2hhckF0Jyk7XG52YXIgJGNoYXJDb2RlQXQgPSBjYWxsQm91bmQoJ1N0cmluZy5wcm90b3R5cGUuY2hhckNvZGVBdCcpO1xuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvMTIuMC8jc2VjLWNvZGVwb2ludGF0XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gQ29kZVBvaW50QXQoc3RyaW5nLCBwb3NpdGlvbikge1xuXHRpZiAoVHlwZShzdHJpbmcpICE9PSAnU3RyaW5nJykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBgc3RyaW5nYCBtdXN0IGJlIGEgU3RyaW5nJyk7XG5cdH1cblx0dmFyIHNpemUgPSBzdHJpbmcubGVuZ3RoO1xuXHRpZiAocG9zaXRpb24gPCAwIHx8IHBvc2l0aW9uID49IHNpemUpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogYHBvc2l0aW9uYCBtdXN0IGJlID49IDAsIGFuZCA8IHRoZSBsZW5ndGggb2YgYHN0cmluZ2AnKTtcblx0fVxuXHR2YXIgZmlyc3QgPSAkY2hhckNvZGVBdChzdHJpbmcsIHBvc2l0aW9uKTtcblx0dmFyIGNwID0gJGNoYXJBdChzdHJpbmcsIHBvc2l0aW9uKTtcblx0dmFyIGZpcnN0SXNMZWFkaW5nID0gaXNMZWFkaW5nU3Vycm9nYXRlKGZpcnN0KTtcblx0dmFyIGZpcnN0SXNUcmFpbGluZyA9IGlzVHJhaWxpbmdTdXJyb2dhdGUoZmlyc3QpO1xuXHRpZiAoIWZpcnN0SXNMZWFkaW5nICYmICFmaXJzdElzVHJhaWxpbmcpIHtcblx0XHRyZXR1cm4ge1xuXHRcdFx0J1tbQ29kZVBvaW50XV0nOiBjcCxcblx0XHRcdCdbW0NvZGVVbml0Q291bnRdXSc6IDEsXG5cdFx0XHQnW1tJc1VucGFpcmVkU3Vycm9nYXRlXV0nOiBmYWxzZVxuXHRcdH07XG5cdH1cblx0aWYgKGZpcnN0SXNUcmFpbGluZyB8fCAocG9zaXRpb24gKyAxID09PSBzaXplKSkge1xuXHRcdHJldHVybiB7XG5cdFx0XHQnW1tDb2RlUG9pbnRdXSc6IGNwLFxuXHRcdFx0J1tbQ29kZVVuaXRDb3VudF1dJzogMSxcblx0XHRcdCdbW0lzVW5wYWlyZWRTdXJyb2dhdGVdXSc6IHRydWVcblx0XHR9O1xuXHR9XG5cdHZhciBzZWNvbmQgPSAkY2hhckNvZGVBdChzdHJpbmcsIHBvc2l0aW9uICsgMSk7XG5cdGlmICghaXNUcmFpbGluZ1N1cnJvZ2F0ZShzZWNvbmQpKSB7XG5cdFx0cmV0dXJuIHtcblx0XHRcdCdbW0NvZGVQb2ludF1dJzogY3AsXG5cdFx0XHQnW1tDb2RlVW5pdENvdW50XV0nOiAxLFxuXHRcdFx0J1tbSXNVbnBhaXJlZFN1cnJvZ2F0ZV1dJzogdHJ1ZVxuXHRcdH07XG5cdH1cblxuXHRyZXR1cm4ge1xuXHRcdCdbW0NvZGVQb2ludF1dJzogVVRGMTZTdXJyb2dhdGVQYWlyVG9Db2RlUG9pbnQoZmlyc3QsIHNlY29uZCksXG5cdFx0J1tbQ29kZVVuaXRDb3VudF1dJzogMixcblx0XHQnW1tJc1VucGFpcmVkU3Vycm9nYXRlXV0nOiBmYWxzZVxuXHR9O1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2432\n")},2658:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-createiterresultobject\n\nmodule.exports = function CreateIterResultObject(value, done) {\n\tif (Type(done) !== 'Boolean') {\n\t\tthrow new $TypeError('Assertion failed: Type(done) is not Boolean');\n\t}\n\treturn {\n\t\tvalue: value,\n\t\tdone: done\n\t};\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjY1OC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSxXQUFXLG1CQUFPLENBQUMsSUFBUTs7QUFFM0I7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL0NyZWF0ZUl0ZXJSZXN1bHRPYmplY3QuanM/NDk1YSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xuXG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtY3JlYXRlaXRlcnJlc3VsdG9iamVjdFxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIENyZWF0ZUl0ZXJSZXN1bHRPYmplY3QodmFsdWUsIGRvbmUpIHtcblx0aWYgKFR5cGUoZG9uZSkgIT09ICdCb29sZWFuJykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBUeXBlKGRvbmUpIGlzIG5vdCBCb29sZWFuJyk7XG5cdH1cblx0cmV0dXJuIHtcblx0XHR2YWx1ZTogdmFsdWUsXG5cdFx0ZG9uZTogZG9uZVxuXHR9O1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2658\n")},7730:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar DefineOwnProperty = __webpack_require__(3682);\n\nvar FromPropertyDescriptor = __webpack_require__(8334);\nvar IsDataDescriptor = __webpack_require__(3746);\nvar IsPropertyKey = __webpack_require__(4305);\nvar SameValue = __webpack_require__(484);\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-createmethodproperty\n\nmodule.exports = function CreateMethodProperty(O, P, V) {\n\tif (Type(O) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: Type(O) is not Object');\n\t}\n\n\tif (!IsPropertyKey(P)) {\n\t\tthrow new $TypeError('Assertion failed: IsPropertyKey(P) is not true');\n\t}\n\n\tvar newDesc = {\n\t\t'[[Configurable]]': true,\n\t\t'[[Enumerable]]': false,\n\t\t'[[Value]]': V,\n\t\t'[[Writable]]': true\n\t};\n\treturn DefineOwnProperty(\n\t\tIsDataDescriptor,\n\t\tSameValue,\n\t\tFromPropertyDescriptor,\n\t\tO,\n\t\tP,\n\t\tnewDesc\n\t);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzczMC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSx3QkFBd0IsbUJBQU8sQ0FBQyxJQUE4Qjs7QUFFOUQsNkJBQTZCLG1CQUFPLENBQUMsSUFBMEI7QUFDL0QsdUJBQXVCLG1CQUFPLENBQUMsSUFBb0I7QUFDbkQsb0JBQW9CLG1CQUFPLENBQUMsSUFBaUI7QUFDN0MsZ0JBQWdCLG1CQUFPLENBQUMsR0FBYTtBQUNyQyxXQUFXLG1CQUFPLENBQUMsSUFBUTs7QUFFM0I7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9DcmVhdGVNZXRob2RQcm9wZXJ0eS5qcz9iODljIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG5cbnZhciBEZWZpbmVPd25Qcm9wZXJ0eSA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvRGVmaW5lT3duUHJvcGVydHknKTtcblxudmFyIEZyb21Qcm9wZXJ0eURlc2NyaXB0b3IgPSByZXF1aXJlKCcuL0Zyb21Qcm9wZXJ0eURlc2NyaXB0b3InKTtcbnZhciBJc0RhdGFEZXNjcmlwdG9yID0gcmVxdWlyZSgnLi9Jc0RhdGFEZXNjcmlwdG9yJyk7XG52YXIgSXNQcm9wZXJ0eUtleSA9IHJlcXVpcmUoJy4vSXNQcm9wZXJ0eUtleScpO1xudmFyIFNhbWVWYWx1ZSA9IHJlcXVpcmUoJy4vU2FtZVZhbHVlJyk7XG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtY3JlYXRlbWV0aG9kcHJvcGVydHlcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBDcmVhdGVNZXRob2RQcm9wZXJ0eShPLCBQLCBWKSB7XG5cdGlmIChUeXBlKE8pICE9PSAnT2JqZWN0Jykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBUeXBlKE8pIGlzIG5vdCBPYmplY3QnKTtcblx0fVxuXG5cdGlmICghSXNQcm9wZXJ0eUtleShQKSkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBJc1Byb3BlcnR5S2V5KFApIGlzIG5vdCB0cnVlJyk7XG5cdH1cblxuXHR2YXIgbmV3RGVzYyA9IHtcblx0XHQnW1tDb25maWd1cmFibGVdXSc6IHRydWUsXG5cdFx0J1tbRW51bWVyYWJsZV1dJzogZmFsc2UsXG5cdFx0J1tbVmFsdWVdXSc6IFYsXG5cdFx0J1tbV3JpdGFibGVdXSc6IHRydWVcblx0fTtcblx0cmV0dXJuIERlZmluZU93blByb3BlcnR5KFxuXHRcdElzRGF0YURlc2NyaXB0b3IsXG5cdFx0U2FtZVZhbHVlLFxuXHRcdEZyb21Qcm9wZXJ0eURlc2NyaXB0b3IsXG5cdFx0Tyxcblx0XHRQLFxuXHRcdG5ld0Rlc2Ncblx0KTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///7730\n")},3937:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\nvar hasSymbols = __webpack_require__(1405)();\n\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar IteratorPrototype = GetIntrinsic('%IteratorPrototype%', true);\nvar $defineProperty = GetIntrinsic('%Object.defineProperty%', true);\n\nvar AdvanceStringIndex = __webpack_require__(4200);\nvar CreateIterResultObject = __webpack_require__(2658);\nvar CreateMethodProperty = __webpack_require__(7730);\nvar Get = __webpack_require__(1391);\nvar OrdinaryObjectCreate = __webpack_require__(953);\nvar RegExpExec = __webpack_require__(6258);\nvar Set = __webpack_require__(105);\nvar ToLength = __webpack_require__(8305);\nvar ToString = __webpack_require__(6846);\nvar Type = __webpack_require__(3633);\n\nvar SLOT = __webpack_require__(9496);\n\nvar RegExpStringIterator = function RegExpStringIterator(R, S, global, fullUnicode) {\n\tif (Type(S) !== 'String') {\n\t\tthrow new $TypeError('`S` must be a string');\n\t}\n\tif (Type(global) !== 'Boolean') {\n\t\tthrow new $TypeError('`global` must be a boolean');\n\t}\n\tif (Type(fullUnicode) !== 'Boolean') {\n\t\tthrow new $TypeError('`fullUnicode` must be a boolean');\n\t}\n\tSLOT.set(this, '[[IteratingRegExp]]', R);\n\tSLOT.set(this, '[[IteratedString]]', S);\n\tSLOT.set(this, '[[Global]]', global);\n\tSLOT.set(this, '[[Unicode]]', fullUnicode);\n\tSLOT.set(this, '[[Done]]', false);\n};\n\nif (IteratorPrototype) {\n\tRegExpStringIterator.prototype = OrdinaryObjectCreate(IteratorPrototype);\n}\n\nvar RegExpStringIteratorNext = function next() {\n\tvar O = this; // eslint-disable-line no-invalid-this\n\tif (Type(O) !== 'Object') {\n\t\tthrow new $TypeError('receiver must be an object');\n\t}\n\tif (\n\t\t!(O instanceof RegExpStringIterator)\n || !SLOT.has(O, '[[IteratingRegExp]]')\n || !SLOT.has(O, '[[IteratedString]]')\n || !SLOT.has(O, '[[Global]]')\n || !SLOT.has(O, '[[Unicode]]')\n || !SLOT.has(O, '[[Done]]')\n\t) {\n\t\tthrow new $TypeError('\"this\" value must be a RegExpStringIterator instance');\n\t}\n\tif (SLOT.get(O, '[[Done]]')) {\n\t\treturn CreateIterResultObject(undefined, true);\n\t}\n\tvar R = SLOT.get(O, '[[IteratingRegExp]]');\n\tvar S = SLOT.get(O, '[[IteratedString]]');\n\tvar global = SLOT.get(O, '[[Global]]');\n\tvar fullUnicode = SLOT.get(O, '[[Unicode]]');\n\tvar match = RegExpExec(R, S);\n\tif (match === null) {\n\t\tSLOT.set(O, '[[Done]]', true);\n\t\treturn CreateIterResultObject(undefined, true);\n\t}\n\tif (global) {\n\t\tvar matchStr = ToString(Get(match, '0'));\n\t\tif (matchStr === '') {\n\t\t\tvar thisIndex = ToLength(Get(R, 'lastIndex'));\n\t\t\tvar nextIndex = AdvanceStringIndex(S, thisIndex, fullUnicode);\n\t\t\tSet(R, 'lastIndex', nextIndex, true);\n\t\t}\n\t\treturn CreateIterResultObject(match, false);\n\t}\n\tSLOT.set(O, '[[Done]]', true);\n\treturn CreateIterResultObject(match, false);\n};\nCreateMethodProperty(RegExpStringIterator.prototype, 'next', RegExpStringIteratorNext);\n\nif (hasSymbols) {\n\tif (Symbol.toStringTag) {\n\t\tif ($defineProperty) {\n\t\t\t$defineProperty(RegExpStringIterator.prototype, Symbol.toStringTag, {\n\t\t\t\tconfigurable: true,\n\t\t\t\tenumerable: false,\n\t\t\t\tvalue: 'RegExp String Iterator',\n\t\t\t\twritable: false\n\t\t\t});\n\t\t} else {\n\t\t\tRegExpStringIterator.prototype[Symbol.toStringTag] = 'RegExp String Iterator';\n\t\t}\n\t}\n\n\tif (Symbol.iterator && typeof RegExpStringIterator.prototype[Symbol.iterator] !== 'function') {\n\t\tvar iteratorFn = function SymbolIterator() {\n\t\t\treturn this;\n\t\t};\n\t\tCreateMethodProperty(RegExpStringIterator.prototype, Symbol.iterator, iteratorFn);\n\t}\n}\n\n// https://262.ecma-international.org/11.0/#sec-createregexpstringiterator\nmodule.exports = function CreateRegExpStringIterator(R, S, global, fullUnicode) {\n\t// assert R.global === global && R.unicode === fullUnicode?\n\treturn new RegExpStringIterator(R, S, global, fullUnicode);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzkzNy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlO0FBQzFDLGlCQUFpQixtQkFBTyxDQUFDLElBQWE7O0FBRXRDO0FBQ0E7QUFDQTs7QUFFQSx5QkFBeUIsbUJBQU8sQ0FBQyxJQUFzQjtBQUN2RCw2QkFBNkIsbUJBQU8sQ0FBQyxJQUEwQjtBQUMvRCwyQkFBMkIsbUJBQU8sQ0FBQyxJQUF3QjtBQUMzRCxVQUFVLG1CQUFPLENBQUMsSUFBTztBQUN6QiwyQkFBMkIsbUJBQU8sQ0FBQyxHQUF3QjtBQUMzRCxpQkFBaUIsbUJBQU8sQ0FBQyxJQUFjO0FBQ3ZDLFVBQVUsbUJBQU8sQ0FBQyxHQUFPO0FBQ3pCLGVBQWUsbUJBQU8sQ0FBQyxJQUFZO0FBQ25DLGVBQWUsbUJBQU8sQ0FBQyxJQUFZO0FBQ25DLFdBQVcsbUJBQU8sQ0FBQyxJQUFROztBQUUzQixXQUFXLG1CQUFPLENBQUMsSUFBZTs7QUFFbEM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0EsZUFBZTtBQUNmO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLElBQUk7QUFDSixJQUFJO0FBQ0o7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvQ3JlYXRlUmVnRXhwU3RyaW5nSXRlcmF0b3IuanM/MGUzOSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG52YXIgaGFzU3ltYm9scyA9IHJlcXVpcmUoJ2hhcy1zeW1ib2xzJykoKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG52YXIgSXRlcmF0b3JQcm90b3R5cGUgPSBHZXRJbnRyaW5zaWMoJyVJdGVyYXRvclByb3RvdHlwZSUnLCB0cnVlKTtcbnZhciAkZGVmaW5lUHJvcGVydHkgPSBHZXRJbnRyaW5zaWMoJyVPYmplY3QuZGVmaW5lUHJvcGVydHklJywgdHJ1ZSk7XG5cbnZhciBBZHZhbmNlU3RyaW5nSW5kZXggPSByZXF1aXJlKCcuL0FkdmFuY2VTdHJpbmdJbmRleCcpO1xudmFyIENyZWF0ZUl0ZXJSZXN1bHRPYmplY3QgPSByZXF1aXJlKCcuL0NyZWF0ZUl0ZXJSZXN1bHRPYmplY3QnKTtcbnZhciBDcmVhdGVNZXRob2RQcm9wZXJ0eSA9IHJlcXVpcmUoJy4vQ3JlYXRlTWV0aG9kUHJvcGVydHknKTtcbnZhciBHZXQgPSByZXF1aXJlKCcuL0dldCcpO1xudmFyIE9yZGluYXJ5T2JqZWN0Q3JlYXRlID0gcmVxdWlyZSgnLi9PcmRpbmFyeU9iamVjdENyZWF0ZScpO1xudmFyIFJlZ0V4cEV4ZWMgPSByZXF1aXJlKCcuL1JlZ0V4cEV4ZWMnKTtcbnZhciBTZXQgPSByZXF1aXJlKCcuL1NldCcpO1xudmFyIFRvTGVuZ3RoID0gcmVxdWlyZSgnLi9Ub0xlbmd0aCcpO1xudmFyIFRvU3RyaW5nID0gcmVxdWlyZSgnLi9Ub1N0cmluZycpO1xudmFyIFR5cGUgPSByZXF1aXJlKCcuL1R5cGUnKTtcblxudmFyIFNMT1QgPSByZXF1aXJlKCdpbnRlcm5hbC1zbG90Jyk7XG5cbnZhciBSZWdFeHBTdHJpbmdJdGVyYXRvciA9IGZ1bmN0aW9uIFJlZ0V4cFN0cmluZ0l0ZXJhdG9yKFIsIFMsIGdsb2JhbCwgZnVsbFVuaWNvZGUpIHtcblx0aWYgKFR5cGUoUykgIT09ICdTdHJpbmcnKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ2BTYCBtdXN0IGJlIGEgc3RyaW5nJyk7XG5cdH1cblx0aWYgKFR5cGUoZ2xvYmFsKSAhPT0gJ0Jvb2xlYW4nKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ2BnbG9iYWxgIG11c3QgYmUgYSBib29sZWFuJyk7XG5cdH1cblx0aWYgKFR5cGUoZnVsbFVuaWNvZGUpICE9PSAnQm9vbGVhbicpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignYGZ1bGxVbmljb2RlYCBtdXN0IGJlIGEgYm9vbGVhbicpO1xuXHR9XG5cdFNMT1Quc2V0KHRoaXMsICdbW0l0ZXJhdGluZ1JlZ0V4cF1dJywgUik7XG5cdFNMT1Quc2V0KHRoaXMsICdbW0l0ZXJhdGVkU3RyaW5nXV0nLCBTKTtcblx0U0xPVC5zZXQodGhpcywgJ1tbR2xvYmFsXV0nLCBnbG9iYWwpO1xuXHRTTE9ULnNldCh0aGlzLCAnW1tVbmljb2RlXV0nLCBmdWxsVW5pY29kZSk7XG5cdFNMT1Quc2V0KHRoaXMsICdbW0RvbmVdXScsIGZhbHNlKTtcbn07XG5cbmlmIChJdGVyYXRvclByb3RvdHlwZSkge1xuXHRSZWdFeHBTdHJpbmdJdGVyYXRvci5wcm90b3R5cGUgPSBPcmRpbmFyeU9iamVjdENyZWF0ZShJdGVyYXRvclByb3RvdHlwZSk7XG59XG5cbnZhciBSZWdFeHBTdHJpbmdJdGVyYXRvck5leHQgPSBmdW5jdGlvbiBuZXh0KCkge1xuXHR2YXIgTyA9IHRoaXM7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8taW52YWxpZC10aGlzXG5cdGlmIChUeXBlKE8pICE9PSAnT2JqZWN0Jykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdyZWNlaXZlciBtdXN0IGJlIGFuIG9iamVjdCcpO1xuXHR9XG5cdGlmIChcblx0XHQhKE8gaW5zdGFuY2VvZiBSZWdFeHBTdHJpbmdJdGVyYXRvcilcbiAgICAgICAgfHwgIVNMT1QuaGFzKE8sICdbW0l0ZXJhdGluZ1JlZ0V4cF1dJylcbiAgICAgICAgfHwgIVNMT1QuaGFzKE8sICdbW0l0ZXJhdGVkU3RyaW5nXV0nKVxuICAgICAgICB8fCAhU0xPVC5oYXMoTywgJ1tbR2xvYmFsXV0nKVxuICAgICAgICB8fCAhU0xPVC5oYXMoTywgJ1tbVW5pY29kZV1dJylcbiAgICAgICAgfHwgIVNMT1QuaGFzKE8sICdbW0RvbmVdXScpXG5cdCkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdcInRoaXNcIiB2YWx1ZSBtdXN0IGJlIGEgUmVnRXhwU3RyaW5nSXRlcmF0b3IgaW5zdGFuY2UnKTtcblx0fVxuXHRpZiAoU0xPVC5nZXQoTywgJ1tbRG9uZV1dJykpIHtcblx0XHRyZXR1cm4gQ3JlYXRlSXRlclJlc3VsdE9iamVjdCh1bmRlZmluZWQsIHRydWUpO1xuXHR9XG5cdHZhciBSID0gU0xPVC5nZXQoTywgJ1tbSXRlcmF0aW5nUmVnRXhwXV0nKTtcblx0dmFyIFMgPSBTTE9ULmdldChPLCAnW1tJdGVyYXRlZFN0cmluZ11dJyk7XG5cdHZhciBnbG9iYWwgPSBTTE9ULmdldChPLCAnW1tHbG9iYWxdXScpO1xuXHR2YXIgZnVsbFVuaWNvZGUgPSBTTE9ULmdldChPLCAnW1tVbmljb2RlXV0nKTtcblx0dmFyIG1hdGNoID0gUmVnRXhwRXhlYyhSLCBTKTtcblx0aWYgKG1hdGNoID09PSBudWxsKSB7XG5cdFx0U0xPVC5zZXQoTywgJ1tbRG9uZV1dJywgdHJ1ZSk7XG5cdFx0cmV0dXJuIENyZWF0ZUl0ZXJSZXN1bHRPYmplY3QodW5kZWZpbmVkLCB0cnVlKTtcblx0fVxuXHRpZiAoZ2xvYmFsKSB7XG5cdFx0dmFyIG1hdGNoU3RyID0gVG9TdHJpbmcoR2V0KG1hdGNoLCAnMCcpKTtcblx0XHRpZiAobWF0Y2hTdHIgPT09ICcnKSB7XG5cdFx0XHR2YXIgdGhpc0luZGV4ID0gVG9MZW5ndGgoR2V0KFIsICdsYXN0SW5kZXgnKSk7XG5cdFx0XHR2YXIgbmV4dEluZGV4ID0gQWR2YW5jZVN0cmluZ0luZGV4KFMsIHRoaXNJbmRleCwgZnVsbFVuaWNvZGUpO1xuXHRcdFx0U2V0KFIsICdsYXN0SW5kZXgnLCBuZXh0SW5kZXgsIHRydWUpO1xuXHRcdH1cblx0XHRyZXR1cm4gQ3JlYXRlSXRlclJlc3VsdE9iamVjdChtYXRjaCwgZmFsc2UpO1xuXHR9XG5cdFNMT1Quc2V0KE8sICdbW0RvbmVdXScsIHRydWUpO1xuXHRyZXR1cm4gQ3JlYXRlSXRlclJlc3VsdE9iamVjdChtYXRjaCwgZmFsc2UpO1xufTtcbkNyZWF0ZU1ldGhvZFByb3BlcnR5KFJlZ0V4cFN0cmluZ0l0ZXJhdG9yLnByb3RvdHlwZSwgJ25leHQnLCBSZWdFeHBTdHJpbmdJdGVyYXRvck5leHQpO1xuXG5pZiAoaGFzU3ltYm9scykge1xuXHRpZiAoU3ltYm9sLnRvU3RyaW5nVGFnKSB7XG5cdFx0aWYgKCRkZWZpbmVQcm9wZXJ0eSkge1xuXHRcdFx0JGRlZmluZVByb3BlcnR5KFJlZ0V4cFN0cmluZ0l0ZXJhdG9yLnByb3RvdHlwZSwgU3ltYm9sLnRvU3RyaW5nVGFnLCB7XG5cdFx0XHRcdGNvbmZpZ3VyYWJsZTogdHJ1ZSxcblx0XHRcdFx0ZW51bWVyYWJsZTogZmFsc2UsXG5cdFx0XHRcdHZhbHVlOiAnUmVnRXhwIFN0cmluZyBJdGVyYXRvcicsXG5cdFx0XHRcdHdyaXRhYmxlOiBmYWxzZVxuXHRcdFx0fSk7XG5cdFx0fSBlbHNlIHtcblx0XHRcdFJlZ0V4cFN0cmluZ0l0ZXJhdG9yLnByb3RvdHlwZVtTeW1ib2wudG9TdHJpbmdUYWddID0gJ1JlZ0V4cCBTdHJpbmcgSXRlcmF0b3InO1xuXHRcdH1cblx0fVxuXG5cdGlmIChTeW1ib2wuaXRlcmF0b3IgJiYgdHlwZW9mIFJlZ0V4cFN0cmluZ0l0ZXJhdG9yLnByb3RvdHlwZVtTeW1ib2wuaXRlcmF0b3JdICE9PSAnZnVuY3Rpb24nKSB7XG5cdFx0dmFyIGl0ZXJhdG9yRm4gPSBmdW5jdGlvbiBTeW1ib2xJdGVyYXRvcigpIHtcblx0XHRcdHJldHVybiB0aGlzO1xuXHRcdH07XG5cdFx0Q3JlYXRlTWV0aG9kUHJvcGVydHkoUmVnRXhwU3RyaW5nSXRlcmF0b3IucHJvdG90eXBlLCBTeW1ib2wuaXRlcmF0b3IsIGl0ZXJhdG9yRm4pO1xuXHR9XG59XG5cbi8vIGh0dHBzOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvMTEuMC8jc2VjLWNyZWF0ZXJlZ2V4cHN0cmluZ2l0ZXJhdG9yXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIENyZWF0ZVJlZ0V4cFN0cmluZ0l0ZXJhdG9yKFIsIFMsIGdsb2JhbCwgZnVsbFVuaWNvZGUpIHtcblx0Ly8gYXNzZXJ0IFIuZ2xvYmFsID09PSBnbG9iYWwgJiYgUi51bmljb2RlID09PSBmdWxsVW5pY29kZT9cblx0cmV0dXJuIG5ldyBSZWdFeHBTdHJpbmdJdGVyYXRvcihSLCBTLCBnbG9iYWwsIGZ1bGxVbmljb2RlKTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3937\n")},3950:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar isPropertyDescriptor = __webpack_require__(2435);\nvar DefineOwnProperty = __webpack_require__(3682);\n\nvar FromPropertyDescriptor = __webpack_require__(8334);\nvar IsAccessorDescriptor = __webpack_require__(9527);\nvar IsDataDescriptor = __webpack_require__(3746);\nvar IsPropertyKey = __webpack_require__(4305);\nvar SameValue = __webpack_require__(484);\nvar ToPropertyDescriptor = __webpack_require__(9916);\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-definepropertyorthrow\n\nmodule.exports = function DefinePropertyOrThrow(O, P, desc) {\n\tif (Type(O) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: Type(O) is not Object');\n\t}\n\n\tif (!IsPropertyKey(P)) {\n\t\tthrow new $TypeError('Assertion failed: IsPropertyKey(P) is not true');\n\t}\n\n\tvar Desc = isPropertyDescriptor({\n\t\tType: Type,\n\t\tIsDataDescriptor: IsDataDescriptor,\n\t\tIsAccessorDescriptor: IsAccessorDescriptor\n\t}, desc) ? desc : ToPropertyDescriptor(desc);\n\tif (!isPropertyDescriptor({\n\t\tType: Type,\n\t\tIsDataDescriptor: IsDataDescriptor,\n\t\tIsAccessorDescriptor: IsAccessorDescriptor\n\t}, Desc)) {\n\t\tthrow new $TypeError('Assertion failed: Desc is not a valid Property Descriptor');\n\t}\n\n\treturn DefineOwnProperty(\n\t\tIsDataDescriptor,\n\t\tSameValue,\n\t\tFromPropertyDescriptor,\n\t\tO,\n\t\tP,\n\t\tDesc\n\t);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzk1MC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSwyQkFBMkIsbUJBQU8sQ0FBQyxJQUFpQztBQUNwRSx3QkFBd0IsbUJBQU8sQ0FBQyxJQUE4Qjs7QUFFOUQsNkJBQTZCLG1CQUFPLENBQUMsSUFBMEI7QUFDL0QsMkJBQTJCLG1CQUFPLENBQUMsSUFBd0I7QUFDM0QsdUJBQXVCLG1CQUFPLENBQUMsSUFBb0I7QUFDbkQsb0JBQW9CLG1CQUFPLENBQUMsSUFBaUI7QUFDN0MsZ0JBQWdCLG1CQUFPLENBQUMsR0FBYTtBQUNyQywyQkFBMkIsbUJBQU8sQ0FBQyxJQUF3QjtBQUMzRCxXQUFXLG1CQUFPLENBQUMsSUFBUTs7QUFFM0I7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsRUFBRTtBQUNGO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsRUFBRTtBQUNGO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL0RlZmluZVByb3BlcnR5T3JUaHJvdy5qcz8wYTEwIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG5cbnZhciBpc1Byb3BlcnR5RGVzY3JpcHRvciA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvaXNQcm9wZXJ0eURlc2NyaXB0b3InKTtcbnZhciBEZWZpbmVPd25Qcm9wZXJ0eSA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvRGVmaW5lT3duUHJvcGVydHknKTtcblxudmFyIEZyb21Qcm9wZXJ0eURlc2NyaXB0b3IgPSByZXF1aXJlKCcuL0Zyb21Qcm9wZXJ0eURlc2NyaXB0b3InKTtcbnZhciBJc0FjY2Vzc29yRGVzY3JpcHRvciA9IHJlcXVpcmUoJy4vSXNBY2Nlc3NvckRlc2NyaXB0b3InKTtcbnZhciBJc0RhdGFEZXNjcmlwdG9yID0gcmVxdWlyZSgnLi9Jc0RhdGFEZXNjcmlwdG9yJyk7XG52YXIgSXNQcm9wZXJ0eUtleSA9IHJlcXVpcmUoJy4vSXNQcm9wZXJ0eUtleScpO1xudmFyIFNhbWVWYWx1ZSA9IHJlcXVpcmUoJy4vU2FtZVZhbHVlJyk7XG52YXIgVG9Qcm9wZXJ0eURlc2NyaXB0b3IgPSByZXF1aXJlKCcuL1RvUHJvcGVydHlEZXNjcmlwdG9yJyk7XG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtZGVmaW5lcHJvcGVydHlvcnRocm93XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gRGVmaW5lUHJvcGVydHlPclRocm93KE8sIFAsIGRlc2MpIHtcblx0aWYgKFR5cGUoTykgIT09ICdPYmplY3QnKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IFR5cGUoTykgaXMgbm90IE9iamVjdCcpO1xuXHR9XG5cblx0aWYgKCFJc1Byb3BlcnR5S2V5KFApKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IElzUHJvcGVydHlLZXkoUCkgaXMgbm90IHRydWUnKTtcblx0fVxuXG5cdHZhciBEZXNjID0gaXNQcm9wZXJ0eURlc2NyaXB0b3Ioe1xuXHRcdFR5cGU6IFR5cGUsXG5cdFx0SXNEYXRhRGVzY3JpcHRvcjogSXNEYXRhRGVzY3JpcHRvcixcblx0XHRJc0FjY2Vzc29yRGVzY3JpcHRvcjogSXNBY2Nlc3NvckRlc2NyaXB0b3Jcblx0fSwgZGVzYykgPyBkZXNjIDogVG9Qcm9wZXJ0eURlc2NyaXB0b3IoZGVzYyk7XG5cdGlmICghaXNQcm9wZXJ0eURlc2NyaXB0b3Ioe1xuXHRcdFR5cGU6IFR5cGUsXG5cdFx0SXNEYXRhRGVzY3JpcHRvcjogSXNEYXRhRGVzY3JpcHRvcixcblx0XHRJc0FjY2Vzc29yRGVzY3JpcHRvcjogSXNBY2Nlc3NvckRlc2NyaXB0b3Jcblx0fSwgRGVzYykpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogRGVzYyBpcyBub3QgYSB2YWxpZCBQcm9wZXJ0eSBEZXNjcmlwdG9yJyk7XG5cdH1cblxuXHRyZXR1cm4gRGVmaW5lT3duUHJvcGVydHkoXG5cdFx0SXNEYXRhRGVzY3JpcHRvcixcblx0XHRTYW1lVmFsdWUsXG5cdFx0RnJvbVByb3BlcnR5RGVzY3JpcHRvcixcblx0XHRPLFxuXHRcdFAsXG5cdFx0RGVzY1xuXHQpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///3950\n")},8334:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar assertRecord = __webpack_require__(2188);\n\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-frompropertydescriptor\n\nmodule.exports = function FromPropertyDescriptor(Desc) {\n\tif (typeof Desc === 'undefined') {\n\t\treturn Desc;\n\t}\n\n\tassertRecord(Type, 'Property Descriptor', 'Desc', Desc);\n\n\tvar obj = {};\n\tif ('[[Value]]' in Desc) {\n\t\tobj.value = Desc['[[Value]]'];\n\t}\n\tif ('[[Writable]]' in Desc) {\n\t\tobj.writable = Desc['[[Writable]]'];\n\t}\n\tif ('[[Get]]' in Desc) {\n\t\tobj.get = Desc['[[Get]]'];\n\t}\n\tif ('[[Set]]' in Desc) {\n\t\tobj.set = Desc['[[Set]]'];\n\t}\n\tif ('[[Enumerable]]' in Desc) {\n\t\tobj.enumerable = Desc['[[Enumerable]]'];\n\t}\n\tif ('[[Configurable]]' in Desc) {\n\t\tobj.configurable = Desc['[[Configurable]]'];\n\t}\n\treturn obj;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODMzNC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxJQUF5Qjs7QUFFcEQsV0FBVyxtQkFBTyxDQUFDLElBQVE7O0FBRTNCOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Gcm9tUHJvcGVydHlEZXNjcmlwdG9yLmpzPzFkZDciXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgYXNzZXJ0UmVjb3JkID0gcmVxdWlyZSgnLi4vaGVscGVycy9hc3NlcnRSZWNvcmQnKTtcblxudmFyIFR5cGUgPSByZXF1aXJlKCcuL1R5cGUnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWZyb21wcm9wZXJ0eWRlc2NyaXB0b3JcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBGcm9tUHJvcGVydHlEZXNjcmlwdG9yKERlc2MpIHtcblx0aWYgKHR5cGVvZiBEZXNjID09PSAndW5kZWZpbmVkJykge1xuXHRcdHJldHVybiBEZXNjO1xuXHR9XG5cblx0YXNzZXJ0UmVjb3JkKFR5cGUsICdQcm9wZXJ0eSBEZXNjcmlwdG9yJywgJ0Rlc2MnLCBEZXNjKTtcblxuXHR2YXIgb2JqID0ge307XG5cdGlmICgnW1tWYWx1ZV1dJyBpbiBEZXNjKSB7XG5cdFx0b2JqLnZhbHVlID0gRGVzY1snW1tWYWx1ZV1dJ107XG5cdH1cblx0aWYgKCdbW1dyaXRhYmxlXV0nIGluIERlc2MpIHtcblx0XHRvYmoud3JpdGFibGUgPSBEZXNjWydbW1dyaXRhYmxlXV0nXTtcblx0fVxuXHRpZiAoJ1tbR2V0XV0nIGluIERlc2MpIHtcblx0XHRvYmouZ2V0ID0gRGVzY1snW1tHZXRdXSddO1xuXHR9XG5cdGlmICgnW1tTZXRdXScgaW4gRGVzYykge1xuXHRcdG9iai5zZXQgPSBEZXNjWydbW1NldF1dJ107XG5cdH1cblx0aWYgKCdbW0VudW1lcmFibGVdXScgaW4gRGVzYykge1xuXHRcdG9iai5lbnVtZXJhYmxlID0gRGVzY1snW1tFbnVtZXJhYmxlXV0nXTtcblx0fVxuXHRpZiAoJ1tbQ29uZmlndXJhYmxlXV0nIGluIERlc2MpIHtcblx0XHRvYmouY29uZmlndXJhYmxlID0gRGVzY1snW1tDb25maWd1cmFibGVdXSddO1xuXHR9XG5cdHJldHVybiBvYmo7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///8334\n")},1391:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar inspect = __webpack_require__(631);\n\nvar IsPropertyKey = __webpack_require__(4305);\nvar Type = __webpack_require__(3633);\n\n/**\n * 7.3.1 Get (O, P) - https://ecma-international.org/ecma-262/6.0/#sec-get-o-p\n * 1. Assert: Type(O) is Object.\n * 2. Assert: IsPropertyKey(P) is true.\n * 3. Return O.[[Get]](P, O).\n */\n\nmodule.exports = function Get(O, P) {\n\t// 7.3.1.1\n\tif (Type(O) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: Type(O) is not Object');\n\t}\n\t// 7.3.1.2\n\tif (!IsPropertyKey(P)) {\n\t\tthrow new $TypeError('Assertion failed: IsPropertyKey(P) is not true, got ' + inspect(P));\n\t}\n\t// 7.3.1.3\n\treturn O[P];\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTM5MS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSxjQUFjLG1CQUFPLENBQUMsR0FBZ0I7O0FBRXRDLG9CQUFvQixtQkFBTyxDQUFDLElBQWlCO0FBQzdDLFdBQVcsbUJBQU8sQ0FBQyxJQUFROztBQUUzQjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL0dldC5qcz9hODc1Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG5cbnZhciBpbnNwZWN0ID0gcmVxdWlyZSgnb2JqZWN0LWluc3BlY3QnKTtcblxudmFyIElzUHJvcGVydHlLZXkgPSByZXF1aXJlKCcuL0lzUHJvcGVydHlLZXknKTtcbnZhciBUeXBlID0gcmVxdWlyZSgnLi9UeXBlJyk7XG5cbi8qKlxuICogNy4zLjEgR2V0IChPLCBQKSAtIGh0dHBzOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi82LjAvI3NlYy1nZXQtby1wXG4gKiAxLiBBc3NlcnQ6IFR5cGUoTykgaXMgT2JqZWN0LlxuICogMi4gQXNzZXJ0OiBJc1Byb3BlcnR5S2V5KFApIGlzIHRydWUuXG4gKiAzLiBSZXR1cm4gTy5bW0dldF1dKFAsIE8pLlxuICovXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gR2V0KE8sIFApIHtcblx0Ly8gNy4zLjEuMVxuXHRpZiAoVHlwZShPKSAhPT0gJ09iamVjdCcpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogVHlwZShPKSBpcyBub3QgT2JqZWN0Jyk7XG5cdH1cblx0Ly8gNy4zLjEuMlxuXHRpZiAoIUlzUHJvcGVydHlLZXkoUCkpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogSXNQcm9wZXJ0eUtleShQKSBpcyBub3QgdHJ1ZSwgZ290ICcgKyBpbnNwZWN0KFApKTtcblx0fVxuXHQvLyA3LjMuMS4zXG5cdHJldHVybiBPW1BdO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///1391\n")},7364:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar GetV = __webpack_require__(8509);\nvar IsCallable = __webpack_require__(1787);\nvar IsPropertyKey = __webpack_require__(4305);\n\n/**\n * 7.3.9 - https://ecma-international.org/ecma-262/6.0/#sec-getmethod\n * 1. Assert: IsPropertyKey(P) is true.\n * 2. Let func be GetV(O, P).\n * 3. ReturnIfAbrupt(func).\n * 4. If func is either undefined or null, return undefined.\n * 5. If IsCallable(func) is false, throw a TypeError exception.\n * 6. Return func.\n */\n\nmodule.exports = function GetMethod(O, P) {\n\t// 7.3.9.1\n\tif (!IsPropertyKey(P)) {\n\t\tthrow new $TypeError('Assertion failed: IsPropertyKey(P) is not true');\n\t}\n\n\t// 7.3.9.2\n\tvar func = GetV(O, P);\n\n\t// 7.3.9.4\n\tif (func == null) {\n\t\treturn void 0;\n\t}\n\n\t// 7.3.9.5\n\tif (!IsCallable(func)) {\n\t\tthrow new $TypeError(P + 'is not a function');\n\t}\n\n\t// 7.3.9.6\n\treturn func;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzM2NC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSxXQUFXLG1CQUFPLENBQUMsSUFBUTtBQUMzQixpQkFBaUIsbUJBQU8sQ0FBQyxJQUFjO0FBQ3ZDLG9CQUFvQixtQkFBTyxDQUFDLElBQWlCOztBQUU3QztBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9HZXRNZXRob2QuanM/NmZiMyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xuXG52YXIgR2V0ViA9IHJlcXVpcmUoJy4vR2V0VicpO1xudmFyIElzQ2FsbGFibGUgPSByZXF1aXJlKCcuL0lzQ2FsbGFibGUnKTtcbnZhciBJc1Byb3BlcnR5S2V5ID0gcmVxdWlyZSgnLi9Jc1Byb3BlcnR5S2V5Jyk7XG5cbi8qKlxuICogNy4zLjkgLSBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtZ2V0bWV0aG9kXG4gKiAxLiBBc3NlcnQ6IElzUHJvcGVydHlLZXkoUCkgaXMgdHJ1ZS5cbiAqIDIuIExldCBmdW5jIGJlIEdldFYoTywgUCkuXG4gKiAzLiBSZXR1cm5JZkFicnVwdChmdW5jKS5cbiAqIDQuIElmIGZ1bmMgaXMgZWl0aGVyIHVuZGVmaW5lZCBvciBudWxsLCByZXR1cm4gdW5kZWZpbmVkLlxuICogNS4gSWYgSXNDYWxsYWJsZShmdW5jKSBpcyBmYWxzZSwgdGhyb3cgYSBUeXBlRXJyb3IgZXhjZXB0aW9uLlxuICogNi4gUmV0dXJuIGZ1bmMuXG4gKi9cblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBHZXRNZXRob2QoTywgUCkge1xuXHQvLyA3LjMuOS4xXG5cdGlmICghSXNQcm9wZXJ0eUtleShQKSkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBJc1Byb3BlcnR5S2V5KFApIGlzIG5vdCB0cnVlJyk7XG5cdH1cblxuXHQvLyA3LjMuOS4yXG5cdHZhciBmdW5jID0gR2V0VihPLCBQKTtcblxuXHQvLyA3LjMuOS40XG5cdGlmIChmdW5jID09IG51bGwpIHtcblx0XHRyZXR1cm4gdm9pZCAwO1xuXHR9XG5cblx0Ly8gNy4zLjkuNVxuXHRpZiAoIUlzQ2FsbGFibGUoZnVuYykpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcihQICsgJ2lzIG5vdCBhIGZ1bmN0aW9uJyk7XG5cdH1cblxuXHQvLyA3LjMuOS42XG5cdHJldHVybiBmdW5jO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///7364\n")},8509:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar IsPropertyKey = __webpack_require__(4305);\nvar ToObject = __webpack_require__(821);\n\n/**\n * 7.3.2 GetV (V, P)\n * 1. Assert: IsPropertyKey(P) is true.\n * 2. Let O be ToObject(V).\n * 3. ReturnIfAbrupt(O).\n * 4. Return O.[[Get]](P, V).\n */\n\nmodule.exports = function GetV(V, P) {\n\t// 7.3.2.1\n\tif (!IsPropertyKey(P)) {\n\t\tthrow new $TypeError('Assertion failed: IsPropertyKey(P) is not true');\n\t}\n\n\t// 7.3.2.2-3\n\tvar O = ToObject(V);\n\n\t// 7.3.2.4\n\treturn O[P];\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODUwOS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSxvQkFBb0IsbUJBQU8sQ0FBQyxJQUFpQjtBQUM3QyxlQUFlLG1CQUFPLENBQUMsR0FBWTs7QUFFbkM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9HZXRWLmpzPzcwYjIiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJFR5cGVFcnJvciA9IEdldEludHJpbnNpYygnJVR5cGVFcnJvciUnKTtcblxudmFyIElzUHJvcGVydHlLZXkgPSByZXF1aXJlKCcuL0lzUHJvcGVydHlLZXknKTtcbnZhciBUb09iamVjdCA9IHJlcXVpcmUoJy4vVG9PYmplY3QnKTtcblxuLyoqXG4gKiA3LjMuMiBHZXRWIChWLCBQKVxuICogMS4gQXNzZXJ0OiBJc1Byb3BlcnR5S2V5KFApIGlzIHRydWUuXG4gKiAyLiBMZXQgTyBiZSBUb09iamVjdChWKS5cbiAqIDMuIFJldHVybklmQWJydXB0KE8pLlxuICogNC4gUmV0dXJuIE8uW1tHZXRdXShQLCBWKS5cbiAqL1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIEdldFYoViwgUCkge1xuXHQvLyA3LjMuMi4xXG5cdGlmICghSXNQcm9wZXJ0eUtleShQKSkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBJc1Byb3BlcnR5S2V5KFApIGlzIG5vdCB0cnVlJyk7XG5cdH1cblxuXHQvLyA3LjMuMi4yLTNcblx0dmFyIE8gPSBUb09iamVjdChWKTtcblxuXHQvLyA3LjMuMi40XG5cdHJldHVybiBPW1BdO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///8509\n")},9527:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar has = __webpack_require__(7642);\n\nvar assertRecord = __webpack_require__(2188);\n\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-isaccessordescriptor\n\nmodule.exports = function IsAccessorDescriptor(Desc) {\n\tif (typeof Desc === 'undefined') {\n\t\treturn false;\n\t}\n\n\tassertRecord(Type, 'Property Descriptor', 'Desc', Desc);\n\n\tif (!has(Desc, '[[Get]]') && !has(Desc, '[[Set]]')) {\n\t\treturn false;\n\t}\n\n\treturn true;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTUyNy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixVQUFVLG1CQUFPLENBQUMsSUFBSzs7QUFFdkIsbUJBQW1CLG1CQUFPLENBQUMsSUFBeUI7O0FBRXBELFdBQVcsbUJBQU8sQ0FBQyxJQUFROztBQUUzQjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Jc0FjY2Vzc29yRGVzY3JpcHRvci5qcz84YWI1Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGhhcyA9IHJlcXVpcmUoJ2hhcycpO1xuXG52YXIgYXNzZXJ0UmVjb3JkID0gcmVxdWlyZSgnLi4vaGVscGVycy9hc3NlcnRSZWNvcmQnKTtcblxudmFyIFR5cGUgPSByZXF1aXJlKCcuL1R5cGUnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWlzYWNjZXNzb3JkZXNjcmlwdG9yXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gSXNBY2Nlc3NvckRlc2NyaXB0b3IoRGVzYykge1xuXHRpZiAodHlwZW9mIERlc2MgPT09ICd1bmRlZmluZWQnKSB7XG5cdFx0cmV0dXJuIGZhbHNlO1xuXHR9XG5cblx0YXNzZXJ0UmVjb3JkKFR5cGUsICdQcm9wZXJ0eSBEZXNjcmlwdG9yJywgJ0Rlc2MnLCBEZXNjKTtcblxuXHRpZiAoIWhhcyhEZXNjLCAnW1tHZXRdXScpICYmICFoYXMoRGVzYywgJ1tbU2V0XV0nKSkge1xuXHRcdHJldHVybiBmYWxzZTtcblx0fVxuXG5cdHJldHVybiB0cnVlO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///9527\n")},6975:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $Array = GetIntrinsic('%Array%');\n\n// eslint-disable-next-line global-require\nvar toStr = !$Array.isArray && __webpack_require__(1924)('Object.prototype.toString');\n\n// https://ecma-international.org/ecma-262/6.0/#sec-isarray\n\nmodule.exports = $Array.isArray || function IsArray(argument) {\n\treturn toStr(argument) === '[object Array]';\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNjk3NS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQTtBQUNBLCtCQUErQixtQkFBTyxDQUFDLElBQXFCOztBQUU1RDs7QUFFQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvSXNBcnJheS5qcz9jMTI1Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRBcnJheSA9IEdldEludHJpbnNpYygnJUFycmF5JScpO1xuXG4vLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgZ2xvYmFsLXJlcXVpcmVcbnZhciB0b1N0ciA9ICEkQXJyYXkuaXNBcnJheSAmJiByZXF1aXJlKCdjYWxsLWJpbmQvY2FsbEJvdW5kJykoJ09iamVjdC5wcm90b3R5cGUudG9TdHJpbmcnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWlzYXJyYXlcblxubW9kdWxlLmV4cG9ydHMgPSAkQXJyYXkuaXNBcnJheSB8fCBmdW5jdGlvbiBJc0FycmF5KGFyZ3VtZW50KSB7XG5cdHJldHVybiB0b1N0cihhcmd1bWVudCkgPT09ICdbb2JqZWN0IEFycmF5XSc7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///6975\n")},1787:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\n// http://262.ecma-international.org/5.1/#sec-9.11\n\nmodule.exports = __webpack_require__(5320);\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTc4Ny5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQSwwQ0FBdUMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvSXNDYWxsYWJsZS5qcz81NTA4Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxuLy8gaHR0cDovLzI2Mi5lY21hLWludGVybmF0aW9uYWwub3JnLzUuMS8jc2VjLTkuMTFcblxubW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKCdpcy1jYWxsYWJsZScpO1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///1787\n")},1974:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(4445);\n\nvar $construct = GetIntrinsic('%Reflect.construct%', true);\n\nvar DefinePropertyOrThrow = __webpack_require__(3950);\ntry {\n\tDefinePropertyOrThrow({}, '', { '[[Get]]': function () {} });\n} catch (e) {\n\t// Accessor properties aren't supported\n\tDefinePropertyOrThrow = null;\n}\n\n// https://ecma-international.org/ecma-262/6.0/#sec-isconstructor\n\nif (DefinePropertyOrThrow && $construct) {\n\tvar isConstructorMarker = {};\n\tvar badArrayLike = {};\n\tDefinePropertyOrThrow(badArrayLike, 'length', {\n\t\t'[[Get]]': function () {\n\t\t\tthrow isConstructorMarker;\n\t\t},\n\t\t'[[Enumerable]]': true\n\t});\n\n\tmodule.exports = function IsConstructor(argument) {\n\t\ttry {\n\t\t\t// `Reflect.construct` invokes `IsConstructor(target)` before `Get(args, 'length')`:\n\t\t\t$construct(argument, badArrayLike);\n\t\t} catch (err) {\n\t\t\treturn err === isConstructorMarker;\n\t\t}\n\t};\n} else {\n\tmodule.exports = function IsConstructor(argument) {\n\t\t// unfortunately there's no way to truly check this without try/catch `new argument` in old environments\n\t\treturn typeof argument === 'function' && !!argument.prototype;\n\t};\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTk3NC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxJQUFvQjs7QUFFL0M7O0FBRUEsNEJBQTRCLG1CQUFPLENBQUMsSUFBeUI7QUFDN0Q7QUFDQSx5QkFBeUIsUUFBUSwyQkFBMkI7QUFDNUQsRUFBRTtBQUNGO0FBQ0E7QUFDQTs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0g7QUFDQSxFQUFFOztBQUVGO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSTtBQUNKO0FBQ0E7QUFDQTtBQUNBLEVBQUU7QUFDRjtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL0lzQ29uc3RydWN0b3IuanM/ZTc0NyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCcuLi9HZXRJbnRyaW5zaWMuanMnKTtcblxudmFyICRjb25zdHJ1Y3QgPSBHZXRJbnRyaW5zaWMoJyVSZWZsZWN0LmNvbnN0cnVjdCUnLCB0cnVlKTtcblxudmFyIERlZmluZVByb3BlcnR5T3JUaHJvdyA9IHJlcXVpcmUoJy4vRGVmaW5lUHJvcGVydHlPclRocm93Jyk7XG50cnkge1xuXHREZWZpbmVQcm9wZXJ0eU9yVGhyb3coe30sICcnLCB7ICdbW0dldF1dJzogZnVuY3Rpb24gKCkge30gfSk7XG59IGNhdGNoIChlKSB7XG5cdC8vIEFjY2Vzc29yIHByb3BlcnRpZXMgYXJlbid0IHN1cHBvcnRlZFxuXHREZWZpbmVQcm9wZXJ0eU9yVGhyb3cgPSBudWxsO1xufVxuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtaXNjb25zdHJ1Y3RvclxuXG5pZiAoRGVmaW5lUHJvcGVydHlPclRocm93ICYmICRjb25zdHJ1Y3QpIHtcblx0dmFyIGlzQ29uc3RydWN0b3JNYXJrZXIgPSB7fTtcblx0dmFyIGJhZEFycmF5TGlrZSA9IHt9O1xuXHREZWZpbmVQcm9wZXJ0eU9yVGhyb3coYmFkQXJyYXlMaWtlLCAnbGVuZ3RoJywge1xuXHRcdCdbW0dldF1dJzogZnVuY3Rpb24gKCkge1xuXHRcdFx0dGhyb3cgaXNDb25zdHJ1Y3Rvck1hcmtlcjtcblx0XHR9LFxuXHRcdCdbW0VudW1lcmFibGVdXSc6IHRydWVcblx0fSk7XG5cblx0bW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBJc0NvbnN0cnVjdG9yKGFyZ3VtZW50KSB7XG5cdFx0dHJ5IHtcblx0XHRcdC8vIGBSZWZsZWN0LmNvbnN0cnVjdGAgaW52b2tlcyBgSXNDb25zdHJ1Y3Rvcih0YXJnZXQpYCBiZWZvcmUgYEdldChhcmdzLCAnbGVuZ3RoJylgOlxuXHRcdFx0JGNvbnN0cnVjdChhcmd1bWVudCwgYmFkQXJyYXlMaWtlKTtcblx0XHR9IGNhdGNoIChlcnIpIHtcblx0XHRcdHJldHVybiBlcnIgPT09IGlzQ29uc3RydWN0b3JNYXJrZXI7XG5cdFx0fVxuXHR9O1xufSBlbHNlIHtcblx0bW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBJc0NvbnN0cnVjdG9yKGFyZ3VtZW50KSB7XG5cdFx0Ly8gdW5mb3J0dW5hdGVseSB0aGVyZSdzIG5vIHdheSB0byB0cnVseSBjaGVjayB0aGlzIHdpdGhvdXQgdHJ5L2NhdGNoIGBuZXcgYXJndW1lbnRgIGluIG9sZCBlbnZpcm9ubWVudHNcblx0XHRyZXR1cm4gdHlwZW9mIGFyZ3VtZW50ID09PSAnZnVuY3Rpb24nICYmICEhYXJndW1lbnQucHJvdG90eXBlO1xuXHR9O1xufVxuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///1974\n")},3746:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar has = __webpack_require__(7642);\n\nvar assertRecord = __webpack_require__(2188);\n\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-isdatadescriptor\n\nmodule.exports = function IsDataDescriptor(Desc) {\n\tif (typeof Desc === 'undefined') {\n\t\treturn false;\n\t}\n\n\tassertRecord(Type, 'Property Descriptor', 'Desc', Desc);\n\n\tif (!has(Desc, '[[Value]]') && !has(Desc, '[[Writable]]')) {\n\t\treturn false;\n\t}\n\n\treturn true;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzc0Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixVQUFVLG1CQUFPLENBQUMsSUFBSzs7QUFFdkIsbUJBQW1CLG1CQUFPLENBQUMsSUFBeUI7O0FBRXBELFdBQVcsbUJBQU8sQ0FBQyxJQUFROztBQUUzQjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Jc0RhdGFEZXNjcmlwdG9yLmpzP2EwYmEiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgaGFzID0gcmVxdWlyZSgnaGFzJyk7XG5cbnZhciBhc3NlcnRSZWNvcmQgPSByZXF1aXJlKCcuLi9oZWxwZXJzL2Fzc2VydFJlY29yZCcpO1xuXG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtaXNkYXRhZGVzY3JpcHRvclxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIElzRGF0YURlc2NyaXB0b3IoRGVzYykge1xuXHRpZiAodHlwZW9mIERlc2MgPT09ICd1bmRlZmluZWQnKSB7XG5cdFx0cmV0dXJuIGZhbHNlO1xuXHR9XG5cblx0YXNzZXJ0UmVjb3JkKFR5cGUsICdQcm9wZXJ0eSBEZXNjcmlwdG9yJywgJ0Rlc2MnLCBEZXNjKTtcblxuXHRpZiAoIWhhcyhEZXNjLCAnW1tWYWx1ZV1dJykgJiYgIWhhcyhEZXNjLCAnW1tXcml0YWJsZV1dJykpIHtcblx0XHRyZXR1cm4gZmFsc2U7XG5cdH1cblxuXHRyZXR1cm4gdHJ1ZTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3746\n")},7312:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar abs = __webpack_require__(4908);\nvar floor = __webpack_require__(375);\nvar Type = __webpack_require__(3633);\n\nvar $isNaN = __webpack_require__(9086);\nvar $isFinite = __webpack_require__(2633);\n\n// https://tc39.es/ecma262/#sec-isintegralnumber\n\nmodule.exports = function IsIntegralNumber(argument) {\n\tif (Type(argument) !== 'Number' || $isNaN(argument) || !$isFinite(argument)) {\n\t\treturn false;\n\t}\n\tvar absValue = abs(argument);\n\treturn floor(absValue) === absValue;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzMxMi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixVQUFVLG1CQUFPLENBQUMsSUFBTztBQUN6QixZQUFZLG1CQUFPLENBQUMsR0FBUztBQUM3QixXQUFXLG1CQUFPLENBQUMsSUFBUTs7QUFFM0IsYUFBYSxtQkFBTyxDQUFDLElBQWtCO0FBQ3ZDLGdCQUFnQixtQkFBTyxDQUFDLElBQXFCOztBQUU3Qzs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Jc0ludGVncmFsTnVtYmVyLmpzPzZhYjEiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgYWJzID0gcmVxdWlyZSgnLi9hYnMnKTtcbnZhciBmbG9vciA9IHJlcXVpcmUoJy4vZmxvb3InKTtcbnZhciBUeXBlID0gcmVxdWlyZSgnLi9UeXBlJyk7XG5cbnZhciAkaXNOYU4gPSByZXF1aXJlKCcuLi9oZWxwZXJzL2lzTmFOJyk7XG52YXIgJGlzRmluaXRlID0gcmVxdWlyZSgnLi4vaGVscGVycy9pc0Zpbml0ZScpO1xuXG4vLyBodHRwczovL3RjMzkuZXMvZWNtYTI2Mi8jc2VjLWlzaW50ZWdyYWxudW1iZXJcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBJc0ludGVncmFsTnVtYmVyKGFyZ3VtZW50KSB7XG5cdGlmIChUeXBlKGFyZ3VtZW50KSAhPT0gJ051bWJlcicgfHwgJGlzTmFOKGFyZ3VtZW50KSB8fCAhJGlzRmluaXRlKGFyZ3VtZW50KSkge1xuXHRcdHJldHVybiBmYWxzZTtcblx0fVxuXHR2YXIgYWJzVmFsdWUgPSBhYnMoYXJndW1lbnQpO1xuXHRyZXR1cm4gZmxvb3IoYWJzVmFsdWUpID09PSBhYnNWYWx1ZTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///7312\n")},4305:function(module){"use strict";eval("\n\n// https://ecma-international.org/ecma-262/6.0/#sec-ispropertykey\n\nmodule.exports = function IsPropertyKey(argument) {\n\treturn typeof argument === 'string' || typeof argument === 'symbol';\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDMwNS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvSXNQcm9wZXJ0eUtleS5qcz9lMzE3Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWlzcHJvcGVydHlrZXlcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBJc1Byb3BlcnR5S2V5KGFyZ3VtZW50KSB7XG5cdHJldHVybiB0eXBlb2YgYXJndW1lbnQgPT09ICdzdHJpbmcnIHx8IHR5cGVvZiBhcmd1bWVudCA9PT0gJ3N5bWJvbCc7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///4305\n")},840:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $match = GetIntrinsic('%Symbol.match%', true);\n\nvar hasRegExpMatcher = __webpack_require__(8420);\n\nvar ToBoolean = __webpack_require__(9731);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-isregexp\n\nmodule.exports = function IsRegExp(argument) {\n\tif (!argument || typeof argument !== 'object') {\n\t\treturn false;\n\t}\n\tif ($match) {\n\t\tvar isRegExp = argument[$match];\n\t\tif (typeof isRegExp !== 'undefined') {\n\t\t\treturn ToBoolean(isRegExp);\n\t\t}\n\t}\n\treturn hasRegExpMatcher(argument);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODQwLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWU7O0FBRTFDOztBQUVBLHVCQUF1QixtQkFBTyxDQUFDLElBQVU7O0FBRXpDLGdCQUFnQixtQkFBTyxDQUFDLElBQWE7O0FBRXJDOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Jc1JlZ0V4cC5qcz8xMjA2Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRtYXRjaCA9IEdldEludHJpbnNpYygnJVN5bWJvbC5tYXRjaCUnLCB0cnVlKTtcblxudmFyIGhhc1JlZ0V4cE1hdGNoZXIgPSByZXF1aXJlKCdpcy1yZWdleCcpO1xuXG52YXIgVG9Cb29sZWFuID0gcmVxdWlyZSgnLi9Ub0Jvb2xlYW4nKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWlzcmVnZXhwXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gSXNSZWdFeHAoYXJndW1lbnQpIHtcblx0aWYgKCFhcmd1bWVudCB8fCB0eXBlb2YgYXJndW1lbnQgIT09ICdvYmplY3QnKSB7XG5cdFx0cmV0dXJuIGZhbHNlO1xuXHR9XG5cdGlmICgkbWF0Y2gpIHtcblx0XHR2YXIgaXNSZWdFeHAgPSBhcmd1bWVudFskbWF0Y2hdO1xuXHRcdGlmICh0eXBlb2YgaXNSZWdFeHAgIT09ICd1bmRlZmluZWQnKSB7XG5cdFx0XHRyZXR1cm4gVG9Cb29sZWFuKGlzUmVnRXhwKTtcblx0XHR9XG5cdH1cblx0cmV0dXJuIGhhc1JlZ0V4cE1hdGNoZXIoYXJndW1lbnQpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///840\n")},953:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $ObjectCreate = GetIntrinsic('%Object.create%', true);\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar $SyntaxError = GetIntrinsic('%SyntaxError%');\n\nvar IsArray = __webpack_require__(6975);\nvar Type = __webpack_require__(3633);\n\nvar hasProto = !({ __proto__: null } instanceof Object);\n\n// https://262.ecma-international.org/6.0/#sec-objectcreate\n\nmodule.exports = function OrdinaryObjectCreate(proto) {\n\tif (proto !== null && Type(proto) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: `proto` must be null or an object');\n\t}\n\tvar additionalInternalSlotsList = arguments.length < 2 ? [] : arguments[1];\n\tif (!IsArray(additionalInternalSlotsList)) {\n\t\tthrow new $TypeError('Assertion failed: `additionalInternalSlotsList` must be an Array');\n\t}\n\t// var internalSlotsList = ['[[Prototype]]', '[[Extensible]]'];\n\tif (additionalInternalSlotsList.length > 0) {\n\t\tthrow new $SyntaxError('es-abstract does not yet support internal slots');\n\t\t// internalSlotsList.push(...additionalInternalSlotsList);\n\t}\n\t// var O = MakeBasicObject(internalSlotsList);\n\t// setProto(O, proto);\n\t// return O;\n\n\tif ($ObjectCreate) {\n\t\treturn $ObjectCreate(proto);\n\t}\n\tif (hasProto) {\n\t\treturn { __proto__: proto };\n\t}\n\n\tif (proto === null) {\n\t\tthrow new $SyntaxError('native Object.create support is required to create null objects');\n\t}\n\tvar T = function T() {};\n\tT.prototype = proto;\n\treturn new T();\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTUzLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWU7O0FBRTFDO0FBQ0E7QUFDQTs7QUFFQSxjQUFjLG1CQUFPLENBQUMsSUFBVztBQUNqQyxXQUFXLG1CQUFPLENBQUMsSUFBUTs7QUFFM0IsbUJBQW1CLGtCQUFrQjs7QUFFckM7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQSxXQUFXO0FBQ1g7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvT3JkaW5hcnlPYmplY3RDcmVhdGUuanM/NzRmZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkT2JqZWN0Q3JlYXRlID0gR2V0SW50cmluc2ljKCclT2JqZWN0LmNyZWF0ZSUnLCB0cnVlKTtcbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xudmFyICRTeW50YXhFcnJvciA9IEdldEludHJpbnNpYygnJVN5bnRheEVycm9yJScpO1xuXG52YXIgSXNBcnJheSA9IHJlcXVpcmUoJy4vSXNBcnJheScpO1xudmFyIFR5cGUgPSByZXF1aXJlKCcuL1R5cGUnKTtcblxudmFyIGhhc1Byb3RvID0gISh7IF9fcHJvdG9fXzogbnVsbCB9IGluc3RhbmNlb2YgT2JqZWN0KTtcblxuLy8gaHR0cHM6Ly8yNjIuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy82LjAvI3NlYy1vYmplY3RjcmVhdGVcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBPcmRpbmFyeU9iamVjdENyZWF0ZShwcm90bykge1xuXHRpZiAocHJvdG8gIT09IG51bGwgJiYgVHlwZShwcm90bykgIT09ICdPYmplY3QnKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IGBwcm90b2AgbXVzdCBiZSBudWxsIG9yIGFuIG9iamVjdCcpO1xuXHR9XG5cdHZhciBhZGRpdGlvbmFsSW50ZXJuYWxTbG90c0xpc3QgPSBhcmd1bWVudHMubGVuZ3RoIDwgMiA/IFtdIDogYXJndW1lbnRzWzFdO1xuXHRpZiAoIUlzQXJyYXkoYWRkaXRpb25hbEludGVybmFsU2xvdHNMaXN0KSkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBgYWRkaXRpb25hbEludGVybmFsU2xvdHNMaXN0YCBtdXN0IGJlIGFuIEFycmF5Jyk7XG5cdH1cblx0Ly8gdmFyIGludGVybmFsU2xvdHNMaXN0ID0gWydbW1Byb3RvdHlwZV1dJywgJ1tbRXh0ZW5zaWJsZV1dJ107XG5cdGlmIChhZGRpdGlvbmFsSW50ZXJuYWxTbG90c0xpc3QubGVuZ3RoID4gMCkge1xuXHRcdHRocm93IG5ldyAkU3ludGF4RXJyb3IoJ2VzLWFic3RyYWN0IGRvZXMgbm90IHlldCBzdXBwb3J0IGludGVybmFsIHNsb3RzJyk7XG5cdFx0Ly8gaW50ZXJuYWxTbG90c0xpc3QucHVzaCguLi5hZGRpdGlvbmFsSW50ZXJuYWxTbG90c0xpc3QpO1xuXHR9XG5cdC8vIHZhciBPID0gTWFrZUJhc2ljT2JqZWN0KGludGVybmFsU2xvdHNMaXN0KTtcblx0Ly8gc2V0UHJvdG8oTywgcHJvdG8pO1xuXHQvLyByZXR1cm4gTztcblxuXHRpZiAoJE9iamVjdENyZWF0ZSkge1xuXHRcdHJldHVybiAkT2JqZWN0Q3JlYXRlKHByb3RvKTtcblx0fVxuXHRpZiAoaGFzUHJvdG8pIHtcblx0XHRyZXR1cm4geyBfX3Byb3RvX186IHByb3RvIH07XG5cdH1cblxuXHRpZiAocHJvdG8gPT09IG51bGwpIHtcblx0XHR0aHJvdyBuZXcgJFN5bnRheEVycm9yKCduYXRpdmUgT2JqZWN0LmNyZWF0ZSBzdXBwb3J0IGlzIHJlcXVpcmVkIHRvIGNyZWF0ZSBudWxsIG9iamVjdHMnKTtcblx0fVxuXHR2YXIgVCA9IGZ1bmN0aW9uIFQoKSB7fTtcblx0VC5wcm90b3R5cGUgPSBwcm90bztcblx0cmV0dXJuIG5ldyBUKCk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///953\n")},6258:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar regexExec = __webpack_require__(1924)('RegExp.prototype.exec');\n\nvar Call = __webpack_require__(581);\nvar Get = __webpack_require__(1391);\nvar IsCallable = __webpack_require__(1787);\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-regexpexec\n\nmodule.exports = function RegExpExec(R, S) {\n\tif (Type(R) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: `R` must be an Object');\n\t}\n\tif (Type(S) !== 'String') {\n\t\tthrow new $TypeError('Assertion failed: `S` must be a String');\n\t}\n\tvar exec = Get(R, 'exec');\n\tif (IsCallable(exec)) {\n\t\tvar result = Call(exec, R, [S]);\n\t\tif (result === null || Type(result) === 'Object') {\n\t\t\treturn result;\n\t\t}\n\t\tthrow new $TypeError('\"exec\" method must return `null` or an Object');\n\t}\n\treturn regexExec(R, S);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNjI1OC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSxnQkFBZ0IsbUJBQU8sQ0FBQyxJQUFxQjs7QUFFN0MsV0FBVyxtQkFBTyxDQUFDLEdBQVE7QUFDM0IsVUFBVSxtQkFBTyxDQUFDLElBQU87QUFDekIsaUJBQWlCLG1CQUFPLENBQUMsSUFBYztBQUN2QyxXQUFXLG1CQUFPLENBQUMsSUFBUTs7QUFFM0I7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9SZWdFeHBFeGVjLmpzPzU4ZGMiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJFR5cGVFcnJvciA9IEdldEludHJpbnNpYygnJVR5cGVFcnJvciUnKTtcblxudmFyIHJlZ2V4RXhlYyA9IHJlcXVpcmUoJ2NhbGwtYmluZC9jYWxsQm91bmQnKSgnUmVnRXhwLnByb3RvdHlwZS5leGVjJyk7XG5cbnZhciBDYWxsID0gcmVxdWlyZSgnLi9DYWxsJyk7XG52YXIgR2V0ID0gcmVxdWlyZSgnLi9HZXQnKTtcbnZhciBJc0NhbGxhYmxlID0gcmVxdWlyZSgnLi9Jc0NhbGxhYmxlJyk7XG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtcmVnZXhwZXhlY1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFJlZ0V4cEV4ZWMoUiwgUykge1xuXHRpZiAoVHlwZShSKSAhPT0gJ09iamVjdCcpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogYFJgIG11c3QgYmUgYW4gT2JqZWN0Jyk7XG5cdH1cblx0aWYgKFR5cGUoUykgIT09ICdTdHJpbmcnKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IGBTYCBtdXN0IGJlIGEgU3RyaW5nJyk7XG5cdH1cblx0dmFyIGV4ZWMgPSBHZXQoUiwgJ2V4ZWMnKTtcblx0aWYgKElzQ2FsbGFibGUoZXhlYykpIHtcblx0XHR2YXIgcmVzdWx0ID0gQ2FsbChleGVjLCBSLCBbU10pO1xuXHRcdGlmIChyZXN1bHQgPT09IG51bGwgfHwgVHlwZShyZXN1bHQpID09PSAnT2JqZWN0Jykge1xuXHRcdFx0cmV0dXJuIHJlc3VsdDtcblx0XHR9XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ1wiZXhlY1wiIG1ldGhvZCBtdXN0IHJldHVybiBgbnVsbGAgb3IgYW4gT2JqZWN0Jyk7XG5cdH1cblx0cmV0dXJuIHJlZ2V4RXhlYyhSLCBTKTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///6258\n")},9619:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nmodule.exports = __webpack_require__(4559);\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTYxOS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYiwwQ0FBcUQiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvUmVxdWlyZU9iamVjdENvZXJjaWJsZS5qcz8wMWJjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxubW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKCcuLi81L0NoZWNrT2JqZWN0Q29lcmNpYmxlJyk7XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///9619\n")},484:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar $isNaN = __webpack_require__(9086);\n\n// http://262.ecma-international.org/5.1/#sec-9.12\n\nmodule.exports = function SameValue(x, y) {\n\tif (x === y) { // 0 === -0, but they are not identical.\n\t\tif (x === 0) { return 1 / x === 1 / y; }\n\t\treturn true;\n\t}\n\treturn $isNaN(x) && $isNaN(y);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDg0LmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLGFBQWEsbUJBQU8sQ0FBQyxJQUFrQjs7QUFFdkM7O0FBRUE7QUFDQSxnQkFBZ0I7QUFDaEIsaUJBQWlCO0FBQ2pCO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL1NhbWVWYWx1ZS5qcz80MzZlIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyICRpc05hTiA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvaXNOYU4nKTtcblxuLy8gaHR0cDovLzI2Mi5lY21hLWludGVybmF0aW9uYWwub3JnLzUuMS8jc2VjLTkuMTJcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBTYW1lVmFsdWUoeCwgeSkge1xuXHRpZiAoeCA9PT0geSkgeyAvLyAwID09PSAtMCwgYnV0IHRoZXkgYXJlIG5vdCBpZGVudGljYWwuXG5cdFx0aWYgKHggPT09IDApIHsgcmV0dXJuIDEgLyB4ID09PSAxIC8geTsgfVxuXHRcdHJldHVybiB0cnVlO1xuXHR9XG5cdHJldHVybiAkaXNOYU4oeCkgJiYgJGlzTmFOKHkpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///484\n")},105:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar IsPropertyKey = __webpack_require__(4305);\nvar SameValue = __webpack_require__(484);\nvar Type = __webpack_require__(3633);\n\n// IE 9 does not throw in strict mode when writability/configurability/extensibility is violated\nvar noThrowOnStrictViolation = (function () {\n\ttry {\n\t\tdelete [].length;\n\t\treturn true;\n\t} catch (e) {\n\t\treturn false;\n\t}\n}());\n\n// https://ecma-international.org/ecma-262/6.0/#sec-set-o-p-v-throw\n\nmodule.exports = function Set(O, P, V, Throw) {\n\tif (Type(O) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: `O` must be an Object');\n\t}\n\tif (!IsPropertyKey(P)) {\n\t\tthrow new $TypeError('Assertion failed: `P` must be a Property Key');\n\t}\n\tif (Type(Throw) !== 'Boolean') {\n\t\tthrow new $TypeError('Assertion failed: `Throw` must be a Boolean');\n\t}\n\tif (Throw) {\n\t\tO[P] = V; // eslint-disable-line no-param-reassign\n\t\tif (noThrowOnStrictViolation && !SameValue(O[P], V)) {\n\t\t\tthrow new $TypeError('Attempted to assign to readonly property.');\n\t\t}\n\t\treturn true;\n\t}\n\ttry {\n\t\tO[P] = V; // eslint-disable-line no-param-reassign\n\t\treturn noThrowOnStrictViolation ? SameValue(O[P], V) : true;\n\t} catch (e) {\n\t\treturn false;\n\t}\n\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTA1LmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWU7O0FBRTFDOztBQUVBLG9CQUFvQixtQkFBTyxDQUFDLElBQWlCO0FBQzdDLGdCQUFnQixtQkFBTyxDQUFDLEdBQWE7QUFDckMsV0FBVyxtQkFBTyxDQUFDLElBQVE7O0FBRTNCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0g7QUFDQTtBQUNBLENBQUM7O0FBRUQ7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLFlBQVk7QUFDWjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxZQUFZO0FBQ1o7QUFDQSxHQUFHO0FBQ0g7QUFDQTs7QUFFQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9TZXQuanM/MjdmZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xuXG52YXIgSXNQcm9wZXJ0eUtleSA9IHJlcXVpcmUoJy4vSXNQcm9wZXJ0eUtleScpO1xudmFyIFNhbWVWYWx1ZSA9IHJlcXVpcmUoJy4vU2FtZVZhbHVlJyk7XG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG4vLyBJRSA5IGRvZXMgbm90IHRocm93IGluIHN0cmljdCBtb2RlIHdoZW4gd3JpdGFiaWxpdHkvY29uZmlndXJhYmlsaXR5L2V4dGVuc2liaWxpdHkgaXMgdmlvbGF0ZWRcbnZhciBub1Rocm93T25TdHJpY3RWaW9sYXRpb24gPSAoZnVuY3Rpb24gKCkge1xuXHR0cnkge1xuXHRcdGRlbGV0ZSBbXS5sZW5ndGg7XG5cdFx0cmV0dXJuIHRydWU7XG5cdH0gY2F0Y2ggKGUpIHtcblx0XHRyZXR1cm4gZmFsc2U7XG5cdH1cbn0oKSk7XG5cbi8vIGh0dHBzOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi82LjAvI3NlYy1zZXQtby1wLXYtdGhyb3dcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBTZXQoTywgUCwgViwgVGhyb3cpIHtcblx0aWYgKFR5cGUoTykgIT09ICdPYmplY3QnKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IGBPYCBtdXN0IGJlIGFuIE9iamVjdCcpO1xuXHR9XG5cdGlmICghSXNQcm9wZXJ0eUtleShQKSkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBgUGAgbXVzdCBiZSBhIFByb3BlcnR5IEtleScpO1xuXHR9XG5cdGlmIChUeXBlKFRocm93KSAhPT0gJ0Jvb2xlYW4nKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IGBUaHJvd2AgbXVzdCBiZSBhIEJvb2xlYW4nKTtcblx0fVxuXHRpZiAoVGhyb3cpIHtcblx0XHRPW1BdID0gVjsgLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1wYXJhbS1yZWFzc2lnblxuXHRcdGlmIChub1Rocm93T25TdHJpY3RWaW9sYXRpb24gJiYgIVNhbWVWYWx1ZShPW1BdLCBWKSkge1xuXHRcdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0F0dGVtcHRlZCB0byBhc3NpZ24gdG8gcmVhZG9ubHkgcHJvcGVydHkuJyk7XG5cdFx0fVxuXHRcdHJldHVybiB0cnVlO1xuXHR9XG5cdHRyeSB7XG5cdFx0T1tQXSA9IFY7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tcGFyYW0tcmVhc3NpZ25cblx0XHRyZXR1cm4gbm9UaHJvd09uU3RyaWN0VmlvbGF0aW9uID8gU2FtZVZhbHVlKE9bUF0sIFYpIDogdHJ1ZTtcblx0fSBjYXRjaCAoZSkge1xuXHRcdHJldHVybiBmYWxzZTtcblx0fVxuXG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///105\n")},9655:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $species = GetIntrinsic('%Symbol.species%', true);\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar IsConstructor = __webpack_require__(1974);\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-speciesconstructor\n\nmodule.exports = function SpeciesConstructor(O, defaultConstructor) {\n\tif (Type(O) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: Type(O) is not Object');\n\t}\n\tvar C = O.constructor;\n\tif (typeof C === 'undefined') {\n\t\treturn defaultConstructor;\n\t}\n\tif (Type(C) !== 'Object') {\n\t\tthrow new $TypeError('O.constructor is not an Object');\n\t}\n\tvar S = $species ? C[$species] : void 0;\n\tif (S == null) {\n\t\treturn defaultConstructor;\n\t}\n\tif (IsConstructor(S)) {\n\t\treturn S;\n\t}\n\tthrow new $TypeError('no constructor found');\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTY1NS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBOztBQUVBLG9CQUFvQixtQkFBTyxDQUFDLElBQWlCO0FBQzdDLFdBQVcsbUJBQU8sQ0FBQyxJQUFROztBQUUzQjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL1NwZWNpZXNDb25zdHJ1Y3Rvci5qcz82MzAxIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRzcGVjaWVzID0gR2V0SW50cmluc2ljKCclU3ltYm9sLnNwZWNpZXMlJywgdHJ1ZSk7XG52YXIgJFR5cGVFcnJvciA9IEdldEludHJpbnNpYygnJVR5cGVFcnJvciUnKTtcblxudmFyIElzQ29uc3RydWN0b3IgPSByZXF1aXJlKCcuL0lzQ29uc3RydWN0b3InKTtcbnZhciBUeXBlID0gcmVxdWlyZSgnLi9UeXBlJyk7XG5cbi8vIGh0dHBzOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi82LjAvI3NlYy1zcGVjaWVzY29uc3RydWN0b3JcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBTcGVjaWVzQ29uc3RydWN0b3IoTywgZGVmYXVsdENvbnN0cnVjdG9yKSB7XG5cdGlmIChUeXBlKE8pICE9PSAnT2JqZWN0Jykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBUeXBlKE8pIGlzIG5vdCBPYmplY3QnKTtcblx0fVxuXHR2YXIgQyA9IE8uY29uc3RydWN0b3I7XG5cdGlmICh0eXBlb2YgQyA9PT0gJ3VuZGVmaW5lZCcpIHtcblx0XHRyZXR1cm4gZGVmYXVsdENvbnN0cnVjdG9yO1xuXHR9XG5cdGlmIChUeXBlKEMpICE9PSAnT2JqZWN0Jykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdPLmNvbnN0cnVjdG9yIGlzIG5vdCBhbiBPYmplY3QnKTtcblx0fVxuXHR2YXIgUyA9ICRzcGVjaWVzID8gQ1skc3BlY2llc10gOiB2b2lkIDA7XG5cdGlmIChTID09IG51bGwpIHtcblx0XHRyZXR1cm4gZGVmYXVsdENvbnN0cnVjdG9yO1xuXHR9XG5cdGlmIChJc0NvbnN0cnVjdG9yKFMpKSB7XG5cdFx0cmV0dXJuIFM7XG5cdH1cblx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ25vIGNvbnN0cnVjdG9yIGZvdW5kJyk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///9655\n")},9731:function(module){"use strict";eval("\n\n// http://262.ecma-international.org/5.1/#sec-9.2\n\nmodule.exports = function ToBoolean(value) { return !!value; };\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTczMS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQSw2Q0FBNkMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvVG9Cb29sZWFuLmpzPzNhMGQiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG4vLyBodHRwOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvNS4xLyNzZWMtOS4yXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVG9Cb29sZWFuKHZhbHVlKSB7IHJldHVybiAhIXZhbHVlOyB9O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///9731\n")},751:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar ES5ToInteger = __webpack_require__(775);\n\nvar ToNumber = __webpack_require__(5631);\n\n// https://www.ecma-international.org/ecma-262/11.0/#sec-tointeger\n\nmodule.exports = function ToInteger(value) {\n\tvar number = ToNumber(value);\n\tif (number !== 0) {\n\t\tnumber = ES5ToInteger(number);\n\t}\n\treturn number === 0 ? 0 : number;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzUxLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWdCOztBQUUzQyxlQUFlLG1CQUFPLENBQUMsSUFBWTs7QUFFbkM7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvVG9JbnRlZ2VyT3JJbmZpbml0eS5qcz84MmNjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEVTNVRvSW50ZWdlciA9IHJlcXVpcmUoJy4uLzUvVG9JbnRlZ2VyJyk7XG5cbnZhciBUb051bWJlciA9IHJlcXVpcmUoJy4vVG9OdW1iZXInKTtcblxuLy8gaHR0cHM6Ly93d3cuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi8xMS4wLyNzZWMtdG9pbnRlZ2VyXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVG9JbnRlZ2VyKHZhbHVlKSB7XG5cdHZhciBudW1iZXIgPSBUb051bWJlcih2YWx1ZSk7XG5cdGlmIChudW1iZXIgIT09IDApIHtcblx0XHRudW1iZXIgPSBFUzVUb0ludGVnZXIobnVtYmVyKTtcblx0fVxuXHRyZXR1cm4gbnVtYmVyID09PSAwID8gMCA6IG51bWJlcjtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///751\n")},8305:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar MAX_SAFE_INTEGER = __webpack_require__(1645);\n\nvar ToIntegerOrInfinity = __webpack_require__(751);\n\nmodule.exports = function ToLength(argument) {\n\tvar len = ToIntegerOrInfinity(argument);\n\tif (len <= 0) { return 0; } // includes converting -0 to +0\n\tif (len > MAX_SAFE_INTEGER) { return MAX_SAFE_INTEGER; }\n\treturn len;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODMwNS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYix1QkFBdUIsbUJBQU8sQ0FBQyxJQUEyQjs7QUFFMUQsMEJBQTBCLG1CQUFPLENBQUMsR0FBdUI7O0FBRXpEO0FBQ0E7QUFDQSxpQkFBaUIsWUFBWTtBQUM3QiwrQkFBK0I7QUFDL0I7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Ub0xlbmd0aC5qcz9mZWRlIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIE1BWF9TQUZFX0lOVEVHRVIgPSByZXF1aXJlKCcuLi9oZWxwZXJzL21heFNhZmVJbnRlZ2VyJyk7XG5cbnZhciBUb0ludGVnZXJPckluZmluaXR5ID0gcmVxdWlyZSgnLi9Ub0ludGVnZXJPckluZmluaXR5Jyk7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVG9MZW5ndGgoYXJndW1lbnQpIHtcblx0dmFyIGxlbiA9IFRvSW50ZWdlck9ySW5maW5pdHkoYXJndW1lbnQpO1xuXHRpZiAobGVuIDw9IDApIHsgcmV0dXJuIDA7IH0gLy8gaW5jbHVkZXMgY29udmVydGluZyAtMCB0byArMFxuXHRpZiAobGVuID4gTUFYX1NBRkVfSU5URUdFUikgeyByZXR1cm4gTUFYX1NBRkVfSU5URUdFUjsgfVxuXHRyZXR1cm4gbGVuO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///8305\n")},5631:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar $Number = GetIntrinsic('%Number%');\nvar $RegExp = GetIntrinsic('%RegExp%');\nvar $parseInteger = GetIntrinsic('%parseInt%');\n\nvar callBound = __webpack_require__(1924);\nvar regexTester = __webpack_require__(823);\nvar isPrimitive = __webpack_require__(4790);\n\nvar $strSlice = callBound('String.prototype.slice');\nvar isBinary = regexTester(/^0b[01]+$/i);\nvar isOctal = regexTester(/^0o[0-7]+$/i);\nvar isInvalidHexLiteral = regexTester(/^[-+]0x[0-9a-f]+$/i);\nvar nonWS = ['\\u0085', '\\u200b', '\\ufffe'].join('');\nvar nonWSregex = new $RegExp('[' + nonWS + ']', 'g');\nvar hasNonWS = regexTester(nonWSregex);\n\n// whitespace from: https://es5.github.io/#x15.5.4.20\n// implementation from https://github.com/es-shims/es5-shim/blob/v3.4.0/es5-shim.js#L1304-L1324\nvar ws = [\n\t'\\x09\\x0A\\x0B\\x0C\\x0D\\x20\\xA0\\u1680\\u180E\\u2000\\u2001\\u2002\\u2003',\n\t'\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200A\\u202F\\u205F\\u3000\\u2028',\n\t'\\u2029\\uFEFF'\n].join('');\nvar trimRegex = new RegExp('(^[' + ws + ']+)|([' + ws + ']+$)', 'g');\nvar $replace = callBound('String.prototype.replace');\nvar $trim = function (value) {\n\treturn $replace(value, trimRegex, '');\n};\n\nvar ToPrimitive = __webpack_require__(4607);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-tonumber\n\nmodule.exports = function ToNumber(argument) {\n\tvar value = isPrimitive(argument) ? argument : ToPrimitive(argument, $Number);\n\tif (typeof value === 'symbol') {\n\t\tthrow new $TypeError('Cannot convert a Symbol value to a number');\n\t}\n\tif (typeof value === 'bigint') {\n\t\tthrow new $TypeError('Conversion from \\'BigInt\\' to \\'number\\' is not allowed.');\n\t}\n\tif (typeof value === 'string') {\n\t\tif (isBinary(value)) {\n\t\t\treturn ToNumber($parseInteger($strSlice(value, 2), 2));\n\t\t} else if (isOctal(value)) {\n\t\t\treturn ToNumber($parseInteger($strSlice(value, 2), 8));\n\t\t} else if (hasNonWS(value) || isInvalidHexLiteral(value)) {\n\t\t\treturn NaN;\n\t\t}\n\t\tvar trimmed = $trim(value);\n\t\tif (trimmed !== value) {\n\t\t\treturn ToNumber(trimmed);\n\t\t}\n\n\t}\n\treturn $Number(value);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTYzMS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBO0FBQ0E7QUFDQTs7QUFFQSxnQkFBZ0IsbUJBQU8sQ0FBQyxJQUFxQjtBQUM3QyxrQkFBa0IsbUJBQU8sQ0FBQyxHQUF3QjtBQUNsRCxrQkFBa0IsbUJBQU8sQ0FBQyxJQUF3Qjs7QUFFbEQ7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBLGtCQUFrQixtQkFBTyxDQUFDLElBQWU7O0FBRXpDOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxJQUFJO0FBQ0o7QUFDQSxJQUFJO0FBQ0o7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Ub051bWJlci5qcz81YWM0Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG52YXIgJE51bWJlciA9IEdldEludHJpbnNpYygnJU51bWJlciUnKTtcbnZhciAkUmVnRXhwID0gR2V0SW50cmluc2ljKCclUmVnRXhwJScpO1xudmFyICRwYXJzZUludGVnZXIgPSBHZXRJbnRyaW5zaWMoJyVwYXJzZUludCUnKTtcblxudmFyIGNhbGxCb3VuZCA9IHJlcXVpcmUoJ2NhbGwtYmluZC9jYWxsQm91bmQnKTtcbnZhciByZWdleFRlc3RlciA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvcmVnZXhUZXN0ZXInKTtcbnZhciBpc1ByaW1pdGl2ZSA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvaXNQcmltaXRpdmUnKTtcblxudmFyICRzdHJTbGljZSA9IGNhbGxCb3VuZCgnU3RyaW5nLnByb3RvdHlwZS5zbGljZScpO1xudmFyIGlzQmluYXJ5ID0gcmVnZXhUZXN0ZXIoL14wYlswMV0rJC9pKTtcbnZhciBpc09jdGFsID0gcmVnZXhUZXN0ZXIoL14wb1swLTddKyQvaSk7XG52YXIgaXNJbnZhbGlkSGV4TGl0ZXJhbCA9IHJlZ2V4VGVzdGVyKC9eWy0rXTB4WzAtOWEtZl0rJC9pKTtcbnZhciBub25XUyA9IFsnXFx1MDA4NScsICdcXHUyMDBiJywgJ1xcdWZmZmUnXS5qb2luKCcnKTtcbnZhciBub25XU3JlZ2V4ID0gbmV3ICRSZWdFeHAoJ1snICsgbm9uV1MgKyAnXScsICdnJyk7XG52YXIgaGFzTm9uV1MgPSByZWdleFRlc3Rlcihub25XU3JlZ2V4KTtcblxuLy8gd2hpdGVzcGFjZSBmcm9tOiBodHRwczovL2VzNS5naXRodWIuaW8vI3gxNS41LjQuMjBcbi8vIGltcGxlbWVudGF0aW9uIGZyb20gaHR0cHM6Ly9naXRodWIuY29tL2VzLXNoaW1zL2VzNS1zaGltL2Jsb2IvdjMuNC4wL2VzNS1zaGltLmpzI0wxMzA0LUwxMzI0XG52YXIgd3MgPSBbXG5cdCdcXHgwOVxceDBBXFx4MEJcXHgwQ1xceDBEXFx4MjBcXHhBMFxcdTE2ODBcXHUxODBFXFx1MjAwMFxcdTIwMDFcXHUyMDAyXFx1MjAwMycsXG5cdCdcXHUyMDA0XFx1MjAwNVxcdTIwMDZcXHUyMDA3XFx1MjAwOFxcdTIwMDlcXHUyMDBBXFx1MjAyRlxcdTIwNUZcXHUzMDAwXFx1MjAyOCcsXG5cdCdcXHUyMDI5XFx1RkVGRidcbl0uam9pbignJyk7XG52YXIgdHJpbVJlZ2V4ID0gbmV3IFJlZ0V4cCgnKF5bJyArIHdzICsgJ10rKXwoWycgKyB3cyArICddKyQpJywgJ2cnKTtcbnZhciAkcmVwbGFjZSA9IGNhbGxCb3VuZCgnU3RyaW5nLnByb3RvdHlwZS5yZXBsYWNlJyk7XG52YXIgJHRyaW0gPSBmdW5jdGlvbiAodmFsdWUpIHtcblx0cmV0dXJuICRyZXBsYWNlKHZhbHVlLCB0cmltUmVnZXgsICcnKTtcbn07XG5cbnZhciBUb1ByaW1pdGl2ZSA9IHJlcXVpcmUoJy4vVG9QcmltaXRpdmUnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLXRvbnVtYmVyXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVG9OdW1iZXIoYXJndW1lbnQpIHtcblx0dmFyIHZhbHVlID0gaXNQcmltaXRpdmUoYXJndW1lbnQpID8gYXJndW1lbnQgOiBUb1ByaW1pdGl2ZShhcmd1bWVudCwgJE51bWJlcik7XG5cdGlmICh0eXBlb2YgdmFsdWUgPT09ICdzeW1ib2wnKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Nhbm5vdCBjb252ZXJ0IGEgU3ltYm9sIHZhbHVlIHRvIGEgbnVtYmVyJyk7XG5cdH1cblx0aWYgKHR5cGVvZiB2YWx1ZSA9PT0gJ2JpZ2ludCcpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQ29udmVyc2lvbiBmcm9tIFxcJ0JpZ0ludFxcJyB0byBcXCdudW1iZXJcXCcgaXMgbm90IGFsbG93ZWQuJyk7XG5cdH1cblx0aWYgKHR5cGVvZiB2YWx1ZSA9PT0gJ3N0cmluZycpIHtcblx0XHRpZiAoaXNCaW5hcnkodmFsdWUpKSB7XG5cdFx0XHRyZXR1cm4gVG9OdW1iZXIoJHBhcnNlSW50ZWdlcigkc3RyU2xpY2UodmFsdWUsIDIpLCAyKSk7XG5cdFx0fSBlbHNlIGlmIChpc09jdGFsKHZhbHVlKSkge1xuXHRcdFx0cmV0dXJuIFRvTnVtYmVyKCRwYXJzZUludGVnZXIoJHN0clNsaWNlKHZhbHVlLCAyKSwgOCkpO1xuXHRcdH0gZWxzZSBpZiAoaGFzTm9uV1ModmFsdWUpIHx8IGlzSW52YWxpZEhleExpdGVyYWwodmFsdWUpKSB7XG5cdFx0XHRyZXR1cm4gTmFOO1xuXHRcdH1cblx0XHR2YXIgdHJpbW1lZCA9ICR0cmltKHZhbHVlKTtcblx0XHRpZiAodHJpbW1lZCAhPT0gdmFsdWUpIHtcblx0XHRcdHJldHVybiBUb051bWJlcih0cmltbWVkKTtcblx0XHR9XG5cblx0fVxuXHRyZXR1cm4gJE51bWJlcih2YWx1ZSk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///5631\n")},821:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $Object = GetIntrinsic('%Object%');\n\nvar RequireObjectCoercible = __webpack_require__(9619);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-toobject\n\nmodule.exports = function ToObject(value) {\n\tRequireObjectCoercible(value);\n\treturn $Object(value);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODIxLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWU7O0FBRTFDOztBQUVBLDZCQUE2QixtQkFBTyxDQUFDLElBQTBCOztBQUUvRDs7QUFFQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Ub09iamVjdC5qcz81Mzc0Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRPYmplY3QgPSBHZXRJbnRyaW5zaWMoJyVPYmplY3QlJyk7XG5cbnZhciBSZXF1aXJlT2JqZWN0Q29lcmNpYmxlID0gcmVxdWlyZSgnLi9SZXF1aXJlT2JqZWN0Q29lcmNpYmxlJyk7XG5cbi8vIGh0dHBzOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi82LjAvI3NlYy10b29iamVjdFxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFRvT2JqZWN0KHZhbHVlKSB7XG5cdFJlcXVpcmVPYmplY3RDb2VyY2libGUodmFsdWUpO1xuXHRyZXR1cm4gJE9iamVjdCh2YWx1ZSk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///821\n")},4607:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar toPrimitive = __webpack_require__(1503);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-toprimitive\n\nmodule.exports = function ToPrimitive(input) {\n\tif (arguments.length > 1) {\n\t\treturn toPrimitive(input, arguments[1]);\n\t}\n\treturn toPrimitive(input);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDYwNy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixrQkFBa0IsbUJBQU8sQ0FBQyxJQUF3Qjs7QUFFbEQ7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL1RvUHJpbWl0aXZlLmpzP2I1MGMiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgdG9QcmltaXRpdmUgPSByZXF1aXJlKCdlcy10by1wcmltaXRpdmUvZXMyMDE1Jyk7XG5cbi8vIGh0dHBzOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi82LjAvI3NlYy10b3ByaW1pdGl2ZVxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFRvUHJpbWl0aXZlKGlucHV0KSB7XG5cdGlmIChhcmd1bWVudHMubGVuZ3RoID4gMSkge1xuXHRcdHJldHVybiB0b1ByaW1pdGl2ZShpbnB1dCwgYXJndW1lbnRzWzFdKTtcblx0fVxuXHRyZXR1cm4gdG9QcmltaXRpdmUoaW5wdXQpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///4607\n")},9916:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar has = __webpack_require__(7642);\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar Type = __webpack_require__(3633);\nvar ToBoolean = __webpack_require__(9731);\nvar IsCallable = __webpack_require__(1787);\n\n// https://262.ecma-international.org/5.1/#sec-8.10.5\n\nmodule.exports = function ToPropertyDescriptor(Obj) {\n\tif (Type(Obj) !== 'Object') {\n\t\tthrow new $TypeError('ToPropertyDescriptor requires an object');\n\t}\n\n\tvar desc = {};\n\tif (has(Obj, 'enumerable')) {\n\t\tdesc['[[Enumerable]]'] = ToBoolean(Obj.enumerable);\n\t}\n\tif (has(Obj, 'configurable')) {\n\t\tdesc['[[Configurable]]'] = ToBoolean(Obj.configurable);\n\t}\n\tif (has(Obj, 'value')) {\n\t\tdesc['[[Value]]'] = Obj.value;\n\t}\n\tif (has(Obj, 'writable')) {\n\t\tdesc['[[Writable]]'] = ToBoolean(Obj.writable);\n\t}\n\tif (has(Obj, 'get')) {\n\t\tvar getter = Obj.get;\n\t\tif (typeof getter !== 'undefined' && !IsCallable(getter)) {\n\t\t\tthrow new $TypeError('getter must be a function');\n\t\t}\n\t\tdesc['[[Get]]'] = getter;\n\t}\n\tif (has(Obj, 'set')) {\n\t\tvar setter = Obj.set;\n\t\tif (typeof setter !== 'undefined' && !IsCallable(setter)) {\n\t\t\tthrow new $TypeError('setter must be a function');\n\t\t}\n\t\tdesc['[[Set]]'] = setter;\n\t}\n\n\tif ((has(desc, '[[Get]]') || has(desc, '[[Set]]')) && (has(desc, '[[Value]]') || has(desc, '[[Writable]]'))) {\n\t\tthrow new $TypeError('Invalid property descriptor. Cannot both specify accessors and a value or writable attribute');\n\t}\n\treturn desc;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTkxNi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixVQUFVLG1CQUFPLENBQUMsSUFBSzs7QUFFdkIsbUJBQW1CLG1CQUFPLENBQUMsR0FBZTs7QUFFMUM7O0FBRUEsV0FBVyxtQkFBTyxDQUFDLElBQVE7QUFDM0IsZ0JBQWdCLG1CQUFPLENBQUMsSUFBYTtBQUNyQyxpQkFBaUIsbUJBQU8sQ0FBQyxJQUFjOztBQUV2Qzs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Ub1Byb3BlcnR5RGVzY3JpcHRvci5qcz8yMTJjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGhhcyA9IHJlcXVpcmUoJ2hhcycpO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJFR5cGVFcnJvciA9IEdldEludHJpbnNpYygnJVR5cGVFcnJvciUnKTtcblxudmFyIFR5cGUgPSByZXF1aXJlKCcuL1R5cGUnKTtcbnZhciBUb0Jvb2xlYW4gPSByZXF1aXJlKCcuL1RvQm9vbGVhbicpO1xudmFyIElzQ2FsbGFibGUgPSByZXF1aXJlKCcuL0lzQ2FsbGFibGUnKTtcblxuLy8gaHR0cHM6Ly8yNjIuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy81LjEvI3NlYy04LjEwLjVcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBUb1Byb3BlcnR5RGVzY3JpcHRvcihPYmopIHtcblx0aWYgKFR5cGUoT2JqKSAhPT0gJ09iamVjdCcpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignVG9Qcm9wZXJ0eURlc2NyaXB0b3IgcmVxdWlyZXMgYW4gb2JqZWN0Jyk7XG5cdH1cblxuXHR2YXIgZGVzYyA9IHt9O1xuXHRpZiAoaGFzKE9iaiwgJ2VudW1lcmFibGUnKSkge1xuXHRcdGRlc2NbJ1tbRW51bWVyYWJsZV1dJ10gPSBUb0Jvb2xlYW4oT2JqLmVudW1lcmFibGUpO1xuXHR9XG5cdGlmIChoYXMoT2JqLCAnY29uZmlndXJhYmxlJykpIHtcblx0XHRkZXNjWydbW0NvbmZpZ3VyYWJsZV1dJ10gPSBUb0Jvb2xlYW4oT2JqLmNvbmZpZ3VyYWJsZSk7XG5cdH1cblx0aWYgKGhhcyhPYmosICd2YWx1ZScpKSB7XG5cdFx0ZGVzY1snW1tWYWx1ZV1dJ10gPSBPYmoudmFsdWU7XG5cdH1cblx0aWYgKGhhcyhPYmosICd3cml0YWJsZScpKSB7XG5cdFx0ZGVzY1snW1tXcml0YWJsZV1dJ10gPSBUb0Jvb2xlYW4oT2JqLndyaXRhYmxlKTtcblx0fVxuXHRpZiAoaGFzKE9iaiwgJ2dldCcpKSB7XG5cdFx0dmFyIGdldHRlciA9IE9iai5nZXQ7XG5cdFx0aWYgKHR5cGVvZiBnZXR0ZXIgIT09ICd1bmRlZmluZWQnICYmICFJc0NhbGxhYmxlKGdldHRlcikpIHtcblx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdnZXR0ZXIgbXVzdCBiZSBhIGZ1bmN0aW9uJyk7XG5cdFx0fVxuXHRcdGRlc2NbJ1tbR2V0XV0nXSA9IGdldHRlcjtcblx0fVxuXHRpZiAoaGFzKE9iaiwgJ3NldCcpKSB7XG5cdFx0dmFyIHNldHRlciA9IE9iai5zZXQ7XG5cdFx0aWYgKHR5cGVvZiBzZXR0ZXIgIT09ICd1bmRlZmluZWQnICYmICFJc0NhbGxhYmxlKHNldHRlcikpIHtcblx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdzZXR0ZXIgbXVzdCBiZSBhIGZ1bmN0aW9uJyk7XG5cdFx0fVxuXHRcdGRlc2NbJ1tbU2V0XV0nXSA9IHNldHRlcjtcblx0fVxuXG5cdGlmICgoaGFzKGRlc2MsICdbW0dldF1dJykgfHwgaGFzKGRlc2MsICdbW1NldF1dJykpICYmIChoYXMoZGVzYywgJ1tbVmFsdWVdXScpIHx8IGhhcyhkZXNjLCAnW1tXcml0YWJsZV1dJykpKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0ludmFsaWQgcHJvcGVydHkgZGVzY3JpcHRvci4gQ2Fubm90IGJvdGggc3BlY2lmeSBhY2Nlc3NvcnMgYW5kIGEgdmFsdWUgb3Igd3JpdGFibGUgYXR0cmlidXRlJyk7XG5cdH1cblx0cmV0dXJuIGRlc2M7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///9916\n")},6846:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $String = GetIntrinsic('%String%');\nvar $TypeError = GetIntrinsic('%TypeError%');\n\n// https://ecma-international.org/ecma-262/6.0/#sec-tostring\n\nmodule.exports = function ToString(argument) {\n\tif (typeof argument === 'symbol') {\n\t\tthrow new $TypeError('Cannot convert a Symbol value to a string');\n\t}\n\treturn $String(argument);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNjg0Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBOztBQUVBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Ub1N0cmluZy5qcz8xNTk0Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRTdHJpbmcgPSBHZXRJbnRyaW5zaWMoJyVTdHJpbmclJyk7XG52YXIgJFR5cGVFcnJvciA9IEdldEludHJpbnNpYygnJVR5cGVFcnJvciUnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLXRvc3RyaW5nXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVG9TdHJpbmcoYXJndW1lbnQpIHtcblx0aWYgKHR5cGVvZiBhcmd1bWVudCA9PT0gJ3N5bWJvbCcpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQ2Fubm90IGNvbnZlcnQgYSBTeW1ib2wgdmFsdWUgdG8gYSBzdHJpbmcnKTtcblx0fVxuXHRyZXR1cm4gJFN0cmluZyhhcmd1bWVudCk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///6846\n")},3633:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar ES5Type = __webpack_require__(3951);\n\n// https://262.ecma-international.org/11.0/#sec-ecmascript-data-types-and-values\n\nmodule.exports = function Type(x) {\n\tif (typeof x === 'symbol') {\n\t\treturn 'Symbol';\n\t}\n\tif (typeof x === 'bigint') {\n\t\treturn 'BigInt';\n\t}\n\treturn ES5Type(x);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzYzMy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixjQUFjLG1CQUFPLENBQUMsSUFBVzs7QUFFakM7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL1R5cGUuanM/YTdmMyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBFUzVUeXBlID0gcmVxdWlyZSgnLi4vNS9UeXBlJyk7XG5cbi8vIGh0dHBzOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvMTEuMC8jc2VjLWVjbWFzY3JpcHQtZGF0YS10eXBlcy1hbmQtdmFsdWVzXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVHlwZSh4KSB7XG5cdGlmICh0eXBlb2YgeCA9PT0gJ3N5bWJvbCcpIHtcblx0XHRyZXR1cm4gJ1N5bWJvbCc7XG5cdH1cblx0aWYgKHR5cGVvZiB4ID09PSAnYmlnaW50Jykge1xuXHRcdHJldHVybiAnQmlnSW50Jztcblx0fVxuXHRyZXR1cm4gRVM1VHlwZSh4KTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3633\n")},4857:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar $fromCharCode = GetIntrinsic('%String.fromCharCode%');\n\nvar isLeadingSurrogate = __webpack_require__(9544);\nvar isTrailingSurrogate = __webpack_require__(5424);\n\n// https://tc39.es/ecma262/2020/#sec-utf16decodesurrogatepair\n\nmodule.exports = function UTF16DecodeSurrogatePair(lead, trail) {\n\tif (!isLeadingSurrogate(lead) || !isTrailingSurrogate(trail)) {\n\t\tthrow new $TypeError('Assertion failed: `lead` must be a leading surrogate char code, and `trail` must be a trailing surrogate char code');\n\t}\n\t// var cp = (lead - 0xD800) * 0x400 + (trail - 0xDC00) + 0x10000;\n\treturn $fromCharCode(lead) + $fromCharCode(trail);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDg1Ny5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBOztBQUVBLHlCQUF5QixtQkFBTyxDQUFDLElBQStCO0FBQ2hFLDBCQUEwQixtQkFBTyxDQUFDLElBQWdDOztBQUVsRTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9VVEYxNlN1cnJvZ2F0ZVBhaXJUb0NvZGVQb2ludC5qcz9iMGE0Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG52YXIgJGZyb21DaGFyQ29kZSA9IEdldEludHJpbnNpYygnJVN0cmluZy5mcm9tQ2hhckNvZGUlJyk7XG5cbnZhciBpc0xlYWRpbmdTdXJyb2dhdGUgPSByZXF1aXJlKCcuLi9oZWxwZXJzL2lzTGVhZGluZ1N1cnJvZ2F0ZScpO1xudmFyIGlzVHJhaWxpbmdTdXJyb2dhdGUgPSByZXF1aXJlKCcuLi9oZWxwZXJzL2lzVHJhaWxpbmdTdXJyb2dhdGUnKTtcblxuLy8gaHR0cHM6Ly90YzM5LmVzL2VjbWEyNjIvMjAyMC8jc2VjLXV0ZjE2ZGVjb2Rlc3Vycm9nYXRlcGFpclxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFVURjE2RGVjb2RlU3Vycm9nYXRlUGFpcihsZWFkLCB0cmFpbCkge1xuXHRpZiAoIWlzTGVhZGluZ1N1cnJvZ2F0ZShsZWFkKSB8fCAhaXNUcmFpbGluZ1N1cnJvZ2F0ZSh0cmFpbCkpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogYGxlYWRgIG11c3QgYmUgYSBsZWFkaW5nIHN1cnJvZ2F0ZSBjaGFyIGNvZGUsIGFuZCBgdHJhaWxgIG11c3QgYmUgYSB0cmFpbGluZyBzdXJyb2dhdGUgY2hhciBjb2RlJyk7XG5cdH1cblx0Ly8gdmFyIGNwID0gKGxlYWQgLSAweEQ4MDApICogMHg0MDAgKyAodHJhaWwgLSAweERDMDApICsgMHgxMDAwMDtcblx0cmV0dXJuICRmcm9tQ2hhckNvZGUobGVhZCkgKyAkZnJvbUNoYXJDb2RlKHRyYWlsKTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///4857\n")},4908:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $abs = GetIntrinsic('%Math.abs%');\n\n// http://262.ecma-international.org/5.1/#sec-5.2\n\nmodule.exports = function abs(x) {\n\treturn $abs(x);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDkwOC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQTs7QUFFQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvYWJzLmpzPzZjMmEiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJGFicyA9IEdldEludHJpbnNpYygnJU1hdGguYWJzJScpO1xuXG4vLyBodHRwOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvNS4xLyNzZWMtNS4yXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gYWJzKHgpIHtcblx0cmV0dXJuICRhYnMoeCk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///4908\n")},375:function(module){"use strict";eval("\n\n// var modulo = require('./modulo');\nvar $floor = Math.floor;\n\n// http://262.ecma-international.org/5.1/#sec-5.2\n\nmodule.exports = function floor(x) {\n\t// return x - modulo(x, 1);\n\treturn $floor(x);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzc1LmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViO0FBQ0E7O0FBRUE7O0FBRUE7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvZmxvb3IuanM/ODY4NCJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbi8vIHZhciBtb2R1bG8gPSByZXF1aXJlKCcuL21vZHVsbycpO1xudmFyICRmbG9vciA9IE1hdGguZmxvb3I7XG5cbi8vIGh0dHA6Ly8yNjIuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy81LjEvI3NlYy01LjJcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBmbG9vcih4KSB7XG5cdC8vIHJldHVybiB4IC0gbW9kdWxvKHgsIDEpO1xuXHRyZXR1cm4gJGZsb29yKHgpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///375\n")},4559:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\n// http://262.ecma-international.org/5.1/#sec-9.10\n\nmodule.exports = function CheckObjectCoercible(value, optMessage) {\n\tif (value == null) {\n\t\tthrow new $TypeError(optMessage || ('Cannot call method on ' + value));\n\t}\n\treturn value;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDU1OS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzUvQ2hlY2tPYmplY3RDb2VyY2libGUuanM/NWM5MiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xuXG4vLyBodHRwOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvNS4xLyNzZWMtOS4xMFxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIENoZWNrT2JqZWN0Q29lcmNpYmxlKHZhbHVlLCBvcHRNZXNzYWdlKSB7XG5cdGlmICh2YWx1ZSA9PSBudWxsKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3Iob3B0TWVzc2FnZSB8fCAoJ0Nhbm5vdCBjYWxsIG1ldGhvZCBvbiAnICsgdmFsdWUpKTtcblx0fVxuXHRyZXR1cm4gdmFsdWU7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///4559\n")},775:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar abs = __webpack_require__(7890);\nvar floor = __webpack_require__(2748);\nvar ToNumber = __webpack_require__(7709);\n\nvar $isNaN = __webpack_require__(9086);\nvar $isFinite = __webpack_require__(2633);\nvar $sign = __webpack_require__(8111);\n\n// http://262.ecma-international.org/5.1/#sec-9.4\n\nmodule.exports = function ToInteger(value) {\n\tvar number = ToNumber(value);\n\tif ($isNaN(number)) { return 0; }\n\tif (number === 0 || !$isFinite(number)) { return number; }\n\treturn $sign(number) * floor(abs(number));\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzc1LmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLFVBQVUsbUJBQU8sQ0FBQyxJQUFPO0FBQ3pCLFlBQVksbUJBQU8sQ0FBQyxJQUFTO0FBQzdCLGVBQWUsbUJBQU8sQ0FBQyxJQUFZOztBQUVuQyxhQUFhLG1CQUFPLENBQUMsSUFBa0I7QUFDdkMsZ0JBQWdCLG1CQUFPLENBQUMsSUFBcUI7QUFDN0MsWUFBWSxtQkFBTyxDQUFDLElBQWlCOztBQUVyQzs7QUFFQTtBQUNBO0FBQ0EsdUJBQXVCO0FBQ3ZCLDJDQUEyQztBQUMzQztBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC81L1RvSW50ZWdlci5qcz81YThjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGFicyA9IHJlcXVpcmUoJy4vYWJzJyk7XG52YXIgZmxvb3IgPSByZXF1aXJlKCcuL2Zsb29yJyk7XG52YXIgVG9OdW1iZXIgPSByZXF1aXJlKCcuL1RvTnVtYmVyJyk7XG5cbnZhciAkaXNOYU4gPSByZXF1aXJlKCcuLi9oZWxwZXJzL2lzTmFOJyk7XG52YXIgJGlzRmluaXRlID0gcmVxdWlyZSgnLi4vaGVscGVycy9pc0Zpbml0ZScpO1xudmFyICRzaWduID0gcmVxdWlyZSgnLi4vaGVscGVycy9zaWduJyk7XG5cbi8vIGh0dHA6Ly8yNjIuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy81LjEvI3NlYy05LjRcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBUb0ludGVnZXIodmFsdWUpIHtcblx0dmFyIG51bWJlciA9IFRvTnVtYmVyKHZhbHVlKTtcblx0aWYgKCRpc05hTihudW1iZXIpKSB7IHJldHVybiAwOyB9XG5cdGlmIChudW1iZXIgPT09IDAgfHwgISRpc0Zpbml0ZShudW1iZXIpKSB7IHJldHVybiBudW1iZXI7IH1cblx0cmV0dXJuICRzaWduKG51bWJlcikgKiBmbG9vcihhYnMobnVtYmVyKSk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///775\n")},7709:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar ToPrimitive = __webpack_require__(1950);\n\n// http://262.ecma-international.org/5.1/#sec-9.3\n\nmodule.exports = function ToNumber(value) {\n\tvar prim = ToPrimitive(value, Number);\n\tif (typeof prim !== 'string') {\n\t\treturn +prim; // eslint-disable-line no-implicit-coercion\n\t}\n\n\t// eslint-disable-next-line no-control-regex\n\tvar trimmed = prim.replace(/^[ \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\u0085]+|[ \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\u0085]+$/g, '');\n\tif ((/^0[ob]|^[+-]0x/).test(trimmed)) {\n\t\treturn NaN;\n\t}\n\n\treturn +trimmed; // eslint-disable-line no-implicit-coercion\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzcwOS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixrQkFBa0IsbUJBQU8sQ0FBQyxJQUFlOztBQUV6Qzs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxnQkFBZ0I7QUFDaEI7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQSxrQkFBa0I7QUFDbEIiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzUvVG9OdW1iZXIuanM/ZTU2YiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBUb1ByaW1pdGl2ZSA9IHJlcXVpcmUoJy4vVG9QcmltaXRpdmUnKTtcblxuLy8gaHR0cDovLzI2Mi5lY21hLWludGVybmF0aW9uYWwub3JnLzUuMS8jc2VjLTkuM1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFRvTnVtYmVyKHZhbHVlKSB7XG5cdHZhciBwcmltID0gVG9QcmltaXRpdmUodmFsdWUsIE51bWJlcik7XG5cdGlmICh0eXBlb2YgcHJpbSAhPT0gJ3N0cmluZycpIHtcblx0XHRyZXR1cm4gK3ByaW07IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8taW1wbGljaXQtY29lcmNpb25cblx0fVxuXG5cdC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1jb250cm9sLXJlZ2V4XG5cdHZhciB0cmltbWVkID0gcHJpbS5yZXBsYWNlKC9eWyBcXHRcXHgwYlxcZlxceGEwXFx1ZmVmZlxcblxcclxcdTIwMjhcXHUyMDI5XFx1MTY4MFxcdTE4MGVcXHUyMDAwXFx1MjAwMVxcdTIwMDJcXHUyMDAzXFx1MjAwNFxcdTIwMDVcXHUyMDA2XFx1MjAwN1xcdTIwMDhcXHUyMDA5XFx1MjAwYVxcdTIwMmZcXHUyMDVmXFx1MzAwMFxcdTAwODVdK3xbIFxcdFxceDBiXFxmXFx4YTBcXHVmZWZmXFxuXFxyXFx1MjAyOFxcdTIwMjlcXHUxNjgwXFx1MTgwZVxcdTIwMDBcXHUyMDAxXFx1MjAwMlxcdTIwMDNcXHUyMDA0XFx1MjAwNVxcdTIwMDZcXHUyMDA3XFx1MjAwOFxcdTIwMDlcXHUyMDBhXFx1MjAyZlxcdTIwNWZcXHUzMDAwXFx1MDA4NV0rJC9nLCAnJyk7XG5cdGlmICgoL14wW29iXXxeWystXTB4LykudGVzdCh0cmltbWVkKSkge1xuXHRcdHJldHVybiBOYU47XG5cdH1cblxuXHRyZXR1cm4gK3RyaW1tZWQ7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8taW1wbGljaXQtY29lcmNpb25cbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///7709\n")},1950:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\n// http://262.ecma-international.org/5.1/#sec-9.1\n\nmodule.exports = __webpack_require__(2116);\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTk1MC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQSwwQ0FBK0MiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzUvVG9QcmltaXRpdmUuanM/ZmEwZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbi8vIGh0dHA6Ly8yNjIuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy81LjEvI3NlYy05LjFcblxubW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKCdlcy10by1wcmltaXRpdmUvZXM1Jyk7XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///1950\n")},3951:function(module){"use strict";eval("\n\n// https://262.ecma-international.org/5.1/#sec-8\n\nmodule.exports = function Type(x) {\n\tif (x === null) {\n\t\treturn 'Null';\n\t}\n\tif (typeof x === 'undefined') {\n\t\treturn 'Undefined';\n\t}\n\tif (typeof x === 'function' || typeof x === 'object') {\n\t\treturn 'Object';\n\t}\n\tif (typeof x === 'number') {\n\t\treturn 'Number';\n\t}\n\tif (typeof x === 'boolean') {\n\t\treturn 'Boolean';\n\t}\n\tif (typeof x === 'string') {\n\t\treturn 'String';\n\t}\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzk1MS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC81L1R5cGUuanM/ODYzZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbi8vIGh0dHBzOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvNS4xLyNzZWMtOFxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFR5cGUoeCkge1xuXHRpZiAoeCA9PT0gbnVsbCkge1xuXHRcdHJldHVybiAnTnVsbCc7XG5cdH1cblx0aWYgKHR5cGVvZiB4ID09PSAndW5kZWZpbmVkJykge1xuXHRcdHJldHVybiAnVW5kZWZpbmVkJztcblx0fVxuXHRpZiAodHlwZW9mIHggPT09ICdmdW5jdGlvbicgfHwgdHlwZW9mIHggPT09ICdvYmplY3QnKSB7XG5cdFx0cmV0dXJuICdPYmplY3QnO1xuXHR9XG5cdGlmICh0eXBlb2YgeCA9PT0gJ251bWJlcicpIHtcblx0XHRyZXR1cm4gJ051bWJlcic7XG5cdH1cblx0aWYgKHR5cGVvZiB4ID09PSAnYm9vbGVhbicpIHtcblx0XHRyZXR1cm4gJ0Jvb2xlYW4nO1xuXHR9XG5cdGlmICh0eXBlb2YgeCA9PT0gJ3N0cmluZycpIHtcblx0XHRyZXR1cm4gJ1N0cmluZyc7XG5cdH1cbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3951\n")},7890:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $abs = GetIntrinsic('%Math.abs%');\n\n// http://262.ecma-international.org/5.1/#sec-5.2\n\nmodule.exports = function abs(x) {\n\treturn $abs(x);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzg5MC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQTs7QUFFQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzUvYWJzLmpzPzRjNmUiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJGFicyA9IEdldEludHJpbnNpYygnJU1hdGguYWJzJScpO1xuXG4vLyBodHRwOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvNS4xLyNzZWMtNS4yXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gYWJzKHgpIHtcblx0cmV0dXJuICRhYnMoeCk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///7890\n")},2748:function(module){"use strict";eval("\n\n// var modulo = require('./modulo');\nvar $floor = Math.floor;\n\n// http://262.ecma-international.org/5.1/#sec-5.2\n\nmodule.exports = function floor(x) {\n\t// return x - modulo(x, 1);\n\treturn $floor(x);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjc0OC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBOztBQUVBOztBQUVBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC81L2Zsb29yLmpzPzlkZGYiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG4vLyB2YXIgbW9kdWxvID0gcmVxdWlyZSgnLi9tb2R1bG8nKTtcbnZhciAkZmxvb3IgPSBNYXRoLmZsb29yO1xuXG4vLyBodHRwOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvNS4xLyNzZWMtNS4yXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gZmxvb3IoeCkge1xuXHQvLyByZXR1cm4geCAtIG1vZHVsbyh4LCAxKTtcblx0cmV0dXJuICRmbG9vcih4KTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///2748\n")},4445:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\n// TODO: remove, semver-major\n\nmodule.exports = __webpack_require__(210);\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDQ0NS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQSx5Q0FBeUMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L0dldEludHJpbnNpYy5qcz83MWZjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxuLy8gVE9ETzogcmVtb3ZlLCBzZW12ZXItbWFqb3JcblxubW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///4445\n")},3682:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $defineProperty = GetIntrinsic('%Object.defineProperty%', true);\n\nif ($defineProperty) {\n\ttry {\n\t\t$defineProperty({}, 'a', { value: 1 });\n\t} catch (e) {\n\t\t// IE 8 has a broken defineProperty\n\t\t$defineProperty = null;\n\t}\n}\n\n// node v0.6 has a bug where array lengths can be Set but not Defined\nvar hasArrayLengthDefineBug = Object.defineProperty && Object.defineProperty([], 'length', { value: 1 }).length === 0;\n\n// eslint-disable-next-line global-require\nvar isArray = hasArrayLengthDefineBug && __webpack_require__(7912); // this does not depend on any other AOs.\n\nvar callBound = __webpack_require__(1924);\n\nvar $isEnumerable = callBound('Object.prototype.propertyIsEnumerable');\n\n// eslint-disable-next-line max-params\nmodule.exports = function DefineOwnProperty(IsDataDescriptor, SameValue, FromPropertyDescriptor, O, P, desc) {\n\tif (!$defineProperty) {\n\t\tif (!IsDataDescriptor(desc)) {\n\t\t\t// ES3 does not support getters/setters\n\t\t\treturn false;\n\t\t}\n\t\tif (!desc['[[Configurable]]'] || !desc['[[Writable]]']) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// fallback for ES3\n\t\tif (P in O && $isEnumerable(O, P) !== !!desc['[[Enumerable]]']) {\n\t\t\t// a non-enumerable existing property\n\t\t\treturn false;\n\t\t}\n\n\t\t// property does not exist at all, or exists but is enumerable\n\t\tvar V = desc['[[Value]]'];\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tO[P] = V; // will use [[Define]]\n\t\treturn SameValue(O[P], V);\n\t}\n\tif (\n\t\thasArrayLengthDefineBug\n\t\t&& P === 'length'\n\t\t&& '[[Value]]' in desc\n\t\t&& isArray(O)\n\t\t&& O.length !== desc['[[Value]]']\n\t) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tO.length = desc['[[Value]]'];\n\t\treturn O.length === desc['[[Value]]'];\n\t}\n\n\t$defineProperty(O, P, FromPropertyDescriptor(desc));\n\treturn true;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzY4Mi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQTtBQUNBO0FBQ0Esb0JBQW9CLFNBQVMsVUFBVTtBQUN2QyxHQUFHO0FBQ0g7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQSw2RkFBNkYsVUFBVTs7QUFFdkc7QUFDQSx5Q0FBeUMsbUJBQU8sQ0FBQyxJQUFpQixHQUFHOztBQUVyRSxnQkFBZ0IsbUJBQU8sQ0FBQyxJQUFxQjs7QUFFN0M7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxZQUFZO0FBQ1o7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC9oZWxwZXJzL0RlZmluZU93blByb3BlcnR5LmpzPzExOTQiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJGRlZmluZVByb3BlcnR5ID0gR2V0SW50cmluc2ljKCclT2JqZWN0LmRlZmluZVByb3BlcnR5JScsIHRydWUpO1xuXG5pZiAoJGRlZmluZVByb3BlcnR5KSB7XG5cdHRyeSB7XG5cdFx0JGRlZmluZVByb3BlcnR5KHt9LCAnYScsIHsgdmFsdWU6IDEgfSk7XG5cdH0gY2F0Y2ggKGUpIHtcblx0XHQvLyBJRSA4IGhhcyBhIGJyb2tlbiBkZWZpbmVQcm9wZXJ0eVxuXHRcdCRkZWZpbmVQcm9wZXJ0eSA9IG51bGw7XG5cdH1cbn1cblxuLy8gbm9kZSB2MC42IGhhcyBhIGJ1ZyB3aGVyZSBhcnJheSBsZW5ndGhzIGNhbiBiZSBTZXQgYnV0IG5vdCBEZWZpbmVkXG52YXIgaGFzQXJyYXlMZW5ndGhEZWZpbmVCdWcgPSBPYmplY3QuZGVmaW5lUHJvcGVydHkgJiYgT2JqZWN0LmRlZmluZVByb3BlcnR5KFtdLCAnbGVuZ3RoJywgeyB2YWx1ZTogMSB9KS5sZW5ndGggPT09IDA7XG5cbi8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBnbG9iYWwtcmVxdWlyZVxudmFyIGlzQXJyYXkgPSBoYXNBcnJheUxlbmd0aERlZmluZUJ1ZyAmJiByZXF1aXJlKCcuLi8yMDIwL0lzQXJyYXknKTsgLy8gdGhpcyBkb2VzIG5vdCBkZXBlbmQgb24gYW55IG90aGVyIEFPcy5cblxudmFyIGNhbGxCb3VuZCA9IHJlcXVpcmUoJ2NhbGwtYmluZC9jYWxsQm91bmQnKTtcblxudmFyICRpc0VudW1lcmFibGUgPSBjYWxsQm91bmQoJ09iamVjdC5wcm90b3R5cGUucHJvcGVydHlJc0VudW1lcmFibGUnKTtcblxuLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG1heC1wYXJhbXNcbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gRGVmaW5lT3duUHJvcGVydHkoSXNEYXRhRGVzY3JpcHRvciwgU2FtZVZhbHVlLCBGcm9tUHJvcGVydHlEZXNjcmlwdG9yLCBPLCBQLCBkZXNjKSB7XG5cdGlmICghJGRlZmluZVByb3BlcnR5KSB7XG5cdFx0aWYgKCFJc0RhdGFEZXNjcmlwdG9yKGRlc2MpKSB7XG5cdFx0XHQvLyBFUzMgZG9lcyBub3Qgc3VwcG9ydCBnZXR0ZXJzL3NldHRlcnNcblx0XHRcdHJldHVybiBmYWxzZTtcblx0XHR9XG5cdFx0aWYgKCFkZXNjWydbW0NvbmZpZ3VyYWJsZV1dJ10gfHwgIWRlc2NbJ1tbV3JpdGFibGVdXSddKSB7XG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0fVxuXG5cdFx0Ly8gZmFsbGJhY2sgZm9yIEVTM1xuXHRcdGlmIChQIGluIE8gJiYgJGlzRW51bWVyYWJsZShPLCBQKSAhPT0gISFkZXNjWydbW0VudW1lcmFibGVdXSddKSB7XG5cdFx0XHQvLyBhIG5vbi1lbnVtZXJhYmxlIGV4aXN0aW5nIHByb3BlcnR5XG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0fVxuXG5cdFx0Ly8gcHJvcGVydHkgZG9lcyBub3QgZXhpc3QgYXQgYWxsLCBvciBleGlzdHMgYnV0IGlzIGVudW1lcmFibGVcblx0XHR2YXIgViA9IGRlc2NbJ1tbVmFsdWVdXSddO1xuXHRcdC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1wYXJhbS1yZWFzc2lnblxuXHRcdE9bUF0gPSBWOyAvLyB3aWxsIHVzZSBbW0RlZmluZV1dXG5cdFx0cmV0dXJuIFNhbWVWYWx1ZShPW1BdLCBWKTtcblx0fVxuXHRpZiAoXG5cdFx0aGFzQXJyYXlMZW5ndGhEZWZpbmVCdWdcblx0XHQmJiBQID09PSAnbGVuZ3RoJ1xuXHRcdCYmICdbW1ZhbHVlXV0nIGluIGRlc2Ncblx0XHQmJiBpc0FycmF5KE8pXG5cdFx0JiYgTy5sZW5ndGggIT09IGRlc2NbJ1tbVmFsdWVdXSddXG5cdCkge1xuXHRcdC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1wYXJhbS1yZWFzc2lnblxuXHRcdE8ubGVuZ3RoID0gZGVzY1snW1tWYWx1ZV1dJ107XG5cdFx0cmV0dXJuIE8ubGVuZ3RoID09PSBkZXNjWydbW1ZhbHVlXV0nXTtcblx0fVxuXG5cdCRkZWZpbmVQcm9wZXJ0eShPLCBQLCBGcm9tUHJvcGVydHlEZXNjcmlwdG9yKGRlc2MpKTtcblx0cmV0dXJuIHRydWU7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///3682\n")},2188:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar $SyntaxError = GetIntrinsic('%SyntaxError%');\n\nvar has = __webpack_require__(7642);\n\nvar predicates = {\n\t// https://262.ecma-international.org/6.0/#sec-property-descriptor-specification-type\n\t'Property Descriptor': function isPropertyDescriptor(Type, Desc) {\n\t\tif (Type(Desc) !== 'Object') {\n\t\t\treturn false;\n\t\t}\n\t\tvar allowed = {\n\t\t\t'[[Configurable]]': true,\n\t\t\t'[[Enumerable]]': true,\n\t\t\t'[[Get]]': true,\n\t\t\t'[[Set]]': true,\n\t\t\t'[[Value]]': true,\n\t\t\t'[[Writable]]': true\n\t\t};\n\n\t\tfor (var key in Desc) { // eslint-disable-line\n\t\t\tif (has(Desc, key) && !allowed[key]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\tvar isData = has(Desc, '[[Value]]');\n\t\tvar IsAccessor = has(Desc, '[[Get]]') || has(Desc, '[[Set]]');\n\t\tif (isData && IsAccessor) {\n\t\t\tthrow new $TypeError('Property Descriptors may not be both accessor and data descriptors');\n\t\t}\n\t\treturn true;\n\t}\n};\n\nmodule.exports = function assertRecord(Type, recordType, argumentName, value) {\n\tvar predicate = predicates[recordType];\n\tif (typeof predicate !== 'function') {\n\t\tthrow new $SyntaxError('unknown record type: ' + recordType);\n\t}\n\tif (!predicate(Type, value)) {\n\t\tthrow new $TypeError(argumentName + ' must be a ' + recordType);\n\t}\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjE4OC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBOztBQUVBLFVBQVUsbUJBQU8sQ0FBQyxJQUFLOztBQUV2QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBLDBCQUEwQjtBQUMxQjtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvaGVscGVycy9hc3NlcnRSZWNvcmQuanM/NzlhMiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xudmFyICRTeW50YXhFcnJvciA9IEdldEludHJpbnNpYygnJVN5bnRheEVycm9yJScpO1xuXG52YXIgaGFzID0gcmVxdWlyZSgnaGFzJyk7XG5cbnZhciBwcmVkaWNhdGVzID0ge1xuXHQvLyBodHRwczovLzI2Mi5lY21hLWludGVybmF0aW9uYWwub3JnLzYuMC8jc2VjLXByb3BlcnR5LWRlc2NyaXB0b3Itc3BlY2lmaWNhdGlvbi10eXBlXG5cdCdQcm9wZXJ0eSBEZXNjcmlwdG9yJzogZnVuY3Rpb24gaXNQcm9wZXJ0eURlc2NyaXB0b3IoVHlwZSwgRGVzYykge1xuXHRcdGlmIChUeXBlKERlc2MpICE9PSAnT2JqZWN0Jykge1xuXHRcdFx0cmV0dXJuIGZhbHNlO1xuXHRcdH1cblx0XHR2YXIgYWxsb3dlZCA9IHtcblx0XHRcdCdbW0NvbmZpZ3VyYWJsZV1dJzogdHJ1ZSxcblx0XHRcdCdbW0VudW1lcmFibGVdXSc6IHRydWUsXG5cdFx0XHQnW1tHZXRdXSc6IHRydWUsXG5cdFx0XHQnW1tTZXRdXSc6IHRydWUsXG5cdFx0XHQnW1tWYWx1ZV1dJzogdHJ1ZSxcblx0XHRcdCdbW1dyaXRhYmxlXV0nOiB0cnVlXG5cdFx0fTtcblxuXHRcdGZvciAodmFyIGtleSBpbiBEZXNjKSB7IC8vIGVzbGludC1kaXNhYmxlLWxpbmVcblx0XHRcdGlmIChoYXMoRGVzYywga2V5KSAmJiAhYWxsb3dlZFtrZXldKSB7XG5cdFx0XHRcdHJldHVybiBmYWxzZTtcblx0XHRcdH1cblx0XHR9XG5cblx0XHR2YXIgaXNEYXRhID0gaGFzKERlc2MsICdbW1ZhbHVlXV0nKTtcblx0XHR2YXIgSXNBY2Nlc3NvciA9IGhhcyhEZXNjLCAnW1tHZXRdXScpIHx8IGhhcyhEZXNjLCAnW1tTZXRdXScpO1xuXHRcdGlmIChpc0RhdGEgJiYgSXNBY2Nlc3Nvcikge1xuXHRcdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ1Byb3BlcnR5IERlc2NyaXB0b3JzIG1heSBub3QgYmUgYm90aCBhY2Nlc3NvciBhbmQgZGF0YSBkZXNjcmlwdG9ycycpO1xuXHRcdH1cblx0XHRyZXR1cm4gdHJ1ZTtcblx0fVxufTtcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBhc3NlcnRSZWNvcmQoVHlwZSwgcmVjb3JkVHlwZSwgYXJndW1lbnROYW1lLCB2YWx1ZSkge1xuXHR2YXIgcHJlZGljYXRlID0gcHJlZGljYXRlc1tyZWNvcmRUeXBlXTtcblx0aWYgKHR5cGVvZiBwcmVkaWNhdGUgIT09ICdmdW5jdGlvbicpIHtcblx0XHR0aHJvdyBuZXcgJFN5bnRheEVycm9yKCd1bmtub3duIHJlY29yZCB0eXBlOiAnICsgcmVjb3JkVHlwZSk7XG5cdH1cblx0aWYgKCFwcmVkaWNhdGUoVHlwZSwgdmFsdWUpKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoYXJndW1lbnROYW1lICsgJyBtdXN0IGJlIGEgJyArIHJlY29yZFR5cGUpO1xuXHR9XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///2188\n")},2633:function(module){"use strict";eval("\n\nvar $isNaN = Number.isNaN || function (a) { return a !== a; };\n\nmodule.exports = Number.isFinite || function (x) { return typeof x === 'number' && !$isNaN(x) && x !== Infinity && x !== -Infinity; };\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjYzMy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYiw0Q0FBNEM7O0FBRTVDLG1EQUFtRCIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvaGVscGVycy9pc0Zpbml0ZS5qcz80YjU2Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyICRpc05hTiA9IE51bWJlci5pc05hTiB8fCBmdW5jdGlvbiAoYSkgeyByZXR1cm4gYSAhPT0gYTsgfTtcblxubW9kdWxlLmV4cG9ydHMgPSBOdW1iZXIuaXNGaW5pdGUgfHwgZnVuY3Rpb24gKHgpIHsgcmV0dXJuIHR5cGVvZiB4ID09PSAnbnVtYmVyJyAmJiAhJGlzTmFOKHgpICYmIHggIT09IEluZmluaXR5ICYmIHggIT09IC1JbmZpbml0eTsgfTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2633\n")},9544:function(module){"use strict";eval("\n\nmodule.exports = function isLeadingSurrogate(charCode) {\n\treturn typeof charCode === 'number' && charCode >= 0xD800 && charCode <= 0xDBFF;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTU0NC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvaXNMZWFkaW5nU3Vycm9nYXRlLmpzPzc0NzIiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIGlzTGVhZGluZ1N1cnJvZ2F0ZShjaGFyQ29kZSkge1xuXHRyZXR1cm4gdHlwZW9mIGNoYXJDb2RlID09PSAnbnVtYmVyJyAmJiBjaGFyQ29kZSA+PSAweEQ4MDAgJiYgY2hhckNvZGUgPD0gMHhEQkZGO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///9544\n")},9086:function(module){"use strict";eval("\n\nmodule.exports = Number.isNaN || function isNaN(a) {\n\treturn a !== a;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTA4Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvaXNOYU4uanM/MGVjZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbm1vZHVsZS5leHBvcnRzID0gTnVtYmVyLmlzTmFOIHx8IGZ1bmN0aW9uIGlzTmFOKGEpIHtcblx0cmV0dXJuIGEgIT09IGE7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///9086\n")},4790:function(module){"use strict";eval("\n\nmodule.exports = function isPrimitive(value) {\n\treturn value === null || (typeof value !== 'function' && typeof value !== 'object');\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDc5MC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvaXNQcmltaXRpdmUuanM/NjJkZiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaXNQcmltaXRpdmUodmFsdWUpIHtcblx0cmV0dXJuIHZhbHVlID09PSBudWxsIHx8ICh0eXBlb2YgdmFsdWUgIT09ICdmdW5jdGlvbicgJiYgdHlwZW9mIHZhbHVlICE9PSAnb2JqZWN0Jyk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///4790\n")},2435:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar has = __webpack_require__(7642);\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nmodule.exports = function IsPropertyDescriptor(ES, Desc) {\n\tif (ES.Type(Desc) !== 'Object') {\n\t\treturn false;\n\t}\n\tvar allowed = {\n\t\t'[[Configurable]]': true,\n\t\t'[[Enumerable]]': true,\n\t\t'[[Get]]': true,\n\t\t'[[Set]]': true,\n\t\t'[[Value]]': true,\n\t\t'[[Writable]]': true\n\t};\n\n\tfor (var key in Desc) { // eslint-disable-line no-restricted-syntax\n\t\tif (has(Desc, key) && !allowed[key]) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tif (ES.IsDataDescriptor(Desc) && ES.IsAccessorDescriptor(Desc)) {\n\t\tthrow new $TypeError('Property Descriptors may not be both accessor and data descriptors');\n\t}\n\treturn true;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjQzNS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQyxVQUFVLG1CQUFPLENBQUMsSUFBSztBQUN2Qjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUEseUJBQXlCO0FBQ3pCO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvaXNQcm9wZXJ0eURlc2NyaXB0b3IuanM/MDc4YiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciBoYXMgPSByZXF1aXJlKCdoYXMnKTtcbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIElzUHJvcGVydHlEZXNjcmlwdG9yKEVTLCBEZXNjKSB7XG5cdGlmIChFUy5UeXBlKERlc2MpICE9PSAnT2JqZWN0Jykge1xuXHRcdHJldHVybiBmYWxzZTtcblx0fVxuXHR2YXIgYWxsb3dlZCA9IHtcblx0XHQnW1tDb25maWd1cmFibGVdXSc6IHRydWUsXG5cdFx0J1tbRW51bWVyYWJsZV1dJzogdHJ1ZSxcblx0XHQnW1tHZXRdXSc6IHRydWUsXG5cdFx0J1tbU2V0XV0nOiB0cnVlLFxuXHRcdCdbW1ZhbHVlXV0nOiB0cnVlLFxuXHRcdCdbW1dyaXRhYmxlXV0nOiB0cnVlXG5cdH07XG5cblx0Zm9yICh2YXIga2V5IGluIERlc2MpIHsgLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1yZXN0cmljdGVkLXN5bnRheFxuXHRcdGlmIChoYXMoRGVzYywga2V5KSAmJiAhYWxsb3dlZFtrZXldKSB7XG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0fVxuXHR9XG5cblx0aWYgKEVTLklzRGF0YURlc2NyaXB0b3IoRGVzYykgJiYgRVMuSXNBY2Nlc3NvckRlc2NyaXB0b3IoRGVzYykpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignUHJvcGVydHkgRGVzY3JpcHRvcnMgbWF5IG5vdCBiZSBib3RoIGFjY2Vzc29yIGFuZCBkYXRhIGRlc2NyaXB0b3JzJyk7XG5cdH1cblx0cmV0dXJuIHRydWU7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///2435\n")},5424:function(module){"use strict";eval("\n\nmodule.exports = function isTrailingSurrogate(charCode) {\n\treturn typeof charCode === 'number' && charCode >= 0xDC00 && charCode <= 0xDFFF;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTQyNC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvaXNUcmFpbGluZ1N1cnJvZ2F0ZS5qcz82ZTE0Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBpc1RyYWlsaW5nU3Vycm9nYXRlKGNoYXJDb2RlKSB7XG5cdHJldHVybiB0eXBlb2YgY2hhckNvZGUgPT09ICdudW1iZXInICYmIGNoYXJDb2RlID49IDB4REMwMCAmJiBjaGFyQ29kZSA8PSAweERGRkY7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///5424\n")},1645:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $Math = GetIntrinsic('%Math%');\nvar $Number = GetIntrinsic('%Number%');\n\nmodule.exports = $Number.MAX_SAFE_INTEGER || $Math.pow(2, 53) - 1;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTY0NS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBOztBQUVBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC9oZWxwZXJzL21heFNhZmVJbnRlZ2VyLmpzP2YzOTkiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJE1hdGggPSBHZXRJbnRyaW5zaWMoJyVNYXRoJScpO1xudmFyICROdW1iZXIgPSBHZXRJbnRyaW5zaWMoJyVOdW1iZXIlJyk7XG5cbm1vZHVsZS5leHBvcnRzID0gJE51bWJlci5NQVhfU0FGRV9JTlRFR0VSIHx8ICRNYXRoLnBvdygyLCA1MykgLSAxO1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///1645\n")},823:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $test = GetIntrinsic('RegExp.prototype.test');\n\nvar callBind = __webpack_require__(5559);\n\nmodule.exports = function regexTester(regex) {\n\treturn callBind($test, regex);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODIzLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWU7O0FBRTFDOztBQUVBLGVBQWUsbUJBQU8sQ0FBQyxJQUFXOztBQUVsQztBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvcmVnZXhUZXN0ZXIuanM/YWVjOSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkdGVzdCA9IEdldEludHJpbnNpYygnUmVnRXhwLnByb3RvdHlwZS50ZXN0Jyk7XG5cbnZhciBjYWxsQmluZCA9IHJlcXVpcmUoJ2NhbGwtYmluZCcpO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIHJlZ2V4VGVzdGVyKHJlZ2V4KSB7XG5cdHJldHVybiBjYWxsQmluZCgkdGVzdCwgcmVnZXgpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///823\n")},8111:function(module){"use strict";eval("\n\nmodule.exports = function sign(number) {\n\treturn number >= 0 ? 1 : -1;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODExMS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvc2lnbi5qcz80MjdlIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBzaWduKG51bWJlcikge1xuXHRyZXR1cm4gbnVtYmVyID49IDAgPyAxIDogLTE7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///8111\n")}},__webpack_module_cache__={};function __webpack_require__(Q){var t=__webpack_module_cache__[Q];if(void 0!==t)return t.exports;var n=__webpack_module_cache__[Q]={exports:{}};return __webpack_modules__[Q](n,n.exports,__webpack_require__),n.exports}__webpack_require__.n=function(Q){var t=Q&&Q.__esModule?function(){return Q.default}:function(){return Q};return __webpack_require__.d(t,{a:t}),t},__webpack_require__.d=function(Q,t){for(var n in t)__webpack_require__.o(t,n)&&!__webpack_require__.o(Q,n)&&Object.defineProperty(Q,n,{enumerable:!0,get:t[n]})},__webpack_require__.o=function(Q,t){return Object.prototype.hasOwnProperty.call(Q,t)};var __webpack_exports__=__webpack_require__(6396)})(); \ No newline at end of file +!function(){var u={1844:function(u,t){"use strict";function e(u){return u.split("").reverse().join("")}function r(u){return(u|-u)>>31&1}function n(u,t,e,n){var o=u.P[e],D=u.M[e],i=n>>>31,a=t[e]|i,F=a|D,c=(a&o)+o^o|a,l=D|~(c|o),f=o&c,s=r(l&u.lastRowMask[e])-r(f&u.lastRowMask[e]);return l<<=1,f<<=1,o=(f|=i)|~(F|(l|=r(n)-i)),D=l&F,u.P[e]=o,u.M[e]=D,s}function o(u,t,e){if(0===t.length)return[];e=Math.min(e,t.length);var r=[],o=32,D=Math.ceil(t.length/o)-1,i={P:new Uint32Array(D+1),M:new Uint32Array(D+1),lastRowMask:new Uint32Array(D+1)};i.lastRowMask.fill(1<<31),i.lastRowMask[D]=1<<(t.length-1)%o;for(var a=new Uint32Array(D+1),F=new Map,c=[],l=0;l<256;l++)c.push(a);for(var f=0;f<t.length;f+=1){var s=t.charCodeAt(f);if(!F.has(s)){var A=new Uint32Array(D+1);F.set(s,A),s<c.length&&(c[s]=A);for(var p=0;p<=D;p+=1){A[p]=0;for(var E=0;E<o;E+=1){var C=p*o+E;C>=t.length||t.charCodeAt(C)===s&&(A[p]|=1<<E)}}}}var y=Math.max(0,Math.ceil(e/o)-1),d=new Uint32Array(D+1);for(p=0;p<=y;p+=1)d[p]=(p+1)*o;for(d[D]=t.length,p=0;p<=y;p+=1)i.P[p]=-1,i.M[p]=0;for(var B=0;B<u.length;B+=1){var h=u.charCodeAt(B);A=void 0,h<c.length?A=c[h]:void 0===(A=F.get(h))&&(A=a);var g=0;for(p=0;p<=y;p+=1)g=n(i,A,p,g),d[p]+=g;if(d[y]-g<=e&&y<D&&(1&A[y+1]||g<0)){y+=1,i.P[y]=-1,i.M[y]=0;var m=y===D?t.length%o:o;d[y]=d[y-1]+m-g+n(i,A,y,g)}else for(;y>0&&d[y]>=e+o;)y-=1;y===D&&d[y]<=e&&(d[y]<e&&r.splice(0,r.length),r.push({start:-1,end:B+1,errors:d[y]}),e=d[y])}return r}t.Z=function(u,t,r){return function(u,t,r){var n=e(t);return r.map((function(r){var D=Math.max(0,r.end-t.length-r.errors);return{start:o(e(u.slice(D,r.end)),n,r.errors).reduce((function(u,t){return r.end-t.end<u?r.end-t.end:u}),r.end),end:r.end,errors:r.errors}}))}(u,t,o(u,t,r))}},3099:function(u,t,e){"use strict";var r=e(2870),n=e(2755),o=n(r("String.prototype.indexOf"));u.exports=function(u,t){var e=r(u,!!t);return"function"==typeof e&&o(u,".prototype.")>-1?n(e):e}},2755:function(u,t,e){"use strict";var r=e(3569),n=e(2870),o=n("%Function.prototype.apply%"),D=n("%Function.prototype.call%"),i=n("%Reflect.apply%",!0)||r.call(D,o),a=n("%Object.getOwnPropertyDescriptor%",!0),F=n("%Object.defineProperty%",!0),c=n("%Math.max%");if(F)try{F({},"a",{value:1})}catch(u){F=null}u.exports=function(u){var t=i(r,D,arguments);return a&&F&&a(t,"length").configurable&&F(t,"length",{value:1+c(0,u.length-(arguments.length-1))}),t};var l=function(){return i(r,o,arguments)};F?F(u.exports,"apply",{value:l}):u.exports.apply=l},6663:function(u,t,e){"use strict";var r=e(229)(),n=e(2870),o=r&&n("%Object.defineProperty%",!0),D=n("%SyntaxError%"),i=n("%TypeError%"),a=e(658);u.exports=function(u,t,e){if(!u||"object"!=typeof u&&"function"!=typeof u)throw new i("`obj` must be an object or a function`");if("string"!=typeof t&&"symbol"!=typeof t)throw new i("`property` must be a string or a symbol`");if(arguments.length>3&&"boolean"!=typeof arguments[3]&&null!==arguments[3])throw new i("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&"boolean"!=typeof arguments[4]&&null!==arguments[4])throw new i("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&"boolean"!=typeof arguments[5]&&null!==arguments[5])throw new i("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&"boolean"!=typeof arguments[6])throw new i("`loose`, if provided, must be a boolean");var r=arguments.length>3?arguments[3]:null,n=arguments.length>4?arguments[4]:null,F=arguments.length>5?arguments[5]:null,c=arguments.length>6&&arguments[6],l=!!a&&a(u,t);if(o)o(u,t,{configurable:null===F&&l?l.configurable:!F,enumerable:null===r&&l?l.enumerable:!r,value:e,writable:null===n&&l?l.writable:!n});else{if(!c&&(r||n||F))throw new D("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.");u[t]=e}}},9722:function(u,t,e){"use strict";var r=e(2051),n="function"==typeof Symbol&&"symbol"==typeof Symbol("foo"),o=Object.prototype.toString,D=Array.prototype.concat,i=e(6663),a=e(229)(),F=function(u,t,e,r){if(t in u)if(!0===r){if(u[t]===e)return}else if("function"!=typeof(n=r)||"[object Function]"!==o.call(n)||!r())return;var n;a?i(u,t,e,!0):i(u,t,e)},c=function(u,t){var e=arguments.length>2?arguments[2]:{},o=r(t);n&&(o=D.call(o,Object.getOwnPropertySymbols(t)));for(var i=0;i<o.length;i+=1)F(u,o[i],t[o[i]],e[o[i]])};c.supportsDescriptors=!!a,u.exports=c},2263:function(u,t,e){"use strict";var r=e(2870)("%Object.defineProperty%",!0),n=e(3060)(),o=e(9545),D=n?Symbol.toStringTag:null;u.exports=function(u,t){var e=arguments.length>2&&arguments[2]&&arguments[2].force;!D||!e&&o(u,D)||(r?r(u,D,{configurable:!0,enumerable:!1,value:t,writable:!1}):u[D]=t)}},7358:function(u,t,e){"use strict";var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator,n=e(7959),o=e(3655),D=e(455),i=e(8760);u.exports=function(u){if(n(u))return u;var t,e="default";if(arguments.length>1&&(arguments[1]===String?e="string":arguments[1]===Number&&(e="number")),r&&(Symbol.toPrimitive?t=function(u,t){var e=u[t];if(null!=e){if(!o(e))throw new TypeError(e+" returned for property "+t+" of object "+u+" is not a function");return e}}(u,Symbol.toPrimitive):i(u)&&(t=Symbol.prototype.valueOf)),void 0!==t){var a=t.call(u,e);if(n(a))return a;throw new TypeError("unable to convert exotic object to primitive")}return"default"===e&&(D(u)||i(u))&&(e="string"),function(u,t){if(null==u)throw new TypeError("Cannot call method on "+u);if("string"!=typeof t||"number"!==t&&"string"!==t)throw new TypeError('hint must be "string" or "number"');var e,r,D,i="string"===t?["toString","valueOf"]:["valueOf","toString"];for(D=0;D<i.length;++D)if(e=u[i[D]],o(e)&&(r=e.call(u),n(r)))return r;throw new TypeError("No default value")}(u,"default"===e?"number":e)}},7959:function(u){"use strict";u.exports=function(u){return null===u||"function"!=typeof u&&"object"!=typeof u}},8640:function(u){"use strict";var t=Array.prototype.slice,e=Object.prototype.toString;u.exports=function(u){var r=this;if("function"!=typeof r||"[object Function]"!==e.call(r))throw new TypeError("Function.prototype.bind called on incompatible "+r);for(var n,o=t.call(arguments,1),D=Math.max(0,r.length-o.length),i=[],a=0;a<D;a++)i.push("$"+a);if(n=Function("binder","return function ("+i.join(",")+"){ return binder.apply(this,arguments); }")((function(){if(this instanceof n){var e=r.apply(this,o.concat(t.call(arguments)));return Object(e)===e?e:this}return r.apply(u,o.concat(t.call(arguments)))})),r.prototype){var F=function(){};F.prototype=r.prototype,n.prototype=new F,F.prototype=null}return n}},3569:function(u,t,e){"use strict";var r=e(8640);u.exports=Function.prototype.bind||r},5610:function(u){"use strict";var t=function(){return"string"==typeof function(){}.name},e=Object.getOwnPropertyDescriptor;if(e)try{e([],"length")}catch(u){e=null}t.functionsHaveConfigurableNames=function(){if(!t()||!e)return!1;var u=e((function(){}),"name");return!!u&&!!u.configurable};var r=Function.prototype.bind;t.boundFunctionsHaveNames=function(){return t()&&"function"==typeof r&&""!==function(){}.bind().name},u.exports=t},2870:function(u,t,e){"use strict";var r,n=SyntaxError,o=Function,D=TypeError,i=function(u){try{return o('"use strict"; return ('+u+").constructor;")()}catch(u){}},a=Object.getOwnPropertyDescriptor;if(a)try{a({},"")}catch(u){a=null}var F=function(){throw new D},c=a?function(){try{return F}catch(u){try{return a(arguments,"callee").get}catch(u){return F}}}():F,l=e(1143)(),f=e(3413)(),s=Object.getPrototypeOf||(f?function(u){return u.__proto__}:null),A={},p="undefined"!=typeof Uint8Array&&s?s(Uint8Array):r,E={"%AggregateError%":"undefined"==typeof AggregateError?r:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?r:ArrayBuffer,"%ArrayIteratorPrototype%":l&&s?s([][Symbol.iterator]()):r,"%AsyncFromSyncIteratorPrototype%":r,"%AsyncFunction%":A,"%AsyncGenerator%":A,"%AsyncGeneratorFunction%":A,"%AsyncIteratorPrototype%":A,"%Atomics%":"undefined"==typeof Atomics?r:Atomics,"%BigInt%":"undefined"==typeof BigInt?r:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?r:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?r:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?r:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":Error,"%eval%":eval,"%EvalError%":EvalError,"%Float32Array%":"undefined"==typeof Float32Array?r:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?r:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?r:FinalizationRegistry,"%Function%":o,"%GeneratorFunction%":A,"%Int8Array%":"undefined"==typeof Int8Array?r:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?r:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?r:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":l&&s?s(s([][Symbol.iterator]())):r,"%JSON%":"object"==typeof JSON?JSON:r,"%Map%":"undefined"==typeof Map?r:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&l&&s?s((new Map)[Symbol.iterator]()):r,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?r:Promise,"%Proxy%":"undefined"==typeof Proxy?r:Proxy,"%RangeError%":RangeError,"%ReferenceError%":ReferenceError,"%Reflect%":"undefined"==typeof Reflect?r:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?r:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&l&&s?s((new Set)[Symbol.iterator]()):r,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?r:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":l&&s?s(""[Symbol.iterator]()):r,"%Symbol%":l?Symbol:r,"%SyntaxError%":n,"%ThrowTypeError%":c,"%TypedArray%":p,"%TypeError%":D,"%Uint8Array%":"undefined"==typeof Uint8Array?r:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?r:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?r:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?r:Uint32Array,"%URIError%":URIError,"%WeakMap%":"undefined"==typeof WeakMap?r:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?r:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?r:WeakSet};if(s)try{null.error}catch(u){var C=s(s(u));E["%Error.prototype%"]=C}var y=function u(t){var e;if("%AsyncFunction%"===t)e=i("async function () {}");else if("%GeneratorFunction%"===t)e=i("function* () {}");else if("%AsyncGeneratorFunction%"===t)e=i("async function* () {}");else if("%AsyncGenerator%"===t){var r=u("%AsyncGeneratorFunction%");r&&(e=r.prototype)}else if("%AsyncIteratorPrototype%"===t){var n=u("%AsyncGenerator%");n&&s&&(e=s(n.prototype))}return E[t]=e,e},d={"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},B=e(3569),h=e(9545),g=B.call(Function.call,Array.prototype.concat),m=B.call(Function.apply,Array.prototype.splice),b=B.call(Function.call,String.prototype.replace),v=B.call(Function.call,String.prototype.slice),w=B.call(Function.call,RegExp.prototype.exec),S=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,x=/\\(\\)?/g,O=function(u,t){var e,r=u;if(h(d,r)&&(r="%"+(e=d[r])[0]+"%"),h(E,r)){var o=E[r];if(o===A&&(o=y(r)),void 0===o&&!t)throw new D("intrinsic "+u+" exists, but is not available. Please file an issue!");return{alias:e,name:r,value:o}}throw new n("intrinsic "+u+" does not exist!")};u.exports=function(u,t){if("string"!=typeof u||0===u.length)throw new D("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof t)throw new D('"allowMissing" argument must be a boolean');if(null===w(/^%?[^%]*%?$/,u))throw new n("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var e=function(u){var t=v(u,0,1),e=v(u,-1);if("%"===t&&"%"!==e)throw new n("invalid intrinsic syntax, expected closing `%`");if("%"===e&&"%"!==t)throw new n("invalid intrinsic syntax, expected opening `%`");var r=[];return b(u,S,(function(u,t,e,n){r[r.length]=e?b(n,x,"$1"):t||u})),r}(u),r=e.length>0?e[0]:"",o=O("%"+r+"%",t),i=o.name,F=o.value,c=!1,l=o.alias;l&&(r=l[0],m(e,g([0,1],l)));for(var f=1,s=!0;f<e.length;f+=1){var A=e[f],p=v(A,0,1),C=v(A,-1);if(('"'===p||"'"===p||"`"===p||'"'===C||"'"===C||"`"===C)&&p!==C)throw new n("property names with quotes must have matching quotes");if("constructor"!==A&&s||(c=!0),h(E,i="%"+(r+="."+A)+"%"))F=E[i];else if(null!=F){if(!(A in F)){if(!t)throw new D("base intrinsic for "+u+" exists, but the property is not available.");return}if(a&&f+1>=e.length){var y=a(F,A);F=(s=!!y)&&"get"in y&&!("originalValue"in y.get)?y.get:F[A]}else s=h(F,A),F=F[A];s&&!c&&(E[i]=F)}}return F}},658:function(u,t,e){"use strict";var r=e(2870)("%Object.getOwnPropertyDescriptor%",!0);if(r)try{r([],"length")}catch(u){r=null}u.exports=r},229:function(u,t,e){"use strict";var r=e(2870)("%Object.defineProperty%",!0),n=function(){if(r)try{return r({},"a",{value:1}),!0}catch(u){return!1}return!1};n.hasArrayLengthDefineBug=function(){if(!n())return null;try{return 1!==r([],"length",{value:1}).length}catch(u){return!0}},u.exports=n},3413:function(u){"use strict";var t={foo:{}},e=Object;u.exports=function(){return{__proto__:t}.foo===t.foo&&!({__proto__:null}instanceof e)}},1143:function(u,t,e){"use strict";var r="undefined"!=typeof Symbol&&Symbol,n=e(9985);u.exports=function(){return"function"==typeof r&&"function"==typeof Symbol&&"symbol"==typeof r("foo")&&"symbol"==typeof Symbol("bar")&&n()}},9985:function(u){"use strict";u.exports=function(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var u={},t=Symbol("test"),e=Object(t);if("string"==typeof t)return!1;if("[object Symbol]"!==Object.prototype.toString.call(t))return!1;if("[object Symbol]"!==Object.prototype.toString.call(e))return!1;for(t in u[t]=42,u)return!1;if("function"==typeof Object.keys&&0!==Object.keys(u).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(u).length)return!1;var r=Object.getOwnPropertySymbols(u);if(1!==r.length||r[0]!==t)return!1;if(!Object.prototype.propertyIsEnumerable.call(u,t))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var n=Object.getOwnPropertyDescriptor(u,t);if(42!==n.value||!0!==n.enumerable)return!1}return!0}},3060:function(u,t,e){"use strict";var r=e(9985);u.exports=function(){return r()&&!!Symbol.toStringTag}},9545:function(u){"use strict";var t={}.hasOwnProperty,e=Function.prototype.call;u.exports=e.bind?e.bind(t):function(u,r){return e.call(t,u,r)}},7284:function(u,t,e){"use strict";var r=e(2870),n=e(9545),o=e(5714)(),D=r("%TypeError%"),i={assert:function(u,t){if(!u||"object"!=typeof u&&"function"!=typeof u)throw new D("`O` is not an object");if("string"!=typeof t)throw new D("`slot` must be a string");if(o.assert(u),!i.has(u,t))throw new D("`"+t+"` is not present on `O`")},get:function(u,t){if(!u||"object"!=typeof u&&"function"!=typeof u)throw new D("`O` is not an object");if("string"!=typeof t)throw new D("`slot` must be a string");var e=o.get(u);return e&&e["$"+t]},has:function(u,t){if(!u||"object"!=typeof u&&"function"!=typeof u)throw new D("`O` is not an object");if("string"!=typeof t)throw new D("`slot` must be a string");var e=o.get(u);return!!e&&n(e,"$"+t)},set:function(u,t,e){if(!u||"object"!=typeof u&&"function"!=typeof u)throw new D("`O` is not an object");if("string"!=typeof t)throw new D("`slot` must be a string");var r=o.get(u);r||(r={},o.set(u,r)),r["$"+t]=e}};Object.freeze&&Object.freeze(i),u.exports=i},3655:function(u){"use strict";var t,e,r=Function.prototype.toString,n="object"==typeof Reflect&&null!==Reflect&&Reflect.apply;if("function"==typeof n&&"function"==typeof Object.defineProperty)try{t=Object.defineProperty({},"length",{get:function(){throw e}}),e={},n((function(){throw 42}),null,t)}catch(u){u!==e&&(n=null)}else n=null;var o=/^\s*class\b/,D=function(u){try{var t=r.call(u);return o.test(t)}catch(u){return!1}},i=function(u){try{return!D(u)&&(r.call(u),!0)}catch(u){return!1}},a=Object.prototype.toString,F="function"==typeof Symbol&&!!Symbol.toStringTag,c=!(0 in[,]),l=function(){return!1};if("object"==typeof document){var f=document.all;a.call(f)===a.call(document.all)&&(l=function(u){if((c||!u)&&(void 0===u||"object"==typeof u))try{var t=a.call(u);return("[object HTMLAllCollection]"===t||"[object HTML document.all class]"===t||"[object HTMLCollection]"===t||"[object Object]"===t)&&null==u("")}catch(u){}return!1})}u.exports=n?function(u){if(l(u))return!0;if(!u)return!1;if("function"!=typeof u&&"object"!=typeof u)return!1;try{n(u,null,t)}catch(u){if(u!==e)return!1}return!D(u)&&i(u)}:function(u){if(l(u))return!0;if(!u)return!1;if("function"!=typeof u&&"object"!=typeof u)return!1;if(F)return i(u);if(D(u))return!1;var t=a.call(u);return!("[object Function]"!==t&&"[object GeneratorFunction]"!==t&&!/^\[object HTML/.test(t))&&i(u)}},455:function(u,t,e){"use strict";var r=Date.prototype.getDay,n=Object.prototype.toString,o=e(3060)();u.exports=function(u){return"object"==typeof u&&null!==u&&(o?function(u){try{return r.call(u),!0}catch(u){return!1}}(u):"[object Date]"===n.call(u))}},5494:function(u,t,e){"use strict";var r,n,o,D,i=e(3099),a=e(3060)();if(a){r=i("Object.prototype.hasOwnProperty"),n=i("RegExp.prototype.exec"),o={};var F=function(){throw o};D={toString:F,valueOf:F},"symbol"==typeof Symbol.toPrimitive&&(D[Symbol.toPrimitive]=F)}var c=i("Object.prototype.toString"),l=Object.getOwnPropertyDescriptor;u.exports=a?function(u){if(!u||"object"!=typeof u)return!1;var t=l(u,"lastIndex");if(!t||!r(t,"value"))return!1;try{n(u,D)}catch(u){return u===o}}:function(u){return!(!u||"object"!=typeof u&&"function"!=typeof u)&&"[object RegExp]"===c(u)}},8760:function(u,t,e){"use strict";var r=Object.prototype.toString;if(e(1143)()){var n=Symbol.prototype.toString,o=/^Symbol\(.*\)$/;u.exports=function(u){if("symbol"==typeof u)return!0;if("[object Symbol]"!==r.call(u))return!1;try{return function(u){return"symbol"==typeof u.valueOf()&&o.test(n.call(u))}(u)}catch(u){return!1}}}else u.exports=function(u){return!1}},4538:function(u,t,e){var r="function"==typeof Map&&Map.prototype,n=Object.getOwnPropertyDescriptor&&r?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,o=r&&n&&"function"==typeof n.get?n.get:null,D=r&&Map.prototype.forEach,i="function"==typeof Set&&Set.prototype,a=Object.getOwnPropertyDescriptor&&i?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,F=i&&a&&"function"==typeof a.get?a.get:null,c=i&&Set.prototype.forEach,l="function"==typeof WeakMap&&WeakMap.prototype?WeakMap.prototype.has:null,f="function"==typeof WeakSet&&WeakSet.prototype?WeakSet.prototype.has:null,s="function"==typeof WeakRef&&WeakRef.prototype?WeakRef.prototype.deref:null,A=Boolean.prototype.valueOf,p=Object.prototype.toString,E=Function.prototype.toString,C=String.prototype.match,y=String.prototype.slice,d=String.prototype.replace,B=String.prototype.toUpperCase,h=String.prototype.toLowerCase,g=RegExp.prototype.test,m=Array.prototype.concat,b=Array.prototype.join,v=Array.prototype.slice,w=Math.floor,S="function"==typeof BigInt?BigInt.prototype.valueOf:null,x=Object.getOwnPropertySymbols,O="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,j="function"==typeof Symbol&&"object"==typeof Symbol.iterator,P="function"==typeof Symbol&&Symbol.toStringTag&&(Symbol.toStringTag,1)?Symbol.toStringTag:null,T=Object.prototype.propertyIsEnumerable,I=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(u){return u.__proto__}:null);function R(u,t){if(u===1/0||u===-1/0||u!=u||u&&u>-1e3&&u<1e3||g.call(/e/,t))return t;var e=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof u){var r=u<0?-w(-u):w(u);if(r!==u){var n=String(r),o=y.call(t,n.length+1);return d.call(n,e,"$&_")+"."+d.call(d.call(o,/([0-9]{3})/g,"$&_"),/_$/,"")}}return d.call(t,e,"$&_")}var N=e(7002),M=N.custom,k=W(M)?M:null;function $(u,t,e){var r="double"===(e.quoteStyle||t)?'"':"'";return r+u+r}function L(u){return d.call(String(u),/"/g,""")}function U(u){return!("[object Array]"!==H(u)||P&&"object"==typeof u&&P in u)}function _(u){return!("[object RegExp]"!==H(u)||P&&"object"==typeof u&&P in u)}function W(u){if(j)return u&&"object"==typeof u&&u instanceof Symbol;if("symbol"==typeof u)return!0;if(!u||"object"!=typeof u||!O)return!1;try{return O.call(u),!0}catch(u){}return!1}u.exports=function u(t,e,r,n){var i=e||{};if(V(i,"quoteStyle")&&"single"!==i.quoteStyle&&"double"!==i.quoteStyle)throw new TypeError('option "quoteStyle" must be "single" or "double"');if(V(i,"maxStringLength")&&("number"==typeof i.maxStringLength?i.maxStringLength<0&&i.maxStringLength!==1/0:null!==i.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var a=!V(i,"customInspect")||i.customInspect;if("boolean"!=typeof a&&"symbol"!==a)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(V(i,"indent")&&null!==i.indent&&"\t"!==i.indent&&!(parseInt(i.indent,10)===i.indent&&i.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(V(i,"numericSeparator")&&"boolean"!=typeof i.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var p=i.numericSeparator;if(void 0===t)return"undefined";if(null===t)return"null";if("boolean"==typeof t)return t?"true":"false";if("string"==typeof t)return X(t,i);if("number"==typeof t){if(0===t)return 1/0/t>0?"0":"-0";var B=String(t);return p?R(t,B):B}if("bigint"==typeof t){var g=String(t)+"n";return p?R(t,g):g}var w=void 0===i.depth?5:i.depth;if(void 0===r&&(r=0),r>=w&&w>0&&"object"==typeof t)return U(t)?"[Array]":"[Object]";var x,M=function(u,t){var e;if("\t"===u.indent)e="\t";else{if(!("number"==typeof u.indent&&u.indent>0))return null;e=b.call(Array(u.indent+1)," ")}return{base:e,prev:b.call(Array(t+1),e)}}(i,r);if(void 0===n)n=[];else if(q(n,t)>=0)return"[Circular]";function G(t,e,o){if(e&&(n=v.call(n)).push(e),o){var D={depth:i.depth};return V(i,"quoteStyle")&&(D.quoteStyle=i.quoteStyle),u(t,D,r+1,n)}return u(t,i,r+1,n)}if("function"==typeof t&&!_(t)){var Y=function(u){if(u.name)return u.name;var t=C.call(E.call(u),/^function\s*([\w$]+)/);return t?t[1]:null}(t),uu=Q(t,G);return"[Function"+(Y?": "+Y:" (anonymous)")+"]"+(uu.length>0?" { "+b.call(uu,", ")+" }":"")}if(W(t)){var tu=j?d.call(String(t),/^(Symbol\(.*\))_[^)]*$/,"$1"):O.call(t);return"object"!=typeof t||j?tu:z(tu)}if((x=t)&&"object"==typeof x&&("undefined"!=typeof HTMLElement&&x instanceof HTMLElement||"string"==typeof x.nodeName&&"function"==typeof x.getAttribute)){for(var eu="<"+h.call(String(t.nodeName)),ru=t.attributes||[],nu=0;nu<ru.length;nu++)eu+=" "+ru[nu].name+"="+$(L(ru[nu].value),"double",i);return eu+=">",t.childNodes&&t.childNodes.length&&(eu+="..."),eu+"</"+h.call(String(t.nodeName))+">"}if(U(t)){if(0===t.length)return"[]";var ou=Q(t,G);return M&&!function(u){for(var t=0;t<u.length;t++)if(q(u[t],"\n")>=0)return!1;return!0}(ou)?"["+Z(ou,M)+"]":"[ "+b.call(ou,", ")+" ]"}if(function(u){return!("[object Error]"!==H(u)||P&&"object"==typeof u&&P in u)}(t)){var Du=Q(t,G);return"cause"in Error.prototype||!("cause"in t)||T.call(t,"cause")?0===Du.length?"["+String(t)+"]":"{ ["+String(t)+"] "+b.call(Du,", ")+" }":"{ ["+String(t)+"] "+b.call(m.call("[cause]: "+G(t.cause),Du),", ")+" }"}if("object"==typeof t&&a){if(k&&"function"==typeof t[k]&&N)return N(t,{depth:w-r});if("symbol"!==a&&"function"==typeof t.inspect)return t.inspect()}if(function(u){if(!o||!u||"object"!=typeof u)return!1;try{o.call(u);try{F.call(u)}catch(u){return!0}return u instanceof Map}catch(u){}return!1}(t)){var iu=[];return D&&D.call(t,(function(u,e){iu.push(G(e,t,!0)+" => "+G(u,t))})),K("Map",o.call(t),iu,M)}if(function(u){if(!F||!u||"object"!=typeof u)return!1;try{F.call(u);try{o.call(u)}catch(u){return!0}return u instanceof Set}catch(u){}return!1}(t)){var au=[];return c&&c.call(t,(function(u){au.push(G(u,t))})),K("Set",F.call(t),au,M)}if(function(u){if(!l||!u||"object"!=typeof u)return!1;try{l.call(u,l);try{f.call(u,f)}catch(u){return!0}return u instanceof WeakMap}catch(u){}return!1}(t))return J("WeakMap");if(function(u){if(!f||!u||"object"!=typeof u)return!1;try{f.call(u,f);try{l.call(u,l)}catch(u){return!0}return u instanceof WeakSet}catch(u){}return!1}(t))return J("WeakSet");if(function(u){if(!s||!u||"object"!=typeof u)return!1;try{return s.call(u),!0}catch(u){}return!1}(t))return J("WeakRef");if(function(u){return!("[object Number]"!==H(u)||P&&"object"==typeof u&&P in u)}(t))return z(G(Number(t)));if(function(u){if(!u||"object"!=typeof u||!S)return!1;try{return S.call(u),!0}catch(u){}return!1}(t))return z(G(S.call(t)));if(function(u){return!("[object Boolean]"!==H(u)||P&&"object"==typeof u&&P in u)}(t))return z(A.call(t));if(function(u){return!("[object String]"!==H(u)||P&&"object"==typeof u&&P in u)}(t))return z(G(String(t)));if(!function(u){return!("[object Date]"!==H(u)||P&&"object"==typeof u&&P in u)}(t)&&!_(t)){var Fu=Q(t,G),cu=I?I(t)===Object.prototype:t instanceof Object||t.constructor===Object,lu=t instanceof Object?"":"null prototype",fu=!cu&&P&&Object(t)===t&&P in t?y.call(H(t),8,-1):lu?"Object":"",su=(cu||"function"!=typeof t.constructor?"":t.constructor.name?t.constructor.name+" ":"")+(fu||lu?"["+b.call(m.call([],fu||[],lu||[]),": ")+"] ":"");return 0===Fu.length?su+"{}":M?su+"{"+Z(Fu,M)+"}":su+"{ "+b.call(Fu,", ")+" }"}return String(t)};var G=Object.prototype.hasOwnProperty||function(u){return u in this};function V(u,t){return G.call(u,t)}function H(u){return p.call(u)}function q(u,t){if(u.indexOf)return u.indexOf(t);for(var e=0,r=u.length;e<r;e++)if(u[e]===t)return e;return-1}function X(u,t){if(u.length>t.maxStringLength){var e=u.length-t.maxStringLength,r="... "+e+" more character"+(e>1?"s":"");return X(y.call(u,0,t.maxStringLength),t)+r}return $(d.call(d.call(u,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,Y),"single",t)}function Y(u){var t=u.charCodeAt(0),e={8:"b",9:"t",10:"n",12:"f",13:"r"}[t];return e?"\\"+e:"\\x"+(t<16?"0":"")+B.call(t.toString(16))}function z(u){return"Object("+u+")"}function J(u){return u+" { ? }"}function K(u,t,e,r){return u+" ("+t+") {"+(r?Z(e,r):b.call(e,", "))+"}"}function Z(u,t){if(0===u.length)return"";var e="\n"+t.prev+t.base;return e+b.call(u,","+e)+"\n"+t.prev}function Q(u,t){var e=U(u),r=[];if(e){r.length=u.length;for(var n=0;n<u.length;n++)r[n]=V(u,n)?t(u[n],u):""}var o,D="function"==typeof x?x(u):[];if(j){o={};for(var i=0;i<D.length;i++)o["$"+D[i]]=D[i]}for(var a in u)V(u,a)&&(e&&String(Number(a))===a&&a<u.length||j&&o["$"+a]instanceof Symbol||(g.call(/[^\w$]/,a)?r.push(t(a,u)+": "+t(u[a],u)):r.push(a+": "+t(u[a],u))));if("function"==typeof x)for(var F=0;F<D.length;F++)T.call(u,D[F])&&r.push("["+t(D[F])+"]: "+t(u[D[F]],u));return r}},9121:function(u,t,e){"use strict";var r;if(!Object.keys){var n=Object.prototype.hasOwnProperty,o=Object.prototype.toString,D=e(999),i=Object.prototype.propertyIsEnumerable,a=!i.call({toString:null},"toString"),F=i.call((function(){}),"prototype"),c=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],l=function(u){var t=u.constructor;return t&&t.prototype===u},f={$applicationCache:!0,$console:!0,$external:!0,$frame:!0,$frameElement:!0,$frames:!0,$innerHeight:!0,$innerWidth:!0,$onmozfullscreenchange:!0,$onmozfullscreenerror:!0,$outerHeight:!0,$outerWidth:!0,$pageXOffset:!0,$pageYOffset:!0,$parent:!0,$scrollLeft:!0,$scrollTop:!0,$scrollX:!0,$scrollY:!0,$self:!0,$webkitIndexedDB:!0,$webkitStorageInfo:!0,$window:!0},s=function(){if("undefined"==typeof window)return!1;for(var u in window)try{if(!f["$"+u]&&n.call(window,u)&&null!==window[u]&&"object"==typeof window[u])try{l(window[u])}catch(u){return!0}}catch(u){return!0}return!1}();r=function(u){var t=null!==u&&"object"==typeof u,e="[object Function]"===o.call(u),r=D(u),i=t&&"[object String]"===o.call(u),f=[];if(!t&&!e&&!r)throw new TypeError("Object.keys called on a non-object");var A=F&&e;if(i&&u.length>0&&!n.call(u,0))for(var p=0;p<u.length;++p)f.push(String(p));if(r&&u.length>0)for(var E=0;E<u.length;++E)f.push(String(E));else for(var C in u)A&&"prototype"===C||!n.call(u,C)||f.push(String(C));if(a)for(var y=function(u){if("undefined"==typeof window||!s)return l(u);try{return l(u)}catch(u){return!1}}(u),d=0;d<c.length;++d)y&&"constructor"===c[d]||!n.call(u,c[d])||f.push(c[d]);return f}}u.exports=r},2051:function(u,t,e){"use strict";var r=Array.prototype.slice,n=e(999),o=Object.keys,D=o?function(u){return o(u)}:e(9121),i=Object.keys;D.shim=function(){if(Object.keys){var u=function(){var u=Object.keys(arguments);return u&&u.length===arguments.length}(1,2);u||(Object.keys=function(u){return n(u)?i(r.call(u)):i(u)})}else Object.keys=D;return Object.keys||D},u.exports=D},999:function(u){"use strict";var t=Object.prototype.toString;u.exports=function(u){var e=t.call(u),r="[object Arguments]"===e;return r||(r="[object Array]"!==e&&null!==u&&"object"==typeof u&&"number"==typeof u.length&&u.length>=0&&"[object Function]"===t.call(u.callee)),r}},9766:function(u,t,e){"use strict";var r=e(8921),n=Object,o=TypeError;u.exports=r((function(){if(null!=this&&this!==n(this))throw new o("RegExp.prototype.flags getter called on non-object");var u="";return this.hasIndices&&(u+="d"),this.global&&(u+="g"),this.ignoreCase&&(u+="i"),this.multiline&&(u+="m"),this.dotAll&&(u+="s"),this.unicode&&(u+="u"),this.unicodeSets&&(u+="v"),this.sticky&&(u+="y"),u}),"get flags",!0)},483:function(u,t,e){"use strict";var r=e(9722),n=e(2755),o=e(9766),D=e(5113),i=e(7299),a=n(D());r(a,{getPolyfill:D,implementation:o,shim:i}),u.exports=a},5113:function(u,t,e){"use strict";var r=e(9766),n=e(9722).supportsDescriptors,o=Object.getOwnPropertyDescriptor;u.exports=function(){if(n&&"gim"===/a/gim.flags){var u=o(RegExp.prototype,"flags");if(u&&"function"==typeof u.get&&"boolean"==typeof RegExp.prototype.dotAll&&"boolean"==typeof RegExp.prototype.hasIndices){var t="",e={};if(Object.defineProperty(e,"hasIndices",{get:function(){t+="d"}}),Object.defineProperty(e,"sticky",{get:function(){t+="y"}}),"dy"===t)return u.get}}return r}},7299:function(u,t,e){"use strict";var r=e(9722).supportsDescriptors,n=e(5113),o=Object.getOwnPropertyDescriptor,D=Object.defineProperty,i=TypeError,a=Object.getPrototypeOf,F=/a/;u.exports=function(){if(!r||!a)throw new i("RegExp.prototype.flags requires a true ES5 environment that supports property descriptors");var u=n(),t=a(F),e=o(t,"flags");return e&&e.get===u||D(t,"flags",{configurable:!0,enumerable:!1,get:u}),u}},7582:function(u,t,e){"use strict";var r=e(3099),n=e(2870),o=e(5494),D=r("RegExp.prototype.exec"),i=n("%TypeError%");u.exports=function(u){if(!o(u))throw new i("`regex` must be a RegExp");return function(t){return null!==D(u,t)}}},8921:function(u,t,e){"use strict";var r=e(6663),n=e(229)(),o=e(5610).functionsHaveConfigurableNames(),D=TypeError;u.exports=function(u,t){if("function"!=typeof u)throw new D("`fn` is not a function");return arguments.length>2&&!!arguments[2]&&!o||(n?r(u,"name",t,!0,!0):r(u,"name",t)),u}},5714:function(u,t,e){"use strict";var r=e(2870),n=e(3099),o=e(4538),D=r("%TypeError%"),i=r("%WeakMap%",!0),a=r("%Map%",!0),F=n("WeakMap.prototype.get",!0),c=n("WeakMap.prototype.set",!0),l=n("WeakMap.prototype.has",!0),f=n("Map.prototype.get",!0),s=n("Map.prototype.set",!0),A=n("Map.prototype.has",!0),p=function(u,t){for(var e,r=u;null!==(e=r.next);r=e)if(e.key===t)return r.next=e.next,e.next=u.next,u.next=e,e};u.exports=function(){var u,t,e,r={assert:function(u){if(!r.has(u))throw new D("Side channel does not contain "+o(u))},get:function(r){if(i&&r&&("object"==typeof r||"function"==typeof r)){if(u)return F(u,r)}else if(a){if(t)return f(t,r)}else if(e)return function(u,t){var e=p(u,t);return e&&e.value}(e,r)},has:function(r){if(i&&r&&("object"==typeof r||"function"==typeof r)){if(u)return l(u,r)}else if(a){if(t)return A(t,r)}else if(e)return function(u,t){return!!p(u,t)}(e,r);return!1},set:function(r,n){i&&r&&("object"==typeof r||"function"==typeof r)?(u||(u=new i),c(u,r,n)):a?(t||(t=new a),s(t,r,n)):(e||(e={key:{},next:null}),function(u,t,e){var r=p(u,t);r?r.value=e:u.next={key:t,next:u.next,value:e}}(e,r,n))}};return r}},3073:function(u,t,e){"use strict";var r=e(7113),n=e(151),o=e(1959),D=e(9497),i=e(5128),a=e(6751),F=e(3099),c=e(1143)(),l=e(483),f=F("String.prototype.indexOf"),s=e(2009),A=function(u){var t=s();if(c&&"symbol"==typeof Symbol.matchAll){var e=o(u,Symbol.matchAll);return e===RegExp.prototype[Symbol.matchAll]&&e!==t?t:e}if(D(u))return t};u.exports=function(u){var t=a(this);if(null!=u){if(D(u)){var e="flags"in u?n(u,"flags"):l(u);if(a(e),f(i(e),"g")<0)throw new TypeError("matchAll requires a global regular expression")}var o=A(u);if(void 0!==o)return r(o,u,[t])}var F=i(t),c=new RegExp(u,"g");return r(A(c),c,[F])}},5155:function(u,t,e){"use strict";var r=e(2755),n=e(9722),o=e(3073),D=e(1794),i=e(3911),a=r(o);n(a,{getPolyfill:D,implementation:o,shim:i}),u.exports=a},2009:function(u,t,e){"use strict";var r=e(1143)(),n=e(8012);u.exports=function(){return r&&"symbol"==typeof Symbol.matchAll&&"function"==typeof RegExp.prototype[Symbol.matchAll]?RegExp.prototype[Symbol.matchAll]:n}},1794:function(u,t,e){"use strict";var r=e(3073);u.exports=function(){if(String.prototype.matchAll)try{"".matchAll(RegExp.prototype)}catch(u){return String.prototype.matchAll}return r}},8012:function(u,t,e){"use strict";var r=e(1398),n=e(151),o=e(8322),D=e(2449),i=e(3995),a=e(5128),F=e(1874),c=e(483),l=e(8921),f=e(3099)("String.prototype.indexOf"),s=RegExp,A="flags"in RegExp.prototype,p=l((function(u){var t=this;if("Object"!==F(t))throw new TypeError('"this" value must be an Object');var e=a(u),l=function(u,t){var e="flags"in t?n(t,"flags"):a(c(t));return{flags:e,matcher:new u(A&&"string"==typeof e?t:u===s?t.source:t,e)}}(D(t,s),t),p=l.flags,E=l.matcher,C=i(n(t,"lastIndex"));o(E,"lastIndex",C,!0);var y=f(p,"g")>-1,d=f(p,"u")>-1;return r(E,e,y,d)}),"[Symbol.matchAll]",!0);u.exports=p},3911:function(u,t,e){"use strict";var r=e(9722),n=e(1143)(),o=e(1794),D=e(2009),i=Object.defineProperty,a=Object.getOwnPropertyDescriptor;u.exports=function(){var u=o();if(r(String.prototype,{matchAll:u},{matchAll:function(){return String.prototype.matchAll!==u}}),n){var t=Symbol.matchAll||(Symbol.for?Symbol.for("Symbol.matchAll"):Symbol("Symbol.matchAll"));if(r(Symbol,{matchAll:t},{matchAll:function(){return Symbol.matchAll!==t}}),i&&a){var e=a(Symbol,t);e&&!e.configurable||i(Symbol,t,{configurable:!1,enumerable:!1,value:t,writable:!1})}var F=D(),c={};c[t]=F;var l={};l[t]=function(){return RegExp.prototype[t]!==F},r(RegExp.prototype,c,l)}return u}},8125:function(u,t,e){"use strict";var r=e(6751),n=e(5128),o=e(3099)("String.prototype.replace"),D=/^\s$/.test(""),i=D?/^[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+/:/^[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+/,a=D?/[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+$/:/[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+$/;u.exports=function(){var u=n(r(this));return o(o(u,i,""),a,"")}},9434:function(u,t,e){"use strict";var r=e(2755),n=e(9722),o=e(6751),D=e(8125),i=e(3228),a=e(818),F=r(i()),c=function(u){return o(u),F(u)};n(c,{getPolyfill:i,implementation:D,shim:a}),u.exports=c},3228:function(u,t,e){"use strict";var r=e(8125);u.exports=function(){return String.prototype.trim&&""==="".trim()&&""==="".trim()&&"_"==="_".trim()&&"_"==="_".trim()?String.prototype.trim:r}},818:function(u,t,e){"use strict";var r=e(9722),n=e(3228);u.exports=function(){var u=n();return r(String.prototype,{trim:u},{trim:function(){return String.prototype.trim!==u}}),u}},7002:function(){},1510:function(u,t,e){"use strict";var r=e(2870),n=e(6318),o=e(1874),D=e(2990),i=e(5674),a=r("%TypeError%");u.exports=function(u,t,e){if("String"!==o(u))throw new a("Assertion failed: `S` must be a String");if(!D(t)||t<0||t>i)throw new a("Assertion failed: `length` must be an integer >= 0 and <= 2**53");if("Boolean"!==o(e))throw new a("Assertion failed: `unicode` must be a Boolean");return e?t+1>=u.length?t+1:t+n(u,t)["[[CodeUnitCount]]"]:t+1}},7113:function(u,t,e){"use strict";var r=e(2870),n=e(3099),o=r("%TypeError%"),D=e(6287),i=r("%Reflect.apply%",!0)||n("Function.prototype.apply");u.exports=function(u,t){var e=arguments.length>2?arguments[2]:[];if(!D(e))throw new o("Assertion failed: optional `argumentsList`, if provided, must be a List");return i(u,t,e)}},6318:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(3099),o=e(5541),D=e(959),i=e(1874),a=e(1751),F=n("String.prototype.charAt"),c=n("String.prototype.charCodeAt");u.exports=function(u,t){if("String"!==i(u))throw new r("Assertion failed: `string` must be a String");var e=u.length;if(t<0||t>=e)throw new r("Assertion failed: `position` must be >= 0, and < the length of `string`");var n=c(u,t),l=F(u,t),f=o(n),s=D(n);if(!f&&!s)return{"[[CodePoint]]":l,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!1};if(s||t+1===e)return{"[[CodePoint]]":l,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!0};var A=c(u,t+1);return D(A)?{"[[CodePoint]]":a(n,A),"[[CodeUnitCount]]":2,"[[IsUnpairedSurrogate]]":!1}:{"[[CodePoint]]":l,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!0}}},5702:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(1874);u.exports=function(u,t){if("Boolean"!==n(t))throw new r("Assertion failed: Type(done) is not Boolean");return{value:u,done:t}}},6782:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(2860),o=e(8357),D=e(3301),i=e(6284),a=e(8277),F=e(1874);u.exports=function(u,t,e){if("Object"!==F(u))throw new r("Assertion failed: Type(O) is not Object");if(!i(t))throw new r("Assertion failed: IsPropertyKey(P) is not true");return n(D,a,o,u,t,{"[[Configurable]]":!0,"[[Enumerable]]":!1,"[[Value]]":e,"[[Writable]]":!0})}},1398:function(u,t,e){"use strict";var r=e(2870),n=e(1143)(),o=r("%TypeError%"),D=r("%IteratorPrototype%",!0),i=e(1510),a=e(5702),F=e(6782),c=e(151),l=e(5716),f=e(3500),s=e(8322),A=e(3995),p=e(5128),E=e(1874),C=e(7284),y=e(2263),d=function(u,t,e,r){if("String"!==E(t))throw new o("`S` must be a string");if("Boolean"!==E(e))throw new o("`global` must be a boolean");if("Boolean"!==E(r))throw new o("`fullUnicode` must be a boolean");C.set(this,"[[IteratingRegExp]]",u),C.set(this,"[[IteratedString]]",t),C.set(this,"[[Global]]",e),C.set(this,"[[Unicode]]",r),C.set(this,"[[Done]]",!1)};D&&(d.prototype=l(D)),F(d.prototype,"next",(function(){var u=this;if("Object"!==E(u))throw new o("receiver must be an object");if(!(u instanceof d&&C.has(u,"[[IteratingRegExp]]")&&C.has(u,"[[IteratedString]]")&&C.has(u,"[[Global]]")&&C.has(u,"[[Unicode]]")&&C.has(u,"[[Done]]")))throw new o('"this" value must be a RegExpStringIterator instance');if(C.get(u,"[[Done]]"))return a(void 0,!0);var t=C.get(u,"[[IteratingRegExp]]"),e=C.get(u,"[[IteratedString]]"),r=C.get(u,"[[Global]]"),n=C.get(u,"[[Unicode]]"),D=f(t,e);if(null===D)return C.set(u,"[[Done]]",!0),a(void 0,!0);if(r){if(""===p(c(D,"0"))){var F=A(c(t,"lastIndex")),l=i(e,F,n);s(t,"lastIndex",l,!0)}return a(D,!1)}return C.set(u,"[[Done]]",!0),a(D,!1)})),n&&(y(d.prototype,"RegExp String Iterator"),Symbol.iterator&&"function"!=typeof d.prototype[Symbol.iterator])&&F(d.prototype,Symbol.iterator,(function(){return this})),u.exports=function(u,t,e,r){return new d(u,t,e,r)}},3645:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(7999),o=e(2860),D=e(8357),i=e(8355),a=e(3301),F=e(6284),c=e(8277),l=e(7628),f=e(1874);u.exports=function(u,t,e){if("Object"!==f(u))throw new r("Assertion failed: Type(O) is not Object");if(!F(t))throw new r("Assertion failed: IsPropertyKey(P) is not true");var s=n({Type:f,IsDataDescriptor:a,IsAccessorDescriptor:i},e)?e:l(e);if(!n({Type:f,IsDataDescriptor:a,IsAccessorDescriptor:i},s))throw new r("Assertion failed: Desc is not a valid Property Descriptor");return o(a,c,D,u,t,s)}},8357:function(u,t,e){"use strict";var r=e(1489),n=e(1598),o=e(1874);u.exports=function(u){return void 0!==u&&r(o,"Property Descriptor","Desc",u),n(u)}},151:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(4538),o=e(6284),D=e(1874);u.exports=function(u,t){if("Object"!==D(u))throw new r("Assertion failed: Type(O) is not Object");if(!o(t))throw new r("Assertion failed: IsPropertyKey(P) is not true, got "+n(t));return u[t]}},1959:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(9374),o=e(7304),D=e(6284),i=e(4538);u.exports=function(u,t){if(!D(t))throw new r("Assertion failed: IsPropertyKey(P) is not true");var e=n(u,t);if(null!=e){if(!o(e))throw new r(i(t)+" is not a function: "+i(e));return e}}},9374:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(4538),o=e(6284);u.exports=function(u,t){if(!o(t))throw new r("Assertion failed: IsPropertyKey(P) is not true, got "+n(t));return u[t]}},8355:function(u,t,e){"use strict";var r=e(9545),n=e(1874),o=e(1489);u.exports=function(u){return void 0!==u&&(o(n,"Property Descriptor","Desc",u),!(!r(u,"[[Get]]")&&!r(u,"[[Set]]")))}},6287:function(u,t,e){"use strict";u.exports=e(2403)},7304:function(u,t,e){"use strict";u.exports=e(3655)},4791:function(u,t,e){"use strict";var r=e(6740)("%Reflect.construct%",!0),n=e(3645);try{n({},"",{"[[Get]]":function(){}})}catch(u){n=null}if(n&&r){var o={},D={};n(D,"length",{"[[Get]]":function(){throw o},"[[Enumerable]]":!0}),u.exports=function(u){try{r(u,D)}catch(u){return u===o}}}else u.exports=function(u){return"function"==typeof u&&!!u.prototype}},3301:function(u,t,e){"use strict";var r=e(9545),n=e(1874),o=e(1489);u.exports=function(u){return void 0!==u&&(o(n,"Property Descriptor","Desc",u),!(!r(u,"[[Value]]")&&!r(u,"[[Writable]]")))}},6284:function(u){"use strict";u.exports=function(u){return"string"==typeof u||"symbol"==typeof u}},9497:function(u,t,e){"use strict";var r=e(2870)("%Symbol.match%",!0),n=e(5494),o=e(5695);u.exports=function(u){if(!u||"object"!=typeof u)return!1;if(r){var t=u[r];if(void 0!==t)return o(t)}return n(u)}},5716:function(u,t,e){"use strict";var r=e(2870),n=r("%Object.create%",!0),o=r("%TypeError%"),D=r("%SyntaxError%"),i=e(6287),a=e(1874),F=e(7735),c=e(7284),l=e(3413)();u.exports=function(u){if(null!==u&&"Object"!==a(u))throw new o("Assertion failed: `proto` must be null or an object");var t,e=arguments.length<2?[]:arguments[1];if(!i(e))throw new o("Assertion failed: `additionalInternalSlotsList` must be an Array");if(n)t=n(u);else if(l)t={__proto__:u};else{if(null===u)throw new D("native Object.create support is required to create null objects");var r=function(){};r.prototype=u,t=new r}return e.length>0&&F(e,(function(u){c.set(t,u,void 0)})),t}},3500:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(3099)("RegExp.prototype.exec"),o=e(7113),D=e(151),i=e(7304),a=e(1874);u.exports=function(u,t){if("Object"!==a(u))throw new r("Assertion failed: `R` must be an Object");if("String"!==a(t))throw new r("Assertion failed: `S` must be a String");var e=D(u,"exec");if(i(e)){var F=o(e,u,[t]);if(null===F||"Object"===a(F))return F;throw new r('"exec" method must return `null` or an Object')}return n(u,t)}},6751:function(u,t,e){"use strict";u.exports=e(9572)},8277:function(u,t,e){"use strict";var r=e(159);u.exports=function(u,t){return u===t?0!==u||1/u==1/t:r(u)&&r(t)}},8322:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(6284),o=e(8277),D=e(1874),i=function(){try{return delete[].length,!0}catch(u){return!1}}();u.exports=function(u,t,e,a){if("Object"!==D(u))throw new r("Assertion failed: `O` must be an Object");if(!n(t))throw new r("Assertion failed: `P` must be a Property Key");if("Boolean"!==D(a))throw new r("Assertion failed: `Throw` must be a Boolean");if(a){if(u[t]=e,i&&!o(u[t],e))throw new r("Attempted to assign to readonly property.");return!0}try{return u[t]=e,!i||o(u[t],e)}catch(u){return!1}}},2449:function(u,t,e){"use strict";var r=e(2870),n=r("%Symbol.species%",!0),o=r("%TypeError%"),D=e(4791),i=e(1874);u.exports=function(u,t){if("Object"!==i(u))throw new o("Assertion failed: Type(O) is not Object");var e=u.constructor;if(void 0===e)return t;if("Object"!==i(e))throw new o("O.constructor is not an Object");var r=n?e[n]:void 0;if(null==r)return t;if(D(r))return r;throw new o("no constructor found")}},6207:function(u,t,e){"use strict";var r=e(2870),n=r("%Number%"),o=r("%RegExp%"),D=r("%TypeError%"),i=r("%parseInt%"),a=e(3099),F=e(7582),c=a("String.prototype.slice"),l=F(/^0b[01]+$/i),f=F(/^0o[0-7]+$/i),s=F(/^[-+]0x[0-9a-f]+$/i),A=F(new o("["+[" ","",""].join("")+"]","g")),p=e(9434),E=e(1874);u.exports=function u(t){if("String"!==E(t))throw new D("Assertion failed: `argument` is not a String");if(l(t))return n(i(c(t,2),2));if(f(t))return n(i(c(t,2),8));if(A(t)||s(t))return NaN;var e=p(t);return e!==t?u(e):n(t)}},5695:function(u){"use strict";u.exports=function(u){return!!u}},1200:function(u,t,e){"use strict";var r=e(6542),n=e(5693),o=e(159),D=e(1117);u.exports=function(u){var t=r(u);return o(t)||0===t?0:D(t)?n(t):t}},3995:function(u,t,e){"use strict";var r=e(5674),n=e(1200);u.exports=function(u){var t=n(u);return t<=0?0:t>r?r:t}},6542:function(u,t,e){"use strict";var r=e(2870),n=r("%TypeError%"),o=r("%Number%"),D=e(8606),i=e(703),a=e(6207);u.exports=function(u){var t=D(u)?u:i(u,o);if("symbol"==typeof t)throw new n("Cannot convert a Symbol value to a number");if("bigint"==typeof t)throw new n("Conversion from 'BigInt' to 'number' is not allowed.");return"string"==typeof t?a(t):o(t)}},703:function(u,t,e){"use strict";var r=e(7358);u.exports=function(u){return arguments.length>1?r(u,arguments[1]):r(u)}},7628:function(u,t,e){"use strict";var r=e(9545),n=e(2870)("%TypeError%"),o=e(1874),D=e(5695),i=e(7304);u.exports=function(u){if("Object"!==o(u))throw new n("ToPropertyDescriptor requires an object");var t={};if(r(u,"enumerable")&&(t["[[Enumerable]]"]=D(u.enumerable)),r(u,"configurable")&&(t["[[Configurable]]"]=D(u.configurable)),r(u,"value")&&(t["[[Value]]"]=u.value),r(u,"writable")&&(t["[[Writable]]"]=D(u.writable)),r(u,"get")){var e=u.get;if(void 0!==e&&!i(e))throw new n("getter must be a function");t["[[Get]]"]=e}if(r(u,"set")){var a=u.set;if(void 0!==a&&!i(a))throw new n("setter must be a function");t["[[Set]]"]=a}if((r(t,"[[Get]]")||r(t,"[[Set]]"))&&(r(t,"[[Value]]")||r(t,"[[Writable]]")))throw new n("Invalid property descriptor. Cannot both specify accessors and a value or writable attribute");return t}},5128:function(u,t,e){"use strict";var r=e(2870),n=r("%String%"),o=r("%TypeError%");u.exports=function(u){if("symbol"==typeof u)throw new o("Cannot convert a Symbol value to a string");return n(u)}},1874:function(u,t,e){"use strict";var r=e(6101);u.exports=function(u){return"symbol"==typeof u?"Symbol":"bigint"==typeof u?"BigInt":r(u)}},1751:function(u,t,e){"use strict";var r=e(2870),n=r("%TypeError%"),o=r("%String.fromCharCode%"),D=e(5541),i=e(959);u.exports=function(u,t){if(!D(u)||!i(t))throw new n("Assertion failed: `lead` must be a leading surrogate char code, and `trail` must be a trailing surrogate char code");return o(u)+o(t)}},3567:function(u,t,e){"use strict";var r=e(1874),n=Math.floor;u.exports=function(u){return"BigInt"===r(u)?u:n(u)}},5693:function(u,t,e){"use strict";var r=e(2870),n=e(3567),o=r("%TypeError%");u.exports=function(u){if("number"!=typeof u&&"bigint"!=typeof u)throw new o("argument must be a Number or a BigInt");var t=u<0?-n(-u):n(u);return 0===t?0:t}},9572:function(u,t,e){"use strict";var r=e(2870)("%TypeError%");u.exports=function(u,t){if(null==u)throw new r(t||"Cannot call method on "+u);return u}},6101:function(u){"use strict";u.exports=function(u){return null===u?"Null":void 0===u?"Undefined":"function"==typeof u||"object"==typeof u?"Object":"number"==typeof u?"Number":"boolean"==typeof u?"Boolean":"string"==typeof u?"String":void 0}},6740:function(u,t,e){"use strict";u.exports=e(2870)},2860:function(u,t,e){"use strict";var r=e(229),n=e(2870),o=r()&&n("%Object.defineProperty%",!0),D=r.hasArrayLengthDefineBug(),i=D&&e(2403),a=e(3099)("Object.prototype.propertyIsEnumerable");u.exports=function(u,t,e,r,n,F){if(!o){if(!u(F))return!1;if(!F["[[Configurable]]"]||!F["[[Writable]]"])return!1;if(n in r&&a(r,n)!==!!F["[[Enumerable]]"])return!1;var c=F["[[Value]]"];return r[n]=c,t(r[n],c)}return D&&"length"===n&&"[[Value]]"in F&&i(r)&&r.length!==F["[[Value]]"]?(r.length=F["[[Value]]"],r.length===F["[[Value]]"]):(o(r,n,e(F)),!0)}},2403:function(u,t,e){"use strict";var r=e(2870)("%Array%"),n=!r.isArray&&e(3099)("Object.prototype.toString");u.exports=r.isArray||function(u){return"[object Array]"===n(u)}},1489:function(u,t,e){"use strict";var r=e(2870),n=r("%TypeError%"),o=r("%SyntaxError%"),D=e(9545),i=e(2990),a={"Property Descriptor":function(u){var t={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};if(!u)return!1;for(var e in u)if(D(u,e)&&!t[e])return!1;var r=D(u,"[[Value]]"),o=D(u,"[[Get]]")||D(u,"[[Set]]");if(r&&o)throw new n("Property Descriptors may not be both accessor and data descriptors");return!0},"Match Record":e(900),"Iterator Record":function(u){return D(u,"[[Iterator]]")&&D(u,"[[NextMethod]]")&&D(u,"[[Done]]")},"PromiseCapability Record":function(u){return!!u&&D(u,"[[Resolve]]")&&"function"==typeof u["[[Resolve]]"]&&D(u,"[[Reject]]")&&"function"==typeof u["[[Reject]]"]&&D(u,"[[Promise]]")&&u["[[Promise]]"]&&"function"==typeof u["[[Promise]]"].then},"AsyncGeneratorRequest Record":function(u){return!!u&&D(u,"[[Completion]]")&&D(u,"[[Capability]]")&&a["PromiseCapability Record"](u["[[Capability]]"])},"RegExp Record":function(u){return u&&D(u,"[[IgnoreCase]]")&&"boolean"==typeof u["[[IgnoreCase]]"]&&D(u,"[[Multiline]]")&&"boolean"==typeof u["[[Multiline]]"]&&D(u,"[[DotAll]]")&&"boolean"==typeof u["[[DotAll]]"]&&D(u,"[[Unicode]]")&&"boolean"==typeof u["[[Unicode]]"]&&D(u,"[[CapturingGroupsCount]]")&&"number"==typeof u["[[CapturingGroupsCount]]"]&&i(u["[[CapturingGroupsCount]]"])&&u["[[CapturingGroupsCount]]"]>=0}};u.exports=function(u,t,e,r){var D=a[t];if("function"!=typeof D)throw new o("unknown record type: "+t);if("Object"!==u(r)||!D(r))throw new n(e+" must be a "+t)}},7735:function(u){"use strict";u.exports=function(u,t){for(var e=0;e<u.length;e+=1)t(u[e],e,u)}},1598:function(u){"use strict";u.exports=function(u){if(void 0===u)return u;var t={};return"[[Value]]"in u&&(t.value=u["[[Value]]"]),"[[Writable]]"in u&&(t.writable=!!u["[[Writable]]"]),"[[Get]]"in u&&(t.get=u["[[Get]]"]),"[[Set]]"in u&&(t.set=u["[[Set]]"]),"[[Enumerable]]"in u&&(t.enumerable=!!u["[[Enumerable]]"]),"[[Configurable]]"in u&&(t.configurable=!!u["[[Configurable]]"]),t}},1117:function(u,t,e){"use strict";var r=e(159);u.exports=function(u){return("number"==typeof u||"bigint"==typeof u)&&!r(u)&&u!==1/0&&u!==-1/0}},2990:function(u,t,e){"use strict";var r=e(2870),n=r("%Math.abs%"),o=r("%Math.floor%"),D=e(159),i=e(1117);u.exports=function(u){if("number"!=typeof u||D(u)||!i(u))return!1;var t=n(u);return o(t)===t}},5541:function(u){"use strict";u.exports=function(u){return"number"==typeof u&&u>=55296&&u<=56319}},900:function(u,t,e){"use strict";var r=e(9545);u.exports=function(u){return r(u,"[[StartIndex]]")&&r(u,"[[EndIndex]]")&&u["[[StartIndex]]"]>=0&&u["[[EndIndex]]"]>=u["[[StartIndex]]"]&&String(parseInt(u["[[StartIndex]]"],10))===String(u["[[StartIndex]]"])&&String(parseInt(u["[[EndIndex]]"],10))===String(u["[[EndIndex]]"])}},159:function(u){"use strict";u.exports=Number.isNaN||function(u){return u!=u}},8606:function(u){"use strict";u.exports=function(u){return null===u||"function"!=typeof u&&"object"!=typeof u}},7999:function(u,t,e){"use strict";var r=e(2870),n=e(9545),o=r("%TypeError%");u.exports=function(u,t){if("Object"!==u.Type(t))return!1;var e={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};for(var r in t)if(n(t,r)&&!e[r])return!1;if(u.IsDataDescriptor(t)&&u.IsAccessorDescriptor(t))throw new o("Property Descriptors may not be both accessor and data descriptors");return!0}},959:function(u){"use strict";u.exports=function(u){return"number"==typeof u&&u>=56320&&u<=57343}},5674:function(u){"use strict";u.exports=Number.MAX_SAFE_INTEGER||9007199254740991}},t={};function e(r){var n=t[r];if(void 0!==n)return n.exports;var o=t[r]={exports:{}};return u[r](o,o.exports,e),o.exports}e.n=function(u){var t=u&&u.__esModule?function(){return u.default}:function(){return u};return e.d(t,{a:t}),t},e.d=function(u,t){for(var r in t)e.o(t,r)&&!e.o(u,r)&&Object.defineProperty(u,r,{enumerable:!0,get:t[r]})},e.o=function(u,t){return Object.prototype.hasOwnProperty.call(u,t)},function(){"use strict";var u=e(1844);function t(t,e,r){for(var n=0,o=[];-1!==n;)-1!==(n=t.indexOf(e,n))&&(o.push({start:n,end:n+e.length,errors:0}),n+=1);return o.length>0?o:(0,u.Z)(t,e,r)}function r(u,e){return 0===e.length||0===u.length?0:1-t(u,e,e.length)[0].errors/e.length}function n(u){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(u){return typeof u}:function(u){return u&&"function"==typeof Symbol&&u.constructor===Symbol&&u!==Symbol.prototype?"symbol":typeof u},n(u)}function o(u,t){(null==t||t>u.length)&&(t=u.length);for(var e=0,r=new Array(t);e<t;e++)r[e]=u[e];return r}function D(u,t){if(!(u instanceof t))throw new TypeError("Cannot call a class as a function")}function i(u,t){for(var e=0;e<t.length;e++){var r=t[e];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(u,(void 0,o=function(u,t){if("object"!==n(u)||null===u)return u;var e=u[Symbol.toPrimitive];if(void 0!==e){var r=e.call(u,"string");if("object"!==n(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(u)}(r.key),"symbol"===n(o)?o:String(o)),r)}var o}function a(u,t,e){return t&&i(u.prototype,t),e&&i(u,e),Object.defineProperty(u,"prototype",{writable:!1}),u}function F(u){switch(u.nodeType){case Node.ELEMENT_NODE:case Node.TEXT_NODE:return u.textContent.length;default:return 0}}function c(u){for(var t=u.previousSibling,e=0;t;)e+=F(t),t=t.previousSibling;return e}function l(u){for(var t=arguments.length,e=new Array(t>1?t-1:0),r=1;r<t;r++)e[r-1]=arguments[r];for(var n,o=e.shift(),D=u.ownerDocument.createNodeIterator(u,NodeFilter.SHOW_TEXT),i=[],a=D.nextNode(),F=0;void 0!==o&&a;)F+(n=a).data.length>o?(i.push({node:n,offset:o-F}),o=e.shift()):(a=D.nextNode(),F+=n.data.length);for(;void 0!==o&&n&&F===o;)i.push({node:n,offset:n.data.length}),o=e.shift();if(void 0!==o)throw new RangeError("Offset exceeds text length");return i}var f=function(){function u(t,e){if(D(this,u),e<0)throw new Error("Offset is invalid");this.element=t,this.offset=e}return a(u,[{key:"relativeTo",value:function(t){if(!t.contains(this.element))throw new Error("Parent is not an ancestor of current element");for(var e=this.element,r=this.offset;e!==t;)r+=c(e),e=e.parentElement;return new u(e,r)}},{key:"resolve",value:function(){var u=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};try{return l(this.element,this.offset)[0]}catch(n){if(0===this.offset&&void 0!==u.direction){var t=document.createTreeWalker(this.element.getRootNode(),NodeFilter.SHOW_TEXT);t.currentNode=this.element;var e=1===u.direction,r=e?t.nextNode():t.previousNode();if(!r)throw n;return{node:r,offset:e?0:r.data.length}}throw n}}}],[{key:"fromCharOffset",value:function(t,e){switch(t.nodeType){case Node.TEXT_NODE:return u.fromPoint(t,e);case Node.ELEMENT_NODE:return new u(t,e);default:throw new Error("Node is not an element or text node")}}},{key:"fromPoint",value:function(t,e){switch(t.nodeType){case Node.TEXT_NODE:if(e<0||e>t.data.length)throw new Error("Text node offset is out of range");if(!t.parentElement)throw new Error("Text node has no parent");var r=c(t)+e;return new u(t.parentElement,r);case Node.ELEMENT_NODE:if(e<0||e>t.childNodes.length)throw new Error("Child node offset is out of range");for(var n=0,o=0;o<e;o++)n+=F(t.childNodes[o]);return new u(t,n);default:throw new Error("Point is not in an element or text node")}}}]),u}(),s=function(){function u(t,e){D(this,u),this.start=t,this.end=e}return a(u,[{key:"relativeTo",value:function(t){return new u(this.start.relativeTo(t),this.end.relativeTo(t))}},{key:"toRange",value:function(){var u,t,e,r;if(this.start.element===this.end.element&&this.start.offset<=this.end.offset){var n=(e=l(this.start.element,this.start.offset,this.end.offset),r=2,function(u){if(Array.isArray(u))return u}(e)||function(u,t){var e=null==u?null:"undefined"!=typeof Symbol&&u[Symbol.iterator]||u["@@iterator"];if(null!=e){var r,n,o,D,i=[],a=!0,F=!1;try{if(o=(e=e.call(u)).next,0===t){if(Object(e)!==e)return;a=!1}else for(;!(a=(r=o.call(e)).done)&&(i.push(r.value),i.length!==t);a=!0);}catch(u){F=!0,n=u}finally{try{if(!a&&null!=e.return&&(D=e.return(),Object(D)!==D))return}finally{if(F)throw n}}return i}}(e,r)||function(u,t){if(u){if("string"==typeof u)return o(u,t);var e=Object.prototype.toString.call(u).slice(8,-1);return"Object"===e&&u.constructor&&(e=u.constructor.name),"Map"===e||"Set"===e?Array.from(u):"Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e)?o(u,t):void 0}}(e,r)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}());u=n[0],t=n[1]}else u=this.start.resolve({direction:1}),t=this.end.resolve({direction:2});var D=new Range;return D.setStart(u.node,u.offset),D.setEnd(t.node,t.offset),D}}],[{key:"fromRange",value:function(t){return new u(f.fromPoint(t.startContainer,t.startOffset),f.fromPoint(t.endContainer,t.endOffset))}},{key:"fromOffsets",value:function(t,e,r){return new u(new f(t,e),new f(t,r))}}]),u}();function A(u){return A="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(u){return typeof u}:function(u){return u&&"function"==typeof Symbol&&u.constructor===Symbol&&u!==Symbol.prototype?"symbol":typeof u},A(u)}function p(u,t){var e=Object.keys(u);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(u);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(u,t).enumerable}))),e.push.apply(e,r)}return e}function E(u){for(var t=1;t<arguments.length;t++){var e=null!=arguments[t]?arguments[t]:{};t%2?p(Object(e),!0).forEach((function(t){var r,n,o;r=u,n=t,o=e[t],(n=B(n))in r?Object.defineProperty(r,n,{value:o,enumerable:!0,configurable:!0,writable:!0}):r[n]=o})):Object.getOwnPropertyDescriptors?Object.defineProperties(u,Object.getOwnPropertyDescriptors(e)):p(Object(e)).forEach((function(t){Object.defineProperty(u,t,Object.getOwnPropertyDescriptor(e,t))}))}return u}function C(u,t){if(!(u instanceof t))throw new TypeError("Cannot call a class as a function")}function y(u,t){for(var e=0;e<t.length;e++){var r=t[e];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(u,B(r.key),r)}}function d(u,t,e){return t&&y(u.prototype,t),e&&y(u,e),Object.defineProperty(u,"prototype",{writable:!1}),u}function B(u){var t=function(u,t){if("object"!==A(u)||null===u)return u;var e=u[Symbol.toPrimitive];if(void 0!==e){var r=e.call(u,"string");if("object"!==A(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(u)}(u);return"symbol"===A(t)?t:String(t)}var h=function(){function u(t,e,r){C(this,u),this.root=t,this.start=e,this.end=r}return d(u,[{key:"toSelector",value:function(){return{type:"TextPositionSelector",start:this.start,end:this.end}}},{key:"toRange",value:function(){return s.fromOffsets(this.root,this.start,this.end).toRange()}}],[{key:"fromRange",value:function(t,e){var r=s.fromRange(e).relativeTo(t);return new u(t,r.start.offset,r.end.offset)}},{key:"fromSelector",value:function(t,e){return new u(t,e.start,e.end)}}]),u}(),g=function(){function u(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};C(this,u),this.root=t,this.exact=e,this.context=r}return d(u,[{key:"toSelector",value:function(){return{type:"TextQuoteSelector",exact:this.exact,prefix:this.context.prefix,suffix:this.context.suffix}}},{key:"toRange",value:function(){var u=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.toPositionAnchor(u).toRange()}},{key:"toPositionAnchor",value:function(){var u=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},e=function(u,e){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(0===e.length)return null;var o=Math.min(256,e.length/2),D=t(u,e,o);if(0===D.length)return null;var i=function(t){var o=1-t.errors/e.length,D=n.prefix?r(u.slice(Math.max(0,t.start-n.prefix.length),t.start),n.prefix):1,i=n.suffix?r(u.slice(t.end,t.end+n.suffix.length),n.suffix):1,a=1;return"number"==typeof n.hint&&(a=1-Math.abs(t.start-n.hint)/u.length),(50*o+20*D+20*i+2*a)/92},a=D.map((function(u){return{start:u.start,end:u.end,score:i(u)}}));return a.sort((function(u,t){return t.score-u.score})),a[0]}(this.root.textContent,this.exact,E(E({},this.context),{},{hint:u.hint}));if(!e)throw new Error("Quote not found");return new h(this.root,e.start,e.end)}}],[{key:"fromRange",value:function(t,e){var r=t.textContent,n=s.fromRange(e).relativeTo(t),o=n.start.offset,D=n.end.offset;return new u(t,r.slice(o,D),{prefix:r.slice(Math.max(0,o-32),o),suffix:r.slice(D,Math.min(r.length,D+32))})}},{key:"fromSelector",value:function(t,e){var r=e.prefix,n=e.suffix;return new u(t,e.exact,{prefix:r,suffix:n})}}]),u}();function m(u,t){(null==t||t>u.length)&&(t=u.length);for(var e=0,r=new Array(t);e<t;e++)r[e]=u[e];return r}window.addEventListener("error",(function(u){Android.logError(u.message,u.filename,u.lineno)}),!1),window.addEventListener("load",(function(){new ResizeObserver((function(){var u;u=Android.getViewportWidth(),b=u/window.devicePixelRatio,T("--RS__viewportWidth","calc("+u+"px / "+window.devicePixelRatio+")"),function(){var u="readium-virtual-page",t=document.getElementById(u);if(v()||2!=parseInt(window.getComputedStyle(document.documentElement).getPropertyValue("column-count")))t&&t.remove();else{var e=document.scrollingElement.scrollWidth/b;Math.round(2*e)/2%1>.1&&(t?t.remove():((t=document.createElement("div")).setAttribute("id",u),t.style.breakBefore="column",t.innerHTML="​",document.body.appendChild(t)))}}(),j()})).observe(document.body)}),!1);var b=1;function v(){var u=document.documentElement.style;return"readium-scroll-on"==u.getPropertyValue("--USER__view").trim()||"readium-scroll-on"==u.getPropertyValue("--USER__scroll").trim()}function w(){return"rtl"==document.body.dir.toLowerCase()}function S(u){return v()?document.scrollingElement.scrollTop=u.top+window.scrollY:document.scrollingElement.scrollLeft=O(u.left+window.scrollX),!0}function x(u){if(v())throw"Called scrollToOffset() with scroll mode enabled. This can only be used in paginated mode.";var t=window.scrollX;return document.scrollingElement.scrollLeft=O(u),Math.abs(t-u)/b>.01}function O(u){var t=u+(w()?-1:1);return t-t%b}function j(){if(!v()){var u=window.scrollX,t=(w()?-1:1)*(b/2);document.scrollingElement.scrollLeft=O(u+t)}}function P(u){try{var t,e=u.locations,r=u.text;if(r&&r.highlight)return e&&e.cssSelector&&(t=document.querySelector(e.cssSelector)),t||(t=document.body),new g(t,r.highlight,{prefix:r.before,suffix:r.after}).toRange();if(e){var n=null;if(!n&&e.cssSelector&&(n=document.querySelector(e.cssSelector)),!n&&e.fragments){var o,D=function(u,t){var e="undefined"!=typeof Symbol&&u[Symbol.iterator]||u["@@iterator"];if(!e){if(Array.isArray(u)||(e=function(u,t){if(u){if("string"==typeof u)return m(u,t);var e=Object.prototype.toString.call(u).slice(8,-1);return"Object"===e&&u.constructor&&(e=u.constructor.name),"Map"===e||"Set"===e?Array.from(u):"Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e)?m(u,t):void 0}}(u))||t&&u&&"number"==typeof u.length){e&&(u=e);var r=0,n=function(){};return{s:n,n:function(){return r>=u.length?{done:!0}:{done:!1,value:u[r++]}},e:function(u){throw u},f:n}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,D=!0,i=!1;return{s:function(){e=e.call(u)},n:function(){var u=e.next();return D=u.done,u},e:function(u){i=!0,o=u},f:function(){try{D||null==e.return||e.return()}finally{if(i)throw o}}}}(e.fragments);try{for(D.s();!(o=D.n()).done;){var i=o.value;if(n=document.getElementById(i))break}}catch(u){D.e(u)}finally{D.f()}}if(n){var a=document.createRange();return a.setStartBefore(n),a.setEndAfter(n),a}}}catch(u){N(u)}return null}function T(u,t){null===t||""===t?I(u):document.documentElement.style.setProperty(u,t,"important")}function I(u){document.documentElement.style.removeProperty(u)}function R(){var u=Array.prototype.slice.call(arguments).join(" ");Android.log(u)}function N(u){Android.logError(u,"",0)}function M(u,t){var e="undefined"!=typeof Symbol&&u[Symbol.iterator]||u["@@iterator"];if(!e){if(Array.isArray(u)||(e=function(u,t){if(u){if("string"==typeof u)return k(u,t);var e=Object.prototype.toString.call(u).slice(8,-1);return"Object"===e&&u.constructor&&(e=u.constructor.name),"Map"===e||"Set"===e?Array.from(u):"Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e)?k(u,t):void 0}}(u))||t&&u&&"number"==typeof u.length){e&&(u=e);var r=0,n=function(){};return{s:n,n:function(){return r>=u.length?{done:!0}:{done:!1,value:u[r++]}},e:function(u){throw u},f:n}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,D=!0,i=!1;return{s:function(){e=e.call(u)},n:function(){var u=e.next();return D=u.done,u},e:function(u){i=!0,o=u},f:function(){try{D||null==e.return||e.return()}finally{if(i)throw o}}}}function k(u,t){(null==t||t>u.length)&&(t=u.length);for(var e=0,r=new Array(t);e<t;e++)r[e]=u[e];return r}var $=!1;function L(u){var t=window.devicePixelRatio,e=u.width*t,r=u.height*t,n=u.left*t,o=u.top*t;return{width:e,height:r,left:n,top:o,right:n+e,bottom:o+r}}function U(u,t){var e,r=[],n=M(u.getClientRects());try{for(n.s();!(e=n.n()).done;){var o=e.value;r.push({bottom:o.bottom,height:o.height,left:o.left,right:o.right,top:o.top,width:o.width})}}catch(u){n.e(u)}finally{n.f()}for(var D=function(u,t){var e,r=new Set(u),n=M(u);try{for(n.s();!(e=n.n()).done;){var o=e.value;if(o.width>1&&o.height>1){var D,i=M(u);try{for(i.s();!(D=i.n()).done;){var a=D.value;if(o!==a&&r.has(a)&&G(a,o,1)){z("CLIENT RECT: remove contained"),r.delete(o);break}}}catch(u){i.e(u)}finally{i.f()}}else z("CLIENT RECT: remove tiny"),r.delete(o)}}catch(u){n.e(u)}finally{n.f()}return Array.from(r)}(_(r,1,t)),i=H(D),a=i.length-1;a>=0;a--){var F=i[a];if(!(F.width*F.height>4)){if(!(i.length>1)){z("CLIENT RECT: remove small, but keep otherwise empty!");break}z("CLIENT RECT: remove small"),i.splice(a,1)}}return z("CLIENT RECT: reduced ".concat(r.length," --\x3e ").concat(i.length)),i}function _(u,t,e){for(var r=0;r<u.length;r++)for(var n,o=function(){var n=u[r],o=u[D];if(n===o)return z("mergeTouchingRects rect1 === rect2 ??!"),0;var i=Y(n.top,o.top,t)&&Y(n.bottom,o.bottom,t),a=Y(n.left,o.left,t)&&Y(n.right,o.right,t);if((a&&!e||i&&!a)&&X(n,o,t)){z("CLIENT RECT: merging two into one, VERTICAL: ".concat(i," HORIZONTAL: ").concat(a," (").concat(e,")"));var F=u.filter((function(u){return u!==n&&u!==o})),c=W(n,o);return F.push(c),{v:_(F,t,e)}}},D=r+1;D<u.length;D++)if(0!==(n=o())&&n)return n.v;return u}function W(u,t){var e=Math.min(u.left,t.left),r=Math.max(u.right,t.right),n=Math.min(u.top,t.top),o=Math.max(u.bottom,t.bottom);return{bottom:o,height:o-n,left:e,right:r,top:n,width:r-e}}function G(u,t,e){return V(u,t.left,t.top,e)&&V(u,t.right,t.top,e)&&V(u,t.left,t.bottom,e)&&V(u,t.right,t.bottom,e)}function V(u,t,e,r){return(u.left<t||Y(u.left,t,r))&&(u.right>t||Y(u.right,t,r))&&(u.top<e||Y(u.top,e,r))&&(u.bottom>e||Y(u.bottom,e,r))}function H(u){for(var t=0;t<u.length;t++)for(var e,r=function(){var e=u[t],r=u[n];if(e===r)return z("replaceOverlapingRects rect1 === rect2 ??!"),0;if(X(e,r,-1)){var o,D=[],i=q(e,r);if(1===i.length)D=i,o=e;else{var a=q(r,e);i.length<a.length?(D=i,o=e):(D=a,o=r)}z("CLIENT RECT: overlap, cut one rect into ".concat(D.length));var F=u.filter((function(u){return u!==o}));return Array.prototype.push.apply(F,D),{v:H(F)}}},n=t+1;n<u.length;n++)if(0!==(e=r())&&e)return e.v;return u}function q(u,t){var e=function(u,t){var e=Math.max(u.left,t.left),r=Math.min(u.right,t.right),n=Math.max(u.top,t.top),o=Math.min(u.bottom,t.bottom);return{bottom:o,height:Math.max(0,o-n),left:e,right:r,top:n,width:Math.max(0,r-e)}}(t,u);if(0===e.height||0===e.width)return[u];var r=[],n={bottom:u.bottom,height:0,left:u.left,right:e.left,top:u.top,width:0};n.width=n.right-n.left,n.height=n.bottom-n.top,0!==n.height&&0!==n.width&&r.push(n);var o={bottom:e.top,height:0,left:e.left,right:e.right,top:u.top,width:0};o.width=o.right-o.left,o.height=o.bottom-o.top,0!==o.height&&0!==o.width&&r.push(o);var D={bottom:u.bottom,height:0,left:e.left,right:e.right,top:e.bottom,width:0};D.width=D.right-D.left,D.height=D.bottom-D.top,0!==D.height&&0!==D.width&&r.push(D);var i={bottom:u.bottom,height:0,left:e.right,right:u.right,top:u.top,width:0};return i.width=i.right-i.left,i.height=i.bottom-i.top,0!==i.height&&0!==i.width&&r.push(i),r}function X(u,t,e){return(u.left<t.right||e>=0&&Y(u.left,t.right,e))&&(t.left<u.right||e>=0&&Y(t.left,u.right,e))&&(u.top<t.bottom||e>=0&&Y(u.top,t.bottom,e))&&(t.top<u.bottom||e>=0&&Y(t.top,u.bottom,e))}function Y(u,t,e){return Math.abs(u-t)<=e}function z(){$&&R.apply(null,arguments)}function J(u,t){var e="undefined"!=typeof Symbol&&u[Symbol.iterator]||u["@@iterator"];if(!e){if(Array.isArray(u)||(e=Z(u))||t&&u&&"number"==typeof u.length){e&&(u=e);var r=0,n=function(){};return{s:n,n:function(){return r>=u.length?{done:!0}:{done:!1,value:u[r++]}},e:function(u){throw u},f:n}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,D=!0,i=!1;return{s:function(){e=e.call(u)},n:function(){var u=e.next();return D=u.done,u},e:function(u){i=!0,o=u},f:function(){try{D||null==e.return||e.return()}finally{if(i)throw o}}}}function K(u,t){return function(u){if(Array.isArray(u))return u}(u)||function(u,t){var e=null==u?null:"undefined"!=typeof Symbol&&u[Symbol.iterator]||u["@@iterator"];if(null!=e){var r,n,o,D,i=[],a=!0,F=!1;try{if(o=(e=e.call(u)).next,0===t){if(Object(e)!==e)return;a=!1}else for(;!(a=(r=o.call(e)).done)&&(i.push(r.value),i.length!==t);a=!0);}catch(u){F=!0,n=u}finally{try{if(!a&&null!=e.return&&(D=e.return(),Object(D)!==D))return}finally{if(F)throw n}}return i}}(u,t)||Z(u,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Z(u,t){if(u){if("string"==typeof u)return Q(u,t);var e=Object.prototype.toString.call(u).slice(8,-1);return"Object"===e&&u.constructor&&(e=u.constructor.name),"Map"===e||"Set"===e?Array.from(u):"Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e)?Q(u,t):void 0}}function Q(u,t){(null==t||t>u.length)&&(t=u.length);for(var e=0,r=new Array(t);e<t;e++)r[e]=u[e];return r}var uu,tu,eu=new Map,ru=new Map,nu=0;function ou(u){return u&&u instanceof Element}window.addEventListener("load",(function(){var u=document.body,t={width:0,height:0};new ResizeObserver((function(){t.width===u.clientWidth&&t.height===u.clientHeight||(t={width:u.clientWidth,height:u.clientHeight},ru.forEach((function(u){u.requestLayout()})))})).observe(u)}),!1),function(u){u.NONE="none",u.DESCENDANT="descendant",u.CHILD="child"}(uu||(uu={})),function(u){u.id="id",u.class="class",u.tag="tag",u.attribute="attribute",u.nthchild="nthchild",u.nthoftype="nthoftype"}(tu||(tu={}));const Du="CssSelectorGenerator";function iu(u="unknown problem",...t){console.warn(`${Du}: ${u}`,...t)}const au={selectors:[tu.id,tu.class,tu.tag,tu.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function Fu(u){return u instanceof RegExp}function cu(u){return["string","function"].includes(typeof u)||Fu(u)}function lu(u){return Array.isArray(u)?u.filter(cu):[]}function fu(u){const t=[Node.DOCUMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE,Node.ELEMENT_NODE];return function(u){return u instanceof Node}(u)&&t.includes(u.nodeType)}function su(u,t){if(fu(u))return u.contains(t)||iu("element root mismatch","Provided root does not contain the element. This will most likely result in producing a fallback selector using element's real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will nto work as intended."),u;const e=t.getRootNode({composed:!1});return fu(e)?(e!==document&&iu("shadow root inferred","You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended."),e):t.ownerDocument.querySelector(":root")}function Au(u){return"number"==typeof u?u:Number.POSITIVE_INFINITY}function pu(u=[]){const[t=[],...e]=u;return 0===e.length?t:e.reduce(((u,t)=>u.filter((u=>t.includes(u)))),t)}function Eu(u){return[].concat(...u)}function Cu(u){const t=u.map((u=>{if(Fu(u))return t=>u.test(t);if("function"==typeof u)return t=>{const e=u(t);return"boolean"!=typeof e?(iu("pattern matcher function invalid","Provided pattern matching function does not return boolean. It's result will be ignored.",u),!1):e};if("string"==typeof u){const t=new RegExp("^"+u.replace(/[|\\{}()[\]^$+?.]/g,"\\$&").replace(/\*/g,".+")+"$");return u=>t.test(u)}return iu("pattern matcher invalid","Pattern matching only accepts strings, regular expressions and/or functions. This item is invalid and will be ignored.",u),()=>!1}));return u=>t.some((t=>t(u)))}function yu(u,t,e){const r=Array.from(su(e,u[0]).querySelectorAll(t));return r.length===u.length&&u.every((u=>r.includes(u)))}function du(u,t){t=null!=t?t:function(u){return u.ownerDocument.querySelector(":root")}(u);const e=[];let r=u;for(;ou(r)&&r!==t;)e.push(r),r=r.parentElement;return e}function Bu(u,t){return pu(u.map((u=>du(u,t))))}const hu=" > ",gu=" ",mu={[uu.NONE]:{type:uu.NONE,value:""},[uu.DESCENDANT]:{type:uu.DESCENDANT,value:hu},[uu.CHILD]:{type:uu.CHILD,value:gu}},bu=new RegExp(["^$","\\s"].join("|")),vu=new RegExp(["^$"].join("|")),wu=[tu.nthoftype,tu.tag,tu.id,tu.class,tu.attribute,tu.nthchild],Su=Cu(["class","id","ng-*"]);function xu({nodeName:u}){return`[${u}]`}function Ou({nodeName:u,nodeValue:t}){return`[${u}='${Wu(t)}']`}function ju(u){const t=Array.from(u.attributes).filter((t=>function({nodeName:u},t){const e=t.tagName.toLowerCase();return!(["input","option"].includes(e)&&"value"===u||Su(u))}(t,u)));return[...t.map(xu),...t.map(Ou)]}function Pu(u){return(u.getAttribute("class")||"").trim().split(/\s+/).filter((u=>!vu.test(u))).map((u=>`.${Wu(u)}`))}function Tu(u){const t=u.getAttribute("id")||"",e=`#${Wu(t)}`,r=u.getRootNode({composed:!1});return!bu.test(t)&&yu([u],e,r)?[e]:[]}function Iu(u){const t=u.parentNode;if(t){const e=Array.from(t.childNodes).filter(ou).indexOf(u);if(e>-1)return[`:nth-child(${e+1})`]}return[]}function Ru(u){return[Wu(u.tagName.toLowerCase())]}function Nu(u){const t=[...new Set(Eu(u.map(Ru)))];return 0===t.length||t.length>1?[]:[t[0]]}function Mu(u){const t=Nu([u])[0],e=u.parentElement;if(e){const r=Array.from(e.children).filter((u=>u.tagName.toLowerCase()===t)),n=r.indexOf(u);if(n>-1)return[`${t}:nth-of-type(${n+1})`]}return[]}function ku(u=[],{maxResults:t=Number.POSITIVE_INFINITY}={}){const e=[];let r=0,n=Lu(1);for(;n.length<=u.length&&r<t;)r+=1,e.push(n.map((t=>u[t]))),n=$u(n,u.length-1);return e}function $u(u=[],t=0){const e=u.length;if(0===e)return[];const r=[...u];r[e-1]+=1;for(let u=e-1;u>=0;u--)if(r[u]>t){if(0===u)return Lu(e+1);r[u-1]++,r[u]=r[u-1]+1}return r[e-1]>t?Lu(e+1):r}function Lu(u=1){return Array.from(Array(u).keys())}const Uu=":".charCodeAt(0).toString(16).toUpperCase(),_u=/[ !"#$%&'()\[\]{|}<>*+,./;=?@^`~\\]/;function Wu(u=""){var t,e;return null!==(e=null===(t=null===CSS||void 0===CSS?void 0:CSS.escape)||void 0===t?void 0:t.call(CSS,u))&&void 0!==e?e:function(u=""){return u.split("").map((u=>":"===u?`\\${Uu} `:_u.test(u)?`\\${u}`:escape(u).replace(/%/g,"\\"))).join("")}(u)}const Gu={tag:Nu,id:function(u){return 0===u.length||u.length>1?[]:Tu(u[0])},class:function(u){return pu(u.map(Pu))},attribute:function(u){return pu(u.map(ju))},nthchild:function(u){return pu(u.map(Iu))},nthoftype:function(u){return pu(u.map(Mu))}},Vu={tag:Ru,id:Tu,class:Pu,attribute:ju,nthchild:Iu,nthoftype:Mu};function Hu(u){return u.includes(tu.tag)||u.includes(tu.nthoftype)?[...u]:[...u,tu.tag]}function qu(u={}){const t=[...wu];return u[tu.tag]&&u[tu.nthoftype]&&t.splice(t.indexOf(tu.tag),1),t.map((t=>{return(r=u)[e=t]?r[e].join(""):"";var e,r})).join("")}function Xu(u,t,e="",r){const n=function(u,t){return""===t?u:function(u,t){return[...u.map((u=>t+gu+u)),...u.map((u=>t+hu+u))]}(u,t)}(function(u,t,e){const r=function(u,t){const{blacklist:e,whitelist:r,combineWithinSelector:n,maxCombinations:o}=t,D=Cu(e),i=Cu(r);return function(u){const{selectors:t,includeTag:e}=u,r=[].concat(t);return e&&!r.includes("tag")&&r.push("tag"),r}(t).reduce(((t,e)=>{const r=function(u,t){var e;return(null!==(e=Gu[t])&&void 0!==e?e:()=>[])(u)}(u,e),a=function(u=[],t,e){return u.filter((u=>e(u)||!t(u)))}(r,D,i),F=function(u=[],t){return u.sort(((u,e)=>{const r=t(u),n=t(e);return r&&!n?-1:!r&&n?1:0}))}(a,i);return t[e]=n?ku(F,{maxResults:o}):F.map((u=>[u])),t}),{})}(u,e),n=function(u,t){return function(u){const{selectors:t,combineBetweenSelectors:e,includeTag:r,maxCandidates:n}=u,o=e?ku(t,{maxResults:n}):t.map((u=>[u]));return r?o.map(Hu):o}(t).map((t=>function(u,t){const e={};return u.forEach((u=>{const r=t[u];r.length>0&&(e[u]=r)})),function(u={}){let t=[];return Object.entries(u).forEach((([u,e])=>{t=e.flatMap((e=>0===t.length?[{[u]:e}]:t.map((t=>Object.assign(Object.assign({},t),{[u]:e})))))})),t}(e).map(qu)}(t,u))).filter((u=>u.length>0))}(r,e),o=Eu(n);return[...new Set(o)]}(u,r.root,r),e);for(const t of n)if(yu(u,t,r.root))return t;return null}function Yu(u){return{value:u,include:!1}}function zu({selectors:u,operator:t}){let e=[...wu];u[tu.tag]&&u[tu.nthoftype]&&(e=e.filter((u=>u!==tu.tag)));let r="";return e.forEach((t=>{(u[t]||[]).forEach((({value:u,include:t})=>{t&&(r+=u)}))})),t.value+r}function Ju(u){return[":root",...du(u).reverse().map((u=>{const t=function(u,t,e=uu.NONE){const r={};return t.forEach((t=>{Reflect.set(r,t,function(u,t){return Vu[t](u)}(u,t).map(Yu))})),{element:u,operator:mu[e],selectors:r}}(u,[tu.nthchild],uu.DESCENDANT);return t.selectors.nthchild.forEach((u=>{u.include=!0})),t})).map(zu)].join("")}function Ku(u,t={}){const e=function(u){const t=(Array.isArray(u)?u:[u]).filter(ou);return[...new Set(t)]}(u),r=function(u,t={}){const e=Object.assign(Object.assign({},au),t);return{selectors:(r=e.selectors,Array.isArray(r)?r.filter((u=>{return t=tu,e=u,Object.values(t).includes(e);var t,e})):[]),whitelist:lu(e.whitelist),blacklist:lu(e.blacklist),root:su(e.root,u),combineWithinSelector:!!e.combineWithinSelector,combineBetweenSelectors:!!e.combineBetweenSelectors,includeTag:!!e.includeTag,maxCombinations:Au(e.maxCombinations),maxCandidates:Au(e.maxCandidates)};var r}(e[0],t);let n="",o=r.root;function D(){return function(u,t,e="",r){if(0===u.length)return null;const n=[u.length>1?u:[],...Bu(u,t).map((u=>[u]))];for(const u of n){const t=Xu(u,0,e,r);if(t)return{foundElements:u,selector:t}}return null}(e,o,n,r)}let i=D();for(;i;){const{foundElements:u,selector:t}=i;if(yu(e,t,r.root))return t;o=u[0],n=t,i=D()}return e.length>1?e.map((u=>Ku(u,r))).join(", "):function(u){return u.map(Ju).join(", ")}(e)}function Zu(u){return null==u?null:-1!=["a","audio","button","canvas","details","input","label","option","select","submit","textarea","video"].indexOf(u.nodeName.toLowerCase())||u.hasAttribute("contenteditable")&&"false"!=u.getAttribute("contenteditable").toLowerCase()?u.outerHTML:u.parentElement?Zu(u.parentElement):null}function Qu(u){for(var t=0;t<u.children.length;t++){var e=u.children[t];if(!tt(e)&&ut(e))return Qu(e)}return u}function ut(u){if(readium.isFixedLayout)return!0;if(u===document.body||u===document.documentElement)return!0;if(!document||!document.documentElement||!document.body)return!1;var t=u.getBoundingClientRect();return v()?t.bottom>0&&t.top<window.innerHeight:t.right>0&&t.left<window.innerWidth}function tt(u){var t=getComputedStyle(u);if(t){if("block"!=t.getPropertyValue("display"))return!0;if("0"===t.getPropertyValue("opacity"))return!0}return!1}function et(u){if(window.getSelection().isCollapsed){var t=window.devicePixelRatio,e={defaultPrevented:u.defaultPrevented,x:u.clientX*t,y:u.clientY*t,targetElement:u.target.outerHTML,interactiveElement:Zu(u.target)};(function(u,t){if(0===ru.size)return!1;var e=function(){var t,e=J(ru);try{for(e.s();!(t=e.n()).done;){var r,n=K(t.value,2),o=n[0],D=J(n[1].items.reverse());try{for(D.s();!(r=D.n()).done;){var i=r.value;if(i.clickableElements){var a,F=J(i.clickableElements);try{for(F.s();!(a=F.n()).done;){var c=a.value,l=c.getBoundingClientRect().toJSON();if(V(l,u.clientX,u.clientY,1))return{group:o,item:i,element:c,rect:l}}}catch(u){F.e(u)}finally{F.f()}}}}catch(u){D.e(u)}finally{D.f()}}}catch(u){e.e(u)}finally{e.f()}}();return!!e&&Android.onDecorationActivated(JSON.stringify({id:e.item.decoration.id,group:e.group,rect:L(e.item.range.getBoundingClientRect()),click:t}))})(u,e)||Android.onTap(JSON.stringify(e))&&(u.stopPropagation(),u.preventDefault())}}function rt(u){return u.defaultPrevented||null!=Zu(document.activeElement)}function nt(u){u.stopPropagation(),u.preventDefault()}function ot(u,t){if(!u.repeat){var e={type:t,code:u.code,characters:String.fromCharCode(u.keyCode),alt:u.altKey,control:u.ctrlKey,shift:u.shiftKey,meta:u.metaKey};Android.onKey(JSON.stringify(e))}}window.addEventListener("DOMContentLoaded",(function(){document.addEventListener("click",et,!1),function(u){u.addEventListener("touchstart",(function(u){e=!0;var n=u.touches[0].clientX*r,o=u.touches[0].clientY*r;t={defaultPrevented:u.defaultPrevented,startX:n,startY:o,currentX:n,currentY:o,offsetX:0,offsetY:0,interactiveElement:Zu(u.target)}}),{passive:!1}),u.addEventListener("touchend",(function(u){t&&(Android.onDragEnd(JSON.stringify(t))&&(u.stopPropagation(),u.preventDefault()),t=void 0)}),{passive:!1}),u.addEventListener("touchmove",(function(u){if(t){t.currentX=u.touches[0].clientX*r,t.currentY=u.touches[0].clientY*r,t.offsetX=t.currentX-t.startX,t.offsetY=t.currentY-t.startY;var n=!1;e?(Math.abs(t.offsetX)>=6||Math.abs(t.offsetY)>=6)&&(e=!1,n=Android.onDragStart(JSON.stringify(t))):n=Android.onDragMove(JSON.stringify(t)),n&&(u.stopPropagation(),u.preventDefault())}}),{passive:!1});var t=void 0,e=!1,r=window.devicePixelRatio}(document)})),window.addEventListener("keydown",(function(u){rt(u)||(nt(u),ot(u,"down"))})),window.addEventListener("keyup",(function(u){rt(u)||(nt(u),ot(u,"up"))}));var Dt=e(5155);e.n(Dt)().shim();var it=!0;function at(){it&&R.apply(null,arguments)}window.addEventListener("load",(function(){var u=!1;document.addEventListener("selectionchange",(function(){var t=window.getSelection().isCollapsed;t&&u?(u=!1,Android.onSelectionEnd(),j()):t||u||(u=!0,Android.onSelectionStart())}))}),!1),window.readium={scrollToId:function(u){var t=document.getElementById(u);return!!t&&S(t.getBoundingClientRect())},scrollToPosition:function(u){if(u<0||u>1)throw"scrollToPosition() must be given a position from 0.0 to 1.0";var t;v()?(t=document.scrollingElement.scrollHeight*u,document.scrollingElement.scrollTop=t):(t=document.scrollingElement.scrollWidth*u*(w()?-1:1),document.scrollingElement.scrollLeft=O(t))},scrollToText:function(u){var t=P({text:u});return!!t&&(function(u){S(u.getBoundingClientRect())}(t),!0)},scrollLeft:function(){var u=document.scrollingElement.scrollWidth,t=window.scrollX-b,e=w()?-(u-b):0;return x(Math.max(t,e))},scrollRight:function(){var u=document.scrollingElement.scrollWidth,t=window.scrollX+b,e=w()?0:u-b;return x(Math.min(t,e))},scrollToStart:function(){v()?(document.scrollingElement.scrollTop=0,window.scrollTo(0,0)):document.scrollingElement.scrollLeft=0},scrollToEnd:function(){if(v())document.scrollingElement.scrollTop=document.body.scrollHeight,window.scrollTo(0,document.body.scrollHeight);else{var u=w()?-1:1;document.scrollingElement.scrollLeft=O(document.scrollingElement.scrollWidth*u)}},setCSSProperties:function(u){for(var t in u)T(t,u[t])},setProperty:T,removeProperty:I,getCurrentSelection:function(){var u=function(){var u=window.getSelection();if(u&&!u.isCollapsed){var t=u.toString();if(0!==t.trim().replace(/\n/g," ").replace(/\s\s+/g," ").length&&u.anchorNode&&u.focusNode){var e=1===u.rangeCount?u.getRangeAt(0):function(u,t,e,r){var n=new Range;if(n.setStart(u,t),n.setEnd(e,r),!n.collapsed)return n;at(">>> createOrderedRange COLLAPSED ... RANGE REVERSE?");var o=new Range;if(o.setStart(e,r),o.setEnd(u,t),!o.collapsed)return at(">>> createOrderedRange RANGE REVERSE OK."),n;at(">>> createOrderedRange RANGE REVERSE ALSO COLLAPSED?!")}(u.anchorNode,u.anchorOffset,u.focusNode,u.focusOffset);if(e&&!e.collapsed){var r=document.body.textContent,n=s.fromRange(e).relativeTo(document.body),o=n.start.offset,D=n.end.offset,i=r.slice(Math.max(0,o-200),o),a=i.search(/(?:[\0-@\[-`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u036F\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482-\u0489\u0530\u0557\u0558\u055A-\u055F\u0589-\u05CF\u05EB-\u05EE\u05F3-\u061F\u064B-\u066D\u0670\u06D4\u06D6-\u06E4\u06E7-\u06ED\u06F0-\u06F9\u06FD\u06FE\u0700-\u070F\u0711\u0730-\u074C\u07A6-\u07B0\u07B2-\u07C9\u07EB-\u07F3\u07F6-\u07F9\u07FB-\u07FF\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u083F\u0859-\u085F\u086B-\u086F\u0888\u088F-\u089F\u08CA-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962-\u0970\u0981-\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA-\u09BC\u09BE-\u09CD\u09CF-\u09DB\u09DE\u09E2-\u09EF\u09F2-\u09FB\u09FD-\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A-\u0A58\u0A5D\u0A5F-\u0A71\u0A75-\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA-\u0ABC\u0ABE-\u0ACF\u0AD1-\u0ADF\u0AE2-\u0AF8\u0AFA-\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A-\u0B3C\u0B3E-\u0B5B\u0B5E\u0B62-\u0B70\u0B72-\u0B82\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BCF\u0BD1-\u0C04\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C3E-\u0C57\u0C5B\u0C5C\u0C5E\u0C5F\u0C62-\u0C7F\u0C81-\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA-\u0CBC\u0CBE-\u0CDC\u0CDF\u0CE2-\u0CF0\u0CF3-\u0D03\u0D0D\u0D11\u0D3B\u0D3C\u0D3E-\u0D4D\u0D4F-\u0D53\u0D57-\u0D5E\u0D62-\u0D79\u0D80-\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0E00\u0E31\u0E34-\u0E3F\u0E47-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EB1\u0EB4-\u0EBC\u0EBE\u0EBF\u0EC5\u0EC7-\u0EDB\u0EE0-\u0EFF\u0F01-\u0F3F\u0F48\u0F6D-\u0F87\u0F8D-\u0FFF\u102B-\u103E\u1040-\u104F\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F-\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16F0\u16F9-\u16FF\u1712-\u171E\u1732-\u173F\u1752-\u175F\u176D\u1771-\u177F\u17B4-\u17D6\u17D8-\u17DB\u17DD-\u181F\u1879-\u187F\u1885\u1886\u18A9\u18AB-\u18AF\u18F6-\u18FF\u191F-\u194F\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19FF\u1A17-\u1A1F\u1A55-\u1AA6\u1AA8-\u1B04\u1B34-\u1B44\u1B4D-\u1B82\u1BA1-\u1BAD\u1BB0-\u1BB9\u1BE6-\u1BFF\u1C24-\u1C4C\u1C50-\u1C59\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CE8\u1CED\u1CF4\u1CF7-\u1CF9\u1CFB-\u1CFF\u1DC0-\u1DFF\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u2182\u2185-\u2BFF\u2CE5-\u2CEA\u2CEF-\u2CF1\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7F\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF-\u2E2E\u2E30-\u3004\u3007-\u3030\u3036-\u303A\u303D-\u3040\u3097-\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31C0-\u31EF\u3200-\u33FF\u4DC0-\u4DFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA620-\uA629\uA62C-\uA63F\uA66F-\uA67E\uA69E\uA69F\uA6E6-\uA716\uA720\uA721\uA789\uA78A\uA7CB-\uA7CF\uA7D2\uA7D4\uA7DA-\uA7F1\uA802\uA806\uA80B\uA823-\uA83F\uA874-\uA881\uA8B4-\uA8F1\uA8F8-\uA8FA\uA8FC\uA8FF-\uA909\uA926-\uA92F\uA947-\uA95F\uA97D-\uA983\uA9B3-\uA9CE\uA9D0-\uA9DF\uA9E5\uA9F0-\uA9F9\uA9FF\uAA29-\uAA3F\uAA43\uAA4C-\uAA5F\uAA77-\uAA79\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAC3-\uAADA\uAADE\uAADF\uAAEB-\uAAF1\uAAF5-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB6A-\uAB6F\uABE3-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB1E\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFE6F\uFE75\uFEFD-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEFF\uDF20-\uDF2C\uDF41\uDF4A-\uDF4F\uDF76-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0-\uDFFF]|\uD801[\uDC9E-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDD6F\uDD7B\uDD8B\uDD93\uDD96\uDDA2\uDDB2\uDDBA\uDDBD-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDF7F\uDF86\uDFB1\uDFBB-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE01-\uDE0F\uDE14\uDE18\uDE36-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE5-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD24-\uDE7F\uDEAA-\uDEAF\uDEB2-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF46-\uDF6F\uDF82-\uDFAF\uDFC5-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC00-\uDC02\uDC38-\uDC70\uDC73\uDC74\uDC76-\uDC82\uDCB0-\uDCCF\uDCE9-\uDD02\uDD27-\uDD43\uDD45\uDD46\uDD48-\uDD4F\uDD73-\uDD75\uDD77-\uDD82\uDDB3-\uDDC0\uDDC5-\uDDD9\uDDDB\uDDDD-\uDDFF\uDE12\uDE2C-\uDE3E\uDE41-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEDF-\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A-\uDF3C\uDF3E-\uDF4F\uDF51-\uDF5C\uDF62-\uDFFF]|\uD805[\uDC35-\uDC46\uDC4B-\uDC5E\uDC62-\uDC7F\uDCB0-\uDCC3\uDCC6\uDCC8-\uDD7F\uDDAF-\uDDD7\uDDDC-\uDDFF\uDE30-\uDE43\uDE45-\uDE7F\uDEAB-\uDEB7\uDEB9-\uDEFF\uDF1B-\uDF3F\uDF47-\uDFFF]|\uD806[\uDC2C-\uDC9F\uDCE0-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD30-\uDD3E\uDD40\uDD42-\uDD9F\uDDA8\uDDA9\uDDD1-\uDDE0\uDDE2\uDDE4-\uDDFF\uDE01-\uDE0A\uDE33-\uDE39\uDE3B-\uDE4F\uDE51-\uDE5B\uDE8A-\uDE9C\uDE9E-\uDEAF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC2F-\uDC3F\uDC41-\uDC71\uDC90-\uDCFF\uDD07\uDD0A\uDD31-\uDD45\uDD47-\uDD5F\uDD66\uDD69\uDD8A-\uDD97\uDD99-\uDEDF\uDEF3-\uDF01\uDF03\uDF11\uDF34-\uDFAF\uDFB1-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC00-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80E-\uD810\uD812-\uD819\uD824-\uD82A\uD82D\uD82E\uD830-\uD834\uD836\uD83C-\uD83F\uD87C\uD87D\uD87F\uD889-\uDBFF][\uDC00-\uDFFF]|\uD80B[\uDC00-\uDF8F\uDFF1-\uDFFF]|\uD80D[\uDC30-\uDC40\uDC47-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F-\uDE6F\uDEBF-\uDECF\uDEEE-\uDEFF\uDF30-\uDF3F\uDF44-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4F\uDF51-\uDF92\uDFA0-\uDFDF\uDFE2\uDFE4-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82B[\uDC00-\uDFEF\uDFF4\uDFFC\uDFFF]|\uD82C[\uDD23-\uDD31\uDD33-\uDD4F\uDD53\uDD54\uDD56-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC-\uDFFF]|\uD837[\uDC00-\uDEFF\uDF1F-\uDF24\uDF2B-\uDFFF]|\uD838[\uDC00-\uDC2F\uDC6E-\uDCFF\uDD2D-\uDD36\uDD3E-\uDD4D\uDD4F-\uDE8F\uDEAE-\uDEBF\uDEEC-\uDFFF]|\uD839[\uDC00-\uDCCF\uDCEC-\uDFDF\uDFE7\uDFEC\uDFEF\uDFFF]|\uD83A[\uDCC5-\uDCFF\uDD44-\uDD4A\uDD4C-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD869[\uDEE0-\uDEFF]|\uD86D[\uDF3A-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFEF]|\uD87B[\uDE5E-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDF4F]|\uD888[\uDFB0-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])(?:[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE3F\uDE40\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDF02\uDF04-\uDF10\uDF12-\uDF33\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883\uD885-\uD887][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F\uDC41-\uDC46]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD32\uDD50-\uDD52\uDD55\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E\uDF25-\uDF2A]|\uD838[\uDC30-\uDC6D\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDCD0-\uDCEB\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF39\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0\uDFF0-\uDFFF]|\uD87B[\uDC00-\uDE5D]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A\uDF50-\uDFFF]|\uD888[\uDC00-\uDFAF])/g);-1!==a&&(i=i.slice(a+1));var F=r.slice(D,Math.min(r.length,D+200)),c=Array.from(F.matchAll(/(?:[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE3F\uDE40\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDF02\uDF04-\uDF10\uDF12-\uDF33\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883\uD885-\uD887][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F\uDC41-\uDC46]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD32\uDD50-\uDD52\uDD55\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E\uDF25-\uDF2A]|\uD838[\uDC30-\uDC6D\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDCD0-\uDCEB\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF39\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0\uDFF0-\uDFFF]|\uD87B[\uDC00-\uDE5D]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A\uDF50-\uDFFF]|\uD888[\uDC00-\uDFAF])(?:[\0-@\[-`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u036F\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482-\u0489\u0530\u0557\u0558\u055A-\u055F\u0589-\u05CF\u05EB-\u05EE\u05F3-\u061F\u064B-\u066D\u0670\u06D4\u06D6-\u06E4\u06E7-\u06ED\u06F0-\u06F9\u06FD\u06FE\u0700-\u070F\u0711\u0730-\u074C\u07A6-\u07B0\u07B2-\u07C9\u07EB-\u07F3\u07F6-\u07F9\u07FB-\u07FF\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u083F\u0859-\u085F\u086B-\u086F\u0888\u088F-\u089F\u08CA-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962-\u0970\u0981-\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA-\u09BC\u09BE-\u09CD\u09CF-\u09DB\u09DE\u09E2-\u09EF\u09F2-\u09FB\u09FD-\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A-\u0A58\u0A5D\u0A5F-\u0A71\u0A75-\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA-\u0ABC\u0ABE-\u0ACF\u0AD1-\u0ADF\u0AE2-\u0AF8\u0AFA-\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A-\u0B3C\u0B3E-\u0B5B\u0B5E\u0B62-\u0B70\u0B72-\u0B82\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BCF\u0BD1-\u0C04\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C3E-\u0C57\u0C5B\u0C5C\u0C5E\u0C5F\u0C62-\u0C7F\u0C81-\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA-\u0CBC\u0CBE-\u0CDC\u0CDF\u0CE2-\u0CF0\u0CF3-\u0D03\u0D0D\u0D11\u0D3B\u0D3C\u0D3E-\u0D4D\u0D4F-\u0D53\u0D57-\u0D5E\u0D62-\u0D79\u0D80-\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0E00\u0E31\u0E34-\u0E3F\u0E47-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EB1\u0EB4-\u0EBC\u0EBE\u0EBF\u0EC5\u0EC7-\u0EDB\u0EE0-\u0EFF\u0F01-\u0F3F\u0F48\u0F6D-\u0F87\u0F8D-\u0FFF\u102B-\u103E\u1040-\u104F\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F-\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16F0\u16F9-\u16FF\u1712-\u171E\u1732-\u173F\u1752-\u175F\u176D\u1771-\u177F\u17B4-\u17D6\u17D8-\u17DB\u17DD-\u181F\u1879-\u187F\u1885\u1886\u18A9\u18AB-\u18AF\u18F6-\u18FF\u191F-\u194F\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19FF\u1A17-\u1A1F\u1A55-\u1AA6\u1AA8-\u1B04\u1B34-\u1B44\u1B4D-\u1B82\u1BA1-\u1BAD\u1BB0-\u1BB9\u1BE6-\u1BFF\u1C24-\u1C4C\u1C50-\u1C59\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CE8\u1CED\u1CF4\u1CF7-\u1CF9\u1CFB-\u1CFF\u1DC0-\u1DFF\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u2182\u2185-\u2BFF\u2CE5-\u2CEA\u2CEF-\u2CF1\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7F\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF-\u2E2E\u2E30-\u3004\u3007-\u3030\u3036-\u303A\u303D-\u3040\u3097-\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31C0-\u31EF\u3200-\u33FF\u4DC0-\u4DFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA620-\uA629\uA62C-\uA63F\uA66F-\uA67E\uA69E\uA69F\uA6E6-\uA716\uA720\uA721\uA789\uA78A\uA7CB-\uA7CF\uA7D2\uA7D4\uA7DA-\uA7F1\uA802\uA806\uA80B\uA823-\uA83F\uA874-\uA881\uA8B4-\uA8F1\uA8F8-\uA8FA\uA8FC\uA8FF-\uA909\uA926-\uA92F\uA947-\uA95F\uA97D-\uA983\uA9B3-\uA9CE\uA9D0-\uA9DF\uA9E5\uA9F0-\uA9F9\uA9FF\uAA29-\uAA3F\uAA43\uAA4C-\uAA5F\uAA77-\uAA79\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAC3-\uAADA\uAADE\uAADF\uAAEB-\uAAF1\uAAF5-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB6A-\uAB6F\uABE3-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB1E\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFE6F\uFE75\uFEFD-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEFF\uDF20-\uDF2C\uDF41\uDF4A-\uDF4F\uDF76-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0-\uDFFF]|\uD801[\uDC9E-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDD6F\uDD7B\uDD8B\uDD93\uDD96\uDDA2\uDDB2\uDDBA\uDDBD-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDF7F\uDF86\uDFB1\uDFBB-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE01-\uDE0F\uDE14\uDE18\uDE36-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE5-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD24-\uDE7F\uDEAA-\uDEAF\uDEB2-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF46-\uDF6F\uDF82-\uDFAF\uDFC5-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC00-\uDC02\uDC38-\uDC70\uDC73\uDC74\uDC76-\uDC82\uDCB0-\uDCCF\uDCE9-\uDD02\uDD27-\uDD43\uDD45\uDD46\uDD48-\uDD4F\uDD73-\uDD75\uDD77-\uDD82\uDDB3-\uDDC0\uDDC5-\uDDD9\uDDDB\uDDDD-\uDDFF\uDE12\uDE2C-\uDE3E\uDE41-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEDF-\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A-\uDF3C\uDF3E-\uDF4F\uDF51-\uDF5C\uDF62-\uDFFF]|\uD805[\uDC35-\uDC46\uDC4B-\uDC5E\uDC62-\uDC7F\uDCB0-\uDCC3\uDCC6\uDCC8-\uDD7F\uDDAF-\uDDD7\uDDDC-\uDDFF\uDE30-\uDE43\uDE45-\uDE7F\uDEAB-\uDEB7\uDEB9-\uDEFF\uDF1B-\uDF3F\uDF47-\uDFFF]|\uD806[\uDC2C-\uDC9F\uDCE0-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD30-\uDD3E\uDD40\uDD42-\uDD9F\uDDA8\uDDA9\uDDD1-\uDDE0\uDDE2\uDDE4-\uDDFF\uDE01-\uDE0A\uDE33-\uDE39\uDE3B-\uDE4F\uDE51-\uDE5B\uDE8A-\uDE9C\uDE9E-\uDEAF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC2F-\uDC3F\uDC41-\uDC71\uDC90-\uDCFF\uDD07\uDD0A\uDD31-\uDD45\uDD47-\uDD5F\uDD66\uDD69\uDD8A-\uDD97\uDD99-\uDEDF\uDEF3-\uDF01\uDF03\uDF11\uDF34-\uDFAF\uDFB1-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC00-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80E-\uD810\uD812-\uD819\uD824-\uD82A\uD82D\uD82E\uD830-\uD834\uD836\uD83C-\uD83F\uD87C\uD87D\uD87F\uD889-\uDBFF][\uDC00-\uDFFF]|\uD80B[\uDC00-\uDF8F\uDFF1-\uDFFF]|\uD80D[\uDC30-\uDC40\uDC47-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F-\uDE6F\uDEBF-\uDECF\uDEEE-\uDEFF\uDF30-\uDF3F\uDF44-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4F\uDF51-\uDF92\uDFA0-\uDFDF\uDFE2\uDFE4-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82B[\uDC00-\uDFEF\uDFF4\uDFFC\uDFFF]|\uD82C[\uDD23-\uDD31\uDD33-\uDD4F\uDD53\uDD54\uDD56-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC-\uDFFF]|\uD837[\uDC00-\uDEFF\uDF1F-\uDF24\uDF2B-\uDFFF]|\uD838[\uDC00-\uDC2F\uDC6E-\uDCFF\uDD2D-\uDD36\uDD3E-\uDD4D\uDD4F-\uDE8F\uDEAE-\uDEBF\uDEEC-\uDFFF]|\uD839[\uDC00-\uDCCF\uDCEC-\uDFDF\uDFE7\uDFEC\uDFEF\uDFFF]|\uD83A[\uDCC5-\uDCFF\uDD44-\uDD4A\uDD4C-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD869[\uDEE0-\uDEFF]|\uD86D[\uDF3A-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFEF]|\uD87B[\uDE5E-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDF4F]|\uD888[\uDFB0-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g)).pop();return void 0!==c&&c.index>1&&(F=F.slice(0,c.index+1)),{highlight:t,before:i,after:F}}at("$$$$$$$$$$$$$$$$$ CANNOT GET NON-COLLAPSED SELECTION RANGE?!")}}}();return u?{text:u,rect:function(){try{var u=window.getSelection();if(!u)return;return L(u.getRangeAt(0).getBoundingClientRect())}catch(u){return N(u),null}}()}:null},registerDecorationTemplates:function(u){for(var t="",e=0,r=Object.entries(u);e<r.length;e++){var n=K(r[e],2),o=n[0],D=n[1];eu.set(o,D),D.stylesheet&&(t+=D.stylesheet+"\n")}if(t){var i=document.createElement("style");i.innerHTML=t,document.getElementsByTagName("head")[0].appendChild(i)}},getDecorations:function(u){var t=ru.get(u);return t||(t=function(u,t){var e=[],r=0,n=null;function o(t){var n=u+"-"+r++,o=P(t.locator);if(o){var D={id:n,decoration:t,range:o};e.push(D),i(D)}else R("Can't locate DOM range for decoration",t)}function D(u){var t=e.findIndex((function(t){return t.decoration.id===u}));if(-1!==t){var r=e[t];e.splice(t,1),r.clickableElements=null,r.container&&(r.container.remove(),r.container=null)}}function i(e){var r=(n||((n=document.createElement("div")).setAttribute("id",u),n.setAttribute("data-group",t),n.style.setProperty("pointer-events","none"),document.body.append(n)),n),o=eu.get(e.decoration.style);if(o){var D=document.createElement("div");D.setAttribute("id",e.id),D.setAttribute("data-style",e.decoration.style),D.style.setProperty("pointer-events","none");var i,a=window.innerWidth,F=parseInt(getComputedStyle(document.documentElement).getPropertyValue("column-count")),c=a/(F||1),l=document.scrollingElement,f=l.scrollLeft,s=l.scrollTop,A=e.range.getBoundingClientRect();try{var p=document.createElement("template");p.innerHTML=e.decoration.element.trim(),i=p.content.firstElementChild}catch(u){return void N('Invalid decoration element "'.concat(e.decoration.element,'": ').concat(u.message))}if("boxes"===o.layout){var E,C=U(e.range,!0),y=J(C=C.sort((function(u,t){return u.top<t.top?-1:u.top>t.top?1:0})));try{for(y.s();!(E=y.n()).done;){var d=E.value,B=i.cloneNode(!0);B.style.setProperty("pointer-events","none"),g(B,d,A),D.append(B)}}catch(u){y.e(u)}finally{y.f()}}else if("bounds"===o.layout){var h=i.cloneNode(!0);h.style.setProperty("pointer-events","none"),g(h,A,A),D.append(h)}r.append(D),e.container=D,e.clickableElements=Array.from(D.querySelectorAll("[data-activable='1']")),0===e.clickableElements.length&&(e.clickableElements=Array.from(D.children))}else N("Unknown decoration style: ".concat(e.decoration.style));function g(u,t,e){if(u.style.position="absolute","wrap"===o.width)u.style.width="".concat(t.width,"px"),u.style.height="".concat(t.height,"px"),u.style.left="".concat(t.left+f,"px"),u.style.top="".concat(t.top+s,"px");else if("viewport"===o.width){u.style.width="".concat(a,"px"),u.style.height="".concat(t.height,"px");var r=Math.floor(t.left/a)*a;u.style.left="".concat(r+f,"px"),u.style.top="".concat(t.top+s,"px")}else if("bounds"===o.width)u.style.width="".concat(e.width,"px"),u.style.height="".concat(t.height,"px"),u.style.left="".concat(e.left+f,"px"),u.style.top="".concat(t.top+s,"px");else if("page"===o.width){u.style.width="".concat(c,"px"),u.style.height="".concat(t.height,"px");var n=Math.floor(t.left/c)*c;u.style.left="".concat(n+f,"px"),u.style.top="".concat(t.top+s,"px")}}}function a(){n&&(n.remove(),n=null)}return{add:o,remove:D,update:function(u){D(u.id),o(u)},clear:function(){a(),e.length=0},items:e,requestLayout:function(){a(),e.forEach((function(u){return i(u)}))}}}("r2-decoration-"+nu++,u),ru.set(u,t)),t},findFirstVisibleLocator:function(){var u=Qu(document.body);return{href:"#",type:"application/xhtml+xml",locations:{cssSelector:Ku(u)},text:{highlight:u.textContent}}}},window.readium.isFixedLayout=!0}()}(); +//# sourceMappingURL=readium-fixed.js.map \ No newline at end of file diff --git a/readium/navigator/src/main/assets/readium/scripts/readium-reflowable.js b/readium/navigator/src/main/assets/readium/scripts/readium-reflowable.js index 589c440581..ccc53dea23 100644 --- a/readium/navigator/src/main/assets/readium/scripts/readium-reflowable.js +++ b/readium/navigator/src/main/assets/readium/scripts/readium-reflowable.js @@ -1 +1,2 @@ -(function(){var __webpack_modules__={3089:function(__unused_webpack_module,exports){"use strict";eval('var __webpack_unused_export__;\n\n/**\n * Implementation of Myers\' online approximate string matching algorithm [1],\n * with additional optimizations suggested by [2].\n *\n * This has O((k/w) * n) complexity where `n` is the length of the text, `k` is\n * the maximum number of errors allowed (always <= the pattern length) and `w`\n * is the word size. Because JS only supports bitwise operations on 32 bit\n * integers, `w` is 32.\n *\n * As far as I am aware, there aren\'t any online algorithms which are\n * significantly better for a wide range of input parameters. The problem can be\n * solved faster using "filter then verify" approaches which first filter out\n * regions of the text that cannot match using a "cheap" check and then verify\n * the remaining potential matches. The verify step requires an algorithm such\n * as this one however.\n *\n * The algorithm\'s approach is essentially to optimize the classic dynamic\n * programming solution to the problem by computing columns of the matrix in\n * word-sized chunks (ie. dealing with 32 chars of the pattern at a time) and\n * avoiding calculating regions of the matrix where the minimum error count is\n * guaranteed to exceed the input threshold.\n *\n * The paper consists of two parts, the first describes the core algorithm for\n * matching patterns <= the size of a word (implemented by `advanceBlock` here).\n * The second uses the core algorithm as part of a larger block-based algorithm\n * to handle longer patterns.\n *\n * [1] G. Myers, “A Fast Bit-Vector Algorithm for Approximate String Matching\n * Based on Dynamic Programming,” vol. 46, no. 3, pp. 395–415, 1999.\n *\n * [2] Šošić, M. (2014). An simd dynamic programming c/c++ library (Doctoral\n * dissertation, Fakultet Elektrotehnike i računarstva, Sveučilište u Zagrebu).\n */\n__webpack_unused_export__ = ({ value: true });\nfunction reverse(s) {\n return s\n .split("")\n .reverse()\n .join("");\n}\n/**\n * Given the ends of approximate matches for `pattern` in `text`, find\n * the start of the matches.\n *\n * @param findEndFn - Function for finding the end of matches in\n * text.\n * @return Matches with the `start` property set.\n */\nfunction findMatchStarts(text, pattern, matches) {\n var patRev = reverse(pattern);\n return matches.map(function (m) {\n // Find start of each match by reversing the pattern and matching segment\n // of text and searching for an approx match with the same number of\n // errors.\n var minStart = Math.max(0, m.end - pattern.length - m.errors);\n var textRev = reverse(text.slice(minStart, m.end));\n // If there are multiple possible start points, choose the one that\n // maximizes the length of the match.\n var start = findMatchEnds(textRev, patRev, m.errors).reduce(function (min, rm) {\n if (m.end - rm.end < min) {\n return m.end - rm.end;\n }\n return min;\n }, m.end);\n return {\n start: start,\n end: m.end,\n errors: m.errors\n };\n });\n}\n/**\n * Return 1 if a number is non-zero or zero otherwise, without using\n * conditional operators.\n *\n * This should get inlined into `advanceBlock` below by the JIT.\n *\n * Adapted from https://stackoverflow.com/a/3912218/434243\n */\nfunction oneIfNotZero(n) {\n return ((n | -n) >> 31) & 1;\n}\n/**\n * Block calculation step of the algorithm.\n *\n * From Fig 8. on p. 408 of [1], additionally optimized to replace conditional\n * checks with bitwise operations as per Section 4.2.3 of [2].\n *\n * @param ctx - The pattern context object\n * @param peq - The `peq` array for the current character (`ctx.peq.get(ch)`)\n * @param b - The block level\n * @param hIn - Horizontal input delta ∈ {1,0,-1}\n * @return Horizontal output delta ∈ {1,0,-1}\n */\nfunction advanceBlock(ctx, peq, b, hIn) {\n var pV = ctx.P[b];\n var mV = ctx.M[b];\n var hInIsNegative = hIn >>> 31; // 1 if hIn < 0 or 0 otherwise.\n var eq = peq[b] | hInIsNegative;\n // Step 1: Compute horizontal deltas.\n var xV = eq | mV;\n var xH = (((eq & pV) + pV) ^ pV) | eq;\n var pH = mV | ~(xH | pV);\n var mH = pV & xH;\n // Step 2: Update score (value of last row of this block).\n var hOut = oneIfNotZero(pH & ctx.lastRowMask[b]) -\n oneIfNotZero(mH & ctx.lastRowMask[b]);\n // Step 3: Update vertical deltas for use when processing next char.\n pH <<= 1;\n mH <<= 1;\n mH |= hInIsNegative;\n pH |= oneIfNotZero(hIn) - hInIsNegative; // set pH[0] if hIn > 0\n pV = mH | ~(xV | pH);\n mV = pH & xV;\n ctx.P[b] = pV;\n ctx.M[b] = mV;\n return hOut;\n}\n/**\n * Find the ends and error counts for matches of `pattern` in `text`.\n *\n * Only the matches with the lowest error count are reported. Other matches\n * with error counts <= maxErrors are discarded.\n *\n * This is the block-based search algorithm from Fig. 9 on p.410 of [1].\n */\nfunction findMatchEnds(text, pattern, maxErrors) {\n if (pattern.length === 0) {\n return [];\n }\n // Clamp error count so we can rely on the `maxErrors` and `pattern.length`\n // rows being in the same block below.\n maxErrors = Math.min(maxErrors, pattern.length);\n var matches = [];\n // Word size.\n var w = 32;\n // Index of maximum block level.\n var bMax = Math.ceil(pattern.length / w) - 1;\n // Context used across block calculations.\n var ctx = {\n P: new Uint32Array(bMax + 1),\n M: new Uint32Array(bMax + 1),\n lastRowMask: new Uint32Array(bMax + 1)\n };\n ctx.lastRowMask.fill(1 << 31);\n ctx.lastRowMask[bMax] = 1 << (pattern.length - 1) % w;\n // Dummy "peq" array for chars in the text which do not occur in the pattern.\n var emptyPeq = new Uint32Array(bMax + 1);\n // Map of UTF-16 character code to bit vector indicating positions in the\n // pattern that equal that character.\n var peq = new Map();\n // Version of `peq` that only stores mappings for small characters. This\n // allows faster lookups when iterating through the text because a simple\n // array lookup can be done instead of a hash table lookup.\n var asciiPeq = [];\n for (var i = 0; i < 256; i++) {\n asciiPeq.push(emptyPeq);\n }\n // Calculate `ctx.peq` - a map of character values to bitmasks indicating\n // positions of that character within the pattern, where each bit represents\n // a position in the pattern.\n for (var c = 0; c < pattern.length; c += 1) {\n var val = pattern.charCodeAt(c);\n if (peq.has(val)) {\n // Duplicate char in pattern.\n continue;\n }\n var charPeq = new Uint32Array(bMax + 1);\n peq.set(val, charPeq);\n if (val < asciiPeq.length) {\n asciiPeq[val] = charPeq;\n }\n for (var b = 0; b <= bMax; b += 1) {\n charPeq[b] = 0;\n // Set all the bits where the pattern matches the current char (ch).\n // For indexes beyond the end of the pattern, always set the bit as if the\n // pattern contained a wildcard char in that position.\n for (var r = 0; r < w; r += 1) {\n var idx = b * w + r;\n if (idx >= pattern.length) {\n continue;\n }\n var match = pattern.charCodeAt(idx) === val;\n if (match) {\n charPeq[b] |= 1 << r;\n }\n }\n }\n }\n // Index of last-active block level in the column.\n var y = Math.max(0, Math.ceil(maxErrors / w) - 1);\n // Initialize maximum error count at bottom of each block.\n var score = new Uint32Array(bMax + 1);\n for (var b = 0; b <= y; b += 1) {\n score[b] = (b + 1) * w;\n }\n score[bMax] = pattern.length;\n // Initialize vertical deltas for each block.\n for (var b = 0; b <= y; b += 1) {\n ctx.P[b] = ~0;\n ctx.M[b] = 0;\n }\n // Process each char of the text, computing the error count for `w` chars of\n // the pattern at a time.\n for (var j = 0; j < text.length; j += 1) {\n // Lookup the bitmask representing the positions of the current char from\n // the text within the pattern.\n var charCode = text.charCodeAt(j);\n var charPeq = void 0;\n if (charCode < asciiPeq.length) {\n // Fast array lookup.\n charPeq = asciiPeq[charCode];\n }\n else {\n // Slower hash table lookup.\n charPeq = peq.get(charCode);\n if (typeof charPeq === "undefined") {\n charPeq = emptyPeq;\n }\n }\n // Calculate error count for blocks that we definitely have to process for\n // this column.\n var carry = 0;\n for (var b = 0; b <= y; b += 1) {\n carry = advanceBlock(ctx, charPeq, b, carry);\n score[b] += carry;\n }\n // Check if we also need to compute an additional block, or if we can reduce\n // the number of blocks processed for the next column.\n if (score[y] - carry <= maxErrors &&\n y < bMax &&\n (charPeq[y + 1] & 1 || carry < 0)) {\n // Error count for bottom block is under threshold, increase the number of\n // blocks processed for this column & next by 1.\n y += 1;\n ctx.P[y] = ~0;\n ctx.M[y] = 0;\n var maxBlockScore = y === bMax ? pattern.length % w : w;\n score[y] =\n score[y - 1] +\n maxBlockScore -\n carry +\n advanceBlock(ctx, charPeq, y, carry);\n }\n else {\n // Error count for bottom block exceeds threshold, reduce the number of\n // blocks processed for the next column.\n while (y > 0 && score[y] >= maxErrors + w) {\n y -= 1;\n }\n }\n // If error count is under threshold, report a match.\n if (y === bMax && score[y] <= maxErrors) {\n if (score[y] < maxErrors) {\n // Discard any earlier, worse matches.\n matches.splice(0, matches.length);\n }\n matches.push({\n start: -1,\n end: j + 1,\n errors: score[y]\n });\n // Because `search` only reports the matches with the lowest error count,\n // we can "ratchet down" the max error threshold whenever a match is\n // encountered and thereby save a small amount of work for the remainder\n // of the text.\n maxErrors = score[y];\n }\n }\n return matches;\n}\n/**\n * Search for matches for `pattern` in `text` allowing up to `maxErrors` errors.\n *\n * Returns the start, and end positions and error counts for each lowest-cost\n * match. Only the "best" matches are returned.\n */\nfunction search(text, pattern, maxErrors) {\n var matches = findMatchEnds(text, pattern, maxErrors);\n return findMatchStarts(text, pattern, matches);\n}\nexports.Z = search;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzA4OS5qcyIsIm1hcHBpbmdzIjoiO0FBQWE7QUFDYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSw2QkFBNkMsRUFBRSxhQUFhLENBQUM7QUFDN0Q7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLFNBQVM7QUFDVDtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsS0FBSztBQUNMO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLDBDQUEwQztBQUMxQyxzQ0FBc0M7QUFDdEM7QUFDQTtBQUNBO0FBQ0E7QUFDQSxvQ0FBb0M7QUFDcEM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSw2Q0FBNkM7QUFDN0M7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxvQkFBb0IsU0FBUztBQUM3QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esb0JBQW9CLG9CQUFvQjtBQUN4QztBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLHdCQUF3QixXQUFXO0FBQ25DO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsNEJBQTRCLE9BQU87QUFDbkM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esb0JBQW9CLFFBQVE7QUFDNUI7QUFDQTtBQUNBO0FBQ0E7QUFDQSxvQkFBb0IsUUFBUTtBQUM1QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esb0JBQW9CLGlCQUFpQjtBQUNyQztBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSx3QkFBd0IsUUFBUTtBQUNoQztBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLGFBQWE7QUFDYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLFNBQWUiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2FwcHJveC1zdHJpbmctbWF0Y2gvZGlzdC9pbmRleC5qcz83MjMwIl0sInNvdXJjZXNDb250ZW50IjpbIlwidXNlIHN0cmljdFwiO1xuLyoqXG4gKiBJbXBsZW1lbnRhdGlvbiBvZiBNeWVycycgb25saW5lIGFwcHJveGltYXRlIHN0cmluZyBtYXRjaGluZyBhbGdvcml0aG0gWzFdLFxuICogd2l0aCBhZGRpdGlvbmFsIG9wdGltaXphdGlvbnMgc3VnZ2VzdGVkIGJ5IFsyXS5cbiAqXG4gKiBUaGlzIGhhcyBPKChrL3cpICogbikgY29tcGxleGl0eSB3aGVyZSBgbmAgaXMgdGhlIGxlbmd0aCBvZiB0aGUgdGV4dCwgYGtgIGlzXG4gKiB0aGUgbWF4aW11bSBudW1iZXIgb2YgZXJyb3JzIGFsbG93ZWQgKGFsd2F5cyA8PSB0aGUgcGF0dGVybiBsZW5ndGgpIGFuZCBgd2BcbiAqIGlzIHRoZSB3b3JkIHNpemUuIEJlY2F1c2UgSlMgb25seSBzdXBwb3J0cyBiaXR3aXNlIG9wZXJhdGlvbnMgb24gMzIgYml0XG4gKiBpbnRlZ2VycywgYHdgIGlzIDMyLlxuICpcbiAqIEFzIGZhciBhcyBJIGFtIGF3YXJlLCB0aGVyZSBhcmVuJ3QgYW55IG9ubGluZSBhbGdvcml0aG1zIHdoaWNoIGFyZVxuICogc2lnbmlmaWNhbnRseSBiZXR0ZXIgZm9yIGEgd2lkZSByYW5nZSBvZiBpbnB1dCBwYXJhbWV0ZXJzLiBUaGUgcHJvYmxlbSBjYW4gYmVcbiAqIHNvbHZlZCBmYXN0ZXIgdXNpbmcgXCJmaWx0ZXIgdGhlbiB2ZXJpZnlcIiBhcHByb2FjaGVzIHdoaWNoIGZpcnN0IGZpbHRlciBvdXRcbiAqIHJlZ2lvbnMgb2YgdGhlIHRleHQgdGhhdCBjYW5ub3QgbWF0Y2ggdXNpbmcgYSBcImNoZWFwXCIgY2hlY2sgYW5kIHRoZW4gdmVyaWZ5XG4gKiB0aGUgcmVtYWluaW5nIHBvdGVudGlhbCBtYXRjaGVzLiBUaGUgdmVyaWZ5IHN0ZXAgcmVxdWlyZXMgYW4gYWxnb3JpdGhtIHN1Y2hcbiAqIGFzIHRoaXMgb25lIGhvd2V2ZXIuXG4gKlxuICogVGhlIGFsZ29yaXRobSdzIGFwcHJvYWNoIGlzIGVzc2VudGlhbGx5IHRvIG9wdGltaXplIHRoZSBjbGFzc2ljIGR5bmFtaWNcbiAqIHByb2dyYW1taW5nIHNvbHV0aW9uIHRvIHRoZSBwcm9ibGVtIGJ5IGNvbXB1dGluZyBjb2x1bW5zIG9mIHRoZSBtYXRyaXggaW5cbiAqIHdvcmQtc2l6ZWQgY2h1bmtzIChpZS4gZGVhbGluZyB3aXRoIDMyIGNoYXJzIG9mIHRoZSBwYXR0ZXJuIGF0IGEgdGltZSkgYW5kXG4gKiBhdm9pZGluZyBjYWxjdWxhdGluZyByZWdpb25zIG9mIHRoZSBtYXRyaXggd2hlcmUgdGhlIG1pbmltdW0gZXJyb3IgY291bnQgaXNcbiAqIGd1YXJhbnRlZWQgdG8gZXhjZWVkIHRoZSBpbnB1dCB0aHJlc2hvbGQuXG4gKlxuICogVGhlIHBhcGVyIGNvbnNpc3RzIG9mIHR3byBwYXJ0cywgdGhlIGZpcnN0IGRlc2NyaWJlcyB0aGUgY29yZSBhbGdvcml0aG0gZm9yXG4gKiBtYXRjaGluZyBwYXR0ZXJucyA8PSB0aGUgc2l6ZSBvZiBhIHdvcmQgKGltcGxlbWVudGVkIGJ5IGBhZHZhbmNlQmxvY2tgIGhlcmUpLlxuICogVGhlIHNlY29uZCB1c2VzIHRoZSBjb3JlIGFsZ29yaXRobSBhcyBwYXJ0IG9mIGEgbGFyZ2VyIGJsb2NrLWJhc2VkIGFsZ29yaXRobVxuICogdG8gaGFuZGxlIGxvbmdlciBwYXR0ZXJucy5cbiAqXG4gKiBbMV0gRy4gTXllcnMsIOKAnEEgRmFzdCBCaXQtVmVjdG9yIEFsZ29yaXRobSBmb3IgQXBwcm94aW1hdGUgU3RyaW5nIE1hdGNoaW5nXG4gKiBCYXNlZCBvbiBEeW5hbWljIFByb2dyYW1taW5nLOKAnSB2b2wuIDQ2LCBuby4gMywgcHAuIDM5NeKAkzQxNSwgMTk5OS5cbiAqXG4gKiBbMl0gxaBvxaFpxIcsIE0uICgyMDE0KS4gQW4gc2ltZCBkeW5hbWljIHByb2dyYW1taW5nIGMvYysrIGxpYnJhcnkgKERvY3RvcmFsXG4gKiBkaXNzZXJ0YXRpb24sIEZha3VsdGV0IEVsZWt0cm90ZWhuaWtlIGkgcmHEjXVuYXJzdHZhLCBTdmV1xI1pbGnFoXRlIHUgWmFncmVidSkuXG4gKi9cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCBcIl9fZXNNb2R1bGVcIiwgeyB2YWx1ZTogdHJ1ZSB9KTtcbmZ1bmN0aW9uIHJldmVyc2Uocykge1xuICAgIHJldHVybiBzXG4gICAgICAgIC5zcGxpdChcIlwiKVxuICAgICAgICAucmV2ZXJzZSgpXG4gICAgICAgIC5qb2luKFwiXCIpO1xufVxuLyoqXG4gKiBHaXZlbiB0aGUgZW5kcyBvZiBhcHByb3hpbWF0ZSBtYXRjaGVzIGZvciBgcGF0dGVybmAgaW4gYHRleHRgLCBmaW5kXG4gKiB0aGUgc3RhcnQgb2YgdGhlIG1hdGNoZXMuXG4gKlxuICogQHBhcmFtIGZpbmRFbmRGbiAtIEZ1bmN0aW9uIGZvciBmaW5kaW5nIHRoZSBlbmQgb2YgbWF0Y2hlcyBpblxuICogdGV4dC5cbiAqIEByZXR1cm4gTWF0Y2hlcyB3aXRoIHRoZSBgc3RhcnRgIHByb3BlcnR5IHNldC5cbiAqL1xuZnVuY3Rpb24gZmluZE1hdGNoU3RhcnRzKHRleHQsIHBhdHRlcm4sIG1hdGNoZXMpIHtcbiAgICB2YXIgcGF0UmV2ID0gcmV2ZXJzZShwYXR0ZXJuKTtcbiAgICByZXR1cm4gbWF0Y2hlcy5tYXAoZnVuY3Rpb24gKG0pIHtcbiAgICAgICAgLy8gRmluZCBzdGFydCBvZiBlYWNoIG1hdGNoIGJ5IHJldmVyc2luZyB0aGUgcGF0dGVybiBhbmQgbWF0Y2hpbmcgc2VnbWVudFxuICAgICAgICAvLyBvZiB0ZXh0IGFuZCBzZWFyY2hpbmcgZm9yIGFuIGFwcHJveCBtYXRjaCB3aXRoIHRoZSBzYW1lIG51bWJlciBvZlxuICAgICAgICAvLyBlcnJvcnMuXG4gICAgICAgIHZhciBtaW5TdGFydCA9IE1hdGgubWF4KDAsIG0uZW5kIC0gcGF0dGVybi5sZW5ndGggLSBtLmVycm9ycyk7XG4gICAgICAgIHZhciB0ZXh0UmV2ID0gcmV2ZXJzZSh0ZXh0LnNsaWNlKG1pblN0YXJ0LCBtLmVuZCkpO1xuICAgICAgICAvLyBJZiB0aGVyZSBhcmUgbXVsdGlwbGUgcG9zc2libGUgc3RhcnQgcG9pbnRzLCBjaG9vc2UgdGhlIG9uZSB0aGF0XG4gICAgICAgIC8vIG1heGltaXplcyB0aGUgbGVuZ3RoIG9mIHRoZSBtYXRjaC5cbiAgICAgICAgdmFyIHN0YXJ0ID0gZmluZE1hdGNoRW5kcyh0ZXh0UmV2LCBwYXRSZXYsIG0uZXJyb3JzKS5yZWR1Y2UoZnVuY3Rpb24gKG1pbiwgcm0pIHtcbiAgICAgICAgICAgIGlmIChtLmVuZCAtIHJtLmVuZCA8IG1pbikge1xuICAgICAgICAgICAgICAgIHJldHVybiBtLmVuZCAtIHJtLmVuZDtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIHJldHVybiBtaW47XG4gICAgICAgIH0sIG0uZW5kKTtcbiAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICAgIHN0YXJ0OiBzdGFydCxcbiAgICAgICAgICAgIGVuZDogbS5lbmQsXG4gICAgICAgICAgICBlcnJvcnM6IG0uZXJyb3JzXG4gICAgICAgIH07XG4gICAgfSk7XG59XG4vKipcbiAqIFJldHVybiAxIGlmIGEgbnVtYmVyIGlzIG5vbi16ZXJvIG9yIHplcm8gb3RoZXJ3aXNlLCB3aXRob3V0IHVzaW5nXG4gKiBjb25kaXRpb25hbCBvcGVyYXRvcnMuXG4gKlxuICogVGhpcyBzaG91bGQgZ2V0IGlubGluZWQgaW50byBgYWR2YW5jZUJsb2NrYCBiZWxvdyBieSB0aGUgSklULlxuICpcbiAqIEFkYXB0ZWQgZnJvbSBodHRwczovL3N0YWNrb3ZlcmZsb3cuY29tL2EvMzkxMjIxOC80MzQyNDNcbiAqL1xuZnVuY3Rpb24gb25lSWZOb3RaZXJvKG4pIHtcbiAgICByZXR1cm4gKChuIHwgLW4pID4+IDMxKSAmIDE7XG59XG4vKipcbiAqIEJsb2NrIGNhbGN1bGF0aW9uIHN0ZXAgb2YgdGhlIGFsZ29yaXRobS5cbiAqXG4gKiBGcm9tIEZpZyA4LiBvbiBwLiA0MDggb2YgWzFdLCBhZGRpdGlvbmFsbHkgb3B0aW1pemVkIHRvIHJlcGxhY2UgY29uZGl0aW9uYWxcbiAqIGNoZWNrcyB3aXRoIGJpdHdpc2Ugb3BlcmF0aW9ucyBhcyBwZXIgU2VjdGlvbiA0LjIuMyBvZiBbMl0uXG4gKlxuICogQHBhcmFtIGN0eCAtIFRoZSBwYXR0ZXJuIGNvbnRleHQgb2JqZWN0XG4gKiBAcGFyYW0gcGVxIC0gVGhlIGBwZXFgIGFycmF5IGZvciB0aGUgY3VycmVudCBjaGFyYWN0ZXIgKGBjdHgucGVxLmdldChjaClgKVxuICogQHBhcmFtIGIgLSBUaGUgYmxvY2sgbGV2ZWxcbiAqIEBwYXJhbSBoSW4gLSBIb3Jpem9udGFsIGlucHV0IGRlbHRhIOKIiCB7MSwwLC0xfVxuICogQHJldHVybiBIb3Jpem9udGFsIG91dHB1dCBkZWx0YSDiiIggezEsMCwtMX1cbiAqL1xuZnVuY3Rpb24gYWR2YW5jZUJsb2NrKGN0eCwgcGVxLCBiLCBoSW4pIHtcbiAgICB2YXIgcFYgPSBjdHguUFtiXTtcbiAgICB2YXIgbVYgPSBjdHguTVtiXTtcbiAgICB2YXIgaEluSXNOZWdhdGl2ZSA9IGhJbiA+Pj4gMzE7IC8vIDEgaWYgaEluIDwgMCBvciAwIG90aGVyd2lzZS5cbiAgICB2YXIgZXEgPSBwZXFbYl0gfCBoSW5Jc05lZ2F0aXZlO1xuICAgIC8vIFN0ZXAgMTogQ29tcHV0ZSBob3Jpem9udGFsIGRlbHRhcy5cbiAgICB2YXIgeFYgPSBlcSB8IG1WO1xuICAgIHZhciB4SCA9ICgoKGVxICYgcFYpICsgcFYpIF4gcFYpIHwgZXE7XG4gICAgdmFyIHBIID0gbVYgfCB+KHhIIHwgcFYpO1xuICAgIHZhciBtSCA9IHBWICYgeEg7XG4gICAgLy8gU3RlcCAyOiBVcGRhdGUgc2NvcmUgKHZhbHVlIG9mIGxhc3Qgcm93IG9mIHRoaXMgYmxvY2spLlxuICAgIHZhciBoT3V0ID0gb25lSWZOb3RaZXJvKHBIICYgY3R4Lmxhc3RSb3dNYXNrW2JdKSAtXG4gICAgICAgIG9uZUlmTm90WmVybyhtSCAmIGN0eC5sYXN0Um93TWFza1tiXSk7XG4gICAgLy8gU3RlcCAzOiBVcGRhdGUgdmVydGljYWwgZGVsdGFzIGZvciB1c2Ugd2hlbiBwcm9jZXNzaW5nIG5leHQgY2hhci5cbiAgICBwSCA8PD0gMTtcbiAgICBtSCA8PD0gMTtcbiAgICBtSCB8PSBoSW5Jc05lZ2F0aXZlO1xuICAgIHBIIHw9IG9uZUlmTm90WmVybyhoSW4pIC0gaEluSXNOZWdhdGl2ZTsgLy8gc2V0IHBIWzBdIGlmIGhJbiA+IDBcbiAgICBwViA9IG1IIHwgfih4ViB8IHBIKTtcbiAgICBtViA9IHBIICYgeFY7XG4gICAgY3R4LlBbYl0gPSBwVjtcbiAgICBjdHguTVtiXSA9IG1WO1xuICAgIHJldHVybiBoT3V0O1xufVxuLyoqXG4gKiBGaW5kIHRoZSBlbmRzIGFuZCBlcnJvciBjb3VudHMgZm9yIG1hdGNoZXMgb2YgYHBhdHRlcm5gIGluIGB0ZXh0YC5cbiAqXG4gKiBPbmx5IHRoZSBtYXRjaGVzIHdpdGggdGhlIGxvd2VzdCBlcnJvciBjb3VudCBhcmUgcmVwb3J0ZWQuIE90aGVyIG1hdGNoZXNcbiAqIHdpdGggZXJyb3IgY291bnRzIDw9IG1heEVycm9ycyBhcmUgZGlzY2FyZGVkLlxuICpcbiAqIFRoaXMgaXMgdGhlIGJsb2NrLWJhc2VkIHNlYXJjaCBhbGdvcml0aG0gZnJvbSBGaWcuIDkgb24gcC40MTAgb2YgWzFdLlxuICovXG5mdW5jdGlvbiBmaW5kTWF0Y2hFbmRzKHRleHQsIHBhdHRlcm4sIG1heEVycm9ycykge1xuICAgIGlmIChwYXR0ZXJuLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICByZXR1cm4gW107XG4gICAgfVxuICAgIC8vIENsYW1wIGVycm9yIGNvdW50IHNvIHdlIGNhbiByZWx5IG9uIHRoZSBgbWF4RXJyb3JzYCBhbmQgYHBhdHRlcm4ubGVuZ3RoYFxuICAgIC8vIHJvd3MgYmVpbmcgaW4gdGhlIHNhbWUgYmxvY2sgYmVsb3cuXG4gICAgbWF4RXJyb3JzID0gTWF0aC5taW4obWF4RXJyb3JzLCBwYXR0ZXJuLmxlbmd0aCk7XG4gICAgdmFyIG1hdGNoZXMgPSBbXTtcbiAgICAvLyBXb3JkIHNpemUuXG4gICAgdmFyIHcgPSAzMjtcbiAgICAvLyBJbmRleCBvZiBtYXhpbXVtIGJsb2NrIGxldmVsLlxuICAgIHZhciBiTWF4ID0gTWF0aC5jZWlsKHBhdHRlcm4ubGVuZ3RoIC8gdykgLSAxO1xuICAgIC8vIENvbnRleHQgdXNlZCBhY3Jvc3MgYmxvY2sgY2FsY3VsYXRpb25zLlxuICAgIHZhciBjdHggPSB7XG4gICAgICAgIFA6IG5ldyBVaW50MzJBcnJheShiTWF4ICsgMSksXG4gICAgICAgIE06IG5ldyBVaW50MzJBcnJheShiTWF4ICsgMSksXG4gICAgICAgIGxhc3RSb3dNYXNrOiBuZXcgVWludDMyQXJyYXkoYk1heCArIDEpXG4gICAgfTtcbiAgICBjdHgubGFzdFJvd01hc2suZmlsbCgxIDw8IDMxKTtcbiAgICBjdHgubGFzdFJvd01hc2tbYk1heF0gPSAxIDw8IChwYXR0ZXJuLmxlbmd0aCAtIDEpICUgdztcbiAgICAvLyBEdW1teSBcInBlcVwiIGFycmF5IGZvciBjaGFycyBpbiB0aGUgdGV4dCB3aGljaCBkbyBub3Qgb2NjdXIgaW4gdGhlIHBhdHRlcm4uXG4gICAgdmFyIGVtcHR5UGVxID0gbmV3IFVpbnQzMkFycmF5KGJNYXggKyAxKTtcbiAgICAvLyBNYXAgb2YgVVRGLTE2IGNoYXJhY3RlciBjb2RlIHRvIGJpdCB2ZWN0b3IgaW5kaWNhdGluZyBwb3NpdGlvbnMgaW4gdGhlXG4gICAgLy8gcGF0dGVybiB0aGF0IGVxdWFsIHRoYXQgY2hhcmFjdGVyLlxuICAgIHZhciBwZXEgPSBuZXcgTWFwKCk7XG4gICAgLy8gVmVyc2lvbiBvZiBgcGVxYCB0aGF0IG9ubHkgc3RvcmVzIG1hcHBpbmdzIGZvciBzbWFsbCBjaGFyYWN0ZXJzLiBUaGlzXG4gICAgLy8gYWxsb3dzIGZhc3RlciBsb29rdXBzIHdoZW4gaXRlcmF0aW5nIHRocm91Z2ggdGhlIHRleHQgYmVjYXVzZSBhIHNpbXBsZVxuICAgIC8vIGFycmF5IGxvb2t1cCBjYW4gYmUgZG9uZSBpbnN0ZWFkIG9mIGEgaGFzaCB0YWJsZSBsb29rdXAuXG4gICAgdmFyIGFzY2lpUGVxID0gW107XG4gICAgZm9yICh2YXIgaSA9IDA7IGkgPCAyNTY7IGkrKykge1xuICAgICAgICBhc2NpaVBlcS5wdXNoKGVtcHR5UGVxKTtcbiAgICB9XG4gICAgLy8gQ2FsY3VsYXRlIGBjdHgucGVxYCAtIGEgbWFwIG9mIGNoYXJhY3RlciB2YWx1ZXMgdG8gYml0bWFza3MgaW5kaWNhdGluZ1xuICAgIC8vIHBvc2l0aW9ucyBvZiB0aGF0IGNoYXJhY3RlciB3aXRoaW4gdGhlIHBhdHRlcm4sIHdoZXJlIGVhY2ggYml0IHJlcHJlc2VudHNcbiAgICAvLyBhIHBvc2l0aW9uIGluIHRoZSBwYXR0ZXJuLlxuICAgIGZvciAodmFyIGMgPSAwOyBjIDwgcGF0dGVybi5sZW5ndGg7IGMgKz0gMSkge1xuICAgICAgICB2YXIgdmFsID0gcGF0dGVybi5jaGFyQ29kZUF0KGMpO1xuICAgICAgICBpZiAocGVxLmhhcyh2YWwpKSB7XG4gICAgICAgICAgICAvLyBEdXBsaWNhdGUgY2hhciBpbiBwYXR0ZXJuLlxuICAgICAgICAgICAgY29udGludWU7XG4gICAgICAgIH1cbiAgICAgICAgdmFyIGNoYXJQZXEgPSBuZXcgVWludDMyQXJyYXkoYk1heCArIDEpO1xuICAgICAgICBwZXEuc2V0KHZhbCwgY2hhclBlcSk7XG4gICAgICAgIGlmICh2YWwgPCBhc2NpaVBlcS5sZW5ndGgpIHtcbiAgICAgICAgICAgIGFzY2lpUGVxW3ZhbF0gPSBjaGFyUGVxO1xuICAgICAgICB9XG4gICAgICAgIGZvciAodmFyIGIgPSAwOyBiIDw9IGJNYXg7IGIgKz0gMSkge1xuICAgICAgICAgICAgY2hhclBlcVtiXSA9IDA7XG4gICAgICAgICAgICAvLyBTZXQgYWxsIHRoZSBiaXRzIHdoZXJlIHRoZSBwYXR0ZXJuIG1hdGNoZXMgdGhlIGN1cnJlbnQgY2hhciAoY2gpLlxuICAgICAgICAgICAgLy8gRm9yIGluZGV4ZXMgYmV5b25kIHRoZSBlbmQgb2YgdGhlIHBhdHRlcm4sIGFsd2F5cyBzZXQgdGhlIGJpdCBhcyBpZiB0aGVcbiAgICAgICAgICAgIC8vIHBhdHRlcm4gY29udGFpbmVkIGEgd2lsZGNhcmQgY2hhciBpbiB0aGF0IHBvc2l0aW9uLlxuICAgICAgICAgICAgZm9yICh2YXIgciA9IDA7IHIgPCB3OyByICs9IDEpIHtcbiAgICAgICAgICAgICAgICB2YXIgaWR4ID0gYiAqIHcgKyByO1xuICAgICAgICAgICAgICAgIGlmIChpZHggPj0gcGF0dGVybi5sZW5ndGgpIHtcbiAgICAgICAgICAgICAgICAgICAgY29udGludWU7XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIHZhciBtYXRjaCA9IHBhdHRlcm4uY2hhckNvZGVBdChpZHgpID09PSB2YWw7XG4gICAgICAgICAgICAgICAgaWYgKG1hdGNoKSB7XG4gICAgICAgICAgICAgICAgICAgIGNoYXJQZXFbYl0gfD0gMSA8PCByO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cbiAgICAvLyBJbmRleCBvZiBsYXN0LWFjdGl2ZSBibG9jayBsZXZlbCBpbiB0aGUgY29sdW1uLlxuICAgIHZhciB5ID0gTWF0aC5tYXgoMCwgTWF0aC5jZWlsKG1heEVycm9ycyAvIHcpIC0gMSk7XG4gICAgLy8gSW5pdGlhbGl6ZSBtYXhpbXVtIGVycm9yIGNvdW50IGF0IGJvdHRvbSBvZiBlYWNoIGJsb2NrLlxuICAgIHZhciBzY29yZSA9IG5ldyBVaW50MzJBcnJheShiTWF4ICsgMSk7XG4gICAgZm9yICh2YXIgYiA9IDA7IGIgPD0geTsgYiArPSAxKSB7XG4gICAgICAgIHNjb3JlW2JdID0gKGIgKyAxKSAqIHc7XG4gICAgfVxuICAgIHNjb3JlW2JNYXhdID0gcGF0dGVybi5sZW5ndGg7XG4gICAgLy8gSW5pdGlhbGl6ZSB2ZXJ0aWNhbCBkZWx0YXMgZm9yIGVhY2ggYmxvY2suXG4gICAgZm9yICh2YXIgYiA9IDA7IGIgPD0geTsgYiArPSAxKSB7XG4gICAgICAgIGN0eC5QW2JdID0gfjA7XG4gICAgICAgIGN0eC5NW2JdID0gMDtcbiAgICB9XG4gICAgLy8gUHJvY2VzcyBlYWNoIGNoYXIgb2YgdGhlIHRleHQsIGNvbXB1dGluZyB0aGUgZXJyb3IgY291bnQgZm9yIGB3YCBjaGFycyBvZlxuICAgIC8vIHRoZSBwYXR0ZXJuIGF0IGEgdGltZS5cbiAgICBmb3IgKHZhciBqID0gMDsgaiA8IHRleHQubGVuZ3RoOyBqICs9IDEpIHtcbiAgICAgICAgLy8gTG9va3VwIHRoZSBiaXRtYXNrIHJlcHJlc2VudGluZyB0aGUgcG9zaXRpb25zIG9mIHRoZSBjdXJyZW50IGNoYXIgZnJvbVxuICAgICAgICAvLyB0aGUgdGV4dCB3aXRoaW4gdGhlIHBhdHRlcm4uXG4gICAgICAgIHZhciBjaGFyQ29kZSA9IHRleHQuY2hhckNvZGVBdChqKTtcbiAgICAgICAgdmFyIGNoYXJQZXEgPSB2b2lkIDA7XG4gICAgICAgIGlmIChjaGFyQ29kZSA8IGFzY2lpUGVxLmxlbmd0aCkge1xuICAgICAgICAgICAgLy8gRmFzdCBhcnJheSBsb29rdXAuXG4gICAgICAgICAgICBjaGFyUGVxID0gYXNjaWlQZXFbY2hhckNvZGVdO1xuICAgICAgICB9XG4gICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgLy8gU2xvd2VyIGhhc2ggdGFibGUgbG9va3VwLlxuICAgICAgICAgICAgY2hhclBlcSA9IHBlcS5nZXQoY2hhckNvZGUpO1xuICAgICAgICAgICAgaWYgKHR5cGVvZiBjaGFyUGVxID09PSBcInVuZGVmaW5lZFwiKSB7XG4gICAgICAgICAgICAgICAgY2hhclBlcSA9IGVtcHR5UGVxO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgIC8vIENhbGN1bGF0ZSBlcnJvciBjb3VudCBmb3IgYmxvY2tzIHRoYXQgd2UgZGVmaW5pdGVseSBoYXZlIHRvIHByb2Nlc3MgZm9yXG4gICAgICAgIC8vIHRoaXMgY29sdW1uLlxuICAgICAgICB2YXIgY2FycnkgPSAwO1xuICAgICAgICBmb3IgKHZhciBiID0gMDsgYiA8PSB5OyBiICs9IDEpIHtcbiAgICAgICAgICAgIGNhcnJ5ID0gYWR2YW5jZUJsb2NrKGN0eCwgY2hhclBlcSwgYiwgY2FycnkpO1xuICAgICAgICAgICAgc2NvcmVbYl0gKz0gY2Fycnk7XG4gICAgICAgIH1cbiAgICAgICAgLy8gQ2hlY2sgaWYgd2UgYWxzbyBuZWVkIHRvIGNvbXB1dGUgYW4gYWRkaXRpb25hbCBibG9jaywgb3IgaWYgd2UgY2FuIHJlZHVjZVxuICAgICAgICAvLyB0aGUgbnVtYmVyIG9mIGJsb2NrcyBwcm9jZXNzZWQgZm9yIHRoZSBuZXh0IGNvbHVtbi5cbiAgICAgICAgaWYgKHNjb3JlW3ldIC0gY2FycnkgPD0gbWF4RXJyb3JzICYmXG4gICAgICAgICAgICB5IDwgYk1heCAmJlxuICAgICAgICAgICAgKGNoYXJQZXFbeSArIDFdICYgMSB8fCBjYXJyeSA8IDApKSB7XG4gICAgICAgICAgICAvLyBFcnJvciBjb3VudCBmb3IgYm90dG9tIGJsb2NrIGlzIHVuZGVyIHRocmVzaG9sZCwgaW5jcmVhc2UgdGhlIG51bWJlciBvZlxuICAgICAgICAgICAgLy8gYmxvY2tzIHByb2Nlc3NlZCBmb3IgdGhpcyBjb2x1bW4gJiBuZXh0IGJ5IDEuXG4gICAgICAgICAgICB5ICs9IDE7XG4gICAgICAgICAgICBjdHguUFt5XSA9IH4wO1xuICAgICAgICAgICAgY3R4Lk1beV0gPSAwO1xuICAgICAgICAgICAgdmFyIG1heEJsb2NrU2NvcmUgPSB5ID09PSBiTWF4ID8gcGF0dGVybi5sZW5ndGggJSB3IDogdztcbiAgICAgICAgICAgIHNjb3JlW3ldID1cbiAgICAgICAgICAgICAgICBzY29yZVt5IC0gMV0gK1xuICAgICAgICAgICAgICAgICAgICBtYXhCbG9ja1Njb3JlIC1cbiAgICAgICAgICAgICAgICAgICAgY2FycnkgK1xuICAgICAgICAgICAgICAgICAgICBhZHZhbmNlQmxvY2soY3R4LCBjaGFyUGVxLCB5LCBjYXJyeSk7XG4gICAgICAgIH1cbiAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICAvLyBFcnJvciBjb3VudCBmb3IgYm90dG9tIGJsb2NrIGV4Y2VlZHMgdGhyZXNob2xkLCByZWR1Y2UgdGhlIG51bWJlciBvZlxuICAgICAgICAgICAgLy8gYmxvY2tzIHByb2Nlc3NlZCBmb3IgdGhlIG5leHQgY29sdW1uLlxuICAgICAgICAgICAgd2hpbGUgKHkgPiAwICYmIHNjb3JlW3ldID49IG1heEVycm9ycyArIHcpIHtcbiAgICAgICAgICAgICAgICB5IC09IDE7XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgLy8gSWYgZXJyb3IgY291bnQgaXMgdW5kZXIgdGhyZXNob2xkLCByZXBvcnQgYSBtYXRjaC5cbiAgICAgICAgaWYgKHkgPT09IGJNYXggJiYgc2NvcmVbeV0gPD0gbWF4RXJyb3JzKSB7XG4gICAgICAgICAgICBpZiAoc2NvcmVbeV0gPCBtYXhFcnJvcnMpIHtcbiAgICAgICAgICAgICAgICAvLyBEaXNjYXJkIGFueSBlYXJsaWVyLCB3b3JzZSBtYXRjaGVzLlxuICAgICAgICAgICAgICAgIG1hdGNoZXMuc3BsaWNlKDAsIG1hdGNoZXMubGVuZ3RoKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIG1hdGNoZXMucHVzaCh7XG4gICAgICAgICAgICAgICAgc3RhcnQ6IC0xLFxuICAgICAgICAgICAgICAgIGVuZDogaiArIDEsXG4gICAgICAgICAgICAgICAgZXJyb3JzOiBzY29yZVt5XVxuICAgICAgICAgICAgfSk7XG4gICAgICAgICAgICAvLyBCZWNhdXNlIGBzZWFyY2hgIG9ubHkgcmVwb3J0cyB0aGUgbWF0Y2hlcyB3aXRoIHRoZSBsb3dlc3QgZXJyb3IgY291bnQsXG4gICAgICAgICAgICAvLyB3ZSBjYW4gXCJyYXRjaGV0IGRvd25cIiB0aGUgbWF4IGVycm9yIHRocmVzaG9sZCB3aGVuZXZlciBhIG1hdGNoIGlzXG4gICAgICAgICAgICAvLyBlbmNvdW50ZXJlZCBhbmQgdGhlcmVieSBzYXZlIGEgc21hbGwgYW1vdW50IG9mIHdvcmsgZm9yIHRoZSByZW1haW5kZXJcbiAgICAgICAgICAgIC8vIG9mIHRoZSB0ZXh0LlxuICAgICAgICAgICAgbWF4RXJyb3JzID0gc2NvcmVbeV07XG4gICAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIG1hdGNoZXM7XG59XG4vKipcbiAqIFNlYXJjaCBmb3IgbWF0Y2hlcyBmb3IgYHBhdHRlcm5gIGluIGB0ZXh0YCBhbGxvd2luZyB1cCB0byBgbWF4RXJyb3JzYCBlcnJvcnMuXG4gKlxuICogUmV0dXJucyB0aGUgc3RhcnQsIGFuZCBlbmQgcG9zaXRpb25zIGFuZCBlcnJvciBjb3VudHMgZm9yIGVhY2ggbG93ZXN0LWNvc3RcbiAqIG1hdGNoLiBPbmx5IHRoZSBcImJlc3RcIiBtYXRjaGVzIGFyZSByZXR1cm5lZC5cbiAqL1xuZnVuY3Rpb24gc2VhcmNoKHRleHQsIHBhdHRlcm4sIG1heEVycm9ycykge1xuICAgIHZhciBtYXRjaGVzID0gZmluZE1hdGNoRW5kcyh0ZXh0LCBwYXR0ZXJuLCBtYXhFcnJvcnMpO1xuICAgIHJldHVybiBmaW5kTWF0Y2hTdGFydHModGV4dCwgcGF0dGVybiwgbWF0Y2hlcyk7XG59XG5leHBvcnRzLmRlZmF1bHQgPSBzZWFyY2g7XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3089\n')},5232:function(__unused_webpack_module,__unused_webpack___webpack_exports__,__webpack_require__){"use strict";eval('\n// EXTERNAL MODULE: ./node_modules/approx-string-match/dist/index.js\nvar dist = __webpack_require__(3089);\n;// CONCATENATED MODULE: ./src/vendor/hypothesis/anchoring/match-quote.js\n\n/**\n * @typedef {import(\'approx-string-match\').Match} StringMatch\n */\n\n/**\n * @typedef Match\n * @prop {number} start - Start offset of match in text\n * @prop {number} end - End offset of match in text\n * @prop {number} score -\n * Score for the match between 0 and 1.0, where 1.0 indicates a perfect match\n * for the quote and context.\n */\n\n/**\n * Find the best approximate matches for `str` in `text` allowing up to `maxErrors` errors.\n *\n * @param {string} text\n * @param {string} str\n * @param {number} maxErrors\n * @return {StringMatch[]}\n */\n\nfunction search(text, str, maxErrors) {\n // Do a fast search for exact matches. The `approx-string-match` library\n // doesn\'t currently incorporate this optimization itself.\n var matchPos = 0;\n var exactMatches = [];\n\n while (matchPos !== -1) {\n matchPos = text.indexOf(str, matchPos);\n\n if (matchPos !== -1) {\n exactMatches.push({\n start: matchPos,\n end: matchPos + str.length,\n errors: 0\n });\n matchPos += 1;\n }\n }\n\n if (exactMatches.length > 0) {\n return exactMatches;\n } // If there are no exact matches, do a more expensive search for matches\n // with errors.\n\n\n return (0,dist/* default */.Z)(text, str, maxErrors);\n}\n/**\n * Compute a score between 0 and 1.0 for the similarity between `text` and `str`.\n *\n * @param {string} text\n * @param {string} str\n */\n\n\nfunction textMatchScore(text, str) {\n /* istanbul ignore next - `scoreMatch` will never pass an empty string */\n if (str.length === 0 || text.length === 0) {\n return 0.0;\n }\n\n var matches = search(text, str, str.length); // prettier-ignore\n\n return 1 - matches[0].errors / str.length;\n}\n/**\n * Find the best approximate match for `quote` in `text`.\n *\n * Returns `null` if no match exceeding the minimum quality threshold was found.\n *\n * @param {string} text - Document text to search\n * @param {string} quote - String to find within `text`\n * @param {Object} context -\n * Context in which the quote originally appeared. This is used to choose the\n * best match.\n * @param {string} [context.prefix] - Expected text before the quote\n * @param {string} [context.suffix] - Expected text after the quote\n * @param {number} [context.hint] - Expected offset of match within text\n * @return {Match|null}\n */\n\n\nfunction matchQuote(text, quote) {\n var context = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};\n\n if (quote.length === 0) {\n return null;\n } // Choose the maximum number of errors to allow for the initial search.\n // This choice involves a tradeoff between:\n //\n // - Recall (proportion of "good" matches found)\n // - Precision (proportion of matches found which are "good")\n // - Cost of the initial search and of processing the candidate matches [1]\n //\n // [1] Specifically, the expected-time complexity of the initial search is\n // `O((maxErrors / 32) * text.length)`. See `approx-string-match` docs.\n\n\n var maxErrors = Math.min(256, quote.length / 2); // Find closest matches for `quote` in `text` based on edit distance.\n\n var matches = search(text, quote, maxErrors);\n\n if (matches.length === 0) {\n return null;\n }\n /**\n * Compute a score between 0 and 1.0 for a match candidate.\n *\n * @param {StringMatch} match\n */\n\n\n var scoreMatch = function scoreMatch(match) {\n var quoteWeight = 50; // Similarity of matched text to quote.\n\n var prefixWeight = 20; // Similarity of text before matched text to `context.prefix`.\n\n var suffixWeight = 20; // Similarity of text after matched text to `context.suffix`.\n\n var posWeight = 2; // Proximity to expected location. Used as a tie-breaker.\n\n var quoteScore = 1 - match.errors / quote.length;\n var prefixScore = context.prefix ? textMatchScore(text.slice(Math.max(0, match.start - context.prefix.length), match.start), context.prefix) : 1.0;\n var suffixScore = context.suffix ? textMatchScore(text.slice(match.end, match.end + context.suffix.length), context.suffix) : 1.0;\n var posScore = 1.0;\n\n if (typeof context.hint === \'number\') {\n var offset = Math.abs(match.start - context.hint);\n posScore = 1.0 - offset / text.length;\n }\n\n var rawScore = quoteWeight * quoteScore + prefixWeight * prefixScore + suffixWeight * suffixScore + posWeight * posScore;\n var maxScore = quoteWeight + prefixWeight + suffixWeight + posWeight;\n var normalizedScore = rawScore / maxScore;\n return normalizedScore;\n }; // Rank matches based on similarity of actual and expected surrounding text\n // and actual/expected offset in the document text.\n\n\n var scoredMatches = matches.map(function (m) {\n return {\n start: m.start,\n end: m.end,\n score: scoreMatch(m)\n };\n }); // Choose match with highest score.\n\n scoredMatches.sort(function (a, b) {\n return b.score - a.score;\n });\n return scoredMatches[0];\n}\n;// CONCATENATED MODULE: ./src/vendor/hypothesis/anchoring/text-range.js\nfunction _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }\n\nfunction _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }\n\nfunction _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }\n\nfunction _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\nfunction _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }\n\nfunction _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\n/**\n * Return the combined length of text nodes contained in `node`.\n *\n * @param {Node} node\n */\nfunction nodeTextLength(node) {\n switch (node.nodeType) {\n case Node.ELEMENT_NODE:\n case Node.TEXT_NODE:\n // nb. `textContent` excludes text in comments and processing instructions\n // when called on a parent element, so we don\'t need to subtract that here.\n return (\n /** @type {string} */\n node.textContent.length\n );\n\n default:\n return 0;\n }\n}\n/**\n * Return the total length of the text of all previous siblings of `node`.\n *\n * @param {Node} node\n */\n\n\nfunction previousSiblingsTextLength(node) {\n var sibling = node.previousSibling;\n var length = 0;\n\n while (sibling) {\n length += nodeTextLength(sibling);\n sibling = sibling.previousSibling;\n }\n\n return length;\n}\n/**\n * Resolve one or more character offsets within an element to (text node, position)\n * pairs.\n *\n * @param {Element} element\n * @param {number[]} offsets - Offsets, which must be sorted in ascending order\n * @return {{ node: Text, offset: number }[]}\n */\n\n\nfunction resolveOffsets(element) {\n for (var _len = arguments.length, offsets = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n offsets[_key - 1] = arguments[_key];\n }\n\n var nextOffset = offsets.shift();\n var nodeIter =\n /** @type {Document} */\n element.ownerDocument.createNodeIterator(element, NodeFilter.SHOW_TEXT);\n var results = [];\n var currentNode = nodeIter.nextNode();\n var textNode;\n var length = 0; // Find the text node containing the `nextOffset`th character from the start\n // of `element`.\n\n while (nextOffset !== undefined && currentNode) {\n textNode =\n /** @type {Text} */\n currentNode;\n\n if (length + textNode.data.length > nextOffset) {\n results.push({\n node: textNode,\n offset: nextOffset - length\n });\n nextOffset = offsets.shift();\n } else {\n currentNode = nodeIter.nextNode();\n length += textNode.data.length;\n }\n } // Boundary case.\n\n\n while (nextOffset !== undefined && textNode && length === nextOffset) {\n results.push({\n node: textNode,\n offset: textNode.data.length\n });\n nextOffset = offsets.shift();\n }\n\n if (nextOffset !== undefined) {\n throw new RangeError(\'Offset exceeds text length\');\n }\n\n return results;\n}\n\nvar RESOLVE_FORWARDS = 1;\nvar RESOLVE_BACKWARDS = 2;\n/**\n * Represents an offset within the text content of an element.\n *\n * This position can be resolved to a specific descendant node in the current\n * DOM subtree of the element using the `resolve` method.\n */\n\nvar text_range_TextPosition = /*#__PURE__*/function () {\n /**\n * Construct a `TextPosition` that refers to the text position `offset` within\n * the text content of `element`.\n *\n * @param {Element} element\n * @param {number} offset\n */\n function TextPosition(element, offset) {\n _classCallCheck(this, TextPosition);\n\n if (offset < 0) {\n throw new Error(\'Offset is invalid\');\n }\n /** Element that `offset` is relative to. */\n\n\n this.element = element;\n /** Character offset from the start of the element\'s `textContent`. */\n\n this.offset = offset;\n }\n /**\n * Return a copy of this position with offset relative to a given ancestor\n * element.\n *\n * @param {Element} parent - Ancestor of `this.element`\n * @return {TextPosition}\n */\n\n\n _createClass(TextPosition, [{\n key: "relativeTo",\n value: function relativeTo(parent) {\n if (!parent.contains(this.element)) {\n throw new Error(\'Parent is not an ancestor of current element\');\n }\n\n var el = this.element;\n var offset = this.offset;\n\n while (el !== parent) {\n offset += previousSiblingsTextLength(el);\n el =\n /** @type {Element} */\n el.parentElement;\n }\n\n return new TextPosition(el, offset);\n }\n /**\n * Resolve the position to a specific text node and offset within that node.\n *\n * Throws if `this.offset` exceeds the length of the element\'s text. In the\n * case where the element has no text and `this.offset` is 0, the `direction`\n * option determines what happens.\n *\n * Offsets at the boundary between two nodes are resolved to the start of the\n * node that begins at the boundary.\n *\n * @param {Object} [options]\n * @param {RESOLVE_FORWARDS|RESOLVE_BACKWARDS} [options.direction] -\n * Specifies in which direction to search for the nearest text node if\n * `this.offset` is `0` and `this.element` has no text. If not specified\n * an error is thrown.\n * @return {{ node: Text, offset: number }}\n * @throws {RangeError}\n */\n\n }, {\n key: "resolve",\n value: function resolve() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n\n try {\n return resolveOffsets(this.element, this.offset)[0];\n } catch (err) {\n if (this.offset === 0 && options.direction !== undefined) {\n var tw = document.createTreeWalker(this.element.getRootNode(), NodeFilter.SHOW_TEXT);\n tw.currentNode = this.element;\n var forwards = options.direction === RESOLVE_FORWARDS;\n var text =\n /** @type {Text|null} */\n forwards ? tw.nextNode() : tw.previousNode();\n\n if (!text) {\n throw err;\n }\n\n return {\n node: text,\n offset: forwards ? 0 : text.data.length\n };\n } else {\n throw err;\n }\n }\n }\n /**\n * Construct a `TextPosition` that refers to the `offset`th character within\n * `node`.\n *\n * @param {Node} node\n * @param {number} offset\n * @return {TextPosition}\n */\n\n }], [{\n key: "fromCharOffset",\n value: function fromCharOffset(node, offset) {\n switch (node.nodeType) {\n case Node.TEXT_NODE:\n return TextPosition.fromPoint(node, offset);\n\n case Node.ELEMENT_NODE:\n return new TextPosition(\n /** @type {Element} */\n node, offset);\n\n default:\n throw new Error(\'Node is not an element or text node\');\n }\n }\n /**\n * Construct a `TextPosition` representing the range start or end point (node, offset).\n *\n * @param {Node} node - Text or Element node\n * @param {number} offset - Offset within the node.\n * @return {TextPosition}\n */\n\n }, {\n key: "fromPoint",\n value: function fromPoint(node, offset) {\n switch (node.nodeType) {\n case Node.TEXT_NODE:\n {\n if (offset < 0 || offset >\n /** @type {Text} */\n node.data.length) {\n throw new Error(\'Text node offset is out of range\');\n }\n\n if (!node.parentElement) {\n throw new Error(\'Text node has no parent\');\n } // Get the offset from the start of the parent element.\n\n\n var textOffset = previousSiblingsTextLength(node) + offset;\n return new TextPosition(node.parentElement, textOffset);\n }\n\n case Node.ELEMENT_NODE:\n {\n if (offset < 0 || offset > node.childNodes.length) {\n throw new Error(\'Child node offset is out of range\');\n } // Get the text length before the `offset`th child of element.\n\n\n var _textOffset = 0;\n\n for (var i = 0; i < offset; i++) {\n _textOffset += nodeTextLength(node.childNodes[i]);\n }\n\n return new TextPosition(\n /** @type {Element} */\n node, _textOffset);\n }\n\n default:\n throw new Error(\'Point is not in an element or text node\');\n }\n }\n }]);\n\n return TextPosition;\n}();\n/**\n * Represents a region of a document as a (start, end) pair of `TextPosition` points.\n *\n * Representing a range in this way allows for changes in the DOM content of the\n * range which don\'t affect its text content, without affecting the text content\n * of the range itself.\n */\n\nvar text_range_TextRange = /*#__PURE__*/function () {\n /**\n * Construct an immutable `TextRange` from a `start` and `end` point.\n *\n * @param {TextPosition} start\n * @param {TextPosition} end\n */\n function TextRange(start, end) {\n _classCallCheck(this, TextRange);\n\n this.start = start;\n this.end = end;\n }\n /**\n * Return a copy of this range with start and end positions relative to a\n * given ancestor. See `TextPosition.relativeTo`.\n *\n * @param {Element} element\n */\n\n\n _createClass(TextRange, [{\n key: "relativeTo",\n value: function relativeTo(element) {\n return new TextRange(this.start.relativeTo(element), this.end.relativeTo(element));\n }\n /**\n * Resolve the `TextRange` to a DOM range.\n *\n * The resulting DOM Range will always start and end in a `Text` node.\n * Hence `TextRange.fromRange(range).toRange()` can be used to "shrink" a\n * range to the text it contains.\n *\n * May throw if the `start` or `end` positions cannot be resolved to a range.\n *\n * @return {Range}\n */\n\n }, {\n key: "toRange",\n value: function toRange() {\n var start;\n var end;\n\n if (this.start.element === this.end.element && this.start.offset <= this.end.offset) {\n // Fast path for start and end points in same element.\n var _resolveOffsets = resolveOffsets(this.start.element, this.start.offset, this.end.offset);\n\n var _resolveOffsets2 = _slicedToArray(_resolveOffsets, 2);\n\n start = _resolveOffsets2[0];\n end = _resolveOffsets2[1];\n } else {\n start = this.start.resolve({\n direction: RESOLVE_FORWARDS\n });\n end = this.end.resolve({\n direction: RESOLVE_BACKWARDS\n });\n }\n\n var range = new Range();\n range.setStart(start.node, start.offset);\n range.setEnd(end.node, end.offset);\n return range;\n }\n /**\n * Convert an existing DOM `Range` to a `TextRange`\n *\n * @param {Range} range\n * @return {TextRange}\n */\n\n }], [{\n key: "fromRange",\n value: function fromRange(range) {\n var start = text_range_TextPosition.fromPoint(range.startContainer, range.startOffset);\n var end = text_range_TextPosition.fromPoint(range.endContainer, range.endOffset);\n return new TextRange(start, end);\n }\n /**\n * Return a `TextRange` from the `start`th to `end`th characters in `root`.\n *\n * @param {Element} root\n * @param {number} start\n * @param {number} end\n */\n\n }, {\n key: "fromOffsets",\n value: function fromOffsets(root, start, end) {\n return new TextRange(new text_range_TextPosition(root, start), new text_range_TextPosition(root, end));\n }\n }]);\n\n return TextRange;\n}();\n;// CONCATENATED MODULE: ./src/vendor/hypothesis/anchoring/types.js\nfunction ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }\n\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nfunction types_classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }\n\nfunction types_defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction types_createClass(Constructor, protoProps, staticProps) { if (protoProps) types_defineProperties(Constructor.prototype, protoProps); if (staticProps) types_defineProperties(Constructor, staticProps); return Constructor; }\n\n/**\n * This module exports a set of classes for converting between DOM `Range`\n * objects and different types of selectors. It is mostly a thin wrapper around a\n * set of anchoring libraries. It serves two main purposes:\n *\n * 1. Providing a consistent interface across different types of anchors.\n * 2. Insulating the rest of the code from API changes in the underlying anchoring\n * libraries.\n */\n\n\n\n/**\n * @typedef {import(\'../../types/api\').RangeSelector} RangeSelector\n * @typedef {import(\'../../types/api\').TextPositionSelector} TextPositionSelector\n * @typedef {import(\'../../types/api\').TextQuoteSelector} TextQuoteSelector\n */\n\n/**\n * Converts between `RangeSelector` selectors and `Range` objects.\n */\n\nvar RangeAnchor = /*#__PURE__*/(/* unused pure expression or super */ null && (function () {\n /**\n * @param {Node} root - A root element from which to anchor.\n * @param {Range} range - A range describing the anchor.\n */\n function RangeAnchor(root, range) {\n types_classCallCheck(this, RangeAnchor);\n\n this.root = root;\n this.range = range;\n }\n /**\n * @param {Node} root - A root element from which to anchor.\n * @param {Range} range - A range describing the anchor.\n */\n\n\n types_createClass(RangeAnchor, [{\n key: "toRange",\n value: function toRange() {\n return this.range;\n }\n /**\n * @return {RangeSelector}\n */\n\n }, {\n key: "toSelector",\n value: function toSelector() {\n // "Shrink" the range so that it tightly wraps its text. This ensures more\n // predictable output for a given text selection.\n var normalizedRange = TextRange.fromRange(this.range).toRange();\n var textRange = TextRange.fromRange(normalizedRange);\n var startContainer = xpathFromNode(textRange.start.element, this.root);\n var endContainer = xpathFromNode(textRange.end.element, this.root);\n return {\n type: \'RangeSelector\',\n startContainer: startContainer,\n startOffset: textRange.start.offset,\n endContainer: endContainer,\n endOffset: textRange.end.offset\n };\n }\n }], [{\n key: "fromRange",\n value: function fromRange(root, range) {\n return new RangeAnchor(root, range);\n }\n /**\n * Create an anchor from a serialized `RangeSelector` selector.\n *\n * @param {Element} root - A root element from which to anchor.\n * @param {RangeSelector} selector\n */\n\n }, {\n key: "fromSelector",\n value: function fromSelector(root, selector) {\n var startContainer = nodeFromXPath(selector.startContainer, root);\n\n if (!startContainer) {\n throw new Error(\'Failed to resolve startContainer XPath\');\n }\n\n var endContainer = nodeFromXPath(selector.endContainer, root);\n\n if (!endContainer) {\n throw new Error(\'Failed to resolve endContainer XPath\');\n }\n\n var startPos = TextPosition.fromCharOffset(startContainer, selector.startOffset);\n var endPos = TextPosition.fromCharOffset(endContainer, selector.endOffset);\n var range = new TextRange(startPos, endPos).toRange();\n return new RangeAnchor(root, range);\n }\n }]);\n\n return RangeAnchor;\n}()));\n/**\n * Converts between `TextPositionSelector` selectors and `Range` objects.\n */\n\nvar TextPositionAnchor = /*#__PURE__*/function () {\n /**\n * @param {Element} root\n * @param {number} start\n * @param {number} end\n */\n function TextPositionAnchor(root, start, end) {\n types_classCallCheck(this, TextPositionAnchor);\n\n this.root = root;\n this.start = start;\n this.end = end;\n }\n /**\n * @param {Element} root\n * @param {Range} range\n */\n\n\n types_createClass(TextPositionAnchor, [{\n key: "toSelector",\n value:\n /**\n * @return {TextPositionSelector}\n */\n function toSelector() {\n return {\n type: \'TextPositionSelector\',\n start: this.start,\n end: this.end\n };\n }\n }, {\n key: "toRange",\n value: function toRange() {\n return text_range_TextRange.fromOffsets(this.root, this.start, this.end).toRange();\n }\n }], [{\n key: "fromRange",\n value: function fromRange(root, range) {\n var textRange = text_range_TextRange.fromRange(range).relativeTo(root);\n return new TextPositionAnchor(root, textRange.start.offset, textRange.end.offset);\n }\n /**\n * @param {Element} root\n * @param {TextPositionSelector} selector\n */\n\n }, {\n key: "fromSelector",\n value: function fromSelector(root, selector) {\n return new TextPositionAnchor(root, selector.start, selector.end);\n }\n }]);\n\n return TextPositionAnchor;\n}();\n/**\n * @typedef QuoteMatchOptions\n * @prop {number} [hint] - Expected position of match in text. See `matchQuote`.\n */\n\n/**\n * Converts between `TextQuoteSelector` selectors and `Range` objects.\n */\n\nvar TextQuoteAnchor = /*#__PURE__*/function () {\n /**\n * @param {Element} root - A root element from which to anchor.\n * @param {string} exact\n * @param {Object} context\n * @param {string} [context.prefix]\n * @param {string} [context.suffix]\n */\n function TextQuoteAnchor(root, exact) {\n var context = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};\n\n types_classCallCheck(this, TextQuoteAnchor);\n\n this.root = root;\n this.exact = exact;\n this.context = context;\n }\n /**\n * Create a `TextQuoteAnchor` from a range.\n *\n * Will throw if `range` does not contain any text nodes.\n *\n * @param {Element} root\n * @param {Range} range\n */\n\n\n types_createClass(TextQuoteAnchor, [{\n key: "toSelector",\n value:\n /**\n * @return {TextQuoteSelector}\n */\n function toSelector() {\n return {\n type: \'TextQuoteSelector\',\n exact: this.exact,\n prefix: this.context.prefix,\n suffix: this.context.suffix\n };\n }\n /**\n * @param {QuoteMatchOptions} [options]\n */\n\n }, {\n key: "toRange",\n value: function toRange() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n return this.toPositionAnchor(options).toRange();\n }\n /**\n * @param {QuoteMatchOptions} [options]\n */\n\n }, {\n key: "toPositionAnchor",\n value: function toPositionAnchor() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n var text =\n /** @type {string} */\n this.root.textContent;\n var match = matchQuote(text, this.exact, _objectSpread(_objectSpread({}, this.context), {}, {\n hint: options.hint\n }));\n\n if (!match) {\n throw new Error(\'Quote not found\');\n }\n\n return new TextPositionAnchor(this.root, match.start, match.end);\n }\n }], [{\n key: "fromRange",\n value: function fromRange(root, range) {\n var text =\n /** @type {string} */\n root.textContent;\n var textRange = text_range_TextRange.fromRange(range).relativeTo(root);\n var start = textRange.start.offset;\n var end = textRange.end.offset; // Number of characters around the quote to capture as context. We currently\n // always use a fixed amount, but it would be better if this code was aware\n // of logical boundaries in the document (paragraph, article etc.) to avoid\n // capturing text unrelated to the quote.\n //\n // In regular prose the ideal content would often be the surrounding sentence.\n // This is a natural unit of meaning which enables displaying quotes in\n // context even when the document is not available. We could use `Intl.Segmenter`\n // for this when available.\n\n var contextLen = 32;\n return new TextQuoteAnchor(root, text.slice(start, end), {\n prefix: text.slice(Math.max(0, start - contextLen), start),\n suffix: text.slice(end, Math.min(text.length, end + contextLen))\n });\n }\n /**\n * @param {Element} root\n * @param {TextQuoteSelector} selector\n */\n\n }, {\n key: "fromSelector",\n value: function fromSelector(root, selector) {\n var prefix = selector.prefix,\n suffix = selector.suffix;\n return new TextQuoteAnchor(root, selector.exact, {\n prefix: prefix,\n suffix: suffix\n });\n }\n }]);\n\n return TextQuoteAnchor;\n}();\n;// CONCATENATED MODULE: ./src/utils.js\nfunction _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = utils_unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }\n\nfunction utils_unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return utils_arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return utils_arrayLikeToArray(o, minLen); }\n\nfunction utils_arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\n//\n// Copyright 2021 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n // Catch JS errors to log them in the app.\n\nwindow.addEventListener("error", function (event) {\n Android.logError(event.message, event.filename, event.lineno);\n}, false);\nwindow.addEventListener("load", function () {\n var observer = new ResizeObserver(function () {\n onViewportWidthChanged();\n snapCurrentOffset();\n });\n observer.observe(document.body);\n}, false);\n/**\n * Having an odd number of columns when displaying two columns per screen causes snapping and page\n * turning issues. To fix this, we insert a blank virtual column at the end of the resource.\n */\n\nfunction appendVirtualColumnIfNeeded() {\n var id = "readium-virtual-page";\n var virtualCol = document.getElementById(id);\n\n if (isScrollModeEnabled() || getColumnCountPerScreen() != 2) {\n if (virtualCol) {\n virtualCol.remove();\n }\n } else {\n var documentWidth = document.scrollingElement.scrollWidth;\n var colCount = documentWidth / pageWidth;\n var hasOddColCount = Math.round(colCount * 2) / 2 % 1 > 0.1;\n\n if (hasOddColCount) {\n if (virtualCol) {\n virtualCol.remove();\n } else {\n virtualCol = document.createElement("div");\n virtualCol.setAttribute("id", id);\n virtualCol.style.breakBefore = "column";\n virtualCol.innerHTML = "​"; // zero-width space\n\n document.body.appendChild(virtualCol);\n }\n }\n }\n}\n\nvar pageWidth = 1;\n\nfunction onViewportWidthChanged() {\n // We can\'t rely on window.innerWidth for the pageWidth on Android, because if the\n // device pixel ratio is not an integer, we get rounding issues offsetting the pages.\n //\n // See https://github.com/readium/readium-css/issues/97\n // and https://github.com/readium/r2-navigator-kotlin/issues/146\n var width = Android.getViewportWidth();\n pageWidth = width / window.devicePixelRatio;\n setProperty("--RS__viewportWidth", "calc(" + width + "px / " + window.devicePixelRatio + ")");\n appendVirtualColumnIfNeeded();\n}\n\nfunction getColumnCountPerScreen() {\n return parseInt(window.getComputedStyle(document.documentElement).getPropertyValue("column-count"));\n}\nfunction isScrollModeEnabled() {\n var style = document.documentElement.style;\n return style.getPropertyValue("--USER__view").trim() == "readium-scroll-on" || // FIXME: Will need to be removed in Readium 3.0, --USER__scroll was incorrect.\n style.getPropertyValue("--USER__scroll").trim() == "readium-scroll-on";\n}\nfunction isRTL() {\n return document.body.dir.toLowerCase() == "rtl";\n} // Scroll to the given TagId in document and snap.\n\nfunction scrollToId(id) {\n var element = document.getElementById(id);\n\n if (!element) {\n return false;\n }\n\n return scrollToRect(element.getBoundingClientRect());\n} // Position must be in the range [0 - 1], 0-100%.\n\nfunction scrollToPosition(position) {\n // Android.log("scrollToPosition " + position);\n if (position < 0 || position > 1) {\n throw "scrollToPosition() must be given a position from 0.0 to 1.0";\n }\n\n var offset;\n\n if (isScrollModeEnabled()) {\n offset = document.scrollingElement.scrollHeight * position;\n document.scrollingElement.scrollTop = offset; // window.scrollTo(0, offset);\n } else {\n var documentWidth = document.scrollingElement.scrollWidth;\n var factor = isRTL() ? -1 : 1;\n offset = documentWidth * position * factor;\n document.scrollingElement.scrollLeft = snapOffset(offset);\n }\n} // Scrolls to the first occurrence of the given text snippet.\n//\n// The expected text argument is a Locator Text object, as defined here:\n// https://readium.org/architecture/models/locators/\n\nfunction scrollToText(text) {\n var range = rangeFromLocator({\n text: text\n });\n\n if (!range) {\n return false;\n }\n\n scrollToRange(range);\n return true;\n}\n\nfunction scrollToRange(range) {\n return scrollToRect(range.getBoundingClientRect());\n}\n\nfunction scrollToRect(rect) {\n if (isScrollModeEnabled()) {\n document.scrollingElement.scrollTop = rect.top + window.scrollY;\n } else {\n document.scrollingElement.scrollLeft = snapOffset(rect.left + window.scrollX);\n }\n\n return true;\n}\n\nfunction scrollToStart() {\n // Android.log("scrollToStart");\n if (!isScrollModeEnabled()) {\n document.scrollingElement.scrollLeft = 0;\n } else {\n document.scrollingElement.scrollTop = 0;\n window.scrollTo(0, 0);\n }\n}\nfunction scrollToEnd() {\n // Android.log("scrollToEnd");\n if (!isScrollModeEnabled()) {\n var factor = isRTL() ? -1 : 1;\n document.scrollingElement.scrollLeft = snapOffset(document.scrollingElement.scrollWidth * factor);\n } else {\n document.scrollingElement.scrollTop = document.body.scrollHeight;\n window.scrollTo(0, document.body.scrollHeight);\n }\n} // Returns false if the page is already at the left-most scroll offset.\n\nfunction scrollLeft() {\n var documentWidth = document.scrollingElement.scrollWidth;\n var offset = window.scrollX - pageWidth;\n var minOffset = isRTL() ? -(documentWidth - pageWidth) : 0;\n return scrollToOffset(Math.max(offset, minOffset));\n} // Returns false if the page is already at the right-most scroll offset.\n\nfunction scrollRight() {\n var documentWidth = document.scrollingElement.scrollWidth;\n var offset = window.scrollX + pageWidth;\n var maxOffset = isRTL() ? 0 : documentWidth - pageWidth;\n return scrollToOffset(Math.min(offset, maxOffset));\n} // Scrolls to the given left offset.\n// Returns false if the page scroll position is already close enough to the given offset.\n\nfunction scrollToOffset(offset) {\n // Android.log("scrollToOffset " + offset);\n if (isScrollModeEnabled()) {\n throw "Called scrollToOffset() with scroll mode enabled. This can only be used in paginated mode.";\n }\n\n var currentOffset = window.scrollX;\n document.scrollingElement.scrollLeft = snapOffset(offset); // In some case the scrollX cannot reach the position respecting to innerWidth\n\n var diff = Math.abs(currentOffset - offset) / pageWidth;\n return diff > 0.01;\n} // Snap the offset to the screen width (page width).\n\n\nfunction snapOffset(offset) {\n var value = offset + (isRTL() ? -1 : 1);\n return value - value % pageWidth;\n} // Snaps the current offset to the page width.\n\n\nfunction snapCurrentOffset() {\n // Android.log("snapCurrentOffset");\n if (isScrollModeEnabled()) {\n return;\n }\n\n var currentOffset = window.scrollX; // Adds half a page to make sure we don\'t snap to the previous page.\n\n var factor = isRTL() ? -1 : 1;\n var delta = factor * (pageWidth / 2);\n document.scrollingElement.scrollLeft = snapOffset(currentOffset + delta);\n}\nfunction rangeFromLocator(locator) {\n try {\n var locations = locator.locations;\n var text = locator.text;\n\n if (text && text.highlight) {\n var root;\n\n if (locations && locations.cssSelector) {\n root = document.querySelector(locations.cssSelector);\n }\n\n if (!root) {\n root = document.body;\n }\n\n var anchor = new TextQuoteAnchor(root, text.highlight, {\n prefix: text.before,\n suffix: text.after\n });\n return anchor.toRange();\n }\n\n if (locations) {\n var element = null;\n\n if (!element && locations.cssSelector) {\n element = document.querySelector(locations.cssSelector);\n }\n\n if (!element && locations.fragments) {\n var _iterator = _createForOfIteratorHelper(locations.fragments),\n _step;\n\n try {\n for (_iterator.s(); !(_step = _iterator.n()).done;) {\n var htmlId = _step.value;\n element = document.getElementById(htmlId);\n\n if (element) {\n break;\n }\n }\n } catch (err) {\n _iterator.e(err);\n } finally {\n _iterator.f();\n }\n }\n\n if (element) {\n var range = document.createRange();\n range.setStartBefore(element);\n range.setEndAfter(element);\n return range;\n }\n }\n } catch (e) {\n logError(e);\n }\n\n return null;\n} /// User Settings.\n\nfunction setCSSProperties(properties) {\n for (var name in properties) {\n setProperty(name, properties[name]);\n }\n} // For setting user setting.\n\nfunction setProperty(key, value) {\n if (value === null || value === "") {\n removeProperty(key);\n } else {\n var root = document.documentElement; // The `!important` annotation is added with `setProperty()` because if it\'s part of the\n // `value`, it will be ignored by the Web View.\n\n root.style.setProperty(key, value, "important");\n }\n} // For removing user setting.\n\nfunction removeProperty(key) {\n var root = document.documentElement;\n root.style.removeProperty(key);\n} /// Toolkit\n\nfunction log() {\n var message = Array.prototype.slice.call(arguments).join(" ");\n Android.log(message);\n}\nfunction logError(message) {\n Android.logError(message, "", 0);\n}\n;// CONCATENATED MODULE: ./src/rect.js\nfunction _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }\n\nfunction rect_createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = rect_unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }\n\nfunction rect_unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return rect_arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return rect_arrayLikeToArray(o, minLen); }\n\nfunction rect_arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\n//\n// Copyright 2021 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n\nvar debug = false;\n/**\n * Converts a DOMRect into a JSON object understandable by the native side.\n */\n\nfunction toNativeRect(rect) {\n var pixelRatio = window.devicePixelRatio;\n var width = rect.width * pixelRatio;\n var height = rect.height * pixelRatio;\n var left = rect.left * pixelRatio;\n var top = rect.top * pixelRatio;\n var right = left + width;\n var bottom = top + height;\n return {\n width: width,\n height: height,\n left: left,\n top: top,\n right: right,\n bottom: bottom\n };\n}\nfunction getClientRectsNoOverlap(range, doNotMergeHorizontallyAlignedRects) {\n var clientRects = range.getClientRects();\n var tolerance = 1;\n var originalRects = [];\n\n var _iterator = rect_createForOfIteratorHelper(clientRects),\n _step;\n\n try {\n for (_iterator.s(); !(_step = _iterator.n()).done;) {\n var rangeClientRect = _step.value;\n originalRects.push({\n bottom: rangeClientRect.bottom,\n height: rangeClientRect.height,\n left: rangeClientRect.left,\n right: rangeClientRect.right,\n top: rangeClientRect.top,\n width: rangeClientRect.width\n });\n }\n } catch (err) {\n _iterator.e(err);\n } finally {\n _iterator.f();\n }\n\n var mergedRects = mergeTouchingRects(originalRects, tolerance, doNotMergeHorizontallyAlignedRects);\n var noContainedRects = removeContainedRects(mergedRects, tolerance);\n var newRects = replaceOverlapingRects(noContainedRects);\n var minArea = 2 * 2;\n\n for (var j = newRects.length - 1; j >= 0; j--) {\n var rect = newRects[j];\n var bigEnough = rect.width * rect.height > minArea;\n\n if (!bigEnough) {\n if (newRects.length > 1) {\n rect_log("CLIENT RECT: remove small");\n newRects.splice(j, 1);\n } else {\n rect_log("CLIENT RECT: remove small, but keep otherwise empty!");\n break;\n }\n }\n }\n\n rect_log("CLIENT RECT: reduced ".concat(originalRects.length, " --\x3e ").concat(newRects.length));\n return newRects;\n}\n\nfunction mergeTouchingRects(rects, tolerance, doNotMergeHorizontallyAlignedRects) {\n for (var i = 0; i < rects.length; i++) {\n var _loop = function _loop(j) {\n var rect1 = rects[i];\n var rect2 = rects[j];\n\n if (rect1 === rect2) {\n rect_log("mergeTouchingRects rect1 === rect2 ??!");\n return "continue";\n }\n\n var rectsLineUpVertically = almostEqual(rect1.top, rect2.top, tolerance) && almostEqual(rect1.bottom, rect2.bottom, tolerance);\n var rectsLineUpHorizontally = almostEqual(rect1.left, rect2.left, tolerance) && almostEqual(rect1.right, rect2.right, tolerance);\n var horizontalAllowed = !doNotMergeHorizontallyAlignedRects;\n var aligned = rectsLineUpHorizontally && horizontalAllowed || rectsLineUpVertically && !rectsLineUpHorizontally;\n var canMerge = aligned && rectsTouchOrOverlap(rect1, rect2, tolerance);\n\n if (canMerge) {\n rect_log("CLIENT RECT: merging two into one, VERTICAL: ".concat(rectsLineUpVertically, " HORIZONTAL: ").concat(rectsLineUpHorizontally, " (").concat(doNotMergeHorizontallyAlignedRects, ")"));\n var newRects = rects.filter(function (rect) {\n return rect !== rect1 && rect !== rect2;\n });\n var replacementClientRect = getBoundingRect(rect1, rect2);\n newRects.push(replacementClientRect);\n return {\n v: mergeTouchingRects(newRects, tolerance, doNotMergeHorizontallyAlignedRects)\n };\n }\n };\n\n for (var j = i + 1; j < rects.length; j++) {\n var _ret = _loop(j);\n\n if (_ret === "continue") continue;\n if (_typeof(_ret) === "object") return _ret.v;\n }\n }\n\n return rects;\n}\n\nfunction getBoundingRect(rect1, rect2) {\n var left = Math.min(rect1.left, rect2.left);\n var right = Math.max(rect1.right, rect2.right);\n var top = Math.min(rect1.top, rect2.top);\n var bottom = Math.max(rect1.bottom, rect2.bottom);\n return {\n bottom: bottom,\n height: bottom - top,\n left: left,\n right: right,\n top: top,\n width: right - left\n };\n}\n\nfunction removeContainedRects(rects, tolerance) {\n var rectsToKeep = new Set(rects);\n\n var _iterator2 = rect_createForOfIteratorHelper(rects),\n _step2;\n\n try {\n for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {\n var rect = _step2.value;\n var bigEnough = rect.width > 1 && rect.height > 1;\n\n if (!bigEnough) {\n rect_log("CLIENT RECT: remove tiny");\n rectsToKeep.delete(rect);\n continue;\n }\n\n var _iterator3 = rect_createForOfIteratorHelper(rects),\n _step3;\n\n try {\n for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {\n var possiblyContainingRect = _step3.value;\n\n if (rect === possiblyContainingRect) {\n continue;\n }\n\n if (!rectsToKeep.has(possiblyContainingRect)) {\n continue;\n }\n\n if (rectContains(possiblyContainingRect, rect, tolerance)) {\n rect_log("CLIENT RECT: remove contained");\n rectsToKeep.delete(rect);\n break;\n }\n }\n } catch (err) {\n _iterator3.e(err);\n } finally {\n _iterator3.f();\n }\n }\n } catch (err) {\n _iterator2.e(err);\n } finally {\n _iterator2.f();\n }\n\n return Array.from(rectsToKeep);\n}\n\nfunction rectContains(rect1, rect2, tolerance) {\n return rectContainsPoint(rect1, rect2.left, rect2.top, tolerance) && rectContainsPoint(rect1, rect2.right, rect2.top, tolerance) && rectContainsPoint(rect1, rect2.left, rect2.bottom, tolerance) && rectContainsPoint(rect1, rect2.right, rect2.bottom, tolerance);\n}\n\nfunction rectContainsPoint(rect, x, y, tolerance) {\n return (rect.left < x || almostEqual(rect.left, x, tolerance)) && (rect.right > x || almostEqual(rect.right, x, tolerance)) && (rect.top < y || almostEqual(rect.top, y, tolerance)) && (rect.bottom > y || almostEqual(rect.bottom, y, tolerance));\n}\n\nfunction replaceOverlapingRects(rects) {\n for (var i = 0; i < rects.length; i++) {\n for (var j = i + 1; j < rects.length; j++) {\n var rect1 = rects[i];\n var rect2 = rects[j];\n\n if (rect1 === rect2) {\n rect_log("replaceOverlapingRects rect1 === rect2 ??!");\n continue;\n }\n\n if (rectsTouchOrOverlap(rect1, rect2, -1)) {\n var _ret2 = function () {\n var toAdd = [];\n var toRemove = void 0;\n var subtractRects1 = rectSubtract(rect1, rect2);\n\n if (subtractRects1.length === 1) {\n toAdd = subtractRects1;\n toRemove = rect1;\n } else {\n var subtractRects2 = rectSubtract(rect2, rect1);\n\n if (subtractRects1.length < subtractRects2.length) {\n toAdd = subtractRects1;\n toRemove = rect1;\n } else {\n toAdd = subtractRects2;\n toRemove = rect2;\n }\n }\n\n rect_log("CLIENT RECT: overlap, cut one rect into ".concat(toAdd.length));\n var newRects = rects.filter(function (rect) {\n return rect !== toRemove;\n });\n Array.prototype.push.apply(newRects, toAdd);\n return {\n v: replaceOverlapingRects(newRects)\n };\n }();\n\n if (_typeof(_ret2) === "object") return _ret2.v;\n }\n }\n }\n\n return rects;\n}\n\nfunction rectSubtract(rect1, rect2) {\n var rectIntersected = rectIntersect(rect2, rect1);\n\n if (rectIntersected.height === 0 || rectIntersected.width === 0) {\n return [rect1];\n }\n\n var rects = [];\n {\n var rectA = {\n bottom: rect1.bottom,\n height: 0,\n left: rect1.left,\n right: rectIntersected.left,\n top: rect1.top,\n width: 0\n };\n rectA.width = rectA.right - rectA.left;\n rectA.height = rectA.bottom - rectA.top;\n\n if (rectA.height !== 0 && rectA.width !== 0) {\n rects.push(rectA);\n }\n }\n {\n var rectB = {\n bottom: rectIntersected.top,\n height: 0,\n left: rectIntersected.left,\n right: rectIntersected.right,\n top: rect1.top,\n width: 0\n };\n rectB.width = rectB.right - rectB.left;\n rectB.height = rectB.bottom - rectB.top;\n\n if (rectB.height !== 0 && rectB.width !== 0) {\n rects.push(rectB);\n }\n }\n {\n var rectC = {\n bottom: rect1.bottom,\n height: 0,\n left: rectIntersected.left,\n right: rectIntersected.right,\n top: rectIntersected.bottom,\n width: 0\n };\n rectC.width = rectC.right - rectC.left;\n rectC.height = rectC.bottom - rectC.top;\n\n if (rectC.height !== 0 && rectC.width !== 0) {\n rects.push(rectC);\n }\n }\n {\n var rectD = {\n bottom: rect1.bottom,\n height: 0,\n left: rectIntersected.right,\n right: rect1.right,\n top: rect1.top,\n width: 0\n };\n rectD.width = rectD.right - rectD.left;\n rectD.height = rectD.bottom - rectD.top;\n\n if (rectD.height !== 0 && rectD.width !== 0) {\n rects.push(rectD);\n }\n }\n return rects;\n}\n\nfunction rectIntersect(rect1, rect2) {\n var maxLeft = Math.max(rect1.left, rect2.left);\n var minRight = Math.min(rect1.right, rect2.right);\n var maxTop = Math.max(rect1.top, rect2.top);\n var minBottom = Math.min(rect1.bottom, rect2.bottom);\n return {\n bottom: minBottom,\n height: Math.max(0, minBottom - maxTop),\n left: maxLeft,\n right: minRight,\n top: maxTop,\n width: Math.max(0, minRight - maxLeft)\n };\n}\n\nfunction rectsTouchOrOverlap(rect1, rect2, tolerance) {\n return (rect1.left < rect2.right || tolerance >= 0 && almostEqual(rect1.left, rect2.right, tolerance)) && (rect2.left < rect1.right || tolerance >= 0 && almostEqual(rect2.left, rect1.right, tolerance)) && (rect1.top < rect2.bottom || tolerance >= 0 && almostEqual(rect1.top, rect2.bottom, tolerance)) && (rect2.top < rect1.bottom || tolerance >= 0 && almostEqual(rect2.top, rect1.bottom, tolerance));\n}\n\nfunction almostEqual(a, b, tolerance) {\n return Math.abs(a - b) <= tolerance;\n}\n\nfunction rect_log() {\n if (debug) {\n log.apply(null, arguments);\n }\n}\n;// CONCATENATED MODULE: ./src/decorator.js\nfunction decorator_createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = decorator_unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }\n\nfunction decorator_slicedToArray(arr, i) { return decorator_arrayWithHoles(arr) || decorator_iterableToArrayLimit(arr, i) || decorator_unsupportedIterableToArray(arr, i) || decorator_nonIterableRest(); }\n\nfunction decorator_nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }\n\nfunction decorator_unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return decorator_arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return decorator_arrayLikeToArray(o, minLen); }\n\nfunction decorator_arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\nfunction decorator_iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }\n\nfunction decorator_arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }\n\n//\n// Copyright 2021 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n\n\nvar styles = new Map();\nvar groups = new Map();\nvar lastGroupId = 0;\n/**\n * Registers a list of additional supported Decoration Templates.\n *\n * Each template object is indexed by the style ID.\n */\n\nfunction registerTemplates(newStyles) {\n var stylesheet = "";\n\n for (var _i = 0, _Object$entries = Object.entries(newStyles); _i < _Object$entries.length; _i++) {\n var _Object$entries$_i = decorator_slicedToArray(_Object$entries[_i], 2),\n id = _Object$entries$_i[0],\n style = _Object$entries$_i[1];\n\n styles.set(id, style);\n\n if (style.stylesheet) {\n stylesheet += style.stylesheet + "\\n";\n }\n }\n\n if (stylesheet) {\n var styleElement = document.createElement("style");\n styleElement.innerHTML = stylesheet;\n document.getElementsByTagName("head")[0].appendChild(styleElement);\n }\n}\n/**\n * Returns an instance of DecorationGroup for the given group name.\n */\n\nfunction getDecorations(groupName) {\n var group = groups.get(groupName);\n\n if (!group) {\n var id = "r2-decoration-" + lastGroupId++;\n group = DecorationGroup(id, groupName);\n groups.set(groupName, group);\n }\n\n return group;\n}\n/**\n * Handles click events on a Decoration.\n * Returns whether a decoration matched this event.\n */\n\nfunction handleDecorationClickEvent(event, clickEvent) {\n if (groups.size === 0) {\n return false;\n }\n\n function findTarget() {\n var _iterator = decorator_createForOfIteratorHelper(groups),\n _step;\n\n try {\n for (_iterator.s(); !(_step = _iterator.n()).done;) {\n var _step$value = decorator_slicedToArray(_step.value, 2),\n group = _step$value[0],\n groupContent = _step$value[1];\n\n var _iterator2 = decorator_createForOfIteratorHelper(groupContent.items.reverse()),\n _step2;\n\n try {\n for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {\n var item = _step2.value;\n\n if (!item.clickableElements) {\n continue;\n }\n\n var _iterator3 = decorator_createForOfIteratorHelper(item.clickableElements),\n _step3;\n\n try {\n for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {\n var element = _step3.value;\n var rect = element.getBoundingClientRect().toJSON();\n\n if (rectContainsPoint(rect, event.clientX, event.clientY, 1)) {\n return {\n group: group,\n item: item,\n element: element,\n rect: rect\n };\n }\n }\n } catch (err) {\n _iterator3.e(err);\n } finally {\n _iterator3.f();\n }\n }\n } catch (err) {\n _iterator2.e(err);\n } finally {\n _iterator2.f();\n }\n }\n } catch (err) {\n _iterator.e(err);\n } finally {\n _iterator.f();\n }\n }\n\n var target = findTarget();\n\n if (!target) {\n return false;\n }\n\n return Android.onDecorationActivated(JSON.stringify({\n id: target.item.decoration.id,\n group: target.group,\n rect: toNativeRect(target.item.range.getBoundingClientRect()),\n click: clickEvent\n }));\n}\n/**\n * Creates a DecorationGroup object from a unique HTML ID and its name.\n */\n\nfunction DecorationGroup(groupId, groupName) {\n var items = [];\n var lastItemId = 0;\n var container = null;\n /**\n * Adds a new decoration to the group.\n */\n\n function add(decoration) {\n var id = groupId + "-" + lastItemId++;\n var range = rangeFromLocator(decoration.locator);\n\n if (!range) {\n log("Can\'t locate DOM range for decoration", decoration);\n return;\n }\n\n var item = {\n id: id,\n decoration: decoration,\n range: range\n };\n items.push(item);\n layout(item);\n }\n /**\n * Removes the decoration with given ID from the group.\n */\n\n\n function remove(decorationId) {\n var index = items.findIndex(function (i) {\n return i.decoration.id === decorationId;\n });\n\n if (index === -1) {\n return;\n }\n\n var item = items[index];\n items.splice(index, 1);\n item.clickableElements = null;\n\n if (item.container) {\n item.container.remove();\n item.container = null;\n }\n }\n /**\n * Notifies that the given decoration was modified and needs to be updated.\n */\n\n\n function update(decoration) {\n remove(decoration.id);\n add(decoration);\n }\n /**\n * Removes all decorations from this group.\n */\n\n\n function clear() {\n clearContainer();\n items.length = 0;\n }\n /**\n * Recreates the decoration elements.\n *\n * To be called after reflowing the resource, for example.\n */\n\n\n function requestLayout() {\n clearContainer();\n items.forEach(function (item) {\n return layout(item);\n });\n }\n /**\n * Layouts a single Decoration item.\n */\n\n\n function layout(item) {\n var groupContainer = requireContainer();\n var style = styles.get(item.decoration.style);\n\n if (!style) {\n logError("Unknown decoration style: ".concat(item.decoration.style));\n return;\n }\n\n var itemContainer = document.createElement("div");\n itemContainer.setAttribute("id", item.id);\n itemContainer.setAttribute("data-style", item.decoration.style);\n itemContainer.style.setProperty("pointer-events", "none");\n var viewportWidth = window.innerWidth;\n var columnCount = parseInt(getComputedStyle(document.documentElement).getPropertyValue("column-count"));\n var pageWidth = viewportWidth / (columnCount || 1);\n var scrollingElement = document.scrollingElement;\n var xOffset = scrollingElement.scrollLeft;\n var yOffset = scrollingElement.scrollTop;\n\n function positionElement(element, rect, boundingRect) {\n element.style.position = "absolute";\n\n if (style.width === "wrap") {\n element.style.width = "".concat(rect.width, "px");\n element.style.height = "".concat(rect.height, "px");\n element.style.left = "".concat(rect.left + xOffset, "px");\n element.style.top = "".concat(rect.top + yOffset, "px");\n } else if (style.width === "viewport") {\n element.style.width = "".concat(viewportWidth, "px");\n element.style.height = "".concat(rect.height, "px");\n var left = Math.floor(rect.left / viewportWidth) * viewportWidth;\n element.style.left = "".concat(left + xOffset, "px");\n element.style.top = "".concat(rect.top + yOffset, "px");\n } else if (style.width === "bounds") {\n element.style.width = "".concat(boundingRect.width, "px");\n element.style.height = "".concat(rect.height, "px");\n element.style.left = "".concat(boundingRect.left + xOffset, "px");\n element.style.top = "".concat(rect.top + yOffset, "px");\n } else if (style.width === "page") {\n element.style.width = "".concat(pageWidth, "px");\n element.style.height = "".concat(rect.height, "px");\n\n var _left = Math.floor(rect.left / pageWidth) * pageWidth;\n\n element.style.left = "".concat(_left + xOffset, "px");\n element.style.top = "".concat(rect.top + yOffset, "px");\n }\n }\n\n var boundingRect = item.range.getBoundingClientRect();\n var elementTemplate;\n\n try {\n var template = document.createElement("template");\n template.innerHTML = item.decoration.element.trim();\n elementTemplate = template.content.firstElementChild;\n } catch (error) {\n logError("Invalid decoration element \\"".concat(item.decoration.element, "\\": ").concat(error.message));\n return;\n }\n\n if (style.layout === "boxes") {\n var doNotMergeHorizontallyAlignedRects = true;\n var clientRects = getClientRectsNoOverlap(item.range, doNotMergeHorizontallyAlignedRects);\n clientRects = clientRects.sort(function (r1, r2) {\n if (r1.top < r2.top) {\n return -1;\n } else if (r1.top > r2.top) {\n return 1;\n } else {\n return 0;\n }\n });\n\n var _iterator4 = decorator_createForOfIteratorHelper(clientRects),\n _step4;\n\n try {\n for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {\n var clientRect = _step4.value;\n var line = elementTemplate.cloneNode(true);\n line.style.setProperty("pointer-events", "none");\n positionElement(line, clientRect, boundingRect);\n itemContainer.append(line);\n }\n } catch (err) {\n _iterator4.e(err);\n } finally {\n _iterator4.f();\n }\n } else if (style.layout === "bounds") {\n var bounds = elementTemplate.cloneNode(true);\n bounds.style.setProperty("pointer-events", "none");\n positionElement(bounds, boundingRect, boundingRect);\n itemContainer.append(bounds);\n }\n\n groupContainer.append(itemContainer);\n item.container = itemContainer;\n item.clickableElements = Array.from(itemContainer.querySelectorAll("[data-activable=\'1\']"));\n\n if (item.clickableElements.length === 0) {\n item.clickableElements = Array.from(itemContainer.children);\n }\n }\n /**\n * Returns the group container element, after making sure it exists.\n */\n\n\n function requireContainer() {\n if (!container) {\n container = document.createElement("div");\n container.setAttribute("id", groupId);\n container.setAttribute("data-group", groupName);\n container.style.setProperty("pointer-events", "none");\n document.body.append(container);\n }\n\n return container;\n }\n /**\n * Removes the group container.\n */\n\n\n function clearContainer() {\n if (container) {\n container.remove();\n container = null;\n }\n }\n\n return {\n add: add,\n remove: remove,\n update: update,\n clear: clear,\n items: items,\n requestLayout: requestLayout\n };\n}\nwindow.addEventListener("load", function () {\n // Will relayout all the decorations when the document body is resized.\n var body = document.body;\n var lastSize = {\n width: 0,\n height: 0\n };\n var observer = new ResizeObserver(function () {\n if (lastSize.width === body.clientWidth && lastSize.height === body.clientHeight) {\n return;\n }\n\n lastSize = {\n width: body.clientWidth,\n height: body.clientHeight\n };\n groups.forEach(function (group) {\n group.requestLayout();\n });\n });\n observer.observe(body);\n}, false);\n;// CONCATENATED MODULE: ./src/gestures.js\n/*\n * Copyright 2021 Readium Foundation. All rights reserved.\n * Use of this source code is governed by the BSD-style license\n * available in the top-level LICENSE file of the project.\n */\n\nwindow.addEventListener("DOMContentLoaded", function () {\n document.addEventListener("click", onClick, false);\n bindDragGesture(document);\n});\n\nfunction onClick(event) {\n if (!window.getSelection().isCollapsed) {\n // There\'s an on-going selection, the tap will dismiss it so we don\'t forward it.\n return;\n }\n\n var pixelRatio = window.devicePixelRatio;\n var clickEvent = {\n defaultPrevented: event.defaultPrevented,\n x: event.clientX * pixelRatio,\n y: event.clientY * pixelRatio,\n targetElement: event.target.outerHTML,\n interactiveElement: nearestInteractiveElement(event.target)\n };\n\n if (handleDecorationClickEvent(event, clickEvent)) {\n return;\n } // Send the tap data over the JS bridge even if it\'s been handled within the web view, so that\n // it can be preserved and used by the toolkit if needed.\n\n\n var shouldPreventDefault = Android.onTap(JSON.stringify(clickEvent));\n\n if (shouldPreventDefault) {\n event.stopPropagation();\n event.preventDefault();\n }\n}\n\nfunction bindDragGesture(element) {\n // passive: false is necessary to be able to prevent the default behavior.\n element.addEventListener("touchstart", onStart, {\n passive: false\n });\n element.addEventListener("touchend", onEnd, {\n passive: false\n });\n element.addEventListener("touchmove", onMove, {\n passive: false\n });\n var state = undefined;\n var isStartingDrag = false;\n var pixelRatio = window.devicePixelRatio;\n\n function onStart(event) {\n isStartingDrag = true;\n var startX = event.touches[0].clientX * pixelRatio;\n var startY = event.touches[0].clientY * pixelRatio;\n state = {\n defaultPrevented: event.defaultPrevented,\n startX: startX,\n startY: startY,\n currentX: startX,\n currentY: startY,\n offsetX: 0,\n offsetY: 0,\n interactiveElement: nearestInteractiveElement(event.target)\n };\n }\n\n function onMove(event) {\n if (!state) return;\n state.currentX = event.touches[0].clientX * pixelRatio;\n state.currentY = event.touches[0].clientY * pixelRatio;\n state.offsetX = state.currentX - state.startX;\n state.offsetY = state.currentY - state.startY;\n var shouldPreventDefault = false; // Wait for a movement of at least 6 pixels before reporting a drag.\n\n if (isStartingDrag) {\n if (Math.abs(state.offsetX) >= 6 || Math.abs(state.offsetY) >= 6) {\n isStartingDrag = false;\n shouldPreventDefault = Android.onDragStart(JSON.stringify(state));\n }\n } else {\n shouldPreventDefault = Android.onDragMove(JSON.stringify(state));\n }\n\n if (shouldPreventDefault) {\n event.stopPropagation();\n event.preventDefault();\n }\n }\n\n function onEnd(event) {\n if (!state) return;\n var shouldPreventDefault = Android.onDragEnd(JSON.stringify(state));\n\n if (shouldPreventDefault) {\n event.stopPropagation();\n event.preventDefault();\n }\n\n state = undefined;\n }\n} // See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling\n\n\nfunction nearestInteractiveElement(element) {\n var interactiveTags = ["a", "audio", "button", "canvas", "details", "input", "label", "option", "select", "submit", "textarea", "video"];\n\n if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) {\n return element.outerHTML;\n } // Checks whether the element is editable by the user.\n\n\n if (element.hasAttribute("contenteditable") && element.getAttribute("contenteditable").toLowerCase() != "false") {\n return element.outerHTML;\n } // Checks parents recursively because the touch might be for example on an <em> inside a <a>.\n\n\n if (element.parentElement) {\n return nearestInteractiveElement(element.parentElement);\n }\n\n return null;\n}\n;// CONCATENATED MODULE: ./src/highlight.js\nfunction highlight_typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { highlight_typeof = function _typeof(obj) { return typeof obj; }; } else { highlight_typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return highlight_typeof(obj); }\n\nfunction highlight_createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = highlight_unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }\n\nfunction highlight_unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return highlight_arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return highlight_arrayLikeToArray(o, minLen); }\n\nfunction highlight_arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\n/* eslint-disable */\n//\n// highlight.js\n// r2-navigator-kotlin\n//\n// Organized by Taehyun Kim on 6/27/19 from r2-navigator-js.\n//\n// Copyright 2019 Readium Foundation. All rights reserved.\n// Use of this source code is governed by a BSD-style license which is detailed\n// in the LICENSE file present in the project repository where this source code is maintained.\n//\nvar ROOT_CLASS_REDUCE_MOTION = "r2-reduce-motion";\nvar ROOT_CLASS_NO_FOOTNOTES = "r2-no-popup-foonotes";\nvar POPUP_DIALOG_CLASS = "r2-popup-dialog";\nvar FOOTNOTES_CONTAINER_CLASS = "r2-footnote-container";\nvar FOOTNOTES_CLOSE_BUTTON_CLASS = "r2-footnote-close";\nvar FOOTNOTE_FORCE_SHOW = "r2-footnote-force-show";\nvar TTS_ID_PREVIOUS = "r2-tts-previous";\nvar TTS_ID_NEXT = "r2-tts-next";\nvar TTS_ID_SLIDER = "r2-tts-slider";\nvar TTS_ID_ACTIVE_WORD = "r2-tts-active-word";\nvar TTS_ID_CONTAINER = "r2-tts-txt";\nvar TTS_ID_INFO = "r2-tts-info";\nvar TTS_NAV_BUTTON_CLASS = "r2-tts-button";\nvar TTS_ID_SPEAKING_DOC_ELEMENT = "r2-tts-speaking-el";\nvar TTS_CLASS_INJECTED_SPAN = "r2-tts-speaking-txt";\nvar TTS_CLASS_INJECTED_SUBSPAN = "r2-tts-speaking-word";\nvar TTS_ID_INJECTED_PARENT = "r2-tts-speaking-txt-parent";\nvar ID_HIGHLIGHTS_CONTAINER = "R2_ID_HIGHLIGHTS_CONTAINER";\nvar ID_ANNOTATION_CONTAINER = "R2_ID_ANNOTATION_CONTAINER";\nvar CLASS_HIGHLIGHT_CONTAINER = "R2_CLASS_HIGHLIGHT_CONTAINER";\nvar CLASS_ANNOTATION_CONTAINER = "R2_CLASS_ANNOTATION_CONTAINER";\nvar CLASS_HIGHLIGHT_AREA = "R2_CLASS_HIGHLIGHT_AREA";\nvar CLASS_ANNOTATION_AREA = "R2_CLASS_ANNOTATION_AREA";\nvar CLASS_HIGHLIGHT_BOUNDING_AREA = "R2_CLASS_HIGHLIGHT_BOUNDING_AREA";\nvar CLASS_ANNOTATION_BOUNDING_AREA = "R2_CLASS_ANNOTATION_BOUNDING_AREA"; // tslint:disable-next-line:max-line-length\n\nvar _blacklistIdClassForCFI = [POPUP_DIALOG_CLASS, TTS_CLASS_INJECTED_SPAN, TTS_CLASS_INJECTED_SUBSPAN, ID_HIGHLIGHTS_CONTAINER, CLASS_HIGHLIGHT_CONTAINER, CLASS_HIGHLIGHT_AREA, CLASS_HIGHLIGHT_BOUNDING_AREA, "resize-sensor"];\nvar CLASS_PAGINATED = "r2-css-paginated"; //const IS_DEV = (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "dev");\n\nvar IS_DEV = false;\nvar _highlights = [];\n\nvar _highlightsContainer;\n\nvar _annotationContainer;\n\nvar lastMouseDownX = -1;\nvar lastMouseDownY = -1;\nvar bodyEventListenersSet = false;\nvar USE_SVG = false;\nvar DEFAULT_BACKGROUND_COLOR_OPACITY = 0.3;\nvar ALT_BACKGROUND_COLOR_OPACITY = 0.45; //const DEBUG_VISUALS = false;\n\nvar DEBUG_VISUALS = false;\nvar DEFAULT_BACKGROUND_COLOR = {\n blue: 100,\n green: 50,\n red: 230\n};\nvar ANNOTATION_WIDTH = 15;\n\nfunction resetHighlightBoundingStyle(_win, highlightBounding) {\n if (highlightBounding.getAttribute("class") == CLASS_ANNOTATION_BOUNDING_AREA) {\n return;\n }\n\n highlightBounding.style.outline = "none";\n highlightBounding.style.setProperty("background-color", "transparent", "important");\n}\n\nfunction setHighlightAreaStyle(win, highlightAreas, highlight) {\n var useSVG = !DEBUG_VISUALS && USE_SVG;\n\n var _iterator = highlight_createForOfIteratorHelper(highlightAreas),\n _step;\n\n try {\n for (_iterator.s(); !(_step = _iterator.n()).done;) {\n var highlightArea = _step.value;\n var isSVG = useSVG && highlightArea.namespaceURI === SVG_XML_NAMESPACE;\n var opacity = ALT_BACKGROUND_COLOR_OPACITY;\n\n if (isSVG) {\n highlightArea.style.setProperty("fill", "rgb(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ")"), "important");\n highlightArea.style.setProperty("fill-opacity", "".concat(opacity), "important");\n highlightArea.style.setProperty("stroke", "rgb(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ")"), "important");\n highlightArea.style.setProperty("stroke-opacity", "".concat(opacity), "important");\n } else {\n highlightArea.style.setProperty("background-color", "rgba(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ", ").concat(opacity, ")"), "important");\n }\n }\n } catch (err) {\n _iterator.e(err);\n } finally {\n _iterator.f();\n }\n}\n\nfunction resetHighlightAreaStyle(win, highlightArea) {\n var useSVG = !DEBUG_VISUALS && USE_SVG; //const useSVG = USE_SVG;\n\n var isSVG = useSVG && highlightArea.namespaceURI === SVG_XML_NAMESPACE;\n var id = isSVG ? highlightArea.parentNode && highlightArea.parentNode.parentNode && highlightArea.parentNode.parentNode.nodeType === Node.ELEMENT_NODE && highlightArea.parentNode.parentNode.getAttribute ? highlightArea.parentNode.parentNode.getAttribute("id") : undefined : highlightArea.parentNode && highlightArea.parentNode.nodeType === Node.ELEMENT_NODE && highlightArea.parentNode.getAttribute ? highlightArea.parentNode.getAttribute("id") : undefined;\n\n if (id) {\n var highlight = _highlights.find(function (h) {\n return h.id === id;\n });\n\n if (highlight) {\n var opacity = DEFAULT_BACKGROUND_COLOR_OPACITY;\n\n if (isSVG) {\n highlightArea.style.setProperty("fill", "rgb(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ")"), "important");\n highlightArea.style.setProperty("fill-opacity", "".concat(opacity), "important");\n highlightArea.style.setProperty("stroke", "rgb(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ")"), "important");\n highlightArea.style.setProperty("stroke-opacity", "".concat(opacity), "important");\n } else {\n highlightArea.style.setProperty("background-color", "rgba(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ", ").concat(opacity, ")"), "important");\n }\n }\n }\n}\n\nfunction processTouchEvent(win, ev) {\n var document = win.document;\n var scrollElement = getScrollingElement(document);\n var x = ev.changedTouches[0].clientX;\n var y = ev.changedTouches[0].clientY;\n\n if (!_highlightsContainer) {\n return;\n }\n\n var paginated = isPaginated(document);\n var bodyRect = document.body.getBoundingClientRect();\n var xOffset;\n var yOffset;\n\n if (navigator.userAgent.match(/Android/i)) {\n xOffset = paginated ? -scrollElement.scrollLeft : bodyRect.left;\n yOffset = paginated ? -scrollElement.scrollTop : bodyRect.top;\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n xOffset = paginated ? 0 : -scrollElement.scrollLeft;\n yOffset = paginated ? 0 : bodyRect.top;\n }\n\n var foundHighlight;\n var foundElement;\n var foundRect; // _highlights.sort(function(a, b) {\n // console.log(JSON.stringify(a.selectionInfo))\n // return a.selectionInfo.cleanText.length < b.selectionInfo.cleanText.length\n // })\n\n for (var i = _highlights.length - 1; i >= 0; i--) {\n var highlight = _highlights[i];\n var highlightParent = document.getElementById("".concat(highlight.id));\n\n if (!highlightParent) {\n highlightParent = _highlightsContainer.querySelector("#".concat(highlight.id));\n }\n\n if (!highlightParent) {\n continue;\n }\n\n var hit = false;\n var highlightFragments = highlightParent.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA));\n\n var _iterator2 = highlight_createForOfIteratorHelper(highlightFragments),\n _step2;\n\n try {\n for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {\n var highlightFragment = _step2.value;\n var withRect = highlightFragment;\n var left = withRect.rect.left + xOffset;\n var top = withRect.rect.top + yOffset;\n foundRect = withRect.rect;\n\n if (x >= left && x < left + withRect.rect.width && y >= top && y < top + withRect.rect.height) {\n hit = true;\n break;\n }\n }\n } catch (err) {\n _iterator2.e(err);\n } finally {\n _iterator2.f();\n }\n\n if (hit) {\n foundHighlight = highlight;\n foundElement = highlightParent;\n break;\n }\n }\n\n if (!foundHighlight || !foundElement) {\n var highlightBoundings = _highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_BOUNDING_AREA));\n\n var _iterator3 = highlight_createForOfIteratorHelper(highlightBoundings),\n _step3;\n\n try {\n for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {\n var highlightBounding = _step3.value;\n resetHighlightBoundingStyle(win, highlightBounding);\n }\n } catch (err) {\n _iterator3.e(err);\n } finally {\n _iterator3.f();\n }\n\n var allHighlightAreas = Array.from(_highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA)));\n\n for (var _i = 0, _allHighlightAreas = allHighlightAreas; _i < _allHighlightAreas.length; _i++) {\n var highlightArea = _allHighlightAreas[_i];\n resetHighlightAreaStyle(win, highlightArea);\n }\n\n return;\n }\n\n if (foundElement.getAttribute("data-click")) {\n if (ev.type === "mousemove") {\n var foundElementHighlightAreas = Array.from(foundElement.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA)));\n\n var _allHighlightAreas2 = _highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA));\n\n var _iterator4 = highlight_createForOfIteratorHelper(_allHighlightAreas2),\n _step4;\n\n try {\n for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {\n var _highlightArea = _step4.value;\n\n if (foundElementHighlightAreas.indexOf(_highlightArea) < 0) {\n resetHighlightAreaStyle(win, _highlightArea);\n }\n }\n } catch (err) {\n _iterator4.e(err);\n } finally {\n _iterator4.f();\n }\n\n setHighlightAreaStyle(win, foundElementHighlightAreas, foundHighlight);\n var foundElementHighlightBounding = foundElement.querySelector(".".concat(CLASS_HIGHLIGHT_BOUNDING_AREA));\n\n var allHighlightBoundings = _highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_BOUNDING_AREA));\n\n var _iterator5 = highlight_createForOfIteratorHelper(allHighlightBoundings),\n _step5;\n\n try {\n for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {\n var _highlightBounding = _step5.value;\n\n if (!foundElementHighlightBounding || _highlightBounding !== foundElementHighlightBounding) {\n resetHighlightBoundingStyle(win, _highlightBounding);\n }\n }\n } catch (err) {\n _iterator5.e(err);\n } finally {\n _iterator5.f();\n }\n\n if (foundElementHighlightBounding) {\n if (DEBUG_VISUALS) {\n setHighlightBoundingStyle(win, foundElementHighlightBounding, foundHighlight);\n }\n }\n } else if (ev.type === "touchstart" || ev.type === "touchend") {\n var size = {\n screenWidth: window.outerWidth,\n screenHeight: window.outerHeight,\n left: foundRect.left,\n width: foundRect.width,\n top: foundRect.top,\n height: foundRect.height\n };\n var payload = {\n highlight: foundHighlight.id,\n size: size\n };\n\n if (typeof window !== "undefined" && highlight_typeof(window.process) === "object" && window.process.type === "renderer") {\n electron_1.ipcRenderer.sendToHost(R2_EVENT_HIGHLIGHT_CLICK, payload);\n } else if (window.webkitURL) {\n console.log(foundHighlight.id.includes("R2_ANNOTATION_"));\n\n if (foundHighlight.id.search("R2_ANNOTATION_") >= 0) {\n if (navigator.userAgent.match(/Android/i)) {\n Android.highlightAnnotationMarkActivated(foundHighlight.id);\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n webkit.messageHandlers.highlightAnnotationMarkActivated.postMessage(foundHighlight.id);\n }\n } else if (foundHighlight.id.search("R2_HIGHLIGHT_") >= 0) {\n if (navigator.userAgent.match(/Android/i)) {\n Android.highlightActivated(foundHighlight.id);\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n webkit.messageHandlers.highlightActivated.postMessage(foundHighlight.id);\n }\n }\n }\n\n ev.stopPropagation();\n ev.preventDefault();\n }\n }\n}\n\nfunction processMouseEvent(win, ev) {\n var document = win.document;\n var scrollElement = getScrollingElement(document);\n var x = ev.clientX;\n var y = ev.clientY;\n\n if (!_highlightsContainer) {\n return;\n }\n\n var paginated = isPaginated(document);\n var bodyRect = document.body.getBoundingClientRect();\n var xOffset;\n var yOffset;\n\n if (navigator.userAgent.match(/Android/i)) {\n xOffset = paginated ? -scrollElement.scrollLeft : bodyRect.left;\n yOffset = paginated ? -scrollElement.scrollTop : bodyRect.top;\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n xOffset = paginated ? 0 : -scrollElement.scrollLeft;\n yOffset = paginated ? 0 : bodyRect.top;\n }\n\n var foundHighlight;\n var foundElement;\n var foundRect;\n\n for (var i = _highlights.length - 1; i >= 0; i--) {\n var highlight = _highlights[i];\n var highlightParent = document.getElementById("".concat(highlight.id));\n\n if (!highlightParent) {\n highlightParent = _highlightsContainer.querySelector("#".concat(highlight.id));\n }\n\n if (!highlightParent) {\n continue;\n }\n\n var hit = false;\n var highlightFragments = highlightParent.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA));\n\n var _iterator6 = highlight_createForOfIteratorHelper(highlightFragments),\n _step6;\n\n try {\n for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {\n var highlightFragment = _step6.value;\n var withRect = highlightFragment;\n var left = withRect.rect.left + xOffset;\n var top = withRect.rect.top + yOffset;\n foundRect = withRect.rect;\n\n if (x >= left && x < left + withRect.rect.width && y >= top && y < top + withRect.rect.height) {\n hit = true;\n break;\n }\n }\n } catch (err) {\n _iterator6.e(err);\n } finally {\n _iterator6.f();\n }\n\n if (hit) {\n foundHighlight = highlight;\n foundElement = highlightParent;\n break;\n }\n }\n\n if (!foundHighlight || !foundElement) {\n var highlightBoundings = _highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_BOUNDING_AREA));\n\n var _iterator7 = highlight_createForOfIteratorHelper(highlightBoundings),\n _step7;\n\n try {\n for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {\n var highlightBounding = _step7.value;\n resetHighlightBoundingStyle(win, highlightBounding);\n }\n } catch (err) {\n _iterator7.e(err);\n } finally {\n _iterator7.f();\n }\n\n var allHighlightAreas = Array.from(_highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA)));\n\n for (var _i2 = 0, _allHighlightAreas3 = allHighlightAreas; _i2 < _allHighlightAreas3.length; _i2++) {\n var highlightArea = _allHighlightAreas3[_i2];\n resetHighlightAreaStyle(win, highlightArea);\n }\n\n return;\n }\n\n if (foundElement.getAttribute("data-click")) {\n if (ev.type === "mousemove") {\n var foundElementHighlightAreas = Array.from(foundElement.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA)));\n\n var _allHighlightAreas4 = _highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_AREA));\n\n var _iterator8 = highlight_createForOfIteratorHelper(_allHighlightAreas4),\n _step8;\n\n try {\n for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {\n var _highlightArea2 = _step8.value;\n\n if (foundElementHighlightAreas.indexOf(_highlightArea2) < 0) {\n resetHighlightAreaStyle(win, _highlightArea2);\n }\n }\n } catch (err) {\n _iterator8.e(err);\n } finally {\n _iterator8.f();\n }\n\n setHighlightAreaStyle(win, foundElementHighlightAreas, foundHighlight);\n var foundElementHighlightBounding = foundElement.querySelector(".".concat(CLASS_HIGHLIGHT_BOUNDING_AREA));\n\n var allHighlightBoundings = _highlightsContainer.querySelectorAll(".".concat(CLASS_HIGHLIGHT_BOUNDING_AREA));\n\n var _iterator9 = highlight_createForOfIteratorHelper(allHighlightBoundings),\n _step9;\n\n try {\n for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {\n var _highlightBounding2 = _step9.value;\n\n if (!foundElementHighlightBounding || _highlightBounding2 !== foundElementHighlightBounding) {\n resetHighlightBoundingStyle(win, _highlightBounding2);\n }\n }\n } catch (err) {\n _iterator9.e(err);\n } finally {\n _iterator9.f();\n }\n\n if (foundElementHighlightBounding) {\n if (DEBUG_VISUALS) {\n setHighlightBoundingStyle(win, foundElementHighlightBounding, foundHighlight);\n }\n }\n } else if (ev.type === "mouseup" || ev.type === "touchend") {\n var touchedPosition = {\n screenWidth: window.outerWidth,\n screenHeight: window.innerHeight,\n left: foundRect.left,\n width: foundRect.width,\n top: foundRect.top,\n height: foundRect.height\n };\n var payload = {\n highlight: foundHighlight,\n position: touchedPosition\n };\n\n if (typeof window !== "undefined" && highlight_typeof(window.process) === "object" && window.process.type === "renderer") {\n electron_1.ipcRenderer.sendToHost(R2_EVENT_HIGHLIGHT_CLICK, payload);\n } else if (window.webkitURL) {\n if (foundHighlight.id.search("R2_ANNOTATION_") >= 0) {\n if (navigator.userAgent.match(/Android/i)) {\n Android.highlightAnnotationMarkActivated(foundHighlight.id);\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n webkit.messageHandlers.highlightAnnotationMarkActivated.postMessage(foundHighlight.id);\n }\n } else if (foundHighlight.id.search("R2_HIGHLIGHT_") >= 0) {\n if (navigator.userAgent.match(/Android/i)) {\n Android.highlightActivated(foundHighlight.id);\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n webkit.messageHandlers.highlightActivated.postMessage(foundHighlight.id);\n }\n }\n }\n\n ev.stopPropagation();\n }\n }\n}\n\nfunction highlight_rectsTouchOrOverlap(rect1, rect2, tolerance) {\n return (rect1.left < rect2.right || tolerance >= 0 && highlight_almostEqual(rect1.left, rect2.right, tolerance)) && (rect2.left < rect1.right || tolerance >= 0 && highlight_almostEqual(rect2.left, rect1.right, tolerance)) && (rect1.top < rect2.bottom || tolerance >= 0 && highlight_almostEqual(rect1.top, rect2.bottom, tolerance)) && (rect2.top < rect1.bottom || tolerance >= 0 && highlight_almostEqual(rect2.top, rect1.bottom, tolerance));\n}\n\nfunction highlight_replaceOverlapingRects(rects) {\n for (var i = 0; i < rects.length; i++) {\n for (var j = i + 1; j < rects.length; j++) {\n var rect1 = rects[i];\n var rect2 = rects[j];\n\n if (rect1 === rect2) {\n if (IS_DEV) {\n console.log("replaceOverlapingRects rect1 === rect2 ??!");\n }\n\n continue;\n }\n\n if (highlight_rectsTouchOrOverlap(rect1, rect2, -1)) {\n var _ret = function () {\n var toAdd = [];\n var toRemove = void 0;\n var toPreserve = void 0;\n var subtractRects1 = highlight_rectSubtract(rect1, rect2);\n\n if (subtractRects1.length === 1) {\n toAdd = subtractRects1;\n toRemove = rect1;\n toPreserve = rect2;\n } else {\n var subtractRects2 = highlight_rectSubtract(rect2, rect1);\n\n if (subtractRects1.length < subtractRects2.length) {\n toAdd = subtractRects1;\n toRemove = rect1;\n toPreserve = rect2;\n } else {\n toAdd = subtractRects2;\n toRemove = rect2;\n toPreserve = rect1;\n }\n }\n\n if (IS_DEV) {\n var toCheck = [];\n toCheck.push(toPreserve);\n Array.prototype.push.apply(toCheck, toAdd);\n checkOverlaps(toCheck);\n }\n\n if (IS_DEV) {\n console.log("CLIENT RECT: overlap, cut one rect into ".concat(toAdd.length));\n }\n\n var newRects = rects.filter(function (rect) {\n return rect !== toRemove;\n });\n Array.prototype.push.apply(newRects, toAdd);\n return {\n v: highlight_replaceOverlapingRects(newRects)\n };\n }();\n\n if (highlight_typeof(_ret) === "object") return _ret.v;\n }\n }\n }\n\n return rects;\n}\n\nfunction checkOverlaps(rects) {\n var stillOverlapingRects = [];\n\n var _iterator10 = highlight_createForOfIteratorHelper(rects),\n _step10;\n\n try {\n for (_iterator10.s(); !(_step10 = _iterator10.n()).done;) {\n var rect1 = _step10.value;\n\n var _iterator11 = highlight_createForOfIteratorHelper(rects),\n _step11;\n\n try {\n for (_iterator11.s(); !(_step11 = _iterator11.n()).done;) {\n var rect2 = _step11.value;\n\n if (rect1 === rect2) {\n continue;\n }\n\n var has1 = stillOverlapingRects.indexOf(rect1) >= 0;\n var has2 = stillOverlapingRects.indexOf(rect2) >= 0;\n\n if (!has1 || !has2) {\n if (highlight_rectsTouchOrOverlap(rect1, rect2, -1)) {\n if (!has1) {\n stillOverlapingRects.push(rect1);\n }\n\n if (!has2) {\n stillOverlapingRects.push(rect2);\n }\n\n console.log("CLIENT RECT: overlap ---");\n console.log("#1 TOP:".concat(rect1.top, " BOTTOM:").concat(rect1.bottom, " LEFT:").concat(rect1.left, " RIGHT:").concat(rect1.right, " WIDTH:").concat(rect1.width, " HEIGHT:").concat(rect1.height));\n console.log("#2 TOP:".concat(rect2.top, " BOTTOM:").concat(rect2.bottom, " LEFT:").concat(rect2.left, " RIGHT:").concat(rect2.right, " WIDTH:").concat(rect2.width, " HEIGHT:").concat(rect2.height));\n var xOverlap = getRectOverlapX(rect1, rect2);\n console.log("xOverlap: ".concat(xOverlap));\n var yOverlap = getRectOverlapY(rect1, rect2);\n console.log("yOverlap: ".concat(yOverlap));\n }\n }\n }\n } catch (err) {\n _iterator11.e(err);\n } finally {\n _iterator11.f();\n }\n }\n } catch (err) {\n _iterator10.e(err);\n } finally {\n _iterator10.f();\n }\n\n if (stillOverlapingRects.length) {\n console.log("CLIENT RECT: overlaps ".concat(stillOverlapingRects.length));\n }\n}\n\nfunction highlight_removeContainedRects(rects, tolerance) {\n var rectsToKeep = new Set(rects);\n\n var _iterator12 = highlight_createForOfIteratorHelper(rects),\n _step12;\n\n try {\n for (_iterator12.s(); !(_step12 = _iterator12.n()).done;) {\n var rect = _step12.value;\n var bigEnough = rect.width > 1 && rect.height > 1;\n\n if (!bigEnough) {\n if (IS_DEV) {\n console.log("CLIENT RECT: remove tiny");\n }\n\n rectsToKeep.delete(rect);\n continue;\n }\n\n var _iterator13 = highlight_createForOfIteratorHelper(rects),\n _step13;\n\n try {\n for (_iterator13.s(); !(_step13 = _iterator13.n()).done;) {\n var possiblyContainingRect = _step13.value;\n\n if (rect === possiblyContainingRect) {\n continue;\n }\n\n if (!rectsToKeep.has(possiblyContainingRect)) {\n continue;\n }\n\n if (highlight_rectContains(possiblyContainingRect, rect, tolerance)) {\n if (IS_DEV) {\n console.log("CLIENT RECT: remove contained");\n }\n\n rectsToKeep.delete(rect);\n break;\n }\n }\n } catch (err) {\n _iterator13.e(err);\n } finally {\n _iterator13.f();\n }\n }\n } catch (err) {\n _iterator12.e(err);\n } finally {\n _iterator12.f();\n }\n\n return Array.from(rectsToKeep);\n}\n\nfunction highlight_almostEqual(a, b, tolerance) {\n return Math.abs(a - b) <= tolerance;\n}\n\nfunction highlight_rectIntersect(rect1, rect2) {\n var maxLeft = Math.max(rect1.left, rect2.left);\n var minRight = Math.min(rect1.right, rect2.right);\n var maxTop = Math.max(rect1.top, rect2.top);\n var minBottom = Math.min(rect1.bottom, rect2.bottom);\n var rect = {\n bottom: minBottom,\n height: Math.max(0, minBottom - maxTop),\n left: maxLeft,\n right: minRight,\n top: maxTop,\n width: Math.max(0, minRight - maxLeft)\n };\n return rect;\n}\n\nfunction highlight_rectSubtract(rect1, rect2) {\n var rectIntersected = highlight_rectIntersect(rect2, rect1);\n\n if (rectIntersected.height === 0 || rectIntersected.width === 0) {\n return [rect1];\n }\n\n var rects = [];\n {\n var rectA = {\n bottom: rect1.bottom,\n height: 0,\n left: rect1.left,\n right: rectIntersected.left,\n top: rect1.top,\n width: 0\n };\n rectA.width = rectA.right - rectA.left;\n rectA.height = rectA.bottom - rectA.top;\n\n if (rectA.height !== 0 && rectA.width !== 0) {\n rects.push(rectA);\n }\n }\n {\n var rectB = {\n bottom: rectIntersected.top,\n height: 0,\n left: rectIntersected.left,\n right: rectIntersected.right,\n top: rect1.top,\n width: 0\n };\n rectB.width = rectB.right - rectB.left;\n rectB.height = rectB.bottom - rectB.top;\n\n if (rectB.height !== 0 && rectB.width !== 0) {\n rects.push(rectB);\n }\n }\n {\n var rectC = {\n bottom: rect1.bottom,\n height: 0,\n left: rectIntersected.left,\n right: rectIntersected.right,\n top: rectIntersected.bottom,\n width: 0\n };\n rectC.width = rectC.right - rectC.left;\n rectC.height = rectC.bottom - rectC.top;\n\n if (rectC.height !== 0 && rectC.width !== 0) {\n rects.push(rectC);\n }\n }\n {\n var rectD = {\n bottom: rect1.bottom,\n height: 0,\n left: rectIntersected.right,\n right: rect1.right,\n top: rect1.top,\n width: 0\n };\n rectD.width = rectD.right - rectD.left;\n rectD.height = rectD.bottom - rectD.top;\n\n if (rectD.height !== 0 && rectD.width !== 0) {\n rects.push(rectD);\n }\n }\n return rects;\n}\n\nfunction highlight_rectContainsPoint(rect, x, y, tolerance) {\n return (rect.left < x || highlight_almostEqual(rect.left, x, tolerance)) && (rect.right > x || highlight_almostEqual(rect.right, x, tolerance)) && (rect.top < y || highlight_almostEqual(rect.top, y, tolerance)) && (rect.bottom > y || highlight_almostEqual(rect.bottom, y, tolerance));\n}\n\nfunction highlight_rectContains(rect1, rect2, tolerance) {\n return highlight_rectContainsPoint(rect1, rect2.left, rect2.top, tolerance) && highlight_rectContainsPoint(rect1, rect2.right, rect2.top, tolerance) && highlight_rectContainsPoint(rect1, rect2.left, rect2.bottom, tolerance) && highlight_rectContainsPoint(rect1, rect2.right, rect2.bottom, tolerance);\n}\n\nfunction highlight_getBoundingRect(rect1, rect2) {\n var left = Math.min(rect1.left, rect2.left);\n var right = Math.max(rect1.right, rect2.right);\n var top = Math.min(rect1.top, rect2.top);\n var bottom = Math.max(rect1.bottom, rect2.bottom);\n return {\n bottom: bottom,\n height: bottom - top,\n left: left,\n right: right,\n top: top,\n width: right - left\n };\n}\n\nfunction highlight_mergeTouchingRects(rects, tolerance, doNotMergeHorizontallyAlignedRects) {\n for (var i = 0; i < rects.length; i++) {\n var _loop = function _loop(j) {\n var rect1 = rects[i];\n var rect2 = rects[j];\n\n if (rect1 === rect2) {\n if (IS_DEV) {\n console.log("mergeTouchingRects rect1 === rect2 ??!");\n }\n\n return "continue";\n }\n\n var rectsLineUpVertically = highlight_almostEqual(rect1.top, rect2.top, tolerance) && highlight_almostEqual(rect1.bottom, rect2.bottom, tolerance);\n var rectsLineUpHorizontally = highlight_almostEqual(rect1.left, rect2.left, tolerance) && highlight_almostEqual(rect1.right, rect2.right, tolerance);\n var horizontalAllowed = !doNotMergeHorizontallyAlignedRects;\n var aligned = rectsLineUpHorizontally && horizontalAllowed || rectsLineUpVertically && !rectsLineUpHorizontally;\n var canMerge = aligned && highlight_rectsTouchOrOverlap(rect1, rect2, tolerance);\n\n if (canMerge) {\n if (IS_DEV) {\n console.log("CLIENT RECT: merging two into one, VERTICAL: ".concat(rectsLineUpVertically, " HORIZONTAL: ").concat(rectsLineUpHorizontally, " (").concat(doNotMergeHorizontallyAlignedRects, ")"));\n }\n\n var newRects = rects.filter(function (rect) {\n return rect !== rect1 && rect !== rect2;\n });\n var replacementClientRect = highlight_getBoundingRect(rect1, rect2);\n newRects.push(replacementClientRect);\n return {\n v: highlight_mergeTouchingRects(newRects, tolerance, doNotMergeHorizontallyAlignedRects)\n };\n }\n };\n\n for (var j = i + 1; j < rects.length; j++) {\n var _ret2 = _loop(j);\n\n if (_ret2 === "continue") continue;\n if (highlight_typeof(_ret2) === "object") return _ret2.v;\n }\n }\n\n return rects;\n}\n\nfunction highlight_getClientRectsNoOverlap(range, doNotMergeHorizontallyAlignedRects) {\n var rangeClientRects = range.getClientRects();\n return getClientRectsNoOverlap_(rangeClientRects, doNotMergeHorizontallyAlignedRects);\n}\n\nfunction getClientRectsNoOverlap_(clientRects, doNotMergeHorizontallyAlignedRects) {\n var tolerance = 1;\n var originalRects = [];\n\n var _iterator14 = highlight_createForOfIteratorHelper(clientRects),\n _step14;\n\n try {\n for (_iterator14.s(); !(_step14 = _iterator14.n()).done;) {\n var rangeClientRect = _step14.value;\n originalRects.push({\n bottom: rangeClientRect.bottom,\n height: rangeClientRect.height,\n left: rangeClientRect.left,\n right: rangeClientRect.right,\n top: rangeClientRect.top,\n width: rangeClientRect.width\n });\n }\n } catch (err) {\n _iterator14.e(err);\n } finally {\n _iterator14.f();\n }\n\n var mergedRects = highlight_mergeTouchingRects(originalRects, tolerance, doNotMergeHorizontallyAlignedRects);\n var noContainedRects = highlight_removeContainedRects(mergedRects, tolerance);\n var newRects = highlight_replaceOverlapingRects(noContainedRects);\n var minArea = 2 * 2;\n\n for (var j = newRects.length - 1; j >= 0; j--) {\n var rect = newRects[j];\n var bigEnough = rect.width * rect.height > minArea;\n\n if (!bigEnough) {\n if (newRects.length > 1) {\n if (IS_DEV) {\n console.log("CLIENT RECT: remove small");\n }\n\n newRects.splice(j, 1);\n } else {\n if (IS_DEV) {\n console.log("CLIENT RECT: remove small, but keep otherwise empty!");\n }\n\n break;\n }\n }\n }\n\n if (IS_DEV) {\n checkOverlaps(newRects);\n }\n\n if (IS_DEV) {\n console.log("CLIENT RECT: reduced ".concat(originalRects.length, " --\x3e ").concat(newRects.length));\n }\n\n return newRects;\n}\n\nfunction isPaginated(document) {\n return document && document.documentElement && document.documentElement.classList.contains(CLASS_PAGINATED);\n}\n\nfunction getScrollingElement(document) {\n if (document.scrollingElement) {\n return document.scrollingElement;\n }\n\n return document.body;\n}\n\nfunction ensureContainer(win, annotationFlag) {\n var document = win.document;\n\n if (!_highlightsContainer) {\n if (!bodyEventListenersSet) {\n bodyEventListenersSet = true;\n document.body.addEventListener("mousedown", function (ev) {\n lastMouseDownX = ev.clientX;\n lastMouseDownY = ev.clientY;\n }, false);\n document.body.addEventListener("mouseup", function (ev) {\n if (Math.abs(lastMouseDownX - ev.clientX) < 3 && Math.abs(lastMouseDownY - ev.clientY) < 3) {\n processMouseEvent(win, ev);\n }\n }, false);\n document.body.addEventListener("mousemove", function (ev) {\n processMouseEvent(win, ev);\n }, false);\n document.body.addEventListener("touchend", function touchEnd(e) {\n processTouchEvent(win, e);\n }, false);\n }\n\n _highlightsContainer = document.createElement("div");\n\n _highlightsContainer.setAttribute("id", ID_HIGHLIGHTS_CONTAINER);\n\n _highlightsContainer.style.setProperty("pointer-events", "none");\n\n document.body.append(_highlightsContainer);\n }\n\n return _highlightsContainer;\n}\n\nfunction hideAllhighlights() {\n if (_highlightsContainer) {\n _highlightsContainer.remove();\n\n _highlightsContainer = null;\n }\n}\n\nfunction destroyAllhighlights() {\n hideAllhighlights();\n\n _highlights.splice(0, _highlights.length);\n}\n\nfunction destroyHighlight(id) {\n var i = -1;\n var _document = window.document;\n\n var highlight = _highlights.find(function (h, j) {\n i = j;\n return h.id === id;\n });\n\n if (highlight && i >= 0 && i < _highlights.length) {\n _highlights.splice(i, 1);\n }\n\n var highlightContainer = _document.getElementById(id);\n\n if (highlightContainer) {\n highlightContainer.remove();\n }\n}\n\nfunction isCfiTextNode(node) {\n return node.nodeType !== Node.ELEMENT_NODE;\n}\n\nfunction getChildTextNodeCfiIndex(element, child) {\n var found = -1;\n var textNodeIndex = -1;\n var previousWasElement = false;\n\n for (var i = 0; i < element.childNodes.length; i++) {\n var childNode = element.childNodes[i];\n var isText = isCfiTextNode(childNode);\n\n if (isText || previousWasElement) {\n textNodeIndex += 2;\n }\n\n if (isText) {\n if (childNode === child) {\n found = textNodeIndex;\n break;\n }\n }\n\n previousWasElement = childNode.nodeType === Node.ELEMENT_NODE;\n }\n\n return found;\n}\n\nfunction getCommonAncestorElement(node1, node2) {\n if (node1.nodeType === Node.ELEMENT_NODE && node1 === node2) {\n return node1;\n }\n\n if (node1.nodeType === Node.ELEMENT_NODE && node1.contains(node2)) {\n return node1;\n }\n\n if (node2.nodeType === Node.ELEMENT_NODE && node2.contains(node1)) {\n return node2;\n }\n\n var node1ElementAncestorChain = [];\n var parent = node1.parentNode;\n\n while (parent && parent.nodeType === Node.ELEMENT_NODE) {\n node1ElementAncestorChain.push(parent);\n parent = parent.parentNode;\n }\n\n var node2ElementAncestorChain = [];\n parent = node2.parentNode;\n\n while (parent && parent.nodeType === Node.ELEMENT_NODE) {\n node2ElementAncestorChain.push(parent);\n parent = parent.parentNode;\n }\n\n var commonAncestor = node1ElementAncestorChain.find(function (node1ElementAncestor) {\n return node2ElementAncestorChain.indexOf(node1ElementAncestor) >= 0;\n });\n\n if (!commonAncestor) {\n commonAncestor = node2ElementAncestorChain.find(function (node2ElementAncestor) {\n return node1ElementAncestorChain.indexOf(node2ElementAncestor) >= 0;\n });\n }\n\n return commonAncestor;\n}\n\nfunction fullQualifiedSelector(node) {\n if (node.nodeType !== Node.ELEMENT_NODE) {\n var lowerCaseName = node.localName && node.localName.toLowerCase() || node.nodeName.toLowerCase();\n return lowerCaseName;\n } //return cssPath(node, justSelector);\n\n\n return cssPath(node, true);\n}\n\nfunction getCurrentSelectionInfo() {\n var selection = window.getSelection();\n\n if (!selection) {\n return undefined;\n }\n\n if (selection.isCollapsed) {\n console.log("^^^ SELECTION COLLAPSED.");\n return undefined;\n }\n\n var rawText = selection.toString();\n var cleanText = rawText.trim().replace(/\\n/g, " ").replace(/\\s\\s+/g, " ");\n\n if (cleanText.length === 0) {\n console.log("^^^ SELECTION TEXT EMPTY.");\n return undefined;\n }\n\n if (!selection.anchorNode || !selection.focusNode) {\n return undefined;\n }\n\n var range = selection.rangeCount === 1 ? selection.getRangeAt(0) : createOrderedRange(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);\n\n if (!range || range.collapsed) {\n console.log("$$$$$$$$$$$$$$$$$ CANNOT GET NON-COLLAPSED SELECTION RANGE?!");\n return undefined;\n }\n\n var rangeInfo = convertRange(range, fullQualifiedSelector, computeCFI);\n\n if (!rangeInfo) {\n console.log("^^^ SELECTION RANGE INFO FAIL?!");\n return undefined;\n }\n\n if (IS_DEV && DEBUG_VISUALS) {\n var restoredRange = convertRangeInfo(win.document, rangeInfo);\n\n if (restoredRange) {\n if (restoredRange.startOffset === range.startOffset && restoredRange.endOffset === range.endOffset && restoredRange.startContainer === range.startContainer && restoredRange.endContainer === range.endContainer) {\n console.log("SELECTION RANGE RESTORED OKAY (dev check).");\n } else {\n console.log("SELECTION RANGE RESTORE FAIL (dev check).");\n dumpDebug("SELECTION", selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset, getCssSelector);\n dumpDebug("ORDERED RANGE FROM SELECTION", range.startContainer, range.startOffset, range.endContainer, range.endOffset, getCssSelector);\n dumpDebug("RESTORED RANGE", restoredRange.startContainer, restoredRange.startOffset, restoredRange.endContainer, restoredRange.endOffset, getCssSelector);\n }\n } else {\n console.log("CANNOT RESTORE SELECTION RANGE ??!");\n }\n } else {}\n\n return {\n locations: rangeInfo2Location(rangeInfo),\n text: {\n highlight: rawText\n }\n };\n}\n\nfunction checkBlacklisted(el) {\n var blacklistedId;\n var id = el.getAttribute("id");\n\n if (id && _blacklistIdClassForCFI.indexOf(id) >= 0) {\n console.log("checkBlacklisted ID: " + id);\n blacklistedId = id;\n }\n\n var blacklistedClass;\n\n var _iterator15 = highlight_createForOfIteratorHelper(_blacklistIdClassForCFI),\n _step15;\n\n try {\n for (_iterator15.s(); !(_step15 = _iterator15.n()).done;) {\n var item = _step15.value;\n\n if (el.classList.contains(item)) {\n console.log("checkBlacklisted CLASS: " + item);\n blacklistedClass = item;\n break;\n }\n }\n } catch (err) {\n _iterator15.e(err);\n } finally {\n _iterator15.f();\n }\n\n if (blacklistedId || blacklistedClass) {\n return true;\n }\n\n return false;\n}\n\nfunction cssPath(node, optimized) {\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return "";\n }\n\n var steps = [];\n var contextNode = node;\n\n while (contextNode) {\n var step = _cssPathStep(contextNode, !!optimized, contextNode === node);\n\n if (!step) {\n break; // Error - bail out early.\n }\n\n steps.push(step.value);\n\n if (step.optimized) {\n break;\n }\n\n contextNode = contextNode.parentNode;\n }\n\n steps.reverse();\n return steps.join(" > ");\n} // tslint:disable-next-line:max-line-length\n// https://chromium.googlesource.com/chromium/blink/+/master/Source/devtools/front_end/components/DOMPresentationUtils.js#316\n\n\nfunction _cssPathStep(node, optimized, isTargetNode) {\n function prefixedElementClassNames(nd) {\n var classAttribute = nd.getAttribute("class");\n\n if (!classAttribute) {\n return [];\n }\n\n return classAttribute.split(/\\s+/g).filter(Boolean).map(function (nm) {\n // The prefix is required to store "__proto__" in a object-based map.\n return "$" + nm;\n });\n }\n\n function idSelector(idd) {\n return "#" + escapeIdentifierIfNeeded(idd);\n }\n\n function escapeIdentifierIfNeeded(ident) {\n if (isCSSIdentifier(ident)) {\n return ident;\n }\n\n var shouldEscapeFirst = /^(?:[0-9]|-[0-9-]?)/.test(ident);\n var lastIndex = ident.length - 1;\n return ident.replace(/./g, function (c, ii) {\n return shouldEscapeFirst && ii === 0 || !isCSSIdentChar(c) ? escapeAsciiChar(c, ii === lastIndex) : c;\n });\n }\n\n function escapeAsciiChar(c, isLast) {\n return "\\\\" + toHexByte(c) + (isLast ? "" : " ");\n }\n\n function toHexByte(c) {\n var hexByte = c.charCodeAt(0).toString(16);\n\n if (hexByte.length === 1) {\n hexByte = "0" + hexByte;\n }\n\n return hexByte;\n }\n\n function isCSSIdentChar(c) {\n if (/[a-zA-Z0-9_-]/.test(c)) {\n return true;\n }\n\n return c.charCodeAt(0) >= 0xa0;\n }\n\n function isCSSIdentifier(value) {\n return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value);\n }\n\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return undefined;\n }\n\n var lowerCaseName = node.localName && node.localName.toLowerCase() || node.nodeName.toLowerCase();\n var element = node;\n var id = element.getAttribute("id");\n\n if (optimized) {\n if (id) {\n return {\n optimized: true,\n value: idSelector(id)\n };\n }\n\n if (lowerCaseName === "body" || lowerCaseName === "head" || lowerCaseName === "html") {\n return {\n optimized: true,\n value: lowerCaseName // node.nodeNameInCorrectCase(),\n\n };\n }\n }\n\n var nodeName = lowerCaseName; // node.nodeNameInCorrectCase();\n\n if (id) {\n return {\n optimized: true,\n value: nodeName + idSelector(id)\n };\n }\n\n var parent = node.parentNode;\n\n if (!parent || parent.nodeType === Node.DOCUMENT_NODE) {\n return {\n optimized: true,\n value: nodeName\n };\n }\n\n var prefixedOwnClassNamesArray_ = prefixedElementClassNames(element);\n var prefixedOwnClassNamesArray = []; // .keySet()\n\n prefixedOwnClassNamesArray_.forEach(function (arrItem) {\n if (prefixedOwnClassNamesArray.indexOf(arrItem) < 0) {\n prefixedOwnClassNamesArray.push(arrItem);\n }\n });\n var needsClassNames = false;\n var needsNthChild = false;\n var ownIndex = -1;\n var elementIndex = -1;\n var siblings = parent.children;\n\n var _loop2 = function _loop2(i) {\n var sibling = siblings[i];\n\n if (sibling.nodeType !== Node.ELEMENT_NODE) {\n return "continue";\n }\n\n elementIndex += 1;\n\n if (sibling === node) {\n ownIndex = elementIndex;\n return "continue";\n }\n\n if (needsNthChild) {\n return "continue";\n } // sibling.nodeNameInCorrectCase()\n\n\n var siblingName = sibling.localName && sibling.localName.toLowerCase() || sibling.nodeName.toLowerCase();\n\n if (siblingName !== nodeName) {\n return "continue";\n }\n\n needsClassNames = true;\n var ownClassNames = [];\n prefixedOwnClassNamesArray.forEach(function (arrItem) {\n ownClassNames.push(arrItem);\n });\n var ownClassNameCount = ownClassNames.length;\n\n if (ownClassNameCount === 0) {\n needsNthChild = true;\n return "continue";\n }\n\n var siblingClassNamesArray_ = prefixedElementClassNames(sibling);\n var siblingClassNamesArray = []; // .keySet()\n\n siblingClassNamesArray_.forEach(function (arrItem) {\n if (siblingClassNamesArray.indexOf(arrItem) < 0) {\n siblingClassNamesArray.push(arrItem);\n }\n });\n\n for (var _i3 = 0, _siblingClassNamesArr = siblingClassNamesArray; _i3 < _siblingClassNamesArr.length; _i3++) {\n var siblingClass = _siblingClassNamesArr[_i3];\n var ind = ownClassNames.indexOf(siblingClass);\n\n if (ind < 0) {\n continue;\n }\n\n ownClassNames.splice(ind, 1); // delete ownClassNames[siblingClass];\n\n if (! --ownClassNameCount) {\n needsNthChild = true;\n break;\n }\n }\n };\n\n for (var i = 0; (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) {\n var _ret3 = _loop2(i);\n\n if (_ret3 === "continue") continue;\n }\n\n var result = nodeName;\n\n if (isTargetNode && nodeName === "input" && element.getAttribute("type") && !element.getAttribute("id") && !element.getAttribute("class")) {\n result += \'[type="\' + element.getAttribute("type") + \'"]\';\n }\n\n if (needsNthChild) {\n result += ":nth-child(" + (ownIndex + 1) + ")";\n } else if (needsClassNames) {\n var _iterator16 = highlight_createForOfIteratorHelper(prefixedOwnClassNamesArray),\n _step16;\n\n try {\n for (_iterator16.s(); !(_step16 = _iterator16.n()).done;) {\n var prefixedName = _step16.value;\n result += "." + escapeIdentifierIfNeeded(prefixedName.substr(1));\n }\n } catch (err) {\n _iterator16.e(err);\n } finally {\n _iterator16.f();\n }\n }\n\n return {\n optimized: false,\n value: result\n };\n}\n\nfunction computeCFI(node) {\n // TODO: handle character position inside text node\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return undefined;\n }\n\n var cfi = "";\n var currentElement = node;\n\n while (currentElement.parentNode && currentElement.parentNode.nodeType === Node.ELEMENT_NODE) {\n var blacklisted = checkBlacklisted(currentElement);\n\n if (!blacklisted) {\n var currentElementParentChildren = currentElement.parentNode.children;\n var currentElementIndex = -1;\n\n for (var i = 0; i < currentElementParentChildren.length; i++) {\n if (currentElement === currentElementParentChildren[i]) {\n currentElementIndex = i;\n break;\n }\n }\n\n if (currentElementIndex >= 0) {\n var cfiIndex = (currentElementIndex + 1) * 2;\n cfi = cfiIndex + (currentElement.id ? "[" + currentElement.id + "]" : "") + (cfi.length ? "/" + cfi : "");\n }\n }\n\n currentElement = currentElement.parentNode;\n }\n\n return "/" + cfi;\n}\n\nfunction _createHighlight(locations, color, pointerInteraction, type) {\n var rangeInfo = location2RangeInfo(locations);\n var uniqueStr = "".concat(rangeInfo.cfi).concat(rangeInfo.startContainerElementCssSelector).concat(rangeInfo.startContainerChildTextNodeIndex).concat(rangeInfo.startOffset).concat(rangeInfo.endContainerElementCssSelector).concat(rangeInfo.endContainerChildTextNodeIndex).concat(rangeInfo.endOffset);\n\n var hash = __webpack_require__(3715);\n\n var sha256Hex = hash.sha256().update(uniqueStr).digest("hex");\n var id;\n\n if (type == ID_HIGHLIGHTS_CONTAINER) {\n id = "R2_HIGHLIGHT_" + sha256Hex;\n } else {\n id = "R2_ANNOTATION_" + sha256Hex;\n }\n\n destroyHighlight(id);\n var highlight = {\n color: color ? color : DEFAULT_BACKGROUND_COLOR,\n id: id,\n pointerInteraction: pointerInteraction,\n rangeInfo: rangeInfo\n };\n\n _highlights.push(highlight);\n\n createHighlightDom(window, highlight, type == ID_ANNOTATION_CONTAINER ? true : false);\n return highlight;\n}\n\nfunction createHighlight(selectionInfo, color, pointerInteraction) {\n return _createHighlight(selectionInfo, color, pointerInteraction, ID_HIGHLIGHTS_CONTAINER);\n}\nfunction createAnnotation(id) {\n var i = -1;\n\n var highlight = _highlights.find(function (h, j) {\n i = j;\n return h.id === id;\n });\n\n if (i == _highlights.length) return;\n var locations = {\n locations: rangeInfo2Location(highlight.rangeInfo)\n };\n return _createHighlight(locations, highlight.color, true, ID_ANNOTATION_CONTAINER);\n}\n\nfunction createHighlightDom(win, highlight, annotationFlag) {\n var document = win.document;\n var scale = 1 / (win.READIUM2 && win.READIUM2.isFixedLayout ? win.READIUM2.fxlViewportScale : 1);\n var scrollElement = getScrollingElement(document);\n var range = convertRangeInfo(document, highlight.rangeInfo);\n\n if (!range) {\n return undefined;\n }\n\n var paginated = isPaginated(document);\n var highlightsContainer = ensureContainer(win, annotationFlag);\n var highlightParent = document.createElement("div");\n highlightParent.setAttribute("id", highlight.id);\n highlightParent.setAttribute("class", CLASS_HIGHLIGHT_CONTAINER);\n document.body.style.position = "relative";\n highlightParent.style.setProperty("pointer-events", "none");\n\n if (highlight.pointerInteraction) {\n highlightParent.setAttribute("data-click", "1");\n }\n\n var bodyRect = document.body.getBoundingClientRect();\n var useSVG = !DEBUG_VISUALS && USE_SVG; //const useSVG = USE_SVG;\n\n var drawUnderline = false;\n var drawStrikeThrough = false;\n var doNotMergeHorizontallyAlignedRects = drawUnderline || drawStrikeThrough; //const clientRects = DEBUG_VISUALS ? range.getClientRects() :\n\n var clientRects = highlight_getClientRectsNoOverlap(range, doNotMergeHorizontallyAlignedRects);\n var highlightAreaSVGDocFrag;\n var roundedCorner = 3;\n var underlineThickness = 2;\n var strikeThroughLineThickness = 3;\n var opacity = DEFAULT_BACKGROUND_COLOR_OPACITY;\n var extra = "";\n var rangeAnnotationBoundingClientRect = frameForHighlightAnnotationMarkWithID(win, highlight.id);\n var xOffset;\n var yOffset;\n var annotationOffset;\n\n if (navigator.userAgent.match(/Android/i)) {\n xOffset = paginated ? -scrollElement.scrollLeft : bodyRect.left;\n yOffset = paginated ? -scrollElement.scrollTop : bodyRect.top;\n annotationOffset = parseInt((rangeAnnotationBoundingClientRect.right - xOffset) / window.innerWidth) + 1;\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n xOffset = paginated ? 0 : -scrollElement.scrollLeft;\n yOffset = paginated ? 0 : bodyRect.top;\n annotationOffset = parseInt(rangeAnnotationBoundingClientRect.right / window.innerWidth + 1);\n }\n\n var _iterator17 = highlight_createForOfIteratorHelper(clientRects),\n _step17;\n\n try {\n for (_iterator17.s(); !(_step17 = _iterator17.n()).done;) {\n var clientRect = _step17.value;\n\n if (useSVG) {\n var borderThickness = 0;\n\n if (!highlightAreaSVGDocFrag) {\n highlightAreaSVGDocFrag = document.createDocumentFragment();\n }\n\n var highlightAreaSVGRect = document.createElementNS(SVG_XML_NAMESPACE, "rect");\n highlightAreaSVGRect.setAttribute("class", CLASS_HIGHLIGHT_AREA);\n highlightAreaSVGRect.setAttribute("style", "fill: rgb(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ") !important; fill-opacity: ").concat(opacity, " !important; stroke-width: 0;"));\n highlightAreaSVGRect.scale = scale;\n /*\n highlightAreaSVGRect.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width,\n };\n */\n\n if (annotationFlag) {\n highlightAreaSVGRect.rect = {\n height: ANNOTATION_WIDTH,\n //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4,\n left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH,\n top: rangeAnnotationBoundingClientRect.top - yOffset,\n width: ANNOTATION_WIDTH\n };\n } else {\n highlightAreaSVGRect.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width\n };\n }\n\n highlightAreaSVGRect.setAttribute("rx", "".concat(roundedCorner * scale));\n highlightAreaSVGRect.setAttribute("ry", "".concat(roundedCorner * scale));\n highlightAreaSVGRect.setAttribute("x", "".concat((highlightAreaSVGRect.rect.left - borderThickness) * scale));\n highlightAreaSVGRect.setAttribute("y", "".concat((highlightAreaSVGRect.rect.top - borderThickness) * scale));\n highlightAreaSVGRect.setAttribute("height", "".concat((highlightAreaSVGRect.rect.height + borderThickness * 2) * scale));\n highlightAreaSVGRect.setAttribute("width", "".concat((highlightAreaSVGRect.rect.width + borderThickness * 2) * scale));\n highlightAreaSVGDocFrag.appendChild(highlightAreaSVGRect);\n\n if (drawUnderline) {\n var highlightAreaSVGLine = document.createElementNS(SVG_XML_NAMESPACE, "line");\n highlightAreaSVGRect.setAttribute("class", CLASS_HIGHLIGHT_AREA);\n highlightAreaSVGLine.setAttribute("style", "stroke-linecap: round; stroke-width: ".concat(underlineThickness * scale, "; stroke: rgb(").concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ") !important; stroke-opacity: ").concat(opacity, " !important"));\n highlightAreaSVGLine.scale = scale;\n /*\n highlightAreaSVGLine.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width,\n };\n */\n\n if (annotationFlag) {\n highlightAreaSVGLine.rect = {\n height: ANNOTATION_WIDTH,\n //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4,\n left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH,\n top: rangeAnnotationBoundingClientRect.top - yOffset,\n width: ANNOTATION_WIDTH\n };\n } else {\n highlightAreaSVGLine.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width\n };\n }\n\n var lineOffset = highlightAreaSVGLine.rect.width > roundedCorner ? roundedCorner : 0;\n highlightAreaSVGLine.setAttribute("x1", "".concat((highlightAreaSVGLine.rect.left + lineOffset) * scale));\n highlightAreaSVGLine.setAttribute("x2", "".concat((highlightAreaSVGLine.rect.left + highlightAreaSVGLine.rect.width - lineOffset) * scale));\n var y = (highlightAreaSVGLine.rect.top + highlightAreaSVGLine.rect.height - underlineThickness / 2) * scale;\n highlightAreaSVGLine.setAttribute("y1", "".concat(y));\n highlightAreaSVGLine.setAttribute("y2", "".concat(y));\n highlightAreaSVGLine.setAttribute("height", "".concat(highlightAreaSVGLine.rect.height * scale));\n highlightAreaSVGLine.setAttribute("width", "".concat(highlightAreaSVGLine.rect.width * scale));\n highlightAreaSVGDocFrag.appendChild(highlightAreaSVGLine);\n }\n\n if (drawStrikeThrough) {\n var _highlightAreaSVGLine = document.createElementNS(SVG_XML_NAMESPACE, "line");\n\n highlightAreaSVGRect.setAttribute("class", CLASS_HIGHLIGHT_AREA);\n\n _highlightAreaSVGLine.setAttribute("style", "stroke-linecap: butt; stroke-width: ".concat(strikeThroughLineThickness * scale, "; stroke: rgb(").concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ") !important; stroke-opacity: ").concat(opacity, " !important"));\n\n _highlightAreaSVGLine.scale = scale;\n /*\n highlightAreaSVGLine.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width,\n };\n */\n\n if (annotationFlag) {\n _highlightAreaSVGLine.rect = {\n height: ANNOTATION_WIDTH,\n //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4,\n left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH,\n top: rangeAnnotationBoundingClientRect.top - yOffset,\n width: ANNOTATION_WIDTH\n };\n } else {\n _highlightAreaSVGLine.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width\n };\n }\n\n _highlightAreaSVGLine.setAttribute("x1", "".concat(_highlightAreaSVGLine.rect.left * scale));\n\n _highlightAreaSVGLine.setAttribute("x2", "".concat((_highlightAreaSVGLine.rect.left + _highlightAreaSVGLine.rect.width) * scale));\n\n var _lineOffset = _highlightAreaSVGLine.rect.height / 2;\n\n var _y = (_highlightAreaSVGLine.rect.top + _lineOffset) * scale;\n\n _highlightAreaSVGLine.setAttribute("y1", "".concat(_y));\n\n _highlightAreaSVGLine.setAttribute("y2", "".concat(_y));\n\n _highlightAreaSVGLine.setAttribute("height", "".concat(_highlightAreaSVGLine.rect.height * scale));\n\n _highlightAreaSVGLine.setAttribute("width", "".concat(_highlightAreaSVGLine.rect.width * scale));\n\n highlightAreaSVGDocFrag.appendChild(_highlightAreaSVGLine);\n }\n } else {\n var highlightArea = document.createElement("div");\n highlightArea.setAttribute("class", CLASS_HIGHLIGHT_AREA);\n\n if (DEBUG_VISUALS) {\n var rgb = Math.round(0xffffff * Math.random());\n var r = rgb >> 16;\n var g = rgb >> 8 & 255;\n var b = rgb & 255;\n extra = "outline-color: rgb(".concat(r, ", ").concat(g, ", ").concat(b, "); outline-style: solid; outline-width: 1px; outline-offset: -1px;");\n } else {\n if (drawUnderline) {\n extra += "border-bottom: ".concat(underlineThickness * scale, "px solid rgba(").concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ", ").concat(opacity, ") !important");\n }\n }\n\n highlightArea.setAttribute("style", "border-radius: ".concat(roundedCorner, "px !important; background-color: rgba(").concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ", ").concat(opacity, ") !important; ").concat(extra));\n highlightArea.style.setProperty("pointer-events", "none");\n highlightArea.style.position = paginated ? "fixed" : "absolute";\n highlightArea.scale = scale;\n /*\n highlightArea.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width,\n };\n */\n\n if (annotationFlag) {\n highlightArea.rect = {\n height: ANNOTATION_WIDTH,\n //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4,\n left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH,\n top: rangeAnnotationBoundingClientRect.top - yOffset,\n width: ANNOTATION_WIDTH\n };\n } else {\n highlightArea.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width\n };\n }\n\n highlightArea.style.width = "".concat(highlightArea.rect.width * scale, "px");\n highlightArea.style.height = "".concat(highlightArea.rect.height * scale, "px");\n highlightArea.style.left = "".concat(highlightArea.rect.left * scale, "px");\n highlightArea.style.top = "".concat(highlightArea.rect.top * scale, "px");\n highlightParent.append(highlightArea);\n\n if (!DEBUG_VISUALS && drawStrikeThrough) {\n //if (drawStrikeThrough) {\n var highlightAreaLine = document.createElement("div");\n highlightAreaLine.setAttribute("class", CLASS_HIGHLIGHT_AREA);\n highlightAreaLine.setAttribute("style", "background-color: rgba(".concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ", ").concat(opacity, ") !important;"));\n highlightAreaLine.style.setProperty("pointer-events", "none");\n highlightAreaLine.style.position = paginated ? "fixed" : "absolute";\n highlightAreaLine.scale = scale;\n /*\n highlightAreaLine.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width,\n };\n */\n\n if (annotationFlag) {\n highlightAreaLine.rect = {\n height: ANNOTATION_WIDTH,\n //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4,\n left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH,\n top: rangeAnnotationBoundingClientRect.top - yOffset,\n width: ANNOTATION_WIDTH\n };\n } else {\n highlightAreaLine.rect = {\n height: clientRect.height,\n left: clientRect.left - xOffset,\n top: clientRect.top - yOffset,\n width: clientRect.width\n };\n }\n\n highlightAreaLine.style.width = "".concat(highlightAreaLine.rect.width * scale, "px");\n highlightAreaLine.style.height = "".concat(strikeThroughLineThickness * scale, "px");\n highlightAreaLine.style.left = "".concat(highlightAreaLine.rect.left * scale, "px");\n highlightAreaLine.style.top = "".concat((highlightAreaLine.rect.top + highlightAreaLine.rect.height / 2 - strikeThroughLineThickness / 2) * scale, "px");\n highlightParent.append(highlightAreaLine);\n }\n }\n\n if (annotationFlag) {\n break;\n }\n }\n } catch (err) {\n _iterator17.e(err);\n } finally {\n _iterator17.f();\n }\n\n if (useSVG && highlightAreaSVGDocFrag) {\n var highlightAreaSVG = document.createElementNS(SVG_XML_NAMESPACE, "svg");\n highlightAreaSVG.setAttribute("pointer-events", "none");\n highlightAreaSVG.style.position = paginated ? "fixed" : "absolute";\n highlightAreaSVG.style.overflow = "visible";\n highlightAreaSVG.style.left = "0";\n highlightAreaSVG.style.top = "0";\n highlightAreaSVG.append(highlightAreaSVGDocFrag);\n highlightParent.append(highlightAreaSVG);\n }\n\n var highlightBounding = document.createElement("div");\n\n if (annotationFlag) {\n highlightBounding.setAttribute("class", CLASS_ANNOTATION_BOUNDING_AREA);\n highlightBounding.setAttribute("style", "border-radius: ".concat(roundedCorner, "px !important; background-color: rgba(").concat(highlight.color.red, ", ").concat(highlight.color.green, ", ").concat(highlight.color.blue, ", ").concat(opacity, ") !important; ").concat(extra));\n } else {\n highlightBounding.setAttribute("class", CLASS_HIGHLIGHT_BOUNDING_AREA);\n }\n\n highlightBounding.style.setProperty("pointer-events", "none");\n highlightBounding.style.position = paginated ? "fixed" : "absolute";\n highlightBounding.scale = scale;\n\n if (DEBUG_VISUALS) {\n highlightBounding.setAttribute("style", "outline-color: magenta; outline-style: solid; outline-width: 1px; outline-offset: -1px;");\n }\n\n if (annotationFlag) {\n highlightBounding.rect = {\n height: ANNOTATION_WIDTH,\n //rangeAnnotationBoundingClientRect.height - rangeAnnotationBoundingClientRect.height/4,\n left: window.innerWidth * annotationOffset - ANNOTATION_WIDTH,\n top: rangeAnnotationBoundingClientRect.top - yOffset,\n width: ANNOTATION_WIDTH\n };\n } else {\n var rangeBoundingClientRect = range.getBoundingClientRect();\n highlightBounding.rect = {\n height: rangeBoundingClientRect.height,\n left: rangeBoundingClientRect.left - xOffset,\n top: rangeBoundingClientRect.top - yOffset,\n width: rangeBoundingClientRect.width\n };\n }\n\n highlightBounding.style.width = "".concat(highlightBounding.rect.width * scale, "px");\n highlightBounding.style.height = "".concat(highlightBounding.rect.height * scale, "px");\n highlightBounding.style.left = "".concat(highlightBounding.rect.left * scale, "px");\n highlightBounding.style.top = "".concat(highlightBounding.rect.top * scale, "px");\n highlightParent.append(highlightBounding);\n highlightsContainer.append(highlightParent);\n return highlightParent;\n}\n\nfunction createOrderedRange(startNode, startOffset, endNode, endOffset) {\n var range = new Range();\n range.setStart(startNode, startOffset);\n range.setEnd(endNode, endOffset);\n\n if (!range.collapsed) {\n return range;\n }\n\n console.log(">>> createOrderedRange COLLAPSED ... RANGE REVERSE?");\n var rangeReverse = new Range();\n rangeReverse.setStart(endNode, endOffset);\n rangeReverse.setEnd(startNode, startOffset);\n\n if (!rangeReverse.collapsed) {\n console.log(">>> createOrderedRange RANGE REVERSE OK.");\n return range;\n }\n\n console.log(">>> createOrderedRange RANGE REVERSE ALSO COLLAPSED?!");\n return undefined;\n}\n\nfunction convertRange(range, getCssSelector, computeElementCFI) {\n var startIsElement = range.startContainer.nodeType === Node.ELEMENT_NODE;\n var startContainerElement = startIsElement ? range.startContainer : range.startContainer.parentNode && range.startContainer.parentNode.nodeType === Node.ELEMENT_NODE ? range.startContainer.parentNode : undefined;\n\n if (!startContainerElement) {\n return undefined;\n }\n\n var startContainerChildTextNodeIndex = startIsElement ? -1 : Array.from(startContainerElement.childNodes).indexOf(range.startContainer);\n\n if (startContainerChildTextNodeIndex < -1) {\n return undefined;\n }\n\n var startContainerElementCssSelector = getCssSelector(startContainerElement);\n var endIsElement = range.endContainer.nodeType === Node.ELEMENT_NODE;\n var endContainerElement = endIsElement ? range.endContainer : range.endContainer.parentNode && range.endContainer.parentNode.nodeType === Node.ELEMENT_NODE ? range.endContainer.parentNode : undefined;\n\n if (!endContainerElement) {\n return undefined;\n }\n\n var endContainerChildTextNodeIndex = endIsElement ? -1 : Array.from(endContainerElement.childNodes).indexOf(range.endContainer);\n\n if (endContainerChildTextNodeIndex < -1) {\n return undefined;\n }\n\n var endContainerElementCssSelector = getCssSelector(endContainerElement);\n var commonElementAncestor = getCommonAncestorElement(range.startContainer, range.endContainer);\n\n if (!commonElementAncestor) {\n console.log("^^^ NO RANGE COMMON ANCESTOR?!");\n return undefined;\n }\n\n if (range.commonAncestorContainer) {\n var rangeCommonAncestorElement = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE ? range.commonAncestorContainer : range.commonAncestorContainer.parentNode;\n\n if (rangeCommonAncestorElement && rangeCommonAncestorElement.nodeType === Node.ELEMENT_NODE) {\n if (commonElementAncestor !== rangeCommonAncestorElement) {\n console.log(">>>>>> COMMON ANCESTOR CONTAINER DIFF??!");\n console.log(getCssSelector(commonElementAncestor));\n console.log(getCssSelector(rangeCommonAncestorElement));\n }\n }\n }\n\n var rootElementCfi = computeElementCFI(commonElementAncestor);\n var startElementCfi = computeElementCFI(startContainerElement);\n var endElementCfi = computeElementCFI(endContainerElement);\n var cfi;\n\n if (rootElementCfi && startElementCfi && endElementCfi) {\n var startElementOrTextCfi = startElementCfi;\n\n if (!startIsElement) {\n var startContainerChildTextNodeIndexForCfi = getChildTextNodeCfiIndex(startContainerElement, range.startContainer);\n startElementOrTextCfi = startElementCfi + "/" + startContainerChildTextNodeIndexForCfi + ":" + range.startOffset;\n } else {\n if (range.startOffset >= 0 && range.startOffset < startContainerElement.childNodes.length) {\n var childNode = startContainerElement.childNodes[range.startOffset];\n\n if (childNode.nodeType === Node.ELEMENT_NODE) {\n startElementOrTextCfi = startElementCfi + "/" + (range.startOffset + 1) * 2;\n } else {\n var cfiTextNodeIndex = getChildTextNodeCfiIndex(startContainerElement, childNode);\n startElementOrTextCfi = startElementCfi + "/" + cfiTextNodeIndex;\n }\n } else {\n var cfiIndexOfLastElement = startContainerElement.childElementCount * 2;\n var lastChildNode = startContainerElement.childNodes[startContainerElement.childNodes.length - 1];\n\n if (lastChildNode.nodeType === Node.ELEMENT_NODE) {\n startElementOrTextCfi = startElementCfi + "/" + (cfiIndexOfLastElement + 1);\n } else {\n startElementOrTextCfi = startElementCfi + "/" + (cfiIndexOfLastElement + 2);\n }\n }\n }\n\n var endElementOrTextCfi = endElementCfi;\n\n if (!endIsElement) {\n var endContainerChildTextNodeIndexForCfi = getChildTextNodeCfiIndex(endContainerElement, range.endContainer);\n endElementOrTextCfi = endElementCfi + "/" + endContainerChildTextNodeIndexForCfi + ":" + range.endOffset;\n } else {\n if (range.endOffset >= 0 && range.endOffset < endContainerElement.childNodes.length) {\n var _childNode = endContainerElement.childNodes[range.endOffset];\n\n if (_childNode.nodeType === Node.ELEMENT_NODE) {\n endElementOrTextCfi = endElementCfi + "/" + (range.endOffset + 1) * 2;\n } else {\n var _cfiTextNodeIndex = getChildTextNodeCfiIndex(endContainerElement, _childNode);\n\n endElementOrTextCfi = endElementCfi + "/" + _cfiTextNodeIndex;\n }\n } else {\n var _cfiIndexOfLastElement = endContainerElement.childElementCount * 2;\n\n var _lastChildNode = endContainerElement.childNodes[endContainerElement.childNodes.length - 1];\n\n if (_lastChildNode.nodeType === Node.ELEMENT_NODE) {\n endElementOrTextCfi = endElementCfi + "/" + (_cfiIndexOfLastElement + 1);\n } else {\n endElementOrTextCfi = endElementCfi + "/" + (_cfiIndexOfLastElement + 2);\n }\n }\n }\n\n cfi = rootElementCfi + "," + startElementOrTextCfi.replace(rootElementCfi, "") + "," + endElementOrTextCfi.replace(rootElementCfi, "");\n }\n\n return {\n cfi: cfi,\n endContainerChildTextNodeIndex: endContainerChildTextNodeIndex,\n endContainerElementCssSelector: endContainerElementCssSelector,\n endOffset: range.endOffset,\n startContainerChildTextNodeIndex: startContainerChildTextNodeIndex,\n startContainerElementCssSelector: startContainerElementCssSelector,\n startOffset: range.startOffset\n };\n}\n\nfunction convertRangeInfo(document, rangeInfo) {\n var startElement = document.querySelector(rangeInfo.startContainerElementCssSelector);\n\n if (!startElement) {\n console.log("^^^ convertRangeInfo NO START ELEMENT CSS SELECTOR?!");\n return undefined;\n }\n\n var startContainer = startElement;\n\n if (rangeInfo.startContainerChildTextNodeIndex >= 0) {\n if (rangeInfo.startContainerChildTextNodeIndex >= startElement.childNodes.length) {\n console.log("^^^ convertRangeInfo rangeInfo.startContainerChildTextNodeIndex >= startElement.childNodes.length?!");\n return undefined;\n }\n\n startContainer = startElement.childNodes[rangeInfo.startContainerChildTextNodeIndex];\n\n if (startContainer.nodeType !== Node.TEXT_NODE) {\n console.log("^^^ convertRangeInfo startContainer.nodeType !== Node.TEXT_NODE?!");\n return undefined;\n }\n }\n\n var endElement = document.querySelector(rangeInfo.endContainerElementCssSelector);\n\n if (!endElement) {\n console.log("^^^ convertRangeInfo NO END ELEMENT CSS SELECTOR?!");\n return undefined;\n }\n\n var endContainer = endElement;\n\n if (rangeInfo.endContainerChildTextNodeIndex >= 0) {\n if (rangeInfo.endContainerChildTextNodeIndex >= endElement.childNodes.length) {\n console.log("^^^ convertRangeInfo rangeInfo.endContainerChildTextNodeIndex >= endElement.childNodes.length?!");\n return undefined;\n }\n\n endContainer = endElement.childNodes[rangeInfo.endContainerChildTextNodeIndex];\n\n if (endContainer.nodeType !== Node.TEXT_NODE) {\n console.log("^^^ convertRangeInfo endContainer.nodeType !== Node.TEXT_NODE?!");\n return undefined;\n }\n }\n\n return createOrderedRange(startContainer, rangeInfo.startOffset, endContainer, rangeInfo.endOffset);\n}\n\nfunction frameForHighlightAnnotationMarkWithID(win, id) {\n var clientRects = frameForHighlightWithID(id);\n if (!clientRects) return;\n var topClientRect = clientRects[0];\n var maxHeight = topClientRect.height;\n\n var _iterator18 = highlight_createForOfIteratorHelper(clientRects),\n _step18;\n\n try {\n for (_iterator18.s(); !(_step18 = _iterator18.n()).done;) {\n var clientRect = _step18.value;\n if (clientRect.top < topClientRect.top) topClientRect = clientRect;\n if (clientRect.height > maxHeight) maxHeight = clientRect.height;\n }\n } catch (err) {\n _iterator18.e(err);\n } finally {\n _iterator18.f();\n }\n\n var document = win.document;\n var scrollElement = getScrollingElement(document);\n var paginated = isPaginated(document);\n var bodyRect = document.body.getBoundingClientRect();\n var yOffset;\n\n if (navigator.userAgent.match(/Android/i)) {\n yOffset = paginated ? -scrollElement.scrollTop : bodyRect.top;\n } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n yOffset = paginated ? 0 : bodyRect.top;\n }\n\n var newTop = topClientRect.top;\n\n if (_highlightsContainer) {\n do {\n var boundingAreas = document.getElementsByClassName(CLASS_ANNOTATION_BOUNDING_AREA);\n var found = false; //for (let i = 0, length = boundingAreas.snapshotLength; i < length; ++i) {\n\n for (var i = 0, len = boundingAreas.length | 0; i < len; i = i + 1 | 0) {\n var boundingArea = boundingAreas[i];\n\n if (Math.abs(boundingArea.rect.top - (newTop - yOffset)) < 3) {\n newTop += boundingArea.rect.height;\n found = true;\n break;\n }\n }\n } while (found);\n }\n\n topClientRect.top = newTop;\n topClientRect.height = maxHeight;\n return topClientRect;\n}\n\nfunction highlightWithID(id) {\n var i = -1;\n\n var highlight = _highlights.find(function (h, j) {\n i = j;\n return h.id === id;\n });\n\n return highlight;\n}\n\nfunction frameForHighlightWithID(id) {\n var highlight = highlightWithID(id);\n if (!highlight) return;\n var document = window.document;\n var scrollElement = getScrollingElement(document);\n var range = convertRangeInfo(document, highlight.rangeInfo);\n\n if (!range) {\n return undefined;\n }\n\n var drawUnderline = false;\n var drawStrikeThrough = false;\n var doNotMergeHorizontallyAlignedRects = drawUnderline || drawStrikeThrough; //const clientRects = DEBUG_VISUALS ? range.getClientRects() :\n\n var clientRects = highlight_getClientRectsNoOverlap(range, doNotMergeHorizontallyAlignedRects);\n return clientRects;\n}\n\nfunction rangeInfo2Location(rangeInfo) {\n return {\n cssSelector: rangeInfo.startContainerElementCssSelector,\n partialCfi: rangeInfo.cfi,\n domRange: {\n start: {\n cssSelector: rangeInfo.startContainerElementCssSelector,\n textNodeIndex: rangeInfo.startContainerChildTextNodeIndex,\n offset: rangeInfo.startOffset\n },\n end: {\n cssSelector: rangeInfo.endContainerElementCssSelector,\n textNodeIndex: rangeInfo.endContainerChildTextNodeIndex,\n offset: rangeInfo.endOffset\n }\n }\n };\n}\n\nfunction location2RangeInfo(location) {\n var locations = location.locations;\n var domRange = locations.domRange;\n var start = domRange.start;\n var end = domRange.end;\n return {\n cfi: location.partialCfi,\n endContainerChildTextNodeIndex: end.textNodeIndex,\n endContainerElementCssSelector: end.cssSelector,\n endOffset: end.offset,\n startContainerChildTextNodeIndex: start.textNodeIndex,\n startContainerElementCssSelector: start.cssSelector,\n startOffset: start.offset\n };\n}\n\nfunction rectangleForHighlightWithID(id) {\n var highlight = highlightWithID(id);\n if (!highlight) return;\n var document = window.document;\n var scrollElement = getScrollingElement(document);\n var range = convertRangeInfo(document, highlight.rangeInfo);\n\n if (!range) {\n return undefined;\n }\n\n var drawUnderline = false;\n var drawStrikeThrough = false;\n var doNotMergeHorizontallyAlignedRects = drawUnderline || drawStrikeThrough; //const clientRects = DEBUG_VISUALS ? range.getClientRects() :\n\n var clientRects = highlight_getClientRectsNoOverlap(range, doNotMergeHorizontallyAlignedRects);\n var size = {\n screenWidth: window.outerWidth,\n screenHeight: window.outerHeight,\n left: clientRects[0].left,\n width: clientRects[0].width,\n top: clientRects[0].top,\n height: clientRects[0].height\n };\n return size;\n}\nfunction getSelectionRect() {\n try {\n var sel = window.getSelection();\n\n if (!sel) {\n return;\n }\n\n var range = sel.getRangeAt(0);\n var clientRect = range.getBoundingClientRect();\n var handleBounds = {\n screenWidth: window.outerWidth,\n screenHeight: window.outerHeight,\n left: clientRect.left,\n width: clientRect.width,\n top: clientRect.top,\n height: clientRect.height\n };\n return handleBounds;\n } catch (e) {\n return null;\n }\n}\nfunction setScrollMode(flag) {\n if (!flag) {\n document.documentElement.classList.add(CLASS_PAGINATED);\n } else {\n document.documentElement.classList.remove(CLASS_PAGINATED);\n }\n}\n/*\n if (document.addEventListener) { // IE >= 9; other browsers\n document.addEventListener(\'contextmenu\', function(e) {\n //alert("You\'ve tried to open context menu"); //here you draw your own menu\n //e.preventDefault();\n //let getCssSelector = fullQualifiedSelector;\n \n\t\t\tlet str = window.getSelection();\n\t\t\tlet selectionInfo = getCurrentSelectionInfo();\n\t\t\tlet pos = createHighlight(selectionInfo,{red:10,green:50,blue:230},true);\n\t\t\tlet ret2 = createAnnotation(pos.id);\n\t\t\t\n }, false);\n } else { // IE < 9\n document.attachEvent(\'oncontextmenu\', function() {\n alert("You\'ve tried to open context menu");\n window.event.returnValue = false;\n });\n }\n*/\n// EXTERNAL MODULE: ./node_modules/css-selector-generator/build/index.js\nvar build = __webpack_require__(4766);\n;// CONCATENATED MODULE: ./src/dom.js\n//\n// Copyright 2022 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n\n\nfunction findFirstVisibleLocator() {\n var element = findElement(document.body);\n return {\n href: "#",\n type: "application/xhtml+xml",\n locations: {\n cssSelector: (0,build.getCssSelector)(element)\n },\n text: {\n highlight: element.textContent\n }\n };\n}\n\nfunction findElement(rootElement) {\n for (var i = 0; i < rootElement.children.length; i++) {\n var child = rootElement.children[i];\n\n if (!shouldIgnoreElement(child) && isElementVisible(child)) {\n return findElement(child);\n }\n }\n\n return rootElement;\n}\n\nfunction isElementVisible(element) {\n if (readium.isFixedLayout) return true;\n\n if (element === document.body || element === document.documentElement) {\n return true;\n }\n\n if (!document || !document.documentElement || !document.body) {\n return false;\n }\n\n var rect = element.getBoundingClientRect();\n\n if (isScrollModeEnabled()) {\n return rect.bottom > 0 && rect.top < window.innerHeight;\n } else {\n return rect.right > 0 && rect.left < window.innerWidth;\n }\n}\n\nfunction shouldIgnoreElement(element) {\n var elStyle = getComputedStyle(element);\n\n if (elStyle) {\n var display = elStyle.getPropertyValue("display");\n\n if (display != "block") {\n return true;\n } // Cannot be relied upon, because web browser engine reports invisible when out of view in\n // scrolled columns!\n // const visibility = elStyle.getPropertyValue("visibility");\n // if (visibility === "hidden") {\n // return false;\n // }\n\n\n var opacity = elStyle.getPropertyValue("opacity");\n\n if (opacity === "0") {\n return true;\n }\n }\n\n return false;\n}\n// EXTERNAL MODULE: ./node_modules/string.prototype.matchall/index.js\nvar string_prototype_matchall = __webpack_require__(4956);\nvar string_prototype_matchall_default = /*#__PURE__*/__webpack_require__.n(string_prototype_matchall);\n;// CONCATENATED MODULE: ./src/selection.js\n//\n// Copyright 2021 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n\n\n // Polyfill for Android API 26\n\n\nstring_prototype_matchall_default().shim();\nvar selection_debug = true; // Notify native code that the selection changes.\n\nwindow.addEventListener("load", function () {\n var isSelecting = false;\n document.addEventListener("selectionchange", function () {\n var collapsed = window.getSelection().isCollapsed;\n\n if (collapsed && isSelecting) {\n isSelecting = false;\n Android.onSelectionEnd(); // Snaps the current column in case the user shifted the scroll by dragging the text selection.\n\n snapCurrentOffset();\n } else if (!collapsed && !isSelecting) {\n isSelecting = true;\n Android.onSelectionStart();\n }\n });\n}, false);\nfunction getCurrentSelection() {\n var text = getCurrentSelectionText();\n\n if (!text) {\n return null;\n }\n\n var rect = selection_getSelectionRect();\n return {\n text: text,\n rect: rect\n };\n}\n\nfunction selection_getSelectionRect() {\n try {\n var sel = window.getSelection();\n\n if (!sel) {\n return;\n }\n\n var range = sel.getRangeAt(0);\n return toNativeRect(range.getBoundingClientRect());\n } catch (e) {\n logError(e);\n return null;\n }\n}\n\nfunction getCurrentSelectionText() {\n var selection = window.getSelection();\n\n if (!selection) {\n return undefined;\n }\n\n if (selection.isCollapsed) {\n return undefined;\n }\n\n var highlight = selection.toString();\n var cleanHighlight = highlight.trim().replace(/\\n/g, " ").replace(/\\s\\s+/g, " ");\n\n if (cleanHighlight.length === 0) {\n return undefined;\n }\n\n if (!selection.anchorNode || !selection.focusNode) {\n return undefined;\n }\n\n var range = selection.rangeCount === 1 ? selection.getRangeAt(0) : selection_createOrderedRange(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);\n\n if (!range || range.collapsed) {\n selection_log("$$$$$$$$$$$$$$$$$ CANNOT GET NON-COLLAPSED SELECTION RANGE?!");\n return undefined;\n }\n\n var text = document.body.textContent;\n var textRange = text_range_TextRange.fromRange(range).relativeTo(document.body);\n var start = textRange.start.offset;\n var end = textRange.end.offset;\n var snippetLength = 200; // Compute the text before the highlight, ignoring the first "word", which might be cut.\n\n var before = text.slice(Math.max(0, start - snippetLength), start);\n var firstWordStart = before.search(/(?:[\\0-@\\[-`\\{-\\xA9\\xAB-\\xB4\\xB6-\\xB9\\xBB-\\xBF\\xD7\\xF7\\u02C2-\\u02C5\\u02D2-\\u02DF\\u02E5-\\u02EB\\u02ED\\u02EF-\\u036F\\u0375\\u0378\\u0379\\u037E\\u0380-\\u0385\\u0387\\u038B\\u038D\\u03A2\\u03F6\\u0482-\\u0489\\u0530\\u0557\\u0558\\u055A-\\u055F\\u0589-\\u05CF\\u05EB-\\u05EE\\u05F3-\\u061F\\u064B-\\u066D\\u0670\\u06D4\\u06D6-\\u06E4\\u06E7-\\u06ED\\u06F0-\\u06F9\\u06FD\\u06FE\\u0700-\\u070F\\u0711\\u0730-\\u074C\\u07A6-\\u07B0\\u07B2-\\u07C9\\u07EB-\\u07F3\\u07F6-\\u07F9\\u07FB-\\u07FF\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u083F\\u0859-\\u085F\\u086B-\\u086F\\u0888\\u088F-\\u089F\\u08CA-\\u0903\\u093A-\\u093C\\u093E-\\u094F\\u0951-\\u0957\\u0962-\\u0970\\u0981-\\u0984\\u098D\\u098E\\u0991\\u0992\\u09A9\\u09B1\\u09B3-\\u09B5\\u09BA-\\u09BC\\u09BE-\\u09CD\\u09CF-\\u09DB\\u09DE\\u09E2-\\u09EF\\u09F2-\\u09FB\\u09FD-\\u0A04\\u0A0B-\\u0A0E\\u0A11\\u0A12\\u0A29\\u0A31\\u0A34\\u0A37\\u0A3A-\\u0A58\\u0A5D\\u0A5F-\\u0A71\\u0A75-\\u0A84\\u0A8E\\u0A92\\u0AA9\\u0AB1\\u0AB4\\u0ABA-\\u0ABC\\u0ABE-\\u0ACF\\u0AD1-\\u0ADF\\u0AE2-\\u0AF8\\u0AFA-\\u0B04\\u0B0D\\u0B0E\\u0B11\\u0B12\\u0B29\\u0B31\\u0B34\\u0B3A-\\u0B3C\\u0B3E-\\u0B5B\\u0B5E\\u0B62-\\u0B70\\u0B72-\\u0B82\\u0B84\\u0B8B-\\u0B8D\\u0B91\\u0B96-\\u0B98\\u0B9B\\u0B9D\\u0BA0-\\u0BA2\\u0BA5-\\u0BA7\\u0BAB-\\u0BAD\\u0BBA-\\u0BCF\\u0BD1-\\u0C04\\u0C0D\\u0C11\\u0C29\\u0C3A-\\u0C3C\\u0C3E-\\u0C57\\u0C5B\\u0C5C\\u0C5E\\u0C5F\\u0C62-\\u0C7F\\u0C81-\\u0C84\\u0C8D\\u0C91\\u0CA9\\u0CB4\\u0CBA-\\u0CBC\\u0CBE-\\u0CDC\\u0CDF\\u0CE2-\\u0CF0\\u0CF3-\\u0D03\\u0D0D\\u0D11\\u0D3B\\u0D3C\\u0D3E-\\u0D4D\\u0D4F-\\u0D53\\u0D57-\\u0D5E\\u0D62-\\u0D79\\u0D80-\\u0D84\\u0D97-\\u0D99\\u0DB2\\u0DBC\\u0DBE\\u0DBF\\u0DC7-\\u0E00\\u0E31\\u0E34-\\u0E3F\\u0E47-\\u0E80\\u0E83\\u0E85\\u0E8B\\u0EA4\\u0EA6\\u0EB1\\u0EB4-\\u0EBC\\u0EBE\\u0EBF\\u0EC5\\u0EC7-\\u0EDB\\u0EE0-\\u0EFF\\u0F01-\\u0F3F\\u0F48\\u0F6D-\\u0F87\\u0F8D-\\u0FFF\\u102B-\\u103E\\u1040-\\u104F\\u1056-\\u1059\\u105E-\\u1060\\u1062-\\u1064\\u1067-\\u106D\\u1071-\\u1074\\u1082-\\u108D\\u108F-\\u109F\\u10C6\\u10C8-\\u10CC\\u10CE\\u10CF\\u10FB\\u1249\\u124E\\u124F\\u1257\\u1259\\u125E\\u125F\\u1289\\u128E\\u128F\\u12B1\\u12B6\\u12B7\\u12BF\\u12C1\\u12C6\\u12C7\\u12D7\\u1311\\u1316\\u1317\\u135B-\\u137F\\u1390-\\u139F\\u13F6\\u13F7\\u13FE-\\u1400\\u166D\\u166E\\u1680\\u169B-\\u169F\\u16EB-\\u16F0\\u16F9-\\u16FF\\u1712-\\u171E\\u1732-\\u173F\\u1752-\\u175F\\u176D\\u1771-\\u177F\\u17B4-\\u17D6\\u17D8-\\u17DB\\u17DD-\\u181F\\u1879-\\u187F\\u1885\\u1886\\u18A9\\u18AB-\\u18AF\\u18F6-\\u18FF\\u191F-\\u194F\\u196E\\u196F\\u1975-\\u197F\\u19AC-\\u19AF\\u19CA-\\u19FF\\u1A17-\\u1A1F\\u1A55-\\u1AA6\\u1AA8-\\u1B04\\u1B34-\\u1B44\\u1B4D-\\u1B82\\u1BA1-\\u1BAD\\u1BB0-\\u1BB9\\u1BE6-\\u1BFF\\u1C24-\\u1C4C\\u1C50-\\u1C59\\u1C7E\\u1C7F\\u1C89-\\u1C8F\\u1CBB\\u1CBC\\u1CC0-\\u1CE8\\u1CED\\u1CF4\\u1CF7-\\u1CF9\\u1CFB-\\u1CFF\\u1DC0-\\u1DFF\\u1F16\\u1F17\\u1F1E\\u1F1F\\u1F46\\u1F47\\u1F4E\\u1F4F\\u1F58\\u1F5A\\u1F5C\\u1F5E\\u1F7E\\u1F7F\\u1FB5\\u1FBD\\u1FBF-\\u1FC1\\u1FC5\\u1FCD-\\u1FCF\\u1FD4\\u1FD5\\u1FDC-\\u1FDF\\u1FED-\\u1FF1\\u1FF5\\u1FFD-\\u2070\\u2072-\\u207E\\u2080-\\u208F\\u209D-\\u2101\\u2103-\\u2106\\u2108\\u2109\\u2114\\u2116-\\u2118\\u211E-\\u2123\\u2125\\u2127\\u2129\\u212E\\u213A\\u213B\\u2140-\\u2144\\u214A-\\u214D\\u214F-\\u2182\\u2185-\\u2BFF\\u2CE5-\\u2CEA\\u2CEF-\\u2CF1\\u2CF4-\\u2CFF\\u2D26\\u2D28-\\u2D2C\\u2D2E\\u2D2F\\u2D68-\\u2D6E\\u2D70-\\u2D7F\\u2D97-\\u2D9F\\u2DA7\\u2DAF\\u2DB7\\u2DBF\\u2DC7\\u2DCF\\u2DD7\\u2DDF-\\u2E2E\\u2E30-\\u3004\\u3007-\\u3030\\u3036-\\u303A\\u303D-\\u3040\\u3097-\\u309C\\u30A0\\u30FB\\u3100-\\u3104\\u3130\\u318F-\\u319F\\u31C0-\\u31EF\\u3200-\\u33FF\\u4DC0-\\u4DFF\\uA48D-\\uA4CF\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA620-\\uA629\\uA62C-\\uA63F\\uA66F-\\uA67E\\uA69E\\uA69F\\uA6E6-\\uA716\\uA720\\uA721\\uA789\\uA78A\\uA7CB-\\uA7CF\\uA7D2\\uA7D4\\uA7DA-\\uA7F1\\uA802\\uA806\\uA80B\\uA823-\\uA83F\\uA874-\\uA881\\uA8B4-\\uA8F1\\uA8F8-\\uA8FA\\uA8FC\\uA8FF-\\uA909\\uA926-\\uA92F\\uA947-\\uA95F\\uA97D-\\uA983\\uA9B3-\\uA9CE\\uA9D0-\\uA9DF\\uA9E5\\uA9F0-\\uA9F9\\uA9FF\\uAA29-\\uAA3F\\uAA43\\uAA4C-\\uAA5F\\uAA77-\\uAA79\\uAA7B-\\uAA7D\\uAAB0\\uAAB2-\\uAAB4\\uAAB7\\uAAB8\\uAABE\\uAABF\\uAAC1\\uAAC3-\\uAADA\\uAADE\\uAADF\\uAAEB-\\uAAF1\\uAAF5-\\uAB00\\uAB07\\uAB08\\uAB0F\\uAB10\\uAB17-\\uAB1F\\uAB27\\uAB2F\\uAB5B\\uAB6A-\\uAB6F\\uABE3-\\uABFF\\uD7A4-\\uD7AF\\uD7C7-\\uD7CA\\uD7FC-\\uD7FF\\uE000-\\uF8FF\\uFA6E\\uFA6F\\uFADA-\\uFAFF\\uFB07-\\uFB12\\uFB18-\\uFB1C\\uFB1E\\uFB29\\uFB37\\uFB3D\\uFB3F\\uFB42\\uFB45\\uFBB2-\\uFBD2\\uFD3E-\\uFD4F\\uFD90\\uFD91\\uFDC8-\\uFDEF\\uFDFC-\\uFE6F\\uFE75\\uFEFD-\\uFF20\\uFF3B-\\uFF40\\uFF5B-\\uFF65\\uFFBF-\\uFFC1\\uFFC8\\uFFC9\\uFFD0\\uFFD1\\uFFD8\\uFFD9\\uFFDD-\\uFFFF]|\\uD800[\\uDC0C\\uDC27\\uDC3B\\uDC3E\\uDC4E\\uDC4F\\uDC5E-\\uDC7F\\uDCFB-\\uDE7F\\uDE9D-\\uDE9F\\uDED1-\\uDEFF\\uDF20-\\uDF2C\\uDF41\\uDF4A-\\uDF4F\\uDF76-\\uDF7F\\uDF9E\\uDF9F\\uDFC4-\\uDFC7\\uDFD0-\\uDFFF]|\\uD801[\\uDC9E-\\uDCAF\\uDCD4-\\uDCD7\\uDCFC-\\uDCFF\\uDD28-\\uDD2F\\uDD64-\\uDD6F\\uDD7B\\uDD8B\\uDD93\\uDD96\\uDDA2\\uDDB2\\uDDBA\\uDDBD-\\uDDFF\\uDF37-\\uDF3F\\uDF56-\\uDF5F\\uDF68-\\uDF7F\\uDF86\\uDFB1\\uDFBB-\\uDFFF]|\\uD802[\\uDC06\\uDC07\\uDC09\\uDC36\\uDC39-\\uDC3B\\uDC3D\\uDC3E\\uDC56-\\uDC5F\\uDC77-\\uDC7F\\uDC9F-\\uDCDF\\uDCF3\\uDCF6-\\uDCFF\\uDD16-\\uDD1F\\uDD3A-\\uDD7F\\uDDB8-\\uDDBD\\uDDC0-\\uDDFF\\uDE01-\\uDE0F\\uDE14\\uDE18\\uDE36-\\uDE5F\\uDE7D-\\uDE7F\\uDE9D-\\uDEBF\\uDEC8\\uDEE5-\\uDEFF\\uDF36-\\uDF3F\\uDF56-\\uDF5F\\uDF73-\\uDF7F\\uDF92-\\uDFFF]|\\uD803[\\uDC49-\\uDC7F\\uDCB3-\\uDCBF\\uDCF3-\\uDCFF\\uDD24-\\uDE7F\\uDEAA-\\uDEAF\\uDEB2-\\uDEFF\\uDF1D-\\uDF26\\uDF28-\\uDF2F\\uDF46-\\uDF6F\\uDF82-\\uDFAF\\uDFC5-\\uDFDF\\uDFF7-\\uDFFF]|\\uD804[\\uDC00-\\uDC02\\uDC38-\\uDC70\\uDC73\\uDC74\\uDC76-\\uDC82\\uDCB0-\\uDCCF\\uDCE9-\\uDD02\\uDD27-\\uDD43\\uDD45\\uDD46\\uDD48-\\uDD4F\\uDD73-\\uDD75\\uDD77-\\uDD82\\uDDB3-\\uDDC0\\uDDC5-\\uDDD9\\uDDDB\\uDDDD-\\uDDFF\\uDE12\\uDE2C-\\uDE7F\\uDE87\\uDE89\\uDE8E\\uDE9E\\uDEA9-\\uDEAF\\uDEDF-\\uDF04\\uDF0D\\uDF0E\\uDF11\\uDF12\\uDF29\\uDF31\\uDF34\\uDF3A-\\uDF3C\\uDF3E-\\uDF4F\\uDF51-\\uDF5C\\uDF62-\\uDFFF]|\\uD805[\\uDC35-\\uDC46\\uDC4B-\\uDC5E\\uDC62-\\uDC7F\\uDCB0-\\uDCC3\\uDCC6\\uDCC8-\\uDD7F\\uDDAF-\\uDDD7\\uDDDC-\\uDDFF\\uDE30-\\uDE43\\uDE45-\\uDE7F\\uDEAB-\\uDEB7\\uDEB9-\\uDEFF\\uDF1B-\\uDF3F\\uDF47-\\uDFFF]|\\uD806[\\uDC2C-\\uDC9F\\uDCE0-\\uDCFE\\uDD07\\uDD08\\uDD0A\\uDD0B\\uDD14\\uDD17\\uDD30-\\uDD3E\\uDD40\\uDD42-\\uDD9F\\uDDA8\\uDDA9\\uDDD1-\\uDDE0\\uDDE2\\uDDE4-\\uDDFF\\uDE01-\\uDE0A\\uDE33-\\uDE39\\uDE3B-\\uDE4F\\uDE51-\\uDE5B\\uDE8A-\\uDE9C\\uDE9E-\\uDEAF\\uDEF9-\\uDFFF]|\\uD807[\\uDC09\\uDC2F-\\uDC3F\\uDC41-\\uDC71\\uDC90-\\uDCFF\\uDD07\\uDD0A\\uDD31-\\uDD45\\uDD47-\\uDD5F\\uDD66\\uDD69\\uDD8A-\\uDD97\\uDD99-\\uDEDF\\uDEF3-\\uDFAF\\uDFB1-\\uDFFF]|\\uD808[\\uDF9A-\\uDFFF]|\\uD809[\\uDC00-\\uDC7F\\uDD44-\\uDFFF]|[\\uD80A\\uD80E-\\uD810\\uD812-\\uD819\\uD824-\\uD82A\\uD82D\\uD82E\\uD830-\\uD834\\uD836\\uD83C-\\uD83F\\uD87B-\\uD87D\\uD87F\\uD885-\\uDBFF][\\uDC00-\\uDFFF]|\\uD80B[\\uDC00-\\uDF8F\\uDFF1-\\uDFFF]|\\uD80D[\\uDC2F-\\uDFFF]|\\uD811[\\uDE47-\\uDFFF]|\\uD81A[\\uDE39-\\uDE3F\\uDE5F-\\uDE6F\\uDEBF-\\uDECF\\uDEEE-\\uDEFF\\uDF30-\\uDF3F\\uDF44-\\uDF62\\uDF78-\\uDF7C\\uDF90-\\uDFFF]|\\uD81B[\\uDC00-\\uDE3F\\uDE80-\\uDEFF\\uDF4B-\\uDF4F\\uDF51-\\uDF92\\uDFA0-\\uDFDF\\uDFE2\\uDFE4-\\uDFFF]|\\uD821[\\uDFF8-\\uDFFF]|\\uD823[\\uDCD6-\\uDCFF\\uDD09-\\uDFFF]|\\uD82B[\\uDC00-\\uDFEF\\uDFF4\\uDFFC\\uDFFF]|\\uD82C[\\uDD23-\\uDD4F\\uDD53-\\uDD63\\uDD68-\\uDD6F\\uDEFC-\\uDFFF]|\\uD82F[\\uDC6B-\\uDC6F\\uDC7D-\\uDC7F\\uDC89-\\uDC8F\\uDC9A-\\uDFFF]|\\uD835[\\uDC55\\uDC9D\\uDCA0\\uDCA1\\uDCA3\\uDCA4\\uDCA7\\uDCA8\\uDCAD\\uDCBA\\uDCBC\\uDCC4\\uDD06\\uDD0B\\uDD0C\\uDD15\\uDD1D\\uDD3A\\uDD3F\\uDD45\\uDD47-\\uDD49\\uDD51\\uDEA6\\uDEA7\\uDEC1\\uDEDB\\uDEFB\\uDF15\\uDF35\\uDF4F\\uDF6F\\uDF89\\uDFA9\\uDFC3\\uDFCC-\\uDFFF]|\\uD837[\\uDC00-\\uDEFF\\uDF1F-\\uDFFF]|\\uD838[\\uDC00-\\uDCFF\\uDD2D-\\uDD36\\uDD3E-\\uDD4D\\uDD4F-\\uDE8F\\uDEAE-\\uDEBF\\uDEEC-\\uDFFF]|\\uD839[\\uDC00-\\uDFDF\\uDFE7\\uDFEC\\uDFEF\\uDFFF]|\\uD83A[\\uDCC5-\\uDCFF\\uDD44-\\uDD4A\\uDD4C-\\uDFFF]|\\uD83B[\\uDC00-\\uDDFF\\uDE04\\uDE20\\uDE23\\uDE25\\uDE26\\uDE28\\uDE33\\uDE38\\uDE3A\\uDE3C-\\uDE41\\uDE43-\\uDE46\\uDE48\\uDE4A\\uDE4C\\uDE50\\uDE53\\uDE55\\uDE56\\uDE58\\uDE5A\\uDE5C\\uDE5E\\uDE60\\uDE63\\uDE65\\uDE66\\uDE6B\\uDE73\\uDE78\\uDE7D\\uDE7F\\uDE8A\\uDE9C-\\uDEA0\\uDEA4\\uDEAA\\uDEBC-\\uDFFF]|\\uD869[\\uDEE0-\\uDEFF]|\\uD86D[\\uDF39-\\uDF3F]|\\uD86E[\\uDC1E\\uDC1F]|\\uD873[\\uDEA2-\\uDEAF]|\\uD87A[\\uDFE1-\\uDFFF]|\\uD87E[\\uDE1E-\\uDFFF]|\\uD884[\\uDF4B-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF])(?:[A-Za-z\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088E\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5D\\u0C60\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C88\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183\\u2184\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005\\u3006\\u3031-\\u3035\\u303B\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7CA\\uA7D0\\uA7D1\\uA7D3\\uA7D5-\\uA7D9\\uA7F2-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC]|\\uD800[\\uDC00-\\uDC0B\\uDC0D-\\uDC26\\uDC28-\\uDC3A\\uDC3C\\uDC3D\\uDC3F-\\uDC4D\\uDC50-\\uDC5D\\uDC80-\\uDCFA\\uDE80-\\uDE9C\\uDEA0-\\uDED0\\uDF00-\\uDF1F\\uDF2D-\\uDF40\\uDF42-\\uDF49\\uDF50-\\uDF75\\uDF80-\\uDF9D\\uDFA0-\\uDFC3\\uDFC8-\\uDFCF]|\\uD801[\\uDC00-\\uDC9D\\uDCB0-\\uDCD3\\uDCD8-\\uDCFB\\uDD00-\\uDD27\\uDD30-\\uDD63\\uDD70-\\uDD7A\\uDD7C-\\uDD8A\\uDD8C-\\uDD92\\uDD94\\uDD95\\uDD97-\\uDDA1\\uDDA3-\\uDDB1\\uDDB3-\\uDDB9\\uDDBB\\uDDBC\\uDE00-\\uDF36\\uDF40-\\uDF55\\uDF60-\\uDF67\\uDF80-\\uDF85\\uDF87-\\uDFB0\\uDFB2-\\uDFBA]|\\uD802[\\uDC00-\\uDC05\\uDC08\\uDC0A-\\uDC35\\uDC37\\uDC38\\uDC3C\\uDC3F-\\uDC55\\uDC60-\\uDC76\\uDC80-\\uDC9E\\uDCE0-\\uDCF2\\uDCF4\\uDCF5\\uDD00-\\uDD15\\uDD20-\\uDD39\\uDD80-\\uDDB7\\uDDBE\\uDDBF\\uDE00\\uDE10-\\uDE13\\uDE15-\\uDE17\\uDE19-\\uDE35\\uDE60-\\uDE7C\\uDE80-\\uDE9C\\uDEC0-\\uDEC7\\uDEC9-\\uDEE4\\uDF00-\\uDF35\\uDF40-\\uDF55\\uDF60-\\uDF72\\uDF80-\\uDF91]|\\uD803[\\uDC00-\\uDC48\\uDC80-\\uDCB2\\uDCC0-\\uDCF2\\uDD00-\\uDD23\\uDE80-\\uDEA9\\uDEB0\\uDEB1\\uDF00-\\uDF1C\\uDF27\\uDF30-\\uDF45\\uDF70-\\uDF81\\uDFB0-\\uDFC4\\uDFE0-\\uDFF6]|\\uD804[\\uDC03-\\uDC37\\uDC71\\uDC72\\uDC75\\uDC83-\\uDCAF\\uDCD0-\\uDCE8\\uDD03-\\uDD26\\uDD44\\uDD47\\uDD50-\\uDD72\\uDD76\\uDD83-\\uDDB2\\uDDC1-\\uDDC4\\uDDDA\\uDDDC\\uDE00-\\uDE11\\uDE13-\\uDE2B\\uDE80-\\uDE86\\uDE88\\uDE8A-\\uDE8D\\uDE8F-\\uDE9D\\uDE9F-\\uDEA8\\uDEB0-\\uDEDE\\uDF05-\\uDF0C\\uDF0F\\uDF10\\uDF13-\\uDF28\\uDF2A-\\uDF30\\uDF32\\uDF33\\uDF35-\\uDF39\\uDF3D\\uDF50\\uDF5D-\\uDF61]|\\uD805[\\uDC00-\\uDC34\\uDC47-\\uDC4A\\uDC5F-\\uDC61\\uDC80-\\uDCAF\\uDCC4\\uDCC5\\uDCC7\\uDD80-\\uDDAE\\uDDD8-\\uDDDB\\uDE00-\\uDE2F\\uDE44\\uDE80-\\uDEAA\\uDEB8\\uDF00-\\uDF1A\\uDF40-\\uDF46]|\\uD806[\\uDC00-\\uDC2B\\uDCA0-\\uDCDF\\uDCFF-\\uDD06\\uDD09\\uDD0C-\\uDD13\\uDD15\\uDD16\\uDD18-\\uDD2F\\uDD3F\\uDD41\\uDDA0-\\uDDA7\\uDDAA-\\uDDD0\\uDDE1\\uDDE3\\uDE00\\uDE0B-\\uDE32\\uDE3A\\uDE50\\uDE5C-\\uDE89\\uDE9D\\uDEB0-\\uDEF8]|\\uD807[\\uDC00-\\uDC08\\uDC0A-\\uDC2E\\uDC40\\uDC72-\\uDC8F\\uDD00-\\uDD06\\uDD08\\uDD09\\uDD0B-\\uDD30\\uDD46\\uDD60-\\uDD65\\uDD67\\uDD68\\uDD6A-\\uDD89\\uDD98\\uDEE0-\\uDEF2\\uDFB0]|\\uD808[\\uDC00-\\uDF99]|\\uD809[\\uDC80-\\uDD43]|\\uD80B[\\uDF90-\\uDFF0]|[\\uD80C\\uD81C-\\uD820\\uD822\\uD840-\\uD868\\uD86A-\\uD86C\\uD86F-\\uD872\\uD874-\\uD879\\uD880-\\uD883][\\uDC00-\\uDFFF]|\\uD80D[\\uDC00-\\uDC2E]|\\uD811[\\uDC00-\\uDE46]|\\uD81A[\\uDC00-\\uDE38\\uDE40-\\uDE5E\\uDE70-\\uDEBE\\uDED0-\\uDEED\\uDF00-\\uDF2F\\uDF40-\\uDF43\\uDF63-\\uDF77\\uDF7D-\\uDF8F]|\\uD81B[\\uDE40-\\uDE7F\\uDF00-\\uDF4A\\uDF50\\uDF93-\\uDF9F\\uDFE0\\uDFE1\\uDFE3]|\\uD821[\\uDC00-\\uDFF7]|\\uD823[\\uDC00-\\uDCD5\\uDD00-\\uDD08]|\\uD82B[\\uDFF0-\\uDFF3\\uDFF5-\\uDFFB\\uDFFD\\uDFFE]|\\uD82C[\\uDC00-\\uDD22\\uDD50-\\uDD52\\uDD64-\\uDD67\\uDD70-\\uDEFB]|\\uD82F[\\uDC00-\\uDC6A\\uDC70-\\uDC7C\\uDC80-\\uDC88\\uDC90-\\uDC99]|\\uD835[\\uDC00-\\uDC54\\uDC56-\\uDC9C\\uDC9E\\uDC9F\\uDCA2\\uDCA5\\uDCA6\\uDCA9-\\uDCAC\\uDCAE-\\uDCB9\\uDCBB\\uDCBD-\\uDCC3\\uDCC5-\\uDD05\\uDD07-\\uDD0A\\uDD0D-\\uDD14\\uDD16-\\uDD1C\\uDD1E-\\uDD39\\uDD3B-\\uDD3E\\uDD40-\\uDD44\\uDD46\\uDD4A-\\uDD50\\uDD52-\\uDEA5\\uDEA8-\\uDEC0\\uDEC2-\\uDEDA\\uDEDC-\\uDEFA\\uDEFC-\\uDF14\\uDF16-\\uDF34\\uDF36-\\uDF4E\\uDF50-\\uDF6E\\uDF70-\\uDF88\\uDF8A-\\uDFA8\\uDFAA-\\uDFC2\\uDFC4-\\uDFCB]|\\uD837[\\uDF00-\\uDF1E]|\\uD838[\\uDD00-\\uDD2C\\uDD37-\\uDD3D\\uDD4E\\uDE90-\\uDEAD\\uDEC0-\\uDEEB]|\\uD839[\\uDFE0-\\uDFE6\\uDFE8-\\uDFEB\\uDFED\\uDFEE\\uDFF0-\\uDFFE]|\\uD83A[\\uDC00-\\uDCC4\\uDD00-\\uDD43\\uDD4B]|\\uD83B[\\uDE00-\\uDE03\\uDE05-\\uDE1F\\uDE21\\uDE22\\uDE24\\uDE27\\uDE29-\\uDE32\\uDE34-\\uDE37\\uDE39\\uDE3B\\uDE42\\uDE47\\uDE49\\uDE4B\\uDE4D-\\uDE4F\\uDE51\\uDE52\\uDE54\\uDE57\\uDE59\\uDE5B\\uDE5D\\uDE5F\\uDE61\\uDE62\\uDE64\\uDE67-\\uDE6A\\uDE6C-\\uDE72\\uDE74-\\uDE77\\uDE79-\\uDE7C\\uDE7E\\uDE80-\\uDE89\\uDE8B-\\uDE9B\\uDEA1-\\uDEA3\\uDEA5-\\uDEA9\\uDEAB-\\uDEBB]|\\uD869[\\uDC00-\\uDEDF\\uDF00-\\uDFFF]|\\uD86D[\\uDC00-\\uDF38\\uDF40-\\uDFFF]|\\uD86E[\\uDC00-\\uDC1D\\uDC20-\\uDFFF]|\\uD873[\\uDC00-\\uDEA1\\uDEB0-\\uDFFF]|\\uD87A[\\uDC00-\\uDFE0]|\\uD87E[\\uDC00-\\uDE1D]|\\uD884[\\uDC00-\\uDF4A])/g);\n\n if (firstWordStart !== -1) {\n before = before.slice(firstWordStart + 1);\n } // Compute the text after the highlight, ignoring the last "word", which might be cut.\n\n\n var after = text.slice(end, Math.min(text.length, end + snippetLength));\n var lastWordEnd = Array.from(after.matchAll(/(?:[A-Za-z\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088E\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5D\\u0C60\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C88\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183\\u2184\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005\\u3006\\u3031-\\u3035\\u303B\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7CA\\uA7D0\\uA7D1\\uA7D3\\uA7D5-\\uA7D9\\uA7F2-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC]|\\uD800[\\uDC00-\\uDC0B\\uDC0D-\\uDC26\\uDC28-\\uDC3A\\uDC3C\\uDC3D\\uDC3F-\\uDC4D\\uDC50-\\uDC5D\\uDC80-\\uDCFA\\uDE80-\\uDE9C\\uDEA0-\\uDED0\\uDF00-\\uDF1F\\uDF2D-\\uDF40\\uDF42-\\uDF49\\uDF50-\\uDF75\\uDF80-\\uDF9D\\uDFA0-\\uDFC3\\uDFC8-\\uDFCF]|\\uD801[\\uDC00-\\uDC9D\\uDCB0-\\uDCD3\\uDCD8-\\uDCFB\\uDD00-\\uDD27\\uDD30-\\uDD63\\uDD70-\\uDD7A\\uDD7C-\\uDD8A\\uDD8C-\\uDD92\\uDD94\\uDD95\\uDD97-\\uDDA1\\uDDA3-\\uDDB1\\uDDB3-\\uDDB9\\uDDBB\\uDDBC\\uDE00-\\uDF36\\uDF40-\\uDF55\\uDF60-\\uDF67\\uDF80-\\uDF85\\uDF87-\\uDFB0\\uDFB2-\\uDFBA]|\\uD802[\\uDC00-\\uDC05\\uDC08\\uDC0A-\\uDC35\\uDC37\\uDC38\\uDC3C\\uDC3F-\\uDC55\\uDC60-\\uDC76\\uDC80-\\uDC9E\\uDCE0-\\uDCF2\\uDCF4\\uDCF5\\uDD00-\\uDD15\\uDD20-\\uDD39\\uDD80-\\uDDB7\\uDDBE\\uDDBF\\uDE00\\uDE10-\\uDE13\\uDE15-\\uDE17\\uDE19-\\uDE35\\uDE60-\\uDE7C\\uDE80-\\uDE9C\\uDEC0-\\uDEC7\\uDEC9-\\uDEE4\\uDF00-\\uDF35\\uDF40-\\uDF55\\uDF60-\\uDF72\\uDF80-\\uDF91]|\\uD803[\\uDC00-\\uDC48\\uDC80-\\uDCB2\\uDCC0-\\uDCF2\\uDD00-\\uDD23\\uDE80-\\uDEA9\\uDEB0\\uDEB1\\uDF00-\\uDF1C\\uDF27\\uDF30-\\uDF45\\uDF70-\\uDF81\\uDFB0-\\uDFC4\\uDFE0-\\uDFF6]|\\uD804[\\uDC03-\\uDC37\\uDC71\\uDC72\\uDC75\\uDC83-\\uDCAF\\uDCD0-\\uDCE8\\uDD03-\\uDD26\\uDD44\\uDD47\\uDD50-\\uDD72\\uDD76\\uDD83-\\uDDB2\\uDDC1-\\uDDC4\\uDDDA\\uDDDC\\uDE00-\\uDE11\\uDE13-\\uDE2B\\uDE80-\\uDE86\\uDE88\\uDE8A-\\uDE8D\\uDE8F-\\uDE9D\\uDE9F-\\uDEA8\\uDEB0-\\uDEDE\\uDF05-\\uDF0C\\uDF0F\\uDF10\\uDF13-\\uDF28\\uDF2A-\\uDF30\\uDF32\\uDF33\\uDF35-\\uDF39\\uDF3D\\uDF50\\uDF5D-\\uDF61]|\\uD805[\\uDC00-\\uDC34\\uDC47-\\uDC4A\\uDC5F-\\uDC61\\uDC80-\\uDCAF\\uDCC4\\uDCC5\\uDCC7\\uDD80-\\uDDAE\\uDDD8-\\uDDDB\\uDE00-\\uDE2F\\uDE44\\uDE80-\\uDEAA\\uDEB8\\uDF00-\\uDF1A\\uDF40-\\uDF46]|\\uD806[\\uDC00-\\uDC2B\\uDCA0-\\uDCDF\\uDCFF-\\uDD06\\uDD09\\uDD0C-\\uDD13\\uDD15\\uDD16\\uDD18-\\uDD2F\\uDD3F\\uDD41\\uDDA0-\\uDDA7\\uDDAA-\\uDDD0\\uDDE1\\uDDE3\\uDE00\\uDE0B-\\uDE32\\uDE3A\\uDE50\\uDE5C-\\uDE89\\uDE9D\\uDEB0-\\uDEF8]|\\uD807[\\uDC00-\\uDC08\\uDC0A-\\uDC2E\\uDC40\\uDC72-\\uDC8F\\uDD00-\\uDD06\\uDD08\\uDD09\\uDD0B-\\uDD30\\uDD46\\uDD60-\\uDD65\\uDD67\\uDD68\\uDD6A-\\uDD89\\uDD98\\uDEE0-\\uDEF2\\uDFB0]|\\uD808[\\uDC00-\\uDF99]|\\uD809[\\uDC80-\\uDD43]|\\uD80B[\\uDF90-\\uDFF0]|[\\uD80C\\uD81C-\\uD820\\uD822\\uD840-\\uD868\\uD86A-\\uD86C\\uD86F-\\uD872\\uD874-\\uD879\\uD880-\\uD883][\\uDC00-\\uDFFF]|\\uD80D[\\uDC00-\\uDC2E]|\\uD811[\\uDC00-\\uDE46]|\\uD81A[\\uDC00-\\uDE38\\uDE40-\\uDE5E\\uDE70-\\uDEBE\\uDED0-\\uDEED\\uDF00-\\uDF2F\\uDF40-\\uDF43\\uDF63-\\uDF77\\uDF7D-\\uDF8F]|\\uD81B[\\uDE40-\\uDE7F\\uDF00-\\uDF4A\\uDF50\\uDF93-\\uDF9F\\uDFE0\\uDFE1\\uDFE3]|\\uD821[\\uDC00-\\uDFF7]|\\uD823[\\uDC00-\\uDCD5\\uDD00-\\uDD08]|\\uD82B[\\uDFF0-\\uDFF3\\uDFF5-\\uDFFB\\uDFFD\\uDFFE]|\\uD82C[\\uDC00-\\uDD22\\uDD50-\\uDD52\\uDD64-\\uDD67\\uDD70-\\uDEFB]|\\uD82F[\\uDC00-\\uDC6A\\uDC70-\\uDC7C\\uDC80-\\uDC88\\uDC90-\\uDC99]|\\uD835[\\uDC00-\\uDC54\\uDC56-\\uDC9C\\uDC9E\\uDC9F\\uDCA2\\uDCA5\\uDCA6\\uDCA9-\\uDCAC\\uDCAE-\\uDCB9\\uDCBB\\uDCBD-\\uDCC3\\uDCC5-\\uDD05\\uDD07-\\uDD0A\\uDD0D-\\uDD14\\uDD16-\\uDD1C\\uDD1E-\\uDD39\\uDD3B-\\uDD3E\\uDD40-\\uDD44\\uDD46\\uDD4A-\\uDD50\\uDD52-\\uDEA5\\uDEA8-\\uDEC0\\uDEC2-\\uDEDA\\uDEDC-\\uDEFA\\uDEFC-\\uDF14\\uDF16-\\uDF34\\uDF36-\\uDF4E\\uDF50-\\uDF6E\\uDF70-\\uDF88\\uDF8A-\\uDFA8\\uDFAA-\\uDFC2\\uDFC4-\\uDFCB]|\\uD837[\\uDF00-\\uDF1E]|\\uD838[\\uDD00-\\uDD2C\\uDD37-\\uDD3D\\uDD4E\\uDE90-\\uDEAD\\uDEC0-\\uDEEB]|\\uD839[\\uDFE0-\\uDFE6\\uDFE8-\\uDFEB\\uDFED\\uDFEE\\uDFF0-\\uDFFE]|\\uD83A[\\uDC00-\\uDCC4\\uDD00-\\uDD43\\uDD4B]|\\uD83B[\\uDE00-\\uDE03\\uDE05-\\uDE1F\\uDE21\\uDE22\\uDE24\\uDE27\\uDE29-\\uDE32\\uDE34-\\uDE37\\uDE39\\uDE3B\\uDE42\\uDE47\\uDE49\\uDE4B\\uDE4D-\\uDE4F\\uDE51\\uDE52\\uDE54\\uDE57\\uDE59\\uDE5B\\uDE5D\\uDE5F\\uDE61\\uDE62\\uDE64\\uDE67-\\uDE6A\\uDE6C-\\uDE72\\uDE74-\\uDE77\\uDE79-\\uDE7C\\uDE7E\\uDE80-\\uDE89\\uDE8B-\\uDE9B\\uDEA1-\\uDEA3\\uDEA5-\\uDEA9\\uDEAB-\\uDEBB]|\\uD869[\\uDC00-\\uDEDF\\uDF00-\\uDFFF]|\\uD86D[\\uDC00-\\uDF38\\uDF40-\\uDFFF]|\\uD86E[\\uDC00-\\uDC1D\\uDC20-\\uDFFF]|\\uD873[\\uDC00-\\uDEA1\\uDEB0-\\uDFFF]|\\uD87A[\\uDC00-\\uDFE0]|\\uD87E[\\uDC00-\\uDE1D]|\\uD884[\\uDC00-\\uDF4A])(?:[\\0-@\\[-`\\{-\\xA9\\xAB-\\xB4\\xB6-\\xB9\\xBB-\\xBF\\xD7\\xF7\\u02C2-\\u02C5\\u02D2-\\u02DF\\u02E5-\\u02EB\\u02ED\\u02EF-\\u036F\\u0375\\u0378\\u0379\\u037E\\u0380-\\u0385\\u0387\\u038B\\u038D\\u03A2\\u03F6\\u0482-\\u0489\\u0530\\u0557\\u0558\\u055A-\\u055F\\u0589-\\u05CF\\u05EB-\\u05EE\\u05F3-\\u061F\\u064B-\\u066D\\u0670\\u06D4\\u06D6-\\u06E4\\u06E7-\\u06ED\\u06F0-\\u06F9\\u06FD\\u06FE\\u0700-\\u070F\\u0711\\u0730-\\u074C\\u07A6-\\u07B0\\u07B2-\\u07C9\\u07EB-\\u07F3\\u07F6-\\u07F9\\u07FB-\\u07FF\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u083F\\u0859-\\u085F\\u086B-\\u086F\\u0888\\u088F-\\u089F\\u08CA-\\u0903\\u093A-\\u093C\\u093E-\\u094F\\u0951-\\u0957\\u0962-\\u0970\\u0981-\\u0984\\u098D\\u098E\\u0991\\u0992\\u09A9\\u09B1\\u09B3-\\u09B5\\u09BA-\\u09BC\\u09BE-\\u09CD\\u09CF-\\u09DB\\u09DE\\u09E2-\\u09EF\\u09F2-\\u09FB\\u09FD-\\u0A04\\u0A0B-\\u0A0E\\u0A11\\u0A12\\u0A29\\u0A31\\u0A34\\u0A37\\u0A3A-\\u0A58\\u0A5D\\u0A5F-\\u0A71\\u0A75-\\u0A84\\u0A8E\\u0A92\\u0AA9\\u0AB1\\u0AB4\\u0ABA-\\u0ABC\\u0ABE-\\u0ACF\\u0AD1-\\u0ADF\\u0AE2-\\u0AF8\\u0AFA-\\u0B04\\u0B0D\\u0B0E\\u0B11\\u0B12\\u0B29\\u0B31\\u0B34\\u0B3A-\\u0B3C\\u0B3E-\\u0B5B\\u0B5E\\u0B62-\\u0B70\\u0B72-\\u0B82\\u0B84\\u0B8B-\\u0B8D\\u0B91\\u0B96-\\u0B98\\u0B9B\\u0B9D\\u0BA0-\\u0BA2\\u0BA5-\\u0BA7\\u0BAB-\\u0BAD\\u0BBA-\\u0BCF\\u0BD1-\\u0C04\\u0C0D\\u0C11\\u0C29\\u0C3A-\\u0C3C\\u0C3E-\\u0C57\\u0C5B\\u0C5C\\u0C5E\\u0C5F\\u0C62-\\u0C7F\\u0C81-\\u0C84\\u0C8D\\u0C91\\u0CA9\\u0CB4\\u0CBA-\\u0CBC\\u0CBE-\\u0CDC\\u0CDF\\u0CE2-\\u0CF0\\u0CF3-\\u0D03\\u0D0D\\u0D11\\u0D3B\\u0D3C\\u0D3E-\\u0D4D\\u0D4F-\\u0D53\\u0D57-\\u0D5E\\u0D62-\\u0D79\\u0D80-\\u0D84\\u0D97-\\u0D99\\u0DB2\\u0DBC\\u0DBE\\u0DBF\\u0DC7-\\u0E00\\u0E31\\u0E34-\\u0E3F\\u0E47-\\u0E80\\u0E83\\u0E85\\u0E8B\\u0EA4\\u0EA6\\u0EB1\\u0EB4-\\u0EBC\\u0EBE\\u0EBF\\u0EC5\\u0EC7-\\u0EDB\\u0EE0-\\u0EFF\\u0F01-\\u0F3F\\u0F48\\u0F6D-\\u0F87\\u0F8D-\\u0FFF\\u102B-\\u103E\\u1040-\\u104F\\u1056-\\u1059\\u105E-\\u1060\\u1062-\\u1064\\u1067-\\u106D\\u1071-\\u1074\\u1082-\\u108D\\u108F-\\u109F\\u10C6\\u10C8-\\u10CC\\u10CE\\u10CF\\u10FB\\u1249\\u124E\\u124F\\u1257\\u1259\\u125E\\u125F\\u1289\\u128E\\u128F\\u12B1\\u12B6\\u12B7\\u12BF\\u12C1\\u12C6\\u12C7\\u12D7\\u1311\\u1316\\u1317\\u135B-\\u137F\\u1390-\\u139F\\u13F6\\u13F7\\u13FE-\\u1400\\u166D\\u166E\\u1680\\u169B-\\u169F\\u16EB-\\u16F0\\u16F9-\\u16FF\\u1712-\\u171E\\u1732-\\u173F\\u1752-\\u175F\\u176D\\u1771-\\u177F\\u17B4-\\u17D6\\u17D8-\\u17DB\\u17DD-\\u181F\\u1879-\\u187F\\u1885\\u1886\\u18A9\\u18AB-\\u18AF\\u18F6-\\u18FF\\u191F-\\u194F\\u196E\\u196F\\u1975-\\u197F\\u19AC-\\u19AF\\u19CA-\\u19FF\\u1A17-\\u1A1F\\u1A55-\\u1AA6\\u1AA8-\\u1B04\\u1B34-\\u1B44\\u1B4D-\\u1B82\\u1BA1-\\u1BAD\\u1BB0-\\u1BB9\\u1BE6-\\u1BFF\\u1C24-\\u1C4C\\u1C50-\\u1C59\\u1C7E\\u1C7F\\u1C89-\\u1C8F\\u1CBB\\u1CBC\\u1CC0-\\u1CE8\\u1CED\\u1CF4\\u1CF7-\\u1CF9\\u1CFB-\\u1CFF\\u1DC0-\\u1DFF\\u1F16\\u1F17\\u1F1E\\u1F1F\\u1F46\\u1F47\\u1F4E\\u1F4F\\u1F58\\u1F5A\\u1F5C\\u1F5E\\u1F7E\\u1F7F\\u1FB5\\u1FBD\\u1FBF-\\u1FC1\\u1FC5\\u1FCD-\\u1FCF\\u1FD4\\u1FD5\\u1FDC-\\u1FDF\\u1FED-\\u1FF1\\u1FF5\\u1FFD-\\u2070\\u2072-\\u207E\\u2080-\\u208F\\u209D-\\u2101\\u2103-\\u2106\\u2108\\u2109\\u2114\\u2116-\\u2118\\u211E-\\u2123\\u2125\\u2127\\u2129\\u212E\\u213A\\u213B\\u2140-\\u2144\\u214A-\\u214D\\u214F-\\u2182\\u2185-\\u2BFF\\u2CE5-\\u2CEA\\u2CEF-\\u2CF1\\u2CF4-\\u2CFF\\u2D26\\u2D28-\\u2D2C\\u2D2E\\u2D2F\\u2D68-\\u2D6E\\u2D70-\\u2D7F\\u2D97-\\u2D9F\\u2DA7\\u2DAF\\u2DB7\\u2DBF\\u2DC7\\u2DCF\\u2DD7\\u2DDF-\\u2E2E\\u2E30-\\u3004\\u3007-\\u3030\\u3036-\\u303A\\u303D-\\u3040\\u3097-\\u309C\\u30A0\\u30FB\\u3100-\\u3104\\u3130\\u318F-\\u319F\\u31C0-\\u31EF\\u3200-\\u33FF\\u4DC0-\\u4DFF\\uA48D-\\uA4CF\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA620-\\uA629\\uA62C-\\uA63F\\uA66F-\\uA67E\\uA69E\\uA69F\\uA6E6-\\uA716\\uA720\\uA721\\uA789\\uA78A\\uA7CB-\\uA7CF\\uA7D2\\uA7D4\\uA7DA-\\uA7F1\\uA802\\uA806\\uA80B\\uA823-\\uA83F\\uA874-\\uA881\\uA8B4-\\uA8F1\\uA8F8-\\uA8FA\\uA8FC\\uA8FF-\\uA909\\uA926-\\uA92F\\uA947-\\uA95F\\uA97D-\\uA983\\uA9B3-\\uA9CE\\uA9D0-\\uA9DF\\uA9E5\\uA9F0-\\uA9F9\\uA9FF\\uAA29-\\uAA3F\\uAA43\\uAA4C-\\uAA5F\\uAA77-\\uAA79\\uAA7B-\\uAA7D\\uAAB0\\uAAB2-\\uAAB4\\uAAB7\\uAAB8\\uAABE\\uAABF\\uAAC1\\uAAC3-\\uAADA\\uAADE\\uAADF\\uAAEB-\\uAAF1\\uAAF5-\\uAB00\\uAB07\\uAB08\\uAB0F\\uAB10\\uAB17-\\uAB1F\\uAB27\\uAB2F\\uAB5B\\uAB6A-\\uAB6F\\uABE3-\\uABFF\\uD7A4-\\uD7AF\\uD7C7-\\uD7CA\\uD7FC-\\uD7FF\\uE000-\\uF8FF\\uFA6E\\uFA6F\\uFADA-\\uFAFF\\uFB07-\\uFB12\\uFB18-\\uFB1C\\uFB1E\\uFB29\\uFB37\\uFB3D\\uFB3F\\uFB42\\uFB45\\uFBB2-\\uFBD2\\uFD3E-\\uFD4F\\uFD90\\uFD91\\uFDC8-\\uFDEF\\uFDFC-\\uFE6F\\uFE75\\uFEFD-\\uFF20\\uFF3B-\\uFF40\\uFF5B-\\uFF65\\uFFBF-\\uFFC1\\uFFC8\\uFFC9\\uFFD0\\uFFD1\\uFFD8\\uFFD9\\uFFDD-\\uFFFF]|\\uD800[\\uDC0C\\uDC27\\uDC3B\\uDC3E\\uDC4E\\uDC4F\\uDC5E-\\uDC7F\\uDCFB-\\uDE7F\\uDE9D-\\uDE9F\\uDED1-\\uDEFF\\uDF20-\\uDF2C\\uDF41\\uDF4A-\\uDF4F\\uDF76-\\uDF7F\\uDF9E\\uDF9F\\uDFC4-\\uDFC7\\uDFD0-\\uDFFF]|\\uD801[\\uDC9E-\\uDCAF\\uDCD4-\\uDCD7\\uDCFC-\\uDCFF\\uDD28-\\uDD2F\\uDD64-\\uDD6F\\uDD7B\\uDD8B\\uDD93\\uDD96\\uDDA2\\uDDB2\\uDDBA\\uDDBD-\\uDDFF\\uDF37-\\uDF3F\\uDF56-\\uDF5F\\uDF68-\\uDF7F\\uDF86\\uDFB1\\uDFBB-\\uDFFF]|\\uD802[\\uDC06\\uDC07\\uDC09\\uDC36\\uDC39-\\uDC3B\\uDC3D\\uDC3E\\uDC56-\\uDC5F\\uDC77-\\uDC7F\\uDC9F-\\uDCDF\\uDCF3\\uDCF6-\\uDCFF\\uDD16-\\uDD1F\\uDD3A-\\uDD7F\\uDDB8-\\uDDBD\\uDDC0-\\uDDFF\\uDE01-\\uDE0F\\uDE14\\uDE18\\uDE36-\\uDE5F\\uDE7D-\\uDE7F\\uDE9D-\\uDEBF\\uDEC8\\uDEE5-\\uDEFF\\uDF36-\\uDF3F\\uDF56-\\uDF5F\\uDF73-\\uDF7F\\uDF92-\\uDFFF]|\\uD803[\\uDC49-\\uDC7F\\uDCB3-\\uDCBF\\uDCF3-\\uDCFF\\uDD24-\\uDE7F\\uDEAA-\\uDEAF\\uDEB2-\\uDEFF\\uDF1D-\\uDF26\\uDF28-\\uDF2F\\uDF46-\\uDF6F\\uDF82-\\uDFAF\\uDFC5-\\uDFDF\\uDFF7-\\uDFFF]|\\uD804[\\uDC00-\\uDC02\\uDC38-\\uDC70\\uDC73\\uDC74\\uDC76-\\uDC82\\uDCB0-\\uDCCF\\uDCE9-\\uDD02\\uDD27-\\uDD43\\uDD45\\uDD46\\uDD48-\\uDD4F\\uDD73-\\uDD75\\uDD77-\\uDD82\\uDDB3-\\uDDC0\\uDDC5-\\uDDD9\\uDDDB\\uDDDD-\\uDDFF\\uDE12\\uDE2C-\\uDE7F\\uDE87\\uDE89\\uDE8E\\uDE9E\\uDEA9-\\uDEAF\\uDEDF-\\uDF04\\uDF0D\\uDF0E\\uDF11\\uDF12\\uDF29\\uDF31\\uDF34\\uDF3A-\\uDF3C\\uDF3E-\\uDF4F\\uDF51-\\uDF5C\\uDF62-\\uDFFF]|\\uD805[\\uDC35-\\uDC46\\uDC4B-\\uDC5E\\uDC62-\\uDC7F\\uDCB0-\\uDCC3\\uDCC6\\uDCC8-\\uDD7F\\uDDAF-\\uDDD7\\uDDDC-\\uDDFF\\uDE30-\\uDE43\\uDE45-\\uDE7F\\uDEAB-\\uDEB7\\uDEB9-\\uDEFF\\uDF1B-\\uDF3F\\uDF47-\\uDFFF]|\\uD806[\\uDC2C-\\uDC9F\\uDCE0-\\uDCFE\\uDD07\\uDD08\\uDD0A\\uDD0B\\uDD14\\uDD17\\uDD30-\\uDD3E\\uDD40\\uDD42-\\uDD9F\\uDDA8\\uDDA9\\uDDD1-\\uDDE0\\uDDE2\\uDDE4-\\uDDFF\\uDE01-\\uDE0A\\uDE33-\\uDE39\\uDE3B-\\uDE4F\\uDE51-\\uDE5B\\uDE8A-\\uDE9C\\uDE9E-\\uDEAF\\uDEF9-\\uDFFF]|\\uD807[\\uDC09\\uDC2F-\\uDC3F\\uDC41-\\uDC71\\uDC90-\\uDCFF\\uDD07\\uDD0A\\uDD31-\\uDD45\\uDD47-\\uDD5F\\uDD66\\uDD69\\uDD8A-\\uDD97\\uDD99-\\uDEDF\\uDEF3-\\uDFAF\\uDFB1-\\uDFFF]|\\uD808[\\uDF9A-\\uDFFF]|\\uD809[\\uDC00-\\uDC7F\\uDD44-\\uDFFF]|[\\uD80A\\uD80E-\\uD810\\uD812-\\uD819\\uD824-\\uD82A\\uD82D\\uD82E\\uD830-\\uD834\\uD836\\uD83C-\\uD83F\\uD87B-\\uD87D\\uD87F\\uD885-\\uDBFF][\\uDC00-\\uDFFF]|\\uD80B[\\uDC00-\\uDF8F\\uDFF1-\\uDFFF]|\\uD80D[\\uDC2F-\\uDFFF]|\\uD811[\\uDE47-\\uDFFF]|\\uD81A[\\uDE39-\\uDE3F\\uDE5F-\\uDE6F\\uDEBF-\\uDECF\\uDEEE-\\uDEFF\\uDF30-\\uDF3F\\uDF44-\\uDF62\\uDF78-\\uDF7C\\uDF90-\\uDFFF]|\\uD81B[\\uDC00-\\uDE3F\\uDE80-\\uDEFF\\uDF4B-\\uDF4F\\uDF51-\\uDF92\\uDFA0-\\uDFDF\\uDFE2\\uDFE4-\\uDFFF]|\\uD821[\\uDFF8-\\uDFFF]|\\uD823[\\uDCD6-\\uDCFF\\uDD09-\\uDFFF]|\\uD82B[\\uDC00-\\uDFEF\\uDFF4\\uDFFC\\uDFFF]|\\uD82C[\\uDD23-\\uDD4F\\uDD53-\\uDD63\\uDD68-\\uDD6F\\uDEFC-\\uDFFF]|\\uD82F[\\uDC6B-\\uDC6F\\uDC7D-\\uDC7F\\uDC89-\\uDC8F\\uDC9A-\\uDFFF]|\\uD835[\\uDC55\\uDC9D\\uDCA0\\uDCA1\\uDCA3\\uDCA4\\uDCA7\\uDCA8\\uDCAD\\uDCBA\\uDCBC\\uDCC4\\uDD06\\uDD0B\\uDD0C\\uDD15\\uDD1D\\uDD3A\\uDD3F\\uDD45\\uDD47-\\uDD49\\uDD51\\uDEA6\\uDEA7\\uDEC1\\uDEDB\\uDEFB\\uDF15\\uDF35\\uDF4F\\uDF6F\\uDF89\\uDFA9\\uDFC3\\uDFCC-\\uDFFF]|\\uD837[\\uDC00-\\uDEFF\\uDF1F-\\uDFFF]|\\uD838[\\uDC00-\\uDCFF\\uDD2D-\\uDD36\\uDD3E-\\uDD4D\\uDD4F-\\uDE8F\\uDEAE-\\uDEBF\\uDEEC-\\uDFFF]|\\uD839[\\uDC00-\\uDFDF\\uDFE7\\uDFEC\\uDFEF\\uDFFF]|\\uD83A[\\uDCC5-\\uDCFF\\uDD44-\\uDD4A\\uDD4C-\\uDFFF]|\\uD83B[\\uDC00-\\uDDFF\\uDE04\\uDE20\\uDE23\\uDE25\\uDE26\\uDE28\\uDE33\\uDE38\\uDE3A\\uDE3C-\\uDE41\\uDE43-\\uDE46\\uDE48\\uDE4A\\uDE4C\\uDE50\\uDE53\\uDE55\\uDE56\\uDE58\\uDE5A\\uDE5C\\uDE5E\\uDE60\\uDE63\\uDE65\\uDE66\\uDE6B\\uDE73\\uDE78\\uDE7D\\uDE7F\\uDE8A\\uDE9C-\\uDEA0\\uDEA4\\uDEAA\\uDEBC-\\uDFFF]|\\uD869[\\uDEE0-\\uDEFF]|\\uD86D[\\uDF39-\\uDF3F]|\\uD86E[\\uDC1E\\uDC1F]|\\uD873[\\uDEA2-\\uDEAF]|\\uD87A[\\uDFE1-\\uDFFF]|\\uD87E[\\uDE1E-\\uDFFF]|\\uD884[\\uDF4B-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF])/g)).pop();\n\n if (lastWordEnd !== undefined && lastWordEnd.index > 1) {\n after = after.slice(0, lastWordEnd.index + 1);\n }\n\n return {\n highlight: highlight,\n before: before,\n after: after\n };\n}\n\nfunction selection_createOrderedRange(startNode, startOffset, endNode, endOffset) {\n var range = new Range();\n range.setStart(startNode, startOffset);\n range.setEnd(endNode, endOffset);\n\n if (!range.collapsed) {\n return range;\n }\n\n selection_log(">>> createOrderedRange COLLAPSED ... RANGE REVERSE?");\n var rangeReverse = new Range();\n rangeReverse.setStart(endNode, endOffset);\n rangeReverse.setEnd(startNode, startOffset);\n\n if (!rangeReverse.collapsed) {\n selection_log(">>> createOrderedRange RANGE REVERSE OK.");\n return range;\n }\n\n selection_log(">>> createOrderedRange RANGE REVERSE ALSO COLLAPSED?!");\n return undefined;\n}\n\nfunction selection_convertRangeInfo(document, rangeInfo) {\n var startElement = document.querySelector(rangeInfo.startContainerElementCssSelector);\n\n if (!startElement) {\n selection_log("^^^ convertRangeInfo NO START ELEMENT CSS SELECTOR?!");\n return undefined;\n }\n\n var startContainer = startElement;\n\n if (rangeInfo.startContainerChildTextNodeIndex >= 0) {\n if (rangeInfo.startContainerChildTextNodeIndex >= startElement.childNodes.length) {\n selection_log("^^^ convertRangeInfo rangeInfo.startContainerChildTextNodeIndex >= startElement.childNodes.length?!");\n return undefined;\n }\n\n startContainer = startElement.childNodes[rangeInfo.startContainerChildTextNodeIndex];\n\n if (startContainer.nodeType !== Node.TEXT_NODE) {\n selection_log("^^^ convertRangeInfo startContainer.nodeType !== Node.TEXT_NODE?!");\n return undefined;\n }\n }\n\n var endElement = document.querySelector(rangeInfo.endContainerElementCssSelector);\n\n if (!endElement) {\n selection_log("^^^ convertRangeInfo NO END ELEMENT CSS SELECTOR?!");\n return undefined;\n }\n\n var endContainer = endElement;\n\n if (rangeInfo.endContainerChildTextNodeIndex >= 0) {\n if (rangeInfo.endContainerChildTextNodeIndex >= endElement.childNodes.length) {\n selection_log("^^^ convertRangeInfo rangeInfo.endContainerChildTextNodeIndex >= endElement.childNodes.length?!");\n return undefined;\n }\n\n endContainer = endElement.childNodes[rangeInfo.endContainerChildTextNodeIndex];\n\n if (endContainer.nodeType !== Node.TEXT_NODE) {\n selection_log("^^^ convertRangeInfo endContainer.nodeType !== Node.TEXT_NODE?!");\n return undefined;\n }\n }\n\n return selection_createOrderedRange(startContainer, rangeInfo.startOffset, endContainer, rangeInfo.endOffset);\n}\nfunction selection_location2RangeInfo(location) {\n var locations = location.locations;\n var domRange = locations.domRange;\n var start = domRange.start;\n var end = domRange.end;\n return {\n endContainerChildTextNodeIndex: end.textNodeIndex,\n endContainerElementCssSelector: end.cssSelector,\n endOffset: end.offset,\n startContainerChildTextNodeIndex: start.textNodeIndex,\n startContainerElementCssSelector: start.cssSelector,\n startOffset: start.offset\n };\n}\n\nfunction selection_log() {\n if (selection_debug) {\n log.apply(null, arguments);\n }\n}\n;// CONCATENATED MODULE: ./src/index.js\n//\n// Copyright 2021 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n// Base script used by both reflowable and fixed layout resources.\n\n\n\n\n\n // Public API used by the navigator.\n\nwindow.readium = {\n // utils\n scrollToId: scrollToId,\n scrollToPosition: scrollToPosition,\n scrollToText: scrollToText,\n scrollLeft: scrollLeft,\n scrollRight: scrollRight,\n scrollToStart: scrollToStart,\n scrollToEnd: scrollToEnd,\n setCSSProperties: setCSSProperties,\n setProperty: setProperty,\n removeProperty: removeProperty,\n // selection\n getCurrentSelection: getCurrentSelection,\n // decoration\n registerDecorationTemplates: registerTemplates,\n getDecorations: getDecorations,\n // DOM\n findFirstVisibleLocator: findFirstVisibleLocator\n}; // Legacy highlights API.\n\nwindow.createAnnotation = createAnnotation;\nwindow.createHighlight = createHighlight;\nwindow.destroyHighlight = destroyHighlight;\nwindow.getCurrentSelectionInfo = getCurrentSelectionInfo;\nwindow.getSelectionRect = getSelectionRect;\nwindow.rectangleForHighlightWithID = rectangleForHighlightWithID;\nwindow.setScrollMode = setScrollMode;\n;// CONCATENATED MODULE: ./src/index-reflowable.js\n//\n// Copyright 2021 Readium Foundation. All rights reserved.\n// Use of this source code is governed by the BSD-style license\n// available in the top-level LICENSE file of the project.\n//\n// Script used for reflowable resources.\n\nwindow.readium.isReflowable = true;\ndocument.addEventListener("DOMContentLoaded", function () {\n // Setups the `viewport` meta tag to disable zooming.\n var meta = document.createElement("meta");\n meta.setAttribute("name", "viewport");\n meta.setAttribute("content", "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no");\n document.head.appendChild(meta);\n});//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,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LE1BQU0sQ0FBQ21hLHVCQUFQLEdBQWlDQSx1QkFBakM7QUFDQW5hLE1BQU0sQ0FBQ29rQixnQkFBUCxHQUEwQkEsZ0JBQTFCO0FBQ0Fwa0IsTUFBTSxDQUFDbWtCLDJCQUFQLEdBQXFDQSwyQkFBckM7QUFDQW5rQixNQUFNLENBQUN1a0IsYUFBUCxHQUF1QkEsYUFBdkIsQzs7QUNsRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBRUE7QUFFQXZrQixNQUFNLENBQUMra0IsT0FBUCxDQUFlZ0IsWUFBZixHQUE4QixJQUE5QjtBQUVBdG9CLFFBQVEsQ0FBQ3dDLGdCQUFULENBQTBCLGtCQUExQixFQUE4QyxZQUFZO0FBQ3hEO0FBQ0EsTUFBSStsQixJQUFJLEdBQUd2b0IsUUFBUSxDQUFDbUUsYUFBVCxDQUF1QixNQUF2QixDQUFYO0FBQ0Fva0IsRUFBQUEsSUFBSSxDQUFDbmtCLFlBQUwsQ0FBa0IsTUFBbEIsRUFBMEIsVUFBMUI7QUFDQW1rQixFQUFBQSxJQUFJLENBQUNua0IsWUFBTCxDQUNFLFNBREYsRUFFRSw4RkFGRjtBQUlBcEUsRUFBQUEsUUFBUSxDQUFDd29CLElBQVQsQ0FBY2hrQixXQUFkLENBQTBCK2pCLElBQTFCO0FBQ0QsQ0FURCIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9zcmMvdmVuZG9yL2h5cG90aGVzaXMvYW5jaG9yaW5nL21hdGNoLXF1b3RlLmpzP2RkNmEiLCJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL3NyYy92ZW5kb3IvaHlwb3RoZXNpcy9hbmNob3JpbmcvdGV4dC1yYW5nZS5qcz9mZGVlIiwid2VicGFjazovL3JlYWRpdW0tanMvLi9zcmMvdmVuZG9yL2h5cG90aGVzaXMvYW5jaG9yaW5nL3R5cGVzLmpzPzQwMDQiLCJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL3NyYy91dGlscy5qcz8wMjVlIiwid2VicGFjazovL3JlYWRpdW0tanMvLi9zcmMvcmVjdC5qcz80ZDVhIiwid2VicGFjazovL3JlYWRpdW0tanMvLi9zcmMvZGVjb3JhdG9yLmpzPzFiMDQiLCJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL3NyYy9nZXN0dXJlcy5qcz8xNGMyIiwid2VicGFjazovL3JlYWRpdW0tanMvLi9zcmMvaGlnaGxpZ2h0LmpzPzhkYTgiLCJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL3NyYy9kb20uanM/Y2JmMCIsIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vc3JjL3NlbGVjdGlvbi5qcz81OWFjIiwid2VicGFjazovL3JlYWRpdW0tanMvLi9zcmMvaW5kZXguanM/YjYzNSIsIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vc3JjL2luZGV4LXJlZmxvd2FibGUuanM/MzkyNSJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgYXBwcm94U2VhcmNoIGZyb20gJ2FwcHJveC1zdHJpbmctbWF0Y2gnO1xuXG4vKipcbiAqIEB0eXBlZGVmIHtpbXBvcnQoJ2FwcHJveC1zdHJpbmctbWF0Y2gnKS5NYXRjaH0gU3RyaW5nTWF0Y2hcbiAqL1xuXG4vKipcbiAqIEB0eXBlZGVmIE1hdGNoXG4gKiBAcHJvcCB7bnVtYmVyfSBzdGFydCAtIFN0YXJ0IG9mZnNldCBvZiBtYXRjaCBpbiB0ZXh0XG4gKiBAcHJvcCB7bnVtYmVyfSBlbmQgLSBFbmQgb2Zmc2V0IG9mIG1hdGNoIGluIHRleHRcbiAqIEBwcm9wIHtudW1iZXJ9IHNjb3JlIC1cbiAqICAgU2NvcmUgZm9yIHRoZSBtYXRjaCBiZXR3ZWVuIDAgYW5kIDEuMCwgd2hlcmUgMS4wIGluZGljYXRlcyBhIHBlcmZlY3QgbWF0Y2hcbiAqICAgZm9yIHRoZSBxdW90ZSBhbmQgY29udGV4dC5cbiAqL1xuXG4vKipcbiAqIEZpbmQgdGhlIGJlc3QgYXBwcm94aW1hdGUgbWF0Y2hlcyBmb3IgYHN0cmAgaW4gYHRleHRgIGFsbG93aW5nIHVwIHRvIGBtYXhFcnJvcnNgIGVycm9ycy5cbiAqXG4gKiBAcGFyYW0ge3N0cmluZ30gdGV4dFxuICogQHBhcmFtIHtzdHJpbmd9IHN0clxuICogQHBhcmFtIHtudW1iZXJ9IG1heEVycm9yc1xuICogQHJldHVybiB7U3RyaW5nTWF0Y2hbXX1cbiAqL1xuZnVuY3Rpb24gc2VhcmNoKHRleHQsIHN0ciwgbWF4RXJyb3JzKSB7XG4gIC8vIERvIGEgZmFzdCBzZWFyY2ggZm9yIGV4YWN0IG1hdGNoZXMuIFRoZSBgYXBwcm94LXN0cmluZy1tYXRjaGAgbGlicmFyeVxuICAvLyBkb2Vzbid0IGN1cnJlbnRseSBpbmNvcnBvcmF0ZSB0aGlzIG9wdGltaXphdGlvbiBpdHNlbGYuXG4gIGxldCBtYXRjaFBvcyA9IDA7XG4gIGxldCBleGFjdE1hdGNoZXMgPSBbXTtcbiAgd2hpbGUgKG1hdGNoUG9zICE9PSAtMSkge1xuICAgIG1hdGNoUG9zID0gdGV4dC5pbmRleE9mKHN0ciwgbWF0Y2hQb3MpO1xuICAgIGlmIChtYXRjaFBvcyAhPT0gLTEpIHtcbiAgICAgIGV4YWN0TWF0Y2hlcy5wdXNoKHtcbiAgICAgICAgc3RhcnQ6IG1hdGNoUG9zLFxuICAgICAgICBlbmQ6IG1hdGNoUG9zICsgc3RyLmxlbmd0aCxcbiAgICAgICAgZXJyb3JzOiAwLFxuICAgICAgfSk7XG4gICAgICBtYXRjaFBvcyArPSAxO1xuICAgIH1cbiAgfVxuICBpZiAoZXhhY3RNYXRjaGVzLmxlbmd0aCA+IDApIHtcbiAgICByZXR1cm4gZXhhY3RNYXRjaGVzO1xuICB9XG5cbiAgLy8gSWYgdGhlcmUgYXJlIG5vIGV4YWN0IG1hdGNoZXMsIGRvIGEgbW9yZSBleHBlbnNpdmUgc2VhcmNoIGZvciBtYXRjaGVzXG4gIC8vIHdpdGggZXJyb3JzLlxuICByZXR1cm4gYXBwcm94U2VhcmNoKHRleHQsIHN0ciwgbWF4RXJyb3JzKTtcbn1cblxuLyoqXG4gKiBDb21wdXRlIGEgc2NvcmUgYmV0d2VlbiAwIGFuZCAxLjAgZm9yIHRoZSBzaW1pbGFyaXR5IGJldHdlZW4gYHRleHRgIGFuZCBgc3RyYC5cbiAqXG4gKiBAcGFyYW0ge3N0cmluZ30gdGV4dFxuICogQHBhcmFtIHtzdHJpbmd9IHN0clxuICovXG5mdW5jdGlvbiB0ZXh0TWF0Y2hTY29yZSh0ZXh0LCBzdHIpIHtcbiAgLyogaXN0YW5idWwgaWdub3JlIG5leHQgLSBgc2NvcmVNYXRjaGAgd2lsbCBuZXZlciBwYXNzIGFuIGVtcHR5IHN0cmluZyAqL1xuICBpZiAoc3RyLmxlbmd0aCA9PT0gMCB8fCB0ZXh0Lmxlbmd0aCA9PT0gMCkge1xuICAgIHJldHVybiAwLjA7XG4gIH1cbiAgY29uc3QgbWF0Y2hlcyA9IHNlYXJjaCh0ZXh0LCBzdHIsIHN0ci5sZW5ndGgpO1xuXG4gIC8vIHByZXR0aWVyLWlnbm9yZVxuICByZXR1cm4gMSAtIChtYXRjaGVzWzBdLmVycm9ycyAvIHN0ci5sZW5ndGgpO1xufVxuXG4vKipcbiAqIEZpbmQgdGhlIGJlc3QgYXBwcm94aW1hdGUgbWF0Y2ggZm9yIGBxdW90ZWAgaW4gYHRleHRgLlxuICpcbiAqIFJldHVybnMgYG51bGxgIGlmIG5vIG1hdGNoIGV4Y2VlZGluZyB0aGUgbWluaW11bSBxdWFsaXR5IHRocmVzaG9sZCB3YXMgZm91bmQuXG4gKlxuICogQHBhcmFtIHtzdHJpbmd9IHRleHQgLSBEb2N1bWVudCB0ZXh0IHRvIHNlYXJjaFxuICogQHBhcmFtIHtzdHJpbmd9IHF1b3RlIC0gU3RyaW5nIHRvIGZpbmQgd2l0aGluIGB0ZXh0YFxuICogQHBhcmFtIHtPYmplY3R9IGNvbnRleHQgLVxuICogICBDb250ZXh0IGluIHdoaWNoIHRoZSBxdW90ZSBvcmlnaW5hbGx5IGFwcGVhcmVkLiBUaGlzIGlzIHVzZWQgdG8gY2hvb3NlIHRoZVxuICogICBiZXN0IG1hdGNoLlxuICogICBAcGFyYW0ge3N0cmluZ30gW2NvbnRleHQucHJlZml4XSAtIEV4cGVjdGVkIHRleHQgYmVmb3JlIHRoZSBxdW90ZVxuICogICBAcGFyYW0ge3N0cmluZ30gW2NvbnRleHQuc3VmZml4XSAtIEV4cGVjdGVkIHRleHQgYWZ0ZXIgdGhlIHF1b3RlXG4gKiAgIEBwYXJhbSB7bnVtYmVyfSBbY29udGV4dC5oaW50XSAtIEV4cGVjdGVkIG9mZnNldCBvZiBtYXRjaCB3aXRoaW4gdGV4dFxuICogQHJldHVybiB7TWF0Y2h8bnVsbH1cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIG1hdGNoUXVvdGUodGV4dCwgcXVvdGUsIGNvbnRleHQgPSB7fSkge1xuICBpZiAocXVvdGUubGVuZ3RoID09PSAwKSB7XG4gICAgcmV0dXJuIG51bGw7XG4gIH1cblxuICAvLyBDaG9vc2UgdGhlIG1heGltdW0gbnVtYmVyIG9mIGVycm9ycyB0byBhbGxvdyBmb3IgdGhlIGluaXRpYWwgc2VhcmNoLlxuICAvLyBUaGlzIGNob2ljZSBpbnZvbHZlcyBhIHRyYWRlb2ZmIGJldHdlZW46XG4gIC8vXG4gIC8vICAtIFJlY2FsbCAocHJvcG9ydGlvbiBvZiBcImdvb2RcIiBtYXRjaGVzIGZvdW5kKVxuICAvLyAgLSBQcmVjaXNpb24gKHByb3BvcnRpb24gb2YgbWF0Y2hlcyBmb3VuZCB3aGljaCBhcmUgXCJnb29kXCIpXG4gIC8vICAtIENvc3Qgb2YgdGhlIGluaXRpYWwgc2VhcmNoIGFuZCBvZiBwcm9jZXNzaW5nIHRoZSBjYW5kaWRhdGUgbWF0Y2hlcyBbMV1cbiAgLy9cbiAgLy8gWzFdIFNwZWNpZmljYWxseSwgdGhlIGV4cGVjdGVkLXRpbWUgY29tcGxleGl0eSBvZiB0aGUgaW5pdGlhbCBzZWFyY2ggaXNcbiAgLy8gICAgIGBPKChtYXhFcnJvcnMgLyAzMikgKiB0ZXh0Lmxlbmd0aClgLiBTZWUgYGFwcHJveC1zdHJpbmctbWF0Y2hgIGRvY3MuXG4gIGNvbnN0IG1heEVycm9ycyA9IE1hdGgubWluKDI1NiwgcXVvdGUubGVuZ3RoIC8gMik7XG5cbiAgLy8gRmluZCBjbG9zZXN0IG1hdGNoZXMgZm9yIGBxdW90ZWAgaW4gYHRleHRgIGJhc2VkIG9uIGVkaXQgZGlzdGFuY2UuXG4gIGNvbnN0IG1hdGNoZXMgPSBzZWFyY2godGV4dCwgcXVvdGUsIG1heEVycm9ycyk7XG5cbiAgaWYgKG1hdGNoZXMubGVuZ3RoID09PSAwKSB7XG4gICAgcmV0dXJuIG51bGw7XG4gIH1cblxuICAvKipcbiAgICogQ29tcHV0ZSBhIHNjb3JlIGJldHdlZW4gMCBhbmQgMS4wIGZvciBhIG1hdGNoIGNhbmRpZGF0ZS5cbiAgICpcbiAgICogQHBhcmFtIHtTdHJpbmdNYXRjaH0gbWF0Y2hcbiAgICovXG4gIGNvbnN0IHNjb3JlTWF0Y2ggPSBtYXRjaCA9PiB7XG4gICAgY29uc3QgcXVvdGVXZWlnaHQgPSA1MDsgLy8gU2ltaWxhcml0eSBvZiBtYXRjaGVkIHRleHQgdG8gcXVvdGUuXG4gICAgY29uc3QgcHJlZml4V2VpZ2h0ID0gMjA7IC8vIFNpbWlsYXJpdHkgb2YgdGV4dCBiZWZvcmUgbWF0Y2hlZCB0ZXh0IHRvIGBjb250ZXh0LnByZWZpeGAuXG4gICAgY29uc3Qgc3VmZml4V2VpZ2h0ID0gMjA7IC8vIFNpbWlsYXJpdHkgb2YgdGV4dCBhZnRlciBtYXRjaGVkIHRleHQgdG8gYGNvbnRleHQuc3VmZml4YC5cbiAgICBjb25zdCBwb3NXZWlnaHQgPSAyOyAvLyBQcm94aW1pdHkgdG8gZXhwZWN0ZWQgbG9jYXRpb24uIFVzZWQgYXMgYSB0aWUtYnJlYWtlci5cblxuICAgIGNvbnN0IHF1b3RlU2NvcmUgPSAxIC0gbWF0Y2guZXJyb3JzIC8gcXVvdGUubGVuZ3RoO1xuXG4gICAgY29uc3QgcHJlZml4U2NvcmUgPSBjb250ZXh0LnByZWZpeFxuICAgICAgPyB0ZXh0TWF0Y2hTY29yZShcbiAgICAgICAgICB0ZXh0LnNsaWNlKE1hdGgubWF4KDAsIG1hdGNoLnN0YXJ0IC0gY29udGV4dC5wcmVmaXgubGVuZ3RoKSwgbWF0Y2guc3RhcnQpLFxuICAgICAgICAgIGNvbnRleHQucHJlZml4XG4gICAgICAgIClcbiAgICAgIDogMS4wO1xuICAgIGNvbnN0IHN1ZmZpeFNjb3JlID0gY29udGV4dC5zdWZmaXhcbiAgICAgID8gdGV4dE1hdGNoU2NvcmUoXG4gICAgICAgICAgdGV4dC5zbGljZShtYXRjaC5lbmQsIG1hdGNoLmVuZCArIGNvbnRleHQuc3VmZml4Lmxlbmd0aCksXG4gICAgICAgICAgY29udGV4dC5zdWZmaXhcbiAgICAgICAgKVxuICAgICAgOiAxLjA7XG5cbiAgICBsZXQgcG9zU2NvcmUgPSAxLjA7XG4gICAgaWYgKHR5cGVvZiBjb250ZXh0LmhpbnQgPT09ICdudW1iZXInKSB7XG4gICAgICBjb25zdCBvZmZzZXQgPSBNYXRoLmFicyhtYXRjaC5zdGFydCAtIGNvbnRleHQuaGludCk7XG4gICAgICBwb3NTY29yZSA9IDEuMCAtIG9mZnNldCAvIHRleHQubGVuZ3RoO1xuICAgIH1cblxuICAgIGNvbnN0IHJhd1Njb3JlID1cbiAgICAgIHF1b3RlV2VpZ2h0ICogcXVvdGVTY29yZSArXG4gICAgICBwcmVmaXhXZWlnaHQgKiBwcmVmaXhTY29yZSArXG4gICAgICBzdWZmaXhXZWlnaHQgKiBzdWZmaXhTY29yZSArXG4gICAgICBwb3NXZWlnaHQgKiBwb3NTY29yZTtcbiAgICBjb25zdCBtYXhTY29yZSA9IHF1b3RlV2VpZ2h0ICsgcHJlZml4V2VpZ2h0ICsgc3VmZml4V2VpZ2h0ICsgcG9zV2VpZ2h0O1xuICAgIGNvbnN0IG5vcm1hbGl6ZWRTY29yZSA9IHJhd1Njb3JlIC8gbWF4U2NvcmU7XG5cbiAgICByZXR1cm4gbm9ybWFsaXplZFNjb3JlO1xuICB9O1xuXG4gIC8vIFJhbmsgbWF0Y2hlcyBiYXNlZCBvbiBzaW1pbGFyaXR5IG9mIGFjdHVhbCBhbmQgZXhwZWN0ZWQgc3Vycm91bmRpbmcgdGV4dFxuICAvLyBhbmQgYWN0dWFsL2V4cGVjdGVkIG9mZnNldCBpbiB0aGUgZG9jdW1lbnQgdGV4dC5cbiAgY29uc3Qgc2NvcmVkTWF0Y2hlcyA9IG1hdGNoZXMubWFwKG0gPT4gKHtcbiAgICBzdGFydDogbS5zdGFydCxcbiAgICBlbmQ6IG0uZW5kLFxuICAgIHNjb3JlOiBzY29yZU1hdGNoKG0pLFxuICB9KSk7XG5cbiAgLy8gQ2hvb3NlIG1hdGNoIHdpdGggaGlnaGVzdCBzY29yZS5cbiAgc2NvcmVkTWF0Y2hlcy5zb3J0KChhLCBiKSA9PiBiLnNjb3JlIC0gYS5zY29yZSk7XG4gIHJldHVybiBzY29yZWRNYXRjaGVzWzBdO1xufVxuIiwiLyoqXG4gKiBSZXR1cm4gdGhlIGNvbWJpbmVkIGxlbmd0aCBvZiB0ZXh0IG5vZGVzIGNvbnRhaW5lZCBpbiBgbm9kZWAuXG4gKlxuICogQHBhcmFtIHtOb2RlfSBub2RlXG4gKi9cbmZ1bmN0aW9uIG5vZGVUZXh0TGVuZ3RoKG5vZGUpIHtcbiAgc3dpdGNoIChub2RlLm5vZGVUeXBlKSB7XG4gICAgY2FzZSBOb2RlLkVMRU1FTlRfTk9ERTpcbiAgICBjYXNlIE5vZGUuVEVYVF9OT0RFOlxuICAgICAgLy8gbmIuIGB0ZXh0Q29udGVudGAgZXhjbHVkZXMgdGV4dCBpbiBjb21tZW50cyBhbmQgcHJvY2Vzc2luZyBpbnN0cnVjdGlvbnNcbiAgICAgIC8vIHdoZW4gY2FsbGVkIG9uIGEgcGFyZW50IGVsZW1lbnQsIHNvIHdlIGRvbid0IG5lZWQgdG8gc3VidHJhY3QgdGhhdCBoZXJlLlxuXG4gICAgICByZXR1cm4gLyoqIEB0eXBlIHtzdHJpbmd9ICovIChub2RlLnRleHRDb250ZW50KS5sZW5ndGg7XG4gICAgZGVmYXVsdDpcbiAgICAgIHJldHVybiAwO1xuICB9XG59XG5cbi8qKlxuICogUmV0dXJuIHRoZSB0b3RhbCBsZW5ndGggb2YgdGhlIHRleHQgb2YgYWxsIHByZXZpb3VzIHNpYmxpbmdzIG9mIGBub2RlYC5cbiAqXG4gKiBAcGFyYW0ge05vZGV9IG5vZGVcbiAqL1xuZnVuY3Rpb24gcHJldmlvdXNTaWJsaW5nc1RleHRMZW5ndGgobm9kZSkge1xuICBsZXQgc2libGluZyA9IG5vZGUucHJldmlvdXNTaWJsaW5nO1xuICBsZXQgbGVuZ3RoID0gMDtcbiAgd2hpbGUgKHNpYmxpbmcpIHtcbiAgICBsZW5ndGggKz0gbm9kZVRleHRMZW5ndGgoc2libGluZyk7XG4gICAgc2libGluZyA9IHNpYmxpbmcucHJldmlvdXNTaWJsaW5nO1xuICB9XG4gIHJldHVybiBsZW5ndGg7XG59XG5cbi8qKlxuICogUmVzb2x2ZSBvbmUgb3IgbW9yZSBjaGFyYWN0ZXIgb2Zmc2V0cyB3aXRoaW4gYW4gZWxlbWVudCB0byAodGV4dCBub2RlLCBwb3NpdGlvbilcbiAqIHBhaXJzLlxuICpcbiAqIEBwYXJhbSB7RWxlbWVudH0gZWxlbWVudFxuICogQHBhcmFtIHtudW1iZXJbXX0gb2Zmc2V0cyAtIE9mZnNldHMsIHdoaWNoIG11c3QgYmUgc29ydGVkIGluIGFzY2VuZGluZyBvcmRlclxuICogQHJldHVybiB7eyBub2RlOiBUZXh0LCBvZmZzZXQ6IG51bWJlciB9W119XG4gKi9cbmZ1bmN0aW9uIHJlc29sdmVPZmZzZXRzKGVsZW1lbnQsIC4uLm9mZnNldHMpIHtcbiAgbGV0IG5leHRPZmZzZXQgPSBvZmZzZXRzLnNoaWZ0KCk7XG4gIGNvbnN0IG5vZGVJdGVyID0gLyoqIEB0eXBlIHtEb2N1bWVudH0gKi8gKFxuICAgIGVsZW1lbnQub3duZXJEb2N1bWVudFxuICApLmNyZWF0ZU5vZGVJdGVyYXRvcihlbGVtZW50LCBOb2RlRmlsdGVyLlNIT1dfVEVYVCk7XG4gIGNvbnN0IHJlc3VsdHMgPSBbXTtcblxuICBsZXQgY3VycmVudE5vZGUgPSBub2RlSXRlci5uZXh0Tm9kZSgpO1xuICBsZXQgdGV4dE5vZGU7XG4gIGxldCBsZW5ndGggPSAwO1xuXG4gIC8vIEZpbmQgdGhlIHRleHQgbm9kZSBjb250YWluaW5nIHRoZSBgbmV4dE9mZnNldGB0aCBjaGFyYWN0ZXIgZnJvbSB0aGUgc3RhcnRcbiAgLy8gb2YgYGVsZW1lbnRgLlxuICB3aGlsZSAobmV4dE9mZnNldCAhPT0gdW5kZWZpbmVkICYmIGN1cnJlbnROb2RlKSB7XG4gICAgdGV4dE5vZGUgPSAvKiogQHR5cGUge1RleHR9ICovIChjdXJyZW50Tm9kZSk7XG4gICAgaWYgKGxlbmd0aCArIHRleHROb2RlLmRhdGEubGVuZ3RoID4gbmV4dE9mZnNldCkge1xuICAgICAgcmVzdWx0cy5wdXNoKHsgbm9kZTogdGV4dE5vZGUsIG9mZnNldDogbmV4dE9mZnNldCAtIGxlbmd0aCB9KTtcbiAgICAgIG5leHRPZmZzZXQgPSBvZmZzZXRzLnNoaWZ0KCk7XG4gICAgfSBlbHNlIHtcbiAgICAgIGN1cnJlbnROb2RlID0gbm9kZUl0ZXIubmV4dE5vZGUoKTtcbiAgICAgIGxlbmd0aCArPSB0ZXh0Tm9kZS5kYXRhLmxlbmd0aDtcbiAgICB9XG4gIH1cblxuICAvLyBCb3VuZGFyeSBjYXNlLlxuICB3aGlsZSAobmV4dE9mZnNldCAhPT0gdW5kZWZpbmVkICYmIHRleHROb2RlICYmIGxlbmd0aCA9PT0gbmV4dE9mZnNldCkge1xuICAgIHJlc3VsdHMucHVzaCh7IG5vZGU6IHRleHROb2RlLCBvZmZzZXQ6IHRleHROb2RlLmRhdGEubGVuZ3RoIH0pO1xuICAgIG5leHRPZmZzZXQgPSBvZmZzZXRzLnNoaWZ0KCk7XG4gIH1cblxuICBpZiAobmV4dE9mZnNldCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ09mZnNldCBleGNlZWRzIHRleHQgbGVuZ3RoJyk7XG4gIH1cblxuICByZXR1cm4gcmVzdWx0cztcbn1cblxuZXhwb3J0IGxldCBSRVNPTFZFX0ZPUldBUkRTID0gMTtcbmV4cG9ydCBsZXQgUkVTT0xWRV9CQUNLV0FSRFMgPSAyO1xuXG4vKipcbiAqIFJlcHJlc2VudHMgYW4gb2Zmc2V0IHdpdGhpbiB0aGUgdGV4dCBjb250ZW50IG9mIGFuIGVsZW1lbnQuXG4gKlxuICogVGhpcyBwb3NpdGlvbiBjYW4gYmUgcmVzb2x2ZWQgdG8gYSBzcGVjaWZpYyBkZXNjZW5kYW50IG5vZGUgaW4gdGhlIGN1cnJlbnRcbiAqIERPTSBzdWJ0cmVlIG9mIHRoZSBlbGVtZW50IHVzaW5nIHRoZSBgcmVzb2x2ZWAgbWV0aG9kLlxuICovXG5leHBvcnQgY2xhc3MgVGV4dFBvc2l0aW9uIHtcbiAgLyoqXG4gICAqIENvbnN0cnVjdCBhIGBUZXh0UG9zaXRpb25gIHRoYXQgcmVmZXJzIHRvIHRoZSB0ZXh0IHBvc2l0aW9uIGBvZmZzZXRgIHdpdGhpblxuICAgKiB0aGUgdGV4dCBjb250ZW50IG9mIGBlbGVtZW50YC5cbiAgICpcbiAgICogQHBhcmFtIHtFbGVtZW50fSBlbGVtZW50XG4gICAqIEBwYXJhbSB7bnVtYmVyfSBvZmZzZXRcbiAgICovXG4gIGNvbnN0cnVjdG9yKGVsZW1lbnQsIG9mZnNldCkge1xuICAgIGlmIChvZmZzZXQgPCAwKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoJ09mZnNldCBpcyBpbnZhbGlkJyk7XG4gICAgfVxuXG4gICAgLyoqIEVsZW1lbnQgdGhhdCBgb2Zmc2V0YCBpcyByZWxhdGl2ZSB0by4gKi9cbiAgICB0aGlzLmVsZW1lbnQgPSBlbGVtZW50O1xuXG4gICAgLyoqIENoYXJhY3RlciBvZmZzZXQgZnJvbSB0aGUgc3RhcnQgb2YgdGhlIGVsZW1lbnQncyBgdGV4dENvbnRlbnRgLiAqL1xuICAgIHRoaXMub2Zmc2V0ID0gb2Zmc2V0O1xuICB9XG5cbiAgLyoqXG4gICAqIFJldHVybiBhIGNvcHkgb2YgdGhpcyBwb3NpdGlvbiB3aXRoIG9mZnNldCByZWxhdGl2ZSB0byBhIGdpdmVuIGFuY2VzdG9yXG4gICAqIGVsZW1lbnQuXG4gICAqXG4gICAqIEBwYXJhbSB7RWxlbWVudH0gcGFyZW50IC0gQW5jZXN0b3Igb2YgYHRoaXMuZWxlbWVudGBcbiAgICogQHJldHVybiB7VGV4dFBvc2l0aW9ufVxuICAgKi9cbiAgcmVsYXRpdmVUbyhwYXJlbnQpIHtcbiAgICBpZiAoIXBhcmVudC5jb250YWlucyh0aGlzLmVsZW1lbnQpKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoJ1BhcmVudCBpcyBub3QgYW4gYW5jZXN0b3Igb2YgY3VycmVudCBlbGVtZW50Jyk7XG4gICAgfVxuXG4gICAgbGV0IGVsID0gdGhpcy5lbGVtZW50O1xuICAgIGxldCBvZmZzZXQgPSB0aGlzLm9mZnNldDtcbiAgICB3aGlsZSAoZWwgIT09IHBhcmVudCkge1xuICAgICAgb2Zmc2V0ICs9IHByZXZpb3VzU2libGluZ3NUZXh0TGVuZ3RoKGVsKTtcbiAgICAgIGVsID0gLyoqIEB0eXBlIHtFbGVtZW50fSAqLyAoZWwucGFyZW50RWxlbWVudCk7XG4gICAgfVxuXG4gICAgcmV0dXJuIG5ldyBUZXh0UG9zaXRpb24oZWwsIG9mZnNldCk7XG4gIH1cblxuICAvKipcbiAgICogUmVzb2x2ZSB0aGUgcG9zaXRpb24gdG8gYSBzcGVjaWZpYyB0ZXh0IG5vZGUgYW5kIG9mZnNldCB3aXRoaW4gdGhhdCBub2RlLlxuICAgKlxuICAgKiBUaHJvd3MgaWYgYHRoaXMub2Zmc2V0YCBleGNlZWRzIHRoZSBsZW5ndGggb2YgdGhlIGVsZW1lbnQncyB0ZXh0LiBJbiB0aGVcbiAgICogY2FzZSB3aGVyZSB0aGUgZWxlbWVudCBoYXMgbm8gdGV4dCBhbmQgYHRoaXMub2Zmc2V0YCBpcyAwLCB0aGUgYGRpcmVjdGlvbmBcbiAgICogb3B0aW9uIGRldGVybWluZXMgd2hhdCBoYXBwZW5zLlxuICAgKlxuICAgKiBPZmZzZXRzIGF0IHRoZSBib3VuZGFyeSBiZXR3ZWVuIHR3byBub2RlcyBhcmUgcmVzb2x2ZWQgdG8gdGhlIHN0YXJ0IG9mIHRoZVxuICAgKiBub2RlIHRoYXQgYmVnaW5zIGF0IHRoZSBib3VuZGFyeS5cbiAgICpcbiAgICogQHBhcmFtIHtPYmplY3R9IFtvcHRpb25zXVxuICAgKiAgIEBwYXJhbSB7UkVTT0xWRV9GT1JXQVJEU3xSRVNPTFZFX0JBQ0tXQVJEU30gW29wdGlvbnMuZGlyZWN0aW9uXSAtXG4gICAqICAgICBTcGVjaWZpZXMgaW4gd2hpY2ggZGlyZWN0aW9uIHRvIHNlYXJjaCBmb3IgdGhlIG5lYXJlc3QgdGV4dCBub2RlIGlmXG4gICAqICAgICBgdGhpcy5vZmZzZXRgIGlzIGAwYCBhbmQgYHRoaXMuZWxlbWVudGAgaGFzIG5vIHRleHQuIElmIG5vdCBzcGVjaWZpZWRcbiAgICogICAgIGFuIGVycm9yIGlzIHRocm93bi5cbiAgICogQHJldHVybiB7eyBub2RlOiBUZXh0LCBvZmZzZXQ6IG51bWJlciB9fVxuICAgKiBAdGhyb3dzIHtSYW5nZUVycm9yfVxuICAgKi9cbiAgcmVzb2x2ZShvcHRpb25zID0ge30pIHtcbiAgICB0cnkge1xuICAgICAgcmV0dXJuIHJlc29sdmVPZmZzZXRzKHRoaXMuZWxlbWVudCwgdGhpcy5vZmZzZXQpWzBdO1xuICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgaWYgKHRoaXMub2Zmc2V0ID09PSAwICYmIG9wdGlvbnMuZGlyZWN0aW9uICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgY29uc3QgdHcgPSBkb2N1bWVudC5jcmVhdGVUcmVlV2Fsa2VyKFxuICAgICAgICAgIHRoaXMuZWxlbWVudC5nZXRSb290Tm9kZSgpLFxuICAgICAgICAgIE5vZGVGaWx0ZXIuU0hPV19URVhUXG4gICAgICAgICk7XG4gICAgICAgIHR3LmN1cnJlbnROb2RlID0gdGhpcy5lbGVtZW50O1xuICAgICAgICBjb25zdCBmb3J3YXJkcyA9IG9wdGlvbnMuZGlyZWN0aW9uID09PSBSRVNPTFZFX0ZPUldBUkRTO1xuICAgICAgICBjb25zdCB0ZXh0ID0gLyoqIEB0eXBlIHtUZXh0fG51bGx9ICovIChcbiAgICAgICAgICBmb3J3YXJkcyA/IHR3Lm5leHROb2RlKCkgOiB0dy5wcmV2aW91c05vZGUoKVxuICAgICAgICApO1xuICAgICAgICBpZiAoIXRleHQpIHtcbiAgICAgICAgICB0aHJvdyBlcnI7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIHsgbm9kZTogdGV4dCwgb2Zmc2V0OiBmb3J3YXJkcyA/IDAgOiB0ZXh0LmRhdGEubGVuZ3RoIH07XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0aHJvdyBlcnI7XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIENvbnN0cnVjdCBhIGBUZXh0UG9zaXRpb25gIHRoYXQgcmVmZXJzIHRvIHRoZSBgb2Zmc2V0YHRoIGNoYXJhY3RlciB3aXRoaW5cbiAgICogYG5vZGVgLlxuICAgKlxuICAgKiBAcGFyYW0ge05vZGV9IG5vZGVcbiAgICogQHBhcmFtIHtudW1iZXJ9IG9mZnNldFxuICAgKiBAcmV0dXJuIHtUZXh0UG9zaXRpb259XG4gICAqL1xuICBzdGF0aWMgZnJvbUNoYXJPZmZzZXQobm9kZSwgb2Zmc2V0KSB7XG4gICAgc3dpdGNoIChub2RlLm5vZGVUeXBlKSB7XG4gICAgICBjYXNlIE5vZGUuVEVYVF9OT0RFOlxuICAgICAgICByZXR1cm4gVGV4dFBvc2l0aW9uLmZyb21Qb2ludChub2RlLCBvZmZzZXQpO1xuICAgICAgY2FzZSBOb2RlLkVMRU1FTlRfTk9ERTpcbiAgICAgICAgcmV0dXJuIG5ldyBUZXh0UG9zaXRpb24oLyoqIEB0eXBlIHtFbGVtZW50fSAqLyAobm9kZSksIG9mZnNldCk7XG4gICAgICBkZWZhdWx0OlxuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoJ05vZGUgaXMgbm90IGFuIGVsZW1lbnQgb3IgdGV4dCBub2RlJyk7XG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIENvbnN0cnVjdCBhIGBUZXh0UG9zaXRpb25gIHJlcHJlc2VudGluZyB0aGUgcmFuZ2Ugc3RhcnQgb3IgZW5kIHBvaW50IChub2RlLCBvZmZzZXQpLlxuICAgKlxuICAgKiBAcGFyYW0ge05vZGV9IG5vZGUgLSBUZXh0IG9yIEVsZW1lbnQgbm9kZVxuICAgKiBAcGFyYW0ge251bWJlcn0gb2Zmc2V0IC0gT2Zmc2V0IHdpdGhpbiB0aGUgbm9kZS5cbiAgICogQHJldHVybiB7VGV4dFBvc2l0aW9ufVxuICAgKi9cbiAgc3RhdGljIGZyb21Qb2ludChub2RlLCBvZmZzZXQpIHtcbiAgICBzd2l0Y2ggKG5vZGUubm9kZVR5cGUpIHtcbiAgICAgIGNhc2UgTm9kZS5URVhUX05PREU6IHtcbiAgICAgICAgaWYgKG9mZnNldCA8IDAgfHwgb2Zmc2V0ID4gLyoqIEB0eXBlIHtUZXh0fSAqLyAobm9kZSkuZGF0YS5sZW5ndGgpIHtcbiAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoJ1RleHQgbm9kZSBvZmZzZXQgaXMgb3V0IG9mIHJhbmdlJyk7XG4gICAgICAgIH1cblxuICAgICAgICBpZiAoIW5vZGUucGFyZW50RWxlbWVudCkge1xuICAgICAgICAgIHRocm93IG5ldyBFcnJvcignVGV4dCBub2RlIGhhcyBubyBwYXJlbnQnKTtcbiAgICAgICAgfVxuXG4gICAgICAgIC8vIEdldCB0aGUgb2Zmc2V0IGZyb20gdGhlIHN0YXJ0IG9mIHRoZSBwYXJlbnQgZWxlbWVudC5cbiAgICAgICAgY29uc3QgdGV4dE9mZnNldCA9IHByZXZpb3VzU2libGluZ3NUZXh0TGVuZ3RoKG5vZGUpICsgb2Zmc2V0O1xuXG4gICAgICAgIHJldHVybiBuZXcgVGV4dFBvc2l0aW9uKG5vZGUucGFyZW50RWxlbWVudCwgdGV4dE9mZnNldCk7XG4gICAgICB9XG4gICAgICBjYXNlIE5vZGUuRUxFTUVOVF9OT0RFOiB7XG4gICAgICAgIGlmIChvZmZzZXQgPCAwIHx8IG9mZnNldCA+IG5vZGUuY2hpbGROb2Rlcy5sZW5ndGgpIHtcbiAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoJ0NoaWxkIG5vZGUgb2Zmc2V0IGlzIG91dCBvZiByYW5nZScpO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gR2V0IHRoZSB0ZXh0IGxlbmd0aCBiZWZvcmUgdGhlIGBvZmZzZXRgdGggY2hpbGQgb2YgZWxlbWVudC5cbiAgICAgICAgbGV0IHRleHRPZmZzZXQgPSAwO1xuICAgICAgICBmb3IgKGxldCBpID0gMDsgaSA8IG9mZnNldDsgaSsrKSB7XG4gICAgICAgICAgdGV4dE9mZnNldCArPSBub2RlVGV4dExlbmd0aChub2RlLmNoaWxkTm9kZXNbaV0pO1xuICAgICAgICB9XG5cbiAgICAgICAgcmV0dXJuIG5ldyBUZXh0UG9zaXRpb24oLyoqIEB0eXBlIHtFbGVtZW50fSAqLyAobm9kZSksIHRleHRPZmZzZXQpO1xuICAgICAgfVxuICAgICAgZGVmYXVsdDpcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCdQb2ludCBpcyBub3QgaW4gYW4gZWxlbWVudCBvciB0ZXh0IG5vZGUnKTtcbiAgICB9XG4gIH1cbn1cblxuLyoqXG4gKiBSZXByZXNlbnRzIGEgcmVnaW9uIG9mIGEgZG9jdW1lbnQgYXMgYSAoc3RhcnQsIGVuZCkgcGFpciBvZiBgVGV4dFBvc2l0aW9uYCBwb2ludHMuXG4gKlxuICogUmVwcmVzZW50aW5nIGEgcmFuZ2UgaW4gdGhpcyB3YXkgYWxsb3dzIGZvciBjaGFuZ2VzIGluIHRoZSBET00gY29udGVudCBvZiB0aGVcbiAqIHJhbmdlIHdoaWNoIGRvbid0IGFmZmVjdCBpdHMgdGV4dCBjb250ZW50LCB3aXRob3V0IGFmZmVjdGluZyB0aGUgdGV4dCBjb250ZW50XG4gKiBvZiB0aGUgcmFuZ2UgaXRzZWxmLlxuICovXG5leHBvcnQgY2xhc3MgVGV4dFJhbmdlIHtcbiAgLyoqXG4gICAqIENvbnN0cnVjdCBhbiBpbW11dGFibGUgYFRleHRSYW5nZWAgZnJvbSBhIGBzdGFydGAgYW5kIGBlbmRgIHBvaW50LlxuICAgKlxuICAgKiBAcGFyYW0ge1RleHRQb3NpdGlvbn0gc3RhcnRcbiAgICogQHBhcmFtIHtUZXh0UG9zaXRpb259IGVuZFxuICAgKi9cbiAgY29uc3RydWN0b3Ioc3RhcnQsIGVuZCkge1xuICAgIHRoaXMuc3RhcnQgPSBzdGFydDtcbiAgICB0aGlzLmVuZCA9IGVuZDtcbiAgfVxuXG4gIC8qKlxuICAgKiBSZXR1cm4gYSBjb3B5IG9mIHRoaXMgcmFuZ2Ugd2l0aCBzdGFydCBhbmQgZW5kIHBvc2l0aW9ucyByZWxhdGl2ZSB0byBhXG4gICAqIGdpdmVuIGFuY2VzdG9yLiBTZWUgYFRleHRQb3NpdGlvbi5yZWxhdGl2ZVRvYC5cbiAgICpcbiAgICogQHBhcmFtIHtFbGVtZW50fSBlbGVtZW50XG4gICAqL1xuICByZWxhdGl2ZVRvKGVsZW1lbnQpIHtcbiAgICByZXR1cm4gbmV3IFRleHRSYW5nZShcbiAgICAgIHRoaXMuc3RhcnQucmVsYXRpdmVUbyhlbGVtZW50KSxcbiAgICAgIHRoaXMuZW5kLnJlbGF0aXZlVG8oZWxlbWVudClcbiAgICApO1xuICB9XG5cbiAgLyoqXG4gICAqIFJlc29sdmUgdGhlIGBUZXh0UmFuZ2VgIHRvIGEgRE9NIHJhbmdlLlxuICAgKlxuICAgKiBUaGUgcmVzdWx0aW5nIERPTSBSYW5nZSB3aWxsIGFsd2F5cyBzdGFydCBhbmQgZW5kIGluIGEgYFRleHRgIG5vZGUuXG4gICAqIEhlbmNlIGBUZXh0UmFuZ2UuZnJvbVJhbmdlKHJhbmdlKS50b1JhbmdlKClgIGNhbiBiZSB1c2VkIHRvIFwic2hyaW5rXCIgYVxuICAgKiByYW5nZSB0byB0aGUgdGV4dCBpdCBjb250YWlucy5cbiAgICpcbiAgICogTWF5IHRocm93IGlmIHRoZSBgc3RhcnRgIG9yIGBlbmRgIHBvc2l0aW9ucyBjYW5ub3QgYmUgcmVzb2x2ZWQgdG8gYSByYW5nZS5cbiAgICpcbiAgICogQHJldHVybiB7UmFuZ2V9XG4gICAqL1xuICB0b1JhbmdlKCkge1xuICAgIGxldCBzdGFydDtcbiAgICBsZXQgZW5kO1xuXG4gICAgaWYgKFxuICAgICAgdGhpcy5zdGFydC5lbGVtZW50ID09PSB0aGlzLmVuZC5lbGVtZW50ICYmXG4gICAgICB0aGlzLnN0YXJ0Lm9mZnNldCA8PSB0aGlzLmVuZC5vZmZzZXRcbiAgICApIHtcbiAgICAgIC8vIEZhc3QgcGF0aCBmb3Igc3RhcnQgYW5kIGVuZCBwb2ludHMgaW4gc2FtZSBlbGVtZW50LlxuICAgICAgW3N0YXJ0LCBlbmRdID0gcmVzb2x2ZU9mZnNldHMoXG4gICAgICAgIHRoaXMuc3RhcnQuZWxlbWVudCxcbiAgICAgICAgdGhpcy5zdGFydC5vZmZzZXQsXG4gICAgICAgIHRoaXMuZW5kLm9mZnNldFxuICAgICAgKTtcbiAgICB9IGVsc2Uge1xuICAgICAgc3RhcnQgPSB0aGlzLnN0YXJ0LnJlc29sdmUoeyBkaXJlY3Rpb246IFJFU09MVkVfRk9SV0FSRFMgfSk7XG4gICAgICBlbmQgPSB0aGlzLmVuZC5yZXNvbHZlKHsgZGlyZWN0aW9uOiBSRVNPTFZFX0JBQ0tXQVJEUyB9KTtcbiAgICB9XG5cbiAgICBjb25zdCByYW5nZSA9IG5ldyBSYW5nZSgpO1xuICAgIHJhbmdlLnNldFN0YXJ0KHN0YXJ0Lm5vZGUsIHN0YXJ0Lm9mZnNldCk7XG4gICAgcmFuZ2Uuc2V0RW5kKGVuZC5ub2RlLCBlbmQub2Zmc2V0KTtcbiAgICByZXR1cm4gcmFuZ2U7XG4gIH1cblxuICAvKipcbiAgICogQ29udmVydCBhbiBleGlzdGluZyBET00gYFJhbmdlYCB0byBhIGBUZXh0UmFuZ2VgXG4gICAqXG4gICAqIEBwYXJhbSB7UmFuZ2V9IHJhbmdlXG4gICAqIEByZXR1cm4ge1RleHRSYW5nZX1cbiAgICovXG4gIHN0YXRpYyBmcm9tUmFuZ2UocmFuZ2UpIHtcbiAgICBjb25zdCBzdGFydCA9IFRleHRQb3NpdGlvbi5mcm9tUG9pbnQoXG4gICAgICByYW5nZS5zdGFydENvbnRhaW5lcixcbiAgICAgIHJhbmdlLnN0YXJ0T2Zmc2V0XG4gICAgKTtcbiAgICBjb25zdCBlbmQgPSBUZXh0UG9zaXRpb24uZnJvbVBvaW50KHJhbmdlLmVuZENvbnRhaW5lciwgcmFuZ2UuZW5kT2Zmc2V0KTtcbiAgICByZXR1cm4gbmV3IFRleHRSYW5nZShzdGFydCwgZW5kKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBSZXR1cm4gYSBgVGV4dFJhbmdlYCBmcm9tIHRoZSBgc3RhcnRgdGggdG8gYGVuZGB0aCBjaGFyYWN0ZXJzIGluIGByb290YC5cbiAgICpcbiAgICogQHBhcmFtIHtFbGVtZW50fSByb290XG4gICAqIEBwYXJhbSB7bnVtYmVyfSBzdGFydFxuICAgKiBAcGFyYW0ge251bWJlcn0gZW5kXG4gICAqL1xuICBzdGF0aWMgZnJvbU9mZnNldHMocm9vdCwgc3RhcnQsIGVuZCkge1xuICAgIHJldHVybiBuZXcgVGV4dFJhbmdlKFxuICAgICAgbmV3IFRleHRQb3NpdGlvbihyb290LCBzdGFydCksXG4gICAgICBuZXcgVGV4dFBvc2l0aW9uKHJvb3QsIGVuZClcbiAgICApO1xuICB9XG59XG4iLCIvKipcbiAqIFRoaXMgbW9kdWxlIGV4cG9ydHMgYSBzZXQgb2YgY2xhc3NlcyBmb3IgY29udmVydGluZyBiZXR3ZWVuIERPTSBgUmFuZ2VgXG4gKiBvYmplY3RzIGFuZCBkaWZmZXJlbnQgdHlwZXMgb2Ygc2VsZWN0b3JzLiBJdCBpcyBtb3N0bHkgYSB0aGluIHdyYXBwZXIgYXJvdW5kIGFcbiAqIHNldCBvZiBhbmNob3JpbmcgbGlicmFyaWVzLiBJdCBzZXJ2ZXMgdHdvIG1haW4gcHVycG9zZXM6XG4gKlxuICogIDEuIFByb3ZpZGluZyBhIGNvbnNpc3RlbnQgaW50ZXJmYWNlIGFjcm9zcyBkaWZmZXJlbnQgdHlwZXMgb2YgYW5jaG9ycy5cbiAqICAyLiBJbnN1bGF0aW5nIHRoZSByZXN0IG9mIHRoZSBjb2RlIGZyb20gQVBJIGNoYW5nZXMgaW4gdGhlIHVuZGVybHlpbmcgYW5jaG9yaW5nXG4gKiAgICAgbGlicmFyaWVzLlxuICovXG5cbmltcG9ydCB7IG1hdGNoUXVvdGUgfSBmcm9tICcuL21hdGNoLXF1b3RlJztcbmltcG9ydCB7IFRleHRSYW5nZSwgVGV4dFBvc2l0aW9uIH0gZnJvbSAnLi90ZXh0LXJhbmdlJztcbmltcG9ydCB7IG5vZGVGcm9tWFBhdGgsIHhwYXRoRnJvbU5vZGUgfSBmcm9tICcuL3hwYXRoJztcblxuLyoqXG4gKiBAdHlwZWRlZiB7aW1wb3J0KCcuLi8uLi90eXBlcy9hcGknKS5SYW5nZVNlbGVjdG9yfSBSYW5nZVNlbGVjdG9yXG4gKiBAdHlwZWRlZiB7aW1wb3J0KCcuLi8uLi90eXBlcy9hcGknKS5UZXh0UG9zaXRpb25TZWxlY3Rvcn0gVGV4dFBvc2l0aW9uU2VsZWN0b3JcbiAqIEB0eXBlZGVmIHtpbXBvcnQoJy4uLy4uL3R5cGVzL2FwaScpLlRleHRRdW90ZVNlbGVjdG9yfSBUZXh0UXVvdGVTZWxlY3RvclxuICovXG5cbi8qKlxuICogQ29udmVydHMgYmV0d2VlbiBgUmFuZ2VTZWxlY3RvcmAgc2VsZWN0b3JzIGFuZCBgUmFuZ2VgIG9iamVjdHMuXG4gKi9cbmV4cG9ydCBjbGFzcyBSYW5nZUFuY2hvciB7XG4gIC8qKlxuICAgKiBAcGFyYW0ge05vZGV9IHJvb3QgLSBBIHJvb3QgZWxlbWVudCBmcm9tIHdoaWNoIHRvIGFuY2hvci5cbiAgICogQHBhcmFtIHtSYW5nZX0gcmFuZ2UgLSAgQSByYW5nZSBkZXNjcmliaW5nIHRoZSBhbmNob3IuXG4gICAqL1xuICBjb25zdHJ1Y3Rvcihyb290LCByYW5nZSkge1xuICAgIHRoaXMucm9vdCA9IHJvb3Q7XG4gICAgdGhpcy5yYW5nZSA9IHJhbmdlO1xuICB9XG5cbiAgLyoqXG4gICAqIEBwYXJhbSB7Tm9kZX0gcm9vdCAtICBBIHJvb3QgZWxlbWVudCBmcm9tIHdoaWNoIHRvIGFuY2hvci5cbiAgICogQHBhcmFtIHtSYW5nZX0gcmFuZ2UgLSAgQSByYW5nZSBkZXNjcmliaW5nIHRoZSBhbmNob3IuXG4gICAqL1xuICBzdGF0aWMgZnJvbVJhbmdlKHJvb3QsIHJhbmdlKSB7XG4gICAgcmV0dXJuIG5ldyBSYW5nZUFuY2hvcihyb290LCByYW5nZSk7XG4gIH1cblxuICAvKipcbiAgICogQ3JlYXRlIGFuIGFuY2hvciBmcm9tIGEgc2VyaWFsaXplZCBgUmFuZ2VTZWxlY3RvcmAgc2VsZWN0b3IuXG4gICAqXG4gICAqIEBwYXJhbSB7RWxlbWVudH0gcm9vdCAtICBBIHJvb3QgZWxlbWVudCBmcm9tIHdoaWNoIHRvIGFuY2hvci5cbiAgICogQHBhcmFtIHtSYW5nZVNlbGVjdG9yfSBzZWxlY3RvclxuICAgKi9cbiAgc3RhdGljIGZyb21TZWxlY3Rvcihyb290LCBzZWxlY3Rvcikge1xuICAgIGNvbnN0IHN0YXJ0Q29udGFpbmVyID0gbm9kZUZyb21YUGF0aChzZWxlY3Rvci5zdGFydENvbnRhaW5lciwgcm9vdCk7XG4gICAgaWYgKCFzdGFydENvbnRhaW5lcikge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdGYWlsZWQgdG8gcmVzb2x2ZSBzdGFydENvbnRhaW5lciBYUGF0aCcpO1xuICAgIH1cblxuICAgIGNvbnN0IGVuZENvbnRhaW5lciA9IG5vZGVGcm9tWFBhdGgoc2VsZWN0b3IuZW5kQ29udGFpbmVyLCByb290KTtcbiAgICBpZiAoIWVuZENvbnRhaW5lcikge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdGYWlsZWQgdG8gcmVzb2x2ZSBlbmRDb250YWluZXIgWFBhdGgnKTtcbiAgICB9XG5cbiAgICBjb25zdCBzdGFydFBvcyA9IFRleHRQb3NpdGlvbi5mcm9tQ2hhck9mZnNldChcbiAgICAgIHN0YXJ0Q29udGFpbmVyLFxuICAgICAgc2VsZWN0b3Iuc3RhcnRPZmZzZXRcbiAgICApO1xuICAgIGNvbnN0IGVuZFBvcyA9IFRleHRQb3NpdGlvbi5mcm9tQ2hhck9mZnNldChcbiAgICAgIGVuZENvbnRhaW5lcixcbiAgICAgIHNlbGVjdG9yLmVuZE9mZnNldFxuICAgICk7XG5cbiAgICBjb25zdCByYW5nZSA9IG5ldyBUZXh0UmFuZ2Uoc3RhcnRQb3MsIGVuZFBvcykudG9SYW5nZSgpO1xuICAgIHJldHVybiBuZXcgUmFuZ2VBbmNob3Iocm9vdCwgcmFuZ2UpO1xuICB9XG5cbiAgdG9SYW5nZSgpIHtcbiAgICByZXR1cm4gdGhpcy5yYW5nZTtcbiAgfVxuXG4gIC8qKlxuICAgKiBAcmV0dXJuIHtSYW5nZVNlbGVjdG9yfVxuICAgKi9cbiAgdG9TZWxlY3RvcigpIHtcbiAgICAvLyBcIlNocmlua1wiIHRoZSByYW5nZSBzbyB0aGF0IGl0IHRpZ2h0bHkgd3JhcHMgaXRzIHRleHQuIFRoaXMgZW5zdXJlcyBtb3JlXG4gICAgLy8gcHJlZGljdGFibGUgb3V0cHV0IGZvciBhIGdpdmVuIHRleHQgc2VsZWN0aW9uLlxuICAgIGNvbnN0IG5vcm1hbGl6ZWRSYW5nZSA9IFRleHRSYW5nZS5mcm9tUmFuZ2UodGhpcy5yYW5nZSkudG9SYW5nZSgpO1xuXG4gICAgY29uc3QgdGV4dFJhbmdlID0gVGV4dFJhbmdlLmZyb21SYW5nZShub3JtYWxpemVkUmFuZ2UpO1xuICAgIGNvbnN0IHN0YXJ0Q29udGFpbmVyID0geHBhdGhGcm9tTm9kZSh0ZXh0UmFuZ2Uuc3RhcnQuZWxlbWVudCwgdGhpcy5yb290KTtcbiAgICBjb25zdCBlbmRDb250YWluZXIgPSB4cGF0aEZyb21Ob2RlKHRleHRSYW5nZS5lbmQuZWxlbWVudCwgdGhpcy5yb290KTtcblxuICAgIHJldHVybiB7XG4gICAgICB0eXBlOiAnUmFuZ2VTZWxlY3RvcicsXG4gICAgICBzdGFydENvbnRhaW5lcixcbiAgICAgIHN0YXJ0T2Zmc2V0OiB0ZXh0UmFuZ2Uuc3RhcnQub2Zmc2V0LFxuICAgICAgZW5kQ29udGFpbmVyLFxuICAgICAgZW5kT2Zmc2V0OiB0ZXh0UmFuZ2UuZW5kLm9mZnNldCxcbiAgICB9O1xuICB9XG59XG5cbi8qKlxuICogQ29udmVydHMgYmV0d2VlbiBgVGV4dFBvc2l0aW9uU2VsZWN0b3JgIHNlbGVjdG9ycyBhbmQgYFJhbmdlYCBvYmplY3RzLlxuICovXG5leHBvcnQgY2xhc3MgVGV4dFBvc2l0aW9uQW5jaG9yIHtcbiAgLyoqXG4gICAqIEBwYXJhbSB7RWxlbWVudH0gcm9vdFxuICAgKiBAcGFyYW0ge251bWJlcn0gc3RhcnRcbiAgICogQHBhcmFtIHtudW1iZXJ9IGVuZFxuICAgKi9cbiAgY29uc3RydWN0b3Iocm9vdCwgc3RhcnQsIGVuZCkge1xuICAgIHRoaXMucm9vdCA9IHJvb3Q7XG4gICAgdGhpcy5zdGFydCA9IHN0YXJ0O1xuICAgIHRoaXMuZW5kID0gZW5kO1xuICB9XG5cbiAgLyoqXG4gICAqIEBwYXJhbSB7RWxlbWVudH0gcm9vdFxuICAgKiBAcGFyYW0ge1JhbmdlfSByYW5nZVxuICAgKi9cbiAgc3RhdGljIGZyb21SYW5nZShyb290LCByYW5nZSkge1xuICAgIGNvbnN0IHRleHRSYW5nZSA9IFRleHRSYW5nZS5mcm9tUmFuZ2UocmFuZ2UpLnJlbGF0aXZlVG8ocm9vdCk7XG4gICAgcmV0dXJuIG5ldyBUZXh0UG9zaXRpb25BbmNob3IoXG4gICAgICByb290LFxuICAgICAgdGV4dFJhbmdlLnN0YXJ0Lm9mZnNldCxcbiAgICAgIHRleHRSYW5nZS5lbmQub2Zmc2V0XG4gICAgKTtcbiAgfVxuICAvKipcbiAgICogQHBhcmFtIHtFbGVtZW50fSByb290XG4gICAqIEBwYXJhbSB7VGV4dFBvc2l0aW9uU2VsZWN0b3J9IHNlbGVjdG9yXG4gICAqL1xuICBzdGF0aWMgZnJvbVNlbGVjdG9yKHJvb3QsIHNlbGVjdG9yKSB7XG4gICAgcmV0dXJuIG5ldyBUZXh0UG9zaXRpb25BbmNob3Iocm9vdCwgc2VsZWN0b3Iuc3RhcnQsIHNlbGVjdG9yLmVuZCk7XG4gIH1cblxuICAvKipcbiAgICogQHJldHVybiB7VGV4dFBvc2l0aW9uU2VsZWN0b3J9XG4gICAqL1xuICB0b1NlbGVjdG9yKCkge1xuICAgIHJldHVybiB7XG4gICAgICB0eXBlOiAnVGV4dFBvc2l0aW9uU2VsZWN0b3InLFxuICAgICAgc3RhcnQ6IHRoaXMuc3RhcnQsXG4gICAgICBlbmQ6IHRoaXMuZW5kLFxuICAgIH07XG4gIH1cblxuICB0b1JhbmdlKCkge1xuICAgIHJldHVybiBUZXh0UmFuZ2UuZnJvbU9mZnNldHModGhpcy5yb290LCB0aGlzLnN0YXJ0LCB0aGlzLmVuZCkudG9SYW5nZSgpO1xuICB9XG59XG5cbi8qKlxuICogQHR5cGVkZWYgUXVvdGVNYXRjaE9wdGlvbnNcbiAqIEBwcm9wIHtudW1iZXJ9IFtoaW50XSAtIEV4cGVjdGVkIHBvc2l0aW9uIG9mIG1hdGNoIGluIHRleHQuIFNlZSBgbWF0Y2hRdW90ZWAuXG4gKi9cblxuLyoqXG4gKiBDb252ZXJ0cyBiZXR3ZWVuIGBUZXh0UXVvdGVTZWxlY3RvcmAgc2VsZWN0b3JzIGFuZCBgUmFuZ2VgIG9iamVjdHMuXG4gKi9cbmV4cG9ydCBjbGFzcyBUZXh0UXVvdGVBbmNob3Ige1xuICAvKipcbiAgICogQHBhcmFtIHtFbGVtZW50fSByb290IC0gQSByb290IGVsZW1lbnQgZnJvbSB3aGljaCB0byBhbmNob3IuXG4gICAqIEBwYXJhbSB7c3RyaW5nfSBleGFjdFxuICAgKiBAcGFyYW0ge09iamVjdH0gY29udGV4dFxuICAgKiAgIEBwYXJhbSB7c3RyaW5nfSBbY29udGV4dC5wcmVmaXhdXG4gICAqICAgQHBhcmFtIHtzdHJpbmd9IFtjb250ZXh0LnN1ZmZpeF1cbiAgICovXG4gIGNvbnN0cnVjdG9yKHJvb3QsIGV4YWN0LCBjb250ZXh0ID0ge30pIHtcbiAgICB0aGlzLnJvb3QgPSByb290O1xuICAgIHRoaXMuZXhhY3QgPSBleGFjdDtcbiAgICB0aGlzLmNvbnRleHQgPSBjb250ZXh0O1xuICB9XG5cbiAgLyoqXG4gICAqIENyZWF0ZSBhIGBUZXh0UXVvdGVBbmNob3JgIGZyb20gYSByYW5nZS5cbiAgICpcbiAgICogV2lsbCB0aHJvdyBpZiBgcmFuZ2VgIGRvZXMgbm90IGNvbnRhaW4gYW55IHRleHQgbm9kZXMuXG4gICAqXG4gICAqIEBwYXJhbSB7RWxlbWVudH0gcm9vdFxuICAgKiBAcGFyYW0ge1JhbmdlfSByYW5nZVxuICAgKi9cbiAgc3RhdGljIGZyb21SYW5nZShyb290LCByYW5nZSkge1xuICAgIGNvbnN0IHRleHQgPSAvKiogQHR5cGUge3N0cmluZ30gKi8gKHJvb3QudGV4dENvbnRlbnQpO1xuICAgIGNvbnN0IHRleHRSYW5nZSA9IFRleHRSYW5nZS5mcm9tUmFuZ2UocmFuZ2UpLnJlbGF0aXZlVG8ocm9vdCk7XG5cbiAgICBjb25zdCBzdGFydCA9IHRleHRSYW5nZS5zdGFydC5vZmZzZXQ7XG4gICAgY29uc3QgZW5kID0gdGV4dFJhbmdlLmVuZC5vZmZzZXQ7XG5cbiAgICAvLyBOdW1iZXIgb2YgY2hhcmFjdGVycyBhcm91bmQgdGhlIHF1b3RlIHRvIGNhcHR1cmUgYXMgY29udGV4dC4gV2UgY3VycmVudGx5XG4gICAgLy8gYWx3YXlzIHVzZSBhIGZpeGVkIGFtb3VudCwgYnV0IGl0IHdvdWxkIGJlIGJldHRlciBpZiB0aGlzIGNvZGUgd2FzIGF3YXJlXG4gICAgLy8gb2YgbG9naWNhbCBib3VuZGFyaWVzIGluIHRoZSBkb2N1bWVudCAocGFyYWdyYXBoLCBhcnRpY2xlIGV0Yy4pIHRvIGF2b2lkXG4gICAgLy8gY2FwdHVyaW5nIHRleHQgdW5yZWxhdGVkIHRvIHRoZSBxdW90ZS5cbiAgICAvL1xuICAgIC8vIEluIHJlZ3VsYXIgcHJvc2UgdGhlIGlkZWFsIGNvbnRlbnQgd291bGQgb2Z0ZW4gYmUgdGhlIHN1cnJvdW5kaW5nIHNlbnRlbmNlLlxuICAgIC8vIFRoaXMgaXMgYSBuYXR1cmFsIHVuaXQgb2YgbWVhbmluZyB3aGljaCBlbmFibGVzIGRpc3BsYXlpbmcgcXVvdGVzIGluXG4gICAgLy8gY29udGV4dCBldmVuIHdoZW4gdGhlIGRvY3VtZW50IGlzIG5vdCBhdmFpbGFibGUuIFdlIGNvdWxkIHVzZSBgSW50bC5TZWdtZW50ZXJgXG4gICAgLy8gZm9yIHRoaXMgd2hlbiBhdmFpbGFibGUuXG4gICAgY29uc3QgY29udGV4dExlbiA9IDMyO1xuXG4gICAgcmV0dXJuIG5ldyBUZXh0UXVvdGVBbmNob3Iocm9vdCwgdGV4dC5zbGljZShzdGFydCwgZW5kKSwge1xuICAgICAgcHJlZml4OiB0ZXh0LnNsaWNlKE1hdGgubWF4KDAsIHN0YXJ0IC0gY29udGV4dExlbiksIHN0YXJ0KSxcbiAgICAgIHN1ZmZpeDogdGV4dC5zbGljZShlbmQsIE1hdGgubWluKHRleHQubGVuZ3RoLCBlbmQgKyBjb250ZXh0TGVuKSksXG4gICAgfSk7XG4gIH1cblxuICAvKipcbiAgICogQHBhcmFtIHtFbGVtZW50fSByb290XG4gICAqIEBwYXJhbSB7VGV4dFF1b3RlU2VsZWN0b3J9IHNlbGVjdG9yXG4gICAqL1xuICBzdGF0aWMgZnJvbVNlbGVjdG9yKHJvb3QsIHNlbGVjdG9yKSB7XG4gICAgY29uc3QgeyBwcmVmaXgsIHN1ZmZpeCB9ID0gc2VsZWN0b3I7XG4gICAgcmV0dXJuIG5ldyBUZXh0UXVvdGVBbmNob3Iocm9vdCwgc2VsZWN0b3IuZXhhY3QsIHsgcHJlZml4LCBzdWZmaXggfSk7XG4gIH1cblxuICAvKipcbiAgICogQHJldHVybiB7VGV4dFF1b3RlU2VsZWN0b3J9XG4gICAqL1xuICB0b1NlbGVjdG9yKCkge1xuICAgIHJldHVybiB7XG4gICAgICB0eXBlOiAnVGV4dFF1b3RlU2VsZWN0b3InLFxuICAgICAgZXhhY3Q6IHRoaXMuZXhhY3QsXG4gICAgICBwcmVmaXg6IHRoaXMuY29udGV4dC5wcmVmaXgsXG4gICAgICBzdWZmaXg6IHRoaXMuY29udGV4dC5zdWZmaXgsXG4gICAgfTtcbiAgfVxuXG4gIC8qKlxuICAgKiBAcGFyYW0ge1F1b3RlTWF0Y2hPcHRpb25zfSBbb3B0aW9uc11cbiAgICovXG4gIHRvUmFuZ2Uob3B0aW9ucyA9IHt9KSB7XG4gICAgcmV0dXJuIHRoaXMudG9Qb3NpdGlvbkFuY2hvcihvcHRpb25zKS50b1JhbmdlKCk7XG4gIH1cblxuICAvKipcbiAgICogQHBhcmFtIHtRdW90ZU1hdGNoT3B0aW9uc30gW29wdGlvbnNdXG4gICAqL1xuICB0b1Bvc2l0aW9uQW5jaG9yKG9wdGlvbnMgPSB7fSkge1xuICAgIGNvbnN0IHRleHQgPSAvKiogQHR5cGUge3N0cmluZ30gKi8gKHRoaXMucm9vdC50ZXh0Q29udGVudCk7XG4gICAgY29uc3QgbWF0Y2ggPSBtYXRjaFF1b3RlKHRleHQsIHRoaXMuZXhhY3QsIHtcbiAgICAgIC4uLnRoaXMuY29udGV4dCxcbiAgICAgIGhpbnQ6IG9wdGlvbnMuaGludCxcbiAgICB9KTtcbiAgICBpZiAoIW1hdGNoKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoJ1F1b3RlIG5vdCBmb3VuZCcpO1xuICAgIH1cbiAgICByZXR1cm4gbmV3IFRleHRQb3NpdGlvbkFuY2hvcih0aGlzLnJvb3QsIG1hdGNoLnN0YXJ0LCBtYXRjaC5lbmQpO1xuICB9XG59XG4iLCIvL1xuLy8gIENvcHlyaWdodCAyMDIxIFJlYWRpdW0gRm91bmRhdGlvbi4gQWxsIHJpZ2h0cyByZXNlcnZlZC5cbi8vICBVc2Ugb2YgdGhpcyBzb3VyY2UgY29kZSBpcyBnb3Zlcm5lZCBieSB0aGUgQlNELXN0eWxlIGxpY2Vuc2Vcbi8vICBhdmFpbGFibGUgaW4gdGhlIHRvcC1sZXZlbCBMSUNFTlNFIGZpbGUgb2YgdGhlIHByb2plY3QuXG4vL1xuXG5pbXBvcnQgeyBUZXh0UXVvdGVBbmNob3IgfSBmcm9tIFwiLi92ZW5kb3IvaHlwb3RoZXNpcy9hbmNob3JpbmcvdHlwZXNcIjtcblxuLy8gQ2F0Y2ggSlMgZXJyb3JzIHRvIGxvZyB0aGVtIGluIHRoZSBhcHAuXG53aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcihcbiAgXCJlcnJvclwiLFxuICBmdW5jdGlvbiAoZXZlbnQpIHtcbiAgICBBbmRyb2lkLmxvZ0Vycm9yKGV2ZW50Lm1lc3NhZ2UsIGV2ZW50LmZpbGVuYW1lLCBldmVudC5saW5lbm8pO1xuICB9LFxuICBmYWxzZVxuKTtcblxud2luZG93LmFkZEV2ZW50TGlzdGVuZXIoXG4gIFwibG9hZFwiLFxuICBmdW5jdGlvbiAoKSB7XG4gICAgY29uc3Qgb2JzZXJ2ZXIgPSBuZXcgUmVzaXplT2JzZXJ2ZXIoKCkgPT4ge1xuICAgICAgb25WaWV3cG9ydFdpZHRoQ2hhbmdlZCgpO1xuICAgICAgc25hcEN1cnJlbnRPZmZzZXQoKTtcbiAgICB9KTtcbiAgICBvYnNlcnZlci5vYnNlcnZlKGRvY3VtZW50LmJvZHkpO1xuICB9LFxuICBmYWxzZVxuKTtcblxuLyoqXG4gKiBIYXZpbmcgYW4gb2RkIG51bWJlciBvZiBjb2x1bW5zIHdoZW4gZGlzcGxheWluZyB0d28gY29sdW1ucyBwZXIgc2NyZWVuIGNhdXNlcyBzbmFwcGluZyBhbmQgcGFnZVxuICogdHVybmluZyBpc3N1ZXMuIFRvIGZpeCB0aGlzLCB3ZSBpbnNlcnQgYSBibGFuayB2aXJ0dWFsIGNvbHVtbiBhdCB0aGUgZW5kIG9mIHRoZSByZXNvdXJjZS5cbiAqL1xuZnVuY3Rpb24gYXBwZW5kVmlydHVhbENvbHVtbklmTmVlZGVkKCkge1xuICBjb25zdCBpZCA9IFwicmVhZGl1bS12aXJ0dWFsLXBhZ2VcIjtcbiAgdmFyIHZpcnR1YWxDb2wgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZChpZCk7XG4gIGlmIChpc1Njcm9sbE1vZGVFbmFibGVkKCkgfHwgZ2V0Q29sdW1uQ291bnRQZXJTY3JlZW4oKSAhPSAyKSB7XG4gICAgaWYgKHZpcnR1YWxDb2wpIHtcbiAgICAgIHZpcnR1YWxDb2wucmVtb3ZlKCk7XG4gICAgfVxuICB9IGVsc2Uge1xuICAgIHZhciBkb2N1bWVudFdpZHRoID0gZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudC5zY3JvbGxXaWR0aDtcbiAgICB2YXIgY29sQ291bnQgPSBkb2N1bWVudFdpZHRoIC8gcGFnZVdpZHRoO1xuICAgIHZhciBoYXNPZGRDb2xDb3VudCA9IChNYXRoLnJvdW5kKGNvbENvdW50ICogMikgLyAyKSAlIDEgPiAwLjE7XG4gICAgaWYgKGhhc09kZENvbENvdW50KSB7XG4gICAgICBpZiAodmlydHVhbENvbCkge1xuICAgICAgICB2aXJ0dWFsQ29sLnJlbW92ZSgpO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdmlydHVhbENvbCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoXCJkaXZcIik7XG4gICAgICAgIHZpcnR1YWxDb2wuc2V0QXR0cmlidXRlKFwiaWRcIiwgaWQpO1xuICAgICAgICB2aXJ0dWFsQ29sLnN0eWxlLmJyZWFrQmVmb3JlID0gXCJjb2x1bW5cIjtcbiAgICAgICAgdmlydHVhbENvbC5pbm5lckhUTUwgPSBcIiYjODIwMztcIjsgLy8gemVyby13aWR0aCBzcGFjZVxuICAgICAgICBkb2N1bWVudC5ib2R5LmFwcGVuZENoaWxkKHZpcnR1YWxDb2wpO1xuICAgICAgfVxuICAgIH1cbiAgfVxufVxuXG5leHBvcnQgdmFyIHBhZ2VXaWR0aCA9IDE7XG5cbmZ1bmN0aW9uIG9uVmlld3BvcnRXaWR0aENoYW5nZWQoKSB7XG4gIC8vIFdlIGNhbid0IHJlbHkgb24gd2luZG93LmlubmVyV2lkdGggZm9yIHRoZSBwYWdlV2lkdGggb24gQW5kcm9pZCwgYmVjYXVzZSBpZiB0aGVcbiAgLy8gZGV2aWNlIHBpeGVsIHJhdGlvIGlzIG5vdCBhbiBpbnRlZ2VyLCB3ZSBnZXQgcm91bmRpbmcgaXNzdWVzIG9mZnNldHRpbmcgdGhlIHBhZ2VzLlxuICAvL1xuICAvLyBTZWUgaHR0cHM6Ly9naXRodWIuY29tL3JlYWRpdW0vcmVhZGl1bS1jc3MvaXNzdWVzLzk3XG4gIC8vIGFuZCBodHRwczovL2dpdGh1Yi5jb20vcmVhZGl1bS9yMi1uYXZpZ2F0b3Ita290bGluL2lzc3Vlcy8xNDZcbiAgdmFyIHdpZHRoID0gQW5kcm9pZC5nZXRWaWV3cG9ydFdpZHRoKCk7XG4gIHBhZ2VXaWR0aCA9IHdpZHRoIC8gd2luZG93LmRldmljZVBpeGVsUmF0aW87XG4gIHNldFByb3BlcnR5KFxuICAgIFwiLS1SU19fdmlld3BvcnRXaWR0aFwiLFxuICAgIFwiY2FsYyhcIiArIHdpZHRoICsgXCJweCAvIFwiICsgd2luZG93LmRldmljZVBpeGVsUmF0aW8gKyBcIilcIlxuICApO1xuXG4gIGFwcGVuZFZpcnR1YWxDb2x1bW5JZk5lZWRlZCgpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gZ2V0Q29sdW1uQ291bnRQZXJTY3JlZW4oKSB7XG4gIHJldHVybiBwYXJzZUludChcbiAgICB3aW5kb3dcbiAgICAgIC5nZXRDb21wdXRlZFN0eWxlKGRvY3VtZW50LmRvY3VtZW50RWxlbWVudClcbiAgICAgIC5nZXRQcm9wZXJ0eVZhbHVlKFwiY29sdW1uLWNvdW50XCIpXG4gICk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBpc1Njcm9sbE1vZGVFbmFibGVkKCkge1xuICBjb25zdCBzdHlsZSA9IGRvY3VtZW50LmRvY3VtZW50RWxlbWVudC5zdHlsZTtcbiAgcmV0dXJuIChcbiAgICBzdHlsZS5nZXRQcm9wZXJ0eVZhbHVlKFwiLS1VU0VSX192aWV3XCIpLnRyaW0oKSA9PSBcInJlYWRpdW0tc2Nyb2xsLW9uXCIgfHxcbiAgICAvLyBGSVhNRTogV2lsbCBuZWVkIHRvIGJlIHJlbW92ZWQgaW4gUmVhZGl1bSAzLjAsIC0tVVNFUl9fc2Nyb2xsIHdhcyBpbmNvcnJlY3QuXG4gICAgc3R5bGUuZ2V0UHJvcGVydHlWYWx1ZShcIi0tVVNFUl9fc2Nyb2xsXCIpLnRyaW0oKSA9PSBcInJlYWRpdW0tc2Nyb2xsLW9uXCJcbiAgKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGlzUlRMKCkge1xuICByZXR1cm4gZG9jdW1lbnQuYm9keS5kaXIudG9Mb3dlckNhc2UoKSA9PSBcInJ0bFwiO1xufVxuXG4vLyBTY3JvbGwgdG8gdGhlIGdpdmVuIFRhZ0lkIGluIGRvY3VtZW50IGFuZCBzbmFwLlxuZXhwb3J0IGZ1bmN0aW9uIHNjcm9sbFRvSWQoaWQpIHtcbiAgdmFyIGVsZW1lbnQgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZChpZCk7XG4gIGlmICghZWxlbWVudCkge1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuXG4gIHJldHVybiBzY3JvbGxUb1JlY3QoZWxlbWVudC5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKSk7XG59XG5cbi8vIFBvc2l0aW9uIG11c3QgYmUgaW4gdGhlIHJhbmdlIFswIC0gMV0sIDAtMTAwJS5cbmV4cG9ydCBmdW5jdGlvbiBzY3JvbGxUb1Bvc2l0aW9uKHBvc2l0aW9uKSB7XG4gIC8vICAgICAgICBBbmRyb2lkLmxvZyhcInNjcm9sbFRvUG9zaXRpb24gXCIgKyBwb3NpdGlvbik7XG4gIGlmIChwb3NpdGlvbiA8IDAgfHwgcG9zaXRpb24gPiAxKSB7XG4gICAgdGhyb3cgXCJzY3JvbGxUb1Bvc2l0aW9uKCkgbXVzdCBiZSBnaXZlbiBhIHBvc2l0aW9uIGZyb20gMC4wIHRvICAxLjBcIjtcbiAgfVxuXG4gIGxldCBvZmZzZXQ7XG4gIGlmIChpc1Njcm9sbE1vZGVFbmFibGVkKCkpIHtcbiAgICBvZmZzZXQgPSBkb2N1bWVudC5zY3JvbGxpbmdFbGVtZW50LnNjcm9sbEhlaWdodCAqIHBvc2l0aW9uO1xuICAgIGRvY3VtZW50LnNjcm9sbGluZ0VsZW1lbnQuc2Nyb2xsVG9wID0gb2Zmc2V0O1xuICAgIC8vIHdpbmRvdy5zY3JvbGxUbygwLCBvZmZzZXQpO1xuICB9IGVsc2Uge1xuICAgIHZhciBkb2N1bWVudFdpZHRoID0gZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudC5zY3JvbGxXaWR0aDtcbiAgICB2YXIgZmFjdG9yID0gaXNSVEwoKSA/IC0xIDogMTtcbiAgICBvZmZzZXQgPSBkb2N1bWVudFdpZHRoICogcG9zaXRpb24gKiBmYWN0b3I7XG4gICAgZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudC5zY3JvbGxMZWZ0ID0gc25hcE9mZnNldChvZmZzZXQpO1xuICB9XG59XG5cbi8vIFNjcm9sbHMgdG8gdGhlIGZpcnN0IG9jY3VycmVuY2Ugb2YgdGhlIGdpdmVuIHRleHQgc25pcHBldC5cbi8vXG4vLyBUaGUgZXhwZWN0ZWQgdGV4dCBhcmd1bWVudCBpcyBhIExvY2F0b3IgVGV4dCBvYmplY3QsIGFzIGRlZmluZWQgaGVyZTpcbi8vIGh0dHBzOi8vcmVhZGl1bS5vcmcvYXJjaGl0ZWN0dXJlL21vZGVscy9sb2NhdG9ycy9cbmV4cG9ydCBmdW5jdGlvbiBzY3JvbGxUb1RleHQodGV4dCkge1xuICBsZXQgcmFuZ2UgPSByYW5nZUZyb21Mb2NhdG9yKHsgdGV4dCB9KTtcbiAgaWYgKCFyYW5nZSkge1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuICBzY3JvbGxUb1JhbmdlKHJhbmdlKTtcbiAgcmV0dXJuIHRydWU7XG59XG5cbmZ1bmN0aW9uIHNjcm9sbFRvUmFuZ2UocmFuZ2UpIHtcbiAgcmV0dXJuIHNjcm9sbFRvUmVjdChyYW5nZS5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKSk7XG59XG5cbmZ1bmN0aW9uIHNjcm9sbFRvUmVjdChyZWN0KSB7XG4gIGlmIChpc1Njcm9sbE1vZGVFbmFibGVkKCkpIHtcbiAgICBkb2N1bWVudC5zY3JvbGxpbmdFbGVtZW50LnNjcm9sbFRvcCA9IHJlY3QudG9wICsgd2luZG93LnNjcm9sbFk7XG4gIH0gZWxzZSB7XG4gICAgZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudC5zY3JvbGxMZWZ0ID0gc25hcE9mZnNldChcbiAgICAgIHJlY3QubGVmdCArIHdpbmRvdy5zY3JvbGxYXG4gICAgKTtcbiAgfVxuXG4gIHJldHVybiB0cnVlO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gc2Nyb2xsVG9TdGFydCgpIHtcbiAgLy8gICAgICAgIEFuZHJvaWQubG9nKFwic2Nyb2xsVG9TdGFydFwiKTtcbiAgaWYgKCFpc1Njcm9sbE1vZGVFbmFibGVkKCkpIHtcbiAgICBkb2N1bWVudC5zY3JvbGxpbmdFbGVtZW50LnNjcm9sbExlZnQgPSAwO1xuICB9IGVsc2Uge1xuICAgIGRvY3VtZW50LnNjcm9sbGluZ0VsZW1lbnQuc2Nyb2xsVG9wID0gMDtcbiAgICB3aW5kb3cuc2Nyb2xsVG8oMCwgMCk7XG4gIH1cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHNjcm9sbFRvRW5kKCkge1xuICAvLyAgICAgICAgQW5kcm9pZC5sb2coXCJzY3JvbGxUb0VuZFwiKTtcbiAgaWYgKCFpc1Njcm9sbE1vZGVFbmFibGVkKCkpIHtcbiAgICB2YXIgZmFjdG9yID0gaXNSVEwoKSA/IC0xIDogMTtcbiAgICBkb2N1bWVudC5zY3JvbGxpbmdFbGVtZW50LnNjcm9sbExlZnQgPSBzbmFwT2Zmc2V0KFxuICAgICAgZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudC5zY3JvbGxXaWR0aCAqIGZhY3RvclxuICAgICk7XG4gIH0gZWxzZSB7XG4gICAgZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudC5zY3JvbGxUb3AgPSBkb2N1bWVudC5ib2R5LnNjcm9sbEhlaWdodDtcbiAgICB3aW5kb3cuc2Nyb2xsVG8oMCwgZG9jdW1lbnQuYm9keS5zY3JvbGxIZWlnaHQpO1xuICB9XG59XG5cbi8vIFJldHVybnMgZmFsc2UgaWYgdGhlIHBhZ2UgaXMgYWxyZWFkeSBhdCB0aGUgbGVmdC1tb3N0IHNjcm9sbCBvZmZzZXQuXG5leHBvcnQgZnVuY3Rpb24gc2Nyb2xsTGVmdCgpIHtcbiAgdmFyIGRvY3VtZW50V2lkdGggPSBkb2N1bWVudC5zY3JvbGxpbmdFbGVtZW50LnNjcm9sbFdpZHRoO1xuICB2YXIgb2Zmc2V0ID0gd2luZG93LnNjcm9sbFggLSBwYWdlV2lkdGg7XG4gIHZhciBtaW5PZmZzZXQgPSBpc1JUTCgpID8gLShkb2N1bWVudFdpZHRoIC0gcGFnZVdpZHRoKSA6IDA7XG4gIHJldHVybiBzY3JvbGxUb09mZnNldChNYXRoLm1heChvZmZzZXQsIG1pbk9mZnNldCkpO1xufVxuXG4vLyBSZXR1cm5zIGZhbHNlIGlmIHRoZSBwYWdlIGlzIGFscmVhZHkgYXQgdGhlIHJpZ2h0LW1vc3Qgc2Nyb2xsIG9mZnNldC5cbmV4cG9ydCBmdW5jdGlvbiBzY3JvbGxSaWdodCgpIHtcbiAgdmFyIGRvY3VtZW50V2lkdGggPSBkb2N1bWVudC5zY3JvbGxpbmdFbGVtZW50LnNjcm9sbFdpZHRoO1xuICB2YXIgb2Zmc2V0ID0gd2luZG93LnNjcm9sbFggKyBwYWdlV2lkdGg7XG4gIHZhciBtYXhPZmZzZXQgPSBpc1JUTCgpID8gMCA6IGRvY3VtZW50V2lkdGggLSBwYWdlV2lkdGg7XG4gIHJldHVybiBzY3JvbGxUb09mZnNldChNYXRoLm1pbihvZmZzZXQsIG1heE9mZnNldCkpO1xufVxuXG4vLyBTY3JvbGxzIHRvIHRoZSBnaXZlbiBsZWZ0IG9mZnNldC5cbi8vIFJldHVybnMgZmFsc2UgaWYgdGhlIHBhZ2Ugc2Nyb2xsIHBvc2l0aW9uIGlzIGFscmVhZHkgY2xvc2UgZW5vdWdoIHRvIHRoZSBnaXZlbiBvZmZzZXQuXG5mdW5jdGlvbiBzY3JvbGxUb09mZnNldChvZmZzZXQpIHtcbiAgLy8gICAgICAgIEFuZHJvaWQubG9nKFwic2Nyb2xsVG9PZmZzZXQgXCIgKyBvZmZzZXQpO1xuICBpZiAoaXNTY3JvbGxNb2RlRW5hYmxlZCgpKSB7XG4gICAgdGhyb3cgXCJDYWxsZWQgc2Nyb2xsVG9PZmZzZXQoKSB3aXRoIHNjcm9sbCBtb2RlIGVuYWJsZWQuIFRoaXMgY2FuIG9ubHkgYmUgdXNlZCBpbiBwYWdpbmF0ZWQgbW9kZS5cIjtcbiAgfVxuXG4gIHZhciBjdXJyZW50T2Zmc2V0ID0gd2luZG93LnNjcm9sbFg7XG4gIGRvY3VtZW50LnNjcm9sbGluZ0VsZW1lbnQuc2Nyb2xsTGVmdCA9IHNuYXBPZmZzZXQob2Zmc2V0KTtcbiAgLy8gSW4gc29tZSBjYXNlIHRoZSBzY3JvbGxYIGNhbm5vdCByZWFjaCB0aGUgcG9zaXRpb24gcmVzcGVjdGluZyB0byBpbm5lcldpZHRoXG4gIHZhciBkaWZmID0gTWF0aC5hYnMoY3VycmVudE9mZnNldCAtIG9mZnNldCkgLyBwYWdlV2lkdGg7XG4gIHJldHVybiBkaWZmID4gMC4wMTtcbn1cblxuLy8gU25hcCB0aGUgb2Zmc2V0IHRvIHRoZSBzY3JlZW4gd2lkdGggKHBhZ2Ugd2lkdGgpLlxuZnVuY3Rpb24gc25hcE9mZnNldChvZmZzZXQpIHtcbiAgdmFyIHZhbHVlID0gb2Zmc2V0ICsgKGlzUlRMKCkgPyAtMSA6IDEpO1xuICByZXR1cm4gdmFsdWUgLSAodmFsdWUgJSBwYWdlV2lkdGgpO1xufVxuXG4vLyBTbmFwcyB0aGUgY3VycmVudCBvZmZzZXQgdG8gdGhlIHBhZ2Ugd2lkdGguXG5leHBvcnQgZnVuY3Rpb24gc25hcEN1cnJlbnRPZmZzZXQoKSB7XG4gIC8vICAgICAgICBBbmRyb2lkLmxvZyhcInNuYXBDdXJyZW50T2Zmc2V0XCIpO1xuICBpZiAoaXNTY3JvbGxNb2RlRW5hYmxlZCgpKSB7XG4gICAgcmV0dXJuO1xuICB9XG4gIHZhciBjdXJyZW50T2Zmc2V0ID0gd2luZG93LnNjcm9sbFg7XG4gIC8vIEFkZHMgaGFsZiBhIHBhZ2UgdG8gbWFrZSBzdXJlIHdlIGRvbid0IHNuYXAgdG8gdGhlIHByZXZpb3VzIHBhZ2UuXG4gIHZhciBmYWN0b3IgPSBpc1JUTCgpID8gLTEgOiAxO1xuICB2YXIgZGVsdGEgPSBmYWN0b3IgKiAocGFnZVdpZHRoIC8gMik7XG4gIGRvY3VtZW50LnNjcm9sbGluZ0VsZW1lbnQuc2Nyb2xsTGVmdCA9IHNuYXBPZmZzZXQoY3VycmVudE9mZnNldCArIGRlbHRhKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHJhbmdlRnJvbUxvY2F0b3IobG9jYXRvcikge1xuICB0cnkge1xuICAgIGxldCBsb2NhdGlvbnMgPSBsb2NhdG9yLmxvY2F0aW9ucztcbiAgICBsZXQgdGV4dCA9IGxvY2F0b3IudGV4dDtcbiAgICBpZiAodGV4dCAmJiB0ZXh0LmhpZ2hsaWdodCkge1xuICAgICAgdmFyIHJvb3Q7XG4gICAgICBpZiAobG9jYXRpb25zICYmIGxvY2F0aW9ucy5jc3NTZWxlY3Rvcikge1xuICAgICAgICByb290ID0gZG9jdW1lbnQucXVlcnlTZWxlY3Rvcihsb2NhdGlvbnMuY3NzU2VsZWN0b3IpO1xuICAgICAgfVxuICAgICAgaWYgKCFyb290KSB7XG4gICAgICAgIHJvb3QgPSBkb2N1bWVudC5ib2R5O1xuICAgICAgfVxuXG4gICAgICBsZXQgYW5jaG9yID0gbmV3IFRleHRRdW90ZUFuY2hvcihyb290LCB0ZXh0LmhpZ2hsaWdodCwge1xuICAgICAgICBwcmVmaXg6IHRleHQuYmVmb3JlLFxuICAgICAgICBzdWZmaXg6IHRleHQuYWZ0ZXIsXG4gICAgICB9KTtcbiAgICAgIHJldHVybiBhbmNob3IudG9SYW5nZSgpO1xuICAgIH1cblxuICAgIGlmIChsb2NhdGlvbnMpIHtcbiAgICAgIHZhciBlbGVtZW50ID0gbnVsbDtcblxuICAgICAgaWYgKCFlbGVtZW50ICYmIGxvY2F0aW9ucy5jc3NTZWxlY3Rvcikge1xuICAgICAgICBlbGVtZW50ID0gZG9jdW1lbnQucXVlcnlTZWxlY3Rvcihsb2NhdGlvbnMuY3NzU2VsZWN0b3IpO1xuICAgICAgfVxuXG4gICAgICBpZiAoIWVsZW1lbnQgJiYgbG9jYXRpb25zLmZyYWdtZW50cykge1xuICAgICAgICBmb3IgKGNvbnN0IGh0bWxJZCBvZiBsb2NhdGlvbnMuZnJhZ21lbnRzKSB7XG4gICAgICAgICAgZWxlbWVudCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGh0bWxJZCk7XG4gICAgICAgICAgaWYgKGVsZW1lbnQpIHtcbiAgICAgICAgICAgIGJyZWFrO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICBpZiAoZWxlbWVudCkge1xuICAgICAgICBsZXQgcmFuZ2UgPSBkb2N1bWVudC5jcmVhdGVSYW5nZSgpO1xuICAgICAgICByYW5nZS5zZXRTdGFydEJlZm9yZShlbGVtZW50KTtcbiAgICAgICAgcmFuZ2Uuc2V0RW5kQWZ0ZXIoZWxlbWVudCk7XG4gICAgICAgIHJldHVybiByYW5nZTtcbiAgICAgIH1cbiAgICB9XG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBsb2dFcnJvcihlKTtcbiAgfVxuXG4gIHJldHVybiBudWxsO1xufVxuXG4vLy8gVXNlciBTZXR0aW5ncy5cblxuZXhwb3J0IGZ1bmN0aW9uIHNldENTU1Byb3BlcnRpZXMocHJvcGVydGllcykge1xuICBmb3IgKGNvbnN0IG5hbWUgaW4gcHJvcGVydGllcykge1xuICAgIHNldFByb3BlcnR5KG5hbWUsIHByb3BlcnRpZXNbbmFtZV0pO1xuICB9XG59XG5cbi8vIEZvciBzZXR0aW5nIHVzZXIgc2V0dGluZy5cbmV4cG9ydCBmdW5jdGlvbiBzZXRQcm9wZXJ0eShrZXksIHZhbHVlKSB7XG4gIGlmICh2YWx1ZSA9PT0gbnVsbCB8fCB2YWx1ZSA9PT0gXCJcIikge1xuICAgIHJlbW92ZVByb3BlcnR5KGtleSk7XG4gIH0gZWxzZSB7XG4gICAgdmFyIHJvb3QgPSBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQ7XG4gICAgLy8gVGhlIGAhaW1wb3J0YW50YCBhbm5vdGF0aW9uIGlzIGFkZGVkIHdpdGggYHNldFByb3BlcnR5KClgIGJlY2F1c2UgaWYgaXQncyBwYXJ0IG9mIHRoZVxuICAgIC8vIGB2YWx1ZWAsIGl0IHdpbGwgYmUgaWdub3JlZCBieSB0aGUgV2ViIFZpZXcuXG4gICAgcm9vdC5zdHlsZS5zZXRQcm9wZXJ0eShrZXksIHZhbHVlLCBcImltcG9ydGFudFwiKTtcbiAgfVxufVxuXG4vLyBGb3IgcmVtb3ZpbmcgdXNlciBzZXR0aW5nLlxuZXhwb3J0IGZ1bmN0aW9uIHJlbW92ZVByb3BlcnR5KGtleSkge1xuICB2YXIgcm9vdCA9IGRvY3VtZW50LmRvY3VtZW50RWxlbWVudDtcblxuICByb290LnN0eWxlLnJlbW92ZVByb3BlcnR5KGtleSk7XG59XG5cbi8vLyBUb29sa2l0XG5cbmV4cG9ydCBmdW5jdGlvbiBsb2coKSB7XG4gIHZhciBtZXNzYWdlID0gQXJyYXkucHJvdG90eXBlLnNsaWNlLmNhbGwoYXJndW1lbnRzKS5qb2luKFwiIFwiKTtcbiAgQW5kcm9pZC5sb2cobWVzc2FnZSk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBsb2dFcnJvcihtZXNzYWdlKSB7XG4gIEFuZHJvaWQubG9nRXJyb3IobWVzc2FnZSwgXCJcIiwgMCk7XG59XG4iLCIvL1xuLy8gIENvcHlyaWdodCAyMDIxIFJlYWRpdW0gRm91bmRhdGlvbi4gQWxsIHJpZ2h0cyByZXNlcnZlZC5cbi8vICBVc2Ugb2YgdGhpcyBzb3VyY2UgY29kZSBpcyBnb3Zlcm5lZCBieSB0aGUgQlNELXN0eWxlIGxpY2Vuc2Vcbi8vICBhdmFpbGFibGUgaW4gdGhlIHRvcC1sZXZlbCBMSUNFTlNFIGZpbGUgb2YgdGhlIHByb2plY3QuXG4vL1xuXG5pbXBvcnQgeyBsb2cgYXMgbG9nTmF0aXZlIH0gZnJvbSBcIi4vdXRpbHNcIjtcblxuY29uc3QgZGVidWcgPSBmYWxzZTtcblxuLyoqXG4gKiBDb252ZXJ0cyBhIERPTVJlY3QgaW50byBhIEpTT04gb2JqZWN0IHVuZGVyc3RhbmRhYmxlIGJ5IHRoZSBuYXRpdmUgc2lkZS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHRvTmF0aXZlUmVjdChyZWN0KSB7XG4gIGNvbnN0IHBpeGVsUmF0aW8gPSB3aW5kb3cuZGV2aWNlUGl4ZWxSYXRpbztcbiAgY29uc3Qgd2lkdGggPSByZWN0LndpZHRoICogcGl4ZWxSYXRpbztcbiAgY29uc3QgaGVpZ2h0ID0gcmVjdC5oZWlnaHQgKiBwaXhlbFJhdGlvO1xuICBjb25zdCBsZWZ0ID0gcmVjdC5sZWZ0ICogcGl4ZWxSYXRpbztcbiAgY29uc3QgdG9wID0gcmVjdC50b3AgKiBwaXhlbFJhdGlvO1xuICBjb25zdCByaWdodCA9IGxlZnQgKyB3aWR0aDtcbiAgY29uc3QgYm90dG9tID0gdG9wICsgaGVpZ2h0O1xuICByZXR1cm4geyB3aWR0aCwgaGVpZ2h0LCBsZWZ0LCB0b3AsIHJpZ2h0LCBib3R0b20gfTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGdldENsaWVudFJlY3RzTm9PdmVybGFwKFxuICByYW5nZSxcbiAgZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0c1xuKSB7XG4gIGxldCBjbGllbnRSZWN0cyA9IHJhbmdlLmdldENsaWVudFJlY3RzKCk7XG5cbiAgY29uc3QgdG9sZXJhbmNlID0gMTtcbiAgY29uc3Qgb3JpZ2luYWxSZWN0cyA9IFtdO1xuICBmb3IgKGNvbnN0IHJhbmdlQ2xpZW50UmVjdCBvZiBjbGllbnRSZWN0cykge1xuICAgIG9yaWdpbmFsUmVjdHMucHVzaCh7XG4gICAgICBib3R0b206IHJhbmdlQ2xpZW50UmVjdC5ib3R0b20sXG4gICAgICBoZWlnaHQ6IHJhbmdlQ2xpZW50UmVjdC5oZWlnaHQsXG4gICAgICBsZWZ0OiByYW5nZUNsaWVudFJlY3QubGVmdCxcbiAgICAgIHJpZ2h0OiByYW5nZUNsaWVudFJlY3QucmlnaHQsXG4gICAgICB0b3A6IHJhbmdlQ2xpZW50UmVjdC50b3AsXG4gICAgICB3aWR0aDogcmFuZ2VDbGllbnRSZWN0LndpZHRoLFxuICAgIH0pO1xuICB9XG4gIGNvbnN0IG1lcmdlZFJlY3RzID0gbWVyZ2VUb3VjaGluZ1JlY3RzKFxuICAgIG9yaWdpbmFsUmVjdHMsXG4gICAgdG9sZXJhbmNlLFxuICAgIGRvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHNcbiAgKTtcbiAgY29uc3Qgbm9Db250YWluZWRSZWN0cyA9IHJlbW92ZUNvbnRhaW5lZFJlY3RzKG1lcmdlZFJlY3RzLCB0b2xlcmFuY2UpO1xuICBjb25zdCBuZXdSZWN0cyA9IHJlcGxhY2VPdmVybGFwaW5nUmVjdHMobm9Db250YWluZWRSZWN0cyk7XG4gIGNvbnN0IG1pbkFyZWEgPSAyICogMjtcbiAgZm9yIChsZXQgaiA9IG5ld1JlY3RzLmxlbmd0aCAtIDE7IGogPj0gMDsgai0tKSB7XG4gICAgY29uc3QgcmVjdCA9IG5ld1JlY3RzW2pdO1xuICAgIGNvbnN0IGJpZ0Vub3VnaCA9IHJlY3Qud2lkdGggKiByZWN0LmhlaWdodCA+IG1pbkFyZWE7XG4gICAgaWYgKCFiaWdFbm91Z2gpIHtcbiAgICAgIGlmIChuZXdSZWN0cy5sZW5ndGggPiAxKSB7XG4gICAgICAgIGxvZyhcIkNMSUVOVCBSRUNUOiByZW1vdmUgc21hbGxcIik7XG4gICAgICAgIG5ld1JlY3RzLnNwbGljZShqLCAxKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGxvZyhcIkNMSUVOVCBSRUNUOiByZW1vdmUgc21hbGwsIGJ1dCBrZWVwIG90aGVyd2lzZSBlbXB0eSFcIik7XG4gICAgICAgIGJyZWFrO1xuICAgICAgfVxuICAgIH1cbiAgfVxuICBsb2coYENMSUVOVCBSRUNUOiByZWR1Y2VkICR7b3JpZ2luYWxSZWN0cy5sZW5ndGh9IC0tPiAke25ld1JlY3RzLmxlbmd0aH1gKTtcbiAgcmV0dXJuIG5ld1JlY3RzO1xufVxuXG5mdW5jdGlvbiBtZXJnZVRvdWNoaW5nUmVjdHMoXG4gIHJlY3RzLFxuICB0b2xlcmFuY2UsXG4gIGRvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHNcbikge1xuICBmb3IgKGxldCBpID0gMDsgaSA8IHJlY3RzLmxlbmd0aDsgaSsrKSB7XG4gICAgZm9yIChsZXQgaiA9IGkgKyAxOyBqIDwgcmVjdHMubGVuZ3RoOyBqKyspIHtcbiAgICAgIGNvbnN0IHJlY3QxID0gcmVjdHNbaV07XG4gICAgICBjb25zdCByZWN0MiA9IHJlY3RzW2pdO1xuICAgICAgaWYgKHJlY3QxID09PSByZWN0Mikge1xuICAgICAgICBsb2coXCJtZXJnZVRvdWNoaW5nUmVjdHMgcmVjdDEgPT09IHJlY3QyID8/IVwiKTtcbiAgICAgICAgY29udGludWU7XG4gICAgICB9XG4gICAgICBjb25zdCByZWN0c0xpbmVVcFZlcnRpY2FsbHkgPVxuICAgICAgICBhbG1vc3RFcXVhbChyZWN0MS50b3AsIHJlY3QyLnRvcCwgdG9sZXJhbmNlKSAmJlxuICAgICAgICBhbG1vc3RFcXVhbChyZWN0MS5ib3R0b20sIHJlY3QyLmJvdHRvbSwgdG9sZXJhbmNlKTtcbiAgICAgIGNvbnN0IHJlY3RzTGluZVVwSG9yaXpvbnRhbGx5ID1cbiAgICAgICAgYWxtb3N0RXF1YWwocmVjdDEubGVmdCwgcmVjdDIubGVmdCwgdG9sZXJhbmNlKSAmJlxuICAgICAgICBhbG1vc3RFcXVhbChyZWN0MS5yaWdodCwgcmVjdDIucmlnaHQsIHRvbGVyYW5jZSk7XG4gICAgICBjb25zdCBob3Jpem9udGFsQWxsb3dlZCA9ICFkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzO1xuICAgICAgY29uc3QgYWxpZ25lZCA9XG4gICAgICAgIChyZWN0c0xpbmVVcEhvcml6b250YWxseSAmJiBob3Jpem9udGFsQWxsb3dlZCkgfHxcbiAgICAgICAgKHJlY3RzTGluZVVwVmVydGljYWxseSAmJiAhcmVjdHNMaW5lVXBIb3Jpem9udGFsbHkpO1xuICAgICAgY29uc3QgY2FuTWVyZ2UgPSBhbGlnbmVkICYmIHJlY3RzVG91Y2hPck92ZXJsYXAocmVjdDEsIHJlY3QyLCB0b2xlcmFuY2UpO1xuICAgICAgaWYgKGNhbk1lcmdlKSB7XG4gICAgICAgIGxvZyhcbiAgICAgICAgICBgQ0xJRU5UIFJFQ1Q6IG1lcmdpbmcgdHdvIGludG8gb25lLCBWRVJUSUNBTDogJHtyZWN0c0xpbmVVcFZlcnRpY2FsbHl9IEhPUklaT05UQUw6ICR7cmVjdHNMaW5lVXBIb3Jpem9udGFsbHl9ICgke2RvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHN9KWBcbiAgICAgICAgKTtcbiAgICAgICAgY29uc3QgbmV3UmVjdHMgPSByZWN0cy5maWx0ZXIoKHJlY3QpID0+IHtcbiAgICAgICAgICByZXR1cm4gcmVjdCAhPT0gcmVjdDEgJiYgcmVjdCAhPT0gcmVjdDI7XG4gICAgICAgIH0pO1xuICAgICAgICBjb25zdCByZXBsYWNlbWVudENsaWVudFJlY3QgPSBnZXRCb3VuZGluZ1JlY3QocmVjdDEsIHJlY3QyKTtcbiAgICAgICAgbmV3UmVjdHMucHVzaChyZXBsYWNlbWVudENsaWVudFJlY3QpO1xuICAgICAgICByZXR1cm4gbWVyZ2VUb3VjaGluZ1JlY3RzKFxuICAgICAgICAgIG5ld1JlY3RzLFxuICAgICAgICAgIHRvbGVyYW5jZSxcbiAgICAgICAgICBkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzXG4gICAgICAgICk7XG4gICAgICB9XG4gICAgfVxuICB9XG4gIHJldHVybiByZWN0cztcbn1cblxuZnVuY3Rpb24gZ2V0Qm91bmRpbmdSZWN0KHJlY3QxLCByZWN0Mikge1xuICBjb25zdCBsZWZ0ID0gTWF0aC5taW4ocmVjdDEubGVmdCwgcmVjdDIubGVmdCk7XG4gIGNvbnN0IHJpZ2h0ID0gTWF0aC5tYXgocmVjdDEucmlnaHQsIHJlY3QyLnJpZ2h0KTtcbiAgY29uc3QgdG9wID0gTWF0aC5taW4ocmVjdDEudG9wLCByZWN0Mi50b3ApO1xuICBjb25zdCBib3R0b20gPSBNYXRoLm1heChyZWN0MS5ib3R0b20sIHJlY3QyLmJvdHRvbSk7XG4gIHJldHVybiB7XG4gICAgYm90dG9tLFxuICAgIGhlaWdodDogYm90dG9tIC0gdG9wLFxuICAgIGxlZnQsXG4gICAgcmlnaHQsXG4gICAgdG9wLFxuICAgIHdpZHRoOiByaWdodCAtIGxlZnQsXG4gIH07XG59XG5cbmZ1bmN0aW9uIHJlbW92ZUNvbnRhaW5lZFJlY3RzKHJlY3RzLCB0b2xlcmFuY2UpIHtcbiAgY29uc3QgcmVjdHNUb0tlZXAgPSBuZXcgU2V0KHJlY3RzKTtcbiAgZm9yIChjb25zdCByZWN0IG9mIHJlY3RzKSB7XG4gICAgY29uc3QgYmlnRW5vdWdoID0gcmVjdC53aWR0aCA+IDEgJiYgcmVjdC5oZWlnaHQgPiAxO1xuICAgIGlmICghYmlnRW5vdWdoKSB7XG4gICAgICBsb2coXCJDTElFTlQgUkVDVDogcmVtb3ZlIHRpbnlcIik7XG4gICAgICByZWN0c1RvS2VlcC5kZWxldGUocmVjdCk7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG4gICAgZm9yIChjb25zdCBwb3NzaWJseUNvbnRhaW5pbmdSZWN0IG9mIHJlY3RzKSB7XG4gICAgICBpZiAocmVjdCA9PT0gcG9zc2libHlDb250YWluaW5nUmVjdCkge1xuICAgICAgICBjb250aW51ZTtcbiAgICAgIH1cbiAgICAgIGlmICghcmVjdHNUb0tlZXAuaGFzKHBvc3NpYmx5Q29udGFpbmluZ1JlY3QpKSB7XG4gICAgICAgIGNvbnRpbnVlO1xuICAgICAgfVxuICAgICAgaWYgKHJlY3RDb250YWlucyhwb3NzaWJseUNvbnRhaW5pbmdSZWN0LCByZWN0LCB0b2xlcmFuY2UpKSB7XG4gICAgICAgIGxvZyhcIkNMSUVOVCBSRUNUOiByZW1vdmUgY29udGFpbmVkXCIpO1xuICAgICAgICByZWN0c1RvS2VlcC5kZWxldGUocmVjdCk7XG4gICAgICAgIGJyZWFrO1xuICAgICAgfVxuICAgIH1cbiAgfVxuICByZXR1cm4gQXJyYXkuZnJvbShyZWN0c1RvS2VlcCk7XG59XG5cbmZ1bmN0aW9uIHJlY3RDb250YWlucyhyZWN0MSwgcmVjdDIsIHRvbGVyYW5jZSkge1xuICByZXR1cm4gKFxuICAgIHJlY3RDb250YWluc1BvaW50KHJlY3QxLCByZWN0Mi5sZWZ0LCByZWN0Mi50b3AsIHRvbGVyYW5jZSkgJiZcbiAgICByZWN0Q29udGFpbnNQb2ludChyZWN0MSwgcmVjdDIucmlnaHQsIHJlY3QyLnRvcCwgdG9sZXJhbmNlKSAmJlxuICAgIHJlY3RDb250YWluc1BvaW50KHJlY3QxLCByZWN0Mi5sZWZ0LCByZWN0Mi5ib3R0b20sIHRvbGVyYW5jZSkgJiZcbiAgICByZWN0Q29udGFpbnNQb2ludChyZWN0MSwgcmVjdDIucmlnaHQsIHJlY3QyLmJvdHRvbSwgdG9sZXJhbmNlKVxuICApO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gcmVjdENvbnRhaW5zUG9pbnQocmVjdCwgeCwgeSwgdG9sZXJhbmNlKSB7XG4gIHJldHVybiAoXG4gICAgKHJlY3QubGVmdCA8IHggfHwgYWxtb3N0RXF1YWwocmVjdC5sZWZ0LCB4LCB0b2xlcmFuY2UpKSAmJlxuICAgIChyZWN0LnJpZ2h0ID4geCB8fCBhbG1vc3RFcXVhbChyZWN0LnJpZ2h0LCB4LCB0b2xlcmFuY2UpKSAmJlxuICAgIChyZWN0LnRvcCA8IHkgfHwgYWxtb3N0RXF1YWwocmVjdC50b3AsIHksIHRvbGVyYW5jZSkpICYmXG4gICAgKHJlY3QuYm90dG9tID4geSB8fCBhbG1vc3RFcXVhbChyZWN0LmJvdHRvbSwgeSwgdG9sZXJhbmNlKSlcbiAgKTtcbn1cblxuZnVuY3Rpb24gcmVwbGFjZU92ZXJsYXBpbmdSZWN0cyhyZWN0cykge1xuICBmb3IgKGxldCBpID0gMDsgaSA8IHJlY3RzLmxlbmd0aDsgaSsrKSB7XG4gICAgZm9yIChsZXQgaiA9IGkgKyAxOyBqIDwgcmVjdHMubGVuZ3RoOyBqKyspIHtcbiAgICAgIGNvbnN0IHJlY3QxID0gcmVjdHNbaV07XG4gICAgICBjb25zdCByZWN0MiA9IHJlY3RzW2pdO1xuICAgICAgaWYgKHJlY3QxID09PSByZWN0Mikge1xuICAgICAgICBsb2coXCJyZXBsYWNlT3ZlcmxhcGluZ1JlY3RzIHJlY3QxID09PSByZWN0MiA/PyFcIik7XG4gICAgICAgIGNvbnRpbnVlO1xuICAgICAgfVxuICAgICAgaWYgKHJlY3RzVG91Y2hPck92ZXJsYXAocmVjdDEsIHJlY3QyLCAtMSkpIHtcbiAgICAgICAgbGV0IHRvQWRkID0gW107XG4gICAgICAgIGxldCB0b1JlbW92ZTtcbiAgICAgICAgY29uc3Qgc3VidHJhY3RSZWN0czEgPSByZWN0U3VidHJhY3QocmVjdDEsIHJlY3QyKTtcbiAgICAgICAgaWYgKHN1YnRyYWN0UmVjdHMxLmxlbmd0aCA9PT0gMSkge1xuICAgICAgICAgIHRvQWRkID0gc3VidHJhY3RSZWN0czE7XG4gICAgICAgICAgdG9SZW1vdmUgPSByZWN0MTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBjb25zdCBzdWJ0cmFjdFJlY3RzMiA9IHJlY3RTdWJ0cmFjdChyZWN0MiwgcmVjdDEpO1xuICAgICAgICAgIGlmIChzdWJ0cmFjdFJlY3RzMS5sZW5ndGggPCBzdWJ0cmFjdFJlY3RzMi5sZW5ndGgpIHtcbiAgICAgICAgICAgIHRvQWRkID0gc3VidHJhY3RSZWN0czE7XG4gICAgICAgICAgICB0b1JlbW92ZSA9IHJlY3QxO1xuICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICB0b0FkZCA9IHN1YnRyYWN0UmVjdHMyO1xuICAgICAgICAgICAgdG9SZW1vdmUgPSByZWN0MjtcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgbG9nKGBDTElFTlQgUkVDVDogb3ZlcmxhcCwgY3V0IG9uZSByZWN0IGludG8gJHt0b0FkZC5sZW5ndGh9YCk7XG4gICAgICAgIGNvbnN0IG5ld1JlY3RzID0gcmVjdHMuZmlsdGVyKChyZWN0KSA9PiB7XG4gICAgICAgICAgcmV0dXJuIHJlY3QgIT09IHRvUmVtb3ZlO1xuICAgICAgICB9KTtcbiAgICAgICAgQXJyYXkucHJvdG90eXBlLnB1c2guYXBwbHkobmV3UmVjdHMsIHRvQWRkKTtcbiAgICAgICAgcmV0dXJuIHJlcGxhY2VPdmVybGFwaW5nUmVjdHMobmV3UmVjdHMpO1xuICAgICAgfVxuICAgIH1cbiAgfVxuICByZXR1cm4gcmVjdHM7XG59XG5cbmZ1bmN0aW9uIHJlY3RTdWJ0cmFjdChyZWN0MSwgcmVjdDIpIHtcbiAgY29uc3QgcmVjdEludGVyc2VjdGVkID0gcmVjdEludGVyc2VjdChyZWN0MiwgcmVjdDEpO1xuICBpZiAocmVjdEludGVyc2VjdGVkLmhlaWdodCA9PT0gMCB8fCByZWN0SW50ZXJzZWN0ZWQud2lkdGggPT09IDApIHtcbiAgICByZXR1cm4gW3JlY3QxXTtcbiAgfVxuICBjb25zdCByZWN0cyA9IFtdO1xuICB7XG4gICAgY29uc3QgcmVjdEEgPSB7XG4gICAgICBib3R0b206IHJlY3QxLmJvdHRvbSxcbiAgICAgIGhlaWdodDogMCxcbiAgICAgIGxlZnQ6IHJlY3QxLmxlZnQsXG4gICAgICByaWdodDogcmVjdEludGVyc2VjdGVkLmxlZnQsXG4gICAgICB0b3A6IHJlY3QxLnRvcCxcbiAgICAgIHdpZHRoOiAwLFxuICAgIH07XG4gICAgcmVjdEEud2lkdGggPSByZWN0QS5yaWdodCAtIHJlY3RBLmxlZnQ7XG4gICAgcmVjdEEuaGVpZ2h0ID0gcmVjdEEuYm90dG9tIC0gcmVjdEEudG9wO1xuICAgIGlmIChyZWN0QS5oZWlnaHQgIT09IDAgJiYgcmVjdEEud2lkdGggIT09IDApIHtcbiAgICAgIHJlY3RzLnB1c2gocmVjdEEpO1xuICAgIH1cbiAgfVxuICB7XG4gICAgY29uc3QgcmVjdEIgPSB7XG4gICAgICBib3R0b206IHJlY3RJbnRlcnNlY3RlZC50b3AsXG4gICAgICBoZWlnaHQ6IDAsXG4gICAgICBsZWZ0OiByZWN0SW50ZXJzZWN0ZWQubGVmdCxcbiAgICAgIHJpZ2h0OiByZWN0SW50ZXJzZWN0ZWQucmlnaHQsXG4gICAgICB0b3A6IHJlY3QxLnRvcCxcbiAgICAgIHdpZHRoOiAwLFxuICAgIH07XG4gICAgcmVjdEIud2lkdGggPSByZWN0Qi5yaWdodCAtIHJlY3RCLmxlZnQ7XG4gICAgcmVjdEIuaGVpZ2h0ID0gcmVjdEIuYm90dG9tIC0gcmVjdEIudG9wO1xuICAgIGlmIChyZWN0Qi5oZWlnaHQgIT09IDAgJiYgcmVjdEIud2lkdGggIT09IDApIHtcbiAgICAgIHJlY3RzLnB1c2gocmVjdEIpO1xuICAgIH1cbiAgfVxuICB7XG4gICAgY29uc3QgcmVjdEMgPSB7XG4gICAgICBib3R0b206IHJlY3QxLmJvdHRvbSxcbiAgICAgIGhlaWdodDogMCxcbiAgICAgIGxlZnQ6IHJlY3RJbnRlcnNlY3RlZC5sZWZ0LFxuICAgICAgcmlnaHQ6IHJlY3RJbnRlcnNlY3RlZC5yaWdodCxcbiAgICAgIHRvcDogcmVjdEludGVyc2VjdGVkLmJvdHRvbSxcbiAgICAgIHdpZHRoOiAwLFxuICAgIH07XG4gICAgcmVjdEMud2lkdGggPSByZWN0Qy5yaWdodCAtIHJlY3RDLmxlZnQ7XG4gICAgcmVjdEMuaGVpZ2h0ID0gcmVjdEMuYm90dG9tIC0gcmVjdEMudG9wO1xuICAgIGlmIChyZWN0Qy5oZWlnaHQgIT09IDAgJiYgcmVjdEMud2lkdGggIT09IDApIHtcbiAgICAgIHJlY3RzLnB1c2gocmVjdEMpO1xuICAgIH1cbiAgfVxuICB7XG4gICAgY29uc3QgcmVjdEQgPSB7XG4gICAgICBib3R0b206IHJlY3QxLmJvdHRvbSxcbiAgICAgIGhlaWdodDogMCxcbiAgICAgIGxlZnQ6IHJlY3RJbnRlcnNlY3RlZC5yaWdodCxcbiAgICAgIHJpZ2h0OiByZWN0MS5yaWdodCxcbiAgICAgIHRvcDogcmVjdDEudG9wLFxuICAgICAgd2lkdGg6IDAsXG4gICAgfTtcbiAgICByZWN0RC53aWR0aCA9IHJlY3RELnJpZ2h0IC0gcmVjdEQubGVmdDtcbiAgICByZWN0RC5oZWlnaHQgPSByZWN0RC5ib3R0b20gLSByZWN0RC50b3A7XG4gICAgaWYgKHJlY3RELmhlaWdodCAhPT0gMCAmJiByZWN0RC53aWR0aCAhPT0gMCkge1xuICAgICAgcmVjdHMucHVzaChyZWN0RCk7XG4gICAgfVxuICB9XG4gIHJldHVybiByZWN0cztcbn1cblxuZnVuY3Rpb24gcmVjdEludGVyc2VjdChyZWN0MSwgcmVjdDIpIHtcbiAgY29uc3QgbWF4TGVmdCA9IE1hdGgubWF4KHJlY3QxLmxlZnQsIHJlY3QyLmxlZnQpO1xuICBjb25zdCBtaW5SaWdodCA9IE1hdGgubWluKHJlY3QxLnJpZ2h0LCByZWN0Mi5yaWdodCk7XG4gIGNvbnN0IG1heFRvcCA9IE1hdGgubWF4KHJlY3QxLnRvcCwgcmVjdDIudG9wKTtcbiAgY29uc3QgbWluQm90dG9tID0gTWF0aC5taW4ocmVjdDEuYm90dG9tLCByZWN0Mi5ib3R0b20pO1xuICByZXR1cm4ge1xuICAgIGJvdHRvbTogbWluQm90dG9tLFxuICAgIGhlaWdodDogTWF0aC5tYXgoMCwgbWluQm90dG9tIC0gbWF4VG9wKSxcbiAgICBsZWZ0OiBtYXhMZWZ0LFxuICAgIHJpZ2h0OiBtaW5SaWdodCxcbiAgICB0b3A6IG1heFRvcCxcbiAgICB3aWR0aDogTWF0aC5tYXgoMCwgbWluUmlnaHQgLSBtYXhMZWZ0KSxcbiAgfTtcbn1cblxuZnVuY3Rpb24gcmVjdHNUb3VjaE9yT3ZlcmxhcChyZWN0MSwgcmVjdDIsIHRvbGVyYW5jZSkge1xuICByZXR1cm4gKFxuICAgIChyZWN0MS5sZWZ0IDwgcmVjdDIucmlnaHQgfHxcbiAgICAgICh0b2xlcmFuY2UgPj0gMCAmJiBhbG1vc3RFcXVhbChyZWN0MS5sZWZ0LCByZWN0Mi5yaWdodCwgdG9sZXJhbmNlKSkpICYmXG4gICAgKHJlY3QyLmxlZnQgPCByZWN0MS5yaWdodCB8fFxuICAgICAgKHRvbGVyYW5jZSA+PSAwICYmIGFsbW9zdEVxdWFsKHJlY3QyLmxlZnQsIHJlY3QxLnJpZ2h0LCB0b2xlcmFuY2UpKSkgJiZcbiAgICAocmVjdDEudG9wIDwgcmVjdDIuYm90dG9tIHx8XG4gICAgICAodG9sZXJhbmNlID49IDAgJiYgYWxtb3N0RXF1YWwocmVjdDEudG9wLCByZWN0Mi5ib3R0b20sIHRvbGVyYW5jZSkpKSAmJlxuICAgIChyZWN0Mi50b3AgPCByZWN0MS5ib3R0b20gfHxcbiAgICAgICh0b2xlcmFuY2UgPj0gMCAmJiBhbG1vc3RFcXVhbChyZWN0Mi50b3AsIHJlY3QxLmJvdHRvbSwgdG9sZXJhbmNlKSkpXG4gICk7XG59XG5cbmZ1bmN0aW9uIGFsbW9zdEVxdWFsKGEsIGIsIHRvbGVyYW5jZSkge1xuICByZXR1cm4gTWF0aC5hYnMoYSAtIGIpIDw9IHRvbGVyYW5jZTtcbn1cblxuZnVuY3Rpb24gbG9nKCkge1xuICBpZiAoZGVidWcpIHtcbiAgICBsb2dOYXRpdmUuYXBwbHkobnVsbCwgYXJndW1lbnRzKTtcbiAgfVxufVxuIiwiLy9cbi8vICBDb3B5cmlnaHQgMjAyMSBSZWFkaXVtIEZvdW5kYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuXG4vLyAgVXNlIG9mIHRoaXMgc291cmNlIGNvZGUgaXMgZ292ZXJuZWQgYnkgdGhlIEJTRC1zdHlsZSBsaWNlbnNlXG4vLyAgYXZhaWxhYmxlIGluIHRoZSB0b3AtbGV2ZWwgTElDRU5TRSBmaWxlIG9mIHRoZSBwcm9qZWN0LlxuLy9cblxuaW1wb3J0IHtcbiAgZ2V0Q2xpZW50UmVjdHNOb092ZXJsYXAsXG4gIHJlY3RDb250YWluc1BvaW50LFxuICB0b05hdGl2ZVJlY3QsXG59IGZyb20gXCIuL3JlY3RcIjtcbmltcG9ydCB7IGxvZywgbG9nRXJyb3IsIHJhbmdlRnJvbUxvY2F0b3IgfSBmcm9tIFwiLi91dGlsc1wiO1xuXG5sZXQgc3R5bGVzID0gbmV3IE1hcCgpO1xubGV0IGdyb3VwcyA9IG5ldyBNYXAoKTtcbnZhciBsYXN0R3JvdXBJZCA9IDA7XG5cbi8qKlxuICogUmVnaXN0ZXJzIGEgbGlzdCBvZiBhZGRpdGlvbmFsIHN1cHBvcnRlZCBEZWNvcmF0aW9uIFRlbXBsYXRlcy5cbiAqXG4gKiBFYWNoIHRlbXBsYXRlIG9iamVjdCBpcyBpbmRleGVkIGJ5IHRoZSBzdHlsZSBJRC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlZ2lzdGVyVGVtcGxhdGVzKG5ld1N0eWxlcykge1xuICB2YXIgc3R5bGVzaGVldCA9IFwiXCI7XG5cbiAgZm9yIChjb25zdCBbaWQsIHN0eWxlXSBvZiBPYmplY3QuZW50cmllcyhuZXdTdHlsZXMpKSB7XG4gICAgc3R5bGVzLnNldChpZCwgc3R5bGUpO1xuICAgIGlmIChzdHlsZS5zdHlsZXNoZWV0KSB7XG4gICAgICBzdHlsZXNoZWV0ICs9IHN0eWxlLnN0eWxlc2hlZXQgKyBcIlxcblwiO1xuICAgIH1cbiAgfVxuXG4gIGlmIChzdHlsZXNoZWV0KSB7XG4gICAgbGV0IHN0eWxlRWxlbWVudCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoXCJzdHlsZVwiKTtcbiAgICBzdHlsZUVsZW1lbnQuaW5uZXJIVE1MID0gc3R5bGVzaGVldDtcbiAgICBkb2N1bWVudC5nZXRFbGVtZW50c0J5VGFnTmFtZShcImhlYWRcIilbMF0uYXBwZW5kQ2hpbGQoc3R5bGVFbGVtZW50KTtcbiAgfVxufVxuXG4vKipcbiAqIFJldHVybnMgYW4gaW5zdGFuY2Ugb2YgRGVjb3JhdGlvbkdyb3VwIGZvciB0aGUgZ2l2ZW4gZ3JvdXAgbmFtZS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGdldERlY29yYXRpb25zKGdyb3VwTmFtZSkge1xuICB2YXIgZ3JvdXAgPSBncm91cHMuZ2V0KGdyb3VwTmFtZSk7XG4gIGlmICghZ3JvdXApIHtcbiAgICBsZXQgaWQgPSBcInIyLWRlY29yYXRpb24tXCIgKyBsYXN0R3JvdXBJZCsrO1xuICAgIGdyb3VwID0gRGVjb3JhdGlvbkdyb3VwKGlkLCBncm91cE5hbWUpO1xuICAgIGdyb3Vwcy5zZXQoZ3JvdXBOYW1lLCBncm91cCk7XG4gIH1cbiAgcmV0dXJuIGdyb3VwO1xufVxuXG4vKipcbiAqIEhhbmRsZXMgY2xpY2sgZXZlbnRzIG9uIGEgRGVjb3JhdGlvbi5cbiAqIFJldHVybnMgd2hldGhlciBhIGRlY29yYXRpb24gbWF0Y2hlZCB0aGlzIGV2ZW50LlxuICovXG5leHBvcnQgZnVuY3Rpb24gaGFuZGxlRGVjb3JhdGlvbkNsaWNrRXZlbnQoZXZlbnQsIGNsaWNrRXZlbnQpIHtcbiAgaWYgKGdyb3Vwcy5zaXplID09PSAwKSB7XG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG5cbiAgZnVuY3Rpb24gZmluZFRhcmdldCgpIHtcbiAgICBmb3IgKGNvbnN0IFtncm91cCwgZ3JvdXBDb250ZW50XSBvZiBncm91cHMpIHtcbiAgICAgIGZvciAoY29uc3QgaXRlbSBvZiBncm91cENvbnRlbnQuaXRlbXMucmV2ZXJzZSgpKSB7XG4gICAgICAgIGlmICghaXRlbS5jbGlja2FibGVFbGVtZW50cykge1xuICAgICAgICAgIGNvbnRpbnVlO1xuICAgICAgICB9XG4gICAgICAgIGZvciAoY29uc3QgZWxlbWVudCBvZiBpdGVtLmNsaWNrYWJsZUVsZW1lbnRzKSB7XG4gICAgICAgICAgbGV0IHJlY3QgPSBlbGVtZW50LmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpLnRvSlNPTigpO1xuICAgICAgICAgIGlmIChyZWN0Q29udGFpbnNQb2ludChyZWN0LCBldmVudC5jbGllbnRYLCBldmVudC5jbGllbnRZLCAxKSkge1xuICAgICAgICAgICAgcmV0dXJuIHsgZ3JvdXAsIGl0ZW0sIGVsZW1lbnQsIHJlY3QgfTtcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICBsZXQgdGFyZ2V0ID0gZmluZFRhcmdldCgpO1xuICBpZiAoIXRhcmdldCkge1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuXG4gIHJldHVybiBBbmRyb2lkLm9uRGVjb3JhdGlvbkFjdGl2YXRlZChcbiAgICBKU09OLnN0cmluZ2lmeSh7XG4gICAgICBpZDogdGFyZ2V0Lml0ZW0uZGVjb3JhdGlvbi5pZCxcbiAgICAgIGdyb3VwOiB0YXJnZXQuZ3JvdXAsXG4gICAgICByZWN0OiB0b05hdGl2ZVJlY3QodGFyZ2V0Lml0ZW0ucmFuZ2UuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCkpLFxuICAgICAgY2xpY2s6IGNsaWNrRXZlbnQsXG4gICAgfSlcbiAgKTtcbn1cblxuLyoqXG4gKiBDcmVhdGVzIGEgRGVjb3JhdGlvbkdyb3VwIG9iamVjdCBmcm9tIGEgdW5pcXVlIEhUTUwgSUQgYW5kIGl0cyBuYW1lLlxuICovXG5leHBvcnQgZnVuY3Rpb24gRGVjb3JhdGlvbkdyb3VwKGdyb3VwSWQsIGdyb3VwTmFtZSkge1xuICB2YXIgaXRlbXMgPSBbXTtcbiAgdmFyIGxhc3RJdGVtSWQgPSAwO1xuICB2YXIgY29udGFpbmVyID0gbnVsbDtcblxuICAvKipcbiAgICogQWRkcyBhIG5ldyBkZWNvcmF0aW9uIHRvIHRoZSBncm91cC5cbiAgICovXG4gIGZ1bmN0aW9uIGFkZChkZWNvcmF0aW9uKSB7XG4gICAgbGV0IGlkID0gZ3JvdXBJZCArIFwiLVwiICsgbGFzdEl0ZW1JZCsrO1xuXG4gICAgbGV0IHJhbmdlID0gcmFuZ2VGcm9tTG9jYXRvcihkZWNvcmF0aW9uLmxvY2F0b3IpO1xuICAgIGlmICghcmFuZ2UpIHtcbiAgICAgIGxvZyhcIkNhbid0IGxvY2F0ZSBET00gcmFuZ2UgZm9yIGRlY29yYXRpb25cIiwgZGVjb3JhdGlvbik7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgbGV0IGl0ZW0gPSB7IGlkLCBkZWNvcmF0aW9uLCByYW5nZSB9O1xuICAgIGl0ZW1zLnB1c2goaXRlbSk7XG4gICAgbGF5b3V0KGl0ZW0pO1xuICB9XG5cbiAgLyoqXG4gICAqIFJlbW92ZXMgdGhlIGRlY29yYXRpb24gd2l0aCBnaXZlbiBJRCBmcm9tIHRoZSBncm91cC5cbiAgICovXG4gIGZ1bmN0aW9uIHJlbW92ZShkZWNvcmF0aW9uSWQpIHtcbiAgICBsZXQgaW5kZXggPSBpdGVtcy5maW5kSW5kZXgoKGkpID0+IGkuZGVjb3JhdGlvbi5pZCA9PT0gZGVjb3JhdGlvbklkKTtcbiAgICBpZiAoaW5kZXggPT09IC0xKSB7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgbGV0IGl0ZW0gPSBpdGVtc1tpbmRleF07XG4gICAgaXRlbXMuc3BsaWNlKGluZGV4LCAxKTtcbiAgICBpdGVtLmNsaWNrYWJsZUVsZW1lbnRzID0gbnVsbDtcbiAgICBpZiAoaXRlbS5jb250YWluZXIpIHtcbiAgICAgIGl0ZW0uY29udGFpbmVyLnJlbW92ZSgpO1xuICAgICAgaXRlbS5jb250YWluZXIgPSBudWxsO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBOb3RpZmllcyB0aGF0IHRoZSBnaXZlbiBkZWNvcmF0aW9uIHdhcyBtb2RpZmllZCBhbmQgbmVlZHMgdG8gYmUgdXBkYXRlZC5cbiAgICovXG4gIGZ1bmN0aW9uIHVwZGF0ZShkZWNvcmF0aW9uKSB7XG4gICAgcmVtb3ZlKGRlY29yYXRpb24uaWQpO1xuICAgIGFkZChkZWNvcmF0aW9uKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBSZW1vdmVzIGFsbCBkZWNvcmF0aW9ucyBmcm9tIHRoaXMgZ3JvdXAuXG4gICAqL1xuICBmdW5jdGlvbiBjbGVhcigpIHtcbiAgICBjbGVhckNvbnRhaW5lcigpO1xuICAgIGl0ZW1zLmxlbmd0aCA9IDA7XG4gIH1cblxuICAvKipcbiAgICogUmVjcmVhdGVzIHRoZSBkZWNvcmF0aW9uIGVsZW1lbnRzLlxuICAgKlxuICAgKiBUbyBiZSBjYWxsZWQgYWZ0ZXIgcmVmbG93aW5nIHRoZSByZXNvdXJjZSwgZm9yIGV4YW1wbGUuXG4gICAqL1xuICBmdW5jdGlvbiByZXF1ZXN0TGF5b3V0KCkge1xuICAgIGNsZWFyQ29udGFpbmVyKCk7XG4gICAgaXRlbXMuZm9yRWFjaCgoaXRlbSkgPT4gbGF5b3V0KGl0ZW0pKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBMYXlvdXRzIGEgc2luZ2xlIERlY29yYXRpb24gaXRlbS5cbiAgICovXG4gIGZ1bmN0aW9uIGxheW91dChpdGVtKSB7XG4gICAgbGV0IGdyb3VwQ29udGFpbmVyID0gcmVxdWlyZUNvbnRhaW5lcigpO1xuXG4gICAgbGV0IHN0eWxlID0gc3R5bGVzLmdldChpdGVtLmRlY29yYXRpb24uc3R5bGUpO1xuICAgIGlmICghc3R5bGUpIHtcbiAgICAgIGxvZ0Vycm9yKGBVbmtub3duIGRlY29yYXRpb24gc3R5bGU6ICR7aXRlbS5kZWNvcmF0aW9uLnN0eWxlfWApO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGxldCBpdGVtQ29udGFpbmVyID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudChcImRpdlwiKTtcbiAgICBpdGVtQ29udGFpbmVyLnNldEF0dHJpYnV0ZShcImlkXCIsIGl0ZW0uaWQpO1xuICAgIGl0ZW1Db250YWluZXIuc2V0QXR0cmlidXRlKFwiZGF0YS1zdHlsZVwiLCBpdGVtLmRlY29yYXRpb24uc3R5bGUpO1xuICAgIGl0ZW1Db250YWluZXIuc3R5bGUuc2V0UHJvcGVydHkoXCJwb2ludGVyLWV2ZW50c1wiLCBcIm5vbmVcIik7XG5cbiAgICBsZXQgdmlld3BvcnRXaWR0aCA9IHdpbmRvdy5pbm5lcldpZHRoO1xuICAgIGxldCBjb2x1bW5Db3VudCA9IHBhcnNlSW50KFxuICAgICAgZ2V0Q29tcHV0ZWRTdHlsZShkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQpLmdldFByb3BlcnR5VmFsdWUoXG4gICAgICAgIFwiY29sdW1uLWNvdW50XCJcbiAgICAgIClcbiAgICApO1xuICAgIGxldCBwYWdlV2lkdGggPSB2aWV3cG9ydFdpZHRoIC8gKGNvbHVtbkNvdW50IHx8IDEpO1xuICAgIGxldCBzY3JvbGxpbmdFbGVtZW50ID0gZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudDtcbiAgICBsZXQgeE9mZnNldCA9IHNjcm9sbGluZ0VsZW1lbnQuc2Nyb2xsTGVmdDtcbiAgICBsZXQgeU9mZnNldCA9IHNjcm9sbGluZ0VsZW1lbnQuc2Nyb2xsVG9wO1xuXG4gICAgZnVuY3Rpb24gcG9zaXRpb25FbGVtZW50KGVsZW1lbnQsIHJlY3QsIGJvdW5kaW5nUmVjdCkge1xuICAgICAgZWxlbWVudC5zdHlsZS5wb3NpdGlvbiA9IFwiYWJzb2x1dGVcIjtcblxuICAgICAgaWYgKHN0eWxlLndpZHRoID09PSBcIndyYXBcIikge1xuICAgICAgICBlbGVtZW50LnN0eWxlLndpZHRoID0gYCR7cmVjdC53aWR0aH1weGA7XG4gICAgICAgIGVsZW1lbnQuc3R5bGUuaGVpZ2h0ID0gYCR7cmVjdC5oZWlnaHR9cHhgO1xuICAgICAgICBlbGVtZW50LnN0eWxlLmxlZnQgPSBgJHtyZWN0LmxlZnQgKyB4T2Zmc2V0fXB4YDtcbiAgICAgICAgZWxlbWVudC5zdHlsZS50b3AgPSBgJHtyZWN0LnRvcCArIHlPZmZzZXR9cHhgO1xuICAgICAgfSBlbHNlIGlmIChzdHlsZS53aWR0aCA9PT0gXCJ2aWV3cG9ydFwiKSB7XG4gICAgICAgIGVsZW1lbnQuc3R5bGUud2lkdGggPSBgJHt2aWV3cG9ydFdpZHRofXB4YDtcbiAgICAgICAgZWxlbWVudC5zdHlsZS5oZWlnaHQgPSBgJHtyZWN0LmhlaWdodH1weGA7XG4gICAgICAgIGxldCBsZWZ0ID0gTWF0aC5mbG9vcihyZWN0LmxlZnQgLyB2aWV3cG9ydFdpZHRoKSAqIHZpZXdwb3J0V2lkdGg7XG4gICAgICAgIGVsZW1lbnQuc3R5bGUubGVmdCA9IGAke2xlZnQgKyB4T2Zmc2V0fXB4YDtcbiAgICAgICAgZWxlbWVudC5zdHlsZS50b3AgPSBgJHtyZWN0LnRvcCArIHlPZmZzZXR9cHhgO1xuICAgICAgfSBlbHNlIGlmIChzdHlsZS53aWR0aCA9PT0gXCJib3VuZHNcIikge1xuICAgICAgICBlbGVtZW50LnN0eWxlLndpZHRoID0gYCR7Ym91bmRpbmdSZWN0LndpZHRofXB4YDtcbiAgICAgICAgZWxlbWVudC5zdHlsZS5oZWlnaHQgPSBgJHtyZWN0LmhlaWdodH1weGA7XG4gICAgICAgIGVsZW1lbnQuc3R5bGUubGVmdCA9IGAke2JvdW5kaW5nUmVjdC5sZWZ0ICsgeE9mZnNldH1weGA7XG4gICAgICAgIGVsZW1lbnQuc3R5bGUudG9wID0gYCR7cmVjdC50b3AgKyB5T2Zmc2V0fXB4YDtcbiAgICAgIH0gZWxzZSBpZiAoc3R5bGUud2lkdGggPT09IFwicGFnZVwiKSB7XG4gICAgICAgIGVsZW1lbnQuc3R5bGUud2lkdGggPSBgJHtwYWdlV2lkdGh9cHhgO1xuICAgICAgICBlbGVtZW50LnN0eWxlLmhlaWdodCA9IGAke3JlY3QuaGVpZ2h0fXB4YDtcbiAgICAgICAgbGV0IGxlZnQgPSBNYXRoLmZsb29yKHJlY3QubGVmdCAvIHBhZ2VXaWR0aCkgKiBwYWdlV2lkdGg7XG4gICAgICAgIGVsZW1lbnQuc3R5bGUubGVmdCA9IGAke2xlZnQgKyB4T2Zmc2V0fXB4YDtcbiAgICAgICAgZWxlbWVudC5zdHlsZS50b3AgPSBgJHtyZWN0LnRvcCArIHlPZmZzZXR9cHhgO1xuICAgICAgfVxuICAgIH1cblxuICAgIGxldCBib3VuZGluZ1JlY3QgPSBpdGVtLnJhbmdlLmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpO1xuXG4gICAgbGV0IGVsZW1lbnRUZW1wbGF0ZTtcbiAgICB0cnkge1xuICAgICAgbGV0IHRlbXBsYXRlID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudChcInRlbXBsYXRlXCIpO1xuICAgICAgdGVtcGxhdGUuaW5uZXJIVE1MID0gaXRlbS5kZWNvcmF0aW9uLmVsZW1lbnQudHJpbSgpO1xuICAgICAgZWxlbWVudFRlbXBsYXRlID0gdGVtcGxhdGUuY29udGVudC5maXJzdEVsZW1lbnRDaGlsZDtcbiAgICB9IGNhdGNoIChlcnJvcikge1xuICAgICAgbG9nRXJyb3IoXG4gICAgICAgIGBJbnZhbGlkIGRlY29yYXRpb24gZWxlbWVudCBcIiR7aXRlbS5kZWNvcmF0aW9uLmVsZW1lbnR9XCI6ICR7ZXJyb3IubWVzc2FnZX1gXG4gICAgICApO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGlmIChzdHlsZS5sYXlvdXQgPT09IFwiYm94ZXNcIikge1xuICAgICAgbGV0IGRvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHMgPSB0cnVlO1xuICAgICAgbGV0IGNsaWVudFJlY3RzID0gZ2V0Q2xpZW50UmVjdHNOb092ZXJsYXAoXG4gICAgICAgIGl0ZW0ucmFuZ2UsXG4gICAgICAgIGRvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHNcbiAgICAgICk7XG5cbiAgICAgIGNsaWVudFJlY3RzID0gY2xpZW50UmVjdHMuc29ydCgocjEsIHIyKSA9PiB7XG4gICAgICAgIGlmIChyMS50b3AgPCByMi50b3ApIHtcbiAgICAgICAgICByZXR1cm4gLTE7XG4gICAgICAgIH0gZWxzZSBpZiAocjEudG9wID4gcjIudG9wKSB7XG4gICAgICAgICAgcmV0dXJuIDE7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgcmV0dXJuIDA7XG4gICAgICAgIH1cbiAgICAgIH0pO1xuXG4gICAgICBmb3IgKGxldCBjbGllbnRSZWN0IG9mIGNsaWVudFJlY3RzKSB7XG4gICAgICAgIGNvbnN0IGxpbmUgPSBlbGVtZW50VGVtcGxhdGUuY2xvbmVOb2RlKHRydWUpO1xuICAgICAgICBsaW5lLnN0eWxlLnNldFByb3BlcnR5KFwicG9pbnRlci1ldmVudHNcIiwgXCJub25lXCIpO1xuICAgICAgICBwb3NpdGlvbkVsZW1lbnQobGluZSwgY2xpZW50UmVjdCwgYm91bmRpbmdSZWN0KTtcbiAgICAgICAgaXRlbUNvbnRhaW5lci5hcHBlbmQobGluZSk7XG4gICAgICB9XG4gICAgfSBlbHNlIGlmIChzdHlsZS5sYXlvdXQgPT09IFwiYm91bmRzXCIpIHtcbiAgICAgIGNvbnN0IGJvdW5kcyA9IGVsZW1lbnRUZW1wbGF0ZS5jbG9uZU5vZGUodHJ1ZSk7XG4gICAgICBib3VuZHMuc3R5bGUuc2V0UHJvcGVydHkoXCJwb2ludGVyLWV2ZW50c1wiLCBcIm5vbmVcIik7XG4gICAgICBwb3NpdGlvbkVsZW1lbnQoYm91bmRzLCBib3VuZGluZ1JlY3QsIGJvdW5kaW5nUmVjdCk7XG5cbiAgICAgIGl0ZW1Db250YWluZXIuYXBwZW5kKGJvdW5kcyk7XG4gICAgfVxuXG4gICAgZ3JvdXBDb250YWluZXIuYXBwZW5kKGl0ZW1Db250YWluZXIpO1xuICAgIGl0ZW0uY29udGFpbmVyID0gaXRlbUNvbnRhaW5lcjtcbiAgICBpdGVtLmNsaWNrYWJsZUVsZW1lbnRzID0gQXJyYXkuZnJvbShcbiAgICAgIGl0ZW1Db250YWluZXIucXVlcnlTZWxlY3RvckFsbChcIltkYXRhLWFjdGl2YWJsZT0nMSddXCIpXG4gICAgKTtcbiAgICBpZiAoaXRlbS5jbGlja2FibGVFbGVtZW50cy5sZW5ndGggPT09IDApIHtcbiAgICAgIGl0ZW0uY2xpY2thYmxlRWxlbWVudHMgPSBBcnJheS5mcm9tKGl0ZW1Db250YWluZXIuY2hpbGRyZW4pO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBSZXR1cm5zIHRoZSBncm91cCBjb250YWluZXIgZWxlbWVudCwgYWZ0ZXIgbWFraW5nIHN1cmUgaXQgZXhpc3RzLlxuICAgKi9cbiAgZnVuY3Rpb24gcmVxdWlyZUNvbnRhaW5lcigpIHtcbiAgICBpZiAoIWNvbnRhaW5lcikge1xuICAgICAgY29udGFpbmVyID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudChcImRpdlwiKTtcbiAgICAgIGNvbnRhaW5lci5zZXRBdHRyaWJ1dGUoXCJpZFwiLCBncm91cElkKTtcbiAgICAgIGNvbnRhaW5lci5zZXRBdHRyaWJ1dGUoXCJkYXRhLWdyb3VwXCIsIGdyb3VwTmFtZSk7XG4gICAgICBjb250YWluZXIuc3R5bGUuc2V0UHJvcGVydHkoXCJwb2ludGVyLWV2ZW50c1wiLCBcIm5vbmVcIik7XG4gICAgICBkb2N1bWVudC5ib2R5LmFwcGVuZChjb250YWluZXIpO1xuICAgIH1cbiAgICByZXR1cm4gY29udGFpbmVyO1xuICB9XG5cbiAgLyoqXG4gICAqIFJlbW92ZXMgdGhlIGdyb3VwIGNvbnRhaW5lci5cbiAgICovXG4gIGZ1bmN0aW9uIGNsZWFyQ29udGFpbmVyKCkge1xuICAgIGlmIChjb250YWluZXIpIHtcbiAgICAgIGNvbnRhaW5lci5yZW1vdmUoKTtcbiAgICAgIGNvbnRhaW5lciA9IG51bGw7XG4gICAgfVxuICB9XG5cbiAgcmV0dXJuIHsgYWRkLCByZW1vdmUsIHVwZGF0ZSwgY2xlYXIsIGl0ZW1zLCByZXF1ZXN0TGF5b3V0IH07XG59XG5cbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKFxuICBcImxvYWRcIixcbiAgZnVuY3Rpb24gKCkge1xuICAgIC8vIFdpbGwgcmVsYXlvdXQgYWxsIHRoZSBkZWNvcmF0aW9ucyB3aGVuIHRoZSBkb2N1bWVudCBib2R5IGlzIHJlc2l6ZWQuXG4gICAgY29uc3QgYm9keSA9IGRvY3VtZW50LmJvZHk7XG4gICAgdmFyIGxhc3RTaXplID0geyB3aWR0aDogMCwgaGVpZ2h0OiAwIH07XG4gICAgY29uc3Qgb2JzZXJ2ZXIgPSBuZXcgUmVzaXplT2JzZXJ2ZXIoKCkgPT4ge1xuICAgICAgaWYgKFxuICAgICAgICBsYXN0U2l6ZS53aWR0aCA9PT0gYm9keS5jbGllbnRXaWR0aCAmJlxuICAgICAgICBsYXN0U2l6ZS5oZWlnaHQgPT09IGJvZHkuY2xpZW50SGVpZ2h0XG4gICAgICApIHtcbiAgICAgICAgcmV0dXJuO1xuICAgICAgfVxuICAgICAgbGFzdFNpemUgPSB7XG4gICAgICAgIHdpZHRoOiBib2R5LmNsaWVudFdpZHRoLFxuICAgICAgICBoZWlnaHQ6IGJvZHkuY2xpZW50SGVpZ2h0LFxuICAgICAgfTtcblxuICAgICAgZ3JvdXBzLmZvckVhY2goZnVuY3Rpb24gKGdyb3VwKSB7XG4gICAgICAgIGdyb3VwLnJlcXVlc3RMYXlvdXQoKTtcbiAgICAgIH0pO1xuICAgIH0pO1xuICAgIG9ic2VydmVyLm9ic2VydmUoYm9keSk7XG4gIH0sXG4gIGZhbHNlXG4pO1xuIiwiLypcbiAqIENvcHlyaWdodCAyMDIxIFJlYWRpdW0gRm91bmRhdGlvbi4gQWxsIHJpZ2h0cyByZXNlcnZlZC5cbiAqIFVzZSBvZiB0aGlzIHNvdXJjZSBjb2RlIGlzIGdvdmVybmVkIGJ5IHRoZSBCU0Qtc3R5bGUgbGljZW5zZVxuICogYXZhaWxhYmxlIGluIHRoZSB0b3AtbGV2ZWwgTElDRU5TRSBmaWxlIG9mIHRoZSBwcm9qZWN0LlxuICovXG5cbmltcG9ydCB7IGhhbmRsZURlY29yYXRpb25DbGlja0V2ZW50IH0gZnJvbSBcIi4vZGVjb3JhdG9yXCI7XG5cbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKFwiRE9NQ29udGVudExvYWRlZFwiLCBmdW5jdGlvbiAoKSB7XG4gIGRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoXCJjbGlja1wiLCBvbkNsaWNrLCBmYWxzZSk7XG4gIGJpbmREcmFnR2VzdHVyZShkb2N1bWVudCk7XG59KTtcblxuZnVuY3Rpb24gb25DbGljayhldmVudCkge1xuICBpZiAoIXdpbmRvdy5nZXRTZWxlY3Rpb24oKS5pc0NvbGxhcHNlZCkge1xuICAgIC8vIFRoZXJlJ3MgYW4gb24tZ29pbmcgc2VsZWN0aW9uLCB0aGUgdGFwIHdpbGwgZGlzbWlzcyBpdCBzbyB3ZSBkb24ndCBmb3J3YXJkIGl0LlxuICAgIHJldHVybjtcbiAgfVxuXG4gIHZhciBwaXhlbFJhdGlvID0gd2luZG93LmRldmljZVBpeGVsUmF0aW87XG4gIGxldCBjbGlja0V2ZW50ID0ge1xuICAgIGRlZmF1bHRQcmV2ZW50ZWQ6IGV2ZW50LmRlZmF1bHRQcmV2ZW50ZWQsXG4gICAgeDogZXZlbnQuY2xpZW50WCAqIHBpeGVsUmF0aW8sXG4gICAgeTogZXZlbnQuY2xpZW50WSAqIHBpeGVsUmF0aW8sXG4gICAgdGFyZ2V0RWxlbWVudDogZXZlbnQudGFyZ2V0Lm91dGVySFRNTCxcbiAgICBpbnRlcmFjdGl2ZUVsZW1lbnQ6IG5lYXJlc3RJbnRlcmFjdGl2ZUVsZW1lbnQoZXZlbnQudGFyZ2V0KSxcbiAgfTtcblxuICBpZiAoaGFuZGxlRGVjb3JhdGlvbkNsaWNrRXZlbnQoZXZlbnQsIGNsaWNrRXZlbnQpKSB7XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgLy8gU2VuZCB0aGUgdGFwIGRhdGEgb3ZlciB0aGUgSlMgYnJpZGdlIGV2ZW4gaWYgaXQncyBiZWVuIGhhbmRsZWQgd2l0aGluIHRoZSB3ZWIgdmlldywgc28gdGhhdFxuICAvLyBpdCBjYW4gYmUgcHJlc2VydmVkIGFuZCB1c2VkIGJ5IHRoZSB0b29sa2l0IGlmIG5lZWRlZC5cbiAgdmFyIHNob3VsZFByZXZlbnREZWZhdWx0ID0gQW5kcm9pZC5vblRhcChKU09OLnN0cmluZ2lmeShjbGlja0V2ZW50KSk7XG5cbiAgaWYgKHNob3VsZFByZXZlbnREZWZhdWx0KSB7XG4gICAgZXZlbnQuc3RvcFByb3BhZ2F0aW9uKCk7XG4gICAgZXZlbnQucHJldmVudERlZmF1bHQoKTtcbiAgfVxufVxuXG5mdW5jdGlvbiBiaW5kRHJhZ0dlc3R1cmUoZWxlbWVudCkge1xuICAvLyBwYXNzaXZlOiBmYWxzZSBpcyBuZWNlc3NhcnkgdG8gYmUgYWJsZSB0byBwcmV2ZW50IHRoZSBkZWZhdWx0IGJlaGF2aW9yLlxuICBlbGVtZW50LmFkZEV2ZW50TGlzdGVuZXIoXCJ0b3VjaHN0YXJ0XCIsIG9uU3RhcnQsIHsgcGFzc2l2ZTogZmFsc2UgfSk7XG4gIGVsZW1lbnQuYWRkRXZlbnRMaXN0ZW5lcihcInRvdWNoZW5kXCIsIG9uRW5kLCB7IHBhc3NpdmU6IGZhbHNlIH0pO1xuICBlbGVtZW50LmFkZEV2ZW50TGlzdGVuZXIoXCJ0b3VjaG1vdmVcIiwgb25Nb3ZlLCB7IHBhc3NpdmU6IGZhbHNlIH0pO1xuXG4gIHZhciBzdGF0ZSA9IHVuZGVmaW5lZDtcbiAgdmFyIGlzU3RhcnRpbmdEcmFnID0gZmFsc2U7XG4gIGNvbnN0IHBpeGVsUmF0aW8gPSB3aW5kb3cuZGV2aWNlUGl4ZWxSYXRpbztcblxuICBmdW5jdGlvbiBvblN0YXJ0KGV2ZW50KSB7XG4gICAgaXNTdGFydGluZ0RyYWcgPSB0cnVlO1xuXG4gICAgY29uc3Qgc3RhcnRYID0gZXZlbnQudG91Y2hlc1swXS5jbGllbnRYICogcGl4ZWxSYXRpbztcbiAgICBjb25zdCBzdGFydFkgPSBldmVudC50b3VjaGVzWzBdLmNsaWVudFkgKiBwaXhlbFJhdGlvO1xuICAgIHN0YXRlID0ge1xuICAgICAgZGVmYXVsdFByZXZlbnRlZDogZXZlbnQuZGVmYXVsdFByZXZlbnRlZCxcbiAgICAgIHN0YXJ0WDogc3RhcnRYLFxuICAgICAgc3RhcnRZOiBzdGFydFksXG4gICAgICBjdXJyZW50WDogc3RhcnRYLFxuICAgICAgY3VycmVudFk6IHN0YXJ0WSxcbiAgICAgIG9mZnNldFg6IDAsXG4gICAgICBvZmZzZXRZOiAwLFxuICAgICAgaW50ZXJhY3RpdmVFbGVtZW50OiBuZWFyZXN0SW50ZXJhY3RpdmVFbGVtZW50KGV2ZW50LnRhcmdldCksXG4gICAgfTtcbiAgfVxuXG4gIGZ1bmN0aW9uIG9uTW92ZShldmVudCkge1xuICAgIGlmICghc3RhdGUpIHJldHVybjtcblxuICAgIHN0YXRlLmN1cnJlbnRYID0gZXZlbnQudG91Y2hlc1swXS5jbGllbnRYICogcGl4ZWxSYXRpbztcbiAgICBzdGF0ZS5jdXJyZW50WSA9IGV2ZW50LnRvdWNoZXNbMF0uY2xpZW50WSAqIHBpeGVsUmF0aW87XG4gICAgc3RhdGUub2Zmc2V0WCA9IHN0YXRlLmN1cnJlbnRYIC0gc3RhdGUuc3RhcnRYO1xuICAgIHN0YXRlLm9mZnNldFkgPSBzdGF0ZS5jdXJyZW50WSAtIHN0YXRlLnN0YXJ0WTtcblxuICAgIHZhciBzaG91bGRQcmV2ZW50RGVmYXVsdCA9IGZhbHNlO1xuICAgIC8vIFdhaXQgZm9yIGEgbW92ZW1lbnQgb2YgYXQgbGVhc3QgNiBwaXhlbHMgYmVmb3JlIHJlcG9ydGluZyBhIGRyYWcuXG4gICAgaWYgKGlzU3RhcnRpbmdEcmFnKSB7XG4gICAgICBpZiAoTWF0aC5hYnMoc3RhdGUub2Zmc2V0WCkgPj0gNiB8fCBNYXRoLmFicyhzdGF0ZS5vZmZzZXRZKSA+PSA2KSB7XG4gICAgICAgIGlzU3RhcnRpbmdEcmFnID0gZmFsc2U7XG4gICAgICAgIHNob3VsZFByZXZlbnREZWZhdWx0ID0gQW5kcm9pZC5vbkRyYWdTdGFydChKU09OLnN0cmluZ2lmeShzdGF0ZSkpO1xuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICBzaG91bGRQcmV2ZW50RGVmYXVsdCA9IEFuZHJvaWQub25EcmFnTW92ZShKU09OLnN0cmluZ2lmeShzdGF0ZSkpO1xuICAgIH1cblxuICAgIGlmIChzaG91bGRQcmV2ZW50RGVmYXVsdCkge1xuICAgICAgZXZlbnQuc3RvcFByb3BhZ2F0aW9uKCk7XG4gICAgICBldmVudC5wcmV2ZW50RGVmYXVsdCgpO1xuICAgIH1cbiAgfVxuXG4gIGZ1bmN0aW9uIG9uRW5kKGV2ZW50KSB7XG4gICAgaWYgKCFzdGF0ZSkgcmV0dXJuO1xuXG4gICAgY29uc3Qgc2hvdWxkUHJldmVudERlZmF1bHQgPSBBbmRyb2lkLm9uRHJhZ0VuZChKU09OLnN0cmluZ2lmeShzdGF0ZSkpO1xuICAgIGlmIChzaG91bGRQcmV2ZW50RGVmYXVsdCkge1xuICAgICAgZXZlbnQuc3RvcFByb3BhZ2F0aW9uKCk7XG4gICAgICBldmVudC5wcmV2ZW50RGVmYXVsdCgpO1xuICAgIH1cbiAgICBzdGF0ZSA9IHVuZGVmaW5lZDtcbiAgfVxufVxuXG4vLyBTZWUuIGh0dHBzOi8vZ2l0aHViLmNvbS9KYXlQYW5vei9hcmNoaXRlY3R1cmUvdHJlZS90b3VjaC1oYW5kbGluZy9taXNjL3RvdWNoLWhhbmRsaW5nXG5mdW5jdGlvbiBuZWFyZXN0SW50ZXJhY3RpdmVFbGVtZW50KGVsZW1lbnQpIHtcbiAgdmFyIGludGVyYWN0aXZlVGFncyA9IFtcbiAgICBcImFcIixcbiAgICBcImF1ZGlvXCIsXG4gICAgXCJidXR0b25cIixcbiAgICBcImNhbnZhc1wiLFxuICAgIFwiZGV0YWlsc1wiLFxuICAgIFwiaW5wdXRcIixcbiAgICBcImxhYmVsXCIsXG4gICAgXCJvcHRpb25cIixcbiAgICBcInNlbGVjdFwiLFxuICAgIFwic3VibWl0XCIsXG4gICAgXCJ0ZXh0YXJlYVwiLFxuICAgIFwidmlkZW9cIixcbiAgXTtcbiAgaWYgKGludGVyYWN0aXZlVGFncy5pbmRleE9mKGVsZW1lbnQubm9kZU5hbWUudG9Mb3dlckNhc2UoKSkgIT0gLTEpIHtcbiAgICByZXR1cm4gZWxlbWVudC5vdXRlckhUTUw7XG4gIH1cblxuICAvLyBDaGVja3Mgd2hldGhlciB0aGUgZWxlbWVudCBpcyBlZGl0YWJsZSBieSB0aGUgdXNlci5cbiAgaWYgKFxuICAgIGVsZW1lbnQuaGFzQXR0cmlidXRlKFwiY29udGVudGVkaXRhYmxlXCIpICYmXG4gICAgZWxlbWVudC5nZXRBdHRyaWJ1dGUoXCJjb250ZW50ZWRpdGFibGVcIikudG9Mb3dlckNhc2UoKSAhPSBcImZhbHNlXCJcbiAgKSB7XG4gICAgcmV0dXJuIGVsZW1lbnQub3V0ZXJIVE1MO1xuICB9XG5cbiAgLy8gQ2hlY2tzIHBhcmVudHMgcmVjdXJzaXZlbHkgYmVjYXVzZSB0aGUgdG91Y2ggbWlnaHQgYmUgZm9yIGV4YW1wbGUgb24gYW4gPGVtPiBpbnNpZGUgYSA8YT4uXG4gIGlmIChlbGVtZW50LnBhcmVudEVsZW1lbnQpIHtcbiAgICByZXR1cm4gbmVhcmVzdEludGVyYWN0aXZlRWxlbWVudChlbGVtZW50LnBhcmVudEVsZW1lbnQpO1xuICB9XG5cbiAgcmV0dXJuIG51bGw7XG59XG4iLCIvKiBlc2xpbnQtZGlzYWJsZSAqL1xuLy9cbi8vICBoaWdobGlnaHQuanNcbi8vICByMi1uYXZpZ2F0b3Ita290bGluXG4vL1xuLy8gIE9yZ2FuaXplZCBieSBUYWVoeXVuIEtpbSBvbiA2LzI3LzE5IGZyb20gcjItbmF2aWdhdG9yLWpzLlxuLy9cbi8vICBDb3B5cmlnaHQgMjAxOSBSZWFkaXVtIEZvdW5kYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuXG4vLyAgVXNlIG9mIHRoaXMgc291cmNlIGNvZGUgaXMgZ292ZXJuZWQgYnkgYSBCU0Qtc3R5bGUgbGljZW5zZSB3aGljaCBpcyBkZXRhaWxlZFxuLy8gIGluIHRoZSBMSUNFTlNFIGZpbGUgcHJlc2VudCBpbiB0aGUgcHJvamVjdCByZXBvc2l0b3J5IHdoZXJlIHRoaXMgc291cmNlIGNvZGUgaXMgbWFpbnRhaW5lZC5cbi8vXG5cbmNvbnN0IFJPT1RfQ0xBU1NfUkVEVUNFX01PVElPTiA9IFwicjItcmVkdWNlLW1vdGlvblwiO1xuY29uc3QgUk9PVF9DTEFTU19OT19GT09UTk9URVMgPSBcInIyLW5vLXBvcHVwLWZvb25vdGVzXCI7XG5jb25zdCBQT1BVUF9ESUFMT0dfQ0xBU1MgPSBcInIyLXBvcHVwLWRpYWxvZ1wiO1xuY29uc3QgRk9PVE5PVEVTX0NPTlRBSU5FUl9DTEFTUyA9IFwicjItZm9vdG5vdGUtY29udGFpbmVyXCI7XG5jb25zdCBGT09UTk9URVNfQ0xPU0VfQlVUVE9OX0NMQVNTID0gXCJyMi1mb290bm90ZS1jbG9zZVwiO1xuY29uc3QgRk9PVE5PVEVfRk9SQ0VfU0hPVyA9IFwicjItZm9vdG5vdGUtZm9yY2Utc2hvd1wiO1xuY29uc3QgVFRTX0lEX1BSRVZJT1VTID0gXCJyMi10dHMtcHJldmlvdXNcIjtcbmNvbnN0IFRUU19JRF9ORVhUID0gXCJyMi10dHMtbmV4dFwiO1xuY29uc3QgVFRTX0lEX1NMSURFUiA9IFwicjItdHRzLXNsaWRlclwiO1xuY29uc3QgVFRTX0lEX0FDVElWRV9XT1JEID0gXCJyMi10dHMtYWN0aXZlLXdvcmRcIjtcbmNvbnN0IFRUU19JRF9DT05UQUlORVIgPSBcInIyLXR0cy10eHRcIjtcbmNvbnN0IFRUU19JRF9JTkZPID0gXCJyMi10dHMtaW5mb1wiO1xuY29uc3QgVFRTX05BVl9CVVRUT05fQ0xBU1MgPSBcInIyLXR0cy1idXR0b25cIjtcbmNvbnN0IFRUU19JRF9TUEVBS0lOR19ET0NfRUxFTUVOVCA9IFwicjItdHRzLXNwZWFraW5nLWVsXCI7XG5jb25zdCBUVFNfQ0xBU1NfSU5KRUNURURfU1BBTiA9IFwicjItdHRzLXNwZWFraW5nLXR4dFwiO1xuY29uc3QgVFRTX0NMQVNTX0lOSkVDVEVEX1NVQlNQQU4gPSBcInIyLXR0cy1zcGVha2luZy13b3JkXCI7XG5jb25zdCBUVFNfSURfSU5KRUNURURfUEFSRU5UID0gXCJyMi10dHMtc3BlYWtpbmctdHh0LXBhcmVudFwiO1xuY29uc3QgSURfSElHSExJR0hUU19DT05UQUlORVIgPSBcIlIyX0lEX0hJR0hMSUdIVFNfQ09OVEFJTkVSXCI7XG5jb25zdCBJRF9BTk5PVEFUSU9OX0NPTlRBSU5FUiA9IFwiUjJfSURfQU5OT1RBVElPTl9DT05UQUlORVJcIjtcbmNvbnN0IENMQVNTX0hJR0hMSUdIVF9DT05UQUlORVIgPSBcIlIyX0NMQVNTX0hJR0hMSUdIVF9DT05UQUlORVJcIjtcbmNvbnN0IENMQVNTX0FOTk9UQVRJT05fQ09OVEFJTkVSID0gXCJSMl9DTEFTU19BTk5PVEFUSU9OX0NPTlRBSU5FUlwiO1xuY29uc3QgQ0xBU1NfSElHSExJR0hUX0FSRUEgPSBcIlIyX0NMQVNTX0hJR0hMSUdIVF9BUkVBXCI7XG5jb25zdCBDTEFTU19BTk5PVEFUSU9OX0FSRUEgPSBcIlIyX0NMQVNTX0FOTk9UQVRJT05fQVJFQVwiO1xuY29uc3QgQ0xBU1NfSElHSExJR0hUX0JPVU5ESU5HX0FSRUEgPSBcIlIyX0NMQVNTX0hJR0hMSUdIVF9CT1VORElOR19BUkVBXCI7XG5jb25zdCBDTEFTU19BTk5PVEFUSU9OX0JPVU5ESU5HX0FSRUEgPSBcIlIyX0NMQVNTX0FOTk9UQVRJT05fQk9VTkRJTkdfQVJFQVwiO1xuLy8gdHNsaW50OmRpc2FibGUtbmV4dC1saW5lOm1heC1saW5lLWxlbmd0aFxuY29uc3QgX2JsYWNrbGlzdElkQ2xhc3NGb3JDRkkgPSBbXG4gIFBPUFVQX0RJQUxPR19DTEFTUyxcbiAgVFRTX0NMQVNTX0lOSkVDVEVEX1NQQU4sXG4gIFRUU19DTEFTU19JTkpFQ1RFRF9TVUJTUEFOLFxuICBJRF9ISUdITElHSFRTX0NPTlRBSU5FUixcbiAgQ0xBU1NfSElHSExJR0hUX0NPTlRBSU5FUixcbiAgQ0xBU1NfSElHSExJR0hUX0FSRUEsXG4gIENMQVNTX0hJR0hMSUdIVF9CT1VORElOR19BUkVBLFxuICBcInJlc2l6ZS1zZW5zb3JcIixcbl07XG5jb25zdCBDTEFTU19QQUdJTkFURUQgPSBcInIyLWNzcy1wYWdpbmF0ZWRcIjtcblxuLy9jb25zdCBJU19ERVYgPSAocHJvY2Vzcy5lbnYuTk9ERV9FTlYgPT09IFwiZGV2ZWxvcG1lbnRcIiB8fCBwcm9jZXNzLmVudi5OT0RFX0VOViA9PT0gXCJkZXZcIik7XG5jb25zdCBJU19ERVYgPSBmYWxzZTtcbmNvbnN0IF9oaWdobGlnaHRzID0gW107XG5cbmxldCBfaGlnaGxpZ2h0c0NvbnRhaW5lcjtcbmxldCBfYW5ub3RhdGlvbkNvbnRhaW5lcjtcbmxldCBsYXN0TW91c2VEb3duWCA9IC0xO1xubGV0IGxhc3RNb3VzZURvd25ZID0gLTE7XG5sZXQgYm9keUV2ZW50TGlzdGVuZXJzU2V0ID0gZmFsc2U7XG5cbmNvbnN0IFVTRV9TVkcgPSBmYWxzZTtcbmNvbnN0IERFRkFVTFRfQkFDS0dST1VORF9DT0xPUl9PUEFDSVRZID0gMC4zO1xuY29uc3QgQUxUX0JBQ0tHUk9VTkRfQ09MT1JfT1BBQ0lUWSA9IDAuNDU7XG5cbi8vY29uc3QgREVCVUdfVklTVUFMUyA9IGZhbHNlO1xuY29uc3QgREVCVUdfVklTVUFMUyA9IGZhbHNlO1xuY29uc3QgREVGQVVMVF9CQUNLR1JPVU5EX0NPTE9SID0ge1xuICBibHVlOiAxMDAsXG4gIGdyZWVuOiA1MCxcbiAgcmVkOiAyMzAsXG59O1xuXG5jb25zdCBBTk5PVEFUSU9OX1dJRFRIID0gMTU7XG5cbmZ1bmN0aW9uIHJlc2V0SGlnaGxpZ2h0Qm91bmRpbmdTdHlsZShfd2luLCBoaWdobGlnaHRCb3VuZGluZykge1xuICBpZiAoXG4gICAgaGlnaGxpZ2h0Qm91bmRpbmcuZ2V0QXR0cmlidXRlKFwiY2xhc3NcIikgPT0gQ0xBU1NfQU5OT1RBVElPTl9CT1VORElOR19BUkVBXG4gICkge1xuICAgIHJldHVybjtcbiAgfVxuICBoaWdobGlnaHRCb3VuZGluZy5zdHlsZS5vdXRsaW5lID0gXCJub25lXCI7XG4gIGhpZ2hsaWdodEJvdW5kaW5nLnN0eWxlLnNldFByb3BlcnR5KFxuICAgIFwiYmFja2dyb3VuZC1jb2xvclwiLFxuICAgIFwidHJhbnNwYXJlbnRcIixcbiAgICBcImltcG9ydGFudFwiXG4gICk7XG59XG5cbmZ1bmN0aW9uIHNldEhpZ2hsaWdodEFyZWFTdHlsZSh3aW4sIGhpZ2hsaWdodEFyZWFzLCBoaWdobGlnaHQpIHtcbiAgY29uc3QgdXNlU1ZHID0gIURFQlVHX1ZJU1VBTFMgJiYgVVNFX1NWRztcbiAgZm9yIChjb25zdCBoaWdobGlnaHRBcmVhIG9mIGhpZ2hsaWdodEFyZWFzKSB7XG4gICAgY29uc3QgaXNTVkcgPSB1c2VTVkcgJiYgaGlnaGxpZ2h0QXJlYS5uYW1lc3BhY2VVUkkgPT09IFNWR19YTUxfTkFNRVNQQUNFO1xuICAgIGNvbnN0IG9wYWNpdHkgPSBBTFRfQkFDS0dST1VORF9DT0xPUl9PUEFDSVRZO1xuICAgIGlmIChpc1NWRykge1xuICAgICAgaGlnaGxpZ2h0QXJlYS5zdHlsZS5zZXRQcm9wZXJ0eShcbiAgICAgICAgXCJmaWxsXCIsXG4gICAgICAgIGByZ2IoJHtoaWdobGlnaHQuY29sb3IucmVkfSwgJHtoaWdobGlnaHQuY29sb3IuZ3JlZW59LCAke2hpZ2hsaWdodC5jb2xvci5ibHVlfSlgLFxuICAgICAgICBcImltcG9ydGFudFwiXG4gICAgICApO1xuICAgICAgaGlnaGxpZ2h0QXJlYS5zdHlsZS5zZXRQcm9wZXJ0eShcbiAgICAgICAgXCJmaWxsLW9wYWNpdHlcIixcbiAgICAgICAgYCR7b3BhY2l0eX1gLFxuICAgICAgICBcImltcG9ydGFudFwiXG4gICAgICApO1xuICAgICAgaGlnaGxpZ2h0QXJlYS5zdHlsZS5zZXRQcm9wZXJ0eShcbiAgICAgICAgXCJzdHJva2VcIixcbiAgICAgICAgYHJnYigke2hpZ2hsaWdodC5jb2xvci5yZWR9LCAke2hpZ2hsaWdodC5jb2xvci5ncmVlbn0sICR7aGlnaGxpZ2h0LmNvbG9yLmJsdWV9KWAsXG4gICAgICAgIFwiaW1wb3J0YW50XCJcbiAgICAgICk7XG4gICAgICBoaWdobGlnaHRBcmVhLnN0eWxlLnNldFByb3BlcnR5KFxuICAgICAgICBcInN0cm9rZS1vcGFjaXR5XCIsXG4gICAgICAgIGAke29wYWNpdHl9YCxcbiAgICAgICAgXCJpbXBvcnRhbnRcIlxuICAgICAgKTtcbiAgICB9IGVsc2Uge1xuICAgICAgaGlnaGxpZ2h0QXJlYS5zdHlsZS5zZXRQcm9wZXJ0eShcbiAgICAgICAgXCJiYWNrZ3JvdW5kLWNvbG9yXCIsXG4gICAgICAgIGByZ2JhKCR7aGlnaGxpZ2h0LmNvbG9yLnJlZH0sICR7aGlnaGxpZ2h0LmNvbG9yLmdyZWVufSwgJHtoaWdobGlnaHQuY29sb3IuYmx1ZX0sICR7b3BhY2l0eX0pYCxcbiAgICAgICAgXCJpbXBvcnRhbnRcIlxuICAgICAgKTtcbiAgICB9XG4gIH1cbn1cblxuZnVuY3Rpb24gcmVzZXRIaWdobGlnaHRBcmVhU3R5bGUod2luLCBoaWdobGlnaHRBcmVhKSB7XG4gIGNvbnN0IHVzZVNWRyA9ICFERUJVR19WSVNVQUxTICYmIFVTRV9TVkc7XG4gIC8vY29uc3QgdXNlU1ZHID0gVVNFX1NWRztcbiAgY29uc3QgaXNTVkcgPSB1c2VTVkcgJiYgaGlnaGxpZ2h0QXJlYS5uYW1lc3BhY2VVUkkgPT09IFNWR19YTUxfTkFNRVNQQUNFO1xuICBjb25zdCBpZCA9IGlzU1ZHXG4gICAgPyBoaWdobGlnaHRBcmVhLnBhcmVudE5vZGUgJiZcbiAgICAgIGhpZ2hsaWdodEFyZWEucGFyZW50Tm9kZS5wYXJlbnROb2RlICYmXG4gICAgICBoaWdobGlnaHRBcmVhLnBhcmVudE5vZGUucGFyZW50Tm9kZS5ub2RlVHlwZSA9PT0gTm9kZS5FTEVNRU5UX05PREUgJiZcbiAgICAgIGhpZ2hsaWdodEFyZWEucGFyZW50Tm9kZS5wYXJlbnROb2RlLmdldEF0dHJpYnV0ZVxuICAgICAgPyBoaWdobGlnaHRBcmVhLnBhcmVudE5vZGUucGFyZW50Tm9kZS5nZXRBdHRyaWJ1dGUoXCJpZFwiKVxuICAgICAgOiB1bmRlZmluZWRcbiAgICA6IGhpZ2hsaWdodEFyZWEucGFyZW50Tm9kZSAmJlxuICAgICAgaGlnaGxpZ2h0QXJlYS5wYXJlbnROb2RlLm5vZGVUeXBlID09PSBOb2RlLkVMRU1FTlRfTk9ERSAmJlxuICAgICAgaGlnaGxpZ2h0QXJlYS5wYXJlbnROb2RlLmdldEF0dHJpYnV0ZVxuICAgID8gaGlnaGxpZ2h0QXJlYS5wYXJlbnROb2RlLmdldEF0dHJpYnV0ZShcImlkXCIpXG4gICAgOiB1bmRlZmluZWQ7XG4gIGlmIChpZCkge1xuICAgIGNvbnN0IGhpZ2hsaWdodCA9IF9oaWdobGlnaHRzLmZpbmQoKGgpID0+IHtcbiAgICAgIHJldHVybiBoLmlkID09PSBpZDtcbiAgICB9KTtcbiAgICBpZiAoaGlnaGxpZ2h0KSB7XG4gICAgICBjb25zdCBvcGFjaXR5ID0gREVGQVVMVF9CQUNLR1JPVU5EX0NPTE9SX09QQUNJVFk7XG4gICAgICBpZiAoaXNTVkcpIHtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYS5zdHlsZS5zZXRQcm9wZXJ0eShcbiAgICAgICAgICBcImZpbGxcIixcbiAgICAgICAgICBgcmdiKCR7aGlnaGxpZ2h0LmNvbG9yLnJlZH0sICR7aGlnaGxpZ2h0LmNvbG9yLmdyZWVufSwgJHtoaWdobGlnaHQuY29sb3IuYmx1ZX0pYCxcbiAgICAgICAgICBcImltcG9ydGFudFwiXG4gICAgICAgICk7XG4gICAgICAgIGhpZ2hsaWdodEFyZWEuc3R5bGUuc2V0UHJvcGVydHkoXG4gICAgICAgICAgXCJmaWxsLW9wYWNpdHlcIixcbiAgICAgICAgICBgJHtvcGFjaXR5fWAsXG4gICAgICAgICAgXCJpbXBvcnRhbnRcIlxuICAgICAgICApO1xuICAgICAgICBoaWdobGlnaHRBcmVhLnN0eWxlLnNldFByb3BlcnR5KFxuICAgICAgICAgIFwic3Ryb2tlXCIsXG4gICAgICAgICAgYHJnYigke2hpZ2hsaWdodC5jb2xvci5yZWR9LCAke2hpZ2hsaWdodC5jb2xvci5ncmVlbn0sICR7aGlnaGxpZ2h0LmNvbG9yLmJsdWV9KWAsXG4gICAgICAgICAgXCJpbXBvcnRhbnRcIlxuICAgICAgICApO1xuICAgICAgICBoaWdobGlnaHRBcmVhLnN0eWxlLnNldFByb3BlcnR5KFxuICAgICAgICAgIFwic3Ryb2tlLW9wYWNpdHlcIixcbiAgICAgICAgICBgJHtvcGFjaXR5fWAsXG4gICAgICAgICAgXCJpbXBvcnRhbnRcIlxuICAgICAgICApO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYS5zdHlsZS5zZXRQcm9wZXJ0eShcbiAgICAgICAgICBcImJhY2tncm91bmQtY29sb3JcIixcbiAgICAgICAgICBgcmdiYSgke2hpZ2hsaWdodC5jb2xvci5yZWR9LCAke2hpZ2hsaWdodC5jb2xvci5ncmVlbn0sICR7aGlnaGxpZ2h0LmNvbG9yLmJsdWV9LCAke29wYWNpdHl9KWAsXG4gICAgICAgICAgXCJpbXBvcnRhbnRcIlxuICAgICAgICApO1xuICAgICAgfVxuICAgIH1cbiAgfVxufVxuZnVuY3Rpb24gcHJvY2Vzc1RvdWNoRXZlbnQod2luLCBldikge1xuICBjb25zdCBkb2N1bWVudCA9IHdpbi5kb2N1bWVudDtcbiAgY29uc3Qgc2Nyb2xsRWxlbWVudCA9IGdldFNjcm9sbGluZ0VsZW1lbnQoZG9jdW1lbnQpO1xuICBjb25zdCB4ID0gZXYuY2hhbmdlZFRvdWNoZXNbMF0uY2xpZW50WDtcbiAgY29uc3QgeSA9IGV2LmNoYW5nZWRUb3VjaGVzWzBdLmNsaWVudFk7XG4gIGlmICghX2hpZ2hsaWdodHNDb250YWluZXIpIHtcbiAgICByZXR1cm47XG4gIH1cbiAgY29uc3QgcGFnaW5hdGVkID0gaXNQYWdpbmF0ZWQoZG9jdW1lbnQpO1xuICBjb25zdCBib2R5UmVjdCA9IGRvY3VtZW50LmJvZHkuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCk7XG4gIGxldCB4T2Zmc2V0O1xuICBsZXQgeU9mZnNldDtcbiAgaWYgKG5hdmlnYXRvci51c2VyQWdlbnQubWF0Y2goL0FuZHJvaWQvaSkpIHtcbiAgICB4T2Zmc2V0ID0gcGFnaW5hdGVkID8gLXNjcm9sbEVsZW1lbnQuc2Nyb2xsTGVmdCA6IGJvZHlSZWN0LmxlZnQ7XG4gICAgeU9mZnNldCA9IHBhZ2luYXRlZCA/IC1zY3JvbGxFbGVtZW50LnNjcm9sbFRvcCA6IGJvZHlSZWN0LnRvcDtcbiAgfSBlbHNlIGlmIChuYXZpZ2F0b3IudXNlckFnZW50Lm1hdGNoKC9pUGhvbmV8aVBhZHxpUG9kL2kpKSB7XG4gICAgeE9mZnNldCA9IHBhZ2luYXRlZCA/IDAgOiAtc2Nyb2xsRWxlbWVudC5zY3JvbGxMZWZ0O1xuICAgIHlPZmZzZXQgPSBwYWdpbmF0ZWQgPyAwIDogYm9keVJlY3QudG9wO1xuICB9XG4gIGxldCBmb3VuZEhpZ2hsaWdodDtcbiAgbGV0IGZvdW5kRWxlbWVudDtcbiAgbGV0IGZvdW5kUmVjdDtcbiAgLy8gICAgX2hpZ2hsaWdodHMuc29ydChmdW5jdGlvbihhLCBiKSB7XG4gIC8vICAgICAgICBjb25zb2xlLmxvZyhKU09OLnN0cmluZ2lmeShhLnNlbGVjdGlvbkluZm8pKVxuICAvLyAgICAgICAgcmV0dXJuIGEuc2VsZWN0aW9uSW5mby5jbGVhblRleHQubGVuZ3RoIDwgYi5zZWxlY3Rpb25JbmZvLmNsZWFuVGV4dC5sZW5ndGhcbiAgLy8gICAgfSlcbiAgZm9yIChsZXQgaSA9IF9oaWdobGlnaHRzLmxlbmd0aCAtIDE7IGkgPj0gMDsgaS0tKSB7XG4gICAgY29uc3QgaGlnaGxpZ2h0ID0gX2hpZ2hsaWdodHNbaV07XG4gICAgbGV0IGhpZ2hsaWdodFBhcmVudCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGAke2hpZ2hsaWdodC5pZH1gKTtcbiAgICBpZiAoIWhpZ2hsaWdodFBhcmVudCkge1xuICAgICAgaGlnaGxpZ2h0UGFyZW50ID0gX2hpZ2hsaWdodHNDb250YWluZXIucXVlcnlTZWxlY3RvcihgIyR7aGlnaGxpZ2h0LmlkfWApO1xuICAgIH1cbiAgICBpZiAoIWhpZ2hsaWdodFBhcmVudCkge1xuICAgICAgY29udGludWU7XG4gICAgfVxuICAgIGxldCBoaXQgPSBmYWxzZTtcbiAgICBjb25zdCBoaWdobGlnaHRGcmFnbWVudHMgPSBoaWdobGlnaHRQYXJlbnQucXVlcnlTZWxlY3RvckFsbChcbiAgICAgIGAuJHtDTEFTU19ISUdITElHSFRfQVJFQX1gXG4gICAgKTtcbiAgICBmb3IgKGNvbnN0IGhpZ2hsaWdodEZyYWdtZW50IG9mIGhpZ2hsaWdodEZyYWdtZW50cykge1xuICAgICAgY29uc3Qgd2l0aFJlY3QgPSBoaWdobGlnaHRGcmFnbWVudDtcbiAgICAgIGNvbnN0IGxlZnQgPSB3aXRoUmVjdC5yZWN0LmxlZnQgKyB4T2Zmc2V0O1xuICAgICAgY29uc3QgdG9wID0gd2l0aFJlY3QucmVjdC50b3AgKyB5T2Zmc2V0O1xuICAgICAgZm91bmRSZWN0ID0gd2l0aFJlY3QucmVjdDtcbiAgICAgIGlmIChcbiAgICAgICAgeCA+PSBsZWZ0ICYmXG4gICAgICAgIHggPCBsZWZ0ICsgd2l0aFJlY3QucmVjdC53aWR0aCAmJlxuICAgICAgICB5ID49IHRvcCAmJlxuICAgICAgICB5IDwgdG9wICsgd2l0aFJlY3QucmVjdC5oZWlnaHRcbiAgICAgICkge1xuICAgICAgICBoaXQgPSB0cnVlO1xuICAgICAgICBicmVhaztcbiAgICAgIH1cbiAgICB9XG4gICAgaWYgKGhpdCkge1xuICAgICAgZm91bmRIaWdobGlnaHQgPSBoaWdobGlnaHQ7XG4gICAgICBmb3VuZEVsZW1lbnQgPSBoaWdobGlnaHRQYXJlbnQ7XG4gICAgICBicmVhaztcbiAgICB9XG4gIH1cbiAgaWYgKCFmb3VuZEhpZ2hsaWdodCB8fCAhZm91bmRFbGVtZW50KSB7XG4gICAgY29uc3QgaGlnaGxpZ2h0Qm91bmRpbmdzID0gX2hpZ2hsaWdodHNDb250YWluZXIucXVlcnlTZWxlY3RvckFsbChcbiAgICAgIGAuJHtDTEFTU19ISUdITElHSFRfQk9VTkRJTkdfQVJFQX1gXG4gICAgKTtcbiAgICBmb3IgKGNvbnN0IGhpZ2hsaWdodEJvdW5kaW5nIG9mIGhpZ2hsaWdodEJvdW5kaW5ncykge1xuICAgICAgcmVzZXRIaWdobGlnaHRCb3VuZGluZ1N0eWxlKHdpbiwgaGlnaGxpZ2h0Qm91bmRpbmcpO1xuICAgIH1cbiAgICBjb25zdCBhbGxIaWdobGlnaHRBcmVhcyA9IEFycmF5LmZyb20oXG4gICAgICBfaGlnaGxpZ2h0c0NvbnRhaW5lci5xdWVyeVNlbGVjdG9yQWxsKGAuJHtDTEFTU19ISUdITElHSFRfQVJFQX1gKVxuICAgICk7XG4gICAgZm9yIChjb25zdCBoaWdobGlnaHRBcmVhIG9mIGFsbEhpZ2hsaWdodEFyZWFzKSB7XG4gICAgICByZXNldEhpZ2hsaWdodEFyZWFTdHlsZSh3aW4sIGhpZ2hsaWdodEFyZWEpO1xuICAgIH1cbiAgICByZXR1cm47XG4gIH1cblxuICBpZiAoZm91bmRFbGVtZW50LmdldEF0dHJpYnV0ZShcImRhdGEtY2xpY2tcIikpIHtcbiAgICBpZiAoZXYudHlwZSA9PT0gXCJtb3VzZW1vdmVcIikge1xuICAgICAgY29uc3QgZm91bmRFbGVtZW50SGlnaGxpZ2h0QXJlYXMgPSBBcnJheS5mcm9tKFxuICAgICAgICBmb3VuZEVsZW1lbnQucXVlcnlTZWxlY3RvckFsbChgLiR7Q0xBU1NfSElHSExJR0hUX0FSRUF9YClcbiAgICAgICk7XG4gICAgICBjb25zdCBhbGxIaWdobGlnaHRBcmVhcyA9IF9oaWdobGlnaHRzQ29udGFpbmVyLnF1ZXJ5U2VsZWN0b3JBbGwoXG4gICAgICAgIGAuJHtDTEFTU19ISUdITElHSFRfQVJFQX1gXG4gICAgICApO1xuICAgICAgZm9yIChjb25zdCBoaWdobGlnaHRBcmVhIG9mIGFsbEhpZ2hsaWdodEFyZWFzKSB7XG4gICAgICAgIGlmIChmb3VuZEVsZW1lbnRIaWdobGlnaHRBcmVhcy5pbmRleE9mKGhpZ2hsaWdodEFyZWEpIDwgMCkge1xuICAgICAgICAgIHJlc2V0SGlnaGxpZ2h0QXJlYVN0eWxlKHdpbiwgaGlnaGxpZ2h0QXJlYSk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICAgIHNldEhpZ2hsaWdodEFyZWFTdHlsZSh3aW4sIGZvdW5kRWxlbWVudEhpZ2hsaWdodEFyZWFzLCBmb3VuZEhpZ2hsaWdodCk7XG4gICAgICBjb25zdCBmb3VuZEVsZW1lbnRIaWdobGlnaHRCb3VuZGluZyA9IGZvdW5kRWxlbWVudC5xdWVyeVNlbGVjdG9yKFxuICAgICAgICBgLiR7Q0xBU1NfSElHSExJR0hUX0JPVU5ESU5HX0FSRUF9YFxuICAgICAgKTtcbiAgICAgIGNvbnN0IGFsbEhpZ2hsaWdodEJvdW5kaW5ncyA9IF9oaWdobGlnaHRzQ29udGFpbmVyLnF1ZXJ5U2VsZWN0b3JBbGwoXG4gICAgICAgIGAuJHtDTEFTU19ISUdITElHSFRfQk9VTkRJTkdfQVJFQX1gXG4gICAgICApO1xuICAgICAgZm9yIChjb25zdCBoaWdobGlnaHRCb3VuZGluZyBvZiBhbGxIaWdobGlnaHRCb3VuZGluZ3MpIHtcbiAgICAgICAgaWYgKFxuICAgICAgICAgICFmb3VuZEVsZW1lbnRIaWdobGlnaHRCb3VuZGluZyB8fFxuICAgICAgICAgIGhpZ2hsaWdodEJvdW5kaW5nICE9PSBmb3VuZEVsZW1lbnRIaWdobGlnaHRCb3VuZGluZ1xuICAgICAgICApIHtcbiAgICAgICAgICByZXNldEhpZ2hsaWdodEJvdW5kaW5nU3R5bGUod2luLCBoaWdobGlnaHRCb3VuZGluZyk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICAgIGlmIChmb3VuZEVsZW1lbnRIaWdobGlnaHRCb3VuZGluZykge1xuICAgICAgICBpZiAoREVCVUdfVklTVUFMUykge1xuICAgICAgICAgIHNldEhpZ2hsaWdodEJvdW5kaW5nU3R5bGUoXG4gICAgICAgICAgICB3aW4sXG4gICAgICAgICAgICBmb3VuZEVsZW1lbnRIaWdobGlnaHRCb3VuZGluZyxcbiAgICAgICAgICAgIGZvdW5kSGlnaGxpZ2h0XG4gICAgICAgICAgKTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0gZWxzZSBpZiAoZXYudHlwZSA9PT0gXCJ0b3VjaHN0YXJ0XCIgfHwgZXYudHlwZSA9PT0gXCJ0b3VjaGVuZFwiKSB7XG4gICAgICBjb25zdCBzaXplID0ge1xuICAgICAgICBzY3JlZW5XaWR0aDogd2luZG93Lm91dGVyV2lkdGgsXG4gICAgICAgIHNjcmVlbkhlaWdodDogd2luZG93Lm91dGVySGVpZ2h0LFxuICAgICAgICBsZWZ0OiBmb3VuZFJlY3QubGVmdCxcbiAgICAgICAgd2lkdGg6IGZvdW5kUmVjdC53aWR0aCxcbiAgICAgICAgdG9wOiBmb3VuZFJlY3QudG9wLFxuICAgICAgICBoZWlnaHQ6IGZvdW5kUmVjdC5oZWlnaHQsXG4gICAgICB9O1xuICAgICAgY29uc3QgcGF5bG9hZCA9IHtcbiAgICAgICAgaGlnaGxpZ2h0OiBmb3VuZEhpZ2hsaWdodC5pZCxcbiAgICAgICAgc2l6ZTogc2l6ZSxcbiAgICAgIH07XG5cbiAgICAgIGlmIChcbiAgICAgICAgdHlwZW9mIHdpbmRvdyAhPT0gXCJ1bmRlZmluZWRcIiAmJlxuICAgICAgICB0eXBlb2Ygd2luZG93LnByb2Nlc3MgPT09IFwib2JqZWN0XCIgJiZcbiAgICAgICAgd2luZG93LnByb2Nlc3MudHlwZSA9PT0gXCJyZW5kZXJlclwiXG4gICAgICApIHtcbiAgICAgICAgZWxlY3Ryb25fMS5pcGNSZW5kZXJlci5zZW5kVG9Ib3N0KFIyX0VWRU5UX0hJR0hMSUdIVF9DTElDSywgcGF5bG9hZCk7XG4gICAgICB9IGVsc2UgaWYgKHdpbmRvdy53ZWJraXRVUkwpIHtcbiAgICAgICAgY29uc29sZS5sb2coZm91bmRIaWdobGlnaHQuaWQuaW5jbHVkZXMoXCJSMl9BTk5PVEFUSU9OX1wiKSk7XG4gICAgICAgIGlmIChmb3VuZEhpZ2hsaWdodC5pZC5zZWFyY2goXCJSMl9BTk5PVEFUSU9OX1wiKSA+PSAwKSB7XG4gICAgICAgICAgaWYgKG5hdmlnYXRvci51c2VyQWdlbnQubWF0Y2goL0FuZHJvaWQvaSkpIHtcbiAgICAgICAgICAgIEFuZHJvaWQuaGlnaGxpZ2h0QW5ub3RhdGlvbk1hcmtBY3RpdmF0ZWQoZm91bmRIaWdobGlnaHQuaWQpO1xuICAgICAgICAgIH0gZWxzZSBpZiAobmF2aWdhdG9yLnVzZXJBZ2VudC5tYXRjaCgvaVBob25lfGlQYWR8aVBvZC9pKSkge1xuICAgICAgICAgICAgd2Via2l0Lm1lc3NhZ2VIYW5kbGVycy5oaWdobGlnaHRBbm5vdGF0aW9uTWFya0FjdGl2YXRlZC5wb3N0TWVzc2FnZShcbiAgICAgICAgICAgICAgZm91bmRIaWdobGlnaHQuaWRcbiAgICAgICAgICAgICk7XG4gICAgICAgICAgfVxuICAgICAgICB9IGVsc2UgaWYgKGZvdW5kSGlnaGxpZ2h0LmlkLnNlYXJjaChcIlIyX0hJR0hMSUdIVF9cIikgPj0gMCkge1xuICAgICAgICAgIGlmIChuYXZpZ2F0b3IudXNlckFnZW50Lm1hdGNoKC9BbmRyb2lkL2kpKSB7XG4gICAgICAgICAgICBBbmRyb2lkLmhpZ2hsaWdodEFjdGl2YXRlZChmb3VuZEhpZ2hsaWdodC5pZCk7XG4gICAgICAgICAgfSBlbHNlIGlmIChuYXZpZ2F0b3IudXNlckFnZW50Lm1hdGNoKC9pUGhvbmV8aVBhZHxpUG9kL2kpKSB7XG4gICAgICAgICAgICB3ZWJraXQubWVzc2FnZUhhbmRsZXJzLmhpZ2hsaWdodEFjdGl2YXRlZC5wb3N0TWVzc2FnZShcbiAgICAgICAgICAgICAgZm91bmRIaWdobGlnaHQuaWRcbiAgICAgICAgICAgICk7XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9XG5cbiAgICAgIGV2LnN0b3BQcm9wYWdhdGlvbigpO1xuICAgICAgZXYucHJldmVudERlZmF1bHQoKTtcbiAgICB9XG4gIH1cbn1cblxuZnVuY3Rpb24gcHJvY2Vzc01vdXNlRXZlbnQod2luLCBldikge1xuICBjb25zdCBkb2N1bWVudCA9IHdpbi5kb2N1bWVudDtcbiAgY29uc3Qgc2Nyb2xsRWxlbWVudCA9IGdldFNjcm9sbGluZ0VsZW1lbnQoZG9jdW1lbnQpO1xuICBjb25zdCB4ID0gZXYuY2xpZW50WDtcbiAgY29uc3QgeSA9IGV2LmNsaWVudFk7XG4gIGlmICghX2hpZ2hsaWdodHNDb250YWluZXIpIHtcbiAgICByZXR1cm47XG4gIH1cblxuICBjb25zdCBwYWdpbmF0ZWQgPSBpc1BhZ2luYXRlZChkb2N1bWVudCk7XG4gIGNvbnN0IGJvZHlSZWN0ID0gZG9jdW1lbnQuYm9keS5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKTtcbiAgbGV0IHhPZmZzZXQ7XG4gIGxldCB5T2Zmc2V0O1xuICBpZiAobmF2aWdhdG9yLnVzZXJBZ2VudC5tYXRjaCgvQW5kcm9pZC9pKSkge1xuICAgIHhPZmZzZXQgPSBwYWdpbmF0ZWQgPyAtc2Nyb2xsRWxlbWVudC5zY3JvbGxMZWZ0IDogYm9keVJlY3QubGVmdDtcbiAgICB5T2Zmc2V0ID0gcGFnaW5hdGVkID8gLXNjcm9sbEVsZW1lbnQuc2Nyb2xsVG9wIDogYm9keVJlY3QudG9wO1xuICB9IGVsc2UgaWYgKG5hdmlnYXRvci51c2VyQWdlbnQubWF0Y2goL2lQaG9uZXxpUGFkfGlQb2QvaSkpIHtcbiAgICB4T2Zmc2V0ID0gcGFnaW5hdGVkID8gMCA6IC1zY3JvbGxFbGVtZW50LnNjcm9sbExlZnQ7XG4gICAgeU9mZnNldCA9IHBhZ2luYXRlZCA/IDAgOiBib2R5UmVjdC50b3A7XG4gIH1cbiAgbGV0IGZvdW5kSGlnaGxpZ2h0O1xuICBsZXQgZm91bmRFbGVtZW50O1xuICBsZXQgZm91bmRSZWN0O1xuICBmb3IgKGxldCBpID0gX2hpZ2hsaWdodHMubGVuZ3RoIC0gMTsgaSA+PSAwOyBpLS0pIHtcbiAgICBjb25zdCBoaWdobGlnaHQgPSBfaGlnaGxpZ2h0c1tpXTtcbiAgICBsZXQgaGlnaGxpZ2h0UGFyZW50ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoYCR7aGlnaGxpZ2h0LmlkfWApO1xuICAgIGlmICghaGlnaGxpZ2h0UGFyZW50KSB7XG4gICAgICBoaWdobGlnaHRQYXJlbnQgPSBfaGlnaGxpZ2h0c0NvbnRhaW5lci5xdWVyeVNlbGVjdG9yKGAjJHtoaWdobGlnaHQuaWR9YCk7XG4gICAgfVxuICAgIGlmICghaGlnaGxpZ2h0UGFyZW50KSB7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG4gICAgbGV0IGhpdCA9IGZhbHNlO1xuICAgIGNvbnN0IGhpZ2hsaWdodEZyYWdtZW50cyA9IGhpZ2hsaWdodFBhcmVudC5xdWVyeVNlbGVjdG9yQWxsKFxuICAgICAgYC4ke0NMQVNTX0hJR0hMSUdIVF9BUkVBfWBcbiAgICApO1xuICAgIGZvciAoY29uc3QgaGlnaGxpZ2h0RnJhZ21lbnQgb2YgaGlnaGxpZ2h0RnJhZ21lbnRzKSB7XG4gICAgICBjb25zdCB3aXRoUmVjdCA9IGhpZ2hsaWdodEZyYWdtZW50O1xuICAgICAgY29uc3QgbGVmdCA9IHdpdGhSZWN0LnJlY3QubGVmdCArIHhPZmZzZXQ7XG4gICAgICBjb25zdCB0b3AgPSB3aXRoUmVjdC5yZWN0LnRvcCArIHlPZmZzZXQ7XG4gICAgICBmb3VuZFJlY3QgPSB3aXRoUmVjdC5yZWN0O1xuICAgICAgaWYgKFxuICAgICAgICB4ID49IGxlZnQgJiZcbiAgICAgICAgeCA8IGxlZnQgKyB3aXRoUmVjdC5yZWN0LndpZHRoICYmXG4gICAgICAgIHkgPj0gdG9wICYmXG4gICAgICAgIHkgPCB0b3AgKyB3aXRoUmVjdC5yZWN0LmhlaWdodFxuICAgICAgKSB7XG4gICAgICAgIGhpdCA9IHRydWU7XG4gICAgICAgIGJyZWFrO1xuICAgICAgfVxuICAgIH1cbiAgICBpZiAoaGl0KSB7XG4gICAgICBmb3VuZEhpZ2hsaWdodCA9IGhpZ2hsaWdodDtcbiAgICAgIGZvdW5kRWxlbWVudCA9IGhpZ2hsaWdodFBhcmVudDtcbiAgICAgIGJyZWFrO1xuICAgIH1cbiAgfVxuXG4gIGlmICghZm91bmRIaWdobGlnaHQgfHwgIWZvdW5kRWxlbWVudCkge1xuICAgIGNvbnN0IGhpZ2hsaWdodEJvdW5kaW5ncyA9IF9oaWdobGlnaHRzQ29udGFpbmVyLnF1ZXJ5U2VsZWN0b3JBbGwoXG4gICAgICBgLiR7Q0xBU1NfSElHSExJR0hUX0JPVU5ESU5HX0FSRUF9YFxuICAgICk7XG4gICAgZm9yIChjb25zdCBoaWdobGlnaHRCb3VuZGluZyBvZiBoaWdobGlnaHRCb3VuZGluZ3MpIHtcbiAgICAgIHJlc2V0SGlnaGxpZ2h0Qm91bmRpbmdTdHlsZSh3aW4sIGhpZ2hsaWdodEJvdW5kaW5nKTtcbiAgICB9XG4gICAgY29uc3QgYWxsSGlnaGxpZ2h0QXJlYXMgPSBBcnJheS5mcm9tKFxuICAgICAgX2hpZ2hsaWdodHNDb250YWluZXIucXVlcnlTZWxlY3RvckFsbChgLiR7Q0xBU1NfSElHSExJR0hUX0FSRUF9YClcbiAgICApO1xuICAgIGZvciAoY29uc3QgaGlnaGxpZ2h0QXJlYSBvZiBhbGxIaWdobGlnaHRBcmVhcykge1xuICAgICAgcmVzZXRIaWdobGlnaHRBcmVhU3R5bGUod2luLCBoaWdobGlnaHRBcmVhKTtcbiAgICB9XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgaWYgKGZvdW5kRWxlbWVudC5nZXRBdHRyaWJ1dGUoXCJkYXRhLWNsaWNrXCIpKSB7XG4gICAgaWYgKGV2LnR5cGUgPT09IFwibW91c2Vtb3ZlXCIpIHtcbiAgICAgIGNvbnN0IGZvdW5kRWxlbWVudEhpZ2hsaWdodEFyZWFzID0gQXJyYXkuZnJvbShcbiAgICAgICAgZm91bmRFbGVtZW50LnF1ZXJ5U2VsZWN0b3JBbGwoYC4ke0NMQVNTX0hJR0hMSUdIVF9BUkVBfWApXG4gICAgICApO1xuICAgICAgY29uc3QgYWxsSGlnaGxpZ2h0QXJlYXMgPSBfaGlnaGxpZ2h0c0NvbnRhaW5lci5xdWVyeVNlbGVjdG9yQWxsKFxuICAgICAgICBgLiR7Q0xBU1NfSElHSExJR0hUX0FSRUF9YFxuICAgICAgKTtcbiAgICAgIGZvciAoY29uc3QgaGlnaGxpZ2h0QXJlYSBvZiBhbGxIaWdobGlnaHRBcmVhcykge1xuICAgICAgICBpZiAoZm91bmRFbGVtZW50SGlnaGxpZ2h0QXJlYXMuaW5kZXhPZihoaWdobGlnaHRBcmVhKSA8IDApIHtcbiAgICAgICAgICByZXNldEhpZ2hsaWdodEFyZWFTdHlsZSh3aW4sIGhpZ2hsaWdodEFyZWEpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgICBzZXRIaWdobGlnaHRBcmVhU3R5bGUod2luLCBmb3VuZEVsZW1lbnRIaWdobGlnaHRBcmVhcywgZm91bmRIaWdobGlnaHQpO1xuICAgICAgY29uc3QgZm91bmRFbGVtZW50SGlnaGxpZ2h0Qm91bmRpbmcgPSBmb3VuZEVsZW1lbnQucXVlcnlTZWxlY3RvcihcbiAgICAgICAgYC4ke0NMQVNTX0hJR0hMSUdIVF9CT1VORElOR19BUkVBfWBcbiAgICAgICk7XG4gICAgICBjb25zdCBhbGxIaWdobGlnaHRCb3VuZGluZ3MgPSBfaGlnaGxpZ2h0c0NvbnRhaW5lci5xdWVyeVNlbGVjdG9yQWxsKFxuICAgICAgICBgLiR7Q0xBU1NfSElHSExJR0hUX0JPVU5ESU5HX0FSRUF9YFxuICAgICAgKTtcbiAgICAgIGZvciAoY29uc3QgaGlnaGxpZ2h0Qm91bmRpbmcgb2YgYWxsSGlnaGxpZ2h0Qm91bmRpbmdzKSB7XG4gICAgICAgIGlmIChcbiAgICAgICAgICAhZm91bmRFbGVtZW50SGlnaGxpZ2h0Qm91bmRpbmcgfHxcbiAgICAgICAgICBoaWdobGlnaHRCb3VuZGluZyAhPT0gZm91bmRFbGVtZW50SGlnaGxpZ2h0Qm91bmRpbmdcbiAgICAgICAgKSB7XG4gICAgICAgICAgcmVzZXRIaWdobGlnaHRCb3VuZGluZ1N0eWxlKHdpbiwgaGlnaGxpZ2h0Qm91bmRpbmcpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgICBpZiAoZm91bmRFbGVtZW50SGlnaGxpZ2h0Qm91bmRpbmcpIHtcbiAgICAgICAgaWYgKERFQlVHX1ZJU1VBTFMpIHtcbiAgICAgICAgICBzZXRIaWdobGlnaHRCb3VuZGluZ1N0eWxlKFxuICAgICAgICAgICAgd2luLFxuICAgICAgICAgICAgZm91bmRFbGVtZW50SGlnaGxpZ2h0Qm91bmRpbmcsXG4gICAgICAgICAgICBmb3VuZEhpZ2hsaWdodFxuICAgICAgICAgICk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9IGVsc2UgaWYgKGV2LnR5cGUgPT09IFwibW91c2V1cFwiIHx8IGV2LnR5cGUgPT09IFwidG91Y2hlbmRcIikge1xuICAgICAgY29uc3QgdG91Y2hlZFBvc2l0aW9uID0ge1xuICAgICAgICBzY3JlZW5XaWR0aDogd2luZG93Lm91dGVyV2lkdGgsXG4gICAgICAgIHNjcmVlbkhlaWdodDogd2luZG93LmlubmVySGVpZ2h0LFxuICAgICAgICBsZWZ0OiBmb3VuZFJlY3QubGVmdCxcbiAgICAgICAgd2lkdGg6IGZvdW5kUmVjdC53aWR0aCxcbiAgICAgICAgdG9wOiBmb3VuZFJlY3QudG9wLFxuICAgICAgICBoZWlnaHQ6IGZvdW5kUmVjdC5oZWlnaHQsXG4gICAgICB9O1xuXG4gICAgICBjb25zdCBwYXlsb2FkID0ge1xuICAgICAgICBoaWdobGlnaHQ6IGZvdW5kSGlnaGxpZ2h0LFxuICAgICAgICBwb3NpdGlvbjogdG91Y2hlZFBvc2l0aW9uLFxuICAgICAgfTtcblxuICAgICAgaWYgKFxuICAgICAgICB0eXBlb2Ygd2luZG93ICE9PSBcInVuZGVmaW5lZFwiICYmXG4gICAgICAgIHR5cGVvZiB3aW5kb3cucHJvY2VzcyA9PT0gXCJvYmplY3RcIiAmJlxuICAgICAgICB3aW5kb3cucHJvY2Vzcy50eXBlID09PSBcInJlbmRlcmVyXCJcbiAgICAgICkge1xuICAgICAgICBlbGVjdHJvbl8xLmlwY1JlbmRlcmVyLnNlbmRUb0hvc3QoUjJfRVZFTlRfSElHSExJR0hUX0NMSUNLLCBwYXlsb2FkKTtcbiAgICAgIH0gZWxzZSBpZiAod2luZG93LndlYmtpdFVSTCkge1xuICAgICAgICBpZiAoZm91bmRIaWdobGlnaHQuaWQuc2VhcmNoKFwiUjJfQU5OT1RBVElPTl9cIikgPj0gMCkge1xuICAgICAgICAgIGlmIChuYXZpZ2F0b3IudXNlckFnZW50Lm1hdGNoKC9BbmRyb2lkL2kpKSB7XG4gICAgICAgICAgICBBbmRyb2lkLmhpZ2hsaWdodEFubm90YXRpb25NYXJrQWN0aXZhdGVkKGZvdW5kSGlnaGxpZ2h0LmlkKTtcbiAgICAgICAgICB9IGVsc2UgaWYgKG5hdmlnYXRvci51c2VyQWdlbnQubWF0Y2goL2lQaG9uZXxpUGFkfGlQb2QvaSkpIHtcbiAgICAgICAgICAgIHdlYmtpdC5tZXNzYWdlSGFuZGxlcnMuaGlnaGxpZ2h0QW5ub3RhdGlvbk1hcmtBY3RpdmF0ZWQucG9zdE1lc3NhZ2UoXG4gICAgICAgICAgICAgIGZvdW5kSGlnaGxpZ2h0LmlkXG4gICAgICAgICAgICApO1xuICAgICAgICAgIH1cbiAgICAgICAgfSBlbHNlIGlmIChmb3VuZEhpZ2hsaWdodC5pZC5zZWFyY2goXCJSMl9ISUdITElHSFRfXCIpID49IDApIHtcbiAgICAgICAgICBpZiAobmF2aWdhdG9yLnVzZXJBZ2VudC5tYXRjaCgvQW5kcm9pZC9pKSkge1xuICAgICAgICAgICAgQW5kcm9pZC5oaWdobGlnaHRBY3RpdmF0ZWQoZm91bmRIaWdobGlnaHQuaWQpO1xuICAgICAgICAgIH0gZWxzZSBpZiAobmF2aWdhdG9yLnVzZXJBZ2VudC5tYXRjaCgvaVBob25lfGlQYWR8aVBvZC9pKSkge1xuICAgICAgICAgICAgd2Via2l0Lm1lc3NhZ2VIYW5kbGVycy5oaWdobGlnaHRBY3RpdmF0ZWQucG9zdE1lc3NhZ2UoXG4gICAgICAgICAgICAgIGZvdW5kSGlnaGxpZ2h0LmlkXG4gICAgICAgICAgICApO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICBldi5zdG9wUHJvcGFnYXRpb24oKTtcbiAgICB9XG4gIH1cbn1cblxuZnVuY3Rpb24gcmVjdHNUb3VjaE9yT3ZlcmxhcChyZWN0MSwgcmVjdDIsIHRvbGVyYW5jZSkge1xuICByZXR1cm4gKFxuICAgIChyZWN0MS5sZWZ0IDwgcmVjdDIucmlnaHQgfHxcbiAgICAgICh0b2xlcmFuY2UgPj0gMCAmJiBhbG1vc3RFcXVhbChyZWN0MS5sZWZ0LCByZWN0Mi5yaWdodCwgdG9sZXJhbmNlKSkpICYmXG4gICAgKHJlY3QyLmxlZnQgPCByZWN0MS5yaWdodCB8fFxuICAgICAgKHRvbGVyYW5jZSA+PSAwICYmIGFsbW9zdEVxdWFsKHJlY3QyLmxlZnQsIHJlY3QxLnJpZ2h0LCB0b2xlcmFuY2UpKSkgJiZcbiAgICAocmVjdDEudG9wIDwgcmVjdDIuYm90dG9tIHx8XG4gICAgICAodG9sZXJhbmNlID49IDAgJiYgYWxtb3N0RXF1YWwocmVjdDEudG9wLCByZWN0Mi5ib3R0b20sIHRvbGVyYW5jZSkpKSAmJlxuICAgIChyZWN0Mi50b3AgPCByZWN0MS5ib3R0b20gfHxcbiAgICAgICh0b2xlcmFuY2UgPj0gMCAmJiBhbG1vc3RFcXVhbChyZWN0Mi50b3AsIHJlY3QxLmJvdHRvbSwgdG9sZXJhbmNlKSkpXG4gICk7XG59XG5cbmZ1bmN0aW9uIHJlcGxhY2VPdmVybGFwaW5nUmVjdHMocmVjdHMpIHtcbiAgZm9yIChsZXQgaSA9IDA7IGkgPCByZWN0cy5sZW5ndGg7IGkrKykge1xuICAgIGZvciAobGV0IGogPSBpICsgMTsgaiA8IHJlY3RzLmxlbmd0aDsgaisrKSB7XG4gICAgICBjb25zdCByZWN0MSA9IHJlY3RzW2ldO1xuICAgICAgY29uc3QgcmVjdDIgPSByZWN0c1tqXTtcbiAgICAgIGlmIChyZWN0MSA9PT0gcmVjdDIpIHtcbiAgICAgICAgaWYgKElTX0RFVikge1xuICAgICAgICAgIGNvbnNvbGUubG9nKFwicmVwbGFjZU92ZXJsYXBpbmdSZWN0cyByZWN0MSA9PT0gcmVjdDIgPz8hXCIpO1xuICAgICAgICB9XG4gICAgICAgIGNvbnRpbnVlO1xuICAgICAgfVxuICAgICAgaWYgKHJlY3RzVG91Y2hPck92ZXJsYXAocmVjdDEsIHJlY3QyLCAtMSkpIHtcbiAgICAgICAgbGV0IHRvQWRkID0gW107XG4gICAgICAgIGxldCB0b1JlbW92ZTtcbiAgICAgICAgbGV0IHRvUHJlc2VydmU7XG4gICAgICAgIGNvbnN0IHN1YnRyYWN0UmVjdHMxID0gcmVjdFN1YnRyYWN0KHJlY3QxLCByZWN0Mik7XG4gICAgICAgIGlmIChzdWJ0cmFjdFJlY3RzMS5sZW5ndGggPT09IDEpIHtcbiAgICAgICAgICB0b0FkZCA9IHN1YnRyYWN0UmVjdHMxO1xuICAgICAgICAgIHRvUmVtb3ZlID0gcmVjdDE7XG4gICAgICAgICAgdG9QcmVzZXJ2ZSA9IHJlY3QyO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIGNvbnN0IHN1YnRyYWN0UmVjdHMyID0gcmVjdFN1YnRyYWN0KHJlY3QyLCByZWN0MSk7XG4gICAgICAgICAgaWYgKHN1YnRyYWN0UmVjdHMxLmxlbmd0aCA8IHN1YnRyYWN0UmVjdHMyLmxlbmd0aCkge1xuICAgICAgICAgICAgdG9BZGQgPSBzdWJ0cmFjdFJlY3RzMTtcbiAgICAgICAgICAgIHRvUmVtb3ZlID0gcmVjdDE7XG4gICAgICAgICAgICB0b1ByZXNlcnZlID0gcmVjdDI7XG4gICAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIHRvQWRkID0gc3VidHJhY3RSZWN0czI7XG4gICAgICAgICAgICB0b1JlbW92ZSA9IHJlY3QyO1xuICAgICAgICAgICAgdG9QcmVzZXJ2ZSA9IHJlY3QxO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgICBpZiAoSVNfREVWKSB7XG4gICAgICAgICAgY29uc3QgdG9DaGVjayA9IFtdO1xuICAgICAgICAgIHRvQ2hlY2sucHVzaCh0b1ByZXNlcnZlKTtcbiAgICAgICAgICBBcnJheS5wcm90b3R5cGUucHVzaC5hcHBseSh0b0NoZWNrLCB0b0FkZCk7XG4gICAgICAgICAgY2hlY2tPdmVybGFwcyh0b0NoZWNrKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAoSVNfREVWKSB7XG4gICAgICAgICAgY29uc29sZS5sb2coXG4gICAgICAgICAgICBgQ0xJRU5UIFJFQ1Q6IG92ZXJsYXAsIGN1dCBvbmUgcmVjdCBpbnRvICR7dG9BZGQubGVuZ3RofWBcbiAgICAgICAgICApO1xuICAgICAgICB9XG4gICAgICAgIGNvbnN0IG5ld1JlY3RzID0gcmVjdHMuZmlsdGVyKChyZWN0KSA9PiB7XG4gICAgICAgICAgcmV0dXJuIHJlY3QgIT09IHRvUmVtb3ZlO1xuICAgICAgICB9KTtcbiAgICAgICAgQXJyYXkucHJvdG90eXBlLnB1c2guYXBwbHkobmV3UmVjdHMsIHRvQWRkKTtcbiAgICAgICAgcmV0dXJuIHJlcGxhY2VPdmVybGFwaW5nUmVjdHMobmV3UmVjdHMpO1xuICAgICAgfVxuICAgIH1cbiAgfVxuICByZXR1cm4gcmVjdHM7XG59XG5cbmZ1bmN0aW9uIGNoZWNrT3ZlcmxhcHMocmVjdHMpIHtcbiAgY29uc3Qgc3RpbGxPdmVybGFwaW5nUmVjdHMgPSBbXTtcbiAgZm9yIChjb25zdCByZWN0MSBvZiByZWN0cykge1xuICAgIGZvciAoY29uc3QgcmVjdDIgb2YgcmVjdHMpIHtcbiAgICAgIGlmIChyZWN0MSA9PT0gcmVjdDIpIHtcbiAgICAgICAgY29udGludWU7XG4gICAgICB9XG4gICAgICBjb25zdCBoYXMxID0gc3RpbGxPdmVybGFwaW5nUmVjdHMuaW5kZXhPZihyZWN0MSkgPj0gMDtcbiAgICAgIGNvbnN0IGhhczIgPSBzdGlsbE92ZXJsYXBpbmdSZWN0cy5pbmRleE9mKHJlY3QyKSA+PSAwO1xuICAgICAgaWYgKCFoYXMxIHx8ICFoYXMyKSB7XG4gICAgICAgIGlmIChyZWN0c1RvdWNoT3JPdmVybGFwKHJlY3QxLCByZWN0MiwgLTEpKSB7XG4gICAgICAgICAgaWYgKCFoYXMxKSB7XG4gICAgICAgICAgICBzdGlsbE92ZXJsYXBpbmdSZWN0cy5wdXNoKHJlY3QxKTtcbiAgICAgICAgICB9XG4gICAgICAgICAgaWYgKCFoYXMyKSB7XG4gICAgICAgICAgICBzdGlsbE92ZXJsYXBpbmdSZWN0cy5wdXNoKHJlY3QyKTtcbiAgICAgICAgICB9XG4gICAgICAgICAgY29uc29sZS5sb2coXCJDTElFTlQgUkVDVDogb3ZlcmxhcCAtLS1cIik7XG4gICAgICAgICAgY29uc29sZS5sb2coXG4gICAgICAgICAgICBgIzEgVE9QOiR7cmVjdDEudG9wfSBCT1RUT006JHtyZWN0MS5ib3R0b219IExFRlQ6JHtyZWN0MS5sZWZ0fSBSSUdIVDoke3JlY3QxLnJpZ2h0fSBXSURUSDoke3JlY3QxLndpZHRofSBIRUlHSFQ6JHtyZWN0MS5oZWlnaHR9YFxuICAgICAgICAgICk7XG4gICAgICAgICAgY29uc29sZS5sb2coXG4gICAgICAgICAgICBgIzIgVE9QOiR7cmVjdDIudG9wfSBCT1RUT006JHtyZWN0Mi5ib3R0b219IExFRlQ6JHtyZWN0Mi5sZWZ0fSBSSUdIVDoke3JlY3QyLnJpZ2h0fSBXSURUSDoke3JlY3QyLndpZHRofSBIRUlHSFQ6JHtyZWN0Mi5oZWlnaHR9YFxuICAgICAgICAgICk7XG4gICAgICAgICAgY29uc3QgeE92ZXJsYXAgPSBnZXRSZWN0T3ZlcmxhcFgocmVjdDEsIHJlY3QyKTtcbiAgICAgICAgICBjb25zb2xlLmxvZyhgeE92ZXJsYXA6ICR7eE92ZXJsYXB9YCk7XG4gICAgICAgICAgY29uc3QgeU92ZXJsYXAgPSBnZXRSZWN0T3ZlcmxhcFkocmVjdDEsIHJlY3QyKTtcbiAgICAgICAgICBjb25zb2xlLmxvZyhgeU92ZXJsYXA6ICR7eU92ZXJsYXB9YCk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgaWYgKHN0aWxsT3ZlcmxhcGluZ1JlY3RzLmxlbmd0aCkge1xuICAgIGNvbnNvbGUubG9nKGBDTElFTlQgUkVDVDogb3ZlcmxhcHMgJHtzdGlsbE92ZXJsYXBpbmdSZWN0cy5sZW5ndGh9YCk7XG4gIH1cbn1cblxuZnVuY3Rpb24gcmVtb3ZlQ29udGFpbmVkUmVjdHMocmVjdHMsIHRvbGVyYW5jZSkge1xuICBjb25zdCByZWN0c1RvS2VlcCA9IG5ldyBTZXQocmVjdHMpO1xuICBmb3IgKGNvbnN0IHJlY3Qgb2YgcmVjdHMpIHtcbiAgICBjb25zdCBiaWdFbm91Z2ggPSByZWN0LndpZHRoID4gMSAmJiByZWN0LmhlaWdodCA+IDE7XG4gICAgaWYgKCFiaWdFbm91Z2gpIHtcbiAgICAgIGlmIChJU19ERVYpIHtcbiAgICAgICAgY29uc29sZS5sb2coXCJDTElFTlQgUkVDVDogcmVtb3ZlIHRpbnlcIik7XG4gICAgICB9XG4gICAgICByZWN0c1RvS2VlcC5kZWxldGUocmVjdCk7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG4gICAgZm9yIChjb25zdCBwb3NzaWJseUNvbnRhaW5pbmdSZWN0IG9mIHJlY3RzKSB7XG4gICAgICBpZiAocmVjdCA9PT0gcG9zc2libHlDb250YWluaW5nUmVjdCkge1xuICAgICAgICBjb250aW51ZTtcbiAgICAgIH1cbiAgICAgIGlmICghcmVjdHNUb0tlZXAuaGFzKHBvc3NpYmx5Q29udGFpbmluZ1JlY3QpKSB7XG4gICAgICAgIGNvbnRpbnVlO1xuICAgICAgfVxuICAgICAgaWYgKHJlY3RDb250YWlucyhwb3NzaWJseUNvbnRhaW5pbmdSZWN0LCByZWN0LCB0b2xlcmFuY2UpKSB7XG4gICAgICAgIGlmIChJU19ERVYpIHtcbiAgICAgICAgICBjb25zb2xlLmxvZyhcIkNMSUVOVCBSRUNUOiByZW1vdmUgY29udGFpbmVkXCIpO1xuICAgICAgICB9XG4gICAgICAgIHJlY3RzVG9LZWVwLmRlbGV0ZShyZWN0KTtcbiAgICAgICAgYnJlYWs7XG4gICAgICB9XG4gICAgfVxuICB9XG4gIHJldHVybiBBcnJheS5mcm9tKHJlY3RzVG9LZWVwKTtcbn1cblxuZnVuY3Rpb24gYWxtb3N0RXF1YWwoYSwgYiwgdG9sZXJhbmNlKSB7XG4gIHJldHVybiBNYXRoLmFicyhhIC0gYikgPD0gdG9sZXJhbmNlO1xufVxuXG5mdW5jdGlvbiByZWN0SW50ZXJzZWN0KHJlY3QxLCByZWN0Mikge1xuICBjb25zdCBtYXhMZWZ0ID0gTWF0aC5tYXgocmVjdDEubGVmdCwgcmVjdDIubGVmdCk7XG4gIGNvbnN0IG1pblJpZ2h0ID0gTWF0aC5taW4ocmVjdDEucmlnaHQsIHJlY3QyLnJpZ2h0KTtcbiAgY29uc3QgbWF4VG9wID0gTWF0aC5tYXgocmVjdDEudG9wLCByZWN0Mi50b3ApO1xuICBjb25zdCBtaW5Cb3R0b20gPSBNYXRoLm1pbihyZWN0MS5ib3R0b20sIHJlY3QyLmJvdHRvbSk7XG4gIGNvbnN0IHJlY3QgPSB7XG4gICAgYm90dG9tOiBtaW5Cb3R0b20sXG4gICAgaGVpZ2h0OiBNYXRoLm1heCgwLCBtaW5Cb3R0b20gLSBtYXhUb3ApLFxuICAgIGxlZnQ6IG1heExlZnQsXG4gICAgcmlnaHQ6IG1pblJpZ2h0LFxuICAgIHRvcDogbWF4VG9wLFxuICAgIHdpZHRoOiBNYXRoLm1heCgwLCBtaW5SaWdodCAtIG1heExlZnQpLFxuICB9O1xuICByZXR1cm4gcmVjdDtcbn1cblxuZnVuY3Rpb24gcmVjdFN1YnRyYWN0KHJlY3QxLCByZWN0Mikge1xuICBjb25zdCByZWN0SW50ZXJzZWN0ZWQgPSByZWN0SW50ZXJzZWN0KHJlY3QyLCByZWN0MSk7XG4gIGlmIChyZWN0SW50ZXJzZWN0ZWQuaGVpZ2h0ID09PSAwIHx8IHJlY3RJbnRlcnNlY3RlZC53aWR0aCA9PT0gMCkge1xuICAgIHJldHVybiBbcmVjdDFdO1xuICB9XG4gIGNvbnN0IHJlY3RzID0gW107XG4gIHtcbiAgICBjb25zdCByZWN0QSA9IHtcbiAgICAgIGJvdHRvbTogcmVjdDEuYm90dG9tLFxuICAgICAgaGVpZ2h0OiAwLFxuICAgICAgbGVmdDogcmVjdDEubGVmdCxcbiAgICAgIHJpZ2h0OiByZWN0SW50ZXJzZWN0ZWQubGVmdCxcbiAgICAgIHRvcDogcmVjdDEudG9wLFxuICAgICAgd2lkdGg6IDAsXG4gICAgfTtcbiAgICByZWN0QS53aWR0aCA9IHJlY3RBLnJpZ2h0IC0gcmVjdEEubGVmdDtcbiAgICByZWN0QS5oZWlnaHQgPSByZWN0QS5ib3R0b20gLSByZWN0QS50b3A7XG4gICAgaWYgKHJlY3RBLmhlaWdodCAhPT0gMCAmJiByZWN0QS53aWR0aCAhPT0gMCkge1xuICAgICAgcmVjdHMucHVzaChyZWN0QSk7XG4gICAgfVxuICB9XG4gIHtcbiAgICBjb25zdCByZWN0QiA9IHtcbiAgICAgIGJvdHRvbTogcmVjdEludGVyc2VjdGVkLnRvcCxcbiAgICAgIGhlaWdodDogMCxcbiAgICAgIGxlZnQ6IHJlY3RJbnRlcnNlY3RlZC5sZWZ0LFxuICAgICAgcmlnaHQ6IHJlY3RJbnRlcnNlY3RlZC5yaWdodCxcbiAgICAgIHRvcDogcmVjdDEudG9wLFxuICAgICAgd2lkdGg6IDAsXG4gICAgfTtcbiAgICByZWN0Qi53aWR0aCA9IHJlY3RCLnJpZ2h0IC0gcmVjdEIubGVmdDtcbiAgICByZWN0Qi5oZWlnaHQgPSByZWN0Qi5ib3R0b20gLSByZWN0Qi50b3A7XG4gICAgaWYgKHJlY3RCLmhlaWdodCAhPT0gMCAmJiByZWN0Qi53aWR0aCAhPT0gMCkge1xuICAgICAgcmVjdHMucHVzaChyZWN0Qik7XG4gICAgfVxuICB9XG4gIHtcbiAgICBjb25zdCByZWN0QyA9IHtcbiAgICAgIGJvdHRvbTogcmVjdDEuYm90dG9tLFxuICAgICAgaGVpZ2h0OiAwLFxuICAgICAgbGVmdDogcmVjdEludGVyc2VjdGVkLmxlZnQsXG4gICAgICByaWdodDogcmVjdEludGVyc2VjdGVkLnJpZ2h0LFxuICAgICAgdG9wOiByZWN0SW50ZXJzZWN0ZWQuYm90dG9tLFxuICAgICAgd2lkdGg6IDAsXG4gICAgfTtcbiAgICByZWN0Qy53aWR0aCA9IHJlY3RDLnJpZ2h0IC0gcmVjdEMubGVmdDtcbiAgICByZWN0Qy5oZWlnaHQgPSByZWN0Qy5ib3R0b20gLSByZWN0Qy50b3A7XG4gICAgaWYgKHJlY3RDLmhlaWdodCAhPT0gMCAmJiByZWN0Qy53aWR0aCAhPT0gMCkge1xuICAgICAgcmVjdHMucHVzaChyZWN0Qyk7XG4gICAgfVxuICB9XG4gIHtcbiAgICBjb25zdCByZWN0RCA9IHtcbiAgICAgIGJvdHRvbTogcmVjdDEuYm90dG9tLFxuICAgICAgaGVpZ2h0OiAwLFxuICAgICAgbGVmdDogcmVjdEludGVyc2VjdGVkLnJpZ2h0LFxuICAgICAgcmlnaHQ6IHJlY3QxLnJpZ2h0LFxuICAgICAgdG9wOiByZWN0MS50b3AsXG4gICAgICB3aWR0aDogMCxcbiAgICB9O1xuICAgIHJlY3RELndpZHRoID0gcmVjdEQucmlnaHQgLSByZWN0RC5sZWZ0O1xuICAgIHJlY3RELmhlaWdodCA9IHJlY3RELmJvdHRvbSAtIHJlY3RELnRvcDtcbiAgICBpZiAocmVjdEQuaGVpZ2h0ICE9PSAwICYmIHJlY3RELndpZHRoICE9PSAwKSB7XG4gICAgICByZWN0cy5wdXNoKHJlY3REKTtcbiAgICB9XG4gIH1cbiAgcmV0dXJuIHJlY3RzO1xufVxuXG5mdW5jdGlvbiByZWN0Q29udGFpbnNQb2ludChyZWN0LCB4LCB5LCB0b2xlcmFuY2UpIHtcbiAgcmV0dXJuIChcbiAgICAocmVjdC5sZWZ0IDwgeCB8fCBhbG1vc3RFcXVhbChyZWN0LmxlZnQsIHgsIHRvbGVyYW5jZSkpICYmXG4gICAgKHJlY3QucmlnaHQgPiB4IHx8IGFsbW9zdEVxdWFsKHJlY3QucmlnaHQsIHgsIHRvbGVyYW5jZSkpICYmXG4gICAgKHJlY3QudG9wIDwgeSB8fCBhbG1vc3RFcXVhbChyZWN0LnRvcCwgeSwgdG9sZXJhbmNlKSkgJiZcbiAgICAocmVjdC5ib3R0b20gPiB5IHx8IGFsbW9zdEVxdWFsKHJlY3QuYm90dG9tLCB5LCB0b2xlcmFuY2UpKVxuICApO1xufVxuXG5mdW5jdGlvbiByZWN0Q29udGFpbnMocmVjdDEsIHJlY3QyLCB0b2xlcmFuY2UpIHtcbiAgcmV0dXJuIChcbiAgICByZWN0Q29udGFpbnNQb2ludChyZWN0MSwgcmVjdDIubGVmdCwgcmVjdDIudG9wLCB0b2xlcmFuY2UpICYmXG4gICAgcmVjdENvbnRhaW5zUG9pbnQocmVjdDEsIHJlY3QyLnJpZ2h0LCByZWN0Mi50b3AsIHRvbGVyYW5jZSkgJiZcbiAgICByZWN0Q29udGFpbnNQb2ludChyZWN0MSwgcmVjdDIubGVmdCwgcmVjdDIuYm90dG9tLCB0b2xlcmFuY2UpICYmXG4gICAgcmVjdENvbnRhaW5zUG9pbnQocmVjdDEsIHJlY3QyLnJpZ2h0LCByZWN0Mi5ib3R0b20sIHRvbGVyYW5jZSlcbiAgKTtcbn1cblxuZnVuY3Rpb24gZ2V0Qm91bmRpbmdSZWN0KHJlY3QxLCByZWN0Mikge1xuICBjb25zdCBsZWZ0ID0gTWF0aC5taW4ocmVjdDEubGVmdCwgcmVjdDIubGVmdCk7XG4gIGNvbnN0IHJpZ2h0ID0gTWF0aC5tYXgocmVjdDEucmlnaHQsIHJlY3QyLnJpZ2h0KTtcbiAgY29uc3QgdG9wID0gTWF0aC5taW4ocmVjdDEudG9wLCByZWN0Mi50b3ApO1xuICBjb25zdCBib3R0b20gPSBNYXRoLm1heChyZWN0MS5ib3R0b20sIHJlY3QyLmJvdHRvbSk7XG4gIHJldHVybiB7XG4gICAgYm90dG9tLFxuICAgIGhlaWdodDogYm90dG9tIC0gdG9wLFxuICAgIGxlZnQsXG4gICAgcmlnaHQsXG4gICAgdG9wLFxuICAgIHdpZHRoOiByaWdodCAtIGxlZnQsXG4gIH07XG59XG5cbmZ1bmN0aW9uIG1lcmdlVG91Y2hpbmdSZWN0cyhcbiAgcmVjdHMsXG4gIHRvbGVyYW5jZSxcbiAgZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0c1xuKSB7XG4gIGZvciAobGV0IGkgPSAwOyBpIDwgcmVjdHMubGVuZ3RoOyBpKyspIHtcbiAgICBmb3IgKGxldCBqID0gaSArIDE7IGogPCByZWN0cy5sZW5ndGg7IGorKykge1xuICAgICAgY29uc3QgcmVjdDEgPSByZWN0c1tpXTtcbiAgICAgIGNvbnN0IHJlY3QyID0gcmVjdHNbal07XG4gICAgICBpZiAocmVjdDEgPT09IHJlY3QyKSB7XG4gICAgICAgIGlmIChJU19ERVYpIHtcbiAgICAgICAgICBjb25zb2xlLmxvZyhcIm1lcmdlVG91Y2hpbmdSZWN0cyByZWN0MSA9PT0gcmVjdDIgPz8hXCIpO1xuICAgICAgICB9XG4gICAgICAgIGNvbnRpbnVlO1xuICAgICAgfVxuICAgICAgY29uc3QgcmVjdHNMaW5lVXBWZXJ0aWNhbGx5ID1cbiAgICAgICAgYWxtb3N0RXF1YWwocmVjdDEudG9wLCByZWN0Mi50b3AsIHRvbGVyYW5jZSkgJiZcbiAgICAgICAgYWxtb3N0RXF1YWwocmVjdDEuYm90dG9tLCByZWN0Mi5ib3R0b20sIHRvbGVyYW5jZSk7XG4gICAgICBjb25zdCByZWN0c0xpbmVVcEhvcml6b250YWxseSA9XG4gICAgICAgIGFsbW9zdEVxdWFsKHJlY3QxLmxlZnQsIHJlY3QyLmxlZnQsIHRvbGVyYW5jZSkgJiZcbiAgICAgICAgYWxtb3N0RXF1YWwocmVjdDEucmlnaHQsIHJlY3QyLnJpZ2h0LCB0b2xlcmFuY2UpO1xuICAgICAgY29uc3QgaG9yaXpvbnRhbEFsbG93ZWQgPSAhZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0cztcbiAgICAgIGNvbnN0IGFsaWduZWQgPVxuICAgICAgICAocmVjdHNMaW5lVXBIb3Jpem9udGFsbHkgJiYgaG9yaXpvbnRhbEFsbG93ZWQpIHx8XG4gICAgICAgIChyZWN0c0xpbmVVcFZlcnRpY2FsbHkgJiYgIXJlY3RzTGluZVVwSG9yaXpvbnRhbGx5KTtcbiAgICAgIGNvbnN0IGNhbk1lcmdlID0gYWxpZ25lZCAmJiByZWN0c1RvdWNoT3JPdmVybGFwKHJlY3QxLCByZWN0MiwgdG9sZXJhbmNlKTtcbiAgICAgIGlmIChjYW5NZXJnZSkge1xuICAgICAgICBpZiAoSVNfREVWKSB7XG4gICAgICAgICAgY29uc29sZS5sb2coXG4gICAgICAgICAgICBgQ0xJRU5UIFJFQ1Q6IG1lcmdpbmcgdHdvIGludG8gb25lLCBWRVJUSUNBTDogJHtyZWN0c0xpbmVVcFZlcnRpY2FsbHl9IEhPUklaT05UQUw6ICR7cmVjdHNMaW5lVXBIb3Jpem9udGFsbHl9ICgke2RvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHN9KWBcbiAgICAgICAgICApO1xuICAgICAgICB9XG4gICAgICAgIGNvbnN0IG5ld1JlY3RzID0gcmVjdHMuZmlsdGVyKChyZWN0KSA9PiB7XG4gICAgICAgICAgcmV0dXJuIHJlY3QgIT09IHJlY3QxICYmIHJlY3QgIT09IHJlY3QyO1xuICAgICAgICB9KTtcbiAgICAgICAgY29uc3QgcmVwbGFjZW1lbnRDbGllbnRSZWN0ID0gZ2V0Qm91bmRpbmdSZWN0KHJlY3QxLCByZWN0Mik7XG4gICAgICAgIG5ld1JlY3RzLnB1c2gocmVwbGFjZW1lbnRDbGllbnRSZWN0KTtcbiAgICAgICAgcmV0dXJuIG1lcmdlVG91Y2hpbmdSZWN0cyhcbiAgICAgICAgICBuZXdSZWN0cyxcbiAgICAgICAgICB0b2xlcmFuY2UsXG4gICAgICAgICAgZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0c1xuICAgICAgICApO1xuICAgICAgfVxuICAgIH1cbiAgfVxuICByZXR1cm4gcmVjdHM7XG59XG5cbmZ1bmN0aW9uIGdldENsaWVudFJlY3RzTm9PdmVybGFwKHJhbmdlLCBkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzKSB7XG4gIGNvbnN0IHJhbmdlQ2xpZW50UmVjdHMgPSByYW5nZS5nZXRDbGllbnRSZWN0cygpO1xuICByZXR1cm4gZ2V0Q2xpZW50UmVjdHNOb092ZXJsYXBfKFxuICAgIHJhbmdlQ2xpZW50UmVjdHMsXG4gICAgZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0c1xuICApO1xufVxuXG5mdW5jdGlvbiBnZXRDbGllbnRSZWN0c05vT3ZlcmxhcF8oXG4gIGNsaWVudFJlY3RzLFxuICBkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzXG4pIHtcbiAgY29uc3QgdG9sZXJhbmNlID0gMTtcbiAgY29uc3Qgb3JpZ2luYWxSZWN0cyA9IFtdO1xuICBmb3IgKGNvbnN0IHJhbmdlQ2xpZW50UmVjdCBvZiBjbGllbnRSZWN0cykge1xuICAgIG9yaWdpbmFsUmVjdHMucHVzaCh7XG4gICAgICBib3R0b206IHJhbmdlQ2xpZW50UmVjdC5ib3R0b20sXG4gICAgICBoZWlnaHQ6IHJhbmdlQ2xpZW50UmVjdC5oZWlnaHQsXG4gICAgICBsZWZ0OiByYW5nZUNsaWVudFJlY3QubGVmdCxcbiAgICAgIHJpZ2h0OiByYW5nZUNsaWVudFJlY3QucmlnaHQsXG4gICAgICB0b3A6IHJhbmdlQ2xpZW50UmVjdC50b3AsXG4gICAgICB3aWR0aDogcmFuZ2VDbGllbnRSZWN0LndpZHRoLFxuICAgIH0pO1xuICB9XG4gIGNvbnN0IG1lcmdlZFJlY3RzID0gbWVyZ2VUb3VjaGluZ1JlY3RzKFxuICAgIG9yaWdpbmFsUmVjdHMsXG4gICAgdG9sZXJhbmNlLFxuICAgIGRvTm90TWVyZ2VIb3Jpem9udGFsbHlBbGlnbmVkUmVjdHNcbiAgKTtcbiAgY29uc3Qgbm9Db250YWluZWRSZWN0cyA9IHJlbW92ZUNvbnRhaW5lZFJlY3RzKG1lcmdlZFJlY3RzLCB0b2xlcmFuY2UpO1xuICBjb25zdCBuZXdSZWN0cyA9IHJlcGxhY2VPdmVybGFwaW5nUmVjdHMobm9Db250YWluZWRSZWN0cyk7XG4gIGNvbnN0IG1pbkFyZWEgPSAyICogMjtcbiAgZm9yIChsZXQgaiA9IG5ld1JlY3RzLmxlbmd0aCAtIDE7IGogPj0gMDsgai0tKSB7XG4gICAgY29uc3QgcmVjdCA9IG5ld1JlY3RzW2pdO1xuICAgIGNvbnN0IGJpZ0Vub3VnaCA9IHJlY3Qud2lkdGggKiByZWN0LmhlaWdodCA+IG1pbkFyZWE7XG4gICAgaWYgKCFiaWdFbm91Z2gpIHtcbiAgICAgIGlmIChuZXdSZWN0cy5sZW5ndGggPiAxKSB7XG4gICAgICAgIGlmIChJU19ERVYpIHtcbiAgICAgICAgICBjb25zb2xlLmxvZyhcIkNMSUVOVCBSRUNUOiByZW1vdmUgc21hbGxcIik7XG4gICAgICAgIH1cbiAgICAgICAgbmV3UmVjdHMuc3BsaWNlKGosIDEpO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgaWYgKElTX0RFVikge1xuICAgICAgICAgIGNvbnNvbGUubG9nKFwiQ0xJRU5UIFJFQ1Q6IHJlbW92ZSBzbWFsbCwgYnV0IGtlZXAgb3RoZXJ3aXNlIGVtcHR5IVwiKTtcbiAgICAgICAgfVxuICAgICAgICBicmVhaztcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgaWYgKElTX0RFVikge1xuICAgIGNoZWNrT3ZlcmxhcHMobmV3UmVjdHMpO1xuICB9XG4gIGlmIChJU19ERVYpIHtcbiAgICBjb25zb2xlLmxvZyhcbiAgICAgIGBDTElFTlQgUkVDVDogcmVkdWNlZCAke29yaWdpbmFsUmVjdHMubGVuZ3RofSAtLT4gJHtuZXdSZWN0cy5sZW5ndGh9YFxuICAgICk7XG4gIH1cbiAgcmV0dXJuIG5ld1JlY3RzO1xufVxuXG5mdW5jdGlvbiBpc1BhZ2luYXRlZChkb2N1bWVudCkge1xuICByZXR1cm4gKFxuICAgIGRvY3VtZW50ICYmXG4gICAgZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50ICYmXG4gICAgZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LmNsYXNzTGlzdC5jb250YWlucyhDTEFTU19QQUdJTkFURUQpXG4gICk7XG59XG5cbmZ1bmN0aW9uIGdldFNjcm9sbGluZ0VsZW1lbnQoZG9jdW1lbnQpIHtcbiAgaWYgKGRvY3VtZW50LnNjcm9sbGluZ0VsZW1lbnQpIHtcbiAgICByZXR1cm4gZG9jdW1lbnQuc2Nyb2xsaW5nRWxlbWVudDtcbiAgfVxuICByZXR1cm4gZG9jdW1lbnQuYm9keTtcbn1cblxuZnVuY3Rpb24gZW5zdXJlQ29udGFpbmVyKHdpbiwgYW5ub3RhdGlvbkZsYWcpIHtcbiAgY29uc3QgZG9jdW1lbnQgPSB3aW4uZG9jdW1lbnQ7XG5cbiAgaWYgKCFfaGlnaGxpZ2h0c0NvbnRhaW5lcikge1xuICAgIGlmICghYm9keUV2ZW50TGlzdGVuZXJzU2V0KSB7XG4gICAgICBib2R5RXZlbnRMaXN0ZW5lcnNTZXQgPSB0cnVlO1xuICAgICAgZG9jdW1lbnQuYm9keS5hZGRFdmVudExpc3RlbmVyKFxuICAgICAgICBcIm1vdXNlZG93blwiLFxuICAgICAgICAoZXYpID0+IHtcbiAgICAgICAgICBsYXN0TW91c2VEb3duWCA9IGV2LmNsaWVudFg7XG4gICAgICAgICAgbGFzdE1vdXNlRG93blkgPSBldi5jbGllbnRZO1xuICAgICAgICB9LFxuICAgICAgICBmYWxzZVxuICAgICAgKTtcbiAgICAgIGRvY3VtZW50LmJvZHkuYWRkRXZlbnRMaXN0ZW5lcihcbiAgICAgICAgXCJtb3VzZXVwXCIsXG4gICAgICAgIChldikgPT4ge1xuICAgICAgICAgIGlmIChcbiAgICAgICAgICAgIE1hdGguYWJzKGxhc3RNb3VzZURvd25YIC0gZXYuY2xpZW50WCkgPCAzICYmXG4gICAgICAgICAgICBNYXRoLmFicyhsYXN0TW91c2VEb3duWSAtIGV2LmNsaWVudFkpIDwgM1xuICAgICAgICAgICkge1xuICAgICAgICAgICAgcHJvY2Vzc01vdXNlRXZlbnQod2luLCBldik7XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICBmYWxzZVxuICAgICAgKTtcbiAgICAgIGRvY3VtZW50LmJvZHkuYWRkRXZlbnRMaXN0ZW5lcihcbiAgICAgICAgXCJtb3VzZW1vdmVcIixcbiAgICAgICAgKGV2KSA9PiB7XG4gICAgICAgICAgcHJvY2Vzc01vdXNlRXZlbnQod2luLCBldik7XG4gICAgICAgIH0sXG4gICAgICAgIGZhbHNlXG4gICAgICApO1xuXG4gICAgICBkb2N1bWVudC5ib2R5LmFkZEV2ZW50TGlzdGVuZXIoXG4gICAgICAgIFwidG91Y2hlbmRcIixcbiAgICAgICAgZnVuY3Rpb24gdG91Y2hFbmQoZSkge1xuICAgICAgICAgIHByb2Nlc3NUb3VjaEV2ZW50KHdpbiwgZSk7XG4gICAgICAgIH0sXG4gICAgICAgIGZhbHNlXG4gICAgICApO1xuICAgIH1cbiAgICBfaGlnaGxpZ2h0c0NvbnRhaW5lciA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoXCJkaXZcIik7XG4gICAgX2hpZ2hsaWdodHNDb250YWluZXIuc2V0QXR0cmlidXRlKFwiaWRcIiwgSURfSElHSExJR0hUU19DT05UQUlORVIpO1xuXG4gICAgX2hpZ2hsaWdodHNDb250YWluZXIuc3R5bGUuc2V0UHJvcGVydHkoXCJwb2ludGVyLWV2ZW50c1wiLCBcIm5vbmVcIik7XG4gICAgZG9jdW1lbnQuYm9keS5hcHBlbmQoX2hpZ2hsaWdodHNDb250YWluZXIpO1xuICB9XG5cbiAgcmV0dXJuIF9oaWdobGlnaHRzQ29udGFpbmVyO1xufVxuXG5mdW5jdGlvbiBoaWRlQWxsaGlnaGxpZ2h0cygpIHtcbiAgaWYgKF9oaWdobGlnaHRzQ29udGFpbmVyKSB7XG4gICAgX2hpZ2hsaWdodHNDb250YWluZXIucmVtb3ZlKCk7XG4gICAgX2hpZ2hsaWdodHNDb250YWluZXIgPSBudWxsO1xuICB9XG59XG5cbmZ1bmN0aW9uIGRlc3Ryb3lBbGxoaWdobGlnaHRzKCkge1xuICBoaWRlQWxsaGlnaGxpZ2h0cygpO1xuICBfaGlnaGxpZ2h0cy5zcGxpY2UoMCwgX2hpZ2hsaWdodHMubGVuZ3RoKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGRlc3Ryb3lIaWdobGlnaHQoaWQpIHtcbiAgbGV0IGkgPSAtMTtcbiAgbGV0IF9kb2N1bWVudCA9IHdpbmRvdy5kb2N1bWVudDtcbiAgY29uc3QgaGlnaGxpZ2h0ID0gX2hpZ2hsaWdodHMuZmluZCgoaCwgaikgPT4ge1xuICAgIGkgPSBqO1xuICAgIHJldHVybiBoLmlkID09PSBpZDtcbiAgfSk7XG4gIGlmIChoaWdobGlnaHQgJiYgaSA+PSAwICYmIGkgPCBfaGlnaGxpZ2h0cy5sZW5ndGgpIHtcbiAgICBfaGlnaGxpZ2h0cy5zcGxpY2UoaSwgMSk7XG4gIH1cbiAgY29uc3QgaGlnaGxpZ2h0Q29udGFpbmVyID0gX2RvY3VtZW50LmdldEVsZW1lbnRCeUlkKGlkKTtcbiAgaWYgKGhpZ2hsaWdodENvbnRhaW5lcikge1xuICAgIGhpZ2hsaWdodENvbnRhaW5lci5yZW1vdmUoKTtcbiAgfVxufVxuXG5mdW5jdGlvbiBpc0NmaVRleHROb2RlKG5vZGUpIHtcbiAgcmV0dXJuIG5vZGUubm9kZVR5cGUgIT09IE5vZGUuRUxFTUVOVF9OT0RFO1xufVxuXG5mdW5jdGlvbiBnZXRDaGlsZFRleHROb2RlQ2ZpSW5kZXgoZWxlbWVudCwgY2hpbGQpIHtcbiAgbGV0IGZvdW5kID0gLTE7XG4gIGxldCB0ZXh0Tm9kZUluZGV4ID0gLTE7XG4gIGxldCBwcmV2aW91c1dhc0VsZW1lbnQgPSBmYWxzZTtcbiAgZm9yIChsZXQgaSA9IDA7IGkgPCBlbGVtZW50LmNoaWxkTm9kZXMubGVuZ3RoOyBpKyspIHtcbiAgICBjb25zdCBjaGlsZE5vZGUgPSBlbGVtZW50LmNoaWxkTm9kZXNbaV07XG4gICAgY29uc3QgaXNUZXh0ID0gaXNDZmlUZXh0Tm9kZShjaGlsZE5vZGUpO1xuICAgIGlmIChpc1RleHQgfHwgcHJldmlvdXNXYXNFbGVtZW50KSB7XG4gICAgICB0ZXh0Tm9kZUluZGV4ICs9IDI7XG4gICAgfVxuICAgIGlmIChpc1RleHQpIHtcbiAgICAgIGlmIChjaGlsZE5vZGUgPT09IGNoaWxkKSB7XG4gICAgICAgIGZvdW5kID0gdGV4dE5vZGVJbmRleDtcbiAgICAgICAgYnJlYWs7XG4gICAgICB9XG4gICAgfVxuICAgIHByZXZpb3VzV2FzRWxlbWVudCA9IGNoaWxkTm9kZS5ub2RlVHlwZSA9PT0gTm9kZS5FTEVNRU5UX05PREU7XG4gIH1cbiAgcmV0dXJuIGZvdW5kO1xufVxuXG5mdW5jdGlvbiBnZXRDb21tb25BbmNlc3RvckVsZW1lbnQobm9kZTEsIG5vZGUyKSB7XG4gIGlmIChub2RlMS5ub2RlVHlwZSA9PT0gTm9kZS5FTEVNRU5UX05PREUgJiYgbm9kZTEgPT09IG5vZGUyKSB7XG4gICAgcmV0dXJuIG5vZGUxO1xuICB9XG4gIGlmIChub2RlMS5ub2RlVHlwZSA9PT0gTm9kZS5FTEVNRU5UX05PREUgJiYgbm9kZTEuY29udGFpbnMobm9kZTIpKSB7XG4gICAgcmV0dXJuIG5vZGUxO1xuICB9XG4gIGlmIChub2RlMi5ub2RlVHlwZSA9PT0gTm9kZS5FTEVNRU5UX05PREUgJiYgbm9kZTIuY29udGFpbnMobm9kZTEpKSB7XG4gICAgcmV0dXJuIG5vZGUyO1xuICB9XG4gIGNvbnN0IG5vZGUxRWxlbWVudEFuY2VzdG9yQ2hhaW4gPSBbXTtcbiAgbGV0IHBhcmVudCA9IG5vZGUxLnBhcmVudE5vZGU7XG4gIHdoaWxlIChwYXJlbnQgJiYgcGFyZW50Lm5vZGVUeXBlID09PSBOb2RlLkVMRU1FTlRfTk9ERSkge1xuICAgIG5vZGUxRWxlbWVudEFuY2VzdG9yQ2hhaW4ucHVzaChwYXJlbnQpO1xuICAgIHBhcmVudCA9IHBhcmVudC5wYXJlbnROb2RlO1xuICB9XG4gIGNvbnN0IG5vZGUyRWxlbWVudEFuY2VzdG9yQ2hhaW4gPSBbXTtcbiAgcGFyZW50ID0gbm9kZTIucGFyZW50Tm9kZTtcbiAgd2hpbGUgKHBhcmVudCAmJiBwYXJlbnQubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFKSB7XG4gICAgbm9kZTJFbGVtZW50QW5jZXN0b3JDaGFpbi5wdXNoKHBhcmVudCk7XG4gICAgcGFyZW50ID0gcGFyZW50LnBhcmVudE5vZGU7XG4gIH1cbiAgbGV0IGNvbW1vbkFuY2VzdG9yID0gbm9kZTFFbGVtZW50QW5jZXN0b3JDaGFpbi5maW5kKFxuICAgIChub2RlMUVsZW1lbnRBbmNlc3RvcikgPT4ge1xuICAgICAgcmV0dXJuIG5vZGUyRWxlbWVudEFuY2VzdG9yQ2hhaW4uaW5kZXhPZihub2RlMUVsZW1lbnRBbmNlc3RvcikgPj0gMDtcbiAgICB9XG4gICk7XG4gIGlmICghY29tbW9uQW5jZXN0b3IpIHtcbiAgICBjb21tb25BbmNlc3RvciA9IG5vZGUyRWxlbWVudEFuY2VzdG9yQ2hhaW4uZmluZCgobm9kZTJFbGVtZW50QW5jZXN0b3IpID0+IHtcbiAgICAgIHJldHVybiBub2RlMUVsZW1lbnRBbmNlc3RvckNoYWluLmluZGV4T2Yobm9kZTJFbGVtZW50QW5jZXN0b3IpID49IDA7XG4gICAgfSk7XG4gIH1cbiAgcmV0dXJuIGNvbW1vbkFuY2VzdG9yO1xufVxuXG5mdW5jdGlvbiBmdWxsUXVhbGlmaWVkU2VsZWN0b3Iobm9kZSkge1xuICBpZiAobm9kZS5ub2RlVHlwZSAhPT0gTm9kZS5FTEVNRU5UX05PREUpIHtcbiAgICBjb25zdCBsb3dlckNhc2VOYW1lID1cbiAgICAgIChub2RlLmxvY2FsTmFtZSAmJiBub2RlLmxvY2FsTmFtZS50b0xvd2VyQ2FzZSgpKSB8fFxuICAgICAgbm9kZS5ub2RlTmFtZS50b0xvd2VyQ2FzZSgpO1xuICAgIHJldHVybiBsb3dlckNhc2VOYW1lO1xuICB9XG4gIC8vcmV0dXJuIGNzc1BhdGgobm9kZSwganVzdFNlbGVjdG9yKTtcbiAgcmV0dXJuIGNzc1BhdGgobm9kZSwgdHJ1ZSk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBnZXRDdXJyZW50U2VsZWN0aW9uSW5mbygpIHtcbiAgY29uc3Qgc2VsZWN0aW9uID0gd2luZG93LmdldFNlbGVjdGlvbigpO1xuICBpZiAoIXNlbGVjdGlvbikge1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgaWYgKHNlbGVjdGlvbi5pc0NvbGxhcHNlZCkge1xuICAgIGNvbnNvbGUubG9nKFwiXl5eIFNFTEVDVElPTiBDT0xMQVBTRUQuXCIpO1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgY29uc3QgcmF3VGV4dCA9IHNlbGVjdGlvbi50b1N0cmluZygpO1xuICBjb25zdCBjbGVhblRleHQgPSByYXdUZXh0LnRyaW0oKS5yZXBsYWNlKC9cXG4vZywgXCIgXCIpLnJlcGxhY2UoL1xcc1xccysvZywgXCIgXCIpO1xuICBpZiAoY2xlYW5UZXh0Lmxlbmd0aCA9PT0gMCkge1xuICAgIGNvbnNvbGUubG9nKFwiXl5eIFNFTEVDVElPTiBURVhUIEVNUFRZLlwiKTtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG4gIGlmICghc2VsZWN0aW9uLmFuY2hvck5vZGUgfHwgIXNlbGVjdGlvbi5mb2N1c05vZGUpIHtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG4gIGNvbnN0IHJhbmdlID1cbiAgICBzZWxlY3Rpb24ucmFuZ2VDb3VudCA9PT0gMVxuICAgICAgPyBzZWxlY3Rpb24uZ2V0UmFuZ2VBdCgwKVxuICAgICAgOiBjcmVhdGVPcmRlcmVkUmFuZ2UoXG4gICAgICAgICAgc2VsZWN0aW9uLmFuY2hvck5vZGUsXG4gICAgICAgICAgc2VsZWN0aW9uLmFuY2hvck9mZnNldCxcbiAgICAgICAgICBzZWxlY3Rpb24uZm9jdXNOb2RlLFxuICAgICAgICAgIHNlbGVjdGlvbi5mb2N1c09mZnNldFxuICAgICAgICApO1xuICBpZiAoIXJhbmdlIHx8IHJhbmdlLmNvbGxhcHNlZCkge1xuICAgIGNvbnNvbGUubG9nKFwiJCQkJCQkJCQkJCQkJCQkJCQgQ0FOTk9UIEdFVCBOT04tQ09MTEFQU0VEIFNFTEVDVElPTiBSQU5HRT8hXCIpO1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgY29uc3QgcmFuZ2VJbmZvID0gY29udmVydFJhbmdlKHJhbmdlLCBmdWxsUXVhbGlmaWVkU2VsZWN0b3IsIGNvbXB1dGVDRkkpO1xuICBpZiAoIXJhbmdlSW5mbykge1xuICAgIGNvbnNvbGUubG9nKFwiXl5eIFNFTEVDVElPTiBSQU5HRSBJTkZPIEZBSUw/IVwiKTtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG5cbiAgaWYgKElTX0RFViAmJiBERUJVR19WSVNVQUxTKSB7XG4gICAgY29uc3QgcmVzdG9yZWRSYW5nZSA9IGNvbnZlcnRSYW5nZUluZm8od2luLmRvY3VtZW50LCByYW5nZUluZm8pO1xuICAgIGlmIChyZXN0b3JlZFJhbmdlKSB7XG4gICAgICBpZiAoXG4gICAgICAgIHJlc3RvcmVkUmFuZ2Uuc3RhcnRPZmZzZXQgPT09IHJhbmdlLnN0YXJ0T2Zmc2V0ICYmXG4gICAgICAgIHJlc3RvcmVkUmFuZ2UuZW5kT2Zmc2V0ID09PSByYW5nZS5lbmRPZmZzZXQgJiZcbiAgICAgICAgcmVzdG9yZWRSYW5nZS5zdGFydENvbnRhaW5lciA9PT0gcmFuZ2Uuc3RhcnRDb250YWluZXIgJiZcbiAgICAgICAgcmVzdG9yZWRSYW5nZS5lbmRDb250YWluZXIgPT09IHJhbmdlLmVuZENvbnRhaW5lclxuICAgICAgKSB7XG4gICAgICAgIGNvbnNvbGUubG9nKFwiU0VMRUNUSU9OIFJBTkdFIFJFU1RPUkVEIE9LQVkgKGRldiBjaGVjaykuXCIpO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgY29uc29sZS5sb2coXCJTRUxFQ1RJT04gUkFOR0UgUkVTVE9SRSBGQUlMIChkZXYgY2hlY2spLlwiKTtcbiAgICAgICAgZHVtcERlYnVnKFxuICAgICAgICAgIFwiU0VMRUNUSU9OXCIsXG4gICAgICAgICAgc2VsZWN0aW9uLmFuY2hvck5vZGUsXG4gICAgICAgICAgc2VsZWN0aW9uLmFuY2hvck9mZnNldCxcbiAgICAgICAgICBzZWxlY3Rpb24uZm9jdXNOb2RlLFxuICAgICAgICAgIHNlbGVjdGlvbi5mb2N1c09mZnNldCxcbiAgICAgICAgICBnZXRDc3NTZWxlY3RvclxuICAgICAgICApO1xuICAgICAgICBkdW1wRGVidWcoXG4gICAgICAgICAgXCJPUkRFUkVEIFJBTkdFIEZST00gU0VMRUNUSU9OXCIsXG4gICAgICAgICAgcmFuZ2Uuc3RhcnRDb250YWluZXIsXG4gICAgICAgICAgcmFuZ2Uuc3RhcnRPZmZzZXQsXG4gICAgICAgICAgcmFuZ2UuZW5kQ29udGFpbmVyLFxuICAgICAgICAgIHJhbmdlLmVuZE9mZnNldCxcbiAgICAgICAgICBnZXRDc3NTZWxlY3RvclxuICAgICAgICApO1xuICAgICAgICBkdW1wRGVidWcoXG4gICAgICAgICAgXCJSRVNUT1JFRCBSQU5HRVwiLFxuICAgICAgICAgIHJlc3RvcmVkUmFuZ2Uuc3RhcnRDb250YWluZXIsXG4gICAgICAgICAgcmVzdG9yZWRSYW5nZS5zdGFydE9mZnNldCxcbiAgICAgICAgICByZXN0b3JlZFJhbmdlLmVuZENvbnRhaW5lcixcbiAgICAgICAgICByZXN0b3JlZFJhbmdlLmVuZE9mZnNldCxcbiAgICAgICAgICBnZXRDc3NTZWxlY3RvclxuICAgICAgICApO1xuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICBjb25zb2xlLmxvZyhcIkNBTk5PVCBSRVNUT1JFIFNFTEVDVElPTiBSQU5HRSA/PyFcIik7XG4gICAgfVxuICB9IGVsc2Uge1xuICB9XG5cbiAgcmV0dXJuIHtcbiAgICBsb2NhdGlvbnM6IHJhbmdlSW5mbzJMb2NhdGlvbihyYW5nZUluZm8pLFxuICAgIHRleHQ6IHtcbiAgICAgIGhpZ2hsaWdodDogcmF3VGV4dCxcbiAgICB9LFxuICB9O1xufVxuXG5mdW5jdGlvbiBjaGVja0JsYWNrbGlzdGVkKGVsKSB7XG4gIGxldCBibGFja2xpc3RlZElkO1xuICBjb25zdCBpZCA9IGVsLmdldEF0dHJpYnV0ZShcImlkXCIpO1xuICBpZiAoaWQgJiYgX2JsYWNrbGlzdElkQ2xhc3NGb3JDRkkuaW5kZXhPZihpZCkgPj0gMCkge1xuICAgIGNvbnNvbGUubG9nKFwiY2hlY2tCbGFja2xpc3RlZCBJRDogXCIgKyBpZCk7XG4gICAgYmxhY2tsaXN0ZWRJZCA9IGlkO1xuICB9XG4gIGxldCBibGFja2xpc3RlZENsYXNzO1xuICBmb3IgKGNvbnN0IGl0ZW0gb2YgX2JsYWNrbGlzdElkQ2xhc3NGb3JDRkkpIHtcbiAgICBpZiAoZWwuY2xhc3NMaXN0LmNvbnRhaW5zKGl0ZW0pKSB7XG4gICAgICBjb25zb2xlLmxvZyhcImNoZWNrQmxhY2tsaXN0ZWQgQ0xBU1M6IFwiICsgaXRlbSk7XG4gICAgICBibGFja2xpc3RlZENsYXNzID0gaXRlbTtcbiAgICAgIGJyZWFrO1xuICAgIH1cbiAgfVxuICBpZiAoYmxhY2tsaXN0ZWRJZCB8fCBibGFja2xpc3RlZENsYXNzKSB7XG4gICAgcmV0dXJuIHRydWU7XG4gIH1cblxuICByZXR1cm4gZmFsc2U7XG59XG5cbmZ1bmN0aW9uIGNzc1BhdGgobm9kZSwgb3B0aW1pemVkKSB7XG4gIGlmIChub2RlLm5vZGVUeXBlICE9PSBOb2RlLkVMRU1FTlRfTk9ERSkge1xuICAgIHJldHVybiBcIlwiO1xuICB9XG5cbiAgY29uc3Qgc3RlcHMgPSBbXTtcbiAgbGV0IGNvbnRleHROb2RlID0gbm9kZTtcbiAgd2hpbGUgKGNvbnRleHROb2RlKSB7XG4gICAgY29uc3Qgc3RlcCA9IF9jc3NQYXRoU3RlcChjb250ZXh0Tm9kZSwgISFvcHRpbWl6ZWQsIGNvbnRleHROb2RlID09PSBub2RlKTtcbiAgICBpZiAoIXN0ZXApIHtcbiAgICAgIGJyZWFrOyAvLyBFcnJvciAtIGJhaWwgb3V0IGVhcmx5LlxuICAgIH1cbiAgICBzdGVwcy5wdXNoKHN0ZXAudmFsdWUpO1xuICAgIGlmIChzdGVwLm9wdGltaXplZCkge1xuICAgICAgYnJlYWs7XG4gICAgfVxuICAgIGNvbnRleHROb2RlID0gY29udGV4dE5vZGUucGFyZW50Tm9kZTtcbiAgfVxuICBzdGVwcy5yZXZlcnNlKCk7XG4gIHJldHVybiBzdGVwcy5qb2luKFwiID4gXCIpO1xufVxuLy8gdHNsaW50OmRpc2FibGUtbmV4dC1saW5lOm1heC1saW5lLWxlbmd0aFxuLy8gaHR0cHM6Ly9jaHJvbWl1bS5nb29nbGVzb3VyY2UuY29tL2Nocm9taXVtL2JsaW5rLysvbWFzdGVyL1NvdXJjZS9kZXZ0b29scy9mcm9udF9lbmQvY29tcG9uZW50cy9ET01QcmVzZW50YXRpb25VdGlscy5qcyMzMTZcbmZ1bmN0aW9uIF9jc3NQYXRoU3RlcChub2RlLCBvcHRpbWl6ZWQsIGlzVGFyZ2V0Tm9kZSkge1xuICBmdW5jdGlvbiBwcmVmaXhlZEVsZW1lbnRDbGFzc05hbWVzKG5kKSB7XG4gICAgY29uc3QgY2xhc3NBdHRyaWJ1dGUgPSBuZC5nZXRBdHRyaWJ1dGUoXCJjbGFzc1wiKTtcbiAgICBpZiAoIWNsYXNzQXR0cmlidXRlKSB7XG4gICAgICByZXR1cm4gW107XG4gICAgfVxuXG4gICAgcmV0dXJuIGNsYXNzQXR0cmlidXRlXG4gICAgICAuc3BsaXQoL1xccysvZylcbiAgICAgIC5maWx0ZXIoQm9vbGVhbilcbiAgICAgIC5tYXAoKG5tKSA9PiB7XG4gICAgICAgIC8vIFRoZSBwcmVmaXggaXMgcmVxdWlyZWQgdG8gc3RvcmUgXCJfX3Byb3RvX19cIiBpbiBhIG9iamVjdC1iYXNlZCBtYXAuXG4gICAgICAgIHJldHVybiBcIiRcIiArIG5tO1xuICAgICAgfSk7XG4gIH1cblxuICBmdW5jdGlvbiBpZFNlbGVjdG9yKGlkZCkge1xuICAgIHJldHVybiBcIiNcIiArIGVzY2FwZUlkZW50aWZpZXJJZk5lZWRlZChpZGQpO1xuICB9XG5cbiAgZnVuY3Rpb24gZXNjYXBlSWRlbnRpZmllcklmTmVlZGVkKGlkZW50KSB7XG4gICAgaWYgKGlzQ1NTSWRlbnRpZmllcihpZGVudCkpIHtcbiAgICAgIHJldHVybiBpZGVudDtcbiAgICB9XG5cbiAgICBjb25zdCBzaG91bGRFc2NhcGVGaXJzdCA9IC9eKD86WzAtOV18LVswLTktXT8pLy50ZXN0KGlkZW50KTtcbiAgICBjb25zdCBsYXN0SW5kZXggPSBpZGVudC5sZW5ndGggLSAxO1xuICAgIHJldHVybiBpZGVudC5yZXBsYWNlKC8uL2csIGZ1bmN0aW9uIChjLCBpaSkge1xuICAgICAgcmV0dXJuIChzaG91bGRFc2NhcGVGaXJzdCAmJiBpaSA9PT0gMCkgfHwgIWlzQ1NTSWRlbnRDaGFyKGMpXG4gICAgICAgID8gZXNjYXBlQXNjaWlDaGFyKGMsIGlpID09PSBsYXN0SW5kZXgpXG4gICAgICAgIDogYztcbiAgICB9KTtcbiAgfVxuXG4gIGZ1bmN0aW9uIGVzY2FwZUFzY2lpQ2hhcihjLCBpc0xhc3QpIHtcbiAgICByZXR1cm4gXCJcXFxcXCIgKyB0b0hleEJ5dGUoYykgKyAoaXNMYXN0ID8gXCJcIiA6IFwiIFwiKTtcbiAgfVxuXG4gIGZ1bmN0aW9uIHRvSGV4Qnl0ZShjKSB7XG4gICAgbGV0IGhleEJ5dGUgPSBjLmNoYXJDb2RlQXQoMCkudG9TdHJpbmcoMTYpO1xuICAgIGlmIChoZXhCeXRlLmxlbmd0aCA9PT0gMSkge1xuICAgICAgaGV4Qnl0ZSA9IFwiMFwiICsgaGV4Qnl0ZTtcbiAgICB9XG4gICAgcmV0dXJuIGhleEJ5dGU7XG4gIH1cblxuICBmdW5jdGlvbiBpc0NTU0lkZW50Q2hhcihjKSB7XG4gICAgaWYgKC9bYS16QS1aMC05Xy1dLy50ZXN0KGMpKSB7XG4gICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gICAgcmV0dXJuIGMuY2hhckNvZGVBdCgwKSA+PSAweGEwO1xuICB9XG5cbiAgZnVuY3Rpb24gaXNDU1NJZGVudGlmaWVyKHZhbHVlKSB7XG4gICAgcmV0dXJuIC9eLT9bYS16QS1aX11bYS16QS1aMC05Xy1dKiQvLnRlc3QodmFsdWUpO1xuICB9XG5cbiAgaWYgKG5vZGUubm9kZVR5cGUgIT09IE5vZGUuRUxFTUVOVF9OT0RFKSB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuICBjb25zdCBsb3dlckNhc2VOYW1lID1cbiAgICAobm9kZS5sb2NhbE5hbWUgJiYgbm9kZS5sb2NhbE5hbWUudG9Mb3dlckNhc2UoKSkgfHxcbiAgICBub2RlLm5vZGVOYW1lLnRvTG93ZXJDYXNlKCk7XG5cbiAgY29uc3QgZWxlbWVudCA9IG5vZGU7XG5cbiAgY29uc3QgaWQgPSBlbGVtZW50LmdldEF0dHJpYnV0ZShcImlkXCIpO1xuXG4gIGlmIChvcHRpbWl6ZWQpIHtcbiAgICBpZiAoaWQpIHtcbiAgICAgIHJldHVybiB7XG4gICAgICAgIG9wdGltaXplZDogdHJ1ZSxcbiAgICAgICAgdmFsdWU6IGlkU2VsZWN0b3IoaWQpLFxuICAgICAgfTtcbiAgICB9XG4gICAgaWYgKFxuICAgICAgbG93ZXJDYXNlTmFtZSA9PT0gXCJib2R5XCIgfHxcbiAgICAgIGxvd2VyQ2FzZU5hbWUgPT09IFwiaGVhZFwiIHx8XG4gICAgICBsb3dlckNhc2VOYW1lID09PSBcImh0bWxcIlxuICAgICkge1xuICAgICAgcmV0dXJuIHtcbiAgICAgICAgb3B0aW1pemVkOiB0cnVlLFxuICAgICAgICB2YWx1ZTogbG93ZXJDYXNlTmFtZSwgLy8gbm9kZS5ub2RlTmFtZUluQ29ycmVjdENhc2UoKSxcbiAgICAgIH07XG4gICAgfVxuICB9XG5cbiAgY29uc3Qgbm9kZU5hbWUgPSBsb3dlckNhc2VOYW1lOyAvLyBub2RlLm5vZGVOYW1lSW5Db3JyZWN0Q2FzZSgpO1xuICBpZiAoaWQpIHtcbiAgICByZXR1cm4ge1xuICAgICAgb3B0aW1pemVkOiB0cnVlLFxuICAgICAgdmFsdWU6IG5vZGVOYW1lICsgaWRTZWxlY3RvcihpZCksXG4gICAgfTtcbiAgfVxuXG4gIGNvbnN0IHBhcmVudCA9IG5vZGUucGFyZW50Tm9kZTtcblxuICBpZiAoIXBhcmVudCB8fCBwYXJlbnQubm9kZVR5cGUgPT09IE5vZGUuRE9DVU1FTlRfTk9ERSkge1xuICAgIHJldHVybiB7XG4gICAgICBvcHRpbWl6ZWQ6IHRydWUsXG4gICAgICB2YWx1ZTogbm9kZU5hbWUsXG4gICAgfTtcbiAgfVxuXG4gIGNvbnN0IHByZWZpeGVkT3duQ2xhc3NOYW1lc0FycmF5XyA9IHByZWZpeGVkRWxlbWVudENsYXNzTmFtZXMoZWxlbWVudCk7XG5cbiAgY29uc3QgcHJlZml4ZWRPd25DbGFzc05hbWVzQXJyYXkgPSBbXTsgLy8gLmtleVNldCgpXG4gIHByZWZpeGVkT3duQ2xhc3NOYW1lc0FycmF5Xy5mb3JFYWNoKChhcnJJdGVtKSA9PiB7XG4gICAgaWYgKHByZWZpeGVkT3duQ2xhc3NOYW1lc0FycmF5LmluZGV4T2YoYXJySXRlbSkgPCAwKSB7XG4gICAgICBwcmVmaXhlZE93bkNsYXNzTmFtZXNBcnJheS5wdXNoKGFyckl0ZW0pO1xuICAgIH1cbiAgfSk7XG5cbiAgbGV0IG5lZWRzQ2xhc3NOYW1lcyA9IGZhbHNlO1xuICBsZXQgbmVlZHNOdGhDaGlsZCA9IGZhbHNlO1xuICBsZXQgb3duSW5kZXggPSAtMTtcbiAgbGV0IGVsZW1lbnRJbmRleCA9IC0xO1xuICBjb25zdCBzaWJsaW5ncyA9IHBhcmVudC5jaGlsZHJlbjtcblxuICBmb3IgKFxuICAgIGxldCBpID0gMDtcbiAgICAob3duSW5kZXggPT09IC0xIHx8ICFuZWVkc050aENoaWxkKSAmJiBpIDwgc2libGluZ3MubGVuZ3RoO1xuICAgICsraVxuICApIHtcbiAgICBjb25zdCBzaWJsaW5nID0gc2libGluZ3NbaV07XG4gICAgaWYgKHNpYmxpbmcubm9kZVR5cGUgIT09IE5vZGUuRUxFTUVOVF9OT0RFKSB7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG4gICAgZWxlbWVudEluZGV4ICs9IDE7XG4gICAgaWYgKHNpYmxpbmcgPT09IG5vZGUpIHtcbiAgICAgIG93bkluZGV4ID0gZWxlbWVudEluZGV4O1xuICAgICAgY29udGludWU7XG4gICAgfVxuICAgIGlmIChuZWVkc050aENoaWxkKSB7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG5cbiAgICAvLyBzaWJsaW5nLm5vZGVOYW1lSW5Db3JyZWN0Q2FzZSgpXG4gICAgY29uc3Qgc2libGluZ05hbWUgPVxuICAgICAgKHNpYmxpbmcubG9jYWxOYW1lICYmIHNpYmxpbmcubG9jYWxOYW1lLnRvTG93ZXJDYXNlKCkpIHx8XG4gICAgICBzaWJsaW5nLm5vZGVOYW1lLnRvTG93ZXJDYXNlKCk7XG4gICAgaWYgKHNpYmxpbmdOYW1lICE9PSBub2RlTmFtZSkge1xuICAgICAgY29udGludWU7XG4gICAgfVxuICAgIG5lZWRzQ2xhc3NOYW1lcyA9IHRydWU7XG5cbiAgICBjb25zdCBvd25DbGFzc05hbWVzID0gW107XG4gICAgcHJlZml4ZWRPd25DbGFzc05hbWVzQXJyYXkuZm9yRWFjaCgoYXJySXRlbSkgPT4ge1xuICAgICAgb3duQ2xhc3NOYW1lcy5wdXNoKGFyckl0ZW0pO1xuICAgIH0pO1xuICAgIGxldCBvd25DbGFzc05hbWVDb3VudCA9IG93bkNsYXNzTmFtZXMubGVuZ3RoO1xuXG4gICAgaWYgKG93bkNsYXNzTmFtZUNvdW50ID09PSAwKSB7XG4gICAgICBuZWVkc050aENoaWxkID0gdHJ1ZTtcbiAgICAgIGNvbnRpbnVlO1xuICAgIH1cbiAgICBjb25zdCBzaWJsaW5nQ2xhc3NOYW1lc0FycmF5XyA9IHByZWZpeGVkRWxlbWVudENsYXNzTmFtZXMoc2libGluZyk7XG4gICAgY29uc3Qgc2libGluZ0NsYXNzTmFtZXNBcnJheSA9IFtdOyAvLyAua2V5U2V0KClcbiAgICBzaWJsaW5nQ2xhc3NOYW1lc0FycmF5Xy5mb3JFYWNoKChhcnJJdGVtKSA9PiB7XG4gICAgICBpZiAoc2libGluZ0NsYXNzTmFtZXNBcnJheS5pbmRleE9mKGFyckl0ZW0pIDwgMCkge1xuICAgICAgICBzaWJsaW5nQ2xhc3NOYW1lc0FycmF5LnB1c2goYXJySXRlbSk7XG4gICAgICB9XG4gICAgfSk7XG5cbiAgICBmb3IgKGNvbnN0IHNpYmxpbmdDbGFzcyBvZiBzaWJsaW5nQ2xhc3NOYW1lc0FycmF5KSB7XG4gICAgICBjb25zdCBpbmQgPSBvd25DbGFzc05hbWVzLmluZGV4T2Yoc2libGluZ0NsYXNzKTtcbiAgICAgIGlmIChpbmQgPCAwKSB7XG4gICAgICAgIGNvbnRpbnVlO1xuICAgICAgfVxuXG4gICAgICBvd25DbGFzc05hbWVzLnNwbGljZShpbmQsIDEpOyAvLyBkZWxldGUgb3duQ2xhc3NOYW1lc1tzaWJsaW5nQ2xhc3NdO1xuXG4gICAgICBpZiAoIS0tb3duQ2xhc3NOYW1lQ291bnQpIHtcbiAgICAgICAgbmVlZHNOdGhDaGlsZCA9IHRydWU7XG4gICAgICAgIGJyZWFrO1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIGxldCByZXN1bHQgPSBub2RlTmFtZTtcbiAgaWYgKFxuICAgIGlzVGFyZ2V0Tm9kZSAmJlxuICAgIG5vZGVOYW1lID09PSBcImlucHV0XCIgJiZcbiAgICBlbGVtZW50LmdldEF0dHJpYnV0ZShcInR5cGVcIikgJiZcbiAgICAhZWxlbWVudC5nZXRBdHRyaWJ1dGUoXCJpZFwiKSAmJlxuICAgICFlbGVtZW50LmdldEF0dHJpYnV0ZShcImNsYXNzXCIpXG4gICkge1xuICAgIHJlc3VsdCArPSAnW3R5cGU9XCInICsgZWxlbWVudC5nZXRBdHRyaWJ1dGUoXCJ0eXBlXCIpICsgJ1wiXSc7XG4gIH1cbiAgaWYgKG5lZWRzTnRoQ2hpbGQpIHtcbiAgICByZXN1bHQgKz0gXCI6bnRoLWNoaWxkKFwiICsgKG93bkluZGV4ICsgMSkgKyBcIilcIjtcbiAgfSBlbHNlIGlmIChuZWVkc0NsYXNzTmFtZXMpIHtcbiAgICBmb3IgKGNvbnN0IHByZWZpeGVkTmFtZSBvZiBwcmVmaXhlZE93bkNsYXNzTmFtZXNBcnJheSkge1xuICAgICAgcmVzdWx0ICs9IFwiLlwiICsgZXNjYXBlSWRlbnRpZmllcklmTmVlZGVkKHByZWZpeGVkTmFtZS5zdWJzdHIoMSkpO1xuICAgIH1cbiAgfVxuXG4gIHJldHVybiB7XG4gICAgb3B0aW1pemVkOiBmYWxzZSxcbiAgICB2YWx1ZTogcmVzdWx0LFxuICB9O1xufVxuXG5mdW5jdGlvbiBjb21wdXRlQ0ZJKG5vZGUpIHtcbiAgLy8gVE9ETzogaGFuZGxlIGNoYXJhY3RlciBwb3NpdGlvbiBpbnNpZGUgdGV4dCBub2RlXG4gIGlmIChub2RlLm5vZGVUeXBlICE9PSBOb2RlLkVMRU1FTlRfTk9ERSkge1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cblxuICBsZXQgY2ZpID0gXCJcIjtcblxuICBsZXQgY3VycmVudEVsZW1lbnQgPSBub2RlO1xuICB3aGlsZSAoXG4gICAgY3VycmVudEVsZW1lbnQucGFyZW50Tm9kZSAmJlxuICAgIGN1cnJlbnRFbGVtZW50LnBhcmVudE5vZGUubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFXG4gICkge1xuICAgIGNvbnN0IGJsYWNrbGlzdGVkID0gY2hlY2tCbGFja2xpc3RlZChjdXJyZW50RWxlbWVudCk7XG4gICAgaWYgKCFibGFja2xpc3RlZCkge1xuICAgICAgY29uc3QgY3VycmVudEVsZW1lbnRQYXJlbnRDaGlsZHJlbiA9IGN1cnJlbnRFbGVtZW50LnBhcmVudE5vZGUuY2hpbGRyZW47XG4gICAgICBsZXQgY3VycmVudEVsZW1lbnRJbmRleCA9IC0xO1xuICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCBjdXJyZW50RWxlbWVudFBhcmVudENoaWxkcmVuLmxlbmd0aDsgaSsrKSB7XG4gICAgICAgIGlmIChjdXJyZW50RWxlbWVudCA9PT0gY3VycmVudEVsZW1lbnRQYXJlbnRDaGlsZHJlbltpXSkge1xuICAgICAgICAgIGN1cnJlbnRFbGVtZW50SW5kZXggPSBpO1xuICAgICAgICAgIGJyZWFrO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgICBpZiAoY3VycmVudEVsZW1lbnRJbmRleCA+PSAwKSB7XG4gICAgICAgIGNvbnN0IGNmaUluZGV4ID0gKGN1cnJlbnRFbGVtZW50SW5kZXggKyAxKSAqIDI7XG4gICAgICAgIGNmaSA9XG4gICAgICAgICAgY2ZpSW5kZXggK1xuICAgICAgICAgIChjdXJyZW50RWxlbWVudC5pZCA/IFwiW1wiICsgY3VycmVudEVsZW1lbnQuaWQgKyBcIl1cIiA6IFwiXCIpICtcbiAgICAgICAgICAoY2ZpLmxlbmd0aCA/IFwiL1wiICsgY2ZpIDogXCJcIik7XG4gICAgICB9XG4gICAgfVxuICAgIGN1cnJlbnRFbGVtZW50ID0gY3VycmVudEVsZW1lbnQucGFyZW50Tm9kZTtcbiAgfVxuXG4gIHJldHVybiBcIi9cIiArIGNmaTtcbn1cblxuZnVuY3Rpb24gX2NyZWF0ZUhpZ2hsaWdodChsb2NhdGlvbnMsIGNvbG9yLCBwb2ludGVySW50ZXJhY3Rpb24sIHR5cGUpIHtcbiAgY29uc3QgcmFuZ2VJbmZvID0gbG9jYXRpb24yUmFuZ2VJbmZvKGxvY2F0aW9ucyk7XG4gIGNvbnN0IHVuaXF1ZVN0ciA9IGAke3JhbmdlSW5mby5jZml9JHtyYW5nZUluZm8uc3RhcnRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3J9JHtyYW5nZUluZm8uc3RhcnRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXh9JHtyYW5nZUluZm8uc3RhcnRPZmZzZXR9JHtyYW5nZUluZm8uZW5kQ29udGFpbmVyRWxlbWVudENzc1NlbGVjdG9yfSR7cmFuZ2VJbmZvLmVuZENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleH0ke3JhbmdlSW5mby5lbmRPZmZzZXR9YDtcblxuICBjb25zdCBoYXNoID0gcmVxdWlyZShcImhhc2guanNcIik7XG4gIGNvbnN0IHNoYTI1NkhleCA9IGhhc2guc2hhMjU2KCkudXBkYXRlKHVuaXF1ZVN0cikuZGlnZXN0KFwiaGV4XCIpO1xuXG4gIHZhciBpZDtcbiAgaWYgKHR5cGUgPT0gSURfSElHSExJR0hUU19DT05UQUlORVIpIHtcbiAgICBpZCA9IFwiUjJfSElHSExJR0hUX1wiICsgc2hhMjU2SGV4O1xuICB9IGVsc2Uge1xuICAgIGlkID0gXCJSMl9BTk5PVEFUSU9OX1wiICsgc2hhMjU2SGV4O1xuICB9XG5cbiAgZGVzdHJveUhpZ2hsaWdodChpZCk7XG5cbiAgY29uc3QgaGlnaGxpZ2h0ID0ge1xuICAgIGNvbG9yOiBjb2xvciA/IGNvbG9yIDogREVGQVVMVF9CQUNLR1JPVU5EX0NPTE9SLFxuICAgIGlkLFxuICAgIHBvaW50ZXJJbnRlcmFjdGlvbixcbiAgICByYW5nZUluZm8sXG4gIH07XG4gIF9oaWdobGlnaHRzLnB1c2goaGlnaGxpZ2h0KTtcbiAgY3JlYXRlSGlnaGxpZ2h0RG9tKFxuICAgIHdpbmRvdyxcbiAgICBoaWdobGlnaHQsXG4gICAgdHlwZSA9PSBJRF9BTk5PVEFUSU9OX0NPTlRBSU5FUiA/IHRydWUgOiBmYWxzZVxuICApO1xuXG4gIHJldHVybiBoaWdobGlnaHQ7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVIaWdobGlnaHQoc2VsZWN0aW9uSW5mbywgY29sb3IsIHBvaW50ZXJJbnRlcmFjdGlvbikge1xuICByZXR1cm4gX2NyZWF0ZUhpZ2hsaWdodChcbiAgICBzZWxlY3Rpb25JbmZvLFxuICAgIGNvbG9yLFxuICAgIHBvaW50ZXJJbnRlcmFjdGlvbixcbiAgICBJRF9ISUdITElHSFRTX0NPTlRBSU5FUlxuICApO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gY3JlYXRlQW5ub3RhdGlvbihpZCkge1xuICBsZXQgaSA9IC0xO1xuXG4gIGNvbnN0IGhpZ2hsaWdodCA9IF9oaWdobGlnaHRzLmZpbmQoKGgsIGopID0+IHtcbiAgICBpID0gajtcbiAgICByZXR1cm4gaC5pZCA9PT0gaWQ7XG4gIH0pO1xuICBpZiAoaSA9PSBfaGlnaGxpZ2h0cy5sZW5ndGgpIHJldHVybjtcblxuICB2YXIgbG9jYXRpb25zID0ge1xuICAgIGxvY2F0aW9uczogcmFuZ2VJbmZvMkxvY2F0aW9uKGhpZ2hsaWdodC5yYW5nZUluZm8pLFxuICB9O1xuXG4gIHJldHVybiBfY3JlYXRlSGlnaGxpZ2h0KFxuICAgIGxvY2F0aW9ucyxcbiAgICBoaWdobGlnaHQuY29sb3IsXG4gICAgdHJ1ZSxcbiAgICBJRF9BTk5PVEFUSU9OX0NPTlRBSU5FUlxuICApO1xufVxuXG5mdW5jdGlvbiBjcmVhdGVIaWdobGlnaHREb20od2luLCBoaWdobGlnaHQsIGFubm90YXRpb25GbGFnKSB7XG4gIGNvbnN0IGRvY3VtZW50ID0gd2luLmRvY3VtZW50O1xuXG4gIGNvbnN0IHNjYWxlID1cbiAgICAxIC9cbiAgICAod2luLlJFQURJVU0yICYmIHdpbi5SRUFESVVNMi5pc0ZpeGVkTGF5b3V0XG4gICAgICA/IHdpbi5SRUFESVVNMi5meGxWaWV3cG9ydFNjYWxlXG4gICAgICA6IDEpO1xuXG4gIGNvbnN0IHNjcm9sbEVsZW1lbnQgPSBnZXRTY3JvbGxpbmdFbGVtZW50KGRvY3VtZW50KTtcblxuICBjb25zdCByYW5nZSA9IGNvbnZlcnRSYW5nZUluZm8oZG9jdW1lbnQsIGhpZ2hsaWdodC5yYW5nZUluZm8pO1xuICBpZiAoIXJhbmdlKSB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuXG4gIGNvbnN0IHBhZ2luYXRlZCA9IGlzUGFnaW5hdGVkKGRvY3VtZW50KTtcbiAgY29uc3QgaGlnaGxpZ2h0c0NvbnRhaW5lciA9IGVuc3VyZUNvbnRhaW5lcih3aW4sIGFubm90YXRpb25GbGFnKTtcbiAgY29uc3QgaGlnaGxpZ2h0UGFyZW50ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudChcImRpdlwiKTtcblxuICBoaWdobGlnaHRQYXJlbnQuc2V0QXR0cmlidXRlKFwiaWRcIiwgaGlnaGxpZ2h0LmlkKTtcbiAgaGlnaGxpZ2h0UGFyZW50LnNldEF0dHJpYnV0ZShcImNsYXNzXCIsIENMQVNTX0hJR0hMSUdIVF9DT05UQUlORVIpO1xuXG4gIGRvY3VtZW50LmJvZHkuc3R5bGUucG9zaXRpb24gPSBcInJlbGF0aXZlXCI7XG4gIGhpZ2hsaWdodFBhcmVudC5zdHlsZS5zZXRQcm9wZXJ0eShcInBvaW50ZXItZXZlbnRzXCIsIFwibm9uZVwiKTtcbiAgaWYgKGhpZ2hsaWdodC5wb2ludGVySW50ZXJhY3Rpb24pIHtcbiAgICBoaWdobGlnaHRQYXJlbnQuc2V0QXR0cmlidXRlKFwiZGF0YS1jbGlja1wiLCBcIjFcIik7XG4gIH1cblxuICBjb25zdCBib2R5UmVjdCA9IGRvY3VtZW50LmJvZHkuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCk7XG4gIGNvbnN0IHVzZVNWRyA9ICFERUJVR19WSVNVQUxTICYmIFVTRV9TVkc7XG4gIC8vY29uc3QgdXNlU1ZHID0gVVNFX1NWRztcbiAgY29uc3QgZHJhd1VuZGVybGluZSA9IGZhbHNlO1xuICBjb25zdCBkcmF3U3RyaWtlVGhyb3VnaCA9IGZhbHNlO1xuICBjb25zdCBkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzID0gZHJhd1VuZGVybGluZSB8fCBkcmF3U3RyaWtlVGhyb3VnaDtcbiAgLy9jb25zdCBjbGllbnRSZWN0cyA9IERFQlVHX1ZJU1VBTFMgPyByYW5nZS5nZXRDbGllbnRSZWN0cygpIDpcbiAgY29uc3QgY2xpZW50UmVjdHMgPSBnZXRDbGllbnRSZWN0c05vT3ZlcmxhcChcbiAgICByYW5nZSxcbiAgICBkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzXG4gICk7XG4gIGxldCBoaWdobGlnaHRBcmVhU1ZHRG9jRnJhZztcbiAgY29uc3Qgcm91bmRlZENvcm5lciA9IDM7XG4gIGNvbnN0IHVuZGVybGluZVRoaWNrbmVzcyA9IDI7XG4gIGNvbnN0IHN0cmlrZVRocm91Z2hMaW5lVGhpY2tuZXNzID0gMztcbiAgY29uc3Qgb3BhY2l0eSA9IERFRkFVTFRfQkFDS0dST1VORF9DT0xPUl9PUEFDSVRZO1xuICBsZXQgZXh0cmEgPSBcIlwiO1xuICBjb25zdCByYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QgPVxuICAgIGZyYW1lRm9ySGlnaGxpZ2h0QW5ub3RhdGlvbk1hcmtXaXRoSUQod2luLCBoaWdobGlnaHQuaWQpO1xuXG4gIGxldCB4T2Zmc2V0O1xuICBsZXQgeU9mZnNldDtcbiAgbGV0IGFubm90YXRpb25PZmZzZXQ7XG5cbiAgaWYgKG5hdmlnYXRvci51c2VyQWdlbnQubWF0Y2goL0FuZHJvaWQvaSkpIHtcbiAgICB4T2Zmc2V0ID0gcGFnaW5hdGVkID8gLXNjcm9sbEVsZW1lbnQuc2Nyb2xsTGVmdCA6IGJvZHlSZWN0LmxlZnQ7XG4gICAgeU9mZnNldCA9IHBhZ2luYXRlZCA/IC1zY3JvbGxFbGVtZW50LnNjcm9sbFRvcCA6IGJvZHlSZWN0LnRvcDtcbiAgICBhbm5vdGF0aW9uT2Zmc2V0ID1cbiAgICAgIHBhcnNlSW50KFxuICAgICAgICAocmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0LnJpZ2h0IC0geE9mZnNldCkgLyB3aW5kb3cuaW5uZXJXaWR0aFxuICAgICAgKSArIDE7XG4gIH0gZWxzZSBpZiAobmF2aWdhdG9yLnVzZXJBZ2VudC5tYXRjaCgvaVBob25lfGlQYWR8aVBvZC9pKSkge1xuICAgIHhPZmZzZXQgPSBwYWdpbmF0ZWQgPyAwIDogLXNjcm9sbEVsZW1lbnQuc2Nyb2xsTGVmdDtcbiAgICB5T2Zmc2V0ID0gcGFnaW5hdGVkID8gMCA6IGJvZHlSZWN0LnRvcDtcbiAgICBhbm5vdGF0aW9uT2Zmc2V0ID0gcGFyc2VJbnQoXG4gICAgICByYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QucmlnaHQgLyB3aW5kb3cuaW5uZXJXaWR0aCArIDFcbiAgICApO1xuICB9XG5cbiAgZm9yIChjb25zdCBjbGllbnRSZWN0IG9mIGNsaWVudFJlY3RzKSB7XG4gICAgaWYgKHVzZVNWRykge1xuICAgICAgY29uc3QgYm9yZGVyVGhpY2tuZXNzID0gMDtcbiAgICAgIGlmICghaGlnaGxpZ2h0QXJlYVNWR0RvY0ZyYWcpIHtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0RvY0ZyYWcgPSBkb2N1bWVudC5jcmVhdGVEb2N1bWVudEZyYWdtZW50KCk7XG4gICAgICB9XG4gICAgICBjb25zdCBoaWdobGlnaHRBcmVhU1ZHUmVjdCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnROUyhcbiAgICAgICAgU1ZHX1hNTF9OQU1FU1BBQ0UsXG4gICAgICAgIFwicmVjdFwiXG4gICAgICApO1xuXG4gICAgICBoaWdobGlnaHRBcmVhU1ZHUmVjdC5zZXRBdHRyaWJ1dGUoXCJjbGFzc1wiLCBDTEFTU19ISUdITElHSFRfQVJFQSk7XG4gICAgICBoaWdobGlnaHRBcmVhU1ZHUmVjdC5zZXRBdHRyaWJ1dGUoXG4gICAgICAgIFwic3R5bGVcIixcbiAgICAgICAgYGZpbGw6IHJnYigke2hpZ2hsaWdodC5jb2xvci5yZWR9LCAke2hpZ2hsaWdodC5jb2xvci5ncmVlbn0sICR7aGlnaGxpZ2h0LmNvbG9yLmJsdWV9KSAhaW1wb3J0YW50OyBmaWxsLW9wYWNpdHk6ICR7b3BhY2l0eX0gIWltcG9ydGFudDsgc3Ryb2tlLXdpZHRoOiAwO2BcbiAgICAgICk7XG4gICAgICBoaWdobGlnaHRBcmVhU1ZHUmVjdC5zY2FsZSA9IHNjYWxlO1xuXG4gICAgICAvKlxuICAgICAgICAgICAgIGhpZ2hsaWdodEFyZWFTVkdSZWN0LnJlY3QgPSB7XG4gICAgICAgICAgICAgaGVpZ2h0OiBjbGllbnRSZWN0LmhlaWdodCxcbiAgICAgICAgICAgICBsZWZ0OiBjbGllbnRSZWN0LmxlZnQgLSB4T2Zmc2V0LFxuICAgICAgICAgICAgIHRvcDogY2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgICAgICAgIHdpZHRoOiBjbGllbnRSZWN0LndpZHRoLFxuICAgICAgICAgICAgIH07XG4gICAgICAgICAgICAgKi9cblxuICAgICAgaWYgKGFubm90YXRpb25GbGFnKSB7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdSZWN0LnJlY3QgPSB7XG4gICAgICAgICAgaGVpZ2h0OiBBTk5PVEFUSU9OX1dJRFRILCAvL3JhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdC5oZWlnaHQgLSByYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QuaGVpZ2h0LzQsXG4gICAgICAgICAgbGVmdDogd2luZG93LmlubmVyV2lkdGggKiBhbm5vdGF0aW9uT2Zmc2V0IC0gQU5OT1RBVElPTl9XSURUSCxcbiAgICAgICAgICB0b3A6IHJhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgICAgIHdpZHRoOiBBTk5PVEFUSU9OX1dJRFRILFxuICAgICAgICB9O1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR1JlY3QucmVjdCA9IHtcbiAgICAgICAgICBoZWlnaHQ6IGNsaWVudFJlY3QuaGVpZ2h0LFxuICAgICAgICAgIGxlZnQ6IGNsaWVudFJlY3QubGVmdCAtIHhPZmZzZXQsXG4gICAgICAgICAgdG9wOiBjbGllbnRSZWN0LnRvcCAtIHlPZmZzZXQsXG4gICAgICAgICAgd2lkdGg6IGNsaWVudFJlY3Qud2lkdGgsXG4gICAgICAgIH07XG4gICAgICB9XG5cbiAgICAgIGhpZ2hsaWdodEFyZWFTVkdSZWN0LnNldEF0dHJpYnV0ZShcInJ4XCIsIGAke3JvdW5kZWRDb3JuZXIgKiBzY2FsZX1gKTtcbiAgICAgIGhpZ2hsaWdodEFyZWFTVkdSZWN0LnNldEF0dHJpYnV0ZShcInJ5XCIsIGAke3JvdW5kZWRDb3JuZXIgKiBzY2FsZX1gKTtcbiAgICAgIGhpZ2hsaWdodEFyZWFTVkdSZWN0LnNldEF0dHJpYnV0ZShcbiAgICAgICAgXCJ4XCIsXG4gICAgICAgIGAkeyhoaWdobGlnaHRBcmVhU1ZHUmVjdC5yZWN0LmxlZnQgLSBib3JkZXJUaGlja25lc3MpICogc2NhbGV9YFxuICAgICAgKTtcbiAgICAgIGhpZ2hsaWdodEFyZWFTVkdSZWN0LnNldEF0dHJpYnV0ZShcbiAgICAgICAgXCJ5XCIsXG4gICAgICAgIGAkeyhoaWdobGlnaHRBcmVhU1ZHUmVjdC5yZWN0LnRvcCAtIGJvcmRlclRoaWNrbmVzcykgKiBzY2FsZX1gXG4gICAgICApO1xuICAgICAgaGlnaGxpZ2h0QXJlYVNWR1JlY3Quc2V0QXR0cmlidXRlKFxuICAgICAgICBcImhlaWdodFwiLFxuICAgICAgICBgJHsoaGlnaGxpZ2h0QXJlYVNWR1JlY3QucmVjdC5oZWlnaHQgKyBib3JkZXJUaGlja25lc3MgKiAyKSAqIHNjYWxlfWBcbiAgICAgICk7XG4gICAgICBoaWdobGlnaHRBcmVhU1ZHUmVjdC5zZXRBdHRyaWJ1dGUoXG4gICAgICAgIFwid2lkdGhcIixcbiAgICAgICAgYCR7KGhpZ2hsaWdodEFyZWFTVkdSZWN0LnJlY3Qud2lkdGggKyBib3JkZXJUaGlja25lc3MgKiAyKSAqIHNjYWxlfWBcbiAgICAgICk7XG4gICAgICBoaWdobGlnaHRBcmVhU1ZHRG9jRnJhZy5hcHBlbmRDaGlsZChoaWdobGlnaHRBcmVhU1ZHUmVjdCk7XG4gICAgICBpZiAoZHJhd1VuZGVybGluZSkge1xuICAgICAgICBjb25zdCBoaWdobGlnaHRBcmVhU1ZHTGluZSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnROUyhcbiAgICAgICAgICBTVkdfWE1MX05BTUVTUEFDRSxcbiAgICAgICAgICBcImxpbmVcIlxuICAgICAgICApO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHUmVjdC5zZXRBdHRyaWJ1dGUoXCJjbGFzc1wiLCBDTEFTU19ISUdITElHSFRfQVJFQSk7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnNldEF0dHJpYnV0ZShcbiAgICAgICAgICBcInN0eWxlXCIsXG4gICAgICAgICAgYHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLXdpZHRoOiAke1xuICAgICAgICAgICAgdW5kZXJsaW5lVGhpY2tuZXNzICogc2NhbGVcbiAgICAgICAgICB9OyBzdHJva2U6IHJnYigke2hpZ2hsaWdodC5jb2xvci5yZWR9LCAke2hpZ2hsaWdodC5jb2xvci5ncmVlbn0sICR7XG4gICAgICAgICAgICBoaWdobGlnaHQuY29sb3IuYmx1ZVxuICAgICAgICAgIH0pICFpbXBvcnRhbnQ7IHN0cm9rZS1vcGFjaXR5OiAke29wYWNpdHl9ICFpbXBvcnRhbnRgXG4gICAgICAgICk7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnNjYWxlID0gc2NhbGU7XG4gICAgICAgIC8qXG4gICAgICAgICAgICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnJlY3QgPSB7XG4gICAgICAgICAgICAgICAgIGhlaWdodDogY2xpZW50UmVjdC5oZWlnaHQsXG4gICAgICAgICAgICAgICAgIGxlZnQ6IGNsaWVudFJlY3QubGVmdCAtIHhPZmZzZXQsXG4gICAgICAgICAgICAgICAgIHRvcDogY2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgICAgICAgICAgICB3aWR0aDogY2xpZW50UmVjdC53aWR0aCxcbiAgICAgICAgICAgICAgICAgfTtcbiAgICAgICAgICAgICAgICAgKi9cbiAgICAgICAgaWYgKGFubm90YXRpb25GbGFnKSB7XG4gICAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0xpbmUucmVjdCA9IHtcbiAgICAgICAgICAgIGhlaWdodDogQU5OT1RBVElPTl9XSURUSCwgLy9yYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QuaGVpZ2h0IC0gcmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0LmhlaWdodC80LFxuICAgICAgICAgICAgbGVmdDogd2luZG93LmlubmVyV2lkdGggKiBhbm5vdGF0aW9uT2Zmc2V0IC0gQU5OT1RBVElPTl9XSURUSCxcbiAgICAgICAgICAgIHRvcDogcmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0LnRvcCAtIHlPZmZzZXQsXG4gICAgICAgICAgICB3aWR0aDogQU5OT1RBVElPTl9XSURUSCxcbiAgICAgICAgICB9O1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnJlY3QgPSB7XG4gICAgICAgICAgICBoZWlnaHQ6IGNsaWVudFJlY3QuaGVpZ2h0LFxuICAgICAgICAgICAgbGVmdDogY2xpZW50UmVjdC5sZWZ0IC0geE9mZnNldCxcbiAgICAgICAgICAgIHRvcDogY2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgICAgICAgd2lkdGg6IGNsaWVudFJlY3Qud2lkdGgsXG4gICAgICAgICAgfTtcbiAgICAgICAgfVxuXG4gICAgICAgIGNvbnN0IGxpbmVPZmZzZXQgPVxuICAgICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnJlY3Qud2lkdGggPiByb3VuZGVkQ29ybmVyID8gcm91bmRlZENvcm5lciA6IDA7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnNldEF0dHJpYnV0ZShcbiAgICAgICAgICBcIngxXCIsXG4gICAgICAgICAgYCR7KGhpZ2hsaWdodEFyZWFTVkdMaW5lLnJlY3QubGVmdCArIGxpbmVPZmZzZXQpICogc2NhbGV9YFxuICAgICAgICApO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5zZXRBdHRyaWJ1dGUoXG4gICAgICAgICAgXCJ4MlwiLFxuICAgICAgICAgIGAke1xuICAgICAgICAgICAgKGhpZ2hsaWdodEFyZWFTVkdMaW5lLnJlY3QubGVmdCArXG4gICAgICAgICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnJlY3Qud2lkdGggLVxuICAgICAgICAgICAgICBsaW5lT2Zmc2V0KSAqXG4gICAgICAgICAgICBzY2FsZVxuICAgICAgICAgIH1gXG4gICAgICAgICk7XG4gICAgICAgIGNvbnN0IHkgPVxuICAgICAgICAgIChoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LnRvcCArXG4gICAgICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LmhlaWdodCAtXG4gICAgICAgICAgICB1bmRlcmxpbmVUaGlja25lc3MgLyAyKSAqXG4gICAgICAgICAgc2NhbGU7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnNldEF0dHJpYnV0ZShcInkxXCIsIGAke3l9YCk7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnNldEF0dHJpYnV0ZShcInkyXCIsIGAke3l9YCk7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnNldEF0dHJpYnV0ZShcbiAgICAgICAgICBcImhlaWdodFwiLFxuICAgICAgICAgIGAke2hpZ2hsaWdodEFyZWFTVkdMaW5lLnJlY3QuaGVpZ2h0ICogc2NhbGV9YFxuICAgICAgICApO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5zZXRBdHRyaWJ1dGUoXG4gICAgICAgICAgXCJ3aWR0aFwiLFxuICAgICAgICAgIGAke2hpZ2hsaWdodEFyZWFTVkdMaW5lLnJlY3Qud2lkdGggKiBzY2FsZX1gXG4gICAgICAgICk7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdEb2NGcmFnLmFwcGVuZENoaWxkKGhpZ2hsaWdodEFyZWFTVkdMaW5lKTtcbiAgICAgIH1cbiAgICAgIGlmIChkcmF3U3RyaWtlVGhyb3VnaCkge1xuICAgICAgICBjb25zdCBoaWdobGlnaHRBcmVhU1ZHTGluZSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnROUyhcbiAgICAgICAgICBTVkdfWE1MX05BTUVTUEFDRSxcbiAgICAgICAgICBcImxpbmVcIlxuICAgICAgICApO1xuXG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdSZWN0LnNldEF0dHJpYnV0ZShcImNsYXNzXCIsIENMQVNTX0hJR0hMSUdIVF9BUkVBKTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0xpbmUuc2V0QXR0cmlidXRlKFxuICAgICAgICAgIFwic3R5bGVcIixcbiAgICAgICAgICBgc3Ryb2tlLWxpbmVjYXA6IGJ1dHQ7IHN0cm9rZS13aWR0aDogJHtcbiAgICAgICAgICAgIHN0cmlrZVRocm91Z2hMaW5lVGhpY2tuZXNzICogc2NhbGVcbiAgICAgICAgICB9OyBzdHJva2U6IHJnYigke2hpZ2hsaWdodC5jb2xvci5yZWR9LCAke2hpZ2hsaWdodC5jb2xvci5ncmVlbn0sICR7XG4gICAgICAgICAgICBoaWdobGlnaHQuY29sb3IuYmx1ZVxuICAgICAgICAgIH0pICFpbXBvcnRhbnQ7IHN0cm9rZS1vcGFjaXR5OiAke29wYWNpdHl9ICFpbXBvcnRhbnRgXG4gICAgICAgICk7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnNjYWxlID0gc2NhbGU7XG5cbiAgICAgICAgLypcbiAgICAgICAgICAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0xpbmUucmVjdCA9IHtcbiAgICAgICAgICAgICAgICAgaGVpZ2h0OiBjbGllbnRSZWN0LmhlaWdodCxcbiAgICAgICAgICAgICAgICAgbGVmdDogY2xpZW50UmVjdC5sZWZ0IC0geE9mZnNldCxcbiAgICAgICAgICAgICAgICAgdG9wOiBjbGllbnRSZWN0LnRvcCAtIHlPZmZzZXQsXG4gICAgICAgICAgICAgICAgIHdpZHRoOiBjbGllbnRSZWN0LndpZHRoLFxuICAgICAgICAgICAgICAgICB9O1xuICAgICAgICAgICAgICAgICAqL1xuXG4gICAgICAgIGlmIChhbm5vdGF0aW9uRmxhZykge1xuICAgICAgICAgIGhpZ2hsaWdodEFyZWFTVkdMaW5lLnJlY3QgPSB7XG4gICAgICAgICAgICBoZWlnaHQ6IEFOTk9UQVRJT05fV0lEVEgsIC8vcmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0LmhlaWdodCAtIHJhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdC5oZWlnaHQvNCxcbiAgICAgICAgICAgIGxlZnQ6IHdpbmRvdy5pbm5lcldpZHRoICogYW5ub3RhdGlvbk9mZnNldCAtIEFOTk9UQVRJT05fV0lEVEgsXG4gICAgICAgICAgICB0b3A6IHJhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgICAgICAgd2lkdGg6IEFOTk9UQVRJT05fV0lEVEgsXG4gICAgICAgICAgfTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0ID0ge1xuICAgICAgICAgICAgaGVpZ2h0OiBjbGllbnRSZWN0LmhlaWdodCxcbiAgICAgICAgICAgIGxlZnQ6IGNsaWVudFJlY3QubGVmdCAtIHhPZmZzZXQsXG4gICAgICAgICAgICB0b3A6IGNsaWVudFJlY3QudG9wIC0geU9mZnNldCxcbiAgICAgICAgICAgIHdpZHRoOiBjbGllbnRSZWN0LndpZHRoLFxuICAgICAgICAgIH07XG4gICAgICAgIH1cblxuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5zZXRBdHRyaWJ1dGUoXG4gICAgICAgICAgXCJ4MVwiLFxuICAgICAgICAgIGAke2hpZ2hsaWdodEFyZWFTVkdMaW5lLnJlY3QubGVmdCAqIHNjYWxlfWBcbiAgICAgICAgKTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0xpbmUuc2V0QXR0cmlidXRlKFxuICAgICAgICAgIFwieDJcIixcbiAgICAgICAgICBgJHtcbiAgICAgICAgICAgIChoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LmxlZnQgKyBoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LndpZHRoKSAqXG4gICAgICAgICAgICBzY2FsZVxuICAgICAgICAgIH1gXG4gICAgICAgICk7XG4gICAgICAgIGNvbnN0IGxpbmVPZmZzZXQgPSBoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LmhlaWdodCAvIDI7XG4gICAgICAgIGNvbnN0IHkgPSAoaGlnaGxpZ2h0QXJlYVNWR0xpbmUucmVjdC50b3AgKyBsaW5lT2Zmc2V0KSAqIHNjYWxlO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5zZXRBdHRyaWJ1dGUoXCJ5MVwiLCBgJHt5fWApO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5zZXRBdHRyaWJ1dGUoXCJ5MlwiLCBgJHt5fWApO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHTGluZS5zZXRBdHRyaWJ1dGUoXG4gICAgICAgICAgXCJoZWlnaHRcIixcbiAgICAgICAgICBgJHtoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LmhlaWdodCAqIHNjYWxlfWBcbiAgICAgICAgKTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYVNWR0xpbmUuc2V0QXR0cmlidXRlKFxuICAgICAgICAgIFwid2lkdGhcIixcbiAgICAgICAgICBgJHtoaWdobGlnaHRBcmVhU1ZHTGluZS5yZWN0LndpZHRoICogc2NhbGV9YFxuICAgICAgICApO1xuICAgICAgICBoaWdobGlnaHRBcmVhU1ZHRG9jRnJhZy5hcHBlbmRDaGlsZChoaWdobGlnaHRBcmVhU1ZHTGluZSk7XG4gICAgICB9XG4gICAgfSBlbHNlIHtcbiAgICAgIGNvbnN0IGhpZ2hsaWdodEFyZWEgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KFwiZGl2XCIpO1xuXG4gICAgICBoaWdobGlnaHRBcmVhLnNldEF0dHJpYnV0ZShcImNsYXNzXCIsIENMQVNTX0hJR0hMSUdIVF9BUkVBKTtcblxuICAgICAgaWYgKERFQlVHX1ZJU1VBTFMpIHtcbiAgICAgICAgY29uc3QgcmdiID0gTWF0aC5yb3VuZCgweGZmZmZmZiAqIE1hdGgucmFuZG9tKCkpO1xuICAgICAgICBjb25zdCByID0gcmdiID4+IDE2O1xuICAgICAgICBjb25zdCBnID0gKHJnYiA+PiA4KSAmIDI1NTtcbiAgICAgICAgY29uc3QgYiA9IHJnYiAmIDI1NTtcbiAgICAgICAgZXh0cmEgPSBgb3V0bGluZS1jb2xvcjogcmdiKCR7cn0sICR7Z30sICR7Yn0pOyBvdXRsaW5lLXN0eWxlOiBzb2xpZDsgb3V0bGluZS13aWR0aDogMXB4OyBvdXRsaW5lLW9mZnNldDogLTFweDtgO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgaWYgKGRyYXdVbmRlcmxpbmUpIHtcbiAgICAgICAgICBleHRyYSArPSBgYm9yZGVyLWJvdHRvbTogJHt1bmRlcmxpbmVUaGlja25lc3MgKiBzY2FsZX1weCBzb2xpZCByZ2JhKCR7XG4gICAgICAgICAgICBoaWdobGlnaHQuY29sb3IucmVkXG4gICAgICAgICAgfSwgJHtoaWdobGlnaHQuY29sb3IuZ3JlZW59LCAke1xuICAgICAgICAgICAgaGlnaGxpZ2h0LmNvbG9yLmJsdWVcbiAgICAgICAgICB9LCAke29wYWNpdHl9KSAhaW1wb3J0YW50YDtcbiAgICAgICAgfVxuICAgICAgfVxuICAgICAgaGlnaGxpZ2h0QXJlYS5zZXRBdHRyaWJ1dGUoXG4gICAgICAgIFwic3R5bGVcIixcbiAgICAgICAgYGJvcmRlci1yYWRpdXM6ICR7cm91bmRlZENvcm5lcn1weCAhaW1wb3J0YW50OyBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKCR7aGlnaGxpZ2h0LmNvbG9yLnJlZH0sICR7aGlnaGxpZ2h0LmNvbG9yLmdyZWVufSwgJHtoaWdobGlnaHQuY29sb3IuYmx1ZX0sICR7b3BhY2l0eX0pICFpbXBvcnRhbnQ7ICR7ZXh0cmF9YFxuICAgICAgKTtcbiAgICAgIGhpZ2hsaWdodEFyZWEuc3R5bGUuc2V0UHJvcGVydHkoXCJwb2ludGVyLWV2ZW50c1wiLCBcIm5vbmVcIik7XG4gICAgICBoaWdobGlnaHRBcmVhLnN0eWxlLnBvc2l0aW9uID0gcGFnaW5hdGVkID8gXCJmaXhlZFwiIDogXCJhYnNvbHV0ZVwiO1xuICAgICAgaGlnaGxpZ2h0QXJlYS5zY2FsZSA9IHNjYWxlO1xuICAgICAgLypcbiAgICAgICAgICAgICBoaWdobGlnaHRBcmVhLnJlY3QgPSB7XG4gICAgICAgICAgICAgaGVpZ2h0OiBjbGllbnRSZWN0LmhlaWdodCxcbiAgICAgICAgICAgICBsZWZ0OiBjbGllbnRSZWN0LmxlZnQgLSB4T2Zmc2V0LFxuICAgICAgICAgICAgIHRvcDogY2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgICAgICAgIHdpZHRoOiBjbGllbnRSZWN0LndpZHRoLFxuICAgICAgICAgICAgIH07XG4gICAgICAgICAgICAgKi9cbiAgICAgIGlmIChhbm5vdGF0aW9uRmxhZykge1xuICAgICAgICBoaWdobGlnaHRBcmVhLnJlY3QgPSB7XG4gICAgICAgICAgaGVpZ2h0OiBBTk5PVEFUSU9OX1dJRFRILCAvL3JhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdC5oZWlnaHQgLSByYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QuaGVpZ2h0LzQsXG4gICAgICAgICAgbGVmdDogd2luZG93LmlubmVyV2lkdGggKiBhbm5vdGF0aW9uT2Zmc2V0IC0gQU5OT1RBVElPTl9XSURUSCxcbiAgICAgICAgICB0b3A6IHJhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgICAgIHdpZHRoOiBBTk5PVEFUSU9OX1dJRFRILFxuICAgICAgICB9O1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYS5yZWN0ID0ge1xuICAgICAgICAgIGhlaWdodDogY2xpZW50UmVjdC5oZWlnaHQsXG4gICAgICAgICAgbGVmdDogY2xpZW50UmVjdC5sZWZ0IC0geE9mZnNldCxcbiAgICAgICAgICB0b3A6IGNsaWVudFJlY3QudG9wIC0geU9mZnNldCxcbiAgICAgICAgICB3aWR0aDogY2xpZW50UmVjdC53aWR0aCxcbiAgICAgICAgfTtcbiAgICAgIH1cblxuICAgICAgaGlnaGxpZ2h0QXJlYS5zdHlsZS53aWR0aCA9IGAke2hpZ2hsaWdodEFyZWEucmVjdC53aWR0aCAqIHNjYWxlfXB4YDtcbiAgICAgIGhpZ2hsaWdodEFyZWEuc3R5bGUuaGVpZ2h0ID0gYCR7aGlnaGxpZ2h0QXJlYS5yZWN0LmhlaWdodCAqIHNjYWxlfXB4YDtcbiAgICAgIGhpZ2hsaWdodEFyZWEuc3R5bGUubGVmdCA9IGAke2hpZ2hsaWdodEFyZWEucmVjdC5sZWZ0ICogc2NhbGV9cHhgO1xuICAgICAgaGlnaGxpZ2h0QXJlYS5zdHlsZS50b3AgPSBgJHtoaWdobGlnaHRBcmVhLnJlY3QudG9wICogc2NhbGV9cHhgO1xuICAgICAgaGlnaGxpZ2h0UGFyZW50LmFwcGVuZChoaWdobGlnaHRBcmVhKTtcbiAgICAgIGlmICghREVCVUdfVklTVUFMUyAmJiBkcmF3U3RyaWtlVGhyb3VnaCkge1xuICAgICAgICAvL2lmIChkcmF3U3RyaWtlVGhyb3VnaCkge1xuICAgICAgICBjb25zdCBoaWdobGlnaHRBcmVhTGluZSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoXCJkaXZcIik7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFMaW5lLnNldEF0dHJpYnV0ZShcImNsYXNzXCIsIENMQVNTX0hJR0hMSUdIVF9BUkVBKTtcblxuICAgICAgICBoaWdobGlnaHRBcmVhTGluZS5zZXRBdHRyaWJ1dGUoXG4gICAgICAgICAgXCJzdHlsZVwiLFxuICAgICAgICAgIGBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKCR7aGlnaGxpZ2h0LmNvbG9yLnJlZH0sICR7aGlnaGxpZ2h0LmNvbG9yLmdyZWVufSwgJHtoaWdobGlnaHQuY29sb3IuYmx1ZX0sICR7b3BhY2l0eX0pICFpbXBvcnRhbnQ7YFxuICAgICAgICApO1xuICAgICAgICBoaWdobGlnaHRBcmVhTGluZS5zdHlsZS5zZXRQcm9wZXJ0eShcInBvaW50ZXItZXZlbnRzXCIsIFwibm9uZVwiKTtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYUxpbmUuc3R5bGUucG9zaXRpb24gPSBwYWdpbmF0ZWQgPyBcImZpeGVkXCIgOiBcImFic29sdXRlXCI7XG4gICAgICAgIGhpZ2hsaWdodEFyZWFMaW5lLnNjYWxlID0gc2NhbGU7XG4gICAgICAgIC8qXG4gICAgICAgICAgICAgICAgIGhpZ2hsaWdodEFyZWFMaW5lLnJlY3QgPSB7XG4gICAgICAgICAgICAgICAgIGhlaWdodDogY2xpZW50UmVjdC5oZWlnaHQsXG4gICAgICAgICAgICAgICAgIGxlZnQ6IGNsaWVudFJlY3QubGVmdCAtIHhPZmZzZXQsXG4gICAgICAgICAgICAgICAgIHRvcDogY2xpZW50UmVjdC50b3AgLSB5T2Zmc2V0LFxuICAgICAgICAgICAgICAgICB3aWR0aDogY2xpZW50UmVjdC53aWR0aCxcbiAgICAgICAgICAgICAgICAgfTtcbiAgICAgICAgICAgICAgICAgKi9cblxuICAgICAgICBpZiAoYW5ub3RhdGlvbkZsYWcpIHtcbiAgICAgICAgICBoaWdobGlnaHRBcmVhTGluZS5yZWN0ID0ge1xuICAgICAgICAgICAgaGVpZ2h0OiBBTk5PVEFUSU9OX1dJRFRILCAvL3JhbmdlQW5ub3RhdGlvbkJvdW5kaW5nQ2xpZW50UmVjdC5oZWlnaHQgLSByYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QuaGVpZ2h0LzQsXG4gICAgICAgICAgICBsZWZ0OiB3aW5kb3cuaW5uZXJXaWR0aCAqIGFubm90YXRpb25PZmZzZXQgLSBBTk5PVEFUSU9OX1dJRFRILFxuICAgICAgICAgICAgdG9wOiByYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QudG9wIC0geU9mZnNldCxcbiAgICAgICAgICAgIHdpZHRoOiBBTk5PVEFUSU9OX1dJRFRILFxuICAgICAgICAgIH07XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgaGlnaGxpZ2h0QXJlYUxpbmUucmVjdCA9IHtcbiAgICAgICAgICAgIGhlaWdodDogY2xpZW50UmVjdC5oZWlnaHQsXG4gICAgICAgICAgICBsZWZ0OiBjbGllbnRSZWN0LmxlZnQgLSB4T2Zmc2V0LFxuICAgICAgICAgICAgdG9wOiBjbGllbnRSZWN0LnRvcCAtIHlPZmZzZXQsXG4gICAgICAgICAgICB3aWR0aDogY2xpZW50UmVjdC53aWR0aCxcbiAgICAgICAgICB9O1xuICAgICAgICB9XG5cbiAgICAgICAgaGlnaGxpZ2h0QXJlYUxpbmUuc3R5bGUud2lkdGggPSBgJHtcbiAgICAgICAgICBoaWdobGlnaHRBcmVhTGluZS5yZWN0LndpZHRoICogc2NhbGVcbiAgICAgICAgfXB4YDtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYUxpbmUuc3R5bGUuaGVpZ2h0ID0gYCR7XG4gICAgICAgICAgc3RyaWtlVGhyb3VnaExpbmVUaGlja25lc3MgKiBzY2FsZVxuICAgICAgICB9cHhgO1xuICAgICAgICBoaWdobGlnaHRBcmVhTGluZS5zdHlsZS5sZWZ0ID0gYCR7XG4gICAgICAgICAgaGlnaGxpZ2h0QXJlYUxpbmUucmVjdC5sZWZ0ICogc2NhbGVcbiAgICAgICAgfXB4YDtcbiAgICAgICAgaGlnaGxpZ2h0QXJlYUxpbmUuc3R5bGUudG9wID0gYCR7XG4gICAgICAgICAgKGhpZ2hsaWdodEFyZWFMaW5lLnJlY3QudG9wICtcbiAgICAgICAgICAgIGhpZ2hsaWdodEFyZWFMaW5lLnJlY3QuaGVpZ2h0IC8gMiAtXG4gICAgICAgICAgICBzdHJpa2VUaHJvdWdoTGluZVRoaWNrbmVzcyAvIDIpICpcbiAgICAgICAgICBzY2FsZVxuICAgICAgICB9cHhgO1xuICAgICAgICBoaWdobGlnaHRQYXJlbnQuYXBwZW5kKGhpZ2hsaWdodEFyZWFMaW5lKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBpZiAoYW5ub3RhdGlvbkZsYWcpIHtcbiAgICAgIGJyZWFrO1xuICAgIH1cbiAgfVxuXG4gIGlmICh1c2VTVkcgJiYgaGlnaGxpZ2h0QXJlYVNWR0RvY0ZyYWcpIHtcbiAgICBjb25zdCBoaWdobGlnaHRBcmVhU1ZHID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudE5TKFNWR19YTUxfTkFNRVNQQUNFLCBcInN2Z1wiKTtcbiAgICBoaWdobGlnaHRBcmVhU1ZHLnNldEF0dHJpYnV0ZShcInBvaW50ZXItZXZlbnRzXCIsIFwibm9uZVwiKTtcbiAgICBoaWdobGlnaHRBcmVhU1ZHLnN0eWxlLnBvc2l0aW9uID0gcGFnaW5hdGVkID8gXCJmaXhlZFwiIDogXCJhYnNvbHV0ZVwiO1xuICAgIGhpZ2hsaWdodEFyZWFTVkcuc3R5bGUub3ZlcmZsb3cgPSBcInZpc2libGVcIjtcbiAgICBoaWdobGlnaHRBcmVhU1ZHLnN0eWxlLmxlZnQgPSBcIjBcIjtcbiAgICBoaWdobGlnaHRBcmVhU1ZHLnN0eWxlLnRvcCA9IFwiMFwiO1xuICAgIGhpZ2hsaWdodEFyZWFTVkcuYXBwZW5kKGhpZ2hsaWdodEFyZWFTVkdEb2NGcmFnKTtcbiAgICBoaWdobGlnaHRQYXJlbnQuYXBwZW5kKGhpZ2hsaWdodEFyZWFTVkcpO1xuICB9XG5cbiAgY29uc3QgaGlnaGxpZ2h0Qm91bmRpbmcgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KFwiZGl2XCIpO1xuXG4gIGlmIChhbm5vdGF0aW9uRmxhZykge1xuICAgIGhpZ2hsaWdodEJvdW5kaW5nLnNldEF0dHJpYnV0ZShcImNsYXNzXCIsIENMQVNTX0FOTk9UQVRJT05fQk9VTkRJTkdfQVJFQSk7XG4gICAgaGlnaGxpZ2h0Qm91bmRpbmcuc2V0QXR0cmlidXRlKFxuICAgICAgXCJzdHlsZVwiLFxuICAgICAgYGJvcmRlci1yYWRpdXM6ICR7cm91bmRlZENvcm5lcn1weCAhaW1wb3J0YW50OyBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKCR7aGlnaGxpZ2h0LmNvbG9yLnJlZH0sICR7aGlnaGxpZ2h0LmNvbG9yLmdyZWVufSwgJHtoaWdobGlnaHQuY29sb3IuYmx1ZX0sICR7b3BhY2l0eX0pICFpbXBvcnRhbnQ7ICR7ZXh0cmF9YFxuICAgICk7XG4gIH0gZWxzZSB7XG4gICAgaGlnaGxpZ2h0Qm91bmRpbmcuc2V0QXR0cmlidXRlKFwiY2xhc3NcIiwgQ0xBU1NfSElHSExJR0hUX0JPVU5ESU5HX0FSRUEpO1xuICB9XG5cbiAgaGlnaGxpZ2h0Qm91bmRpbmcuc3R5bGUuc2V0UHJvcGVydHkoXCJwb2ludGVyLWV2ZW50c1wiLCBcIm5vbmVcIik7XG4gIGhpZ2hsaWdodEJvdW5kaW5nLnN0eWxlLnBvc2l0aW9uID0gcGFnaW5hdGVkID8gXCJmaXhlZFwiIDogXCJhYnNvbHV0ZVwiO1xuICBoaWdobGlnaHRCb3VuZGluZy5zY2FsZSA9IHNjYWxlO1xuXG4gIGlmIChERUJVR19WSVNVQUxTKSB7XG4gICAgaGlnaGxpZ2h0Qm91bmRpbmcuc2V0QXR0cmlidXRlKFxuICAgICAgXCJzdHlsZVwiLFxuICAgICAgYG91dGxpbmUtY29sb3I6IG1hZ2VudGE7IG91dGxpbmUtc3R5bGU6IHNvbGlkOyBvdXRsaW5lLXdpZHRoOiAxcHg7IG91dGxpbmUtb2Zmc2V0OiAtMXB4O2BcbiAgICApO1xuICB9XG5cbiAgaWYgKGFubm90YXRpb25GbGFnKSB7XG4gICAgaGlnaGxpZ2h0Qm91bmRpbmcucmVjdCA9IHtcbiAgICAgIGhlaWdodDogQU5OT1RBVElPTl9XSURUSCwgLy9yYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QuaGVpZ2h0IC0gcmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0LmhlaWdodC80LFxuICAgICAgbGVmdDogd2luZG93LmlubmVyV2lkdGggKiBhbm5vdGF0aW9uT2Zmc2V0IC0gQU5OT1RBVElPTl9XSURUSCxcbiAgICAgIHRvcDogcmFuZ2VBbm5vdGF0aW9uQm91bmRpbmdDbGllbnRSZWN0LnRvcCAtIHlPZmZzZXQsXG4gICAgICB3aWR0aDogQU5OT1RBVElPTl9XSURUSCxcbiAgICB9O1xuICB9IGVsc2Uge1xuICAgIGNvbnN0IHJhbmdlQm91bmRpbmdDbGllbnRSZWN0ID0gcmFuZ2UuZ2V0Qm91bmRpbmdDbGllbnRSZWN0KCk7XG4gICAgaGlnaGxpZ2h0Qm91bmRpbmcucmVjdCA9IHtcbiAgICAgIGhlaWdodDogcmFuZ2VCb3VuZGluZ0NsaWVudFJlY3QuaGVpZ2h0LFxuICAgICAgbGVmdDogcmFuZ2VCb3VuZGluZ0NsaWVudFJlY3QubGVmdCAtIHhPZmZzZXQsXG4gICAgICB0b3A6IHJhbmdlQm91bmRpbmdDbGllbnRSZWN0LnRvcCAtIHlPZmZzZXQsXG4gICAgICB3aWR0aDogcmFuZ2VCb3VuZGluZ0NsaWVudFJlY3Qud2lkdGgsXG4gICAgfTtcbiAgfVxuXG4gIGhpZ2hsaWdodEJvdW5kaW5nLnN0eWxlLndpZHRoID0gYCR7aGlnaGxpZ2h0Qm91bmRpbmcucmVjdC53aWR0aCAqIHNjYWxlfXB4YDtcbiAgaGlnaGxpZ2h0Qm91bmRpbmcuc3R5bGUuaGVpZ2h0ID0gYCR7aGlnaGxpZ2h0Qm91bmRpbmcucmVjdC5oZWlnaHQgKiBzY2FsZX1weGA7XG4gIGhpZ2hsaWdodEJvdW5kaW5nLnN0eWxlLmxlZnQgPSBgJHtoaWdobGlnaHRCb3VuZGluZy5yZWN0LmxlZnQgKiBzY2FsZX1weGA7XG4gIGhpZ2hsaWdodEJvdW5kaW5nLnN0eWxlLnRvcCA9IGAke2hpZ2hsaWdodEJvdW5kaW5nLnJlY3QudG9wICogc2NhbGV9cHhgO1xuXG4gIGhpZ2hsaWdodFBhcmVudC5hcHBlbmQoaGlnaGxpZ2h0Qm91bmRpbmcpO1xuICBoaWdobGlnaHRzQ29udGFpbmVyLmFwcGVuZChoaWdobGlnaHRQYXJlbnQpO1xuXG4gIHJldHVybiBoaWdobGlnaHRQYXJlbnQ7XG59XG5cbmZ1bmN0aW9uIGNyZWF0ZU9yZGVyZWRSYW5nZShzdGFydE5vZGUsIHN0YXJ0T2Zmc2V0LCBlbmROb2RlLCBlbmRPZmZzZXQpIHtcbiAgY29uc3QgcmFuZ2UgPSBuZXcgUmFuZ2UoKTtcbiAgcmFuZ2Uuc2V0U3RhcnQoc3RhcnROb2RlLCBzdGFydE9mZnNldCk7XG4gIHJhbmdlLnNldEVuZChlbmROb2RlLCBlbmRPZmZzZXQpO1xuICBpZiAoIXJhbmdlLmNvbGxhcHNlZCkge1xuICAgIHJldHVybiByYW5nZTtcbiAgfVxuICBjb25zb2xlLmxvZyhcIj4+PiBjcmVhdGVPcmRlcmVkUmFuZ2UgQ09MTEFQU0VEIC4uLiBSQU5HRSBSRVZFUlNFP1wiKTtcbiAgY29uc3QgcmFuZ2VSZXZlcnNlID0gbmV3IFJhbmdlKCk7XG4gIHJhbmdlUmV2ZXJzZS5zZXRTdGFydChlbmROb2RlLCBlbmRPZmZzZXQpO1xuICByYW5nZVJldmVyc2Uuc2V0RW5kKHN0YXJ0Tm9kZSwgc3RhcnRPZmZzZXQpO1xuICBpZiAoIXJhbmdlUmV2ZXJzZS5jb2xsYXBzZWQpIHtcbiAgICBjb25zb2xlLmxvZyhcIj4+PiBjcmVhdGVPcmRlcmVkUmFuZ2UgUkFOR0UgUkVWRVJTRSBPSy5cIik7XG4gICAgcmV0dXJuIHJhbmdlO1xuICB9XG4gIGNvbnNvbGUubG9nKFwiPj4+IGNyZWF0ZU9yZGVyZWRSYW5nZSBSQU5HRSBSRVZFUlNFIEFMU08gQ09MTEFQU0VEPyFcIik7XG4gIHJldHVybiB1bmRlZmluZWQ7XG59XG5cbmZ1bmN0aW9uIGNvbnZlcnRSYW5nZShyYW5nZSwgZ2V0Q3NzU2VsZWN0b3IsIGNvbXB1dGVFbGVtZW50Q0ZJKSB7XG4gIGNvbnN0IHN0YXJ0SXNFbGVtZW50ID0gcmFuZ2Uuc3RhcnRDb250YWluZXIubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFO1xuICBjb25zdCBzdGFydENvbnRhaW5lckVsZW1lbnQgPSBzdGFydElzRWxlbWVudFxuICAgID8gcmFuZ2Uuc3RhcnRDb250YWluZXJcbiAgICA6IHJhbmdlLnN0YXJ0Q29udGFpbmVyLnBhcmVudE5vZGUgJiZcbiAgICAgIHJhbmdlLnN0YXJ0Q29udGFpbmVyLnBhcmVudE5vZGUubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFXG4gICAgPyByYW5nZS5zdGFydENvbnRhaW5lci5wYXJlbnROb2RlXG4gICAgOiB1bmRlZmluZWQ7XG4gIGlmICghc3RhcnRDb250YWluZXJFbGVtZW50KSB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuICBjb25zdCBzdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCA9IHN0YXJ0SXNFbGVtZW50XG4gICAgPyAtMVxuICAgIDogQXJyYXkuZnJvbShzdGFydENvbnRhaW5lckVsZW1lbnQuY2hpbGROb2RlcykuaW5kZXhPZihcbiAgICAgICAgcmFuZ2Uuc3RhcnRDb250YWluZXJcbiAgICAgICk7XG4gIGlmIChzdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCA8IC0xKSB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuICBjb25zdCBzdGFydENvbnRhaW5lckVsZW1lbnRDc3NTZWxlY3RvciA9IGdldENzc1NlbGVjdG9yKFxuICAgIHN0YXJ0Q29udGFpbmVyRWxlbWVudFxuICApO1xuICBjb25zdCBlbmRJc0VsZW1lbnQgPSByYW5nZS5lbmRDb250YWluZXIubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFO1xuICBjb25zdCBlbmRDb250YWluZXJFbGVtZW50ID0gZW5kSXNFbGVtZW50XG4gICAgPyByYW5nZS5lbmRDb250YWluZXJcbiAgICA6IHJhbmdlLmVuZENvbnRhaW5lci5wYXJlbnROb2RlICYmXG4gICAgICByYW5nZS5lbmRDb250YWluZXIucGFyZW50Tm9kZS5ub2RlVHlwZSA9PT0gTm9kZS5FTEVNRU5UX05PREVcbiAgICA/IHJhbmdlLmVuZENvbnRhaW5lci5wYXJlbnROb2RlXG4gICAgOiB1bmRlZmluZWQ7XG4gIGlmICghZW5kQ29udGFpbmVyRWxlbWVudCkge1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgY29uc3QgZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4ID0gZW5kSXNFbGVtZW50XG4gICAgPyAtMVxuICAgIDogQXJyYXkuZnJvbShlbmRDb250YWluZXJFbGVtZW50LmNoaWxkTm9kZXMpLmluZGV4T2YocmFuZ2UuZW5kQ29udGFpbmVyKTtcbiAgaWYgKGVuZENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCA8IC0xKSB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuICBjb25zdCBlbmRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3IgPSBnZXRDc3NTZWxlY3RvcihlbmRDb250YWluZXJFbGVtZW50KTtcbiAgY29uc3QgY29tbW9uRWxlbWVudEFuY2VzdG9yID0gZ2V0Q29tbW9uQW5jZXN0b3JFbGVtZW50KFxuICAgIHJhbmdlLnN0YXJ0Q29udGFpbmVyLFxuICAgIHJhbmdlLmVuZENvbnRhaW5lclxuICApO1xuICBpZiAoIWNvbW1vbkVsZW1lbnRBbmNlc3Rvcikge1xuICAgIGNvbnNvbGUubG9nKFwiXl5eIE5PIFJBTkdFIENPTU1PTiBBTkNFU1RPUj8hXCIpO1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgaWYgKHJhbmdlLmNvbW1vbkFuY2VzdG9yQ29udGFpbmVyKSB7XG4gICAgY29uc3QgcmFuZ2VDb21tb25BbmNlc3RvckVsZW1lbnQgPVxuICAgICAgcmFuZ2UuY29tbW9uQW5jZXN0b3JDb250YWluZXIubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFXG4gICAgICAgID8gcmFuZ2UuY29tbW9uQW5jZXN0b3JDb250YWluZXJcbiAgICAgICAgOiByYW5nZS5jb21tb25BbmNlc3RvckNvbnRhaW5lci5wYXJlbnROb2RlO1xuICAgIGlmIChcbiAgICAgIHJhbmdlQ29tbW9uQW5jZXN0b3JFbGVtZW50ICYmXG4gICAgICByYW5nZUNvbW1vbkFuY2VzdG9yRWxlbWVudC5ub2RlVHlwZSA9PT0gTm9kZS5FTEVNRU5UX05PREVcbiAgICApIHtcbiAgICAgIGlmIChjb21tb25FbGVtZW50QW5jZXN0b3IgIT09IHJhbmdlQ29tbW9uQW5jZXN0b3JFbGVtZW50KSB7XG4gICAgICAgIGNvbnNvbGUubG9nKFwiPj4+Pj4+IENPTU1PTiBBTkNFU1RPUiBDT05UQUlORVIgRElGRj8/IVwiKTtcbiAgICAgICAgY29uc29sZS5sb2coZ2V0Q3NzU2VsZWN0b3IoY29tbW9uRWxlbWVudEFuY2VzdG9yKSk7XG4gICAgICAgIGNvbnNvbGUubG9nKGdldENzc1NlbGVjdG9yKHJhbmdlQ29tbW9uQW5jZXN0b3JFbGVtZW50KSk7XG4gICAgICB9XG4gICAgfVxuICB9XG4gIGNvbnN0IHJvb3RFbGVtZW50Q2ZpID0gY29tcHV0ZUVsZW1lbnRDRkkoY29tbW9uRWxlbWVudEFuY2VzdG9yKTtcbiAgY29uc3Qgc3RhcnRFbGVtZW50Q2ZpID0gY29tcHV0ZUVsZW1lbnRDRkkoc3RhcnRDb250YWluZXJFbGVtZW50KTtcbiAgY29uc3QgZW5kRWxlbWVudENmaSA9IGNvbXB1dGVFbGVtZW50Q0ZJKGVuZENvbnRhaW5lckVsZW1lbnQpO1xuICBsZXQgY2ZpO1xuICBpZiAocm9vdEVsZW1lbnRDZmkgJiYgc3RhcnRFbGVtZW50Q2ZpICYmIGVuZEVsZW1lbnRDZmkpIHtcbiAgICBsZXQgc3RhcnRFbGVtZW50T3JUZXh0Q2ZpID0gc3RhcnRFbGVtZW50Q2ZpO1xuICAgIGlmICghc3RhcnRJc0VsZW1lbnQpIHtcbiAgICAgIGNvbnN0IHN0YXJ0Q29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4Rm9yQ2ZpID0gZ2V0Q2hpbGRUZXh0Tm9kZUNmaUluZGV4KFxuICAgICAgICBzdGFydENvbnRhaW5lckVsZW1lbnQsXG4gICAgICAgIHJhbmdlLnN0YXJ0Q29udGFpbmVyXG4gICAgICApO1xuICAgICAgc3RhcnRFbGVtZW50T3JUZXh0Q2ZpID1cbiAgICAgICAgc3RhcnRFbGVtZW50Q2ZpICtcbiAgICAgICAgXCIvXCIgK1xuICAgICAgICBzdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleEZvckNmaSArXG4gICAgICAgIFwiOlwiICtcbiAgICAgICAgcmFuZ2Uuc3RhcnRPZmZzZXQ7XG4gICAgfSBlbHNlIHtcbiAgICAgIGlmIChcbiAgICAgICAgcmFuZ2Uuc3RhcnRPZmZzZXQgPj0gMCAmJlxuICAgICAgICByYW5nZS5zdGFydE9mZnNldCA8IHN0YXJ0Q29udGFpbmVyRWxlbWVudC5jaGlsZE5vZGVzLmxlbmd0aFxuICAgICAgKSB7XG4gICAgICAgIGNvbnN0IGNoaWxkTm9kZSA9IHN0YXJ0Q29udGFpbmVyRWxlbWVudC5jaGlsZE5vZGVzW3JhbmdlLnN0YXJ0T2Zmc2V0XTtcbiAgICAgICAgaWYgKGNoaWxkTm9kZS5ub2RlVHlwZSA9PT0gTm9kZS5FTEVNRU5UX05PREUpIHtcbiAgICAgICAgICBzdGFydEVsZW1lbnRPclRleHRDZmkgPVxuICAgICAgICAgICAgc3RhcnRFbGVtZW50Q2ZpICsgXCIvXCIgKyAocmFuZ2Uuc3RhcnRPZmZzZXQgKyAxKSAqIDI7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgY29uc3QgY2ZpVGV4dE5vZGVJbmRleCA9IGdldENoaWxkVGV4dE5vZGVDZmlJbmRleChcbiAgICAgICAgICAgIHN0YXJ0Q29udGFpbmVyRWxlbWVudCxcbiAgICAgICAgICAgIGNoaWxkTm9kZVxuICAgICAgICAgICk7XG4gICAgICAgICAgc3RhcnRFbGVtZW50T3JUZXh0Q2ZpID0gc3RhcnRFbGVtZW50Q2ZpICsgXCIvXCIgKyBjZmlUZXh0Tm9kZUluZGV4O1xuICAgICAgICB9XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBjb25zdCBjZmlJbmRleE9mTGFzdEVsZW1lbnQgPVxuICAgICAgICAgIHN0YXJ0Q29udGFpbmVyRWxlbWVudC5jaGlsZEVsZW1lbnRDb3VudCAqIDI7XG4gICAgICAgIGNvbnN0IGxhc3RDaGlsZE5vZGUgPVxuICAgICAgICAgIHN0YXJ0Q29udGFpbmVyRWxlbWVudC5jaGlsZE5vZGVzW1xuICAgICAgICAgICAgc3RhcnRDb250YWluZXJFbGVtZW50LmNoaWxkTm9kZXMubGVuZ3RoIC0gMVxuICAgICAgICAgIF07XG4gICAgICAgIGlmIChsYXN0Q2hpbGROb2RlLm5vZGVUeXBlID09PSBOb2RlLkVMRU1FTlRfTk9ERSkge1xuICAgICAgICAgIHN0YXJ0RWxlbWVudE9yVGV4dENmaSA9XG4gICAgICAgICAgICBzdGFydEVsZW1lbnRDZmkgKyBcIi9cIiArIChjZmlJbmRleE9mTGFzdEVsZW1lbnQgKyAxKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBzdGFydEVsZW1lbnRPclRleHRDZmkgPVxuICAgICAgICAgICAgc3RhcnRFbGVtZW50Q2ZpICsgXCIvXCIgKyAoY2ZpSW5kZXhPZkxhc3RFbGVtZW50ICsgMik7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG4gICAgbGV0IGVuZEVsZW1lbnRPclRleHRDZmkgPSBlbmRFbGVtZW50Q2ZpO1xuICAgIGlmICghZW5kSXNFbGVtZW50KSB7XG4gICAgICBjb25zdCBlbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXhGb3JDZmkgPSBnZXRDaGlsZFRleHROb2RlQ2ZpSW5kZXgoXG4gICAgICAgIGVuZENvbnRhaW5lckVsZW1lbnQsXG4gICAgICAgIHJhbmdlLmVuZENvbnRhaW5lclxuICAgICAgKTtcbiAgICAgIGVuZEVsZW1lbnRPclRleHRDZmkgPVxuICAgICAgICBlbmRFbGVtZW50Q2ZpICtcbiAgICAgICAgXCIvXCIgK1xuICAgICAgICBlbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXhGb3JDZmkgK1xuICAgICAgICBcIjpcIiArXG4gICAgICAgIHJhbmdlLmVuZE9mZnNldDtcbiAgICB9IGVsc2Uge1xuICAgICAgaWYgKFxuICAgICAgICByYW5nZS5lbmRPZmZzZXQgPj0gMCAmJlxuICAgICAgICByYW5nZS5lbmRPZmZzZXQgPCBlbmRDb250YWluZXJFbGVtZW50LmNoaWxkTm9kZXMubGVuZ3RoXG4gICAgICApIHtcbiAgICAgICAgY29uc3QgY2hpbGROb2RlID0gZW5kQ29udGFpbmVyRWxlbWVudC5jaGlsZE5vZGVzW3JhbmdlLmVuZE9mZnNldF07XG4gICAgICAgIGlmIChjaGlsZE5vZGUubm9kZVR5cGUgPT09IE5vZGUuRUxFTUVOVF9OT0RFKSB7XG4gICAgICAgICAgZW5kRWxlbWVudE9yVGV4dENmaSA9IGVuZEVsZW1lbnRDZmkgKyBcIi9cIiArIChyYW5nZS5lbmRPZmZzZXQgKyAxKSAqIDI7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgY29uc3QgY2ZpVGV4dE5vZGVJbmRleCA9IGdldENoaWxkVGV4dE5vZGVDZmlJbmRleChcbiAgICAgICAgICAgIGVuZENvbnRhaW5lckVsZW1lbnQsXG4gICAgICAgICAgICBjaGlsZE5vZGVcbiAgICAgICAgICApO1xuICAgICAgICAgIGVuZEVsZW1lbnRPclRleHRDZmkgPSBlbmRFbGVtZW50Q2ZpICsgXCIvXCIgKyBjZmlUZXh0Tm9kZUluZGV4O1xuICAgICAgICB9XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBjb25zdCBjZmlJbmRleE9mTGFzdEVsZW1lbnQgPSBlbmRDb250YWluZXJFbGVtZW50LmNoaWxkRWxlbWVudENvdW50ICogMjtcbiAgICAgICAgY29uc3QgbGFzdENoaWxkTm9kZSA9XG4gICAgICAgICAgZW5kQ29udGFpbmVyRWxlbWVudC5jaGlsZE5vZGVzW1xuICAgICAgICAgICAgZW5kQ29udGFpbmVyRWxlbWVudC5jaGlsZE5vZGVzLmxlbmd0aCAtIDFcbiAgICAgICAgICBdO1xuICAgICAgICBpZiAobGFzdENoaWxkTm9kZS5ub2RlVHlwZSA9PT0gTm9kZS5FTEVNRU5UX05PREUpIHtcbiAgICAgICAgICBlbmRFbGVtZW50T3JUZXh0Q2ZpID1cbiAgICAgICAgICAgIGVuZEVsZW1lbnRDZmkgKyBcIi9cIiArIChjZmlJbmRleE9mTGFzdEVsZW1lbnQgKyAxKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBlbmRFbGVtZW50T3JUZXh0Q2ZpID1cbiAgICAgICAgICAgIGVuZEVsZW1lbnRDZmkgKyBcIi9cIiArIChjZmlJbmRleE9mTGFzdEVsZW1lbnQgKyAyKTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgICBjZmkgPVxuICAgICAgcm9vdEVsZW1lbnRDZmkgK1xuICAgICAgXCIsXCIgK1xuICAgICAgc3RhcnRFbGVtZW50T3JUZXh0Q2ZpLnJlcGxhY2Uocm9vdEVsZW1lbnRDZmksIFwiXCIpICtcbiAgICAgIFwiLFwiICtcbiAgICAgIGVuZEVsZW1lbnRPclRleHRDZmkucmVwbGFjZShyb290RWxlbWVudENmaSwgXCJcIik7XG4gIH1cbiAgcmV0dXJuIHtcbiAgICBjZmksXG4gICAgZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4LFxuICAgIGVuZENvbnRhaW5lckVsZW1lbnRDc3NTZWxlY3RvcixcbiAgICBlbmRPZmZzZXQ6IHJhbmdlLmVuZE9mZnNldCxcbiAgICBzdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCxcbiAgICBzdGFydENvbnRhaW5lckVsZW1lbnRDc3NTZWxlY3RvcixcbiAgICBzdGFydE9mZnNldDogcmFuZ2Uuc3RhcnRPZmZzZXQsXG4gIH07XG59XG5cbmZ1bmN0aW9uIGNvbnZlcnRSYW5nZUluZm8oZG9jdW1lbnQsIHJhbmdlSW5mbykge1xuICBjb25zdCBzdGFydEVsZW1lbnQgPSBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKFxuICAgIHJhbmdlSW5mby5zdGFydENvbnRhaW5lckVsZW1lbnRDc3NTZWxlY3RvclxuICApO1xuICBpZiAoIXN0YXJ0RWxlbWVudCkge1xuICAgIGNvbnNvbGUubG9nKFwiXl5eIGNvbnZlcnRSYW5nZUluZm8gTk8gU1RBUlQgRUxFTUVOVCBDU1MgU0VMRUNUT1I/IVwiKTtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG4gIGxldCBzdGFydENvbnRhaW5lciA9IHN0YXJ0RWxlbWVudDtcbiAgaWYgKHJhbmdlSW5mby5zdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCA+PSAwKSB7XG4gICAgaWYgKFxuICAgICAgcmFuZ2VJbmZvLnN0YXJ0Q29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4ID49XG4gICAgICBzdGFydEVsZW1lbnQuY2hpbGROb2Rlcy5sZW5ndGhcbiAgICApIHtcbiAgICAgIGNvbnNvbGUubG9nKFxuICAgICAgICBcIl5eXiBjb252ZXJ0UmFuZ2VJbmZvIHJhbmdlSW5mby5zdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCA+PSBzdGFydEVsZW1lbnQuY2hpbGROb2Rlcy5sZW5ndGg/IVwiXG4gICAgICApO1xuICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICB9XG4gICAgc3RhcnRDb250YWluZXIgPVxuICAgICAgc3RhcnRFbGVtZW50LmNoaWxkTm9kZXNbcmFuZ2VJbmZvLnN0YXJ0Q29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4XTtcbiAgICBpZiAoc3RhcnRDb250YWluZXIubm9kZVR5cGUgIT09IE5vZGUuVEVYVF9OT0RFKSB7XG4gICAgICBjb25zb2xlLmxvZyhcbiAgICAgICAgXCJeXl4gY29udmVydFJhbmdlSW5mbyBzdGFydENvbnRhaW5lci5ub2RlVHlwZSAhPT0gTm9kZS5URVhUX05PREU/IVwiXG4gICAgICApO1xuICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICB9XG4gIH1cbiAgY29uc3QgZW5kRWxlbWVudCA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoXG4gICAgcmFuZ2VJbmZvLmVuZENvbnRhaW5lckVsZW1lbnRDc3NTZWxlY3RvclxuICApO1xuICBpZiAoIWVuZEVsZW1lbnQpIHtcbiAgICBjb25zb2xlLmxvZyhcIl5eXiBjb252ZXJ0UmFuZ2VJbmZvIE5PIEVORCBFTEVNRU5UIENTUyBTRUxFQ1RPUj8hXCIpO1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgbGV0IGVuZENvbnRhaW5lciA9IGVuZEVsZW1lbnQ7XG4gIGlmIChyYW5nZUluZm8uZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4ID49IDApIHtcbiAgICBpZiAoXG4gICAgICByYW5nZUluZm8uZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4ID49IGVuZEVsZW1lbnQuY2hpbGROb2Rlcy5sZW5ndGhcbiAgICApIHtcbiAgICAgIGNvbnNvbGUubG9nKFxuICAgICAgICBcIl5eXiBjb252ZXJ0UmFuZ2VJbmZvIHJhbmdlSW5mby5lbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXggPj0gZW5kRWxlbWVudC5jaGlsZE5vZGVzLmxlbmd0aD8hXCJcbiAgICAgICk7XG4gICAgICByZXR1cm4gdW5kZWZpbmVkO1xuICAgIH1cbiAgICBlbmRDb250YWluZXIgPVxuICAgICAgZW5kRWxlbWVudC5jaGlsZE5vZGVzW3JhbmdlSW5mby5lbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXhdO1xuICAgIGlmIChlbmRDb250YWluZXIubm9kZVR5cGUgIT09IE5vZGUuVEVYVF9OT0RFKSB7XG4gICAgICBjb25zb2xlLmxvZyhcbiAgICAgICAgXCJeXl4gY29udmVydFJhbmdlSW5mbyBlbmRDb250YWluZXIubm9kZVR5cGUgIT09IE5vZGUuVEVYVF9OT0RFPyFcIlxuICAgICAgKTtcbiAgICAgIHJldHVybiB1bmRlZmluZWQ7XG4gICAgfVxuICB9XG4gIHJldHVybiBjcmVhdGVPcmRlcmVkUmFuZ2UoXG4gICAgc3RhcnRDb250YWluZXIsXG4gICAgcmFuZ2VJbmZvLnN0YXJ0T2Zmc2V0LFxuICAgIGVuZENvbnRhaW5lcixcbiAgICByYW5nZUluZm8uZW5kT2Zmc2V0XG4gICk7XG59XG5cbmZ1bmN0aW9uIGZyYW1lRm9ySGlnaGxpZ2h0QW5ub3RhdGlvbk1hcmtXaXRoSUQod2luLCBpZCkge1xuICBsZXQgY2xpZW50UmVjdHMgPSBmcmFtZUZvckhpZ2hsaWdodFdpdGhJRChpZCk7XG4gIGlmICghY2xpZW50UmVjdHMpIHJldHVybjtcblxuICB2YXIgdG9wQ2xpZW50UmVjdCA9IGNsaWVudFJlY3RzWzBdO1xuICB2YXIgbWF4SGVpZ2h0ID0gdG9wQ2xpZW50UmVjdC5oZWlnaHQ7XG4gIGZvciAoY29uc3QgY2xpZW50UmVjdCBvZiBjbGllbnRSZWN0cykge1xuICAgIGlmIChjbGllbnRSZWN0LnRvcCA8IHRvcENsaWVudFJlY3QudG9wKSB0b3BDbGllbnRSZWN0ID0gY2xpZW50UmVjdDtcbiAgICBpZiAoY2xpZW50UmVjdC5oZWlnaHQgPiBtYXhIZWlnaHQpIG1heEhlaWdodCA9IGNsaWVudFJlY3QuaGVpZ2h0O1xuICB9XG5cbiAgY29uc3QgZG9jdW1lbnQgPSB3aW4uZG9jdW1lbnQ7XG5cbiAgY29uc3Qgc2Nyb2xsRWxlbWVudCA9IGdldFNjcm9sbGluZ0VsZW1lbnQoZG9jdW1lbnQpO1xuICBjb25zdCBwYWdpbmF0ZWQgPSBpc1BhZ2luYXRlZChkb2N1bWVudCk7XG4gIGNvbnN0IGJvZHlSZWN0ID0gZG9jdW1lbnQuYm9keS5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKTtcbiAgbGV0IHlPZmZzZXQ7XG4gIGlmIChuYXZpZ2F0b3IudXNlckFnZW50Lm1hdGNoKC9BbmRyb2lkL2kpKSB7XG4gICAgeU9mZnNldCA9IHBhZ2luYXRlZCA/IC1zY3JvbGxFbGVtZW50LnNjcm9sbFRvcCA6IGJvZHlSZWN0LnRvcDtcbiAgfSBlbHNlIGlmIChuYXZpZ2F0b3IudXNlckFnZW50Lm1hdGNoKC9pUGhvbmV8aVBhZHxpUG9kL2kpKSB7XG4gICAgeU9mZnNldCA9IHBhZ2luYXRlZCA/IDAgOiBib2R5UmVjdC50b3A7XG4gIH1cbiAgdmFyIG5ld1RvcCA9IHRvcENsaWVudFJlY3QudG9wO1xuXG4gIGlmIChfaGlnaGxpZ2h0c0NvbnRhaW5lcikge1xuICAgIGRvIHtcbiAgICAgIHZhciBib3VuZGluZ0FyZWFzID0gZG9jdW1lbnQuZ2V0RWxlbWVudHNCeUNsYXNzTmFtZShcbiAgICAgICAgQ0xBU1NfQU5OT1RBVElPTl9CT1VORElOR19BUkVBXG4gICAgICApO1xuICAgICAgdmFyIGZvdW5kID0gZmFsc2U7XG4gICAgICAvL2ZvciAobGV0IGkgPSAwLCBsZW5ndGggPSBib3VuZGluZ0FyZWFzLnNuYXBzaG90TGVuZ3RoOyBpIDwgbGVuZ3RoOyArK2kpIHtcbiAgICAgIGZvciAoXG4gICAgICAgIHZhciBpID0gMCwgbGVuID0gYm91bmRpbmdBcmVhcy5sZW5ndGggfCAwO1xuICAgICAgICBpIDwgbGVuO1xuICAgICAgICBpID0gKGkgKyAxKSB8IDBcbiAgICAgICkge1xuICAgICAgICB2YXIgYm91bmRpbmdBcmVhID0gYm91bmRpbmdBcmVhc1tpXTtcbiAgICAgICAgaWYgKE1hdGguYWJzKGJvdW5kaW5nQXJlYS5yZWN0LnRvcCAtIChuZXdUb3AgLSB5T2Zmc2V0KSkgPCAzKSB7XG4gICAgICAgICAgbmV3VG9wICs9IGJvdW5kaW5nQXJlYS5yZWN0LmhlaWdodDtcbiAgICAgICAgICBmb3VuZCA9IHRydWU7XG4gICAgICAgICAgYnJlYWs7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9IHdoaWxlIChmb3VuZCk7XG4gIH1cblxuICB0b3BDbGllbnRSZWN0LnRvcCA9IG5ld1RvcDtcbiAgdG9wQ2xpZW50UmVjdC5oZWlnaHQgPSBtYXhIZWlnaHQ7XG5cbiAgcmV0dXJuIHRvcENsaWVudFJlY3Q7XG59XG5cbmZ1bmN0aW9uIGhpZ2hsaWdodFdpdGhJRChpZCkge1xuICBsZXQgaSA9IC0xO1xuICBjb25zdCBoaWdobGlnaHQgPSBfaGlnaGxpZ2h0cy5maW5kKChoLCBqKSA9PiB7XG4gICAgaSA9IGo7XG4gICAgcmV0dXJuIGguaWQgPT09IGlkO1xuICB9KTtcbiAgcmV0dXJuIGhpZ2hsaWdodDtcbn1cblxuZnVuY3Rpb24gZnJhbWVGb3JIaWdobGlnaHRXaXRoSUQoaWQpIHtcbiAgY29uc3QgaGlnaGxpZ2h0ID0gaGlnaGxpZ2h0V2l0aElEKGlkKTtcbiAgaWYgKCFoaWdobGlnaHQpIHJldHVybjtcblxuICBjb25zdCBkb2N1bWVudCA9IHdpbmRvdy5kb2N1bWVudDtcbiAgY29uc3Qgc2Nyb2xsRWxlbWVudCA9IGdldFNjcm9sbGluZ0VsZW1lbnQoZG9jdW1lbnQpO1xuICBjb25zdCByYW5nZSA9IGNvbnZlcnRSYW5nZUluZm8oZG9jdW1lbnQsIGhpZ2hsaWdodC5yYW5nZUluZm8pO1xuICBpZiAoIXJhbmdlKSB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuXG4gIGNvbnN0IGRyYXdVbmRlcmxpbmUgPSBmYWxzZTtcbiAgY29uc3QgZHJhd1N0cmlrZVRocm91Z2ggPSBmYWxzZTtcbiAgY29uc3QgZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0cyA9IGRyYXdVbmRlcmxpbmUgfHwgZHJhd1N0cmlrZVRocm91Z2g7XG4gIC8vY29uc3QgY2xpZW50UmVjdHMgPSBERUJVR19WSVNVQUxTID8gcmFuZ2UuZ2V0Q2xpZW50UmVjdHMoKSA6XG4gIGNvbnN0IGNsaWVudFJlY3RzID0gZ2V0Q2xpZW50UmVjdHNOb092ZXJsYXAoXG4gICAgcmFuZ2UsXG4gICAgZG9Ob3RNZXJnZUhvcml6b250YWxseUFsaWduZWRSZWN0c1xuICApO1xuXG4gIHJldHVybiBjbGllbnRSZWN0cztcbn1cblxuZnVuY3Rpb24gcmFuZ2VJbmZvMkxvY2F0aW9uKHJhbmdlSW5mbykge1xuICByZXR1cm4ge1xuICAgIGNzc1NlbGVjdG9yOiByYW5nZUluZm8uc3RhcnRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3IsXG4gICAgcGFydGlhbENmaTogcmFuZ2VJbmZvLmNmaSxcbiAgICBkb21SYW5nZToge1xuICAgICAgc3RhcnQ6IHtcbiAgICAgICAgY3NzU2VsZWN0b3I6IHJhbmdlSW5mby5zdGFydENvbnRhaW5lckVsZW1lbnRDc3NTZWxlY3RvcixcbiAgICAgICAgdGV4dE5vZGVJbmRleDogcmFuZ2VJbmZvLnN0YXJ0Q29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4LFxuICAgICAgICBvZmZzZXQ6IHJhbmdlSW5mby5zdGFydE9mZnNldCxcbiAgICAgIH0sXG4gICAgICBlbmQ6IHtcbiAgICAgICAgY3NzU2VsZWN0b3I6IHJhbmdlSW5mby5lbmRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3IsXG4gICAgICAgIHRleHROb2RlSW5kZXg6IHJhbmdlSW5mby5lbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXgsXG4gICAgICAgIG9mZnNldDogcmFuZ2VJbmZvLmVuZE9mZnNldCxcbiAgICAgIH0sXG4gICAgfSxcbiAgfTtcbn1cblxuZnVuY3Rpb24gbG9jYXRpb24yUmFuZ2VJbmZvKGxvY2F0aW9uKSB7XG4gIGNvbnN0IGxvY2F0aW9ucyA9IGxvY2F0aW9uLmxvY2F0aW9ucztcbiAgY29uc3QgZG9tUmFuZ2UgPSBsb2NhdGlvbnMuZG9tUmFuZ2U7XG4gIGNvbnN0IHN0YXJ0ID0gZG9tUmFuZ2Uuc3RhcnQ7XG4gIGNvbnN0IGVuZCA9IGRvbVJhbmdlLmVuZDtcblxuICByZXR1cm4ge1xuICAgIGNmaTogbG9jYXRpb24ucGFydGlhbENmaSxcbiAgICBlbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXg6IGVuZC50ZXh0Tm9kZUluZGV4LFxuICAgIGVuZENvbnRhaW5lckVsZW1lbnRDc3NTZWxlY3RvcjogZW5kLmNzc1NlbGVjdG9yLFxuICAgIGVuZE9mZnNldDogZW5kLm9mZnNldCxcbiAgICBzdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleDogc3RhcnQudGV4dE5vZGVJbmRleCxcbiAgICBzdGFydENvbnRhaW5lckVsZW1lbnRDc3NTZWxlY3Rvcjogc3RhcnQuY3NzU2VsZWN0b3IsXG4gICAgc3RhcnRPZmZzZXQ6IHN0YXJ0Lm9mZnNldCxcbiAgfTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHJlY3RhbmdsZUZvckhpZ2hsaWdodFdpdGhJRChpZCkge1xuICBjb25zdCBoaWdobGlnaHQgPSBoaWdobGlnaHRXaXRoSUQoaWQpO1xuICBpZiAoIWhpZ2hsaWdodCkgcmV0dXJuO1xuXG4gIGNvbnN0IGRvY3VtZW50ID0gd2luZG93LmRvY3VtZW50O1xuICBjb25zdCBzY3JvbGxFbGVtZW50ID0gZ2V0U2Nyb2xsaW5nRWxlbWVudChkb2N1bWVudCk7XG4gIGNvbnN0IHJhbmdlID0gY29udmVydFJhbmdlSW5mbyhkb2N1bWVudCwgaGlnaGxpZ2h0LnJhbmdlSW5mbyk7XG4gIGlmICghcmFuZ2UpIHtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG5cbiAgY29uc3QgZHJhd1VuZGVybGluZSA9IGZhbHNlO1xuICBjb25zdCBkcmF3U3RyaWtlVGhyb3VnaCA9IGZhbHNlO1xuICBjb25zdCBkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzID0gZHJhd1VuZGVybGluZSB8fCBkcmF3U3RyaWtlVGhyb3VnaDtcbiAgLy9jb25zdCBjbGllbnRSZWN0cyA9IERFQlVHX1ZJU1VBTFMgPyByYW5nZS5nZXRDbGllbnRSZWN0cygpIDpcbiAgY29uc3QgY2xpZW50UmVjdHMgPSBnZXRDbGllbnRSZWN0c05vT3ZlcmxhcChcbiAgICByYW5nZSxcbiAgICBkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzXG4gICk7XG4gIHZhciBzaXplID0ge1xuICAgIHNjcmVlbldpZHRoOiB3aW5kb3cub3V0ZXJXaWR0aCxcbiAgICBzY3JlZW5IZWlnaHQ6IHdpbmRvdy5vdXRlckhlaWdodCxcbiAgICBsZWZ0OiBjbGllbnRSZWN0c1swXS5sZWZ0LFxuICAgIHdpZHRoOiBjbGllbnRSZWN0c1swXS53aWR0aCxcbiAgICB0b3A6IGNsaWVudFJlY3RzWzBdLnRvcCxcbiAgICBoZWlnaHQ6IGNsaWVudFJlY3RzWzBdLmhlaWdodCxcbiAgfTtcblxuICByZXR1cm4gc2l6ZTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGdldFNlbGVjdGlvblJlY3QoKSB7XG4gIHRyeSB7XG4gICAgdmFyIHNlbCA9IHdpbmRvdy5nZXRTZWxlY3Rpb24oKTtcbiAgICBpZiAoIXNlbCkge1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICB2YXIgcmFuZ2UgPSBzZWwuZ2V0UmFuZ2VBdCgwKTtcblxuICAgIGNvbnN0IGNsaWVudFJlY3QgPSByYW5nZS5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKTtcblxuICAgIHZhciBoYW5kbGVCb3VuZHMgPSB7XG4gICAgICBzY3JlZW5XaWR0aDogd2luZG93Lm91dGVyV2lkdGgsXG4gICAgICBzY3JlZW5IZWlnaHQ6IHdpbmRvdy5vdXRlckhlaWdodCxcbiAgICAgIGxlZnQ6IGNsaWVudFJlY3QubGVmdCxcbiAgICAgIHdpZHRoOiBjbGllbnRSZWN0LndpZHRoLFxuICAgICAgdG9wOiBjbGllbnRSZWN0LnRvcCxcbiAgICAgIGhlaWdodDogY2xpZW50UmVjdC5oZWlnaHQsXG4gICAgfTtcbiAgICByZXR1cm4gaGFuZGxlQm91bmRzO1xuICB9IGNhdGNoIChlKSB7XG4gICAgcmV0dXJuIG51bGw7XG4gIH1cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHNldFNjcm9sbE1vZGUoZmxhZykge1xuICBpZiAoIWZsYWcpIHtcbiAgICBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuY2xhc3NMaXN0LmFkZChDTEFTU19QQUdJTkFURUQpO1xuICB9IGVsc2Uge1xuICAgIGRvY3VtZW50LmRvY3VtZW50RWxlbWVudC5jbGFzc0xpc3QucmVtb3ZlKENMQVNTX1BBR0lOQVRFRCk7XG4gIH1cbn1cblxuLypcbiBpZiAoZG9jdW1lbnQuYWRkRXZlbnRMaXN0ZW5lcikgeyAvLyBJRSA+PSA5OyBvdGhlciBicm93c2Vyc1xuICAgICAgICBkb2N1bWVudC5hZGRFdmVudExpc3RlbmVyKCdjb250ZXh0bWVudScsIGZ1bmN0aW9uKGUpIHtcbiAgICAgICAgICAgIC8vYWxlcnQoXCJZb3UndmUgdHJpZWQgdG8gb3BlbiBjb250ZXh0IG1lbnVcIik7IC8vaGVyZSB5b3UgZHJhdyB5b3VyIG93biBtZW51XG4gICAgICAgICAgICAvL2UucHJldmVudERlZmF1bHQoKTtcbiAgICAgICAgICAgIC8vbGV0IGdldENzc1NlbGVjdG9yID0gZnVsbFF1YWxpZmllZFNlbGVjdG9yO1xuICAgICAgICAgICAgXG5cdFx0XHRsZXQgc3RyID0gd2luZG93LmdldFNlbGVjdGlvbigpO1xuXHRcdFx0bGV0IHNlbGVjdGlvbkluZm8gPSBnZXRDdXJyZW50U2VsZWN0aW9uSW5mbygpO1xuXHRcdFx0bGV0IHBvcyA9IGNyZWF0ZUhpZ2hsaWdodChzZWxlY3Rpb25JbmZvLHtyZWQ6MTAsZ3JlZW46NTAsYmx1ZToyMzB9LHRydWUpO1xuXHRcdFx0bGV0IHJldDIgPSBjcmVhdGVBbm5vdGF0aW9uKHBvcy5pZCk7XG5cdFx0XHRcbiAgfSwgZmFsc2UpO1xuICAgIH0gZWxzZSB7IC8vIElFIDwgOVxuICAgICAgICBkb2N1bWVudC5hdHRhY2hFdmVudCgnb25jb250ZXh0bWVudScsIGZ1bmN0aW9uKCkge1xuICAgICAgICAgICAgYWxlcnQoXCJZb3UndmUgdHJpZWQgdG8gb3BlbiBjb250ZXh0IG1lbnVcIik7XG4gICAgICAgICAgICB3aW5kb3cuZXZlbnQucmV0dXJuVmFsdWUgPSBmYWxzZTtcbiAgICAgICAgfSk7XG4gICAgfVxuKi9cbiIsIi8vXG4vLyAgQ29weXJpZ2h0IDIwMjIgUmVhZGl1bSBGb3VuZGF0aW9uLiBBbGwgcmlnaHRzIHJlc2VydmVkLlxuLy8gIFVzZSBvZiB0aGlzIHNvdXJjZSBjb2RlIGlzIGdvdmVybmVkIGJ5IHRoZSBCU0Qtc3R5bGUgbGljZW5zZVxuLy8gIGF2YWlsYWJsZSBpbiB0aGUgdG9wLWxldmVsIExJQ0VOU0UgZmlsZSBvZiB0aGUgcHJvamVjdC5cbi8vXG5cbmltcG9ydCB7IGlzU2Nyb2xsTW9kZUVuYWJsZWQgfSBmcm9tIFwiLi91dGlsc1wiO1xuaW1wb3J0IHsgZ2V0Q3NzU2VsZWN0b3IgfSBmcm9tIFwiY3NzLXNlbGVjdG9yLWdlbmVyYXRvclwiO1xuXG5leHBvcnQgZnVuY3Rpb24gZmluZEZpcnN0VmlzaWJsZUxvY2F0b3IoKSB7XG4gIGNvbnN0IGVsZW1lbnQgPSBmaW5kRWxlbWVudChkb2N1bWVudC5ib2R5KTtcbiAgcmV0dXJuIHtcbiAgICBocmVmOiBcIiNcIixcbiAgICB0eXBlOiBcImFwcGxpY2F0aW9uL3hodG1sK3htbFwiLFxuICAgIGxvY2F0aW9uczoge1xuICAgICAgY3NzU2VsZWN0b3I6IGdldENzc1NlbGVjdG9yKGVsZW1lbnQpLFxuICAgIH0sXG4gICAgdGV4dDoge1xuICAgICAgaGlnaGxpZ2h0OiBlbGVtZW50LnRleHRDb250ZW50LFxuICAgIH0sXG4gIH07XG59XG5cbmZ1bmN0aW9uIGZpbmRFbGVtZW50KHJvb3RFbGVtZW50KSB7XG4gIGZvciAodmFyIGkgPSAwOyBpIDwgcm9vdEVsZW1lbnQuY2hpbGRyZW4ubGVuZ3RoOyBpKyspIHtcbiAgICBjb25zdCBjaGlsZCA9IHJvb3RFbGVtZW50LmNoaWxkcmVuW2ldO1xuICAgIGlmICghc2hvdWxkSWdub3JlRWxlbWVudChjaGlsZCkgJiYgaXNFbGVtZW50VmlzaWJsZShjaGlsZCkpIHtcbiAgICAgIHJldHVybiBmaW5kRWxlbWVudChjaGlsZCk7XG4gICAgfVxuICB9XG4gIHJldHVybiByb290RWxlbWVudDtcbn1cblxuZnVuY3Rpb24gaXNFbGVtZW50VmlzaWJsZShlbGVtZW50KSB7XG4gIGlmIChyZWFkaXVtLmlzRml4ZWRMYXlvdXQpIHJldHVybiB0cnVlO1xuXG4gIGlmIChlbGVtZW50ID09PSBkb2N1bWVudC5ib2R5IHx8IGVsZW1lbnQgPT09IGRvY3VtZW50LmRvY3VtZW50RWxlbWVudCkge1xuICAgIHJldHVybiB0cnVlO1xuICB9XG4gIGlmICghZG9jdW1lbnQgfHwgIWRvY3VtZW50LmRvY3VtZW50RWxlbWVudCB8fCAhZG9jdW1lbnQuYm9keSkge1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuXG4gIGNvbnN0IHJlY3QgPSBlbGVtZW50LmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpO1xuICBpZiAoaXNTY3JvbGxNb2RlRW5hYmxlZCgpKSB7XG4gICAgcmV0dXJuIHJlY3QuYm90dG9tID4gMCAmJiByZWN0LnRvcCA8IHdpbmRvdy5pbm5lckhlaWdodDtcbiAgfSBlbHNlIHtcbiAgICByZXR1cm4gcmVjdC5yaWdodCA+IDAgJiYgcmVjdC5sZWZ0IDwgd2luZG93LmlubmVyV2lkdGg7XG4gIH1cbn1cblxuZnVuY3Rpb24gc2hvdWxkSWdub3JlRWxlbWVudChlbGVtZW50KSB7XG4gIGNvbnN0IGVsU3R5bGUgPSBnZXRDb21wdXRlZFN0eWxlKGVsZW1lbnQpO1xuICBpZiAoZWxTdHlsZSkge1xuICAgIGNvbnN0IGRpc3BsYXkgPSBlbFN0eWxlLmdldFByb3BlcnR5VmFsdWUoXCJkaXNwbGF5XCIpO1xuICAgIGlmIChkaXNwbGF5ICE9IFwiYmxvY2tcIikge1xuICAgICAgcmV0dXJuIHRydWU7XG4gICAgfVxuICAgIC8vIENhbm5vdCBiZSByZWxpZWQgdXBvbiwgYmVjYXVzZSB3ZWIgYnJvd3NlciBlbmdpbmUgcmVwb3J0cyBpbnZpc2libGUgd2hlbiBvdXQgb2YgdmlldyBpblxuICAgIC8vIHNjcm9sbGVkIGNvbHVtbnMhXG4gICAgLy8gY29uc3QgdmlzaWJpbGl0eSA9IGVsU3R5bGUuZ2V0UHJvcGVydHlWYWx1ZShcInZpc2liaWxpdHlcIik7XG4gICAgLy8gaWYgKHZpc2liaWxpdHkgPT09IFwiaGlkZGVuXCIpIHtcbiAgICAvLyAgICAgcmV0dXJuIGZhbHNlO1xuICAgIC8vIH1cbiAgICBjb25zdCBvcGFjaXR5ID0gZWxTdHlsZS5nZXRQcm9wZXJ0eVZhbHVlKFwib3BhY2l0eVwiKTtcbiAgICBpZiAob3BhY2l0eSA9PT0gXCIwXCIpIHtcbiAgICAgIHJldHVybiB0cnVlO1xuICAgIH1cbiAgfVxuXG4gIHJldHVybiBmYWxzZTtcbn1cbiIsIi8vXG4vLyAgQ29weXJpZ2h0IDIwMjEgUmVhZGl1bSBGb3VuZGF0aW9uLiBBbGwgcmlnaHRzIHJlc2VydmVkLlxuLy8gIFVzZSBvZiB0aGlzIHNvdXJjZSBjb2RlIGlzIGdvdmVybmVkIGJ5IHRoZSBCU0Qtc3R5bGUgbGljZW5zZVxuLy8gIGF2YWlsYWJsZSBpbiB0aGUgdG9wLWxldmVsIExJQ0VOU0UgZmlsZSBvZiB0aGUgcHJvamVjdC5cbi8vXG5cbmltcG9ydCB7IGxvZyBhcyBsb2dOYXRpdmUsIGxvZ0Vycm9yLCBzbmFwQ3VycmVudE9mZnNldCB9IGZyb20gXCIuL3V0aWxzXCI7XG5pbXBvcnQgeyB0b05hdGl2ZVJlY3QgfSBmcm9tIFwiLi9yZWN0XCI7XG5pbXBvcnQgeyBUZXh0UmFuZ2UgfSBmcm9tIFwiLi92ZW5kb3IvaHlwb3RoZXNpcy9hbmNob3JpbmcvdGV4dC1yYW5nZVwiO1xuXG4vLyBQb2x5ZmlsbCBmb3IgQW5kcm9pZCBBUEkgMjZcbmltcG9ydCBtYXRjaEFsbCBmcm9tIFwic3RyaW5nLnByb3RvdHlwZS5tYXRjaGFsbFwiO1xubWF0Y2hBbGwuc2hpbSgpO1xuXG5jb25zdCBkZWJ1ZyA9IHRydWU7XG5cbi8vIE5vdGlmeSBuYXRpdmUgY29kZSB0aGF0IHRoZSBzZWxlY3Rpb24gY2hhbmdlcy5cbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKFxuICBcImxvYWRcIixcbiAgZnVuY3Rpb24gKCkge1xuICAgIHZhciBpc1NlbGVjdGluZyA9IGZhbHNlO1xuICAgIGRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoXCJzZWxlY3Rpb25jaGFuZ2VcIiwgZnVuY3Rpb24gKCkge1xuICAgICAgY29uc3QgY29sbGFwc2VkID0gd2luZG93LmdldFNlbGVjdGlvbigpLmlzQ29sbGFwc2VkO1xuXG4gICAgICBpZiAoY29sbGFwc2VkICYmIGlzU2VsZWN0aW5nKSB7XG4gICAgICAgIGlzU2VsZWN0aW5nID0gZmFsc2U7XG4gICAgICAgIEFuZHJvaWQub25TZWxlY3Rpb25FbmQoKTtcbiAgICAgICAgLy8gU25hcHMgdGhlIGN1cnJlbnQgY29sdW1uIGluIGNhc2UgdGhlIHVzZXIgc2hpZnRlZCB0aGUgc2Nyb2xsIGJ5IGRyYWdnaW5nIHRoZSB0ZXh0IHNlbGVjdGlvbi5cbiAgICAgICAgc25hcEN1cnJlbnRPZmZzZXQoKTtcbiAgICAgIH0gZWxzZSBpZiAoIWNvbGxhcHNlZCAmJiAhaXNTZWxlY3RpbmcpIHtcbiAgICAgICAgaXNTZWxlY3RpbmcgPSB0cnVlO1xuICAgICAgICBBbmRyb2lkLm9uU2VsZWN0aW9uU3RhcnQoKTtcbiAgICAgIH1cbiAgICB9KTtcbiAgfSxcbiAgZmFsc2Vcbik7XG5cbmV4cG9ydCBmdW5jdGlvbiBnZXRDdXJyZW50U2VsZWN0aW9uKCkge1xuICBjb25zdCB0ZXh0ID0gZ2V0Q3VycmVudFNlbGVjdGlvblRleHQoKTtcbiAgaWYgKCF0ZXh0KSB7XG4gICAgcmV0dXJuIG51bGw7XG4gIH1cbiAgY29uc3QgcmVjdCA9IGdldFNlbGVjdGlvblJlY3QoKTtcbiAgcmV0dXJuIHsgdGV4dCwgcmVjdCB9O1xufVxuXG5mdW5jdGlvbiBnZXRTZWxlY3Rpb25SZWN0KCkge1xuICB0cnkge1xuICAgIGxldCBzZWwgPSB3aW5kb3cuZ2V0U2VsZWN0aW9uKCk7XG4gICAgaWYgKCFzZWwpIHtcbiAgICAgIHJldHVybjtcbiAgICB9XG4gICAgbGV0IHJhbmdlID0gc2VsLmdldFJhbmdlQXQoMCk7XG5cbiAgICByZXR1cm4gdG9OYXRpdmVSZWN0KHJhbmdlLmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpKTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIGxvZ0Vycm9yKGUpO1xuICAgIHJldHVybiBudWxsO1xuICB9XG59XG5cbmZ1bmN0aW9uIGdldEN1cnJlbnRTZWxlY3Rpb25UZXh0KCkge1xuICBjb25zdCBzZWxlY3Rpb24gPSB3aW5kb3cuZ2V0U2VsZWN0aW9uKCk7XG4gIGlmICghc2VsZWN0aW9uKSB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuICBpZiAoc2VsZWN0aW9uLmlzQ29sbGFwc2VkKSB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuICBjb25zdCBoaWdobGlnaHQgPSBzZWxlY3Rpb24udG9TdHJpbmcoKTtcbiAgY29uc3QgY2xlYW5IaWdobGlnaHQgPSBoaWdobGlnaHRcbiAgICAudHJpbSgpXG4gICAgLnJlcGxhY2UoL1xcbi9nLCBcIiBcIilcbiAgICAucmVwbGFjZSgvXFxzXFxzKy9nLCBcIiBcIik7XG4gIGlmIChjbGVhbkhpZ2hsaWdodC5sZW5ndGggPT09IDApIHtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG4gIGlmICghc2VsZWN0aW9uLmFuY2hvck5vZGUgfHwgIXNlbGVjdGlvbi5mb2N1c05vZGUpIHtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG4gIGNvbnN0IHJhbmdlID1cbiAgICBzZWxlY3Rpb24ucmFuZ2VDb3VudCA9PT0gMVxuICAgICAgPyBzZWxlY3Rpb24uZ2V0UmFuZ2VBdCgwKVxuICAgICAgOiBjcmVhdGVPcmRlcmVkUmFuZ2UoXG4gICAgICAgICAgc2VsZWN0aW9uLmFuY2hvck5vZGUsXG4gICAgICAgICAgc2VsZWN0aW9uLmFuY2hvck9mZnNldCxcbiAgICAgICAgICBzZWxlY3Rpb24uZm9jdXNOb2RlLFxuICAgICAgICAgIHNlbGVjdGlvbi5mb2N1c09mZnNldFxuICAgICAgICApO1xuICBpZiAoIXJhbmdlIHx8IHJhbmdlLmNvbGxhcHNlZCkge1xuICAgIGxvZyhcIiQkJCQkJCQkJCQkJCQkJCQkIENBTk5PVCBHRVQgTk9OLUNPTExBUFNFRCBTRUxFQ1RJT04gUkFOR0U/IVwiKTtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG5cbiAgY29uc3QgdGV4dCA9IGRvY3VtZW50LmJvZHkudGV4dENvbnRlbnQ7XG4gIGNvbnN0IHRleHRSYW5nZSA9IFRleHRSYW5nZS5mcm9tUmFuZ2UocmFuZ2UpLnJlbGF0aXZlVG8oZG9jdW1lbnQuYm9keSk7XG4gIGNvbnN0IHN0YXJ0ID0gdGV4dFJhbmdlLnN0YXJ0Lm9mZnNldDtcbiAgY29uc3QgZW5kID0gdGV4dFJhbmdlLmVuZC5vZmZzZXQ7XG5cbiAgY29uc3Qgc25pcHBldExlbmd0aCA9IDIwMDtcblxuICAvLyBDb21wdXRlIHRoZSB0ZXh0IGJlZm9yZSB0aGUgaGlnaGxpZ2h0LCBpZ25vcmluZyB0aGUgZmlyc3QgXCJ3b3JkXCIsIHdoaWNoIG1pZ2h0IGJlIGN1dC5cbiAgbGV0IGJlZm9yZSA9IHRleHQuc2xpY2UoTWF0aC5tYXgoMCwgc3RhcnQgLSBzbmlwcGV0TGVuZ3RoKSwgc3RhcnQpO1xuICBsZXQgZmlyc3RXb3JkU3RhcnQgPSBiZWZvcmUuc2VhcmNoKC9cXFB7TH1cXHB7TH0vZ3UpO1xuICBpZiAoZmlyc3RXb3JkU3RhcnQgIT09IC0xKSB7XG4gICAgYmVmb3JlID0gYmVmb3JlLnNsaWNlKGZpcnN0V29yZFN0YXJ0ICsgMSk7XG4gIH1cblxuICAvLyBDb21wdXRlIHRoZSB0ZXh0IGFmdGVyIHRoZSBoaWdobGlnaHQsIGlnbm9yaW5nIHRoZSBsYXN0IFwid29yZFwiLCB3aGljaCBtaWdodCBiZSBjdXQuXG4gIGxldCBhZnRlciA9IHRleHQuc2xpY2UoZW5kLCBNYXRoLm1pbih0ZXh0Lmxlbmd0aCwgZW5kICsgc25pcHBldExlbmd0aCkpO1xuICBsZXQgbGFzdFdvcmRFbmQgPSBBcnJheS5mcm9tKGFmdGVyLm1hdGNoQWxsKC9cXHB7TH1cXFB7TH0vZ3UpKS5wb3AoKTtcbiAgaWYgKGxhc3RXb3JkRW5kICE9PSB1bmRlZmluZWQgJiYgbGFzdFdvcmRFbmQuaW5kZXggPiAxKSB7XG4gICAgYWZ0ZXIgPSBhZnRlci5zbGljZSgwLCBsYXN0V29yZEVuZC5pbmRleCArIDEpO1xuICB9XG5cbiAgcmV0dXJuIHsgaGlnaGxpZ2h0LCBiZWZvcmUsIGFmdGVyIH07XG59XG5cbmZ1bmN0aW9uIGNyZWF0ZU9yZGVyZWRSYW5nZShzdGFydE5vZGUsIHN0YXJ0T2Zmc2V0LCBlbmROb2RlLCBlbmRPZmZzZXQpIHtcbiAgY29uc3QgcmFuZ2UgPSBuZXcgUmFuZ2UoKTtcbiAgcmFuZ2Uuc2V0U3RhcnQoc3RhcnROb2RlLCBzdGFydE9mZnNldCk7XG4gIHJhbmdlLnNldEVuZChlbmROb2RlLCBlbmRPZmZzZXQpO1xuICBpZiAoIXJhbmdlLmNvbGxhcHNlZCkge1xuICAgIHJldHVybiByYW5nZTtcbiAgfVxuICBsb2coXCI+Pj4gY3JlYXRlT3JkZXJlZFJhbmdlIENPTExBUFNFRCAuLi4gUkFOR0UgUkVWRVJTRT9cIik7XG4gIGNvbnN0IHJhbmdlUmV2ZXJzZSA9IG5ldyBSYW5nZSgpO1xuICByYW5nZVJldmVyc2Uuc2V0U3RhcnQoZW5kTm9kZSwgZW5kT2Zmc2V0KTtcbiAgcmFuZ2VSZXZlcnNlLnNldEVuZChzdGFydE5vZGUsIHN0YXJ0T2Zmc2V0KTtcbiAgaWYgKCFyYW5nZVJldmVyc2UuY29sbGFwc2VkKSB7XG4gICAgbG9nKFwiPj4+IGNyZWF0ZU9yZGVyZWRSYW5nZSBSQU5HRSBSRVZFUlNFIE9LLlwiKTtcbiAgICByZXR1cm4gcmFuZ2U7XG4gIH1cbiAgbG9nKFwiPj4+IGNyZWF0ZU9yZGVyZWRSYW5nZSBSQU5HRSBSRVZFUlNFIEFMU08gQ09MTEFQU0VEPyFcIik7XG4gIHJldHVybiB1bmRlZmluZWQ7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBjb252ZXJ0UmFuZ2VJbmZvKGRvY3VtZW50LCByYW5nZUluZm8pIHtcbiAgY29uc3Qgc3RhcnRFbGVtZW50ID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvcihcbiAgICByYW5nZUluZm8uc3RhcnRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3JcbiAgKTtcbiAgaWYgKCFzdGFydEVsZW1lbnQpIHtcbiAgICBsb2coXCJeXl4gY29udmVydFJhbmdlSW5mbyBOTyBTVEFSVCBFTEVNRU5UIENTUyBTRUxFQ1RPUj8hXCIpO1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbiAgbGV0IHN0YXJ0Q29udGFpbmVyID0gc3RhcnRFbGVtZW50O1xuICBpZiAocmFuZ2VJbmZvLnN0YXJ0Q29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4ID49IDApIHtcbiAgICBpZiAoXG4gICAgICByYW5nZUluZm8uc3RhcnRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXggPj1cbiAgICAgIHN0YXJ0RWxlbWVudC5jaGlsZE5vZGVzLmxlbmd0aFxuICAgICkge1xuICAgICAgbG9nKFxuICAgICAgICBcIl5eXiBjb252ZXJ0UmFuZ2VJbmZvIHJhbmdlSW5mby5zdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleCA+PSBzdGFydEVsZW1lbnQuY2hpbGROb2Rlcy5sZW5ndGg/IVwiXG4gICAgICApO1xuICAgICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICB9XG4gICAgc3RhcnRDb250YWluZXIgPVxuICAgICAgc3RhcnRFbGVtZW50LmNoaWxkTm9kZXNbcmFuZ2VJbmZvLnN0YXJ0Q29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4XTtcbiAgICBpZiAoc3RhcnRDb250YWluZXIubm9kZVR5cGUgIT09IE5vZGUuVEVYVF9OT0RFKSB7XG4gICAgICBsb2coXCJeXl4gY29udmVydFJhbmdlSW5mbyBzdGFydENvbnRhaW5lci5ub2RlVHlwZSAhPT0gTm9kZS5URVhUX05PREU/IVwiKTtcbiAgICAgIHJldHVybiB1bmRlZmluZWQ7XG4gICAgfVxuICB9XG4gIGNvbnN0IGVuZEVsZW1lbnQgPSBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKFxuICAgIHJhbmdlSW5mby5lbmRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3JcbiAgKTtcbiAgaWYgKCFlbmRFbGVtZW50KSB7XG4gICAgbG9nKFwiXl5eIGNvbnZlcnRSYW5nZUluZm8gTk8gRU5EIEVMRU1FTlQgQ1NTIFNFTEVDVE9SPyFcIik7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxuICBsZXQgZW5kQ29udGFpbmVyID0gZW5kRWxlbWVudDtcbiAgaWYgKHJhbmdlSW5mby5lbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXggPj0gMCkge1xuICAgIGlmIChcbiAgICAgIHJhbmdlSW5mby5lbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXggPj0gZW5kRWxlbWVudC5jaGlsZE5vZGVzLmxlbmd0aFxuICAgICkge1xuICAgICAgbG9nKFxuICAgICAgICBcIl5eXiBjb252ZXJ0UmFuZ2VJbmZvIHJhbmdlSW5mby5lbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXggPj0gZW5kRWxlbWVudC5jaGlsZE5vZGVzLmxlbmd0aD8hXCJcbiAgICAgICk7XG4gICAgICByZXR1cm4gdW5kZWZpbmVkO1xuICAgIH1cbiAgICBlbmRDb250YWluZXIgPVxuICAgICAgZW5kRWxlbWVudC5jaGlsZE5vZGVzW3JhbmdlSW5mby5lbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXhdO1xuICAgIGlmIChlbmRDb250YWluZXIubm9kZVR5cGUgIT09IE5vZGUuVEVYVF9OT0RFKSB7XG4gICAgICBsb2coXCJeXl4gY29udmVydFJhbmdlSW5mbyBlbmRDb250YWluZXIubm9kZVR5cGUgIT09IE5vZGUuVEVYVF9OT0RFPyFcIik7XG4gICAgICByZXR1cm4gdW5kZWZpbmVkO1xuICAgIH1cbiAgfVxuICByZXR1cm4gY3JlYXRlT3JkZXJlZFJhbmdlKFxuICAgIHN0YXJ0Q29udGFpbmVyLFxuICAgIHJhbmdlSW5mby5zdGFydE9mZnNldCxcbiAgICBlbmRDb250YWluZXIsXG4gICAgcmFuZ2VJbmZvLmVuZE9mZnNldFxuICApO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gbG9jYXRpb24yUmFuZ2VJbmZvKGxvY2F0aW9uKSB7XG4gIGNvbnN0IGxvY2F0aW9ucyA9IGxvY2F0aW9uLmxvY2F0aW9ucztcbiAgY29uc3QgZG9tUmFuZ2UgPSBsb2NhdGlvbnMuZG9tUmFuZ2U7XG4gIGNvbnN0IHN0YXJ0ID0gZG9tUmFuZ2Uuc3RhcnQ7XG4gIGNvbnN0IGVuZCA9IGRvbVJhbmdlLmVuZDtcblxuICByZXR1cm4ge1xuICAgIGVuZENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleDogZW5kLnRleHROb2RlSW5kZXgsXG4gICAgZW5kQ29udGFpbmVyRWxlbWVudENzc1NlbGVjdG9yOiBlbmQuY3NzU2VsZWN0b3IsXG4gICAgZW5kT2Zmc2V0OiBlbmQub2Zmc2V0LFxuICAgIHN0YXJ0Q29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4OiBzdGFydC50ZXh0Tm9kZUluZGV4LFxuICAgIHN0YXJ0Q29udGFpbmVyRWxlbWVudENzc1NlbGVjdG9yOiBzdGFydC5jc3NTZWxlY3RvcixcbiAgICBzdGFydE9mZnNldDogc3RhcnQub2Zmc2V0LFxuICB9O1xufVxuXG5mdW5jdGlvbiBsb2coKSB7XG4gIGlmIChkZWJ1Zykge1xuICAgIGxvZ05hdGl2ZS5hcHBseShudWxsLCBhcmd1bWVudHMpO1xuICB9XG59XG4iLCIvL1xuLy8gIENvcHlyaWdodCAyMDIxIFJlYWRpdW0gRm91bmRhdGlvbi4gQWxsIHJpZ2h0cyByZXNlcnZlZC5cbi8vICBVc2Ugb2YgdGhpcyBzb3VyY2UgY29kZSBpcyBnb3Zlcm5lZCBieSB0aGUgQlNELXN0eWxlIGxpY2Vuc2Vcbi8vICBhdmFpbGFibGUgaW4gdGhlIHRvcC1sZXZlbCBMSUNFTlNFIGZpbGUgb2YgdGhlIHByb2plY3QuXG4vL1xuXG4vLyBCYXNlIHNjcmlwdCB1c2VkIGJ5IGJvdGggcmVmbG93YWJsZSBhbmQgZml4ZWQgbGF5b3V0IHJlc291cmNlcy5cblxuaW1wb3J0IFwiLi9nZXN0dXJlc1wiO1xuaW1wb3J0IHtcbiAgcmVtb3ZlUHJvcGVydHksXG4gIHNjcm9sbExlZnQsXG4gIHNjcm9sbFJpZ2h0LFxuICBzY3JvbGxUb0VuZCxcbiAgc2Nyb2xsVG9JZCxcbiAgc2Nyb2xsVG9Qb3NpdGlvbixcbiAgc2Nyb2xsVG9TdGFydCxcbiAgc2Nyb2xsVG9UZXh0LFxuICBzZXRQcm9wZXJ0eSxcbiAgc2V0Q1NTUHJvcGVydGllcyxcbn0gZnJvbSBcIi4vdXRpbHNcIjtcbmltcG9ydCB7XG4gIGNyZWF0ZUFubm90YXRpb24sXG4gIGNyZWF0ZUhpZ2hsaWdodCxcbiAgZGVzdHJveUhpZ2hsaWdodCxcbiAgZ2V0Q3VycmVudFNlbGVjdGlvbkluZm8sXG4gIGdldFNlbGVjdGlvblJlY3QsXG4gIHJlY3RhbmdsZUZvckhpZ2hsaWdodFdpdGhJRCxcbiAgc2V0U2Nyb2xsTW9kZSxcbn0gZnJvbSBcIi4vaGlnaGxpZ2h0XCI7XG5pbXBvcnQgeyBmaW5kRmlyc3RWaXNpYmxlTG9jYXRvciB9IGZyb20gXCIuL2RvbVwiO1xuaW1wb3J0IHsgZ2V0Q3VycmVudFNlbGVjdGlvbiB9IGZyb20gXCIuL3NlbGVjdGlvblwiO1xuaW1wb3J0IHsgZ2V0RGVjb3JhdGlvbnMsIHJlZ2lzdGVyVGVtcGxhdGVzIH0gZnJvbSBcIi4vZGVjb3JhdG9yXCI7XG5cbi8vIFB1YmxpYyBBUEkgdXNlZCBieSB0aGUgbmF2aWdhdG9yLlxud2luZG93LnJlYWRpdW0gPSB7XG4gIC8vIHV0aWxzXG4gIHNjcm9sbFRvSWQ6IHNjcm9sbFRvSWQsXG4gIHNjcm9sbFRvUG9zaXRpb246IHNjcm9sbFRvUG9zaXRpb24sXG4gIHNjcm9sbFRvVGV4dDogc2Nyb2xsVG9UZXh0LFxuICBzY3JvbGxMZWZ0OiBzY3JvbGxMZWZ0LFxuICBzY3JvbGxSaWdodDogc2Nyb2xsUmlnaHQsXG4gIHNjcm9sbFRvU3RhcnQ6IHNjcm9sbFRvU3RhcnQsXG4gIHNjcm9sbFRvRW5kOiBzY3JvbGxUb0VuZCxcbiAgc2V0Q1NTUHJvcGVydGllczogc2V0Q1NTUHJvcGVydGllcyxcbiAgc2V0UHJvcGVydHk6IHNldFByb3BlcnR5LFxuICByZW1vdmVQcm9wZXJ0eTogcmVtb3ZlUHJvcGVydHksXG5cbiAgLy8gc2VsZWN0aW9uXG4gIGdldEN1cnJlbnRTZWxlY3Rpb246IGdldEN1cnJlbnRTZWxlY3Rpb24sXG5cbiAgLy8gZGVjb3JhdGlvblxuICByZWdpc3RlckRlY29yYXRpb25UZW1wbGF0ZXM6IHJlZ2lzdGVyVGVtcGxhdGVzLFxuICBnZXREZWNvcmF0aW9uczogZ2V0RGVjb3JhdGlvbnMsXG5cbiAgLy8gRE9NXG4gIGZpbmRGaXJzdFZpc2libGVMb2NhdG9yOiBmaW5kRmlyc3RWaXNpYmxlTG9jYXRvcixcbn07XG5cbi8vIExlZ2FjeSBoaWdobGlnaHRzIEFQSS5cbndpbmRvdy5jcmVhdGVBbm5vdGF0aW9uID0gY3JlYXRlQW5ub3RhdGlvbjtcbndpbmRvdy5jcmVhdGVIaWdobGlnaHQgPSBjcmVhdGVIaWdobGlnaHQ7XG53aW5kb3cuZGVzdHJveUhpZ2hsaWdodCA9IGRlc3Ryb3lIaWdobGlnaHQ7XG53aW5kb3cuZ2V0Q3VycmVudFNlbGVjdGlvbkluZm8gPSBnZXRDdXJyZW50U2VsZWN0aW9uSW5mbztcbndpbmRvdy5nZXRTZWxlY3Rpb25SZWN0ID0gZ2V0U2VsZWN0aW9uUmVjdDtcbndpbmRvdy5yZWN0YW5nbGVGb3JIaWdobGlnaHRXaXRoSUQgPSByZWN0YW5nbGVGb3JIaWdobGlnaHRXaXRoSUQ7XG53aW5kb3cuc2V0U2Nyb2xsTW9kZSA9IHNldFNjcm9sbE1vZGU7XG4iLCIvL1xuLy8gIENvcHlyaWdodCAyMDIxIFJlYWRpdW0gRm91bmRhdGlvbi4gQWxsIHJpZ2h0cyByZXNlcnZlZC5cbi8vICBVc2Ugb2YgdGhpcyBzb3VyY2UgY29kZSBpcyBnb3Zlcm5lZCBieSB0aGUgQlNELXN0eWxlIGxpY2Vuc2Vcbi8vICBhdmFpbGFibGUgaW4gdGhlIHRvcC1sZXZlbCBMSUNFTlNFIGZpbGUgb2YgdGhlIHByb2plY3QuXG4vL1xuXG4vLyBTY3JpcHQgdXNlZCBmb3IgcmVmbG93YWJsZSByZXNvdXJjZXMuXG5cbmltcG9ydCBcIi4vaW5kZXhcIjtcblxud2luZG93LnJlYWRpdW0uaXNSZWZsb3dhYmxlID0gdHJ1ZTtcblxuZG9jdW1lbnQuYWRkRXZlbnRMaXN0ZW5lcihcIkRPTUNvbnRlbnRMb2FkZWRcIiwgZnVuY3Rpb24gKCkge1xuICAvLyBTZXR1cHMgdGhlIGB2aWV3cG9ydGAgbWV0YSB0YWcgdG8gZGlzYWJsZSB6b29taW5nLlxuICBsZXQgbWV0YSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoXCJtZXRhXCIpO1xuICBtZXRhLnNldEF0dHJpYnV0ZShcIm5hbWVcIiwgXCJ2aWV3cG9ydFwiKTtcbiAgbWV0YS5zZXRBdHRyaWJ1dGUoXG4gICAgXCJjb250ZW50XCIsXG4gICAgXCJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MS4wLCBtYXhpbXVtLXNjYWxlPTEuMCwgdXNlci1zY2FsYWJsZT1ubywgc2hyaW5rLXRvLWZpdD1ub1wiXG4gICk7XG4gIGRvY3VtZW50LmhlYWQuYXBwZW5kQ2hpbGQobWV0YSk7XG59KTtcbiJdLCJuYW1lcyI6WyJhcHByb3hTZWFyY2giLCJzZWFyY2giLCJ0ZXh0Iiwic3RyIiwibWF4RXJyb3JzIiwibWF0Y2hQb3MiLCJleGFjdE1hdGNoZXMiLCJpbmRleE9mIiwicHVzaCIsInN0YXJ0IiwiZW5kIiwibGVuZ3RoIiwiZXJyb3JzIiwidGV4dE1hdGNoU2NvcmUiLCJtYXRjaGVzIiwibWF0Y2hRdW90ZSIsInF1b3RlIiwiY29udGV4dCIsIk1hdGgiLCJtaW4iLCJzY29yZU1hdGNoIiwibWF0Y2giLCJxdW90ZVdlaWdodCIsInByZWZpeFdlaWdodCIsInN1ZmZpeFdlaWdodCIsInBvc1dlaWdodCIsInF1b3RlU2NvcmUiLCJwcmVmaXhTY29yZSIsInByZWZpeCIsInNsaWNlIiwibWF4Iiwic3VmZml4U2NvcmUiLCJzdWZmaXgiLCJwb3NTY29yZSIsImhpbnQiLCJvZmZzZXQiLCJhYnMiLCJyYXdTY29yZSIsIm1heFNjb3JlIiwibm9ybWFsaXplZFNjb3JlIiwic2NvcmVkTWF0Y2hlcyIsIm1hcCIsIm0iLCJzY29yZSIsInNvcnQiLCJhIiwiYiIsIm5vZGVUZXh0TGVuZ3RoIiwibm9kZSIsIm5vZGVUeXBlIiwiTm9kZSIsIkVMRU1FTlRfTk9ERSIsIlRFWFRfTk9ERSIsInRleHRDb250ZW50IiwicHJldmlvdXNTaWJsaW5nc1RleHRMZW5ndGgiLCJzaWJsaW5nIiwicHJldmlvdXNTaWJsaW5nIiwicmVzb2x2ZU9mZnNldHMiLCJlbGVtZW50Iiwib2Zmc2V0cyIsIm5leHRPZmZzZXQiLCJzaGlmdCIsIm5vZGVJdGVyIiwib3duZXJEb2N1bWVudCIsImNyZWF0ZU5vZGVJdGVyYXRvciIsIk5vZGVGaWx0ZXIiLCJTSE9XX1RFWFQiLCJyZXN1bHRzIiwiY3VycmVudE5vZGUiLCJuZXh0Tm9kZSIsInRleHROb2RlIiwidW5kZWZpbmVkIiwiZGF0YSIsIlJhbmdlRXJyb3IiLCJSRVNPTFZFX0ZPUldBUkRTIiwiUkVTT0xWRV9CQUNLV0FSRFMiLCJUZXh0UG9zaXRpb24iLCJFcnJvciIsInBhcmVudCIsImNvbnRhaW5zIiwiZWwiLCJwYXJlbnRFbGVtZW50Iiwib3B0aW9ucyIsImVyciIsImRpcmVjdGlvbiIsInR3IiwiZG9jdW1lbnQiLCJjcmVhdGVUcmVlV2Fsa2VyIiwiZ2V0Um9vdE5vZGUiLCJmb3J3YXJkcyIsInByZXZpb3VzTm9kZSIsImZyb21Qb2ludCIsInRleHRPZmZzZXQiLCJjaGlsZE5vZGVzIiwiaSIsIlRleHRSYW5nZSIsInJlbGF0aXZlVG8iLCJyZXNvbHZlIiwicmFuZ2UiLCJSYW5nZSIsInNldFN0YXJ0Iiwic2V0RW5kIiwic3RhcnRDb250YWluZXIiLCJzdGFydE9mZnNldCIsImVuZENvbnRhaW5lciIsImVuZE9mZnNldCIsInJvb3QiLCJub2RlRnJvbVhQYXRoIiwieHBhdGhGcm9tTm9kZSIsIlJhbmdlQW5jaG9yIiwibm9ybWFsaXplZFJhbmdlIiwiZnJvbVJhbmdlIiwidG9SYW5nZSIsInRleHRSYW5nZSIsInR5cGUiLCJzZWxlY3RvciIsInN0YXJ0UG9zIiwiZnJvbUNoYXJPZmZzZXQiLCJlbmRQb3MiLCJUZXh0UG9zaXRpb25BbmNob3IiLCJmcm9tT2Zmc2V0cyIsIlRleHRRdW90ZUFuY2hvciIsImV4YWN0IiwidG9Qb3NpdGlvbkFuY2hvciIsImNvbnRleHRMZW4iLCJ3aW5kb3ciLCJhZGRFdmVudExpc3RlbmVyIiwiZXZlbnQiLCJBbmRyb2lkIiwibG9nRXJyb3IiLCJtZXNzYWdlIiwiZmlsZW5hbWUiLCJsaW5lbm8iLCJvYnNlcnZlciIsIlJlc2l6ZU9ic2VydmVyIiwib25WaWV3cG9ydFdpZHRoQ2hhbmdlZCIsInNuYXBDdXJyZW50T2Zmc2V0Iiwib2JzZXJ2ZSIsImJvZHkiLCJhcHBlbmRWaXJ0dWFsQ29sdW1uSWZOZWVkZWQiLCJpZCIsInZpcnR1YWxDb2wiLCJnZXRFbGVtZW50QnlJZCIsImlzU2Nyb2xsTW9kZUVuYWJsZWQiLCJnZXRDb2x1bW5Db3VudFBlclNjcmVlbiIsInJlbW92ZSIsImRvY3VtZW50V2lkdGgiLCJzY3JvbGxpbmdFbGVtZW50Iiwic2Nyb2xsV2lkdGgiLCJjb2xDb3VudCIsInBhZ2VXaWR0aCIsImhhc09kZENvbENvdW50Iiwicm91bmQiLCJjcmVhdGVFbGVtZW50Iiwic2V0QXR0cmlidXRlIiwic3R5bGUiLCJicmVha0JlZm9yZSIsImlubmVySFRNTCIsImFwcGVuZENoaWxkIiwid2lkdGgiLCJnZXRWaWV3cG9ydFdpZHRoIiwiZGV2aWNlUGl4ZWxSYXRpbyIsInNldFByb3BlcnR5IiwicGFyc2VJbnQiLCJnZXRDb21wdXRlZFN0eWxlIiwiZG9jdW1lbnRFbGVtZW50IiwiZ2V0UHJvcGVydHlWYWx1ZSIsInRyaW0iLCJpc1JUTCIsImRpciIsInRvTG93ZXJDYXNlIiwic2Nyb2xsVG9JZCIsInNjcm9sbFRvUmVjdCIsImdldEJvdW5kaW5nQ2xpZW50UmVjdCIsInNjcm9sbFRvUG9zaXRpb24iLCJwb3NpdGlvbiIsInNjcm9sbEhlaWdodCIsInNjcm9sbFRvcCIsImZhY3RvciIsInNjcm9sbExlZnQiLCJzbmFwT2Zmc2V0Iiwic2Nyb2xsVG9UZXh0IiwicmFuZ2VGcm9tTG9jYXRvciIsInNjcm9sbFRvUmFuZ2UiLCJyZWN0IiwidG9wIiwic2Nyb2xsWSIsImxlZnQiLCJzY3JvbGxYIiwic2Nyb2xsVG9TdGFydCIsInNjcm9sbFRvIiwic2Nyb2xsVG9FbmQiLCJtaW5PZmZzZXQiLCJzY3JvbGxUb09mZnNldCIsInNjcm9sbFJpZ2h0IiwibWF4T2Zmc2V0IiwiY3VycmVudE9mZnNldCIsImRpZmYiLCJ2YWx1ZSIsImRlbHRhIiwibG9jYXRvciIsImxvY2F0aW9ucyIsImhpZ2hsaWdodCIsImNzc1NlbGVjdG9yIiwicXVlcnlTZWxlY3RvciIsImFuY2hvciIsImJlZm9yZSIsImFmdGVyIiwiZnJhZ21lbnRzIiwiaHRtbElkIiwiY3JlYXRlUmFuZ2UiLCJzZXRTdGFydEJlZm9yZSIsInNldEVuZEFmdGVyIiwiZSIsInNldENTU1Byb3BlcnRpZXMiLCJwcm9wZXJ0aWVzIiwibmFtZSIsImtleSIsInJlbW92ZVByb3BlcnR5IiwibG9nIiwiQXJyYXkiLCJwcm90b3R5cGUiLCJjYWxsIiwiYXJndW1lbnRzIiwiam9pbiIsImxvZ05hdGl2ZSIsImRlYnVnIiwidG9OYXRpdmVSZWN0IiwicGl4ZWxSYXRpbyIsImhlaWdodCIsInJpZ2h0IiwiYm90dG9tIiwiZ2V0Q2xpZW50UmVjdHNOb092ZXJsYXAiLCJkb05vdE1lcmdlSG9yaXpvbnRhbGx5QWxpZ25lZFJlY3RzIiwiY2xpZW50UmVjdHMiLCJnZXRDbGllbnRSZWN0cyIsInRvbGVyYW5jZSIsIm9yaWdpbmFsUmVjdHMiLCJyYW5nZUNsaWVudFJlY3QiLCJtZXJnZWRSZWN0cyIsIm1lcmdlVG91Y2hpbmdSZWN0cyIsIm5vQ29udGFpbmVkUmVjdHMiLCJyZW1vdmVDb250YWluZWRSZWN0cyIsIm5ld1JlY3RzIiwicmVwbGFjZU92ZXJsYXBpbmdSZWN0cyIsIm1pbkFyZWEiLCJqIiwiYmlnRW5vdWdoIiwic3BsaWNlIiwicmVjdHMiLCJyZWN0MSIsInJlY3QyIiwicmVjdHNMaW5lVXBWZXJ0aWNhbGx5IiwiYWxtb3N0RXF1YWwiLCJyZWN0c0xpbmVVcEhvcml6b250YWxseSIsImhvcml6b250YWxBbGxvd2VkIiwiYWxpZ25lZCIsImNhbk1lcmdlIiwicmVjdHNUb3VjaE9yT3ZlcmxhcCIsImZpbHRlciIsInJlcGxhY2VtZW50Q2xpZW50UmVjdCIsImdldEJvdW5kaW5nUmVjdCIsInJlY3RzVG9LZWVwIiwiU2V0IiwiZGVsZXRlIiwicG9zc2libHlDb250YWluaW5nUmVjdCIsImhhcyIsInJlY3RDb250YWlucyIsImZyb20iLCJyZWN0Q29udGFpbnNQb2ludCIsIngiLCJ5IiwidG9BZGQiLCJ0b1JlbW92ZSIsInN1YnRyYWN0UmVjdHMxIiwicmVjdFN1YnRyYWN0Iiwic3VidHJhY3RSZWN0czIiLCJhcHBseSIsInJlY3RJbnRlcnNlY3RlZCIsInJlY3RJbnRlcnNlY3QiLCJyZWN0QSIsInJlY3RCIiwicmVjdEMiLCJyZWN0RCIsIm1heExlZnQiLCJtaW5SaWdodCIsIm1heFRvcCIsIm1pbkJvdHRvbSIsInN0eWxlcyIsIk1hcCIsImdyb3VwcyIsImxhc3RHcm91cElkIiwicmVnaXN0ZXJUZW1wbGF0ZXMiLCJuZXdTdHlsZXMiLCJzdHlsZXNoZWV0IiwiT2JqZWN0IiwiZW50cmllcyIsInNldCIsInN0eWxlRWxlbWVudCIsImdldEVsZW1lbnRzQnlUYWdOYW1lIiwiZ2V0RGVjb3JhdGlvbnMiLCJncm91cE5hbWUiLCJncm91cCIsImdldCIsIkRlY29yYXRpb25Hcm91cCIsImhhbmRsZURlY29yYXRpb25DbGlja0V2ZW50IiwiY2xpY2tFdmVudCIsInNpemUiLCJmaW5kVGFyZ2V0IiwiZ3JvdXBDb250ZW50IiwiaXRlbXMiLCJyZXZlcnNlIiwiaXRlbSIsImNsaWNrYWJsZUVsZW1lbnRzIiwidG9KU09OIiwiY2xpZW50WCIsImNsaWVudFkiLCJ0YXJnZXQiLCJvbkRlY29yYXRpb25BY3RpdmF0ZWQiLCJKU09OIiwic3RyaW5naWZ5IiwiZGVjb3JhdGlvbiIsImNsaWNrIiwiZ3JvdXBJZCIsImxhc3RJdGVtSWQiLCJjb250YWluZXIiLCJhZGQiLCJsYXlvdXQiLCJkZWNvcmF0aW9uSWQiLCJpbmRleCIsImZpbmRJbmRleCIsInVwZGF0ZSIsImNsZWFyIiwiY2xlYXJDb250YWluZXIiLCJyZXF1ZXN0TGF5b3V0IiwiZm9yRWFjaCIsImdyb3VwQ29udGFpbmVyIiwicmVxdWlyZUNvbnRhaW5lciIsIml0ZW1Db250YWluZXIiLCJ2aWV3cG9ydFdpZHRoIiwiaW5uZXJXaWR0aCIsImNvbHVtbkNvdW50IiwieE9mZnNldCIsInlPZmZzZXQiLCJwb3NpdGlvbkVsZW1lbnQiLCJib3VuZGluZ1JlY3QiLCJmbG9vciIsImVsZW1lbnRUZW1wbGF0ZSIsInRlbXBsYXRlIiwiY29udGVudCIsImZpcnN0RWxlbWVudENoaWxkIiwiZXJyb3IiLCJyMSIsInIyIiwiY2xpZW50UmVjdCIsImxpbmUiLCJjbG9uZU5vZGUiLCJhcHBlbmQiLCJib3VuZHMiLCJxdWVyeVNlbGVjdG9yQWxsIiwiY2hpbGRyZW4iLCJsYXN0U2l6ZSIsImNsaWVudFdpZHRoIiwiY2xpZW50SGVpZ2h0Iiwib25DbGljayIsImJpbmREcmFnR2VzdHVyZSIsImdldFNlbGVjdGlvbiIsImlzQ29sbGFwc2VkIiwiZGVmYXVsdFByZXZlbnRlZCIsInRhcmdldEVsZW1lbnQiLCJvdXRlckhUTUwiLCJpbnRlcmFjdGl2ZUVsZW1lbnQiLCJuZWFyZXN0SW50ZXJhY3RpdmVFbGVtZW50Iiwic2hvdWxkUHJldmVudERlZmF1bHQiLCJvblRhcCIsInN0b3BQcm9wYWdhdGlvbiIsInByZXZlbnREZWZhdWx0Iiwib25TdGFydCIsInBhc3NpdmUiLCJvbkVuZCIsIm9uTW92ZSIsInN0YXRlIiwiaXNTdGFydGluZ0RyYWciLCJzdGFydFgiLCJ0b3VjaGVzIiwic3RhcnRZIiwiY3VycmVudFgiLCJjdXJyZW50WSIsIm9mZnNldFgiLCJvZmZzZXRZIiwib25EcmFnU3RhcnQiLCJvbkRyYWdNb3ZlIiwib25EcmFnRW5kIiwiaW50ZXJhY3RpdmVUYWdzIiwibm9kZU5hbWUiLCJoYXNBdHRyaWJ1dGUiLCJnZXRBdHRyaWJ1dGUiLCJST09UX0NMQVNTX1JFRFVDRV9NT1RJT04iLCJST09UX0NMQVNTX05PX0ZPT1ROT1RFUyIsIlBPUFVQX0RJQUxPR19DTEFTUyIsIkZPT1ROT1RFU19DT05UQUlORVJfQ0xBU1MiLCJGT09UTk9URVNfQ0xPU0VfQlVUVE9OX0NMQVNTIiwiRk9PVE5PVEVfRk9SQ0VfU0hPVyIsIlRUU19JRF9QUkVWSU9VUyIsIlRUU19JRF9ORVhUIiwiVFRTX0lEX1NMSURFUiIsIlRUU19JRF9BQ1RJVkVfV09SRCIsIlRUU19JRF9DT05UQUlORVIiLCJUVFNfSURfSU5GTyIsIlRUU19OQVZfQlVUVE9OX0NMQVNTIiwiVFRTX0lEX1NQRUFLSU5HX0RPQ19FTEVNRU5UIiwiVFRTX0NMQVNTX0lOSkVDVEVEX1NQQU4iLCJUVFNfQ0xBU1NfSU5KRUNURURfU1VCU1BBTiIsIlRUU19JRF9JTkpFQ1RFRF9QQVJFTlQiLCJJRF9ISUdITElHSFRTX0NPTlRBSU5FUiIsIklEX0FOTk9UQVRJT05fQ09OVEFJTkVSIiwiQ0xBU1NfSElHSExJR0hUX0NPTlRBSU5FUiIsIkNMQVNTX0FOTk9UQVRJT05fQ09OVEFJTkVSIiwiQ0xBU1NfSElHSExJR0hUX0FSRUEiLCJDTEFTU19BTk5PVEFUSU9OX0FSRUEiLCJDTEFTU19ISUdITElHSFRfQk9VTkRJTkdfQVJFQSIsIkNMQVNTX0FOTk9UQVRJT05fQk9VTkRJTkdfQVJFQSIsIl9ibGFja2xpc3RJZENsYXNzRm9yQ0ZJIiwiQ0xBU1NfUEFHSU5BVEVEIiwiSVNfREVWIiwiX2hpZ2hsaWdodHMiLCJfaGlnaGxpZ2h0c0NvbnRhaW5lciIsIl9hbm5vdGF0aW9uQ29udGFpbmVyIiwibGFzdE1vdXNlRG93blgiLCJsYXN0TW91c2VEb3duWSIsImJvZHlFdmVudExpc3RlbmVyc1NldCIsIlVTRV9TVkciLCJERUZBVUxUX0JBQ0tHUk9VTkRfQ09MT1JfT1BBQ0lUWSIsIkFMVF9CQUNLR1JPVU5EX0NPTE9SX09QQUNJVFkiLCJERUJVR19WSVNVQUxTIiwiREVGQVVMVF9CQUNLR1JPVU5EX0NPTE9SIiwiYmx1ZSIsImdyZWVuIiwicmVkIiwiQU5OT1RBVElPTl9XSURUSCIsInJlc2V0SGlnaGxpZ2h0Qm91bmRpbmdTdHlsZSIsIl93aW4iLCJoaWdobGlnaHRCb3VuZGluZyIsIm91dGxpbmUiLCJzZXRIaWdobGlnaHRBcmVhU3R5bGUiLCJ3aW4iLCJoaWdobGlnaHRBcmVhcyIsInVzZVNWRyIsImhpZ2hsaWdodEFyZWEiLCJpc1NWRyIsIm5hbWVzcGFjZVVSSSIsIlNWR19YTUxfTkFNRVNQQUNFIiwib3BhY2l0eSIsImNvbG9yIiwicmVzZXRIaWdobGlnaHRBcmVhU3R5bGUiLCJwYXJlbnROb2RlIiwiZmluZCIsImgiLCJwcm9jZXNzVG91Y2hFdmVudCIsImV2Iiwic2Nyb2xsRWxlbWVudCIsImdldFNjcm9sbGluZ0VsZW1lbnQiLCJjaGFuZ2VkVG91Y2hlcyIsInBhZ2luYXRlZCIsImlzUGFnaW5hdGVkIiwiYm9keVJlY3QiLCJuYXZpZ2F0b3IiLCJ1c2VyQWdlbnQiLCJmb3VuZEhpZ2hsaWdodCIsImZvdW5kRWxlbWVudCIsImZvdW5kUmVjdCIsImhpZ2hsaWdodFBhcmVudCIsImhpdCIsImhpZ2hsaWdodEZyYWdtZW50cyIsImhpZ2hsaWdodEZyYWdtZW50Iiwid2l0aFJlY3QiLCJoaWdobGlnaHRCb3VuZGluZ3MiLCJhbGxIaWdobGlnaHRBcmVhcyIsImZvdW5kRWxlbWVudEhpZ2hsaWdodEFyZWFzIiwiZm91bmRFbGVtZW50SGlnaGxpZ2h0Qm91bmRpbmciLCJhbGxIaWdobGlnaHRCb3VuZGluZ3MiLCJzZXRIaWdobGlnaHRCb3VuZGluZ1N0eWxlIiwic2NyZWVuV2lkdGgiLCJvdXRlcldpZHRoIiwic2NyZWVuSGVpZ2h0Iiwib3V0ZXJIZWlnaHQiLCJwYXlsb2FkIiwicHJvY2VzcyIsImVsZWN0cm9uXzEiLCJpcGNSZW5kZXJlciIsInNlbmRUb0hvc3QiLCJSMl9FVkVOVF9ISUdITElHSFRfQ0xJQ0siLCJ3ZWJraXRVUkwiLCJjb25zb2xlIiwiaW5jbHVkZXMiLCJoaWdobGlnaHRBbm5vdGF0aW9uTWFya0FjdGl2YXRlZCIsIndlYmtpdCIsIm1lc3NhZ2VIYW5kbGVycyIsInBvc3RNZXNzYWdlIiwiaGlnaGxpZ2h0QWN0aXZhdGVkIiwicHJvY2Vzc01vdXNlRXZlbnQiLCJ0b3VjaGVkUG9zaXRpb24iLCJpbm5lckhlaWdodCIsInRvUHJlc2VydmUiLCJ0b0NoZWNrIiwiY2hlY2tPdmVybGFwcyIsInN0aWxsT3ZlcmxhcGluZ1JlY3RzIiwiaGFzMSIsImhhczIiLCJ4T3ZlcmxhcCIsImdldFJlY3RPdmVybGFwWCIsInlPdmVybGFwIiwiZ2V0UmVjdE92ZXJsYXBZIiwicmFuZ2VDbGllbnRSZWN0cyIsImdldENsaWVudFJlY3RzTm9PdmVybGFwXyIsImNsYXNzTGlzdCIsImVuc3VyZUNvbnRhaW5lciIsImFubm90YXRpb25GbGFnIiwidG91Y2hFbmQiLCJoaWRlQWxsaGlnaGxpZ2h0cyIsImRlc3Ryb3lBbGxoaWdobGlnaHRzIiwiZGVzdHJveUhpZ2hsaWdodCIsIl9kb2N1bWVudCIsImhpZ2hsaWdodENvbnRhaW5lciIsImlzQ2ZpVGV4dE5vZGUiLCJnZXRDaGlsZFRleHROb2RlQ2ZpSW5kZXgiLCJjaGlsZCIsImZvdW5kIiwidGV4dE5vZGVJbmRleCIsInByZXZpb3VzV2FzRWxlbWVudCIsImNoaWxkTm9kZSIsImlzVGV4dCIsImdldENvbW1vbkFuY2VzdG9yRWxlbWVudCIsIm5vZGUxIiwibm9kZTIiLCJub2RlMUVsZW1lbnRBbmNlc3RvckNoYWluIiwibm9kZTJFbGVtZW50QW5jZXN0b3JDaGFpbiIsImNvbW1vbkFuY2VzdG9yIiwibm9kZTFFbGVtZW50QW5jZXN0b3IiLCJub2RlMkVsZW1lbnRBbmNlc3RvciIsImZ1bGxRdWFsaWZpZWRTZWxlY3RvciIsImxvd2VyQ2FzZU5hbWUiLCJsb2NhbE5hbWUiLCJjc3NQYXRoIiwiZ2V0Q3VycmVudFNlbGVjdGlvbkluZm8iLCJzZWxlY3Rpb24iLCJyYXdUZXh0IiwidG9TdHJpbmciLCJjbGVhblRleHQiLCJyZXBsYWNlIiwiYW5jaG9yTm9kZSIsImZvY3VzTm9kZSIsInJhbmdlQ291bnQiLCJnZXRSYW5nZUF0IiwiY3JlYXRlT3JkZXJlZFJhbmdlIiwiYW5jaG9yT2Zmc2V0IiwiZm9jdXNPZmZzZXQiLCJjb2xsYXBzZWQiLCJyYW5nZUluZm8iLCJjb252ZXJ0UmFuZ2UiLCJjb21wdXRlQ0ZJIiwicmVzdG9yZWRSYW5nZSIsImNvbnZlcnRSYW5nZUluZm8iLCJkdW1wRGVidWciLCJnZXRDc3NTZWxlY3RvciIsInJhbmdlSW5mbzJMb2NhdGlvbiIsImNoZWNrQmxhY2tsaXN0ZWQiLCJibGFja2xpc3RlZElkIiwiYmxhY2tsaXN0ZWRDbGFzcyIsIm9wdGltaXplZCIsInN0ZXBzIiwiY29udGV4dE5vZGUiLCJzdGVwIiwiX2Nzc1BhdGhTdGVwIiwiaXNUYXJnZXROb2RlIiwicHJlZml4ZWRFbGVtZW50Q2xhc3NOYW1lcyIsIm5kIiwiY2xhc3NBdHRyaWJ1dGUiLCJzcGxpdCIsIkJvb2xlYW4iLCJubSIsImlkU2VsZWN0b3IiLCJpZGQiLCJlc2NhcGVJZGVudGlmaWVySWZOZWVkZWQiLCJpZGVudCIsImlzQ1NTSWRlbnRpZmllciIsInNob3VsZEVzY2FwZUZpcnN0IiwidGVzdCIsImxhc3RJbmRleCIsImMiLCJpaSIsImlzQ1NTSWRlbnRDaGFyIiwiZXNjYXBlQXNjaWlDaGFyIiwiaXNMYXN0IiwidG9IZXhCeXRlIiwiaGV4Qnl0ZSIsImNoYXJDb2RlQXQiLCJET0NVTUVOVF9OT0RFIiwicHJlZml4ZWRPd25DbGFzc05hbWVzQXJyYXlfIiwicHJlZml4ZWRPd25DbGFzc05hbWVzQXJyYXkiLCJhcnJJdGVtIiwibmVlZHNDbGFzc05hbWVzIiwibmVlZHNOdGhDaGlsZCIsIm93bkluZGV4IiwiZWxlbWVudEluZGV4Iiwic2libGluZ3MiLCJzaWJsaW5nTmFtZSIsIm93bkNsYXNzTmFtZXMiLCJvd25DbGFzc05hbWVDb3VudCIsInNpYmxpbmdDbGFzc05hbWVzQXJyYXlfIiwic2libGluZ0NsYXNzTmFtZXNBcnJheSIsInNpYmxpbmdDbGFzcyIsImluZCIsInJlc3VsdCIsInByZWZpeGVkTmFtZSIsInN1YnN0ciIsImNmaSIsImN1cnJlbnRFbGVtZW50IiwiYmxhY2tsaXN0ZWQiLCJjdXJyZW50RWxlbWVudFBhcmVudENoaWxkcmVuIiwiY3VycmVudEVsZW1lbnRJbmRleCIsImNmaUluZGV4IiwiX2NyZWF0ZUhpZ2hsaWdodCIsInBvaW50ZXJJbnRlcmFjdGlvbiIsImxvY2F0aW9uMlJhbmdlSW5mbyIsInVuaXF1ZVN0ciIsInN0YXJ0Q29udGFpbmVyRWxlbWVudENzc1NlbGVjdG9yIiwic3RhcnRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXgiLCJlbmRDb250YWluZXJFbGVtZW50Q3NzU2VsZWN0b3IiLCJlbmRDb250YWluZXJDaGlsZFRleHROb2RlSW5kZXgiLCJoYXNoIiwicmVxdWlyZSIsInNoYTI1NkhleCIsInNoYTI1NiIsImRpZ2VzdCIsImNyZWF0ZUhpZ2hsaWdodERvbSIsImNyZWF0ZUhpZ2hsaWdodCIsInNlbGVjdGlvbkluZm8iLCJjcmVhdGVBbm5vdGF0aW9uIiwic2NhbGUiLCJSRUFESVVNMiIsImlzRml4ZWRMYXlvdXQiLCJmeGxWaWV3cG9ydFNjYWxlIiwiaGlnaGxpZ2h0c0NvbnRhaW5lciIsImRyYXdVbmRlcmxpbmUiLCJkcmF3U3RyaWtlVGhyb3VnaCIsImhpZ2hsaWdodEFyZWFTVkdEb2NGcmFnIiwicm91bmRlZENvcm5lciIsInVuZGVybGluZVRoaWNrbmVzcyIsInN0cmlrZVRocm91Z2hMaW5lVGhpY2tuZXNzIiwiZXh0cmEiLCJyYW5nZUFubm90YXRpb25Cb3VuZGluZ0NsaWVudFJlY3QiLCJmcmFtZUZvckhpZ2hsaWdodEFubm90YXRpb25NYXJrV2l0aElEIiwiYW5ub3RhdGlvbk9mZnNldCIsImJvcmRlclRoaWNrbmVzcyIsImNyZWF0ZURvY3VtZW50RnJhZ21lbnQiLCJoaWdobGlnaHRBcmVhU1ZHUmVjdCIsImNyZWF0ZUVsZW1lbnROUyIsImhpZ2hsaWdodEFyZWFTVkdMaW5lIiwibGluZU9mZnNldCIsInJnYiIsInJhbmRvbSIsInIiLCJnIiwiaGlnaGxpZ2h0QXJlYUxpbmUiLCJoaWdobGlnaHRBcmVhU1ZHIiwib3ZlcmZsb3ciLCJyYW5nZUJvdW5kaW5nQ2xpZW50UmVjdCIsInN0YXJ0Tm9kZSIsImVuZE5vZGUiLCJyYW5nZVJldmVyc2UiLCJjb21wdXRlRWxlbWVudENGSSIsInN0YXJ0SXNFbGVtZW50Iiwic3RhcnRDb250YWluZXJFbGVtZW50IiwiZW5kSXNFbGVtZW50IiwiZW5kQ29udGFpbmVyRWxlbWVudCIsImNvbW1vbkVsZW1lbnRBbmNlc3RvciIsImNvbW1vbkFuY2VzdG9yQ29udGFpbmVyIiwicmFuZ2VDb21tb25BbmNlc3RvckVsZW1lbnQiLCJyb290RWxlbWVudENmaSIsInN0YXJ0RWxlbWVudENmaSIsImVuZEVsZW1lbnRDZmkiLCJzdGFydEVsZW1lbnRPclRleHRDZmkiLCJzdGFydENvbnRhaW5lckNoaWxkVGV4dE5vZGVJbmRleEZvckNmaSIsImNmaVRleHROb2RlSW5kZXgiLCJjZmlJbmRleE9mTGFzdEVsZW1lbnQiLCJjaGlsZEVsZW1lbnRDb3VudCIsImxhc3RDaGlsZE5vZGUiLCJlbmRFbGVtZW50T3JUZXh0Q2ZpIiwiZW5kQ29udGFpbmVyQ2hpbGRUZXh0Tm9kZUluZGV4Rm9yQ2ZpIiwic3RhcnRFbGVtZW50IiwiZW5kRWxlbWVudCIsImZyYW1lRm9ySGlnaGxpZ2h0V2l0aElEIiwidG9wQ2xpZW50UmVjdCIsIm1heEhlaWdodCIsIm5ld1RvcCIsImJvdW5kaW5nQXJlYXMiLCJnZXRFbGVtZW50c0J5Q2xhc3NOYW1lIiwibGVuIiwiYm91bmRpbmdBcmVhIiwiaGlnaGxpZ2h0V2l0aElEIiwicGFydGlhbENmaSIsImRvbVJhbmdlIiwibG9jYXRpb24iLCJyZWN0YW5nbGVGb3JIaWdobGlnaHRXaXRoSUQiLCJnZXRTZWxlY3Rpb25SZWN0Iiwic2VsIiwiaGFuZGxlQm91bmRzIiwic2V0U2Nyb2xsTW9kZSIsImZsYWciLCJmaW5kRmlyc3RWaXNpYmxlTG9jYXRvciIsImZpbmRFbGVtZW50IiwiaHJlZiIsInJvb3RFbGVtZW50Iiwic2hvdWxkSWdub3JlRWxlbWVudCIsImlzRWxlbWVudFZpc2libGUiLCJyZWFkaXVtIiwiZWxTdHlsZSIsImRpc3BsYXkiLCJtYXRjaEFsbCIsInNoaW0iLCJpc1NlbGVjdGluZyIsIm9uU2VsZWN0aW9uRW5kIiwib25TZWxlY3Rpb25TdGFydCIsImdldEN1cnJlbnRTZWxlY3Rpb24iLCJnZXRDdXJyZW50U2VsZWN0aW9uVGV4dCIsImNsZWFuSGlnaGxpZ2h0Iiwic25pcHBldExlbmd0aCIsImZpcnN0V29yZFN0YXJ0IiwibGFzdFdvcmRFbmQiLCJwb3AiLCJyZWdpc3RlckRlY29yYXRpb25UZW1wbGF0ZXMiLCJpc1JlZmxvd2FibGUiLCJtZXRhIiwiaGVhZCJdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///5232\n')},1924:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar callBind = __webpack_require__(5559);\n\nvar $indexOf = callBind(GetIntrinsic('String.prototype.indexOf'));\n\nmodule.exports = function callBoundIntrinsic(name, allowMissing) {\n\tvar intrinsic = GetIntrinsic(name, !!allowMissing);\n\tif (typeof intrinsic === 'function' && $indexOf(name, '.prototype.') > -1) {\n\t\treturn callBind(intrinsic);\n\t}\n\treturn intrinsic;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTkyNC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQyxlQUFlLG1CQUFPLENBQUMsSUFBSTs7QUFFM0I7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2NhbGwtYmluZC9jYWxsQm91bmQuanM/NTQ1ZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciBjYWxsQmluZCA9IHJlcXVpcmUoJy4vJyk7XG5cbnZhciAkaW5kZXhPZiA9IGNhbGxCaW5kKEdldEludHJpbnNpYygnU3RyaW5nLnByb3RvdHlwZS5pbmRleE9mJykpO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIGNhbGxCb3VuZEludHJpbnNpYyhuYW1lLCBhbGxvd01pc3NpbmcpIHtcblx0dmFyIGludHJpbnNpYyA9IEdldEludHJpbnNpYyhuYW1lLCAhIWFsbG93TWlzc2luZyk7XG5cdGlmICh0eXBlb2YgaW50cmluc2ljID09PSAnZnVuY3Rpb24nICYmICRpbmRleE9mKG5hbWUsICcucHJvdG90eXBlLicpID4gLTEpIHtcblx0XHRyZXR1cm4gY2FsbEJpbmQoaW50cmluc2ljKTtcblx0fVxuXHRyZXR1cm4gaW50cmluc2ljO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///1924\n")},5559:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar bind = __webpack_require__(8612);\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $apply = GetIntrinsic('%Function.prototype.apply%');\nvar $call = GetIntrinsic('%Function.prototype.call%');\nvar $reflectApply = GetIntrinsic('%Reflect.apply%', true) || bind.call($call, $apply);\n\nvar $gOPD = GetIntrinsic('%Object.getOwnPropertyDescriptor%', true);\nvar $defineProperty = GetIntrinsic('%Object.defineProperty%', true);\nvar $max = GetIntrinsic('%Math.max%');\n\nif ($defineProperty) {\n\ttry {\n\t\t$defineProperty({}, 'a', { value: 1 });\n\t} catch (e) {\n\t\t// IE 8 has a broken defineProperty\n\t\t$defineProperty = null;\n\t}\n}\n\nmodule.exports = function callBind(originalFunction) {\n\tvar func = $reflectApply(bind, $call, arguments);\n\tif ($gOPD && $defineProperty) {\n\t\tvar desc = $gOPD(func, 'length');\n\t\tif (desc.configurable) {\n\t\t\t// original length, plus the receiver, minus any additional arguments (after the receiver)\n\t\t\t$defineProperty(\n\t\t\t\tfunc,\n\t\t\t\t'length',\n\t\t\t\t{ value: 1 + $max(0, originalFunction.length - (arguments.length - 1)) }\n\t\t\t);\n\t\t}\n\t}\n\treturn func;\n};\n\nvar applyBind = function applyBind() {\n\treturn $reflectApply(bind, $apply, arguments);\n};\n\nif ($defineProperty) {\n\t$defineProperty(module.exports, 'apply', { value: applyBind });\n} else {\n\tmodule.exports.apply = applyBind;\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTU1OS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixXQUFXLG1CQUFPLENBQUMsSUFBZTtBQUNsQyxtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxvQkFBb0IsU0FBUyxVQUFVO0FBQ3ZDLEdBQUc7QUFDSDtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxNQUFNO0FBQ047QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQSw0Q0FBNEMsa0JBQWtCO0FBQzlELEVBQUU7QUFDRixDQUFDLG9CQUFvQjtBQUNyQiIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvY2FsbC1iaW5kL2luZGV4LmpzPzNlYjEiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgYmluZCA9IHJlcXVpcmUoJ2Z1bmN0aW9uLWJpbmQnKTtcbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkYXBwbHkgPSBHZXRJbnRyaW5zaWMoJyVGdW5jdGlvbi5wcm90b3R5cGUuYXBwbHklJyk7XG52YXIgJGNhbGwgPSBHZXRJbnRyaW5zaWMoJyVGdW5jdGlvbi5wcm90b3R5cGUuY2FsbCUnKTtcbnZhciAkcmVmbGVjdEFwcGx5ID0gR2V0SW50cmluc2ljKCclUmVmbGVjdC5hcHBseSUnLCB0cnVlKSB8fCBiaW5kLmNhbGwoJGNhbGwsICRhcHBseSk7XG5cbnZhciAkZ09QRCA9IEdldEludHJpbnNpYygnJU9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3IlJywgdHJ1ZSk7XG52YXIgJGRlZmluZVByb3BlcnR5ID0gR2V0SW50cmluc2ljKCclT2JqZWN0LmRlZmluZVByb3BlcnR5JScsIHRydWUpO1xudmFyICRtYXggPSBHZXRJbnRyaW5zaWMoJyVNYXRoLm1heCUnKTtcblxuaWYgKCRkZWZpbmVQcm9wZXJ0eSkge1xuXHR0cnkge1xuXHRcdCRkZWZpbmVQcm9wZXJ0eSh7fSwgJ2EnLCB7IHZhbHVlOiAxIH0pO1xuXHR9IGNhdGNoIChlKSB7XG5cdFx0Ly8gSUUgOCBoYXMgYSBicm9rZW4gZGVmaW5lUHJvcGVydHlcblx0XHQkZGVmaW5lUHJvcGVydHkgPSBudWxsO1xuXHR9XG59XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gY2FsbEJpbmQob3JpZ2luYWxGdW5jdGlvbikge1xuXHR2YXIgZnVuYyA9ICRyZWZsZWN0QXBwbHkoYmluZCwgJGNhbGwsIGFyZ3VtZW50cyk7XG5cdGlmICgkZ09QRCAmJiAkZGVmaW5lUHJvcGVydHkpIHtcblx0XHR2YXIgZGVzYyA9ICRnT1BEKGZ1bmMsICdsZW5ndGgnKTtcblx0XHRpZiAoZGVzYy5jb25maWd1cmFibGUpIHtcblx0XHRcdC8vIG9yaWdpbmFsIGxlbmd0aCwgcGx1cyB0aGUgcmVjZWl2ZXIsIG1pbnVzIGFueSBhZGRpdGlvbmFsIGFyZ3VtZW50cyAoYWZ0ZXIgdGhlIHJlY2VpdmVyKVxuXHRcdFx0JGRlZmluZVByb3BlcnR5KFxuXHRcdFx0XHRmdW5jLFxuXHRcdFx0XHQnbGVuZ3RoJyxcblx0XHRcdFx0eyB2YWx1ZTogMSArICRtYXgoMCwgb3JpZ2luYWxGdW5jdGlvbi5sZW5ndGggLSAoYXJndW1lbnRzLmxlbmd0aCAtIDEpKSB9XG5cdFx0XHQpO1xuXHRcdH1cblx0fVxuXHRyZXR1cm4gZnVuYztcbn07XG5cbnZhciBhcHBseUJpbmQgPSBmdW5jdGlvbiBhcHBseUJpbmQoKSB7XG5cdHJldHVybiAkcmVmbGVjdEFwcGx5KGJpbmQsICRhcHBseSwgYXJndW1lbnRzKTtcbn07XG5cbmlmICgkZGVmaW5lUHJvcGVydHkpIHtcblx0JGRlZmluZVByb3BlcnR5KG1vZHVsZS5leHBvcnRzLCAnYXBwbHknLCB7IHZhbHVlOiBhcHBseUJpbmQgfSk7XG59IGVsc2Uge1xuXHRtb2R1bGUuZXhwb3J0cy5hcHBseSA9IGFwcGx5QmluZDtcbn1cbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///5559\n")},4289:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar keys = __webpack_require__(2215);\nvar hasSymbols = typeof Symbol === 'function' && typeof Symbol('foo') === 'symbol';\n\nvar toStr = Object.prototype.toString;\nvar concat = Array.prototype.concat;\nvar origDefineProperty = Object.defineProperty;\n\nvar isFunction = function (fn) {\n\treturn typeof fn === 'function' && toStr.call(fn) === '[object Function]';\n};\n\nvar arePropertyDescriptorsSupported = function () {\n\tvar obj = {};\n\ttry {\n\t\torigDefineProperty(obj, 'x', { enumerable: false, value: obj });\n\t\t// eslint-disable-next-line no-unused-vars, no-restricted-syntax\n\t\tfor (var _ in obj) { // jscs:ignore disallowUnusedVariables\n\t\t\treturn false;\n\t\t}\n\t\treturn obj.x === obj;\n\t} catch (e) { /* this is IE 8. */\n\t\treturn false;\n\t}\n};\nvar supportsDescriptors = origDefineProperty && arePropertyDescriptorsSupported();\n\nvar defineProperty = function (object, name, value, predicate) {\n\tif (name in object && (!isFunction(predicate) || !predicate())) {\n\t\treturn;\n\t}\n\tif (supportsDescriptors) {\n\t\torigDefineProperty(object, name, {\n\t\t\tconfigurable: true,\n\t\t\tenumerable: false,\n\t\t\tvalue: value,\n\t\t\twritable: true\n\t\t});\n\t} else {\n\t\tobject[name] = value;\n\t}\n};\n\nvar defineProperties = function (object, map) {\n\tvar predicates = arguments.length > 2 ? arguments[2] : {};\n\tvar props = keys(map);\n\tif (hasSymbols) {\n\t\tprops = concat.call(props, Object.getOwnPropertySymbols(map));\n\t}\n\tfor (var i = 0; i < props.length; i += 1) {\n\t\tdefineProperty(object, props[i], map[props[i]], predicates[props[i]]);\n\t}\n};\n\ndefineProperties.supportsDescriptors = !!supportsDescriptors;\n\nmodule.exports = defineProperties;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDI4OS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixXQUFXLG1CQUFPLENBQUMsSUFBYTtBQUNoQzs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBLGlDQUFpQywrQkFBK0I7QUFDaEU7QUFDQSx1QkFBdUI7QUFDdkI7QUFDQTtBQUNBO0FBQ0EsR0FBRyxZQUFZO0FBQ2Y7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0gsR0FBRztBQUNIO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxpQkFBaUIsa0JBQWtCO0FBQ25DO0FBQ0E7QUFDQTs7QUFFQTs7QUFFQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZGVmaW5lLXByb3BlcnRpZXMvaW5kZXguanM/ZjM2NyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBrZXlzID0gcmVxdWlyZSgnb2JqZWN0LWtleXMnKTtcbnZhciBoYXNTeW1ib2xzID0gdHlwZW9mIFN5bWJvbCA9PT0gJ2Z1bmN0aW9uJyAmJiB0eXBlb2YgU3ltYm9sKCdmb28nKSA9PT0gJ3N5bWJvbCc7XG5cbnZhciB0b1N0ciA9IE9iamVjdC5wcm90b3R5cGUudG9TdHJpbmc7XG52YXIgY29uY2F0ID0gQXJyYXkucHJvdG90eXBlLmNvbmNhdDtcbnZhciBvcmlnRGVmaW5lUHJvcGVydHkgPSBPYmplY3QuZGVmaW5lUHJvcGVydHk7XG5cbnZhciBpc0Z1bmN0aW9uID0gZnVuY3Rpb24gKGZuKSB7XG5cdHJldHVybiB0eXBlb2YgZm4gPT09ICdmdW5jdGlvbicgJiYgdG9TdHIuY2FsbChmbikgPT09ICdbb2JqZWN0IEZ1bmN0aW9uXSc7XG59O1xuXG52YXIgYXJlUHJvcGVydHlEZXNjcmlwdG9yc1N1cHBvcnRlZCA9IGZ1bmN0aW9uICgpIHtcblx0dmFyIG9iaiA9IHt9O1xuXHR0cnkge1xuXHRcdG9yaWdEZWZpbmVQcm9wZXJ0eShvYmosICd4JywgeyBlbnVtZXJhYmxlOiBmYWxzZSwgdmFsdWU6IG9iaiB9KTtcblx0XHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tdW51c2VkLXZhcnMsIG5vLXJlc3RyaWN0ZWQtc3ludGF4XG5cdFx0Zm9yICh2YXIgXyBpbiBvYmopIHsgLy8ganNjczppZ25vcmUgZGlzYWxsb3dVbnVzZWRWYXJpYWJsZXNcblx0XHRcdHJldHVybiBmYWxzZTtcblx0XHR9XG5cdFx0cmV0dXJuIG9iai54ID09PSBvYmo7XG5cdH0gY2F0Y2ggKGUpIHsgLyogdGhpcyBpcyBJRSA4LiAqL1xuXHRcdHJldHVybiBmYWxzZTtcblx0fVxufTtcbnZhciBzdXBwb3J0c0Rlc2NyaXB0b3JzID0gb3JpZ0RlZmluZVByb3BlcnR5ICYmIGFyZVByb3BlcnR5RGVzY3JpcHRvcnNTdXBwb3J0ZWQoKTtcblxudmFyIGRlZmluZVByb3BlcnR5ID0gZnVuY3Rpb24gKG9iamVjdCwgbmFtZSwgdmFsdWUsIHByZWRpY2F0ZSkge1xuXHRpZiAobmFtZSBpbiBvYmplY3QgJiYgKCFpc0Z1bmN0aW9uKHByZWRpY2F0ZSkgfHwgIXByZWRpY2F0ZSgpKSkge1xuXHRcdHJldHVybjtcblx0fVxuXHRpZiAoc3VwcG9ydHNEZXNjcmlwdG9ycykge1xuXHRcdG9yaWdEZWZpbmVQcm9wZXJ0eShvYmplY3QsIG5hbWUsIHtcblx0XHRcdGNvbmZpZ3VyYWJsZTogdHJ1ZSxcblx0XHRcdGVudW1lcmFibGU6IGZhbHNlLFxuXHRcdFx0dmFsdWU6IHZhbHVlLFxuXHRcdFx0d3JpdGFibGU6IHRydWVcblx0XHR9KTtcblx0fSBlbHNlIHtcblx0XHRvYmplY3RbbmFtZV0gPSB2YWx1ZTtcblx0fVxufTtcblxudmFyIGRlZmluZVByb3BlcnRpZXMgPSBmdW5jdGlvbiAob2JqZWN0LCBtYXApIHtcblx0dmFyIHByZWRpY2F0ZXMgPSBhcmd1bWVudHMubGVuZ3RoID4gMiA/IGFyZ3VtZW50c1syXSA6IHt9O1xuXHR2YXIgcHJvcHMgPSBrZXlzKG1hcCk7XG5cdGlmIChoYXNTeW1ib2xzKSB7XG5cdFx0cHJvcHMgPSBjb25jYXQuY2FsbChwcm9wcywgT2JqZWN0LmdldE93blByb3BlcnR5U3ltYm9scyhtYXApKTtcblx0fVxuXHRmb3IgKHZhciBpID0gMDsgaSA8IHByb3BzLmxlbmd0aDsgaSArPSAxKSB7XG5cdFx0ZGVmaW5lUHJvcGVydHkob2JqZWN0LCBwcm9wc1tpXSwgbWFwW3Byb3BzW2ldXSwgcHJlZGljYXRlc1twcm9wc1tpXV0pO1xuXHR9XG59O1xuXG5kZWZpbmVQcm9wZXJ0aWVzLnN1cHBvcnRzRGVzY3JpcHRvcnMgPSAhIXN1cHBvcnRzRGVzY3JpcHRvcnM7XG5cbm1vZHVsZS5leHBvcnRzID0gZGVmaW5lUHJvcGVydGllcztcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///4289\n")},1503:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar hasSymbols = typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol';\n\nvar isPrimitive = __webpack_require__(4149);\nvar isCallable = __webpack_require__(5320);\nvar isDate = __webpack_require__(8923);\nvar isSymbol = __webpack_require__(2636);\n\nvar ordinaryToPrimitive = function OrdinaryToPrimitive(O, hint) {\n\tif (typeof O === 'undefined' || O === null) {\n\t\tthrow new TypeError('Cannot call method on ' + O);\n\t}\n\tif (typeof hint !== 'string' || (hint !== 'number' && hint !== 'string')) {\n\t\tthrow new TypeError('hint must be \"string\" or \"number\"');\n\t}\n\tvar methodNames = hint === 'string' ? ['toString', 'valueOf'] : ['valueOf', 'toString'];\n\tvar method, result, i;\n\tfor (i = 0; i < methodNames.length; ++i) {\n\t\tmethod = O[methodNames[i]];\n\t\tif (isCallable(method)) {\n\t\t\tresult = method.call(O);\n\t\t\tif (isPrimitive(result)) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\tthrow new TypeError('No default value');\n};\n\nvar GetMethod = function GetMethod(O, P) {\n\tvar func = O[P];\n\tif (func !== null && typeof func !== 'undefined') {\n\t\tif (!isCallable(func)) {\n\t\t\tthrow new TypeError(func + ' returned for property ' + P + ' of object ' + O + ' is not a function');\n\t\t}\n\t\treturn func;\n\t}\n\treturn void 0;\n};\n\n// http://www.ecma-international.org/ecma-262/6.0/#sec-toprimitive\nmodule.exports = function ToPrimitive(input) {\n\tif (isPrimitive(input)) {\n\t\treturn input;\n\t}\n\tvar hint = 'default';\n\tif (arguments.length > 1) {\n\t\tif (arguments[1] === String) {\n\t\t\thint = 'string';\n\t\t} else if (arguments[1] === Number) {\n\t\t\thint = 'number';\n\t\t}\n\t}\n\n\tvar exoticToPrim;\n\tif (hasSymbols) {\n\t\tif (Symbol.toPrimitive) {\n\t\t\texoticToPrim = GetMethod(input, Symbol.toPrimitive);\n\t\t} else if (isSymbol(input)) {\n\t\t\texoticToPrim = Symbol.prototype.valueOf;\n\t\t}\n\t}\n\tif (typeof exoticToPrim !== 'undefined') {\n\t\tvar result = exoticToPrim.call(input, hint);\n\t\tif (isPrimitive(result)) {\n\t\t\treturn result;\n\t\t}\n\t\tthrow new TypeError('unable to convert exotic object to primitive');\n\t}\n\tif (hint === 'default' && (isDate(input) || isSymbol(input))) {\n\t\thint = 'string';\n\t}\n\treturn ordinaryToPrimitive(input, hint === 'default' ? 'number' : hint);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTUwMy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQSxrQkFBa0IsbUJBQU8sQ0FBQyxJQUF1QjtBQUNqRCxpQkFBaUIsbUJBQU8sQ0FBQyxJQUFhO0FBQ3RDLGFBQWEsbUJBQU8sQ0FBQyxJQUFnQjtBQUNyQyxlQUFlLG1CQUFPLENBQUMsSUFBVzs7QUFFbEM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsYUFBYSx3QkFBd0I7QUFDckM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSTtBQUNKO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLElBQUk7QUFDSjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLXRvLXByaW1pdGl2ZS9lczIwMTUuanM/NTk5NyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBoYXNTeW1ib2xzID0gdHlwZW9mIFN5bWJvbCA9PT0gJ2Z1bmN0aW9uJyAmJiB0eXBlb2YgU3ltYm9sLml0ZXJhdG9yID09PSAnc3ltYm9sJztcblxudmFyIGlzUHJpbWl0aXZlID0gcmVxdWlyZSgnLi9oZWxwZXJzL2lzUHJpbWl0aXZlJyk7XG52YXIgaXNDYWxsYWJsZSA9IHJlcXVpcmUoJ2lzLWNhbGxhYmxlJyk7XG52YXIgaXNEYXRlID0gcmVxdWlyZSgnaXMtZGF0ZS1vYmplY3QnKTtcbnZhciBpc1N5bWJvbCA9IHJlcXVpcmUoJ2lzLXN5bWJvbCcpO1xuXG52YXIgb3JkaW5hcnlUb1ByaW1pdGl2ZSA9IGZ1bmN0aW9uIE9yZGluYXJ5VG9QcmltaXRpdmUoTywgaGludCkge1xuXHRpZiAodHlwZW9mIE8gPT09ICd1bmRlZmluZWQnIHx8IE8gPT09IG51bGwpIHtcblx0XHR0aHJvdyBuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FsbCBtZXRob2Qgb24gJyArIE8pO1xuXHR9XG5cdGlmICh0eXBlb2YgaGludCAhPT0gJ3N0cmluZycgfHwgKGhpbnQgIT09ICdudW1iZXInICYmIGhpbnQgIT09ICdzdHJpbmcnKSkge1xuXHRcdHRocm93IG5ldyBUeXBlRXJyb3IoJ2hpbnQgbXVzdCBiZSBcInN0cmluZ1wiIG9yIFwibnVtYmVyXCInKTtcblx0fVxuXHR2YXIgbWV0aG9kTmFtZXMgPSBoaW50ID09PSAnc3RyaW5nJyA/IFsndG9TdHJpbmcnLCAndmFsdWVPZiddIDogWyd2YWx1ZU9mJywgJ3RvU3RyaW5nJ107XG5cdHZhciBtZXRob2QsIHJlc3VsdCwgaTtcblx0Zm9yIChpID0gMDsgaSA8IG1ldGhvZE5hbWVzLmxlbmd0aDsgKytpKSB7XG5cdFx0bWV0aG9kID0gT1ttZXRob2ROYW1lc1tpXV07XG5cdFx0aWYgKGlzQ2FsbGFibGUobWV0aG9kKSkge1xuXHRcdFx0cmVzdWx0ID0gbWV0aG9kLmNhbGwoTyk7XG5cdFx0XHRpZiAoaXNQcmltaXRpdmUocmVzdWx0KSkge1xuXHRcdFx0XHRyZXR1cm4gcmVzdWx0O1xuXHRcdFx0fVxuXHRcdH1cblx0fVxuXHR0aHJvdyBuZXcgVHlwZUVycm9yKCdObyBkZWZhdWx0IHZhbHVlJyk7XG59O1xuXG52YXIgR2V0TWV0aG9kID0gZnVuY3Rpb24gR2V0TWV0aG9kKE8sIFApIHtcblx0dmFyIGZ1bmMgPSBPW1BdO1xuXHRpZiAoZnVuYyAhPT0gbnVsbCAmJiB0eXBlb2YgZnVuYyAhPT0gJ3VuZGVmaW5lZCcpIHtcblx0XHRpZiAoIWlzQ2FsbGFibGUoZnVuYykpIHtcblx0XHRcdHRocm93IG5ldyBUeXBlRXJyb3IoZnVuYyArICcgcmV0dXJuZWQgZm9yIHByb3BlcnR5ICcgKyBQICsgJyBvZiBvYmplY3QgJyArIE8gKyAnIGlzIG5vdCBhIGZ1bmN0aW9uJyk7XG5cdFx0fVxuXHRcdHJldHVybiBmdW5jO1xuXHR9XG5cdHJldHVybiB2b2lkIDA7XG59O1xuXG4vLyBodHRwOi8vd3d3LmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtdG9wcmltaXRpdmVcbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVG9QcmltaXRpdmUoaW5wdXQpIHtcblx0aWYgKGlzUHJpbWl0aXZlKGlucHV0KSkge1xuXHRcdHJldHVybiBpbnB1dDtcblx0fVxuXHR2YXIgaGludCA9ICdkZWZhdWx0Jztcblx0aWYgKGFyZ3VtZW50cy5sZW5ndGggPiAxKSB7XG5cdFx0aWYgKGFyZ3VtZW50c1sxXSA9PT0gU3RyaW5nKSB7XG5cdFx0XHRoaW50ID0gJ3N0cmluZyc7XG5cdFx0fSBlbHNlIGlmIChhcmd1bWVudHNbMV0gPT09IE51bWJlcikge1xuXHRcdFx0aGludCA9ICdudW1iZXInO1xuXHRcdH1cblx0fVxuXG5cdHZhciBleG90aWNUb1ByaW07XG5cdGlmIChoYXNTeW1ib2xzKSB7XG5cdFx0aWYgKFN5bWJvbC50b1ByaW1pdGl2ZSkge1xuXHRcdFx0ZXhvdGljVG9QcmltID0gR2V0TWV0aG9kKGlucHV0LCBTeW1ib2wudG9QcmltaXRpdmUpO1xuXHRcdH0gZWxzZSBpZiAoaXNTeW1ib2woaW5wdXQpKSB7XG5cdFx0XHRleG90aWNUb1ByaW0gPSBTeW1ib2wucHJvdG90eXBlLnZhbHVlT2Y7XG5cdFx0fVxuXHR9XG5cdGlmICh0eXBlb2YgZXhvdGljVG9QcmltICE9PSAndW5kZWZpbmVkJykge1xuXHRcdHZhciByZXN1bHQgPSBleG90aWNUb1ByaW0uY2FsbChpbnB1dCwgaGludCk7XG5cdFx0aWYgKGlzUHJpbWl0aXZlKHJlc3VsdCkpIHtcblx0XHRcdHJldHVybiByZXN1bHQ7XG5cdFx0fVxuXHRcdHRocm93IG5ldyBUeXBlRXJyb3IoJ3VuYWJsZSB0byBjb252ZXJ0IGV4b3RpYyBvYmplY3QgdG8gcHJpbWl0aXZlJyk7XG5cdH1cblx0aWYgKGhpbnQgPT09ICdkZWZhdWx0JyAmJiAoaXNEYXRlKGlucHV0KSB8fCBpc1N5bWJvbChpbnB1dCkpKSB7XG5cdFx0aGludCA9ICdzdHJpbmcnO1xuXHR9XG5cdHJldHVybiBvcmRpbmFyeVRvUHJpbWl0aXZlKGlucHV0LCBoaW50ID09PSAnZGVmYXVsdCcgPyAnbnVtYmVyJyA6IGhpbnQpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///1503\n")},2116:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar toStr = Object.prototype.toString;\n\nvar isPrimitive = __webpack_require__(4149);\n\nvar isCallable = __webpack_require__(5320);\n\n// http://ecma-international.org/ecma-262/5.1/#sec-8.12.8\nvar ES5internalSlots = {\n\t'[[DefaultValue]]': function (O) {\n\t\tvar actualHint;\n\t\tif (arguments.length > 1) {\n\t\t\tactualHint = arguments[1];\n\t\t} else {\n\t\t\tactualHint = toStr.call(O) === '[object Date]' ? String : Number;\n\t\t}\n\n\t\tif (actualHint === String || actualHint === Number) {\n\t\t\tvar methods = actualHint === String ? ['toString', 'valueOf'] : ['valueOf', 'toString'];\n\t\t\tvar value, i;\n\t\t\tfor (i = 0; i < methods.length; ++i) {\n\t\t\t\tif (isCallable(O[methods[i]])) {\n\t\t\t\t\tvalue = O[methods[i]]();\n\t\t\t\t\tif (isPrimitive(value)) {\n\t\t\t\t\t\treturn value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new TypeError('No default value');\n\t\t}\n\t\tthrow new TypeError('invalid [[DefaultValue]] hint supplied');\n\t}\n};\n\n// http://ecma-international.org/ecma-262/5.1/#sec-9.1\nmodule.exports = function ToPrimitive(input) {\n\tif (isPrimitive(input)) {\n\t\treturn input;\n\t}\n\tif (arguments.length > 1) {\n\t\treturn ES5internalSlots['[[DefaultValue]]'](input, arguments[1]);\n\t}\n\treturn ES5internalSlots['[[DefaultValue]]'](input);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjExNi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQSxrQkFBa0IsbUJBQU8sQ0FBQyxJQUF1Qjs7QUFFakQsaUJBQWlCLG1CQUFPLENBQUMsSUFBYTs7QUFFdEM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSTtBQUNKO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0EsZUFBZSxvQkFBb0I7QUFDbkM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy10by1wcmltaXRpdmUvZXM1LmpzPzJmMTciXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgdG9TdHIgPSBPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nO1xuXG52YXIgaXNQcmltaXRpdmUgPSByZXF1aXJlKCcuL2hlbHBlcnMvaXNQcmltaXRpdmUnKTtcblxudmFyIGlzQ2FsbGFibGUgPSByZXF1aXJlKCdpcy1jYWxsYWJsZScpO1xuXG4vLyBodHRwOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi81LjEvI3NlYy04LjEyLjhcbnZhciBFUzVpbnRlcm5hbFNsb3RzID0ge1xuXHQnW1tEZWZhdWx0VmFsdWVdXSc6IGZ1bmN0aW9uIChPKSB7XG5cdFx0dmFyIGFjdHVhbEhpbnQ7XG5cdFx0aWYgKGFyZ3VtZW50cy5sZW5ndGggPiAxKSB7XG5cdFx0XHRhY3R1YWxIaW50ID0gYXJndW1lbnRzWzFdO1xuXHRcdH0gZWxzZSB7XG5cdFx0XHRhY3R1YWxIaW50ID0gdG9TdHIuY2FsbChPKSA9PT0gJ1tvYmplY3QgRGF0ZV0nID8gU3RyaW5nIDogTnVtYmVyO1xuXHRcdH1cblxuXHRcdGlmIChhY3R1YWxIaW50ID09PSBTdHJpbmcgfHwgYWN0dWFsSGludCA9PT0gTnVtYmVyKSB7XG5cdFx0XHR2YXIgbWV0aG9kcyA9IGFjdHVhbEhpbnQgPT09IFN0cmluZyA/IFsndG9TdHJpbmcnLCAndmFsdWVPZiddIDogWyd2YWx1ZU9mJywgJ3RvU3RyaW5nJ107XG5cdFx0XHR2YXIgdmFsdWUsIGk7XG5cdFx0XHRmb3IgKGkgPSAwOyBpIDwgbWV0aG9kcy5sZW5ndGg7ICsraSkge1xuXHRcdFx0XHRpZiAoaXNDYWxsYWJsZShPW21ldGhvZHNbaV1dKSkge1xuXHRcdFx0XHRcdHZhbHVlID0gT1ttZXRob2RzW2ldXSgpO1xuXHRcdFx0XHRcdGlmIChpc1ByaW1pdGl2ZSh2YWx1ZSkpIHtcblx0XHRcdFx0XHRcdHJldHVybiB2YWx1ZTtcblx0XHRcdFx0XHR9XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHRcdHRocm93IG5ldyBUeXBlRXJyb3IoJ05vIGRlZmF1bHQgdmFsdWUnKTtcblx0XHR9XG5cdFx0dGhyb3cgbmV3IFR5cGVFcnJvcignaW52YWxpZCBbW0RlZmF1bHRWYWx1ZV1dIGhpbnQgc3VwcGxpZWQnKTtcblx0fVxufTtcblxuLy8gaHR0cDovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNS4xLyNzZWMtOS4xXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFRvUHJpbWl0aXZlKGlucHV0KSB7XG5cdGlmIChpc1ByaW1pdGl2ZShpbnB1dCkpIHtcblx0XHRyZXR1cm4gaW5wdXQ7XG5cdH1cblx0aWYgKGFyZ3VtZW50cy5sZW5ndGggPiAxKSB7XG5cdFx0cmV0dXJuIEVTNWludGVybmFsU2xvdHNbJ1tbRGVmYXVsdFZhbHVlXV0nXShpbnB1dCwgYXJndW1lbnRzWzFdKTtcblx0fVxuXHRyZXR1cm4gRVM1aW50ZXJuYWxTbG90c1snW1tEZWZhdWx0VmFsdWVdXSddKGlucHV0KTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///2116\n")},4149:function(module){"use strict";eval("\n\nmodule.exports = function isPrimitive(value) {\n\treturn value === null || (typeof value !== 'function' && typeof value !== 'object');\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDE0OS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLXRvLXByaW1pdGl2ZS9oZWxwZXJzL2lzUHJpbWl0aXZlLmpzPzRkZTgiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIGlzUHJpbWl0aXZlKHZhbHVlKSB7XG5cdHJldHVybiB2YWx1ZSA9PT0gbnVsbCB8fCAodHlwZW9mIHZhbHVlICE9PSAnZnVuY3Rpb24nICYmIHR5cGVvZiB2YWx1ZSAhPT0gJ29iamVjdCcpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///4149\n")},7648:function(module){"use strict";eval("\n\n/* eslint no-invalid-this: 1 */\n\nvar ERROR_MESSAGE = 'Function.prototype.bind called on incompatible ';\nvar slice = Array.prototype.slice;\nvar toStr = Object.prototype.toString;\nvar funcType = '[object Function]';\n\nmodule.exports = function bind(that) {\n var target = this;\n if (typeof target !== 'function' || toStr.call(target) !== funcType) {\n throw new TypeError(ERROR_MESSAGE + target);\n }\n var args = slice.call(arguments, 1);\n\n var bound;\n var binder = function () {\n if (this instanceof bound) {\n var result = target.apply(\n this,\n args.concat(slice.call(arguments))\n );\n if (Object(result) === result) {\n return result;\n }\n return this;\n } else {\n return target.apply(\n that,\n args.concat(slice.call(arguments))\n );\n }\n };\n\n var boundLength = Math.max(0, target.length - args.length);\n var boundArgs = [];\n for (var i = 0; i < boundLength; i++) {\n boundArgs.push('$' + i);\n }\n\n bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this,arguments); }')(binder);\n\n if (target.prototype) {\n var Empty = function Empty() {};\n Empty.prototype = target.prototype;\n bound.prototype = new Empty();\n Empty.prototype = null;\n }\n\n return bound;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzY0OC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLFVBQVU7QUFDVjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBLG9CQUFvQixpQkFBaUI7QUFDckM7QUFDQTs7QUFFQSwrRUFBK0Usc0NBQXNDOztBQUVySDtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZnVuY3Rpb24tYmluZC9pbXBsZW1lbnRhdGlvbi5qcz82ODhlIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxuLyogZXNsaW50IG5vLWludmFsaWQtdGhpczogMSAqL1xuXG52YXIgRVJST1JfTUVTU0FHRSA9ICdGdW5jdGlvbi5wcm90b3R5cGUuYmluZCBjYWxsZWQgb24gaW5jb21wYXRpYmxlICc7XG52YXIgc2xpY2UgPSBBcnJheS5wcm90b3R5cGUuc2xpY2U7XG52YXIgdG9TdHIgPSBPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nO1xudmFyIGZ1bmNUeXBlID0gJ1tvYmplY3QgRnVuY3Rpb25dJztcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBiaW5kKHRoYXQpIHtcbiAgICB2YXIgdGFyZ2V0ID0gdGhpcztcbiAgICBpZiAodHlwZW9mIHRhcmdldCAhPT0gJ2Z1bmN0aW9uJyB8fCB0b1N0ci5jYWxsKHRhcmdldCkgIT09IGZ1bmNUeXBlKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoRVJST1JfTUVTU0FHRSArIHRhcmdldCk7XG4gICAgfVxuICAgIHZhciBhcmdzID0gc2xpY2UuY2FsbChhcmd1bWVudHMsIDEpO1xuXG4gICAgdmFyIGJvdW5kO1xuICAgIHZhciBiaW5kZXIgPSBmdW5jdGlvbiAoKSB7XG4gICAgICAgIGlmICh0aGlzIGluc3RhbmNlb2YgYm91bmQpIHtcbiAgICAgICAgICAgIHZhciByZXN1bHQgPSB0YXJnZXQuYXBwbHkoXG4gICAgICAgICAgICAgICAgdGhpcyxcbiAgICAgICAgICAgICAgICBhcmdzLmNvbmNhdChzbGljZS5jYWxsKGFyZ3VtZW50cykpXG4gICAgICAgICAgICApO1xuICAgICAgICAgICAgaWYgKE9iamVjdChyZXN1bHQpID09PSByZXN1bHQpIHtcbiAgICAgICAgICAgICAgICByZXR1cm4gcmVzdWx0O1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgcmV0dXJuIHRoaXM7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICByZXR1cm4gdGFyZ2V0LmFwcGx5KFxuICAgICAgICAgICAgICAgIHRoYXQsXG4gICAgICAgICAgICAgICAgYXJncy5jb25jYXQoc2xpY2UuY2FsbChhcmd1bWVudHMpKVxuICAgICAgICAgICAgKTtcbiAgICAgICAgfVxuICAgIH07XG5cbiAgICB2YXIgYm91bmRMZW5ndGggPSBNYXRoLm1heCgwLCB0YXJnZXQubGVuZ3RoIC0gYXJncy5sZW5ndGgpO1xuICAgIHZhciBib3VuZEFyZ3MgPSBbXTtcbiAgICBmb3IgKHZhciBpID0gMDsgaSA8IGJvdW5kTGVuZ3RoOyBpKyspIHtcbiAgICAgICAgYm91bmRBcmdzLnB1c2goJyQnICsgaSk7XG4gICAgfVxuXG4gICAgYm91bmQgPSBGdW5jdGlvbignYmluZGVyJywgJ3JldHVybiBmdW5jdGlvbiAoJyArIGJvdW5kQXJncy5qb2luKCcsJykgKyAnKXsgcmV0dXJuIGJpbmRlci5hcHBseSh0aGlzLGFyZ3VtZW50cyk7IH0nKShiaW5kZXIpO1xuXG4gICAgaWYgKHRhcmdldC5wcm90b3R5cGUpIHtcbiAgICAgICAgdmFyIEVtcHR5ID0gZnVuY3Rpb24gRW1wdHkoKSB7fTtcbiAgICAgICAgRW1wdHkucHJvdG90eXBlID0gdGFyZ2V0LnByb3RvdHlwZTtcbiAgICAgICAgYm91bmQucHJvdG90eXBlID0gbmV3IEVtcHR5KCk7XG4gICAgICAgIEVtcHR5LnByb3RvdHlwZSA9IG51bGw7XG4gICAgfVxuXG4gICAgcmV0dXJuIGJvdW5kO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///7648\n")},8612:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar implementation = __webpack_require__(7648);\n\nmodule.exports = Function.prototype.bind || implementation;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODYxMi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixxQkFBcUIsbUJBQU8sQ0FBQyxJQUFrQjs7QUFFL0MiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2Z1bmN0aW9uLWJpbmQvaW5kZXguanM/MGY3YyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBpbXBsZW1lbnRhdGlvbiA9IHJlcXVpcmUoJy4vaW1wbGVtZW50YXRpb24nKTtcblxubW9kdWxlLmV4cG9ydHMgPSBGdW5jdGlvbi5wcm90b3R5cGUuYmluZCB8fCBpbXBsZW1lbnRhdGlvbjtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///8612\n")},210:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar undefined;\n\nvar $SyntaxError = SyntaxError;\nvar $Function = Function;\nvar $TypeError = TypeError;\n\n// eslint-disable-next-line consistent-return\nvar getEvalledConstructor = function (expressionSyntax) {\n\ttry {\n\t\treturn $Function('\"use strict\"; return (' + expressionSyntax + ').constructor;')();\n\t} catch (e) {}\n};\n\nvar $gOPD = Object.getOwnPropertyDescriptor;\nif ($gOPD) {\n\ttry {\n\t\t$gOPD({}, '');\n\t} catch (e) {\n\t\t$gOPD = null; // this is IE 8, which has a broken gOPD\n\t}\n}\n\nvar throwTypeError = function () {\n\tthrow new $TypeError();\n};\nvar ThrowTypeError = $gOPD\n\t? (function () {\n\t\ttry {\n\t\t\t// eslint-disable-next-line no-unused-expressions, no-caller, no-restricted-properties\n\t\t\targuments.callee; // IE 8 does not throw here\n\t\t\treturn throwTypeError;\n\t\t} catch (calleeThrows) {\n\t\t\ttry {\n\t\t\t\t// IE 8 throws on Object.getOwnPropertyDescriptor(arguments, '')\n\t\t\t\treturn $gOPD(arguments, 'callee').get;\n\t\t\t} catch (gOPDthrows) {\n\t\t\t\treturn throwTypeError;\n\t\t\t}\n\t\t}\n\t}())\n\t: throwTypeError;\n\nvar hasSymbols = __webpack_require__(1405)();\n\nvar getProto = Object.getPrototypeOf || function (x) { return x.__proto__; }; // eslint-disable-line no-proto\n\nvar needsEval = {};\n\nvar TypedArray = typeof Uint8Array === 'undefined' ? undefined : getProto(Uint8Array);\n\nvar INTRINSICS = {\n\t'%AggregateError%': typeof AggregateError === 'undefined' ? undefined : AggregateError,\n\t'%Array%': Array,\n\t'%ArrayBuffer%': typeof ArrayBuffer === 'undefined' ? undefined : ArrayBuffer,\n\t'%ArrayIteratorPrototype%': hasSymbols ? getProto([][Symbol.iterator]()) : undefined,\n\t'%AsyncFromSyncIteratorPrototype%': undefined,\n\t'%AsyncFunction%': needsEval,\n\t'%AsyncGenerator%': needsEval,\n\t'%AsyncGeneratorFunction%': needsEval,\n\t'%AsyncIteratorPrototype%': needsEval,\n\t'%Atomics%': typeof Atomics === 'undefined' ? undefined : Atomics,\n\t'%BigInt%': typeof BigInt === 'undefined' ? undefined : BigInt,\n\t'%Boolean%': Boolean,\n\t'%DataView%': typeof DataView === 'undefined' ? undefined : DataView,\n\t'%Date%': Date,\n\t'%decodeURI%': decodeURI,\n\t'%decodeURIComponent%': decodeURIComponent,\n\t'%encodeURI%': encodeURI,\n\t'%encodeURIComponent%': encodeURIComponent,\n\t'%Error%': Error,\n\t'%eval%': eval, // eslint-disable-line no-eval\n\t'%EvalError%': EvalError,\n\t'%Float32Array%': typeof Float32Array === 'undefined' ? undefined : Float32Array,\n\t'%Float64Array%': typeof Float64Array === 'undefined' ? undefined : Float64Array,\n\t'%FinalizationRegistry%': typeof FinalizationRegistry === 'undefined' ? undefined : FinalizationRegistry,\n\t'%Function%': $Function,\n\t'%GeneratorFunction%': needsEval,\n\t'%Int8Array%': typeof Int8Array === 'undefined' ? undefined : Int8Array,\n\t'%Int16Array%': typeof Int16Array === 'undefined' ? undefined : Int16Array,\n\t'%Int32Array%': typeof Int32Array === 'undefined' ? undefined : Int32Array,\n\t'%isFinite%': isFinite,\n\t'%isNaN%': isNaN,\n\t'%IteratorPrototype%': hasSymbols ? getProto(getProto([][Symbol.iterator]())) : undefined,\n\t'%JSON%': typeof JSON === 'object' ? JSON : undefined,\n\t'%Map%': typeof Map === 'undefined' ? undefined : Map,\n\t'%MapIteratorPrototype%': typeof Map === 'undefined' || !hasSymbols ? undefined : getProto(new Map()[Symbol.iterator]()),\n\t'%Math%': Math,\n\t'%Number%': Number,\n\t'%Object%': Object,\n\t'%parseFloat%': parseFloat,\n\t'%parseInt%': parseInt,\n\t'%Promise%': typeof Promise === 'undefined' ? undefined : Promise,\n\t'%Proxy%': typeof Proxy === 'undefined' ? undefined : Proxy,\n\t'%RangeError%': RangeError,\n\t'%ReferenceError%': ReferenceError,\n\t'%Reflect%': typeof Reflect === 'undefined' ? undefined : Reflect,\n\t'%RegExp%': RegExp,\n\t'%Set%': typeof Set === 'undefined' ? undefined : Set,\n\t'%SetIteratorPrototype%': typeof Set === 'undefined' || !hasSymbols ? undefined : getProto(new Set()[Symbol.iterator]()),\n\t'%SharedArrayBuffer%': typeof SharedArrayBuffer === 'undefined' ? undefined : SharedArrayBuffer,\n\t'%String%': String,\n\t'%StringIteratorPrototype%': hasSymbols ? getProto(''[Symbol.iterator]()) : undefined,\n\t'%Symbol%': hasSymbols ? Symbol : undefined,\n\t'%SyntaxError%': $SyntaxError,\n\t'%ThrowTypeError%': ThrowTypeError,\n\t'%TypedArray%': TypedArray,\n\t'%TypeError%': $TypeError,\n\t'%Uint8Array%': typeof Uint8Array === 'undefined' ? undefined : Uint8Array,\n\t'%Uint8ClampedArray%': typeof Uint8ClampedArray === 'undefined' ? undefined : Uint8ClampedArray,\n\t'%Uint16Array%': typeof Uint16Array === 'undefined' ? undefined : Uint16Array,\n\t'%Uint32Array%': typeof Uint32Array === 'undefined' ? undefined : Uint32Array,\n\t'%URIError%': URIError,\n\t'%WeakMap%': typeof WeakMap === 'undefined' ? undefined : WeakMap,\n\t'%WeakRef%': typeof WeakRef === 'undefined' ? undefined : WeakRef,\n\t'%WeakSet%': typeof WeakSet === 'undefined' ? undefined : WeakSet\n};\n\nvar doEval = function doEval(name) {\n\tvar value;\n\tif (name === '%AsyncFunction%') {\n\t\tvalue = getEvalledConstructor('async function () {}');\n\t} else if (name === '%GeneratorFunction%') {\n\t\tvalue = getEvalledConstructor('function* () {}');\n\t} else if (name === '%AsyncGeneratorFunction%') {\n\t\tvalue = getEvalledConstructor('async function* () {}');\n\t} else if (name === '%AsyncGenerator%') {\n\t\tvar fn = doEval('%AsyncGeneratorFunction%');\n\t\tif (fn) {\n\t\t\tvalue = fn.prototype;\n\t\t}\n\t} else if (name === '%AsyncIteratorPrototype%') {\n\t\tvar gen = doEval('%AsyncGenerator%');\n\t\tif (gen) {\n\t\t\tvalue = getProto(gen.prototype);\n\t\t}\n\t}\n\n\tINTRINSICS[name] = value;\n\n\treturn value;\n};\n\nvar LEGACY_ALIASES = {\n\t'%ArrayBufferPrototype%': ['ArrayBuffer', 'prototype'],\n\t'%ArrayPrototype%': ['Array', 'prototype'],\n\t'%ArrayProto_entries%': ['Array', 'prototype', 'entries'],\n\t'%ArrayProto_forEach%': ['Array', 'prototype', 'forEach'],\n\t'%ArrayProto_keys%': ['Array', 'prototype', 'keys'],\n\t'%ArrayProto_values%': ['Array', 'prototype', 'values'],\n\t'%AsyncFunctionPrototype%': ['AsyncFunction', 'prototype'],\n\t'%AsyncGenerator%': ['AsyncGeneratorFunction', 'prototype'],\n\t'%AsyncGeneratorPrototype%': ['AsyncGeneratorFunction', 'prototype', 'prototype'],\n\t'%BooleanPrototype%': ['Boolean', 'prototype'],\n\t'%DataViewPrototype%': ['DataView', 'prototype'],\n\t'%DatePrototype%': ['Date', 'prototype'],\n\t'%ErrorPrototype%': ['Error', 'prototype'],\n\t'%EvalErrorPrototype%': ['EvalError', 'prototype'],\n\t'%Float32ArrayPrototype%': ['Float32Array', 'prototype'],\n\t'%Float64ArrayPrototype%': ['Float64Array', 'prototype'],\n\t'%FunctionPrototype%': ['Function', 'prototype'],\n\t'%Generator%': ['GeneratorFunction', 'prototype'],\n\t'%GeneratorPrototype%': ['GeneratorFunction', 'prototype', 'prototype'],\n\t'%Int8ArrayPrototype%': ['Int8Array', 'prototype'],\n\t'%Int16ArrayPrototype%': ['Int16Array', 'prototype'],\n\t'%Int32ArrayPrototype%': ['Int32Array', 'prototype'],\n\t'%JSONParse%': ['JSON', 'parse'],\n\t'%JSONStringify%': ['JSON', 'stringify'],\n\t'%MapPrototype%': ['Map', 'prototype'],\n\t'%NumberPrototype%': ['Number', 'prototype'],\n\t'%ObjectPrototype%': ['Object', 'prototype'],\n\t'%ObjProto_toString%': ['Object', 'prototype', 'toString'],\n\t'%ObjProto_valueOf%': ['Object', 'prototype', 'valueOf'],\n\t'%PromisePrototype%': ['Promise', 'prototype'],\n\t'%PromiseProto_then%': ['Promise', 'prototype', 'then'],\n\t'%Promise_all%': ['Promise', 'all'],\n\t'%Promise_reject%': ['Promise', 'reject'],\n\t'%Promise_resolve%': ['Promise', 'resolve'],\n\t'%RangeErrorPrototype%': ['RangeError', 'prototype'],\n\t'%ReferenceErrorPrototype%': ['ReferenceError', 'prototype'],\n\t'%RegExpPrototype%': ['RegExp', 'prototype'],\n\t'%SetPrototype%': ['Set', 'prototype'],\n\t'%SharedArrayBufferPrototype%': ['SharedArrayBuffer', 'prototype'],\n\t'%StringPrototype%': ['String', 'prototype'],\n\t'%SymbolPrototype%': ['Symbol', 'prototype'],\n\t'%SyntaxErrorPrototype%': ['SyntaxError', 'prototype'],\n\t'%TypedArrayPrototype%': ['TypedArray', 'prototype'],\n\t'%TypeErrorPrototype%': ['TypeError', 'prototype'],\n\t'%Uint8ArrayPrototype%': ['Uint8Array', 'prototype'],\n\t'%Uint8ClampedArrayPrototype%': ['Uint8ClampedArray', 'prototype'],\n\t'%Uint16ArrayPrototype%': ['Uint16Array', 'prototype'],\n\t'%Uint32ArrayPrototype%': ['Uint32Array', 'prototype'],\n\t'%URIErrorPrototype%': ['URIError', 'prototype'],\n\t'%WeakMapPrototype%': ['WeakMap', 'prototype'],\n\t'%WeakSetPrototype%': ['WeakSet', 'prototype']\n};\n\nvar bind = __webpack_require__(8612);\nvar hasOwn = __webpack_require__(7642);\nvar $concat = bind.call(Function.call, Array.prototype.concat);\nvar $spliceApply = bind.call(Function.apply, Array.prototype.splice);\nvar $replace = bind.call(Function.call, String.prototype.replace);\nvar $strSlice = bind.call(Function.call, String.prototype.slice);\n\n/* adapted from https://github.com/lodash/lodash/blob/4.17.15/dist/lodash.js#L6735-L6744 */\nvar rePropName = /[^%.[\\]]+|\\[(?:(-?\\d+(?:\\.\\d+)?)|([\"'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|%$))/g;\nvar reEscapeChar = /\\\\(\\\\)?/g; /** Used to match backslashes in property paths. */\nvar stringToPath = function stringToPath(string) {\n\tvar first = $strSlice(string, 0, 1);\n\tvar last = $strSlice(string, -1);\n\tif (first === '%' && last !== '%') {\n\t\tthrow new $SyntaxError('invalid intrinsic syntax, expected closing `%`');\n\t} else if (last === '%' && first !== '%') {\n\t\tthrow new $SyntaxError('invalid intrinsic syntax, expected opening `%`');\n\t}\n\tvar result = [];\n\t$replace(string, rePropName, function (match, number, quote, subString) {\n\t\tresult[result.length] = quote ? $replace(subString, reEscapeChar, '$1') : number || match;\n\t});\n\treturn result;\n};\n/* end adaptation */\n\nvar getBaseIntrinsic = function getBaseIntrinsic(name, allowMissing) {\n\tvar intrinsicName = name;\n\tvar alias;\n\tif (hasOwn(LEGACY_ALIASES, intrinsicName)) {\n\t\talias = LEGACY_ALIASES[intrinsicName];\n\t\tintrinsicName = '%' + alias[0] + '%';\n\t}\n\n\tif (hasOwn(INTRINSICS, intrinsicName)) {\n\t\tvar value = INTRINSICS[intrinsicName];\n\t\tif (value === needsEval) {\n\t\t\tvalue = doEval(intrinsicName);\n\t\t}\n\t\tif (typeof value === 'undefined' && !allowMissing) {\n\t\t\tthrow new $TypeError('intrinsic ' + name + ' exists, but is not available. Please file an issue!');\n\t\t}\n\n\t\treturn {\n\t\t\talias: alias,\n\t\t\tname: intrinsicName,\n\t\t\tvalue: value\n\t\t};\n\t}\n\n\tthrow new $SyntaxError('intrinsic ' + name + ' does not exist!');\n};\n\nmodule.exports = function GetIntrinsic(name, allowMissing) {\n\tif (typeof name !== 'string' || name.length === 0) {\n\t\tthrow new $TypeError('intrinsic name must be a non-empty string');\n\t}\n\tif (arguments.length > 1 && typeof allowMissing !== 'boolean') {\n\t\tthrow new $TypeError('\"allowMissing\" argument must be a boolean');\n\t}\n\n\tvar parts = stringToPath(name);\n\tvar intrinsicBaseName = parts.length > 0 ? parts[0] : '';\n\n\tvar intrinsic = getBaseIntrinsic('%' + intrinsicBaseName + '%', allowMissing);\n\tvar intrinsicRealName = intrinsic.name;\n\tvar value = intrinsic.value;\n\tvar skipFurtherCaching = false;\n\n\tvar alias = intrinsic.alias;\n\tif (alias) {\n\t\tintrinsicBaseName = alias[0];\n\t\t$spliceApply(parts, $concat([0, 1], alias));\n\t}\n\n\tfor (var i = 1, isOwn = true; i < parts.length; i += 1) {\n\t\tvar part = parts[i];\n\t\tvar first = $strSlice(part, 0, 1);\n\t\tvar last = $strSlice(part, -1);\n\t\tif (\n\t\t\t(\n\t\t\t\t(first === '\"' || first === \"'\" || first === '`')\n\t\t\t\t|| (last === '\"' || last === \"'\" || last === '`')\n\t\t\t)\n\t\t\t&& first !== last\n\t\t) {\n\t\t\tthrow new $SyntaxError('property names with quotes must have matching quotes');\n\t\t}\n\t\tif (part === 'constructor' || !isOwn) {\n\t\t\tskipFurtherCaching = true;\n\t\t}\n\n\t\tintrinsicBaseName += '.' + part;\n\t\tintrinsicRealName = '%' + intrinsicBaseName + '%';\n\n\t\tif (hasOwn(INTRINSICS, intrinsicRealName)) {\n\t\t\tvalue = INTRINSICS[intrinsicRealName];\n\t\t} else if (value != null) {\n\t\t\tif (!(part in value)) {\n\t\t\t\tif (!allowMissing) {\n\t\t\t\t\tthrow new $TypeError('base intrinsic for ' + name + ' exists, but the property is not available.');\n\t\t\t\t}\n\t\t\t\treturn void undefined;\n\t\t\t}\n\t\t\tif ($gOPD && (i + 1) >= parts.length) {\n\t\t\t\tvar desc = $gOPD(value, part);\n\t\t\t\tisOwn = !!desc;\n\n\t\t\t\t// By convention, when a data property is converted to an accessor\n\t\t\t\t// property to emulate a data property that does not suffer from\n\t\t\t\t// the override mistake, that accessor's getter is marked with\n\t\t\t\t// an `originalValue` property. Here, when we detect this, we\n\t\t\t\t// uphold the illusion by pretending to see that original data\n\t\t\t\t// property, i.e., returning the value rather than the getter\n\t\t\t\t// itself.\n\t\t\t\tif (isOwn && 'get' in desc && !('originalValue' in desc.get)) {\n\t\t\t\t\tvalue = desc.get;\n\t\t\t\t} else {\n\t\t\t\t\tvalue = value[part];\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tisOwn = hasOwn(value, part);\n\t\t\t\tvalue = value[part];\n\t\t\t}\n\n\t\t\tif (isOwn && !skipFurtherCaching) {\n\t\t\t\tINTRINSICS[intrinsicRealName] = value;\n\t\t\t}\n\t\t}\n\t}\n\treturn value;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,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O0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZ2V0LWludHJpbnNpYy9pbmRleC5qcz8wMGNlIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIHVuZGVmaW5lZDtcblxudmFyICRTeW50YXhFcnJvciA9IFN5bnRheEVycm9yO1xudmFyICRGdW5jdGlvbiA9IEZ1bmN0aW9uO1xudmFyICRUeXBlRXJyb3IgPSBUeXBlRXJyb3I7XG5cbi8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBjb25zaXN0ZW50LXJldHVyblxudmFyIGdldEV2YWxsZWRDb25zdHJ1Y3RvciA9IGZ1bmN0aW9uIChleHByZXNzaW9uU3ludGF4KSB7XG5cdHRyeSB7XG5cdFx0cmV0dXJuICRGdW5jdGlvbignXCJ1c2Ugc3RyaWN0XCI7IHJldHVybiAoJyArIGV4cHJlc3Npb25TeW50YXggKyAnKS5jb25zdHJ1Y3RvcjsnKSgpO1xuXHR9IGNhdGNoIChlKSB7fVxufTtcblxudmFyICRnT1BEID0gT2JqZWN0LmdldE93blByb3BlcnR5RGVzY3JpcHRvcjtcbmlmICgkZ09QRCkge1xuXHR0cnkge1xuXHRcdCRnT1BEKHt9LCAnJyk7XG5cdH0gY2F0Y2ggKGUpIHtcblx0XHQkZ09QRCA9IG51bGw7IC8vIHRoaXMgaXMgSUUgOCwgd2hpY2ggaGFzIGEgYnJva2VuIGdPUERcblx0fVxufVxuXG52YXIgdGhyb3dUeXBlRXJyb3IgPSBmdW5jdGlvbiAoKSB7XG5cdHRocm93IG5ldyAkVHlwZUVycm9yKCk7XG59O1xudmFyIFRocm93VHlwZUVycm9yID0gJGdPUERcblx0PyAoZnVuY3Rpb24gKCkge1xuXHRcdHRyeSB7XG5cdFx0XHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tdW51c2VkLWV4cHJlc3Npb25zLCBuby1jYWxsZXIsIG5vLXJlc3RyaWN0ZWQtcHJvcGVydGllc1xuXHRcdFx0YXJndW1lbnRzLmNhbGxlZTsgLy8gSUUgOCBkb2VzIG5vdCB0aHJvdyBoZXJlXG5cdFx0XHRyZXR1cm4gdGhyb3dUeXBlRXJyb3I7XG5cdFx0fSBjYXRjaCAoY2FsbGVlVGhyb3dzKSB7XG5cdFx0XHR0cnkge1xuXHRcdFx0XHQvLyBJRSA4IHRocm93cyBvbiBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yKGFyZ3VtZW50cywgJycpXG5cdFx0XHRcdHJldHVybiAkZ09QRChhcmd1bWVudHMsICdjYWxsZWUnKS5nZXQ7XG5cdFx0XHR9IGNhdGNoIChnT1BEdGhyb3dzKSB7XG5cdFx0XHRcdHJldHVybiB0aHJvd1R5cGVFcnJvcjtcblx0XHRcdH1cblx0XHR9XG5cdH0oKSlcblx0OiB0aHJvd1R5cGVFcnJvcjtcblxudmFyIGhhc1N5bWJvbHMgPSByZXF1aXJlKCdoYXMtc3ltYm9scycpKCk7XG5cbnZhciBnZXRQcm90byA9IE9iamVjdC5nZXRQcm90b3R5cGVPZiB8fCBmdW5jdGlvbiAoeCkgeyByZXR1cm4geC5fX3Byb3RvX187IH07IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tcHJvdG9cblxudmFyIG5lZWRzRXZhbCA9IHt9O1xuXG52YXIgVHlwZWRBcnJheSA9IHR5cGVvZiBVaW50OEFycmF5ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IGdldFByb3RvKFVpbnQ4QXJyYXkpO1xuXG52YXIgSU5UUklOU0lDUyA9IHtcblx0JyVBZ2dyZWdhdGVFcnJvciUnOiB0eXBlb2YgQWdncmVnYXRlRXJyb3IgPT09ICd1bmRlZmluZWQnID8gdW5kZWZpbmVkIDogQWdncmVnYXRlRXJyb3IsXG5cdCclQXJyYXklJzogQXJyYXksXG5cdCclQXJyYXlCdWZmZXIlJzogdHlwZW9mIEFycmF5QnVmZmVyID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IEFycmF5QnVmZmVyLFxuXHQnJUFycmF5SXRlcmF0b3JQcm90b3R5cGUlJzogaGFzU3ltYm9scyA/IGdldFByb3RvKFtdW1N5bWJvbC5pdGVyYXRvcl0oKSkgOiB1bmRlZmluZWQsXG5cdCclQXN5bmNGcm9tU3luY0l0ZXJhdG9yUHJvdG90eXBlJSc6IHVuZGVmaW5lZCxcblx0JyVBc3luY0Z1bmN0aW9uJSc6IG5lZWRzRXZhbCxcblx0JyVBc3luY0dlbmVyYXRvciUnOiBuZWVkc0V2YWwsXG5cdCclQXN5bmNHZW5lcmF0b3JGdW5jdGlvbiUnOiBuZWVkc0V2YWwsXG5cdCclQXN5bmNJdGVyYXRvclByb3RvdHlwZSUnOiBuZWVkc0V2YWwsXG5cdCclQXRvbWljcyUnOiB0eXBlb2YgQXRvbWljcyA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBBdG9taWNzLFxuXHQnJUJpZ0ludCUnOiB0eXBlb2YgQmlnSW50ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IEJpZ0ludCxcblx0JyVCb29sZWFuJSc6IEJvb2xlYW4sXG5cdCclRGF0YVZpZXclJzogdHlwZW9mIERhdGFWaWV3ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IERhdGFWaWV3LFxuXHQnJURhdGUlJzogRGF0ZSxcblx0JyVkZWNvZGVVUkklJzogZGVjb2RlVVJJLFxuXHQnJWRlY29kZVVSSUNvbXBvbmVudCUnOiBkZWNvZGVVUklDb21wb25lbnQsXG5cdCclZW5jb2RlVVJJJSc6IGVuY29kZVVSSSxcblx0JyVlbmNvZGVVUklDb21wb25lbnQlJzogZW5jb2RlVVJJQ29tcG9uZW50LFxuXHQnJUVycm9yJSc6IEVycm9yLFxuXHQnJWV2YWwlJzogZXZhbCwgLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1ldmFsXG5cdCclRXZhbEVycm9yJSc6IEV2YWxFcnJvcixcblx0JyVGbG9hdDMyQXJyYXklJzogdHlwZW9mIEZsb2F0MzJBcnJheSA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBGbG9hdDMyQXJyYXksXG5cdCclRmxvYXQ2NEFycmF5JSc6IHR5cGVvZiBGbG9hdDY0QXJyYXkgPT09ICd1bmRlZmluZWQnID8gdW5kZWZpbmVkIDogRmxvYXQ2NEFycmF5LFxuXHQnJUZpbmFsaXphdGlvblJlZ2lzdHJ5JSc6IHR5cGVvZiBGaW5hbGl6YXRpb25SZWdpc3RyeSA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBGaW5hbGl6YXRpb25SZWdpc3RyeSxcblx0JyVGdW5jdGlvbiUnOiAkRnVuY3Rpb24sXG5cdCclR2VuZXJhdG9yRnVuY3Rpb24lJzogbmVlZHNFdmFsLFxuXHQnJUludDhBcnJheSUnOiB0eXBlb2YgSW50OEFycmF5ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IEludDhBcnJheSxcblx0JyVJbnQxNkFycmF5JSc6IHR5cGVvZiBJbnQxNkFycmF5ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IEludDE2QXJyYXksXG5cdCclSW50MzJBcnJheSUnOiB0eXBlb2YgSW50MzJBcnJheSA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBJbnQzMkFycmF5LFxuXHQnJWlzRmluaXRlJSc6IGlzRmluaXRlLFxuXHQnJWlzTmFOJSc6IGlzTmFOLFxuXHQnJUl0ZXJhdG9yUHJvdG90eXBlJSc6IGhhc1N5bWJvbHMgPyBnZXRQcm90byhnZXRQcm90byhbXVtTeW1ib2wuaXRlcmF0b3JdKCkpKSA6IHVuZGVmaW5lZCxcblx0JyVKU09OJSc6IHR5cGVvZiBKU09OID09PSAnb2JqZWN0JyA/IEpTT04gOiB1bmRlZmluZWQsXG5cdCclTWFwJSc6IHR5cGVvZiBNYXAgPT09ICd1bmRlZmluZWQnID8gdW5kZWZpbmVkIDogTWFwLFxuXHQnJU1hcEl0ZXJhdG9yUHJvdG90eXBlJSc6IHR5cGVvZiBNYXAgPT09ICd1bmRlZmluZWQnIHx8ICFoYXNTeW1ib2xzID8gdW5kZWZpbmVkIDogZ2V0UHJvdG8obmV3IE1hcCgpW1N5bWJvbC5pdGVyYXRvcl0oKSksXG5cdCclTWF0aCUnOiBNYXRoLFxuXHQnJU51bWJlciUnOiBOdW1iZXIsXG5cdCclT2JqZWN0JSc6IE9iamVjdCxcblx0JyVwYXJzZUZsb2F0JSc6IHBhcnNlRmxvYXQsXG5cdCclcGFyc2VJbnQlJzogcGFyc2VJbnQsXG5cdCclUHJvbWlzZSUnOiB0eXBlb2YgUHJvbWlzZSA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBQcm9taXNlLFxuXHQnJVByb3h5JSc6IHR5cGVvZiBQcm94eSA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBQcm94eSxcblx0JyVSYW5nZUVycm9yJSc6IFJhbmdlRXJyb3IsXG5cdCclUmVmZXJlbmNlRXJyb3IlJzogUmVmZXJlbmNlRXJyb3IsXG5cdCclUmVmbGVjdCUnOiB0eXBlb2YgUmVmbGVjdCA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBSZWZsZWN0LFxuXHQnJVJlZ0V4cCUnOiBSZWdFeHAsXG5cdCclU2V0JSc6IHR5cGVvZiBTZXQgPT09ICd1bmRlZmluZWQnID8gdW5kZWZpbmVkIDogU2V0LFxuXHQnJVNldEl0ZXJhdG9yUHJvdG90eXBlJSc6IHR5cGVvZiBTZXQgPT09ICd1bmRlZmluZWQnIHx8ICFoYXNTeW1ib2xzID8gdW5kZWZpbmVkIDogZ2V0UHJvdG8obmV3IFNldCgpW1N5bWJvbC5pdGVyYXRvcl0oKSksXG5cdCclU2hhcmVkQXJyYXlCdWZmZXIlJzogdHlwZW9mIFNoYXJlZEFycmF5QnVmZmVyID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IFNoYXJlZEFycmF5QnVmZmVyLFxuXHQnJVN0cmluZyUnOiBTdHJpbmcsXG5cdCclU3RyaW5nSXRlcmF0b3JQcm90b3R5cGUlJzogaGFzU3ltYm9scyA/IGdldFByb3RvKCcnW1N5bWJvbC5pdGVyYXRvcl0oKSkgOiB1bmRlZmluZWQsXG5cdCclU3ltYm9sJSc6IGhhc1N5bWJvbHMgPyBTeW1ib2wgOiB1bmRlZmluZWQsXG5cdCclU3ludGF4RXJyb3IlJzogJFN5bnRheEVycm9yLFxuXHQnJVRocm93VHlwZUVycm9yJSc6IFRocm93VHlwZUVycm9yLFxuXHQnJVR5cGVkQXJyYXklJzogVHlwZWRBcnJheSxcblx0JyVUeXBlRXJyb3IlJzogJFR5cGVFcnJvcixcblx0JyVVaW50OEFycmF5JSc6IHR5cGVvZiBVaW50OEFycmF5ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IFVpbnQ4QXJyYXksXG5cdCclVWludDhDbGFtcGVkQXJyYXklJzogdHlwZW9mIFVpbnQ4Q2xhbXBlZEFycmF5ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IFVpbnQ4Q2xhbXBlZEFycmF5LFxuXHQnJVVpbnQxNkFycmF5JSc6IHR5cGVvZiBVaW50MTZBcnJheSA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBVaW50MTZBcnJheSxcblx0JyVVaW50MzJBcnJheSUnOiB0eXBlb2YgVWludDMyQXJyYXkgPT09ICd1bmRlZmluZWQnID8gdW5kZWZpbmVkIDogVWludDMyQXJyYXksXG5cdCclVVJJRXJyb3IlJzogVVJJRXJyb3IsXG5cdCclV2Vha01hcCUnOiB0eXBlb2YgV2Vha01hcCA9PT0gJ3VuZGVmaW5lZCcgPyB1bmRlZmluZWQgOiBXZWFrTWFwLFxuXHQnJVdlYWtSZWYlJzogdHlwZW9mIFdlYWtSZWYgPT09ICd1bmRlZmluZWQnID8gdW5kZWZpbmVkIDogV2Vha1JlZixcblx0JyVXZWFrU2V0JSc6IHR5cGVvZiBXZWFrU2V0ID09PSAndW5kZWZpbmVkJyA/IHVuZGVmaW5lZCA6IFdlYWtTZXRcbn07XG5cbnZhciBkb0V2YWwgPSBmdW5jdGlvbiBkb0V2YWwobmFtZSkge1xuXHR2YXIgdmFsdWU7XG5cdGlmIChuYW1lID09PSAnJUFzeW5jRnVuY3Rpb24lJykge1xuXHRcdHZhbHVlID0gZ2V0RXZhbGxlZENvbnN0cnVjdG9yKCdhc3luYyBmdW5jdGlvbiAoKSB7fScpO1xuXHR9IGVsc2UgaWYgKG5hbWUgPT09ICclR2VuZXJhdG9yRnVuY3Rpb24lJykge1xuXHRcdHZhbHVlID0gZ2V0RXZhbGxlZENvbnN0cnVjdG9yKCdmdW5jdGlvbiogKCkge30nKTtcblx0fSBlbHNlIGlmIChuYW1lID09PSAnJUFzeW5jR2VuZXJhdG9yRnVuY3Rpb24lJykge1xuXHRcdHZhbHVlID0gZ2V0RXZhbGxlZENvbnN0cnVjdG9yKCdhc3luYyBmdW5jdGlvbiogKCkge30nKTtcblx0fSBlbHNlIGlmIChuYW1lID09PSAnJUFzeW5jR2VuZXJhdG9yJScpIHtcblx0XHR2YXIgZm4gPSBkb0V2YWwoJyVBc3luY0dlbmVyYXRvckZ1bmN0aW9uJScpO1xuXHRcdGlmIChmbikge1xuXHRcdFx0dmFsdWUgPSBmbi5wcm90b3R5cGU7XG5cdFx0fVxuXHR9IGVsc2UgaWYgKG5hbWUgPT09ICclQXN5bmNJdGVyYXRvclByb3RvdHlwZSUnKSB7XG5cdFx0dmFyIGdlbiA9IGRvRXZhbCgnJUFzeW5jR2VuZXJhdG9yJScpO1xuXHRcdGlmIChnZW4pIHtcblx0XHRcdHZhbHVlID0gZ2V0UHJvdG8oZ2VuLnByb3RvdHlwZSk7XG5cdFx0fVxuXHR9XG5cblx0SU5UUklOU0lDU1tuYW1lXSA9IHZhbHVlO1xuXG5cdHJldHVybiB2YWx1ZTtcbn07XG5cbnZhciBMRUdBQ1lfQUxJQVNFUyA9IHtcblx0JyVBcnJheUJ1ZmZlclByb3RvdHlwZSUnOiBbJ0FycmF5QnVmZmVyJywgJ3Byb3RvdHlwZSddLFxuXHQnJUFycmF5UHJvdG90eXBlJSc6IFsnQXJyYXknLCAncHJvdG90eXBlJ10sXG5cdCclQXJyYXlQcm90b19lbnRyaWVzJSc6IFsnQXJyYXknLCAncHJvdG90eXBlJywgJ2VudHJpZXMnXSxcblx0JyVBcnJheVByb3RvX2ZvckVhY2glJzogWydBcnJheScsICdwcm90b3R5cGUnLCAnZm9yRWFjaCddLFxuXHQnJUFycmF5UHJvdG9fa2V5cyUnOiBbJ0FycmF5JywgJ3Byb3RvdHlwZScsICdrZXlzJ10sXG5cdCclQXJyYXlQcm90b192YWx1ZXMlJzogWydBcnJheScsICdwcm90b3R5cGUnLCAndmFsdWVzJ10sXG5cdCclQXN5bmNGdW5jdGlvblByb3RvdHlwZSUnOiBbJ0FzeW5jRnVuY3Rpb24nLCAncHJvdG90eXBlJ10sXG5cdCclQXN5bmNHZW5lcmF0b3IlJzogWydBc3luY0dlbmVyYXRvckZ1bmN0aW9uJywgJ3Byb3RvdHlwZSddLFxuXHQnJUFzeW5jR2VuZXJhdG9yUHJvdG90eXBlJSc6IFsnQXN5bmNHZW5lcmF0b3JGdW5jdGlvbicsICdwcm90b3R5cGUnLCAncHJvdG90eXBlJ10sXG5cdCclQm9vbGVhblByb3RvdHlwZSUnOiBbJ0Jvb2xlYW4nLCAncHJvdG90eXBlJ10sXG5cdCclRGF0YVZpZXdQcm90b3R5cGUlJzogWydEYXRhVmlldycsICdwcm90b3R5cGUnXSxcblx0JyVEYXRlUHJvdG90eXBlJSc6IFsnRGF0ZScsICdwcm90b3R5cGUnXSxcblx0JyVFcnJvclByb3RvdHlwZSUnOiBbJ0Vycm9yJywgJ3Byb3RvdHlwZSddLFxuXHQnJUV2YWxFcnJvclByb3RvdHlwZSUnOiBbJ0V2YWxFcnJvcicsICdwcm90b3R5cGUnXSxcblx0JyVGbG9hdDMyQXJyYXlQcm90b3R5cGUlJzogWydGbG9hdDMyQXJyYXknLCAncHJvdG90eXBlJ10sXG5cdCclRmxvYXQ2NEFycmF5UHJvdG90eXBlJSc6IFsnRmxvYXQ2NEFycmF5JywgJ3Byb3RvdHlwZSddLFxuXHQnJUZ1bmN0aW9uUHJvdG90eXBlJSc6IFsnRnVuY3Rpb24nLCAncHJvdG90eXBlJ10sXG5cdCclR2VuZXJhdG9yJSc6IFsnR2VuZXJhdG9yRnVuY3Rpb24nLCAncHJvdG90eXBlJ10sXG5cdCclR2VuZXJhdG9yUHJvdG90eXBlJSc6IFsnR2VuZXJhdG9yRnVuY3Rpb24nLCAncHJvdG90eXBlJywgJ3Byb3RvdHlwZSddLFxuXHQnJUludDhBcnJheVByb3RvdHlwZSUnOiBbJ0ludDhBcnJheScsICdwcm90b3R5cGUnXSxcblx0JyVJbnQxNkFycmF5UHJvdG90eXBlJSc6IFsnSW50MTZBcnJheScsICdwcm90b3R5cGUnXSxcblx0JyVJbnQzMkFycmF5UHJvdG90eXBlJSc6IFsnSW50MzJBcnJheScsICdwcm90b3R5cGUnXSxcblx0JyVKU09OUGFyc2UlJzogWydKU09OJywgJ3BhcnNlJ10sXG5cdCclSlNPTlN0cmluZ2lmeSUnOiBbJ0pTT04nLCAnc3RyaW5naWZ5J10sXG5cdCclTWFwUHJvdG90eXBlJSc6IFsnTWFwJywgJ3Byb3RvdHlwZSddLFxuXHQnJU51bWJlclByb3RvdHlwZSUnOiBbJ051bWJlcicsICdwcm90b3R5cGUnXSxcblx0JyVPYmplY3RQcm90b3R5cGUlJzogWydPYmplY3QnLCAncHJvdG90eXBlJ10sXG5cdCclT2JqUHJvdG9fdG9TdHJpbmclJzogWydPYmplY3QnLCAncHJvdG90eXBlJywgJ3RvU3RyaW5nJ10sXG5cdCclT2JqUHJvdG9fdmFsdWVPZiUnOiBbJ09iamVjdCcsICdwcm90b3R5cGUnLCAndmFsdWVPZiddLFxuXHQnJVByb21pc2VQcm90b3R5cGUlJzogWydQcm9taXNlJywgJ3Byb3RvdHlwZSddLFxuXHQnJVByb21pc2VQcm90b190aGVuJSc6IFsnUHJvbWlzZScsICdwcm90b3R5cGUnLCAndGhlbiddLFxuXHQnJVByb21pc2VfYWxsJSc6IFsnUHJvbWlzZScsICdhbGwnXSxcblx0JyVQcm9taXNlX3JlamVjdCUnOiBbJ1Byb21pc2UnLCAncmVqZWN0J10sXG5cdCclUHJvbWlzZV9yZXNvbHZlJSc6IFsnUHJvbWlzZScsICdyZXNvbHZlJ10sXG5cdCclUmFuZ2VFcnJvclByb3RvdHlwZSUnOiBbJ1JhbmdlRXJyb3InLCAncHJvdG90eXBlJ10sXG5cdCclUmVmZXJlbmNlRXJyb3JQcm90b3R5cGUlJzogWydSZWZlcmVuY2VFcnJvcicsICdwcm90b3R5cGUnXSxcblx0JyVSZWdFeHBQcm90b3R5cGUlJzogWydSZWdFeHAnLCAncHJvdG90eXBlJ10sXG5cdCclU2V0UHJvdG90eXBlJSc6IFsnU2V0JywgJ3Byb3RvdHlwZSddLFxuXHQnJVNoYXJlZEFycmF5QnVmZmVyUHJvdG90eXBlJSc6IFsnU2hhcmVkQXJyYXlCdWZmZXInLCAncHJvdG90eXBlJ10sXG5cdCclU3RyaW5nUHJvdG90eXBlJSc6IFsnU3RyaW5nJywgJ3Byb3RvdHlwZSddLFxuXHQnJVN5bWJvbFByb3RvdHlwZSUnOiBbJ1N5bWJvbCcsICdwcm90b3R5cGUnXSxcblx0JyVTeW50YXhFcnJvclByb3RvdHlwZSUnOiBbJ1N5bnRheEVycm9yJywgJ3Byb3RvdHlwZSddLFxuXHQnJVR5cGVkQXJyYXlQcm90b3R5cGUlJzogWydUeXBlZEFycmF5JywgJ3Byb3RvdHlwZSddLFxuXHQnJVR5cGVFcnJvclByb3RvdHlwZSUnOiBbJ1R5cGVFcnJvcicsICdwcm90b3R5cGUnXSxcblx0JyVVaW50OEFycmF5UHJvdG90eXBlJSc6IFsnVWludDhBcnJheScsICdwcm90b3R5cGUnXSxcblx0JyVVaW50OENsYW1wZWRBcnJheVByb3RvdHlwZSUnOiBbJ1VpbnQ4Q2xhbXBlZEFycmF5JywgJ3Byb3RvdHlwZSddLFxuXHQnJVVpbnQxNkFycmF5UHJvdG90eXBlJSc6IFsnVWludDE2QXJyYXknLCAncHJvdG90eXBlJ10sXG5cdCclVWludDMyQXJyYXlQcm90b3R5cGUlJzogWydVaW50MzJBcnJheScsICdwcm90b3R5cGUnXSxcblx0JyVVUklFcnJvclByb3RvdHlwZSUnOiBbJ1VSSUVycm9yJywgJ3Byb3RvdHlwZSddLFxuXHQnJVdlYWtNYXBQcm90b3R5cGUlJzogWydXZWFrTWFwJywgJ3Byb3RvdHlwZSddLFxuXHQnJVdlYWtTZXRQcm90b3R5cGUlJzogWydXZWFrU2V0JywgJ3Byb3RvdHlwZSddXG59O1xuXG52YXIgYmluZCA9IHJlcXVpcmUoJ2Z1bmN0aW9uLWJpbmQnKTtcbnZhciBoYXNPd24gPSByZXF1aXJlKCdoYXMnKTtcbnZhciAkY29uY2F0ID0gYmluZC5jYWxsKEZ1bmN0aW9uLmNhbGwsIEFycmF5LnByb3RvdHlwZS5jb25jYXQpO1xudmFyICRzcGxpY2VBcHBseSA9IGJpbmQuY2FsbChGdW5jdGlvbi5hcHBseSwgQXJyYXkucHJvdG90eXBlLnNwbGljZSk7XG52YXIgJHJlcGxhY2UgPSBiaW5kLmNhbGwoRnVuY3Rpb24uY2FsbCwgU3RyaW5nLnByb3RvdHlwZS5yZXBsYWNlKTtcbnZhciAkc3RyU2xpY2UgPSBiaW5kLmNhbGwoRnVuY3Rpb24uY2FsbCwgU3RyaW5nLnByb3RvdHlwZS5zbGljZSk7XG5cbi8qIGFkYXB0ZWQgZnJvbSBodHRwczovL2dpdGh1Yi5jb20vbG9kYXNoL2xvZGFzaC9ibG9iLzQuMTcuMTUvZGlzdC9sb2Rhc2guanMjTDY3MzUtTDY3NDQgKi9cbnZhciByZVByb3BOYW1lID0gL1teJS5bXFxdXSt8XFxbKD86KC0/XFxkKyg/OlxcLlxcZCspPyl8KFtcIiddKSgoPzooPyFcXDIpW15cXFxcXXxcXFxcLikqPylcXDIpXFxdfCg/PSg/OlxcLnxcXFtcXF0pKD86XFwufFxcW1xcXXwlJCkpL2c7XG52YXIgcmVFc2NhcGVDaGFyID0gL1xcXFwoXFxcXCk/L2c7IC8qKiBVc2VkIHRvIG1hdGNoIGJhY2tzbGFzaGVzIGluIHByb3BlcnR5IHBhdGhzLiAqL1xudmFyIHN0cmluZ1RvUGF0aCA9IGZ1bmN0aW9uIHN0cmluZ1RvUGF0aChzdHJpbmcpIHtcblx0dmFyIGZpcnN0ID0gJHN0clNsaWNlKHN0cmluZywgMCwgMSk7XG5cdHZhciBsYXN0ID0gJHN0clNsaWNlKHN0cmluZywgLTEpO1xuXHRpZiAoZmlyc3QgPT09ICclJyAmJiBsYXN0ICE9PSAnJScpIHtcblx0XHR0aHJvdyBuZXcgJFN5bnRheEVycm9yKCdpbnZhbGlkIGludHJpbnNpYyBzeW50YXgsIGV4cGVjdGVkIGNsb3NpbmcgYCVgJyk7XG5cdH0gZWxzZSBpZiAobGFzdCA9PT0gJyUnICYmIGZpcnN0ICE9PSAnJScpIHtcblx0XHR0aHJvdyBuZXcgJFN5bnRheEVycm9yKCdpbnZhbGlkIGludHJpbnNpYyBzeW50YXgsIGV4cGVjdGVkIG9wZW5pbmcgYCVgJyk7XG5cdH1cblx0dmFyIHJlc3VsdCA9IFtdO1xuXHQkcmVwbGFjZShzdHJpbmcsIHJlUHJvcE5hbWUsIGZ1bmN0aW9uIChtYXRjaCwgbnVtYmVyLCBxdW90ZSwgc3ViU3RyaW5nKSB7XG5cdFx0cmVzdWx0W3Jlc3VsdC5sZW5ndGhdID0gcXVvdGUgPyAkcmVwbGFjZShzdWJTdHJpbmcsIHJlRXNjYXBlQ2hhciwgJyQxJykgOiBudW1iZXIgfHwgbWF0Y2g7XG5cdH0pO1xuXHRyZXR1cm4gcmVzdWx0O1xufTtcbi8qIGVuZCBhZGFwdGF0aW9uICovXG5cbnZhciBnZXRCYXNlSW50cmluc2ljID0gZnVuY3Rpb24gZ2V0QmFzZUludHJpbnNpYyhuYW1lLCBhbGxvd01pc3NpbmcpIHtcblx0dmFyIGludHJpbnNpY05hbWUgPSBuYW1lO1xuXHR2YXIgYWxpYXM7XG5cdGlmIChoYXNPd24oTEVHQUNZX0FMSUFTRVMsIGludHJpbnNpY05hbWUpKSB7XG5cdFx0YWxpYXMgPSBMRUdBQ1lfQUxJQVNFU1tpbnRyaW5zaWNOYW1lXTtcblx0XHRpbnRyaW5zaWNOYW1lID0gJyUnICsgYWxpYXNbMF0gKyAnJSc7XG5cdH1cblxuXHRpZiAoaGFzT3duKElOVFJJTlNJQ1MsIGludHJpbnNpY05hbWUpKSB7XG5cdFx0dmFyIHZhbHVlID0gSU5UUklOU0lDU1tpbnRyaW5zaWNOYW1lXTtcblx0XHRpZiAodmFsdWUgPT09IG5lZWRzRXZhbCkge1xuXHRcdFx0dmFsdWUgPSBkb0V2YWwoaW50cmluc2ljTmFtZSk7XG5cdFx0fVxuXHRcdGlmICh0eXBlb2YgdmFsdWUgPT09ICd1bmRlZmluZWQnICYmICFhbGxvd01pc3NpbmcpIHtcblx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdpbnRyaW5zaWMgJyArIG5hbWUgKyAnIGV4aXN0cywgYnV0IGlzIG5vdCBhdmFpbGFibGUuIFBsZWFzZSBmaWxlIGFuIGlzc3VlIScpO1xuXHRcdH1cblxuXHRcdHJldHVybiB7XG5cdFx0XHRhbGlhczogYWxpYXMsXG5cdFx0XHRuYW1lOiBpbnRyaW5zaWNOYW1lLFxuXHRcdFx0dmFsdWU6IHZhbHVlXG5cdFx0fTtcblx0fVxuXG5cdHRocm93IG5ldyAkU3ludGF4RXJyb3IoJ2ludHJpbnNpYyAnICsgbmFtZSArICcgZG9lcyBub3QgZXhpc3QhJyk7XG59O1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIEdldEludHJpbnNpYyhuYW1lLCBhbGxvd01pc3NpbmcpIHtcblx0aWYgKHR5cGVvZiBuYW1lICE9PSAnc3RyaW5nJyB8fCBuYW1lLmxlbmd0aCA9PT0gMCkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdpbnRyaW5zaWMgbmFtZSBtdXN0IGJlIGEgbm9uLWVtcHR5IHN0cmluZycpO1xuXHR9XG5cdGlmIChhcmd1bWVudHMubGVuZ3RoID4gMSAmJiB0eXBlb2YgYWxsb3dNaXNzaW5nICE9PSAnYm9vbGVhbicpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignXCJhbGxvd01pc3NpbmdcIiBhcmd1bWVudCBtdXN0IGJlIGEgYm9vbGVhbicpO1xuXHR9XG5cblx0dmFyIHBhcnRzID0gc3RyaW5nVG9QYXRoKG5hbWUpO1xuXHR2YXIgaW50cmluc2ljQmFzZU5hbWUgPSBwYXJ0cy5sZW5ndGggPiAwID8gcGFydHNbMF0gOiAnJztcblxuXHR2YXIgaW50cmluc2ljID0gZ2V0QmFzZUludHJpbnNpYygnJScgKyBpbnRyaW5zaWNCYXNlTmFtZSArICclJywgYWxsb3dNaXNzaW5nKTtcblx0dmFyIGludHJpbnNpY1JlYWxOYW1lID0gaW50cmluc2ljLm5hbWU7XG5cdHZhciB2YWx1ZSA9IGludHJpbnNpYy52YWx1ZTtcblx0dmFyIHNraXBGdXJ0aGVyQ2FjaGluZyA9IGZhbHNlO1xuXG5cdHZhciBhbGlhcyA9IGludHJpbnNpYy5hbGlhcztcblx0aWYgKGFsaWFzKSB7XG5cdFx0aW50cmluc2ljQmFzZU5hbWUgPSBhbGlhc1swXTtcblx0XHQkc3BsaWNlQXBwbHkocGFydHMsICRjb25jYXQoWzAsIDFdLCBhbGlhcykpO1xuXHR9XG5cblx0Zm9yICh2YXIgaSA9IDEsIGlzT3duID0gdHJ1ZTsgaSA8IHBhcnRzLmxlbmd0aDsgaSArPSAxKSB7XG5cdFx0dmFyIHBhcnQgPSBwYXJ0c1tpXTtcblx0XHR2YXIgZmlyc3QgPSAkc3RyU2xpY2UocGFydCwgMCwgMSk7XG5cdFx0dmFyIGxhc3QgPSAkc3RyU2xpY2UocGFydCwgLTEpO1xuXHRcdGlmIChcblx0XHRcdChcblx0XHRcdFx0KGZpcnN0ID09PSAnXCInIHx8IGZpcnN0ID09PSBcIidcIiB8fCBmaXJzdCA9PT0gJ2AnKVxuXHRcdFx0XHR8fCAobGFzdCA9PT0gJ1wiJyB8fCBsYXN0ID09PSBcIidcIiB8fCBsYXN0ID09PSAnYCcpXG5cdFx0XHQpXG5cdFx0XHQmJiBmaXJzdCAhPT0gbGFzdFxuXHRcdCkge1xuXHRcdFx0dGhyb3cgbmV3ICRTeW50YXhFcnJvcigncHJvcGVydHkgbmFtZXMgd2l0aCBxdW90ZXMgbXVzdCBoYXZlIG1hdGNoaW5nIHF1b3RlcycpO1xuXHRcdH1cblx0XHRpZiAocGFydCA9PT0gJ2NvbnN0cnVjdG9yJyB8fCAhaXNPd24pIHtcblx0XHRcdHNraXBGdXJ0aGVyQ2FjaGluZyA9IHRydWU7XG5cdFx0fVxuXG5cdFx0aW50cmluc2ljQmFzZU5hbWUgKz0gJy4nICsgcGFydDtcblx0XHRpbnRyaW5zaWNSZWFsTmFtZSA9ICclJyArIGludHJpbnNpY0Jhc2VOYW1lICsgJyUnO1xuXG5cdFx0aWYgKGhhc093bihJTlRSSU5TSUNTLCBpbnRyaW5zaWNSZWFsTmFtZSkpIHtcblx0XHRcdHZhbHVlID0gSU5UUklOU0lDU1tpbnRyaW5zaWNSZWFsTmFtZV07XG5cdFx0fSBlbHNlIGlmICh2YWx1ZSAhPSBudWxsKSB7XG5cdFx0XHRpZiAoIShwYXJ0IGluIHZhbHVlKSkge1xuXHRcdFx0XHRpZiAoIWFsbG93TWlzc2luZykge1xuXHRcdFx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdiYXNlIGludHJpbnNpYyBmb3IgJyArIG5hbWUgKyAnIGV4aXN0cywgYnV0IHRoZSBwcm9wZXJ0eSBpcyBub3QgYXZhaWxhYmxlLicpO1xuXHRcdFx0XHR9XG5cdFx0XHRcdHJldHVybiB2b2lkIHVuZGVmaW5lZDtcblx0XHRcdH1cblx0XHRcdGlmICgkZ09QRCAmJiAoaSArIDEpID49IHBhcnRzLmxlbmd0aCkge1xuXHRcdFx0XHR2YXIgZGVzYyA9ICRnT1BEKHZhbHVlLCBwYXJ0KTtcblx0XHRcdFx0aXNPd24gPSAhIWRlc2M7XG5cblx0XHRcdFx0Ly8gQnkgY29udmVudGlvbiwgd2hlbiBhIGRhdGEgcHJvcGVydHkgaXMgY29udmVydGVkIHRvIGFuIGFjY2Vzc29yXG5cdFx0XHRcdC8vIHByb3BlcnR5IHRvIGVtdWxhdGUgYSBkYXRhIHByb3BlcnR5IHRoYXQgZG9lcyBub3Qgc3VmZmVyIGZyb21cblx0XHRcdFx0Ly8gdGhlIG92ZXJyaWRlIG1pc3Rha2UsIHRoYXQgYWNjZXNzb3IncyBnZXR0ZXIgaXMgbWFya2VkIHdpdGhcblx0XHRcdFx0Ly8gYW4gYG9yaWdpbmFsVmFsdWVgIHByb3BlcnR5LiBIZXJlLCB3aGVuIHdlIGRldGVjdCB0aGlzLCB3ZVxuXHRcdFx0XHQvLyB1cGhvbGQgdGhlIGlsbHVzaW9uIGJ5IHByZXRlbmRpbmcgdG8gc2VlIHRoYXQgb3JpZ2luYWwgZGF0YVxuXHRcdFx0XHQvLyBwcm9wZXJ0eSwgaS5lLiwgcmV0dXJuaW5nIHRoZSB2YWx1ZSByYXRoZXIgdGhhbiB0aGUgZ2V0dGVyXG5cdFx0XHRcdC8vIGl0c2VsZi5cblx0XHRcdFx0aWYgKGlzT3duICYmICdnZXQnIGluIGRlc2MgJiYgISgnb3JpZ2luYWxWYWx1ZScgaW4gZGVzYy5nZXQpKSB7XG5cdFx0XHRcdFx0dmFsdWUgPSBkZXNjLmdldDtcblx0XHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0XHR2YWx1ZSA9IHZhbHVlW3BhcnRdO1xuXHRcdFx0XHR9XG5cdFx0XHR9IGVsc2Uge1xuXHRcdFx0XHRpc093biA9IGhhc093bih2YWx1ZSwgcGFydCk7XG5cdFx0XHRcdHZhbHVlID0gdmFsdWVbcGFydF07XG5cdFx0XHR9XG5cblx0XHRcdGlmIChpc093biAmJiAhc2tpcEZ1cnRoZXJDYWNoaW5nKSB7XG5cdFx0XHRcdElOVFJJTlNJQ1NbaW50cmluc2ljUmVhbE5hbWVdID0gdmFsdWU7XG5cdFx0XHR9XG5cdFx0fVxuXHR9XG5cdHJldHVybiB2YWx1ZTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///210\n")},1405:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar origSymbol = typeof Symbol !== 'undefined' && Symbol;\nvar hasSymbolSham = __webpack_require__(5419);\n\nmodule.exports = function hasNativeSymbols() {\n\tif (typeof origSymbol !== 'function') { return false; }\n\tif (typeof Symbol !== 'function') { return false; }\n\tif (typeof origSymbol('foo') !== 'symbol') { return false; }\n\tif (typeof Symbol('bar') !== 'symbol') { return false; }\n\n\treturn hasSymbolSham();\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTQwNS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBLG9CQUFvQixtQkFBTyxDQUFDLElBQVM7O0FBRXJDO0FBQ0EseUNBQXlDO0FBQ3pDLHFDQUFxQztBQUNyQyw4Q0FBOEM7QUFDOUMsMENBQTBDOztBQUUxQztBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9oYXMtc3ltYm9scy9pbmRleC5qcz81MTU2Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIG9yaWdTeW1ib2wgPSB0eXBlb2YgU3ltYm9sICE9PSAndW5kZWZpbmVkJyAmJiBTeW1ib2w7XG52YXIgaGFzU3ltYm9sU2hhbSA9IHJlcXVpcmUoJy4vc2hhbXMnKTtcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBoYXNOYXRpdmVTeW1ib2xzKCkge1xuXHRpZiAodHlwZW9mIG9yaWdTeW1ib2wgIT09ICdmdW5jdGlvbicpIHsgcmV0dXJuIGZhbHNlOyB9XG5cdGlmICh0eXBlb2YgU3ltYm9sICE9PSAnZnVuY3Rpb24nKSB7IHJldHVybiBmYWxzZTsgfVxuXHRpZiAodHlwZW9mIG9yaWdTeW1ib2woJ2ZvbycpICE9PSAnc3ltYm9sJykgeyByZXR1cm4gZmFsc2U7IH1cblx0aWYgKHR5cGVvZiBTeW1ib2woJ2JhcicpICE9PSAnc3ltYm9sJykgeyByZXR1cm4gZmFsc2U7IH1cblxuXHRyZXR1cm4gaGFzU3ltYm9sU2hhbSgpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///1405\n")},5419:function(module){"use strict";eval("\n\n/* eslint complexity: [2, 18], max-statements: [2, 33] */\nmodule.exports = function hasSymbols() {\n\tif (typeof Symbol !== 'function' || typeof Object.getOwnPropertySymbols !== 'function') { return false; }\n\tif (typeof Symbol.iterator === 'symbol') { return true; }\n\n\tvar obj = {};\n\tvar sym = Symbol('test');\n\tvar symObj = Object(sym);\n\tif (typeof sym === 'string') { return false; }\n\n\tif (Object.prototype.toString.call(sym) !== '[object Symbol]') { return false; }\n\tif (Object.prototype.toString.call(symObj) !== '[object Symbol]') { return false; }\n\n\t// temp disabled per https://github.com/ljharb/object.assign/issues/17\n\t// if (sym instanceof Symbol) { return false; }\n\t// temp disabled per https://github.com/WebReflection/get-own-property-symbols/issues/4\n\t// if (!(symObj instanceof Symbol)) { return false; }\n\n\t// if (typeof Symbol.prototype.toString !== 'function') { return false; }\n\t// if (String(sym) !== Symbol.prototype.toString.call(sym)) { return false; }\n\n\tvar symVal = 42;\n\tobj[sym] = symVal;\n\tfor (sym in obj) { return false; } // eslint-disable-line no-restricted-syntax, no-unreachable-loop\n\tif (typeof Object.keys === 'function' && Object.keys(obj).length !== 0) { return false; }\n\n\tif (typeof Object.getOwnPropertyNames === 'function' && Object.getOwnPropertyNames(obj).length !== 0) { return false; }\n\n\tvar syms = Object.getOwnPropertySymbols(obj);\n\tif (syms.length !== 1 || syms[0] !== sym) { return false; }\n\n\tif (!Object.prototype.propertyIsEnumerable.call(obj, sym)) { return false; }\n\n\tif (typeof Object.getOwnPropertyDescriptor === 'function') {\n\t\tvar descriptor = Object.getOwnPropertyDescriptor(obj, sym);\n\t\tif (descriptor.value !== symVal || descriptor.enumerable !== true) { return false; }\n\t}\n\n\treturn true;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTQxOS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EsMkZBQTJGO0FBQzNGLDRDQUE0Qzs7QUFFNUM7QUFDQTtBQUNBO0FBQ0EsZ0NBQWdDOztBQUVoQyxrRUFBa0U7QUFDbEUscUVBQXFFOztBQUVyRTtBQUNBLGlDQUFpQztBQUNqQztBQUNBLHVDQUF1Qzs7QUFFdkMsMkRBQTJEO0FBQzNELCtEQUErRDs7QUFFL0Q7QUFDQTtBQUNBLG9CQUFvQixnQkFBZ0I7QUFDcEMsMkVBQTJFOztBQUUzRSx5R0FBeUc7O0FBRXpHO0FBQ0EsNkNBQTZDOztBQUU3Qyw4REFBOEQ7O0FBRTlEO0FBQ0E7QUFDQSx1RUFBdUU7QUFDdkU7O0FBRUE7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvaGFzLXN5bWJvbHMvc2hhbXMuanM/MTY5NiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbi8qIGVzbGludCBjb21wbGV4aXR5OiBbMiwgMThdLCBtYXgtc3RhdGVtZW50czogWzIsIDMzXSAqL1xubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBoYXNTeW1ib2xzKCkge1xuXHRpZiAodHlwZW9mIFN5bWJvbCAhPT0gJ2Z1bmN0aW9uJyB8fCB0eXBlb2YgT2JqZWN0LmdldE93blByb3BlcnR5U3ltYm9scyAhPT0gJ2Z1bmN0aW9uJykgeyByZXR1cm4gZmFsc2U7IH1cblx0aWYgKHR5cGVvZiBTeW1ib2wuaXRlcmF0b3IgPT09ICdzeW1ib2wnKSB7IHJldHVybiB0cnVlOyB9XG5cblx0dmFyIG9iaiA9IHt9O1xuXHR2YXIgc3ltID0gU3ltYm9sKCd0ZXN0Jyk7XG5cdHZhciBzeW1PYmogPSBPYmplY3Qoc3ltKTtcblx0aWYgKHR5cGVvZiBzeW0gPT09ICdzdHJpbmcnKSB7IHJldHVybiBmYWxzZTsgfVxuXG5cdGlmIChPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nLmNhbGwoc3ltKSAhPT0gJ1tvYmplY3QgU3ltYm9sXScpIHsgcmV0dXJuIGZhbHNlOyB9XG5cdGlmIChPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nLmNhbGwoc3ltT2JqKSAhPT0gJ1tvYmplY3QgU3ltYm9sXScpIHsgcmV0dXJuIGZhbHNlOyB9XG5cblx0Ly8gdGVtcCBkaXNhYmxlZCBwZXIgaHR0cHM6Ly9naXRodWIuY29tL2xqaGFyYi9vYmplY3QuYXNzaWduL2lzc3Vlcy8xN1xuXHQvLyBpZiAoc3ltIGluc3RhbmNlb2YgU3ltYm9sKSB7IHJldHVybiBmYWxzZTsgfVxuXHQvLyB0ZW1wIGRpc2FibGVkIHBlciBodHRwczovL2dpdGh1Yi5jb20vV2ViUmVmbGVjdGlvbi9nZXQtb3duLXByb3BlcnR5LXN5bWJvbHMvaXNzdWVzLzRcblx0Ly8gaWYgKCEoc3ltT2JqIGluc3RhbmNlb2YgU3ltYm9sKSkgeyByZXR1cm4gZmFsc2U7IH1cblxuXHQvLyBpZiAodHlwZW9mIFN5bWJvbC5wcm90b3R5cGUudG9TdHJpbmcgIT09ICdmdW5jdGlvbicpIHsgcmV0dXJuIGZhbHNlOyB9XG5cdC8vIGlmIChTdHJpbmcoc3ltKSAhPT0gU3ltYm9sLnByb3RvdHlwZS50b1N0cmluZy5jYWxsKHN5bSkpIHsgcmV0dXJuIGZhbHNlOyB9XG5cblx0dmFyIHN5bVZhbCA9IDQyO1xuXHRvYmpbc3ltXSA9IHN5bVZhbDtcblx0Zm9yIChzeW0gaW4gb2JqKSB7IHJldHVybiBmYWxzZTsgfSAvLyBlc2xpbnQtZGlzYWJsZS1saW5lIG5vLXJlc3RyaWN0ZWQtc3ludGF4LCBuby11bnJlYWNoYWJsZS1sb29wXG5cdGlmICh0eXBlb2YgT2JqZWN0LmtleXMgPT09ICdmdW5jdGlvbicgJiYgT2JqZWN0LmtleXMob2JqKS5sZW5ndGggIT09IDApIHsgcmV0dXJuIGZhbHNlOyB9XG5cblx0aWYgKHR5cGVvZiBPYmplY3QuZ2V0T3duUHJvcGVydHlOYW1lcyA9PT0gJ2Z1bmN0aW9uJyAmJiBPYmplY3QuZ2V0T3duUHJvcGVydHlOYW1lcyhvYmopLmxlbmd0aCAhPT0gMCkgeyByZXR1cm4gZmFsc2U7IH1cblxuXHR2YXIgc3ltcyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eVN5bWJvbHMob2JqKTtcblx0aWYgKHN5bXMubGVuZ3RoICE9PSAxIHx8IHN5bXNbMF0gIT09IHN5bSkgeyByZXR1cm4gZmFsc2U7IH1cblxuXHRpZiAoIU9iamVjdC5wcm90b3R5cGUucHJvcGVydHlJc0VudW1lcmFibGUuY2FsbChvYmosIHN5bSkpIHsgcmV0dXJuIGZhbHNlOyB9XG5cblx0aWYgKHR5cGVvZiBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yID09PSAnZnVuY3Rpb24nKSB7XG5cdFx0dmFyIGRlc2NyaXB0b3IgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yKG9iaiwgc3ltKTtcblx0XHRpZiAoZGVzY3JpcHRvci52YWx1ZSAhPT0gc3ltVmFsIHx8IGRlc2NyaXB0b3IuZW51bWVyYWJsZSAhPT0gdHJ1ZSkgeyByZXR1cm4gZmFsc2U7IH1cblx0fVxuXG5cdHJldHVybiB0cnVlO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///5419\n")},6410:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar hasSymbols = __webpack_require__(5419);\n\nmodule.exports = function hasToStringTagShams() {\n\treturn hasSymbols() && !!Symbol.toStringTag;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNjQxMC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixpQkFBaUIsbUJBQU8sQ0FBQyxJQUFtQjs7QUFFNUM7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9oYXMtdG9zdHJpbmd0YWcvc2hhbXMuanM/MDdhNCJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBoYXNTeW1ib2xzID0gcmVxdWlyZSgnaGFzLXN5bWJvbHMvc2hhbXMnKTtcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBoYXNUb1N0cmluZ1RhZ1NoYW1zKCkge1xuXHRyZXR1cm4gaGFzU3ltYm9scygpICYmICEhU3ltYm9sLnRvU3RyaW5nVGFnO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///6410\n")},7642:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar bind = __webpack_require__(8612);\n\nmodule.exports = bind.call(Function.call, Object.prototype.hasOwnProperty);\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzY0Mi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixXQUFXLG1CQUFPLENBQUMsSUFBZTs7QUFFbEMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhcy9zcmMvaW5kZXguanM/YTBkMyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBiaW5kID0gcmVxdWlyZSgnZnVuY3Rpb24tYmluZCcpO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGJpbmQuY2FsbChGdW5jdGlvbi5jYWxsLCBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5KTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///7642\n")},3715:function(__unused_webpack_module,exports,__webpack_require__){eval("var hash = exports;\n\nhash.utils = __webpack_require__(6436);\nhash.common = __webpack_require__(5772);\nhash.sha = __webpack_require__(9041);\nhash.ripemd = __webpack_require__(2949);\nhash.hmac = __webpack_require__(2344);\n\n// Proxy hash functions to the main object\nhash.sha1 = hash.sha.sha1;\nhash.sha256 = hash.sha.sha256;\nhash.sha224 = hash.sha.sha224;\nhash.sha384 = hash.sha.sha384;\nhash.sha512 = hash.sha.sha512;\nhash.ripemd160 = hash.ripemd.ripemd160;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzcxNS5qcyIsIm1hcHBpbmdzIjoiQUFBQTs7QUFFQSxhQUFhLG1CQUFPLENBQUMsSUFBYztBQUNuQyxjQUFjLG1CQUFPLENBQUMsSUFBZTtBQUNyQyxXQUFXLG1CQUFPLENBQUMsSUFBWTtBQUMvQixjQUFjLG1CQUFPLENBQUMsSUFBZTtBQUNyQyxZQUFZLG1CQUFPLENBQUMsSUFBYTs7QUFFakM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2guanM/N2Q5MiJdLCJzb3VyY2VzQ29udGVudCI6WyJ2YXIgaGFzaCA9IGV4cG9ydHM7XG5cbmhhc2gudXRpbHMgPSByZXF1aXJlKCcuL2hhc2gvdXRpbHMnKTtcbmhhc2guY29tbW9uID0gcmVxdWlyZSgnLi9oYXNoL2NvbW1vbicpO1xuaGFzaC5zaGEgPSByZXF1aXJlKCcuL2hhc2gvc2hhJyk7XG5oYXNoLnJpcGVtZCA9IHJlcXVpcmUoJy4vaGFzaC9yaXBlbWQnKTtcbmhhc2guaG1hYyA9IHJlcXVpcmUoJy4vaGFzaC9obWFjJyk7XG5cbi8vIFByb3h5IGhhc2ggZnVuY3Rpb25zIHRvIHRoZSBtYWluIG9iamVjdFxuaGFzaC5zaGExID0gaGFzaC5zaGEuc2hhMTtcbmhhc2guc2hhMjU2ID0gaGFzaC5zaGEuc2hhMjU2O1xuaGFzaC5zaGEyMjQgPSBoYXNoLnNoYS5zaGEyMjQ7XG5oYXNoLnNoYTM4NCA9IGhhc2guc2hhLnNoYTM4NDtcbmhhc2guc2hhNTEyID0gaGFzaC5zaGEuc2hhNTEyO1xuaGFzaC5yaXBlbWQxNjAgPSBoYXNoLnJpcGVtZC5yaXBlbWQxNjA7XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3715\n")},5772:function(__unused_webpack_module,exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar assert = __webpack_require__(9746);\n\nfunction BlockHash() {\n this.pending = null;\n this.pendingTotal = 0;\n this.blockSize = this.constructor.blockSize;\n this.outSize = this.constructor.outSize;\n this.hmacStrength = this.constructor.hmacStrength;\n this.padLength = this.constructor.padLength / 8;\n this.endian = 'big';\n\n this._delta8 = this.blockSize / 8;\n this._delta32 = this.blockSize / 32;\n}\nexports.BlockHash = BlockHash;\n\nBlockHash.prototype.update = function update(msg, enc) {\n // Convert message to array, pad it, and join into 32bit blocks\n msg = utils.toArray(msg, enc);\n if (!this.pending)\n this.pending = msg;\n else\n this.pending = this.pending.concat(msg);\n this.pendingTotal += msg.length;\n\n // Enough data, try updating\n if (this.pending.length >= this._delta8) {\n msg = this.pending;\n\n // Process pending data in blocks\n var r = msg.length % this._delta8;\n this.pending = msg.slice(msg.length - r, msg.length);\n if (this.pending.length === 0)\n this.pending = null;\n\n msg = utils.join32(msg, 0, msg.length - r, this.endian);\n for (var i = 0; i < msg.length; i += this._delta32)\n this._update(msg, i, i + this._delta32);\n }\n\n return this;\n};\n\nBlockHash.prototype.digest = function digest(enc) {\n this.update(this._pad());\n assert(this.pending === null);\n\n return this._digest(enc);\n};\n\nBlockHash.prototype._pad = function pad() {\n var len = this.pendingTotal;\n var bytes = this._delta8;\n var k = bytes - ((len + this.padLength) % bytes);\n var res = new Array(k + this.padLength);\n res[0] = 0x80;\n for (var i = 1; i < k; i++)\n res[i] = 0;\n\n // Append length\n len <<= 3;\n if (this.endian === 'big') {\n for (var t = 8; t < this.padLength; t++)\n res[i++] = 0;\n\n res[i++] = 0;\n res[i++] = 0;\n res[i++] = 0;\n res[i++] = 0;\n res[i++] = (len >>> 24) & 0xff;\n res[i++] = (len >>> 16) & 0xff;\n res[i++] = (len >>> 8) & 0xff;\n res[i++] = len & 0xff;\n } else {\n res[i++] = len & 0xff;\n res[i++] = (len >>> 8) & 0xff;\n res[i++] = (len >>> 16) & 0xff;\n res[i++] = (len >>> 24) & 0xff;\n res[i++] = 0;\n res[i++] = 0;\n res[i++] = 0;\n res[i++] = 0;\n\n for (t = 8; t < this.padLength; t++)\n res[i++] = 0;\n }\n\n return res;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTc3Mi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBUztBQUM3QixhQUFhLG1CQUFPLENBQUMsSUFBcUI7O0FBRTFDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0EsaUJBQWlCOztBQUVqQjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0Esb0JBQW9CLGdCQUFnQjtBQUNwQztBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esa0JBQWtCLE9BQU87QUFDekI7O0FBRUE7QUFDQTtBQUNBO0FBQ0Esb0JBQW9CLG9CQUFvQjtBQUN4Qzs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSTtBQUNKO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUEsZ0JBQWdCLG9CQUFvQjtBQUNwQztBQUNBOztBQUVBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2gvY29tbW9uLmpzP2VkYzkiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgdXRpbHMgPSByZXF1aXJlKCcuL3V0aWxzJyk7XG52YXIgYXNzZXJ0ID0gcmVxdWlyZSgnbWluaW1hbGlzdGljLWFzc2VydCcpO1xuXG5mdW5jdGlvbiBCbG9ja0hhc2goKSB7XG4gIHRoaXMucGVuZGluZyA9IG51bGw7XG4gIHRoaXMucGVuZGluZ1RvdGFsID0gMDtcbiAgdGhpcy5ibG9ja1NpemUgPSB0aGlzLmNvbnN0cnVjdG9yLmJsb2NrU2l6ZTtcbiAgdGhpcy5vdXRTaXplID0gdGhpcy5jb25zdHJ1Y3Rvci5vdXRTaXplO1xuICB0aGlzLmhtYWNTdHJlbmd0aCA9IHRoaXMuY29uc3RydWN0b3IuaG1hY1N0cmVuZ3RoO1xuICB0aGlzLnBhZExlbmd0aCA9IHRoaXMuY29uc3RydWN0b3IucGFkTGVuZ3RoIC8gODtcbiAgdGhpcy5lbmRpYW4gPSAnYmlnJztcblxuICB0aGlzLl9kZWx0YTggPSB0aGlzLmJsb2NrU2l6ZSAvIDg7XG4gIHRoaXMuX2RlbHRhMzIgPSB0aGlzLmJsb2NrU2l6ZSAvIDMyO1xufVxuZXhwb3J0cy5CbG9ja0hhc2ggPSBCbG9ja0hhc2g7XG5cbkJsb2NrSGFzaC5wcm90b3R5cGUudXBkYXRlID0gZnVuY3Rpb24gdXBkYXRlKG1zZywgZW5jKSB7XG4gIC8vIENvbnZlcnQgbWVzc2FnZSB0byBhcnJheSwgcGFkIGl0LCBhbmQgam9pbiBpbnRvIDMyYml0IGJsb2Nrc1xuICBtc2cgPSB1dGlscy50b0FycmF5KG1zZywgZW5jKTtcbiAgaWYgKCF0aGlzLnBlbmRpbmcpXG4gICAgdGhpcy5wZW5kaW5nID0gbXNnO1xuICBlbHNlXG4gICAgdGhpcy5wZW5kaW5nID0gdGhpcy5wZW5kaW5nLmNvbmNhdChtc2cpO1xuICB0aGlzLnBlbmRpbmdUb3RhbCArPSBtc2cubGVuZ3RoO1xuXG4gIC8vIEVub3VnaCBkYXRhLCB0cnkgdXBkYXRpbmdcbiAgaWYgKHRoaXMucGVuZGluZy5sZW5ndGggPj0gdGhpcy5fZGVsdGE4KSB7XG4gICAgbXNnID0gdGhpcy5wZW5kaW5nO1xuXG4gICAgLy8gUHJvY2VzcyBwZW5kaW5nIGRhdGEgaW4gYmxvY2tzXG4gICAgdmFyIHIgPSBtc2cubGVuZ3RoICUgdGhpcy5fZGVsdGE4O1xuICAgIHRoaXMucGVuZGluZyA9IG1zZy5zbGljZShtc2cubGVuZ3RoIC0gciwgbXNnLmxlbmd0aCk7XG4gICAgaWYgKHRoaXMucGVuZGluZy5sZW5ndGggPT09IDApXG4gICAgICB0aGlzLnBlbmRpbmcgPSBudWxsO1xuXG4gICAgbXNnID0gdXRpbHMuam9pbjMyKG1zZywgMCwgbXNnLmxlbmd0aCAtIHIsIHRoaXMuZW5kaWFuKTtcbiAgICBmb3IgKHZhciBpID0gMDsgaSA8IG1zZy5sZW5ndGg7IGkgKz0gdGhpcy5fZGVsdGEzMilcbiAgICAgIHRoaXMuX3VwZGF0ZShtc2csIGksIGkgKyB0aGlzLl9kZWx0YTMyKTtcbiAgfVxuXG4gIHJldHVybiB0aGlzO1xufTtcblxuQmxvY2tIYXNoLnByb3RvdHlwZS5kaWdlc3QgPSBmdW5jdGlvbiBkaWdlc3QoZW5jKSB7XG4gIHRoaXMudXBkYXRlKHRoaXMuX3BhZCgpKTtcbiAgYXNzZXJ0KHRoaXMucGVuZGluZyA9PT0gbnVsbCk7XG5cbiAgcmV0dXJuIHRoaXMuX2RpZ2VzdChlbmMpO1xufTtcblxuQmxvY2tIYXNoLnByb3RvdHlwZS5fcGFkID0gZnVuY3Rpb24gcGFkKCkge1xuICB2YXIgbGVuID0gdGhpcy5wZW5kaW5nVG90YWw7XG4gIHZhciBieXRlcyA9IHRoaXMuX2RlbHRhODtcbiAgdmFyIGsgPSBieXRlcyAtICgobGVuICsgdGhpcy5wYWRMZW5ndGgpICUgYnl0ZXMpO1xuICB2YXIgcmVzID0gbmV3IEFycmF5KGsgKyB0aGlzLnBhZExlbmd0aCk7XG4gIHJlc1swXSA9IDB4ODA7XG4gIGZvciAodmFyIGkgPSAxOyBpIDwgazsgaSsrKVxuICAgIHJlc1tpXSA9IDA7XG5cbiAgLy8gQXBwZW5kIGxlbmd0aFxuICBsZW4gPDw9IDM7XG4gIGlmICh0aGlzLmVuZGlhbiA9PT0gJ2JpZycpIHtcbiAgICBmb3IgKHZhciB0ID0gODsgdCA8IHRoaXMucGFkTGVuZ3RoOyB0KyspXG4gICAgICByZXNbaSsrXSA9IDA7XG5cbiAgICByZXNbaSsrXSA9IDA7XG4gICAgcmVzW2krK10gPSAwO1xuICAgIHJlc1tpKytdID0gMDtcbiAgICByZXNbaSsrXSA9IDA7XG4gICAgcmVzW2krK10gPSAobGVuID4+PiAyNCkgJiAweGZmO1xuICAgIHJlc1tpKytdID0gKGxlbiA+Pj4gMTYpICYgMHhmZjtcbiAgICByZXNbaSsrXSA9IChsZW4gPj4+IDgpICYgMHhmZjtcbiAgICByZXNbaSsrXSA9IGxlbiAmIDB4ZmY7XG4gIH0gZWxzZSB7XG4gICAgcmVzW2krK10gPSBsZW4gJiAweGZmO1xuICAgIHJlc1tpKytdID0gKGxlbiA+Pj4gOCkgJiAweGZmO1xuICAgIHJlc1tpKytdID0gKGxlbiA+Pj4gMTYpICYgMHhmZjtcbiAgICByZXNbaSsrXSA9IChsZW4gPj4+IDI0KSAmIDB4ZmY7XG4gICAgcmVzW2krK10gPSAwO1xuICAgIHJlc1tpKytdID0gMDtcbiAgICByZXNbaSsrXSA9IDA7XG4gICAgcmVzW2krK10gPSAwO1xuXG4gICAgZm9yICh0ID0gODsgdCA8IHRoaXMucGFkTGVuZ3RoOyB0KyspXG4gICAgICByZXNbaSsrXSA9IDA7XG4gIH1cblxuICByZXR1cm4gcmVzO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///5772\n")},2344:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar assert = __webpack_require__(9746);\n\nfunction Hmac(hash, key, enc) {\n if (!(this instanceof Hmac))\n return new Hmac(hash, key, enc);\n this.Hash = hash;\n this.blockSize = hash.blockSize / 8;\n this.outSize = hash.outSize / 8;\n this.inner = null;\n this.outer = null;\n\n this._init(utils.toArray(key, enc));\n}\nmodule.exports = Hmac;\n\nHmac.prototype._init = function init(key) {\n // Shorten key, if needed\n if (key.length > this.blockSize)\n key = new this.Hash().update(key).digest();\n assert(key.length <= this.blockSize);\n\n // Add padding to key\n for (var i = key.length; i < this.blockSize; i++)\n key.push(0);\n\n for (i = 0; i < key.length; i++)\n key[i] ^= 0x36;\n this.inner = new this.Hash().update(key);\n\n // 0x36 ^ 0x5c = 0x6a\n for (i = 0; i < key.length; i++)\n key[i] ^= 0x6a;\n this.outer = new this.Hash().update(key);\n};\n\nHmac.prototype.update = function update(msg, enc) {\n this.inner.update(msg, enc);\n return this;\n};\n\nHmac.prototype.digest = function digest(enc) {\n this.outer.update(this.inner.digest());\n return this.outer.digest(enc);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjM0NC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBUztBQUM3QixhQUFhLG1CQUFPLENBQUMsSUFBcUI7O0FBRTFDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQSwyQkFBMkIsb0JBQW9CO0FBQy9DOztBQUVBLGNBQWMsZ0JBQWdCO0FBQzlCO0FBQ0E7O0FBRUE7QUFDQSxjQUFjLGdCQUFnQjtBQUM5QjtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2gvaG1hYy5qcz8yMTM3Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIHV0aWxzID0gcmVxdWlyZSgnLi91dGlscycpO1xudmFyIGFzc2VydCA9IHJlcXVpcmUoJ21pbmltYWxpc3RpYy1hc3NlcnQnKTtcblxuZnVuY3Rpb24gSG1hYyhoYXNoLCBrZXksIGVuYykge1xuICBpZiAoISh0aGlzIGluc3RhbmNlb2YgSG1hYykpXG4gICAgcmV0dXJuIG5ldyBIbWFjKGhhc2gsIGtleSwgZW5jKTtcbiAgdGhpcy5IYXNoID0gaGFzaDtcbiAgdGhpcy5ibG9ja1NpemUgPSBoYXNoLmJsb2NrU2l6ZSAvIDg7XG4gIHRoaXMub3V0U2l6ZSA9IGhhc2gub3V0U2l6ZSAvIDg7XG4gIHRoaXMuaW5uZXIgPSBudWxsO1xuICB0aGlzLm91dGVyID0gbnVsbDtcblxuICB0aGlzLl9pbml0KHV0aWxzLnRvQXJyYXkoa2V5LCBlbmMpKTtcbn1cbm1vZHVsZS5leHBvcnRzID0gSG1hYztcblxuSG1hYy5wcm90b3R5cGUuX2luaXQgPSBmdW5jdGlvbiBpbml0KGtleSkge1xuICAvLyBTaG9ydGVuIGtleSwgaWYgbmVlZGVkXG4gIGlmIChrZXkubGVuZ3RoID4gdGhpcy5ibG9ja1NpemUpXG4gICAga2V5ID0gbmV3IHRoaXMuSGFzaCgpLnVwZGF0ZShrZXkpLmRpZ2VzdCgpO1xuICBhc3NlcnQoa2V5Lmxlbmd0aCA8PSB0aGlzLmJsb2NrU2l6ZSk7XG5cbiAgLy8gQWRkIHBhZGRpbmcgdG8ga2V5XG4gIGZvciAodmFyIGkgPSBrZXkubGVuZ3RoOyBpIDwgdGhpcy5ibG9ja1NpemU7IGkrKylcbiAgICBrZXkucHVzaCgwKTtcblxuICBmb3IgKGkgPSAwOyBpIDwga2V5Lmxlbmd0aDsgaSsrKVxuICAgIGtleVtpXSBePSAweDM2O1xuICB0aGlzLmlubmVyID0gbmV3IHRoaXMuSGFzaCgpLnVwZGF0ZShrZXkpO1xuXG4gIC8vIDB4MzYgXiAweDVjID0gMHg2YVxuICBmb3IgKGkgPSAwOyBpIDwga2V5Lmxlbmd0aDsgaSsrKVxuICAgIGtleVtpXSBePSAweDZhO1xuICB0aGlzLm91dGVyID0gbmV3IHRoaXMuSGFzaCgpLnVwZGF0ZShrZXkpO1xufTtcblxuSG1hYy5wcm90b3R5cGUudXBkYXRlID0gZnVuY3Rpb24gdXBkYXRlKG1zZywgZW5jKSB7XG4gIHRoaXMuaW5uZXIudXBkYXRlKG1zZywgZW5jKTtcbiAgcmV0dXJuIHRoaXM7XG59O1xuXG5IbWFjLnByb3RvdHlwZS5kaWdlc3QgPSBmdW5jdGlvbiBkaWdlc3QoZW5jKSB7XG4gIHRoaXMub3V0ZXIudXBkYXRlKHRoaXMuaW5uZXIuZGlnZXN0KCkpO1xuICByZXR1cm4gdGhpcy5vdXRlci5kaWdlc3QoZW5jKTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///2344\n")},2949:function(__unused_webpack_module,exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar common = __webpack_require__(5772);\n\nvar rotl32 = utils.rotl32;\nvar sum32 = utils.sum32;\nvar sum32_3 = utils.sum32_3;\nvar sum32_4 = utils.sum32_4;\nvar BlockHash = common.BlockHash;\n\nfunction RIPEMD160() {\n if (!(this instanceof RIPEMD160))\n return new RIPEMD160();\n\n BlockHash.call(this);\n\n this.h = [ 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0 ];\n this.endian = 'little';\n}\nutils.inherits(RIPEMD160, BlockHash);\nexports.ripemd160 = RIPEMD160;\n\nRIPEMD160.blockSize = 512;\nRIPEMD160.outSize = 160;\nRIPEMD160.hmacStrength = 192;\nRIPEMD160.padLength = 64;\n\nRIPEMD160.prototype._update = function update(msg, start) {\n var A = this.h[0];\n var B = this.h[1];\n var C = this.h[2];\n var D = this.h[3];\n var E = this.h[4];\n var Ah = A;\n var Bh = B;\n var Ch = C;\n var Dh = D;\n var Eh = E;\n for (var j = 0; j < 80; j++) {\n var T = sum32(\n rotl32(\n sum32_4(A, f(j, B, C, D), msg[r[j] + start], K(j)),\n s[j]),\n E);\n A = E;\n E = D;\n D = rotl32(C, 10);\n C = B;\n B = T;\n T = sum32(\n rotl32(\n sum32_4(Ah, f(79 - j, Bh, Ch, Dh), msg[rh[j] + start], Kh(j)),\n sh[j]),\n Eh);\n Ah = Eh;\n Eh = Dh;\n Dh = rotl32(Ch, 10);\n Ch = Bh;\n Bh = T;\n }\n T = sum32_3(this.h[1], C, Dh);\n this.h[1] = sum32_3(this.h[2], D, Eh);\n this.h[2] = sum32_3(this.h[3], E, Ah);\n this.h[3] = sum32_3(this.h[4], A, Bh);\n this.h[4] = sum32_3(this.h[0], B, Ch);\n this.h[0] = T;\n};\n\nRIPEMD160.prototype._digest = function digest(enc) {\n if (enc === 'hex')\n return utils.toHex32(this.h, 'little');\n else\n return utils.split32(this.h, 'little');\n};\n\nfunction f(j, x, y, z) {\n if (j <= 15)\n return x ^ y ^ z;\n else if (j <= 31)\n return (x & y) | ((~x) & z);\n else if (j <= 47)\n return (x | (~y)) ^ z;\n else if (j <= 63)\n return (x & z) | (y & (~z));\n else\n return x ^ (y | (~z));\n}\n\nfunction K(j) {\n if (j <= 15)\n return 0x00000000;\n else if (j <= 31)\n return 0x5a827999;\n else if (j <= 47)\n return 0x6ed9eba1;\n else if (j <= 63)\n return 0x8f1bbcdc;\n else\n return 0xa953fd4e;\n}\n\nfunction Kh(j) {\n if (j <= 15)\n return 0x50a28be6;\n else if (j <= 31)\n return 0x5c4dd124;\n else if (j <= 47)\n return 0x6d703ef3;\n else if (j <= 63)\n return 0x7a6d76e9;\n else\n return 0x00000000;\n}\n\nvar r = [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,\n 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,\n 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,\n 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,\n 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13\n];\n\nvar rh = [\n 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,\n 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,\n 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,\n 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,\n 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11\n];\n\nvar s = [\n 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,\n 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,\n 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,\n 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,\n 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6\n];\n\nvar sh = [\n 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,\n 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,\n 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,\n 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,\n 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11\n];\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjk0OS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBUztBQUM3QixhQUFhLG1CQUFPLENBQUMsSUFBVTs7QUFFL0I7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQSxpQkFBaUI7O0FBRWpCO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxrQkFBa0IsUUFBUTtBQUMxQjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2gvcmlwZW1kLmpzP2JiNDQiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgdXRpbHMgPSByZXF1aXJlKCcuL3V0aWxzJyk7XG52YXIgY29tbW9uID0gcmVxdWlyZSgnLi9jb21tb24nKTtcblxudmFyIHJvdGwzMiA9IHV0aWxzLnJvdGwzMjtcbnZhciBzdW0zMiA9IHV0aWxzLnN1bTMyO1xudmFyIHN1bTMyXzMgPSB1dGlscy5zdW0zMl8zO1xudmFyIHN1bTMyXzQgPSB1dGlscy5zdW0zMl80O1xudmFyIEJsb2NrSGFzaCA9IGNvbW1vbi5CbG9ja0hhc2g7XG5cbmZ1bmN0aW9uIFJJUEVNRDE2MCgpIHtcbiAgaWYgKCEodGhpcyBpbnN0YW5jZW9mIFJJUEVNRDE2MCkpXG4gICAgcmV0dXJuIG5ldyBSSVBFTUQxNjAoKTtcblxuICBCbG9ja0hhc2guY2FsbCh0aGlzKTtcblxuICB0aGlzLmggPSBbIDB4Njc0NTIzMDEsIDB4ZWZjZGFiODksIDB4OThiYWRjZmUsIDB4MTAzMjU0NzYsIDB4YzNkMmUxZjAgXTtcbiAgdGhpcy5lbmRpYW4gPSAnbGl0dGxlJztcbn1cbnV0aWxzLmluaGVyaXRzKFJJUEVNRDE2MCwgQmxvY2tIYXNoKTtcbmV4cG9ydHMucmlwZW1kMTYwID0gUklQRU1EMTYwO1xuXG5SSVBFTUQxNjAuYmxvY2tTaXplID0gNTEyO1xuUklQRU1EMTYwLm91dFNpemUgPSAxNjA7XG5SSVBFTUQxNjAuaG1hY1N0cmVuZ3RoID0gMTkyO1xuUklQRU1EMTYwLnBhZExlbmd0aCA9IDY0O1xuXG5SSVBFTUQxNjAucHJvdG90eXBlLl91cGRhdGUgPSBmdW5jdGlvbiB1cGRhdGUobXNnLCBzdGFydCkge1xuICB2YXIgQSA9IHRoaXMuaFswXTtcbiAgdmFyIEIgPSB0aGlzLmhbMV07XG4gIHZhciBDID0gdGhpcy5oWzJdO1xuICB2YXIgRCA9IHRoaXMuaFszXTtcbiAgdmFyIEUgPSB0aGlzLmhbNF07XG4gIHZhciBBaCA9IEE7XG4gIHZhciBCaCA9IEI7XG4gIHZhciBDaCA9IEM7XG4gIHZhciBEaCA9IEQ7XG4gIHZhciBFaCA9IEU7XG4gIGZvciAodmFyIGogPSAwOyBqIDwgODA7IGorKykge1xuICAgIHZhciBUID0gc3VtMzIoXG4gICAgICByb3RsMzIoXG4gICAgICAgIHN1bTMyXzQoQSwgZihqLCBCLCBDLCBEKSwgbXNnW3Jbal0gKyBzdGFydF0sIEsoaikpLFxuICAgICAgICBzW2pdKSxcbiAgICAgIEUpO1xuICAgIEEgPSBFO1xuICAgIEUgPSBEO1xuICAgIEQgPSByb3RsMzIoQywgMTApO1xuICAgIEMgPSBCO1xuICAgIEIgPSBUO1xuICAgIFQgPSBzdW0zMihcbiAgICAgIHJvdGwzMihcbiAgICAgICAgc3VtMzJfNChBaCwgZig3OSAtIGosIEJoLCBDaCwgRGgpLCBtc2dbcmhbal0gKyBzdGFydF0sIEtoKGopKSxcbiAgICAgICAgc2hbal0pLFxuICAgICAgRWgpO1xuICAgIEFoID0gRWg7XG4gICAgRWggPSBEaDtcbiAgICBEaCA9IHJvdGwzMihDaCwgMTApO1xuICAgIENoID0gQmg7XG4gICAgQmggPSBUO1xuICB9XG4gIFQgPSBzdW0zMl8zKHRoaXMuaFsxXSwgQywgRGgpO1xuICB0aGlzLmhbMV0gPSBzdW0zMl8zKHRoaXMuaFsyXSwgRCwgRWgpO1xuICB0aGlzLmhbMl0gPSBzdW0zMl8zKHRoaXMuaFszXSwgRSwgQWgpO1xuICB0aGlzLmhbM10gPSBzdW0zMl8zKHRoaXMuaFs0XSwgQSwgQmgpO1xuICB0aGlzLmhbNF0gPSBzdW0zMl8zKHRoaXMuaFswXSwgQiwgQ2gpO1xuICB0aGlzLmhbMF0gPSBUO1xufTtcblxuUklQRU1EMTYwLnByb3RvdHlwZS5fZGlnZXN0ID0gZnVuY3Rpb24gZGlnZXN0KGVuYykge1xuICBpZiAoZW5jID09PSAnaGV4JylcbiAgICByZXR1cm4gdXRpbHMudG9IZXgzMih0aGlzLmgsICdsaXR0bGUnKTtcbiAgZWxzZVxuICAgIHJldHVybiB1dGlscy5zcGxpdDMyKHRoaXMuaCwgJ2xpdHRsZScpO1xufTtcblxuZnVuY3Rpb24gZihqLCB4LCB5LCB6KSB7XG4gIGlmIChqIDw9IDE1KVxuICAgIHJldHVybiB4IF4geSBeIHo7XG4gIGVsc2UgaWYgKGogPD0gMzEpXG4gICAgcmV0dXJuICh4ICYgeSkgfCAoKH54KSAmIHopO1xuICBlbHNlIGlmIChqIDw9IDQ3KVxuICAgIHJldHVybiAoeCB8ICh+eSkpIF4gejtcbiAgZWxzZSBpZiAoaiA8PSA2MylcbiAgICByZXR1cm4gKHggJiB6KSB8ICh5ICYgKH56KSk7XG4gIGVsc2VcbiAgICByZXR1cm4geCBeICh5IHwgKH56KSk7XG59XG5cbmZ1bmN0aW9uIEsoaikge1xuICBpZiAoaiA8PSAxNSlcbiAgICByZXR1cm4gMHgwMDAwMDAwMDtcbiAgZWxzZSBpZiAoaiA8PSAzMSlcbiAgICByZXR1cm4gMHg1YTgyNzk5OTtcbiAgZWxzZSBpZiAoaiA8PSA0NylcbiAgICByZXR1cm4gMHg2ZWQ5ZWJhMTtcbiAgZWxzZSBpZiAoaiA8PSA2MylcbiAgICByZXR1cm4gMHg4ZjFiYmNkYztcbiAgZWxzZVxuICAgIHJldHVybiAweGE5NTNmZDRlO1xufVxuXG5mdW5jdGlvbiBLaChqKSB7XG4gIGlmIChqIDw9IDE1KVxuICAgIHJldHVybiAweDUwYTI4YmU2O1xuICBlbHNlIGlmIChqIDw9IDMxKVxuICAgIHJldHVybiAweDVjNGRkMTI0O1xuICBlbHNlIGlmIChqIDw9IDQ3KVxuICAgIHJldHVybiAweDZkNzAzZWYzO1xuICBlbHNlIGlmIChqIDw9IDYzKVxuICAgIHJldHVybiAweDdhNmQ3NmU5O1xuICBlbHNlXG4gICAgcmV0dXJuIDB4MDAwMDAwMDA7XG59XG5cbnZhciByID0gW1xuICAwLCAxLCAyLCAzLCA0LCA1LCA2LCA3LCA4LCA5LCAxMCwgMTEsIDEyLCAxMywgMTQsIDE1LFxuICA3LCA0LCAxMywgMSwgMTAsIDYsIDE1LCAzLCAxMiwgMCwgOSwgNSwgMiwgMTQsIDExLCA4LFxuICAzLCAxMCwgMTQsIDQsIDksIDE1LCA4LCAxLCAyLCA3LCAwLCA2LCAxMywgMTEsIDUsIDEyLFxuICAxLCA5LCAxMSwgMTAsIDAsIDgsIDEyLCA0LCAxMywgMywgNywgMTUsIDE0LCA1LCA2LCAyLFxuICA0LCAwLCA1LCA5LCA3LCAxMiwgMiwgMTAsIDE0LCAxLCAzLCA4LCAxMSwgNiwgMTUsIDEzXG5dO1xuXG52YXIgcmggPSBbXG4gIDUsIDE0LCA3LCAwLCA5LCAyLCAxMSwgNCwgMTMsIDYsIDE1LCA4LCAxLCAxMCwgMywgMTIsXG4gIDYsIDExLCAzLCA3LCAwLCAxMywgNSwgMTAsIDE0LCAxNSwgOCwgMTIsIDQsIDksIDEsIDIsXG4gIDE1LCA1LCAxLCAzLCA3LCAxNCwgNiwgOSwgMTEsIDgsIDEyLCAyLCAxMCwgMCwgNCwgMTMsXG4gIDgsIDYsIDQsIDEsIDMsIDExLCAxNSwgMCwgNSwgMTIsIDIsIDEzLCA5LCA3LCAxMCwgMTQsXG4gIDEyLCAxNSwgMTAsIDQsIDEsIDUsIDgsIDcsIDYsIDIsIDEzLCAxNCwgMCwgMywgOSwgMTFcbl07XG5cbnZhciBzID0gW1xuICAxMSwgMTQsIDE1LCAxMiwgNSwgOCwgNywgOSwgMTEsIDEzLCAxNCwgMTUsIDYsIDcsIDksIDgsXG4gIDcsIDYsIDgsIDEzLCAxMSwgOSwgNywgMTUsIDcsIDEyLCAxNSwgOSwgMTEsIDcsIDEzLCAxMixcbiAgMTEsIDEzLCA2LCA3LCAxNCwgOSwgMTMsIDE1LCAxNCwgOCwgMTMsIDYsIDUsIDEyLCA3LCA1LFxuICAxMSwgMTIsIDE0LCAxNSwgMTQsIDE1LCA5LCA4LCA5LCAxNCwgNSwgNiwgOCwgNiwgNSwgMTIsXG4gIDksIDE1LCA1LCAxMSwgNiwgOCwgMTMsIDEyLCA1LCAxMiwgMTMsIDE0LCAxMSwgOCwgNSwgNlxuXTtcblxudmFyIHNoID0gW1xuICA4LCA5LCA5LCAxMSwgMTMsIDE1LCAxNSwgNSwgNywgNywgOCwgMTEsIDE0LCAxNCwgMTIsIDYsXG4gIDksIDEzLCAxNSwgNywgMTIsIDgsIDksIDExLCA3LCA3LCAxMiwgNywgNiwgMTUsIDEzLCAxMSxcbiAgOSwgNywgMTUsIDExLCA4LCA2LCA2LCAxNCwgMTIsIDEzLCA1LCAxNCwgMTMsIDEzLCA3LCA1LFxuICAxNSwgNSwgOCwgMTEsIDE0LCAxNCwgNiwgMTQsIDYsIDksIDEyLCA5LCAxMiwgNSwgMTUsIDgsXG4gIDgsIDUsIDEyLCA5LCAxMiwgNSwgMTQsIDYsIDgsIDEzLCA2LCA1LCAxNSwgMTMsIDExLCAxMVxuXTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2949\n")},9041:function(__unused_webpack_module,exports,__webpack_require__){"use strict";eval("\n\nexports.sha1 = __webpack_require__(4761);\nexports.sha224 = __webpack_require__(799);\nexports.sha256 = __webpack_require__(9344);\nexports.sha384 = __webpack_require__(772);\nexports.sha512 = __webpack_require__(5900);\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTA0MS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYix3Q0FBaUM7QUFDakMseUNBQXFDO0FBQ3JDLDBDQUFxQztBQUNyQyx5Q0FBcUM7QUFDckMsMENBQXFDIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9oYXNoLmpzL2xpYi9oYXNoL3NoYS5qcz81OTE5Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxuZXhwb3J0cy5zaGExID0gcmVxdWlyZSgnLi9zaGEvMScpO1xuZXhwb3J0cy5zaGEyMjQgPSByZXF1aXJlKCcuL3NoYS8yMjQnKTtcbmV4cG9ydHMuc2hhMjU2ID0gcmVxdWlyZSgnLi9zaGEvMjU2Jyk7XG5leHBvcnRzLnNoYTM4NCA9IHJlcXVpcmUoJy4vc2hhLzM4NCcpO1xuZXhwb3J0cy5zaGE1MTIgPSByZXF1aXJlKCcuL3NoYS81MTInKTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///9041\n")},4761:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar common = __webpack_require__(5772);\nvar shaCommon = __webpack_require__(7038);\n\nvar rotl32 = utils.rotl32;\nvar sum32 = utils.sum32;\nvar sum32_5 = utils.sum32_5;\nvar ft_1 = shaCommon.ft_1;\nvar BlockHash = common.BlockHash;\n\nvar sha1_K = [\n 0x5A827999, 0x6ED9EBA1,\n 0x8F1BBCDC, 0xCA62C1D6\n];\n\nfunction SHA1() {\n if (!(this instanceof SHA1))\n return new SHA1();\n\n BlockHash.call(this);\n this.h = [\n 0x67452301, 0xefcdab89, 0x98badcfe,\n 0x10325476, 0xc3d2e1f0 ];\n this.W = new Array(80);\n}\n\nutils.inherits(SHA1, BlockHash);\nmodule.exports = SHA1;\n\nSHA1.blockSize = 512;\nSHA1.outSize = 160;\nSHA1.hmacStrength = 80;\nSHA1.padLength = 64;\n\nSHA1.prototype._update = function _update(msg, start) {\n var W = this.W;\n\n for (var i = 0; i < 16; i++)\n W[i] = msg[start + i];\n\n for(; i < W.length; i++)\n W[i] = rotl32(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1);\n\n var a = this.h[0];\n var b = this.h[1];\n var c = this.h[2];\n var d = this.h[3];\n var e = this.h[4];\n\n for (i = 0; i < W.length; i++) {\n var s = ~~(i / 20);\n var t = sum32_5(rotl32(a, 5), ft_1(s, b, c, d), e, W[i], sha1_K[s]);\n e = d;\n d = c;\n c = rotl32(b, 30);\n b = a;\n a = t;\n }\n\n this.h[0] = sum32(this.h[0], a);\n this.h[1] = sum32(this.h[1], b);\n this.h[2] = sum32(this.h[2], c);\n this.h[3] = sum32(this.h[3], d);\n this.h[4] = sum32(this.h[4], e);\n};\n\nSHA1.prototype._digest = function digest(enc) {\n if (enc === 'hex')\n return utils.toHex32(this.h, 'big');\n else\n return utils.split32(this.h, 'big');\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDc2MS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBVTtBQUM5QixhQUFhLG1CQUFPLENBQUMsSUFBVztBQUNoQyxnQkFBZ0IsbUJBQU8sQ0FBQyxJQUFVOztBQUVsQztBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBLGtCQUFrQixRQUFRO0FBQzFCOztBQUVBLFFBQVEsY0FBYztBQUN0Qjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBLGNBQWMsY0FBYztBQUM1QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2gvc2hhLzEuanM/MTNlMiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciB1dGlscyA9IHJlcXVpcmUoJy4uL3V0aWxzJyk7XG52YXIgY29tbW9uID0gcmVxdWlyZSgnLi4vY29tbW9uJyk7XG52YXIgc2hhQ29tbW9uID0gcmVxdWlyZSgnLi9jb21tb24nKTtcblxudmFyIHJvdGwzMiA9IHV0aWxzLnJvdGwzMjtcbnZhciBzdW0zMiA9IHV0aWxzLnN1bTMyO1xudmFyIHN1bTMyXzUgPSB1dGlscy5zdW0zMl81O1xudmFyIGZ0XzEgPSBzaGFDb21tb24uZnRfMTtcbnZhciBCbG9ja0hhc2ggPSBjb21tb24uQmxvY2tIYXNoO1xuXG52YXIgc2hhMV9LID0gW1xuICAweDVBODI3OTk5LCAweDZFRDlFQkExLFxuICAweDhGMUJCQ0RDLCAweENBNjJDMUQ2XG5dO1xuXG5mdW5jdGlvbiBTSEExKCkge1xuICBpZiAoISh0aGlzIGluc3RhbmNlb2YgU0hBMSkpXG4gICAgcmV0dXJuIG5ldyBTSEExKCk7XG5cbiAgQmxvY2tIYXNoLmNhbGwodGhpcyk7XG4gIHRoaXMuaCA9IFtcbiAgICAweDY3NDUyMzAxLCAweGVmY2RhYjg5LCAweDk4YmFkY2ZlLFxuICAgIDB4MTAzMjU0NzYsIDB4YzNkMmUxZjAgXTtcbiAgdGhpcy5XID0gbmV3IEFycmF5KDgwKTtcbn1cblxudXRpbHMuaW5oZXJpdHMoU0hBMSwgQmxvY2tIYXNoKTtcbm1vZHVsZS5leHBvcnRzID0gU0hBMTtcblxuU0hBMS5ibG9ja1NpemUgPSA1MTI7XG5TSEExLm91dFNpemUgPSAxNjA7XG5TSEExLmhtYWNTdHJlbmd0aCA9IDgwO1xuU0hBMS5wYWRMZW5ndGggPSA2NDtcblxuU0hBMS5wcm90b3R5cGUuX3VwZGF0ZSA9IGZ1bmN0aW9uIF91cGRhdGUobXNnLCBzdGFydCkge1xuICB2YXIgVyA9IHRoaXMuVztcblxuICBmb3IgKHZhciBpID0gMDsgaSA8IDE2OyBpKyspXG4gICAgV1tpXSA9IG1zZ1tzdGFydCArIGldO1xuXG4gIGZvcig7IGkgPCBXLmxlbmd0aDsgaSsrKVxuICAgIFdbaV0gPSByb3RsMzIoV1tpIC0gM10gXiBXW2kgLSA4XSBeIFdbaSAtIDE0XSBeIFdbaSAtIDE2XSwgMSk7XG5cbiAgdmFyIGEgPSB0aGlzLmhbMF07XG4gIHZhciBiID0gdGhpcy5oWzFdO1xuICB2YXIgYyA9IHRoaXMuaFsyXTtcbiAgdmFyIGQgPSB0aGlzLmhbM107XG4gIHZhciBlID0gdGhpcy5oWzRdO1xuXG4gIGZvciAoaSA9IDA7IGkgPCBXLmxlbmd0aDsgaSsrKSB7XG4gICAgdmFyIHMgPSB+fihpIC8gMjApO1xuICAgIHZhciB0ID0gc3VtMzJfNShyb3RsMzIoYSwgNSksIGZ0XzEocywgYiwgYywgZCksIGUsIFdbaV0sIHNoYTFfS1tzXSk7XG4gICAgZSA9IGQ7XG4gICAgZCA9IGM7XG4gICAgYyA9IHJvdGwzMihiLCAzMCk7XG4gICAgYiA9IGE7XG4gICAgYSA9IHQ7XG4gIH1cblxuICB0aGlzLmhbMF0gPSBzdW0zMih0aGlzLmhbMF0sIGEpO1xuICB0aGlzLmhbMV0gPSBzdW0zMih0aGlzLmhbMV0sIGIpO1xuICB0aGlzLmhbMl0gPSBzdW0zMih0aGlzLmhbMl0sIGMpO1xuICB0aGlzLmhbM10gPSBzdW0zMih0aGlzLmhbM10sIGQpO1xuICB0aGlzLmhbNF0gPSBzdW0zMih0aGlzLmhbNF0sIGUpO1xufTtcblxuU0hBMS5wcm90b3R5cGUuX2RpZ2VzdCA9IGZ1bmN0aW9uIGRpZ2VzdChlbmMpIHtcbiAgaWYgKGVuYyA9PT0gJ2hleCcpXG4gICAgcmV0dXJuIHV0aWxzLnRvSGV4MzIodGhpcy5oLCAnYmlnJyk7XG4gIGVsc2VcbiAgICByZXR1cm4gdXRpbHMuc3BsaXQzMih0aGlzLmgsICdiaWcnKTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///4761\n")},799:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar SHA256 = __webpack_require__(9344);\n\nfunction SHA224() {\n if (!(this instanceof SHA224))\n return new SHA224();\n\n SHA256.call(this);\n this.h = [\n 0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939,\n 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4 ];\n}\nutils.inherits(SHA224, SHA256);\nmodule.exports = SHA224;\n\nSHA224.blockSize = 512;\nSHA224.outSize = 224;\nSHA224.hmacStrength = 192;\nSHA224.padLength = 64;\n\nSHA224.prototype._digest = function digest(enc) {\n // Just truncate output\n if (enc === 'hex')\n return utils.toHex32(this.h.slice(0, 7), 'big');\n else\n return utils.split32(this.h.slice(0, 7), 'big');\n};\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzk5LmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLFlBQVksbUJBQU8sQ0FBQyxJQUFVO0FBQzlCLGFBQWEsbUJBQU8sQ0FBQyxJQUFPOztBQUU1QjtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2gvc2hhLzIyNC5qcz8wN2YyIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIHV0aWxzID0gcmVxdWlyZSgnLi4vdXRpbHMnKTtcbnZhciBTSEEyNTYgPSByZXF1aXJlKCcuLzI1NicpO1xuXG5mdW5jdGlvbiBTSEEyMjQoKSB7XG4gIGlmICghKHRoaXMgaW5zdGFuY2VvZiBTSEEyMjQpKVxuICAgIHJldHVybiBuZXcgU0hBMjI0KCk7XG5cbiAgU0hBMjU2LmNhbGwodGhpcyk7XG4gIHRoaXMuaCA9IFtcbiAgICAweGMxMDU5ZWQ4LCAweDM2N2NkNTA3LCAweDMwNzBkZDE3LCAweGY3MGU1OTM5LFxuICAgIDB4ZmZjMDBiMzEsIDB4Njg1ODE1MTEsIDB4NjRmOThmYTcsIDB4YmVmYTRmYTQgXTtcbn1cbnV0aWxzLmluaGVyaXRzKFNIQTIyNCwgU0hBMjU2KTtcbm1vZHVsZS5leHBvcnRzID0gU0hBMjI0O1xuXG5TSEEyMjQuYmxvY2tTaXplID0gNTEyO1xuU0hBMjI0Lm91dFNpemUgPSAyMjQ7XG5TSEEyMjQuaG1hY1N0cmVuZ3RoID0gMTkyO1xuU0hBMjI0LnBhZExlbmd0aCA9IDY0O1xuXG5TSEEyMjQucHJvdG90eXBlLl9kaWdlc3QgPSBmdW5jdGlvbiBkaWdlc3QoZW5jKSB7XG4gIC8vIEp1c3QgdHJ1bmNhdGUgb3V0cHV0XG4gIGlmIChlbmMgPT09ICdoZXgnKVxuICAgIHJldHVybiB1dGlscy50b0hleDMyKHRoaXMuaC5zbGljZSgwLCA3KSwgJ2JpZycpO1xuICBlbHNlXG4gICAgcmV0dXJuIHV0aWxzLnNwbGl0MzIodGhpcy5oLnNsaWNlKDAsIDcpLCAnYmlnJyk7XG59O1xuXG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///799\n")},9344:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar common = __webpack_require__(5772);\nvar shaCommon = __webpack_require__(7038);\nvar assert = __webpack_require__(9746);\n\nvar sum32 = utils.sum32;\nvar sum32_4 = utils.sum32_4;\nvar sum32_5 = utils.sum32_5;\nvar ch32 = shaCommon.ch32;\nvar maj32 = shaCommon.maj32;\nvar s0_256 = shaCommon.s0_256;\nvar s1_256 = shaCommon.s1_256;\nvar g0_256 = shaCommon.g0_256;\nvar g1_256 = shaCommon.g1_256;\n\nvar BlockHash = common.BlockHash;\n\nvar sha256_K = [\n 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,\n 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,\n 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,\n 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,\n 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,\n 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,\n 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,\n 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,\n 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,\n 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,\n 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,\n 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,\n 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,\n 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,\n 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,\n 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2\n];\n\nfunction SHA256() {\n if (!(this instanceof SHA256))\n return new SHA256();\n\n BlockHash.call(this);\n this.h = [\n 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,\n 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19\n ];\n this.k = sha256_K;\n this.W = new Array(64);\n}\nutils.inherits(SHA256, BlockHash);\nmodule.exports = SHA256;\n\nSHA256.blockSize = 512;\nSHA256.outSize = 256;\nSHA256.hmacStrength = 192;\nSHA256.padLength = 64;\n\nSHA256.prototype._update = function _update(msg, start) {\n var W = this.W;\n\n for (var i = 0; i < 16; i++)\n W[i] = msg[start + i];\n for (; i < W.length; i++)\n W[i] = sum32_4(g1_256(W[i - 2]), W[i - 7], g0_256(W[i - 15]), W[i - 16]);\n\n var a = this.h[0];\n var b = this.h[1];\n var c = this.h[2];\n var d = this.h[3];\n var e = this.h[4];\n var f = this.h[5];\n var g = this.h[6];\n var h = this.h[7];\n\n assert(this.k.length === W.length);\n for (i = 0; i < W.length; i++) {\n var T1 = sum32_5(h, s1_256(e), ch32(e, f, g), this.k[i], W[i]);\n var T2 = sum32(s0_256(a), maj32(a, b, c));\n h = g;\n g = f;\n f = e;\n e = sum32(d, T1);\n d = c;\n c = b;\n b = a;\n a = sum32(T1, T2);\n }\n\n this.h[0] = sum32(this.h[0], a);\n this.h[1] = sum32(this.h[1], b);\n this.h[2] = sum32(this.h[2], c);\n this.h[3] = sum32(this.h[3], d);\n this.h[4] = sum32(this.h[4], e);\n this.h[5] = sum32(this.h[5], f);\n this.h[6] = sum32(this.h[6], g);\n this.h[7] = sum32(this.h[7], h);\n};\n\nSHA256.prototype._digest = function digest(enc) {\n if (enc === 'hex')\n return utils.toHex32(this.h, 'big');\n else\n return utils.split32(this.h, 'big');\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTM0NC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBVTtBQUM5QixhQUFhLG1CQUFPLENBQUMsSUFBVztBQUNoQyxnQkFBZ0IsbUJBQU8sQ0FBQyxJQUFVO0FBQ2xDLGFBQWEsbUJBQU8sQ0FBQyxJQUFxQjs7QUFFMUM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQSxrQkFBa0IsUUFBUTtBQUMxQjtBQUNBLFNBQVMsY0FBYztBQUN2Qjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0EsY0FBYyxjQUFjO0FBQzVCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvaGFzaC5qcy9saWIvaGFzaC9zaGEvMjU2LmpzPzZlZWQiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgdXRpbHMgPSByZXF1aXJlKCcuLi91dGlscycpO1xudmFyIGNvbW1vbiA9IHJlcXVpcmUoJy4uL2NvbW1vbicpO1xudmFyIHNoYUNvbW1vbiA9IHJlcXVpcmUoJy4vY29tbW9uJyk7XG52YXIgYXNzZXJ0ID0gcmVxdWlyZSgnbWluaW1hbGlzdGljLWFzc2VydCcpO1xuXG52YXIgc3VtMzIgPSB1dGlscy5zdW0zMjtcbnZhciBzdW0zMl80ID0gdXRpbHMuc3VtMzJfNDtcbnZhciBzdW0zMl81ID0gdXRpbHMuc3VtMzJfNTtcbnZhciBjaDMyID0gc2hhQ29tbW9uLmNoMzI7XG52YXIgbWFqMzIgPSBzaGFDb21tb24ubWFqMzI7XG52YXIgczBfMjU2ID0gc2hhQ29tbW9uLnMwXzI1NjtcbnZhciBzMV8yNTYgPSBzaGFDb21tb24uczFfMjU2O1xudmFyIGcwXzI1NiA9IHNoYUNvbW1vbi5nMF8yNTY7XG52YXIgZzFfMjU2ID0gc2hhQ29tbW9uLmcxXzI1NjtcblxudmFyIEJsb2NrSGFzaCA9IGNvbW1vbi5CbG9ja0hhc2g7XG5cbnZhciBzaGEyNTZfSyA9IFtcbiAgMHg0MjhhMmY5OCwgMHg3MTM3NDQ5MSwgMHhiNWMwZmJjZiwgMHhlOWI1ZGJhNSxcbiAgMHgzOTU2YzI1YiwgMHg1OWYxMTFmMSwgMHg5MjNmODJhNCwgMHhhYjFjNWVkNSxcbiAgMHhkODA3YWE5OCwgMHgxMjgzNWIwMSwgMHgyNDMxODViZSwgMHg1NTBjN2RjMyxcbiAgMHg3MmJlNWQ3NCwgMHg4MGRlYjFmZSwgMHg5YmRjMDZhNywgMHhjMTliZjE3NCxcbiAgMHhlNDliNjljMSwgMHhlZmJlNDc4NiwgMHgwZmMxOWRjNiwgMHgyNDBjYTFjYyxcbiAgMHgyZGU5MmM2ZiwgMHg0YTc0ODRhYSwgMHg1Y2IwYTlkYywgMHg3NmY5ODhkYSxcbiAgMHg5ODNlNTE1MiwgMHhhODMxYzY2ZCwgMHhiMDAzMjdjOCwgMHhiZjU5N2ZjNyxcbiAgMHhjNmUwMGJmMywgMHhkNWE3OTE0NywgMHgwNmNhNjM1MSwgMHgxNDI5Mjk2NyxcbiAgMHgyN2I3MGE4NSwgMHgyZTFiMjEzOCwgMHg0ZDJjNmRmYywgMHg1MzM4MGQxMyxcbiAgMHg2NTBhNzM1NCwgMHg3NjZhMGFiYiwgMHg4MWMyYzkyZSwgMHg5MjcyMmM4NSxcbiAgMHhhMmJmZThhMSwgMHhhODFhNjY0YiwgMHhjMjRiOGI3MCwgMHhjNzZjNTFhMyxcbiAgMHhkMTkyZTgxOSwgMHhkNjk5MDYyNCwgMHhmNDBlMzU4NSwgMHgxMDZhYTA3MCxcbiAgMHgxOWE0YzExNiwgMHgxZTM3NmMwOCwgMHgyNzQ4Nzc0YywgMHgzNGIwYmNiNSxcbiAgMHgzOTFjMGNiMywgMHg0ZWQ4YWE0YSwgMHg1YjljY2E0ZiwgMHg2ODJlNmZmMyxcbiAgMHg3NDhmODJlZSwgMHg3OGE1NjM2ZiwgMHg4NGM4NzgxNCwgMHg4Y2M3MDIwOCxcbiAgMHg5MGJlZmZmYSwgMHhhNDUwNmNlYiwgMHhiZWY5YTNmNywgMHhjNjcxNzhmMlxuXTtcblxuZnVuY3Rpb24gU0hBMjU2KCkge1xuICBpZiAoISh0aGlzIGluc3RhbmNlb2YgU0hBMjU2KSlcbiAgICByZXR1cm4gbmV3IFNIQTI1NigpO1xuXG4gIEJsb2NrSGFzaC5jYWxsKHRoaXMpO1xuICB0aGlzLmggPSBbXG4gICAgMHg2YTA5ZTY2NywgMHhiYjY3YWU4NSwgMHgzYzZlZjM3MiwgMHhhNTRmZjUzYSxcbiAgICAweDUxMGU1MjdmLCAweDliMDU2ODhjLCAweDFmODNkOWFiLCAweDViZTBjZDE5XG4gIF07XG4gIHRoaXMuayA9IHNoYTI1Nl9LO1xuICB0aGlzLlcgPSBuZXcgQXJyYXkoNjQpO1xufVxudXRpbHMuaW5oZXJpdHMoU0hBMjU2LCBCbG9ja0hhc2gpO1xubW9kdWxlLmV4cG9ydHMgPSBTSEEyNTY7XG5cblNIQTI1Ni5ibG9ja1NpemUgPSA1MTI7XG5TSEEyNTYub3V0U2l6ZSA9IDI1NjtcblNIQTI1Ni5obWFjU3RyZW5ndGggPSAxOTI7XG5TSEEyNTYucGFkTGVuZ3RoID0gNjQ7XG5cblNIQTI1Ni5wcm90b3R5cGUuX3VwZGF0ZSA9IGZ1bmN0aW9uIF91cGRhdGUobXNnLCBzdGFydCkge1xuICB2YXIgVyA9IHRoaXMuVztcblxuICBmb3IgKHZhciBpID0gMDsgaSA8IDE2OyBpKyspXG4gICAgV1tpXSA9IG1zZ1tzdGFydCArIGldO1xuICBmb3IgKDsgaSA8IFcubGVuZ3RoOyBpKyspXG4gICAgV1tpXSA9IHN1bTMyXzQoZzFfMjU2KFdbaSAtIDJdKSwgV1tpIC0gN10sIGcwXzI1NihXW2kgLSAxNV0pLCBXW2kgLSAxNl0pO1xuXG4gIHZhciBhID0gdGhpcy5oWzBdO1xuICB2YXIgYiA9IHRoaXMuaFsxXTtcbiAgdmFyIGMgPSB0aGlzLmhbMl07XG4gIHZhciBkID0gdGhpcy5oWzNdO1xuICB2YXIgZSA9IHRoaXMuaFs0XTtcbiAgdmFyIGYgPSB0aGlzLmhbNV07XG4gIHZhciBnID0gdGhpcy5oWzZdO1xuICB2YXIgaCA9IHRoaXMuaFs3XTtcblxuICBhc3NlcnQodGhpcy5rLmxlbmd0aCA9PT0gVy5sZW5ndGgpO1xuICBmb3IgKGkgPSAwOyBpIDwgVy5sZW5ndGg7IGkrKykge1xuICAgIHZhciBUMSA9IHN1bTMyXzUoaCwgczFfMjU2KGUpLCBjaDMyKGUsIGYsIGcpLCB0aGlzLmtbaV0sIFdbaV0pO1xuICAgIHZhciBUMiA9IHN1bTMyKHMwXzI1NihhKSwgbWFqMzIoYSwgYiwgYykpO1xuICAgIGggPSBnO1xuICAgIGcgPSBmO1xuICAgIGYgPSBlO1xuICAgIGUgPSBzdW0zMihkLCBUMSk7XG4gICAgZCA9IGM7XG4gICAgYyA9IGI7XG4gICAgYiA9IGE7XG4gICAgYSA9IHN1bTMyKFQxLCBUMik7XG4gIH1cblxuICB0aGlzLmhbMF0gPSBzdW0zMih0aGlzLmhbMF0sIGEpO1xuICB0aGlzLmhbMV0gPSBzdW0zMih0aGlzLmhbMV0sIGIpO1xuICB0aGlzLmhbMl0gPSBzdW0zMih0aGlzLmhbMl0sIGMpO1xuICB0aGlzLmhbM10gPSBzdW0zMih0aGlzLmhbM10sIGQpO1xuICB0aGlzLmhbNF0gPSBzdW0zMih0aGlzLmhbNF0sIGUpO1xuICB0aGlzLmhbNV0gPSBzdW0zMih0aGlzLmhbNV0sIGYpO1xuICB0aGlzLmhbNl0gPSBzdW0zMih0aGlzLmhbNl0sIGcpO1xuICB0aGlzLmhbN10gPSBzdW0zMih0aGlzLmhbN10sIGgpO1xufTtcblxuU0hBMjU2LnByb3RvdHlwZS5fZGlnZXN0ID0gZnVuY3Rpb24gZGlnZXN0KGVuYykge1xuICBpZiAoZW5jID09PSAnaGV4JylcbiAgICByZXR1cm4gdXRpbHMudG9IZXgzMih0aGlzLmgsICdiaWcnKTtcbiAgZWxzZVxuICAgIHJldHVybiB1dGlscy5zcGxpdDMyKHRoaXMuaCwgJ2JpZycpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///9344\n")},772:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\n\nvar SHA512 = __webpack_require__(5900);\n\nfunction SHA384() {\n if (!(this instanceof SHA384))\n return new SHA384();\n\n SHA512.call(this);\n this.h = [\n 0xcbbb9d5d, 0xc1059ed8,\n 0x629a292a, 0x367cd507,\n 0x9159015a, 0x3070dd17,\n 0x152fecd8, 0xf70e5939,\n 0x67332667, 0xffc00b31,\n 0x8eb44a87, 0x68581511,\n 0xdb0c2e0d, 0x64f98fa7,\n 0x47b5481d, 0xbefa4fa4 ];\n}\nutils.inherits(SHA384, SHA512);\nmodule.exports = SHA384;\n\nSHA384.blockSize = 1024;\nSHA384.outSize = 384;\nSHA384.hmacStrength = 192;\nSHA384.padLength = 128;\n\nSHA384.prototype._digest = function digest(enc) {\n if (enc === 'hex')\n return utils.toHex32(this.h.slice(0, 12), 'big');\n else\n return utils.split32(this.h.slice(0, 12), 'big');\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzcyLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLFlBQVksbUJBQU8sQ0FBQyxJQUFVOztBQUU5QixhQUFhLG1CQUFPLENBQUMsSUFBTzs7QUFFNUI7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvaGFzaC5qcy9saWIvaGFzaC9zaGEvMzg0LmpzPzhiOTUiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgdXRpbHMgPSByZXF1aXJlKCcuLi91dGlscycpO1xuXG52YXIgU0hBNTEyID0gcmVxdWlyZSgnLi81MTInKTtcblxuZnVuY3Rpb24gU0hBMzg0KCkge1xuICBpZiAoISh0aGlzIGluc3RhbmNlb2YgU0hBMzg0KSlcbiAgICByZXR1cm4gbmV3IFNIQTM4NCgpO1xuXG4gIFNIQTUxMi5jYWxsKHRoaXMpO1xuICB0aGlzLmggPSBbXG4gICAgMHhjYmJiOWQ1ZCwgMHhjMTA1OWVkOCxcbiAgICAweDYyOWEyOTJhLCAweDM2N2NkNTA3LFxuICAgIDB4OTE1OTAxNWEsIDB4MzA3MGRkMTcsXG4gICAgMHgxNTJmZWNkOCwgMHhmNzBlNTkzOSxcbiAgICAweDY3MzMyNjY3LCAweGZmYzAwYjMxLFxuICAgIDB4OGViNDRhODcsIDB4Njg1ODE1MTEsXG4gICAgMHhkYjBjMmUwZCwgMHg2NGY5OGZhNyxcbiAgICAweDQ3YjU0ODFkLCAweGJlZmE0ZmE0IF07XG59XG51dGlscy5pbmhlcml0cyhTSEEzODQsIFNIQTUxMik7XG5tb2R1bGUuZXhwb3J0cyA9IFNIQTM4NDtcblxuU0hBMzg0LmJsb2NrU2l6ZSA9IDEwMjQ7XG5TSEEzODQub3V0U2l6ZSA9IDM4NDtcblNIQTM4NC5obWFjU3RyZW5ndGggPSAxOTI7XG5TSEEzODQucGFkTGVuZ3RoID0gMTI4O1xuXG5TSEEzODQucHJvdG90eXBlLl9kaWdlc3QgPSBmdW5jdGlvbiBkaWdlc3QoZW5jKSB7XG4gIGlmIChlbmMgPT09ICdoZXgnKVxuICAgIHJldHVybiB1dGlscy50b0hleDMyKHRoaXMuaC5zbGljZSgwLCAxMiksICdiaWcnKTtcbiAgZWxzZVxuICAgIHJldHVybiB1dGlscy5zcGxpdDMyKHRoaXMuaC5zbGljZSgwLCAxMiksICdiaWcnKTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///772\n")},5900:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar common = __webpack_require__(5772);\nvar assert = __webpack_require__(9746);\n\nvar rotr64_hi = utils.rotr64_hi;\nvar rotr64_lo = utils.rotr64_lo;\nvar shr64_hi = utils.shr64_hi;\nvar shr64_lo = utils.shr64_lo;\nvar sum64 = utils.sum64;\nvar sum64_hi = utils.sum64_hi;\nvar sum64_lo = utils.sum64_lo;\nvar sum64_4_hi = utils.sum64_4_hi;\nvar sum64_4_lo = utils.sum64_4_lo;\nvar sum64_5_hi = utils.sum64_5_hi;\nvar sum64_5_lo = utils.sum64_5_lo;\n\nvar BlockHash = common.BlockHash;\n\nvar sha512_K = [\n 0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd,\n 0xb5c0fbcf, 0xec4d3b2f, 0xe9b5dba5, 0x8189dbbc,\n 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019,\n 0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118,\n 0xd807aa98, 0xa3030242, 0x12835b01, 0x45706fbe,\n 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2,\n 0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1,\n 0x9bdc06a7, 0x25c71235, 0xc19bf174, 0xcf692694,\n 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3,\n 0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65,\n 0x2de92c6f, 0x592b0275, 0x4a7484aa, 0x6ea6e483,\n 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5,\n 0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210,\n 0xb00327c8, 0x98fb213f, 0xbf597fc7, 0xbeef0ee4,\n 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725,\n 0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70,\n 0x27b70a85, 0x46d22ffc, 0x2e1b2138, 0x5c26c926,\n 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df,\n 0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8,\n 0x81c2c92e, 0x47edaee6, 0x92722c85, 0x1482353b,\n 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001,\n 0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30,\n 0xd192e819, 0xd6ef5218, 0xd6990624, 0x5565a910,\n 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8,\n 0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53,\n 0x2748774c, 0xdf8eeb99, 0x34b0bcb5, 0xe19b48a8,\n 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb,\n 0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3,\n 0x748f82ee, 0x5defb2fc, 0x78a5636f, 0x43172f60,\n 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec,\n 0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9,\n 0xbef9a3f7, 0xb2c67915, 0xc67178f2, 0xe372532b,\n 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207,\n 0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178,\n 0x06f067aa, 0x72176fba, 0x0a637dc5, 0xa2c898a6,\n 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b,\n 0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493,\n 0x3c9ebe0a, 0x15c9bebc, 0x431d67c4, 0x9c100d4c,\n 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a,\n 0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817\n];\n\nfunction SHA512() {\n if (!(this instanceof SHA512))\n return new SHA512();\n\n BlockHash.call(this);\n this.h = [\n 0x6a09e667, 0xf3bcc908,\n 0xbb67ae85, 0x84caa73b,\n 0x3c6ef372, 0xfe94f82b,\n 0xa54ff53a, 0x5f1d36f1,\n 0x510e527f, 0xade682d1,\n 0x9b05688c, 0x2b3e6c1f,\n 0x1f83d9ab, 0xfb41bd6b,\n 0x5be0cd19, 0x137e2179 ];\n this.k = sha512_K;\n this.W = new Array(160);\n}\nutils.inherits(SHA512, BlockHash);\nmodule.exports = SHA512;\n\nSHA512.blockSize = 1024;\nSHA512.outSize = 512;\nSHA512.hmacStrength = 192;\nSHA512.padLength = 128;\n\nSHA512.prototype._prepareBlock = function _prepareBlock(msg, start) {\n var W = this.W;\n\n // 32 x 32bit words\n for (var i = 0; i < 32; i++)\n W[i] = msg[start + i];\n for (; i < W.length; i += 2) {\n var c0_hi = g1_512_hi(W[i - 4], W[i - 3]); // i - 2\n var c0_lo = g1_512_lo(W[i - 4], W[i - 3]);\n var c1_hi = W[i - 14]; // i - 7\n var c1_lo = W[i - 13];\n var c2_hi = g0_512_hi(W[i - 30], W[i - 29]); // i - 15\n var c2_lo = g0_512_lo(W[i - 30], W[i - 29]);\n var c3_hi = W[i - 32]; // i - 16\n var c3_lo = W[i - 31];\n\n W[i] = sum64_4_hi(\n c0_hi, c0_lo,\n c1_hi, c1_lo,\n c2_hi, c2_lo,\n c3_hi, c3_lo);\n W[i + 1] = sum64_4_lo(\n c0_hi, c0_lo,\n c1_hi, c1_lo,\n c2_hi, c2_lo,\n c3_hi, c3_lo);\n }\n};\n\nSHA512.prototype._update = function _update(msg, start) {\n this._prepareBlock(msg, start);\n\n var W = this.W;\n\n var ah = this.h[0];\n var al = this.h[1];\n var bh = this.h[2];\n var bl = this.h[3];\n var ch = this.h[4];\n var cl = this.h[5];\n var dh = this.h[6];\n var dl = this.h[7];\n var eh = this.h[8];\n var el = this.h[9];\n var fh = this.h[10];\n var fl = this.h[11];\n var gh = this.h[12];\n var gl = this.h[13];\n var hh = this.h[14];\n var hl = this.h[15];\n\n assert(this.k.length === W.length);\n for (var i = 0; i < W.length; i += 2) {\n var c0_hi = hh;\n var c0_lo = hl;\n var c1_hi = s1_512_hi(eh, el);\n var c1_lo = s1_512_lo(eh, el);\n var c2_hi = ch64_hi(eh, el, fh, fl, gh, gl);\n var c2_lo = ch64_lo(eh, el, fh, fl, gh, gl);\n var c3_hi = this.k[i];\n var c3_lo = this.k[i + 1];\n var c4_hi = W[i];\n var c4_lo = W[i + 1];\n\n var T1_hi = sum64_5_hi(\n c0_hi, c0_lo,\n c1_hi, c1_lo,\n c2_hi, c2_lo,\n c3_hi, c3_lo,\n c4_hi, c4_lo);\n var T1_lo = sum64_5_lo(\n c0_hi, c0_lo,\n c1_hi, c1_lo,\n c2_hi, c2_lo,\n c3_hi, c3_lo,\n c4_hi, c4_lo);\n\n c0_hi = s0_512_hi(ah, al);\n c0_lo = s0_512_lo(ah, al);\n c1_hi = maj64_hi(ah, al, bh, bl, ch, cl);\n c1_lo = maj64_lo(ah, al, bh, bl, ch, cl);\n\n var T2_hi = sum64_hi(c0_hi, c0_lo, c1_hi, c1_lo);\n var T2_lo = sum64_lo(c0_hi, c0_lo, c1_hi, c1_lo);\n\n hh = gh;\n hl = gl;\n\n gh = fh;\n gl = fl;\n\n fh = eh;\n fl = el;\n\n eh = sum64_hi(dh, dl, T1_hi, T1_lo);\n el = sum64_lo(dl, dl, T1_hi, T1_lo);\n\n dh = ch;\n dl = cl;\n\n ch = bh;\n cl = bl;\n\n bh = ah;\n bl = al;\n\n ah = sum64_hi(T1_hi, T1_lo, T2_hi, T2_lo);\n al = sum64_lo(T1_hi, T1_lo, T2_hi, T2_lo);\n }\n\n sum64(this.h, 0, ah, al);\n sum64(this.h, 2, bh, bl);\n sum64(this.h, 4, ch, cl);\n sum64(this.h, 6, dh, dl);\n sum64(this.h, 8, eh, el);\n sum64(this.h, 10, fh, fl);\n sum64(this.h, 12, gh, gl);\n sum64(this.h, 14, hh, hl);\n};\n\nSHA512.prototype._digest = function digest(enc) {\n if (enc === 'hex')\n return utils.toHex32(this.h, 'big');\n else\n return utils.split32(this.h, 'big');\n};\n\nfunction ch64_hi(xh, xl, yh, yl, zh) {\n var r = (xh & yh) ^ ((~xh) & zh);\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction ch64_lo(xh, xl, yh, yl, zh, zl) {\n var r = (xl & yl) ^ ((~xl) & zl);\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction maj64_hi(xh, xl, yh, yl, zh) {\n var r = (xh & yh) ^ (xh & zh) ^ (yh & zh);\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction maj64_lo(xh, xl, yh, yl, zh, zl) {\n var r = (xl & yl) ^ (xl & zl) ^ (yl & zl);\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction s0_512_hi(xh, xl) {\n var c0_hi = rotr64_hi(xh, xl, 28);\n var c1_hi = rotr64_hi(xl, xh, 2); // 34\n var c2_hi = rotr64_hi(xl, xh, 7); // 39\n\n var r = c0_hi ^ c1_hi ^ c2_hi;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction s0_512_lo(xh, xl) {\n var c0_lo = rotr64_lo(xh, xl, 28);\n var c1_lo = rotr64_lo(xl, xh, 2); // 34\n var c2_lo = rotr64_lo(xl, xh, 7); // 39\n\n var r = c0_lo ^ c1_lo ^ c2_lo;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction s1_512_hi(xh, xl) {\n var c0_hi = rotr64_hi(xh, xl, 14);\n var c1_hi = rotr64_hi(xh, xl, 18);\n var c2_hi = rotr64_hi(xl, xh, 9); // 41\n\n var r = c0_hi ^ c1_hi ^ c2_hi;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction s1_512_lo(xh, xl) {\n var c0_lo = rotr64_lo(xh, xl, 14);\n var c1_lo = rotr64_lo(xh, xl, 18);\n var c2_lo = rotr64_lo(xl, xh, 9); // 41\n\n var r = c0_lo ^ c1_lo ^ c2_lo;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction g0_512_hi(xh, xl) {\n var c0_hi = rotr64_hi(xh, xl, 1);\n var c1_hi = rotr64_hi(xh, xl, 8);\n var c2_hi = shr64_hi(xh, xl, 7);\n\n var r = c0_hi ^ c1_hi ^ c2_hi;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction g0_512_lo(xh, xl) {\n var c0_lo = rotr64_lo(xh, xl, 1);\n var c1_lo = rotr64_lo(xh, xl, 8);\n var c2_lo = shr64_lo(xh, xl, 7);\n\n var r = c0_lo ^ c1_lo ^ c2_lo;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction g1_512_hi(xh, xl) {\n var c0_hi = rotr64_hi(xh, xl, 19);\n var c1_hi = rotr64_hi(xl, xh, 29); // 61\n var c2_hi = shr64_hi(xh, xl, 6);\n\n var r = c0_hi ^ c1_hi ^ c2_hi;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n\nfunction g1_512_lo(xh, xl) {\n var c0_lo = rotr64_lo(xh, xl, 19);\n var c1_lo = rotr64_lo(xl, xh, 29); // 61\n var c2_lo = shr64_lo(xh, xl, 6);\n\n var r = c0_lo ^ c1_lo ^ c2_lo;\n if (r < 0)\n r += 0x100000000;\n return r;\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTkwMC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBVTtBQUM5QixhQUFhLG1CQUFPLENBQUMsSUFBVztBQUNoQyxhQUFhLG1CQUFPLENBQUMsSUFBcUI7O0FBRTFDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBLGtCQUFrQixRQUFRO0FBQzFCO0FBQ0EsU0FBUyxjQUFjO0FBQ3ZCLGdEQUFnRDtBQUNoRDtBQUNBLDRCQUE0QjtBQUM1QjtBQUNBLGtEQUFrRDtBQUNsRDtBQUNBLDRCQUE0QjtBQUM1Qjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBLGtCQUFrQixjQUFjO0FBQ2hDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxxQ0FBcUM7QUFDckMscUNBQXFDOztBQUVyQztBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxxQ0FBcUM7QUFDckMscUNBQXFDOztBQUVyQztBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBLHFDQUFxQzs7QUFFckM7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxxQ0FBcUM7O0FBRXJDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxzQ0FBc0M7QUFDdEM7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0Esc0NBQXNDO0FBQ3RDOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2hhc2guanMvbGliL2hhc2gvc2hhLzUxMi5qcz9iNTI1Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIHV0aWxzID0gcmVxdWlyZSgnLi4vdXRpbHMnKTtcbnZhciBjb21tb24gPSByZXF1aXJlKCcuLi9jb21tb24nKTtcbnZhciBhc3NlcnQgPSByZXF1aXJlKCdtaW5pbWFsaXN0aWMtYXNzZXJ0Jyk7XG5cbnZhciByb3RyNjRfaGkgPSB1dGlscy5yb3RyNjRfaGk7XG52YXIgcm90cjY0X2xvID0gdXRpbHMucm90cjY0X2xvO1xudmFyIHNocjY0X2hpID0gdXRpbHMuc2hyNjRfaGk7XG52YXIgc2hyNjRfbG8gPSB1dGlscy5zaHI2NF9sbztcbnZhciBzdW02NCA9IHV0aWxzLnN1bTY0O1xudmFyIHN1bTY0X2hpID0gdXRpbHMuc3VtNjRfaGk7XG52YXIgc3VtNjRfbG8gPSB1dGlscy5zdW02NF9sbztcbnZhciBzdW02NF80X2hpID0gdXRpbHMuc3VtNjRfNF9oaTtcbnZhciBzdW02NF80X2xvID0gdXRpbHMuc3VtNjRfNF9sbztcbnZhciBzdW02NF81X2hpID0gdXRpbHMuc3VtNjRfNV9oaTtcbnZhciBzdW02NF81X2xvID0gdXRpbHMuc3VtNjRfNV9sbztcblxudmFyIEJsb2NrSGFzaCA9IGNvbW1vbi5CbG9ja0hhc2g7XG5cbnZhciBzaGE1MTJfSyA9IFtcbiAgMHg0MjhhMmY5OCwgMHhkNzI4YWUyMiwgMHg3MTM3NDQ5MSwgMHgyM2VmNjVjZCxcbiAgMHhiNWMwZmJjZiwgMHhlYzRkM2IyZiwgMHhlOWI1ZGJhNSwgMHg4MTg5ZGJiYyxcbiAgMHgzOTU2YzI1YiwgMHhmMzQ4YjUzOCwgMHg1OWYxMTFmMSwgMHhiNjA1ZDAxOSxcbiAgMHg5MjNmODJhNCwgMHhhZjE5NGY5YiwgMHhhYjFjNWVkNSwgMHhkYTZkODExOCxcbiAgMHhkODA3YWE5OCwgMHhhMzAzMDI0MiwgMHgxMjgzNWIwMSwgMHg0NTcwNmZiZSxcbiAgMHgyNDMxODViZSwgMHg0ZWU0YjI4YywgMHg1NTBjN2RjMywgMHhkNWZmYjRlMixcbiAgMHg3MmJlNWQ3NCwgMHhmMjdiODk2ZiwgMHg4MGRlYjFmZSwgMHgzYjE2OTZiMSxcbiAgMHg5YmRjMDZhNywgMHgyNWM3MTIzNSwgMHhjMTliZjE3NCwgMHhjZjY5MjY5NCxcbiAgMHhlNDliNjljMSwgMHg5ZWYxNGFkMiwgMHhlZmJlNDc4NiwgMHgzODRmMjVlMyxcbiAgMHgwZmMxOWRjNiwgMHg4YjhjZDViNSwgMHgyNDBjYTFjYywgMHg3N2FjOWM2NSxcbiAgMHgyZGU5MmM2ZiwgMHg1OTJiMDI3NSwgMHg0YTc0ODRhYSwgMHg2ZWE2ZTQ4MyxcbiAgMHg1Y2IwYTlkYywgMHhiZDQxZmJkNCwgMHg3NmY5ODhkYSwgMHg4MzExNTNiNSxcbiAgMHg5ODNlNTE1MiwgMHhlZTY2ZGZhYiwgMHhhODMxYzY2ZCwgMHgyZGI0MzIxMCxcbiAgMHhiMDAzMjdjOCwgMHg5OGZiMjEzZiwgMHhiZjU5N2ZjNywgMHhiZWVmMGVlNCxcbiAgMHhjNmUwMGJmMywgMHgzZGE4OGZjMiwgMHhkNWE3OTE0NywgMHg5MzBhYTcyNSxcbiAgMHgwNmNhNjM1MSwgMHhlMDAzODI2ZiwgMHgxNDI5Mjk2NywgMHgwYTBlNmU3MCxcbiAgMHgyN2I3MGE4NSwgMHg0NmQyMmZmYywgMHgyZTFiMjEzOCwgMHg1YzI2YzkyNixcbiAgMHg0ZDJjNmRmYywgMHg1YWM0MmFlZCwgMHg1MzM4MGQxMywgMHg5ZDk1YjNkZixcbiAgMHg2NTBhNzM1NCwgMHg4YmFmNjNkZSwgMHg3NjZhMGFiYiwgMHgzYzc3YjJhOCxcbiAgMHg4MWMyYzkyZSwgMHg0N2VkYWVlNiwgMHg5MjcyMmM4NSwgMHgxNDgyMzUzYixcbiAgMHhhMmJmZThhMSwgMHg0Y2YxMDM2NCwgMHhhODFhNjY0YiwgMHhiYzQyMzAwMSxcbiAgMHhjMjRiOGI3MCwgMHhkMGY4OTc5MSwgMHhjNzZjNTFhMywgMHgwNjU0YmUzMCxcbiAgMHhkMTkyZTgxOSwgMHhkNmVmNTIxOCwgMHhkNjk5MDYyNCwgMHg1NTY1YTkxMCxcbiAgMHhmNDBlMzU4NSwgMHg1NzcxMjAyYSwgMHgxMDZhYTA3MCwgMHgzMmJiZDFiOCxcbiAgMHgxOWE0YzExNiwgMHhiOGQyZDBjOCwgMHgxZTM3NmMwOCwgMHg1MTQxYWI1MyxcbiAgMHgyNzQ4Nzc0YywgMHhkZjhlZWI5OSwgMHgzNGIwYmNiNSwgMHhlMTliNDhhOCxcbiAgMHgzOTFjMGNiMywgMHhjNWM5NWE2MywgMHg0ZWQ4YWE0YSwgMHhlMzQxOGFjYixcbiAgMHg1YjljY2E0ZiwgMHg3NzYzZTM3MywgMHg2ODJlNmZmMywgMHhkNmIyYjhhMyxcbiAgMHg3NDhmODJlZSwgMHg1ZGVmYjJmYywgMHg3OGE1NjM2ZiwgMHg0MzE3MmY2MCxcbiAgMHg4NGM4NzgxNCwgMHhhMWYwYWI3MiwgMHg4Y2M3MDIwOCwgMHgxYTY0MzllYyxcbiAgMHg5MGJlZmZmYSwgMHgyMzYzMWUyOCwgMHhhNDUwNmNlYiwgMHhkZTgyYmRlOSxcbiAgMHhiZWY5YTNmNywgMHhiMmM2NzkxNSwgMHhjNjcxNzhmMiwgMHhlMzcyNTMyYixcbiAgMHhjYTI3M2VjZSwgMHhlYTI2NjE5YywgMHhkMTg2YjhjNywgMHgyMWMwYzIwNyxcbiAgMHhlYWRhN2RkNiwgMHhjZGUwZWIxZSwgMHhmNTdkNGY3ZiwgMHhlZTZlZDE3OCxcbiAgMHgwNmYwNjdhYSwgMHg3MjE3NmZiYSwgMHgwYTYzN2RjNSwgMHhhMmM4OThhNixcbiAgMHgxMTNmOTgwNCwgMHhiZWY5MGRhZSwgMHgxYjcxMGIzNSwgMHgxMzFjNDcxYixcbiAgMHgyOGRiNzdmNSwgMHgyMzA0N2Q4NCwgMHgzMmNhYWI3YiwgMHg0MGM3MjQ5MyxcbiAgMHgzYzllYmUwYSwgMHgxNWM5YmViYywgMHg0MzFkNjdjNCwgMHg5YzEwMGQ0YyxcbiAgMHg0Y2M1ZDRiZSwgMHhjYjNlNDJiNiwgMHg1OTdmMjk5YywgMHhmYzY1N2UyYSxcbiAgMHg1ZmNiNmZhYiwgMHgzYWQ2ZmFlYywgMHg2YzQ0MTk4YywgMHg0YTQ3NTgxN1xuXTtcblxuZnVuY3Rpb24gU0hBNTEyKCkge1xuICBpZiAoISh0aGlzIGluc3RhbmNlb2YgU0hBNTEyKSlcbiAgICByZXR1cm4gbmV3IFNIQTUxMigpO1xuXG4gIEJsb2NrSGFzaC5jYWxsKHRoaXMpO1xuICB0aGlzLmggPSBbXG4gICAgMHg2YTA5ZTY2NywgMHhmM2JjYzkwOCxcbiAgICAweGJiNjdhZTg1LCAweDg0Y2FhNzNiLFxuICAgIDB4M2M2ZWYzNzIsIDB4ZmU5NGY4MmIsXG4gICAgMHhhNTRmZjUzYSwgMHg1ZjFkMzZmMSxcbiAgICAweDUxMGU1MjdmLCAweGFkZTY4MmQxLFxuICAgIDB4OWIwNTY4OGMsIDB4MmIzZTZjMWYsXG4gICAgMHgxZjgzZDlhYiwgMHhmYjQxYmQ2YixcbiAgICAweDViZTBjZDE5LCAweDEzN2UyMTc5IF07XG4gIHRoaXMuayA9IHNoYTUxMl9LO1xuICB0aGlzLlcgPSBuZXcgQXJyYXkoMTYwKTtcbn1cbnV0aWxzLmluaGVyaXRzKFNIQTUxMiwgQmxvY2tIYXNoKTtcbm1vZHVsZS5leHBvcnRzID0gU0hBNTEyO1xuXG5TSEE1MTIuYmxvY2tTaXplID0gMTAyNDtcblNIQTUxMi5vdXRTaXplID0gNTEyO1xuU0hBNTEyLmhtYWNTdHJlbmd0aCA9IDE5MjtcblNIQTUxMi5wYWRMZW5ndGggPSAxMjg7XG5cblNIQTUxMi5wcm90b3R5cGUuX3ByZXBhcmVCbG9jayA9IGZ1bmN0aW9uIF9wcmVwYXJlQmxvY2sobXNnLCBzdGFydCkge1xuICB2YXIgVyA9IHRoaXMuVztcblxuICAvLyAzMiB4IDMyYml0IHdvcmRzXG4gIGZvciAodmFyIGkgPSAwOyBpIDwgMzI7IGkrKylcbiAgICBXW2ldID0gbXNnW3N0YXJ0ICsgaV07XG4gIGZvciAoOyBpIDwgVy5sZW5ndGg7IGkgKz0gMikge1xuICAgIHZhciBjMF9oaSA9IGcxXzUxMl9oaShXW2kgLSA0XSwgV1tpIC0gM10pOyAgLy8gaSAtIDJcbiAgICB2YXIgYzBfbG8gPSBnMV81MTJfbG8oV1tpIC0gNF0sIFdbaSAtIDNdKTtcbiAgICB2YXIgYzFfaGkgPSBXW2kgLSAxNF07ICAvLyBpIC0gN1xuICAgIHZhciBjMV9sbyA9IFdbaSAtIDEzXTtcbiAgICB2YXIgYzJfaGkgPSBnMF81MTJfaGkoV1tpIC0gMzBdLCBXW2kgLSAyOV0pOyAgLy8gaSAtIDE1XG4gICAgdmFyIGMyX2xvID0gZzBfNTEyX2xvKFdbaSAtIDMwXSwgV1tpIC0gMjldKTtcbiAgICB2YXIgYzNfaGkgPSBXW2kgLSAzMl07ICAvLyBpIC0gMTZcbiAgICB2YXIgYzNfbG8gPSBXW2kgLSAzMV07XG5cbiAgICBXW2ldID0gc3VtNjRfNF9oaShcbiAgICAgIGMwX2hpLCBjMF9sbyxcbiAgICAgIGMxX2hpLCBjMV9sbyxcbiAgICAgIGMyX2hpLCBjMl9sbyxcbiAgICAgIGMzX2hpLCBjM19sbyk7XG4gICAgV1tpICsgMV0gPSBzdW02NF80X2xvKFxuICAgICAgYzBfaGksIGMwX2xvLFxuICAgICAgYzFfaGksIGMxX2xvLFxuICAgICAgYzJfaGksIGMyX2xvLFxuICAgICAgYzNfaGksIGMzX2xvKTtcbiAgfVxufTtcblxuU0hBNTEyLnByb3RvdHlwZS5fdXBkYXRlID0gZnVuY3Rpb24gX3VwZGF0ZShtc2csIHN0YXJ0KSB7XG4gIHRoaXMuX3ByZXBhcmVCbG9jayhtc2csIHN0YXJ0KTtcblxuICB2YXIgVyA9IHRoaXMuVztcblxuICB2YXIgYWggPSB0aGlzLmhbMF07XG4gIHZhciBhbCA9IHRoaXMuaFsxXTtcbiAgdmFyIGJoID0gdGhpcy5oWzJdO1xuICB2YXIgYmwgPSB0aGlzLmhbM107XG4gIHZhciBjaCA9IHRoaXMuaFs0XTtcbiAgdmFyIGNsID0gdGhpcy5oWzVdO1xuICB2YXIgZGggPSB0aGlzLmhbNl07XG4gIHZhciBkbCA9IHRoaXMuaFs3XTtcbiAgdmFyIGVoID0gdGhpcy5oWzhdO1xuICB2YXIgZWwgPSB0aGlzLmhbOV07XG4gIHZhciBmaCA9IHRoaXMuaFsxMF07XG4gIHZhciBmbCA9IHRoaXMuaFsxMV07XG4gIHZhciBnaCA9IHRoaXMuaFsxMl07XG4gIHZhciBnbCA9IHRoaXMuaFsxM107XG4gIHZhciBoaCA9IHRoaXMuaFsxNF07XG4gIHZhciBobCA9IHRoaXMuaFsxNV07XG5cbiAgYXNzZXJ0KHRoaXMuay5sZW5ndGggPT09IFcubGVuZ3RoKTtcbiAgZm9yICh2YXIgaSA9IDA7IGkgPCBXLmxlbmd0aDsgaSArPSAyKSB7XG4gICAgdmFyIGMwX2hpID0gaGg7XG4gICAgdmFyIGMwX2xvID0gaGw7XG4gICAgdmFyIGMxX2hpID0gczFfNTEyX2hpKGVoLCBlbCk7XG4gICAgdmFyIGMxX2xvID0gczFfNTEyX2xvKGVoLCBlbCk7XG4gICAgdmFyIGMyX2hpID0gY2g2NF9oaShlaCwgZWwsIGZoLCBmbCwgZ2gsIGdsKTtcbiAgICB2YXIgYzJfbG8gPSBjaDY0X2xvKGVoLCBlbCwgZmgsIGZsLCBnaCwgZ2wpO1xuICAgIHZhciBjM19oaSA9IHRoaXMua1tpXTtcbiAgICB2YXIgYzNfbG8gPSB0aGlzLmtbaSArIDFdO1xuICAgIHZhciBjNF9oaSA9IFdbaV07XG4gICAgdmFyIGM0X2xvID0gV1tpICsgMV07XG5cbiAgICB2YXIgVDFfaGkgPSBzdW02NF81X2hpKFxuICAgICAgYzBfaGksIGMwX2xvLFxuICAgICAgYzFfaGksIGMxX2xvLFxuICAgICAgYzJfaGksIGMyX2xvLFxuICAgICAgYzNfaGksIGMzX2xvLFxuICAgICAgYzRfaGksIGM0X2xvKTtcbiAgICB2YXIgVDFfbG8gPSBzdW02NF81X2xvKFxuICAgICAgYzBfaGksIGMwX2xvLFxuICAgICAgYzFfaGksIGMxX2xvLFxuICAgICAgYzJfaGksIGMyX2xvLFxuICAgICAgYzNfaGksIGMzX2xvLFxuICAgICAgYzRfaGksIGM0X2xvKTtcblxuICAgIGMwX2hpID0gczBfNTEyX2hpKGFoLCBhbCk7XG4gICAgYzBfbG8gPSBzMF81MTJfbG8oYWgsIGFsKTtcbiAgICBjMV9oaSA9IG1hajY0X2hpKGFoLCBhbCwgYmgsIGJsLCBjaCwgY2wpO1xuICAgIGMxX2xvID0gbWFqNjRfbG8oYWgsIGFsLCBiaCwgYmwsIGNoLCBjbCk7XG5cbiAgICB2YXIgVDJfaGkgPSBzdW02NF9oaShjMF9oaSwgYzBfbG8sIGMxX2hpLCBjMV9sbyk7XG4gICAgdmFyIFQyX2xvID0gc3VtNjRfbG8oYzBfaGksIGMwX2xvLCBjMV9oaSwgYzFfbG8pO1xuXG4gICAgaGggPSBnaDtcbiAgICBobCA9IGdsO1xuXG4gICAgZ2ggPSBmaDtcbiAgICBnbCA9IGZsO1xuXG4gICAgZmggPSBlaDtcbiAgICBmbCA9IGVsO1xuXG4gICAgZWggPSBzdW02NF9oaShkaCwgZGwsIFQxX2hpLCBUMV9sbyk7XG4gICAgZWwgPSBzdW02NF9sbyhkbCwgZGwsIFQxX2hpLCBUMV9sbyk7XG5cbiAgICBkaCA9IGNoO1xuICAgIGRsID0gY2w7XG5cbiAgICBjaCA9IGJoO1xuICAgIGNsID0gYmw7XG5cbiAgICBiaCA9IGFoO1xuICAgIGJsID0gYWw7XG5cbiAgICBhaCA9IHN1bTY0X2hpKFQxX2hpLCBUMV9sbywgVDJfaGksIFQyX2xvKTtcbiAgICBhbCA9IHN1bTY0X2xvKFQxX2hpLCBUMV9sbywgVDJfaGksIFQyX2xvKTtcbiAgfVxuXG4gIHN1bTY0KHRoaXMuaCwgMCwgYWgsIGFsKTtcbiAgc3VtNjQodGhpcy5oLCAyLCBiaCwgYmwpO1xuICBzdW02NCh0aGlzLmgsIDQsIGNoLCBjbCk7XG4gIHN1bTY0KHRoaXMuaCwgNiwgZGgsIGRsKTtcbiAgc3VtNjQodGhpcy5oLCA4LCBlaCwgZWwpO1xuICBzdW02NCh0aGlzLmgsIDEwLCBmaCwgZmwpO1xuICBzdW02NCh0aGlzLmgsIDEyLCBnaCwgZ2wpO1xuICBzdW02NCh0aGlzLmgsIDE0LCBoaCwgaGwpO1xufTtcblxuU0hBNTEyLnByb3RvdHlwZS5fZGlnZXN0ID0gZnVuY3Rpb24gZGlnZXN0KGVuYykge1xuICBpZiAoZW5jID09PSAnaGV4JylcbiAgICByZXR1cm4gdXRpbHMudG9IZXgzMih0aGlzLmgsICdiaWcnKTtcbiAgZWxzZVxuICAgIHJldHVybiB1dGlscy5zcGxpdDMyKHRoaXMuaCwgJ2JpZycpO1xufTtcblxuZnVuY3Rpb24gY2g2NF9oaSh4aCwgeGwsIHloLCB5bCwgemgpIHtcbiAgdmFyIHIgPSAoeGggJiB5aCkgXiAoKH54aCkgJiB6aCk7XG4gIGlmIChyIDwgMClcbiAgICByICs9IDB4MTAwMDAwMDAwO1xuICByZXR1cm4gcjtcbn1cblxuZnVuY3Rpb24gY2g2NF9sbyh4aCwgeGwsIHloLCB5bCwgemgsIHpsKSB7XG4gIHZhciByID0gKHhsICYgeWwpIF4gKCh+eGwpICYgemwpO1xuICBpZiAociA8IDApXG4gICAgciArPSAweDEwMDAwMDAwMDtcbiAgcmV0dXJuIHI7XG59XG5cbmZ1bmN0aW9uIG1hajY0X2hpKHhoLCB4bCwgeWgsIHlsLCB6aCkge1xuICB2YXIgciA9ICh4aCAmIHloKSBeICh4aCAmIHpoKSBeICh5aCAmIHpoKTtcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuXG5mdW5jdGlvbiBtYWo2NF9sbyh4aCwgeGwsIHloLCB5bCwgemgsIHpsKSB7XG4gIHZhciByID0gKHhsICYgeWwpIF4gKHhsICYgemwpIF4gKHlsICYgemwpO1xuICBpZiAociA8IDApXG4gICAgciArPSAweDEwMDAwMDAwMDtcbiAgcmV0dXJuIHI7XG59XG5cbmZ1bmN0aW9uIHMwXzUxMl9oaSh4aCwgeGwpIHtcbiAgdmFyIGMwX2hpID0gcm90cjY0X2hpKHhoLCB4bCwgMjgpO1xuICB2YXIgYzFfaGkgPSByb3RyNjRfaGkoeGwsIHhoLCAyKTsgIC8vIDM0XG4gIHZhciBjMl9oaSA9IHJvdHI2NF9oaSh4bCwgeGgsIDcpOyAgLy8gMzlcblxuICB2YXIgciA9IGMwX2hpIF4gYzFfaGkgXiBjMl9oaTtcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuXG5mdW5jdGlvbiBzMF81MTJfbG8oeGgsIHhsKSB7XG4gIHZhciBjMF9sbyA9IHJvdHI2NF9sbyh4aCwgeGwsIDI4KTtcbiAgdmFyIGMxX2xvID0gcm90cjY0X2xvKHhsLCB4aCwgMik7ICAvLyAzNFxuICB2YXIgYzJfbG8gPSByb3RyNjRfbG8oeGwsIHhoLCA3KTsgIC8vIDM5XG5cbiAgdmFyIHIgPSBjMF9sbyBeIGMxX2xvIF4gYzJfbG87XG4gIGlmIChyIDwgMClcbiAgICByICs9IDB4MTAwMDAwMDAwO1xuICByZXR1cm4gcjtcbn1cblxuZnVuY3Rpb24gczFfNTEyX2hpKHhoLCB4bCkge1xuICB2YXIgYzBfaGkgPSByb3RyNjRfaGkoeGgsIHhsLCAxNCk7XG4gIHZhciBjMV9oaSA9IHJvdHI2NF9oaSh4aCwgeGwsIDE4KTtcbiAgdmFyIGMyX2hpID0gcm90cjY0X2hpKHhsLCB4aCwgOSk7ICAvLyA0MVxuXG4gIHZhciByID0gYzBfaGkgXiBjMV9oaSBeIGMyX2hpO1xuICBpZiAociA8IDApXG4gICAgciArPSAweDEwMDAwMDAwMDtcbiAgcmV0dXJuIHI7XG59XG5cbmZ1bmN0aW9uIHMxXzUxMl9sbyh4aCwgeGwpIHtcbiAgdmFyIGMwX2xvID0gcm90cjY0X2xvKHhoLCB4bCwgMTQpO1xuICB2YXIgYzFfbG8gPSByb3RyNjRfbG8oeGgsIHhsLCAxOCk7XG4gIHZhciBjMl9sbyA9IHJvdHI2NF9sbyh4bCwgeGgsIDkpOyAgLy8gNDFcblxuICB2YXIgciA9IGMwX2xvIF4gYzFfbG8gXiBjMl9sbztcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuXG5mdW5jdGlvbiBnMF81MTJfaGkoeGgsIHhsKSB7XG4gIHZhciBjMF9oaSA9IHJvdHI2NF9oaSh4aCwgeGwsIDEpO1xuICB2YXIgYzFfaGkgPSByb3RyNjRfaGkoeGgsIHhsLCA4KTtcbiAgdmFyIGMyX2hpID0gc2hyNjRfaGkoeGgsIHhsLCA3KTtcblxuICB2YXIgciA9IGMwX2hpIF4gYzFfaGkgXiBjMl9oaTtcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuXG5mdW5jdGlvbiBnMF81MTJfbG8oeGgsIHhsKSB7XG4gIHZhciBjMF9sbyA9IHJvdHI2NF9sbyh4aCwgeGwsIDEpO1xuICB2YXIgYzFfbG8gPSByb3RyNjRfbG8oeGgsIHhsLCA4KTtcbiAgdmFyIGMyX2xvID0gc2hyNjRfbG8oeGgsIHhsLCA3KTtcblxuICB2YXIgciA9IGMwX2xvIF4gYzFfbG8gXiBjMl9sbztcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuXG5mdW5jdGlvbiBnMV81MTJfaGkoeGgsIHhsKSB7XG4gIHZhciBjMF9oaSA9IHJvdHI2NF9oaSh4aCwgeGwsIDE5KTtcbiAgdmFyIGMxX2hpID0gcm90cjY0X2hpKHhsLCB4aCwgMjkpOyAgLy8gNjFcbiAgdmFyIGMyX2hpID0gc2hyNjRfaGkoeGgsIHhsLCA2KTtcblxuICB2YXIgciA9IGMwX2hpIF4gYzFfaGkgXiBjMl9oaTtcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuXG5mdW5jdGlvbiBnMV81MTJfbG8oeGgsIHhsKSB7XG4gIHZhciBjMF9sbyA9IHJvdHI2NF9sbyh4aCwgeGwsIDE5KTtcbiAgdmFyIGMxX2xvID0gcm90cjY0X2xvKHhsLCB4aCwgMjkpOyAgLy8gNjFcbiAgdmFyIGMyX2xvID0gc2hyNjRfbG8oeGgsIHhsLCA2KTtcblxuICB2YXIgciA9IGMwX2xvIF4gYzFfbG8gXiBjMl9sbztcbiAgaWYgKHIgPCAwKVxuICAgIHIgKz0gMHgxMDAwMDAwMDA7XG4gIHJldHVybiByO1xufVxuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///5900\n")},7038:function(__unused_webpack_module,exports,__webpack_require__){"use strict";eval("\n\nvar utils = __webpack_require__(6436);\nvar rotr32 = utils.rotr32;\n\nfunction ft_1(s, x, y, z) {\n if (s === 0)\n return ch32(x, y, z);\n if (s === 1 || s === 3)\n return p32(x, y, z);\n if (s === 2)\n return maj32(x, y, z);\n}\nexports.ft_1 = ft_1;\n\nfunction ch32(x, y, z) {\n return (x & y) ^ ((~x) & z);\n}\nexports.ch32 = ch32;\n\nfunction maj32(x, y, z) {\n return (x & y) ^ (x & z) ^ (y & z);\n}\nexports.maj32 = maj32;\n\nfunction p32(x, y, z) {\n return x ^ y ^ z;\n}\nexports.p32 = p32;\n\nfunction s0_256(x) {\n return rotr32(x, 2) ^ rotr32(x, 13) ^ rotr32(x, 22);\n}\nexports.s0_256 = s0_256;\n\nfunction s1_256(x) {\n return rotr32(x, 6) ^ rotr32(x, 11) ^ rotr32(x, 25);\n}\nexports.s1_256 = s1_256;\n\nfunction g0_256(x) {\n return rotr32(x, 7) ^ rotr32(x, 18) ^ (x >>> 3);\n}\nexports.g0_256 = g0_256;\n\nfunction g1_256(x) {\n return rotr32(x, 17) ^ rotr32(x, 19) ^ (x >>> 10);\n}\nexports.g1_256 = g1_256;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzAzOC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixZQUFZLG1CQUFPLENBQUMsSUFBVTtBQUM5Qjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsWUFBWTs7QUFFWjtBQUNBO0FBQ0E7QUFDQSxZQUFZOztBQUVaO0FBQ0E7QUFDQTtBQUNBLGFBQWE7O0FBRWI7QUFDQTtBQUNBO0FBQ0EsV0FBVzs7QUFFWDtBQUNBO0FBQ0E7QUFDQSxjQUFjOztBQUVkO0FBQ0E7QUFDQTtBQUNBLGNBQWM7O0FBRWQ7QUFDQTtBQUNBO0FBQ0EsY0FBYzs7QUFFZDtBQUNBO0FBQ0E7QUFDQSxjQUFjIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9oYXNoLmpzL2xpYi9oYXNoL3NoYS9jb21tb24uanM/YWE1NiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciB1dGlscyA9IHJlcXVpcmUoJy4uL3V0aWxzJyk7XG52YXIgcm90cjMyID0gdXRpbHMucm90cjMyO1xuXG5mdW5jdGlvbiBmdF8xKHMsIHgsIHksIHopIHtcbiAgaWYgKHMgPT09IDApXG4gICAgcmV0dXJuIGNoMzIoeCwgeSwgeik7XG4gIGlmIChzID09PSAxIHx8IHMgPT09IDMpXG4gICAgcmV0dXJuIHAzMih4LCB5LCB6KTtcbiAgaWYgKHMgPT09IDIpXG4gICAgcmV0dXJuIG1hajMyKHgsIHksIHopO1xufVxuZXhwb3J0cy5mdF8xID0gZnRfMTtcblxuZnVuY3Rpb24gY2gzMih4LCB5LCB6KSB7XG4gIHJldHVybiAoeCAmIHkpIF4gKCh+eCkgJiB6KTtcbn1cbmV4cG9ydHMuY2gzMiA9IGNoMzI7XG5cbmZ1bmN0aW9uIG1hajMyKHgsIHksIHopIHtcbiAgcmV0dXJuICh4ICYgeSkgXiAoeCAmIHopIF4gKHkgJiB6KTtcbn1cbmV4cG9ydHMubWFqMzIgPSBtYWozMjtcblxuZnVuY3Rpb24gcDMyKHgsIHksIHopIHtcbiAgcmV0dXJuIHggXiB5IF4gejtcbn1cbmV4cG9ydHMucDMyID0gcDMyO1xuXG5mdW5jdGlvbiBzMF8yNTYoeCkge1xuICByZXR1cm4gcm90cjMyKHgsIDIpIF4gcm90cjMyKHgsIDEzKSBeIHJvdHIzMih4LCAyMik7XG59XG5leHBvcnRzLnMwXzI1NiA9IHMwXzI1NjtcblxuZnVuY3Rpb24gczFfMjU2KHgpIHtcbiAgcmV0dXJuIHJvdHIzMih4LCA2KSBeIHJvdHIzMih4LCAxMSkgXiByb3RyMzIoeCwgMjUpO1xufVxuZXhwb3J0cy5zMV8yNTYgPSBzMV8yNTY7XG5cbmZ1bmN0aW9uIGcwXzI1Nih4KSB7XG4gIHJldHVybiByb3RyMzIoeCwgNykgXiByb3RyMzIoeCwgMTgpIF4gKHggPj4+IDMpO1xufVxuZXhwb3J0cy5nMF8yNTYgPSBnMF8yNTY7XG5cbmZ1bmN0aW9uIGcxXzI1Nih4KSB7XG4gIHJldHVybiByb3RyMzIoeCwgMTcpIF4gcm90cjMyKHgsIDE5KSBeICh4ID4+PiAxMCk7XG59XG5leHBvcnRzLmcxXzI1NiA9IGcxXzI1NjtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///7038\n")},6436:function(__unused_webpack_module,exports,__webpack_require__){"use strict";eval("\n\nvar assert = __webpack_require__(9746);\nvar inherits = __webpack_require__(5717);\n\nexports.inherits = inherits;\n\nfunction isSurrogatePair(msg, i) {\n if ((msg.charCodeAt(i) & 0xFC00) !== 0xD800) {\n return false;\n }\n if (i < 0 || i + 1 >= msg.length) {\n return false;\n }\n return (msg.charCodeAt(i + 1) & 0xFC00) === 0xDC00;\n}\n\nfunction toArray(msg, enc) {\n if (Array.isArray(msg))\n return msg.slice();\n if (!msg)\n return [];\n var res = [];\n if (typeof msg === 'string') {\n if (!enc) {\n // Inspired by stringToUtf8ByteArray() in closure-library by Google\n // https://github.com/google/closure-library/blob/8598d87242af59aac233270742c8984e2b2bdbe0/closure/goog/crypt/crypt.js#L117-L143\n // Apache License 2.0\n // https://github.com/google/closure-library/blob/master/LICENSE\n var p = 0;\n for (var i = 0; i < msg.length; i++) {\n var c = msg.charCodeAt(i);\n if (c < 128) {\n res[p++] = c;\n } else if (c < 2048) {\n res[p++] = (c >> 6) | 192;\n res[p++] = (c & 63) | 128;\n } else if (isSurrogatePair(msg, i)) {\n c = 0x10000 + ((c & 0x03FF) << 10) + (msg.charCodeAt(++i) & 0x03FF);\n res[p++] = (c >> 18) | 240;\n res[p++] = ((c >> 12) & 63) | 128;\n res[p++] = ((c >> 6) & 63) | 128;\n res[p++] = (c & 63) | 128;\n } else {\n res[p++] = (c >> 12) | 224;\n res[p++] = ((c >> 6) & 63) | 128;\n res[p++] = (c & 63) | 128;\n }\n }\n } else if (enc === 'hex') {\n msg = msg.replace(/[^a-z0-9]+/ig, '');\n if (msg.length % 2 !== 0)\n msg = '0' + msg;\n for (i = 0; i < msg.length; i += 2)\n res.push(parseInt(msg[i] + msg[i + 1], 16));\n }\n } else {\n for (i = 0; i < msg.length; i++)\n res[i] = msg[i] | 0;\n }\n return res;\n}\nexports.toArray = toArray;\n\nfunction toHex(msg) {\n var res = '';\n for (var i = 0; i < msg.length; i++)\n res += zero2(msg[i].toString(16));\n return res;\n}\nexports.toHex = toHex;\n\nfunction htonl(w) {\n var res = (w >>> 24) |\n ((w >>> 8) & 0xff00) |\n ((w << 8) & 0xff0000) |\n ((w & 0xff) << 24);\n return res >>> 0;\n}\nexports.htonl = htonl;\n\nfunction toHex32(msg, endian) {\n var res = '';\n for (var i = 0; i < msg.length; i++) {\n var w = msg[i];\n if (endian === 'little')\n w = htonl(w);\n res += zero8(w.toString(16));\n }\n return res;\n}\nexports.toHex32 = toHex32;\n\nfunction zero2(word) {\n if (word.length === 1)\n return '0' + word;\n else\n return word;\n}\nexports.zero2 = zero2;\n\nfunction zero8(word) {\n if (word.length === 7)\n return '0' + word;\n else if (word.length === 6)\n return '00' + word;\n else if (word.length === 5)\n return '000' + word;\n else if (word.length === 4)\n return '0000' + word;\n else if (word.length === 3)\n return '00000' + word;\n else if (word.length === 2)\n return '000000' + word;\n else if (word.length === 1)\n return '0000000' + word;\n else\n return word;\n}\nexports.zero8 = zero8;\n\nfunction join32(msg, start, end, endian) {\n var len = end - start;\n assert(len % 4 === 0);\n var res = new Array(len / 4);\n for (var i = 0, k = start; i < res.length; i++, k += 4) {\n var w;\n if (endian === 'big')\n w = (msg[k] << 24) | (msg[k + 1] << 16) | (msg[k + 2] << 8) | msg[k + 3];\n else\n w = (msg[k + 3] << 24) | (msg[k + 2] << 16) | (msg[k + 1] << 8) | msg[k];\n res[i] = w >>> 0;\n }\n return res;\n}\nexports.join32 = join32;\n\nfunction split32(msg, endian) {\n var res = new Array(msg.length * 4);\n for (var i = 0, k = 0; i < msg.length; i++, k += 4) {\n var m = msg[i];\n if (endian === 'big') {\n res[k] = m >>> 24;\n res[k + 1] = (m >>> 16) & 0xff;\n res[k + 2] = (m >>> 8) & 0xff;\n res[k + 3] = m & 0xff;\n } else {\n res[k + 3] = m >>> 24;\n res[k + 2] = (m >>> 16) & 0xff;\n res[k + 1] = (m >>> 8) & 0xff;\n res[k] = m & 0xff;\n }\n }\n return res;\n}\nexports.split32 = split32;\n\nfunction rotr32(w, b) {\n return (w >>> b) | (w << (32 - b));\n}\nexports.rotr32 = rotr32;\n\nfunction rotl32(w, b) {\n return (w << b) | (w >>> (32 - b));\n}\nexports.rotl32 = rotl32;\n\nfunction sum32(a, b) {\n return (a + b) >>> 0;\n}\nexports.sum32 = sum32;\n\nfunction sum32_3(a, b, c) {\n return (a + b + c) >>> 0;\n}\nexports.sum32_3 = sum32_3;\n\nfunction sum32_4(a, b, c, d) {\n return (a + b + c + d) >>> 0;\n}\nexports.sum32_4 = sum32_4;\n\nfunction sum32_5(a, b, c, d, e) {\n return (a + b + c + d + e) >>> 0;\n}\nexports.sum32_5 = sum32_5;\n\nfunction sum64(buf, pos, ah, al) {\n var bh = buf[pos];\n var bl = buf[pos + 1];\n\n var lo = (al + bl) >>> 0;\n var hi = (lo < al ? 1 : 0) + ah + bh;\n buf[pos] = hi >>> 0;\n buf[pos + 1] = lo;\n}\nexports.sum64 = sum64;\n\nfunction sum64_hi(ah, al, bh, bl) {\n var lo = (al + bl) >>> 0;\n var hi = (lo < al ? 1 : 0) + ah + bh;\n return hi >>> 0;\n}\nexports.sum64_hi = sum64_hi;\n\nfunction sum64_lo(ah, al, bh, bl) {\n var lo = al + bl;\n return lo >>> 0;\n}\nexports.sum64_lo = sum64_lo;\n\nfunction sum64_4_hi(ah, al, bh, bl, ch, cl, dh, dl) {\n var carry = 0;\n var lo = al;\n lo = (lo + bl) >>> 0;\n carry += lo < al ? 1 : 0;\n lo = (lo + cl) >>> 0;\n carry += lo < cl ? 1 : 0;\n lo = (lo + dl) >>> 0;\n carry += lo < dl ? 1 : 0;\n\n var hi = ah + bh + ch + dh + carry;\n return hi >>> 0;\n}\nexports.sum64_4_hi = sum64_4_hi;\n\nfunction sum64_4_lo(ah, al, bh, bl, ch, cl, dh, dl) {\n var lo = al + bl + cl + dl;\n return lo >>> 0;\n}\nexports.sum64_4_lo = sum64_4_lo;\n\nfunction sum64_5_hi(ah, al, bh, bl, ch, cl, dh, dl, eh, el) {\n var carry = 0;\n var lo = al;\n lo = (lo + bl) >>> 0;\n carry += lo < al ? 1 : 0;\n lo = (lo + cl) >>> 0;\n carry += lo < cl ? 1 : 0;\n lo = (lo + dl) >>> 0;\n carry += lo < dl ? 1 : 0;\n lo = (lo + el) >>> 0;\n carry += lo < el ? 1 : 0;\n\n var hi = ah + bh + ch + dh + eh + carry;\n return hi >>> 0;\n}\nexports.sum64_5_hi = sum64_5_hi;\n\nfunction sum64_5_lo(ah, al, bh, bl, ch, cl, dh, dl, eh, el) {\n var lo = al + bl + cl + dl + el;\n\n return lo >>> 0;\n}\nexports.sum64_5_lo = sum64_5_lo;\n\nfunction rotr64_hi(ah, al, num) {\n var r = (al << (32 - num)) | (ah >>> num);\n return r >>> 0;\n}\nexports.rotr64_hi = rotr64_hi;\n\nfunction rotr64_lo(ah, al, num) {\n var r = (ah << (32 - num)) | (al >>> num);\n return r >>> 0;\n}\nexports.rotr64_lo = rotr64_lo;\n\nfunction shr64_hi(ah, al, num) {\n return ah >>> num;\n}\nexports.shr64_hi = shr64_hi;\n\nfunction shr64_lo(ah, al, num) {\n var r = (ah << (32 - num)) | (al >>> num);\n return r >>> 0;\n}\nexports.shr64_lo = shr64_lo;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNjQzNi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixhQUFhLG1CQUFPLENBQUMsSUFBcUI7QUFDMUMsZUFBZSxtQkFBTyxDQUFDLElBQVU7O0FBRWpDLGdCQUFnQjs7QUFFaEI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esc0JBQXNCLGdCQUFnQjtBQUN0QztBQUNBO0FBQ0E7QUFDQSxVQUFVO0FBQ1Y7QUFDQTtBQUNBLFVBQVU7QUFDVjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsVUFBVTtBQUNWO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxNQUFNO0FBQ047QUFDQTtBQUNBO0FBQ0Esa0JBQWtCLGdCQUFnQjtBQUNsQztBQUNBO0FBQ0EsSUFBSTtBQUNKLGdCQUFnQixnQkFBZ0I7QUFDaEM7QUFDQTtBQUNBO0FBQ0E7QUFDQSxlQUFlOztBQUVmO0FBQ0E7QUFDQSxrQkFBa0IsZ0JBQWdCO0FBQ2xDO0FBQ0E7QUFDQTtBQUNBLGFBQWE7O0FBRWI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxhQUFhOztBQUViO0FBQ0E7QUFDQSxrQkFBa0IsZ0JBQWdCO0FBQ2xDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsZUFBZTs7QUFFZjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxhQUFhOztBQUViO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLGFBQWE7O0FBRWI7QUFDQTtBQUNBO0FBQ0E7QUFDQSw2QkFBNkIsZ0JBQWdCO0FBQzdDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLGNBQWM7O0FBRWQ7QUFDQTtBQUNBLHlCQUF5QixnQkFBZ0I7QUFDekM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsTUFBTTtBQUNOO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxlQUFlOztBQUVmO0FBQ0E7QUFDQTtBQUNBLGNBQWM7O0FBRWQ7QUFDQTtBQUNBO0FBQ0EsY0FBYzs7QUFFZDtBQUNBO0FBQ0E7QUFDQSxhQUFhOztBQUViO0FBQ0E7QUFDQTtBQUNBLGVBQWU7O0FBRWY7QUFDQTtBQUNBO0FBQ0EsZUFBZTs7QUFFZjtBQUNBO0FBQ0E7QUFDQSxlQUFlOztBQUVmO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsYUFBYTs7QUFFYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsZ0JBQWdCOztBQUVoQjtBQUNBO0FBQ0E7QUFDQTtBQUNBLGdCQUFnQjs7QUFFaEI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBLGtCQUFrQjs7QUFFbEI7QUFDQTtBQUNBO0FBQ0E7QUFDQSxrQkFBa0I7O0FBRWxCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0Esa0JBQWtCOztBQUVsQjtBQUNBOztBQUVBO0FBQ0E7QUFDQSxrQkFBa0I7O0FBRWxCO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsaUJBQWlCOztBQUVqQjtBQUNBO0FBQ0E7QUFDQTtBQUNBLGlCQUFpQjs7QUFFakI7QUFDQTtBQUNBO0FBQ0EsZ0JBQWdCOztBQUVoQjtBQUNBO0FBQ0E7QUFDQTtBQUNBLGdCQUFnQiIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvaGFzaC5qcy9saWIvaGFzaC91dGlscy5qcz9jM2MwIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGFzc2VydCA9IHJlcXVpcmUoJ21pbmltYWxpc3RpYy1hc3NlcnQnKTtcbnZhciBpbmhlcml0cyA9IHJlcXVpcmUoJ2luaGVyaXRzJyk7XG5cbmV4cG9ydHMuaW5oZXJpdHMgPSBpbmhlcml0cztcblxuZnVuY3Rpb24gaXNTdXJyb2dhdGVQYWlyKG1zZywgaSkge1xuICBpZiAoKG1zZy5jaGFyQ29kZUF0KGkpICYgMHhGQzAwKSAhPT0gMHhEODAwKSB7XG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG4gIGlmIChpIDwgMCB8fCBpICsgMSA+PSBtc2cubGVuZ3RoKSB7XG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG4gIHJldHVybiAobXNnLmNoYXJDb2RlQXQoaSArIDEpICYgMHhGQzAwKSA9PT0gMHhEQzAwO1xufVxuXG5mdW5jdGlvbiB0b0FycmF5KG1zZywgZW5jKSB7XG4gIGlmIChBcnJheS5pc0FycmF5KG1zZykpXG4gICAgcmV0dXJuIG1zZy5zbGljZSgpO1xuICBpZiAoIW1zZylcbiAgICByZXR1cm4gW107XG4gIHZhciByZXMgPSBbXTtcbiAgaWYgKHR5cGVvZiBtc2cgPT09ICdzdHJpbmcnKSB7XG4gICAgaWYgKCFlbmMpIHtcbiAgICAgIC8vIEluc3BpcmVkIGJ5IHN0cmluZ1RvVXRmOEJ5dGVBcnJheSgpIGluIGNsb3N1cmUtbGlicmFyeSBieSBHb29nbGVcbiAgICAgIC8vIGh0dHBzOi8vZ2l0aHViLmNvbS9nb29nbGUvY2xvc3VyZS1saWJyYXJ5L2Jsb2IvODU5OGQ4NzI0MmFmNTlhYWMyMzMyNzA3NDJjODk4NGUyYjJiZGJlMC9jbG9zdXJlL2dvb2cvY3J5cHQvY3J5cHQuanMjTDExNy1MMTQzXG4gICAgICAvLyBBcGFjaGUgTGljZW5zZSAyLjBcbiAgICAgIC8vIGh0dHBzOi8vZ2l0aHViLmNvbS9nb29nbGUvY2xvc3VyZS1saWJyYXJ5L2Jsb2IvbWFzdGVyL0xJQ0VOU0VcbiAgICAgIHZhciBwID0gMDtcbiAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwgbXNnLmxlbmd0aDsgaSsrKSB7XG4gICAgICAgIHZhciBjID0gbXNnLmNoYXJDb2RlQXQoaSk7XG4gICAgICAgIGlmIChjIDwgMTI4KSB7XG4gICAgICAgICAgcmVzW3ArK10gPSBjO1xuICAgICAgICB9IGVsc2UgaWYgKGMgPCAyMDQ4KSB7XG4gICAgICAgICAgcmVzW3ArK10gPSAoYyA+PiA2KSB8IDE5MjtcbiAgICAgICAgICByZXNbcCsrXSA9IChjICYgNjMpIHwgMTI4O1xuICAgICAgICB9IGVsc2UgaWYgKGlzU3Vycm9nYXRlUGFpcihtc2csIGkpKSB7XG4gICAgICAgICAgYyA9IDB4MTAwMDAgKyAoKGMgJiAweDAzRkYpIDw8IDEwKSArIChtc2cuY2hhckNvZGVBdCgrK2kpICYgMHgwM0ZGKTtcbiAgICAgICAgICByZXNbcCsrXSA9IChjID4+IDE4KSB8IDI0MDtcbiAgICAgICAgICByZXNbcCsrXSA9ICgoYyA+PiAxMikgJiA2MykgfCAxMjg7XG4gICAgICAgICAgcmVzW3ArK10gPSAoKGMgPj4gNikgJiA2MykgfCAxMjg7XG4gICAgICAgICAgcmVzW3ArK10gPSAoYyAmIDYzKSB8IDEyODtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICByZXNbcCsrXSA9IChjID4+IDEyKSB8IDIyNDtcbiAgICAgICAgICByZXNbcCsrXSA9ICgoYyA+PiA2KSAmIDYzKSB8IDEyODtcbiAgICAgICAgICByZXNbcCsrXSA9IChjICYgNjMpIHwgMTI4O1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfSBlbHNlIGlmIChlbmMgPT09ICdoZXgnKSB7XG4gICAgICBtc2cgPSBtc2cucmVwbGFjZSgvW15hLXowLTldKy9pZywgJycpO1xuICAgICAgaWYgKG1zZy5sZW5ndGggJSAyICE9PSAwKVxuICAgICAgICBtc2cgPSAnMCcgKyBtc2c7XG4gICAgICBmb3IgKGkgPSAwOyBpIDwgbXNnLmxlbmd0aDsgaSArPSAyKVxuICAgICAgICByZXMucHVzaChwYXJzZUludChtc2dbaV0gKyBtc2dbaSArIDFdLCAxNikpO1xuICAgIH1cbiAgfSBlbHNlIHtcbiAgICBmb3IgKGkgPSAwOyBpIDwgbXNnLmxlbmd0aDsgaSsrKVxuICAgICAgcmVzW2ldID0gbXNnW2ldIHwgMDtcbiAgfVxuICByZXR1cm4gcmVzO1xufVxuZXhwb3J0cy50b0FycmF5ID0gdG9BcnJheTtcblxuZnVuY3Rpb24gdG9IZXgobXNnKSB7XG4gIHZhciByZXMgPSAnJztcbiAgZm9yICh2YXIgaSA9IDA7IGkgPCBtc2cubGVuZ3RoOyBpKyspXG4gICAgcmVzICs9IHplcm8yKG1zZ1tpXS50b1N0cmluZygxNikpO1xuICByZXR1cm4gcmVzO1xufVxuZXhwb3J0cy50b0hleCA9IHRvSGV4O1xuXG5mdW5jdGlvbiBodG9ubCh3KSB7XG4gIHZhciByZXMgPSAodyA+Pj4gMjQpIHxcbiAgICAgICAgICAgICgodyA+Pj4gOCkgJiAweGZmMDApIHxcbiAgICAgICAgICAgICgodyA8PCA4KSAmIDB4ZmYwMDAwKSB8XG4gICAgICAgICAgICAoKHcgJiAweGZmKSA8PCAyNCk7XG4gIHJldHVybiByZXMgPj4+IDA7XG59XG5leHBvcnRzLmh0b25sID0gaHRvbmw7XG5cbmZ1bmN0aW9uIHRvSGV4MzIobXNnLCBlbmRpYW4pIHtcbiAgdmFyIHJlcyA9ICcnO1xuICBmb3IgKHZhciBpID0gMDsgaSA8IG1zZy5sZW5ndGg7IGkrKykge1xuICAgIHZhciB3ID0gbXNnW2ldO1xuICAgIGlmIChlbmRpYW4gPT09ICdsaXR0bGUnKVxuICAgICAgdyA9IGh0b25sKHcpO1xuICAgIHJlcyArPSB6ZXJvOCh3LnRvU3RyaW5nKDE2KSk7XG4gIH1cbiAgcmV0dXJuIHJlcztcbn1cbmV4cG9ydHMudG9IZXgzMiA9IHRvSGV4MzI7XG5cbmZ1bmN0aW9uIHplcm8yKHdvcmQpIHtcbiAgaWYgKHdvcmQubGVuZ3RoID09PSAxKVxuICAgIHJldHVybiAnMCcgKyB3b3JkO1xuICBlbHNlXG4gICAgcmV0dXJuIHdvcmQ7XG59XG5leHBvcnRzLnplcm8yID0gemVybzI7XG5cbmZ1bmN0aW9uIHplcm84KHdvcmQpIHtcbiAgaWYgKHdvcmQubGVuZ3RoID09PSA3KVxuICAgIHJldHVybiAnMCcgKyB3b3JkO1xuICBlbHNlIGlmICh3b3JkLmxlbmd0aCA9PT0gNilcbiAgICByZXR1cm4gJzAwJyArIHdvcmQ7XG4gIGVsc2UgaWYgKHdvcmQubGVuZ3RoID09PSA1KVxuICAgIHJldHVybiAnMDAwJyArIHdvcmQ7XG4gIGVsc2UgaWYgKHdvcmQubGVuZ3RoID09PSA0KVxuICAgIHJldHVybiAnMDAwMCcgKyB3b3JkO1xuICBlbHNlIGlmICh3b3JkLmxlbmd0aCA9PT0gMylcbiAgICByZXR1cm4gJzAwMDAwJyArIHdvcmQ7XG4gIGVsc2UgaWYgKHdvcmQubGVuZ3RoID09PSAyKVxuICAgIHJldHVybiAnMDAwMDAwJyArIHdvcmQ7XG4gIGVsc2UgaWYgKHdvcmQubGVuZ3RoID09PSAxKVxuICAgIHJldHVybiAnMDAwMDAwMCcgKyB3b3JkO1xuICBlbHNlXG4gICAgcmV0dXJuIHdvcmQ7XG59XG5leHBvcnRzLnplcm84ID0gemVybzg7XG5cbmZ1bmN0aW9uIGpvaW4zMihtc2csIHN0YXJ0LCBlbmQsIGVuZGlhbikge1xuICB2YXIgbGVuID0gZW5kIC0gc3RhcnQ7XG4gIGFzc2VydChsZW4gJSA0ID09PSAwKTtcbiAgdmFyIHJlcyA9IG5ldyBBcnJheShsZW4gLyA0KTtcbiAgZm9yICh2YXIgaSA9IDAsIGsgPSBzdGFydDsgaSA8IHJlcy5sZW5ndGg7IGkrKywgayArPSA0KSB7XG4gICAgdmFyIHc7XG4gICAgaWYgKGVuZGlhbiA9PT0gJ2JpZycpXG4gICAgICB3ID0gKG1zZ1trXSA8PCAyNCkgfCAobXNnW2sgKyAxXSA8PCAxNikgfCAobXNnW2sgKyAyXSA8PCA4KSB8IG1zZ1trICsgM107XG4gICAgZWxzZVxuICAgICAgdyA9IChtc2dbayArIDNdIDw8IDI0KSB8IChtc2dbayArIDJdIDw8IDE2KSB8IChtc2dbayArIDFdIDw8IDgpIHwgbXNnW2tdO1xuICAgIHJlc1tpXSA9IHcgPj4+IDA7XG4gIH1cbiAgcmV0dXJuIHJlcztcbn1cbmV4cG9ydHMuam9pbjMyID0gam9pbjMyO1xuXG5mdW5jdGlvbiBzcGxpdDMyKG1zZywgZW5kaWFuKSB7XG4gIHZhciByZXMgPSBuZXcgQXJyYXkobXNnLmxlbmd0aCAqIDQpO1xuICBmb3IgKHZhciBpID0gMCwgayA9IDA7IGkgPCBtc2cubGVuZ3RoOyBpKyssIGsgKz0gNCkge1xuICAgIHZhciBtID0gbXNnW2ldO1xuICAgIGlmIChlbmRpYW4gPT09ICdiaWcnKSB7XG4gICAgICByZXNba10gPSBtID4+PiAyNDtcbiAgICAgIHJlc1trICsgMV0gPSAobSA+Pj4gMTYpICYgMHhmZjtcbiAgICAgIHJlc1trICsgMl0gPSAobSA+Pj4gOCkgJiAweGZmO1xuICAgICAgcmVzW2sgKyAzXSA9IG0gJiAweGZmO1xuICAgIH0gZWxzZSB7XG4gICAgICByZXNbayArIDNdID0gbSA+Pj4gMjQ7XG4gICAgICByZXNbayArIDJdID0gKG0gPj4+IDE2KSAmIDB4ZmY7XG4gICAgICByZXNbayArIDFdID0gKG0gPj4+IDgpICYgMHhmZjtcbiAgICAgIHJlc1trXSA9IG0gJiAweGZmO1xuICAgIH1cbiAgfVxuICByZXR1cm4gcmVzO1xufVxuZXhwb3J0cy5zcGxpdDMyID0gc3BsaXQzMjtcblxuZnVuY3Rpb24gcm90cjMyKHcsIGIpIHtcbiAgcmV0dXJuICh3ID4+PiBiKSB8ICh3IDw8ICgzMiAtIGIpKTtcbn1cbmV4cG9ydHMucm90cjMyID0gcm90cjMyO1xuXG5mdW5jdGlvbiByb3RsMzIodywgYikge1xuICByZXR1cm4gKHcgPDwgYikgfCAodyA+Pj4gKDMyIC0gYikpO1xufVxuZXhwb3J0cy5yb3RsMzIgPSByb3RsMzI7XG5cbmZ1bmN0aW9uIHN1bTMyKGEsIGIpIHtcbiAgcmV0dXJuIChhICsgYikgPj4+IDA7XG59XG5leHBvcnRzLnN1bTMyID0gc3VtMzI7XG5cbmZ1bmN0aW9uIHN1bTMyXzMoYSwgYiwgYykge1xuICByZXR1cm4gKGEgKyBiICsgYykgPj4+IDA7XG59XG5leHBvcnRzLnN1bTMyXzMgPSBzdW0zMl8zO1xuXG5mdW5jdGlvbiBzdW0zMl80KGEsIGIsIGMsIGQpIHtcbiAgcmV0dXJuIChhICsgYiArIGMgKyBkKSA+Pj4gMDtcbn1cbmV4cG9ydHMuc3VtMzJfNCA9IHN1bTMyXzQ7XG5cbmZ1bmN0aW9uIHN1bTMyXzUoYSwgYiwgYywgZCwgZSkge1xuICByZXR1cm4gKGEgKyBiICsgYyArIGQgKyBlKSA+Pj4gMDtcbn1cbmV4cG9ydHMuc3VtMzJfNSA9IHN1bTMyXzU7XG5cbmZ1bmN0aW9uIHN1bTY0KGJ1ZiwgcG9zLCBhaCwgYWwpIHtcbiAgdmFyIGJoID0gYnVmW3Bvc107XG4gIHZhciBibCA9IGJ1Zltwb3MgKyAxXTtcblxuICB2YXIgbG8gPSAoYWwgKyBibCkgPj4+IDA7XG4gIHZhciBoaSA9IChsbyA8IGFsID8gMSA6IDApICsgYWggKyBiaDtcbiAgYnVmW3Bvc10gPSBoaSA+Pj4gMDtcbiAgYnVmW3BvcyArIDFdID0gbG87XG59XG5leHBvcnRzLnN1bTY0ID0gc3VtNjQ7XG5cbmZ1bmN0aW9uIHN1bTY0X2hpKGFoLCBhbCwgYmgsIGJsKSB7XG4gIHZhciBsbyA9IChhbCArIGJsKSA+Pj4gMDtcbiAgdmFyIGhpID0gKGxvIDwgYWwgPyAxIDogMCkgKyBhaCArIGJoO1xuICByZXR1cm4gaGkgPj4+IDA7XG59XG5leHBvcnRzLnN1bTY0X2hpID0gc3VtNjRfaGk7XG5cbmZ1bmN0aW9uIHN1bTY0X2xvKGFoLCBhbCwgYmgsIGJsKSB7XG4gIHZhciBsbyA9IGFsICsgYmw7XG4gIHJldHVybiBsbyA+Pj4gMDtcbn1cbmV4cG9ydHMuc3VtNjRfbG8gPSBzdW02NF9sbztcblxuZnVuY3Rpb24gc3VtNjRfNF9oaShhaCwgYWwsIGJoLCBibCwgY2gsIGNsLCBkaCwgZGwpIHtcbiAgdmFyIGNhcnJ5ID0gMDtcbiAgdmFyIGxvID0gYWw7XG4gIGxvID0gKGxvICsgYmwpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGFsID8gMSA6IDA7XG4gIGxvID0gKGxvICsgY2wpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGNsID8gMSA6IDA7XG4gIGxvID0gKGxvICsgZGwpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGRsID8gMSA6IDA7XG5cbiAgdmFyIGhpID0gYWggKyBiaCArIGNoICsgZGggKyBjYXJyeTtcbiAgcmV0dXJuIGhpID4+PiAwO1xufVxuZXhwb3J0cy5zdW02NF80X2hpID0gc3VtNjRfNF9oaTtcblxuZnVuY3Rpb24gc3VtNjRfNF9sbyhhaCwgYWwsIGJoLCBibCwgY2gsIGNsLCBkaCwgZGwpIHtcbiAgdmFyIGxvID0gYWwgKyBibCArIGNsICsgZGw7XG4gIHJldHVybiBsbyA+Pj4gMDtcbn1cbmV4cG9ydHMuc3VtNjRfNF9sbyA9IHN1bTY0XzRfbG87XG5cbmZ1bmN0aW9uIHN1bTY0XzVfaGkoYWgsIGFsLCBiaCwgYmwsIGNoLCBjbCwgZGgsIGRsLCBlaCwgZWwpIHtcbiAgdmFyIGNhcnJ5ID0gMDtcbiAgdmFyIGxvID0gYWw7XG4gIGxvID0gKGxvICsgYmwpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGFsID8gMSA6IDA7XG4gIGxvID0gKGxvICsgY2wpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGNsID8gMSA6IDA7XG4gIGxvID0gKGxvICsgZGwpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGRsID8gMSA6IDA7XG4gIGxvID0gKGxvICsgZWwpID4+PiAwO1xuICBjYXJyeSArPSBsbyA8IGVsID8gMSA6IDA7XG5cbiAgdmFyIGhpID0gYWggKyBiaCArIGNoICsgZGggKyBlaCArIGNhcnJ5O1xuICByZXR1cm4gaGkgPj4+IDA7XG59XG5leHBvcnRzLnN1bTY0XzVfaGkgPSBzdW02NF81X2hpO1xuXG5mdW5jdGlvbiBzdW02NF81X2xvKGFoLCBhbCwgYmgsIGJsLCBjaCwgY2wsIGRoLCBkbCwgZWgsIGVsKSB7XG4gIHZhciBsbyA9IGFsICsgYmwgKyBjbCArIGRsICsgZWw7XG5cbiAgcmV0dXJuIGxvID4+PiAwO1xufVxuZXhwb3J0cy5zdW02NF81X2xvID0gc3VtNjRfNV9sbztcblxuZnVuY3Rpb24gcm90cjY0X2hpKGFoLCBhbCwgbnVtKSB7XG4gIHZhciByID0gKGFsIDw8ICgzMiAtIG51bSkpIHwgKGFoID4+PiBudW0pO1xuICByZXR1cm4gciA+Pj4gMDtcbn1cbmV4cG9ydHMucm90cjY0X2hpID0gcm90cjY0X2hpO1xuXG5mdW5jdGlvbiByb3RyNjRfbG8oYWgsIGFsLCBudW0pIHtcbiAgdmFyIHIgPSAoYWggPDwgKDMyIC0gbnVtKSkgfCAoYWwgPj4+IG51bSk7XG4gIHJldHVybiByID4+PiAwO1xufVxuZXhwb3J0cy5yb3RyNjRfbG8gPSByb3RyNjRfbG87XG5cbmZ1bmN0aW9uIHNocjY0X2hpKGFoLCBhbCwgbnVtKSB7XG4gIHJldHVybiBhaCA+Pj4gbnVtO1xufVxuZXhwb3J0cy5zaHI2NF9oaSA9IHNocjY0X2hpO1xuXG5mdW5jdGlvbiBzaHI2NF9sbyhhaCwgYWwsIG51bSkge1xuICB2YXIgciA9IChhaCA8PCAoMzIgLSBudW0pKSB8IChhbCA+Pj4gbnVtKTtcbiAgcmV0dXJuIHIgPj4+IDA7XG59XG5leHBvcnRzLnNocjY0X2xvID0gc2hyNjRfbG87XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///6436\n")},5717:function(module){eval("if (typeof Object.create === 'function') {\n // implementation from standard node.js 'util' module\n module.exports = function inherits(ctor, superCtor) {\n if (superCtor) {\n ctor.super_ = superCtor\n ctor.prototype = Object.create(superCtor.prototype, {\n constructor: {\n value: ctor,\n enumerable: false,\n writable: true,\n configurable: true\n }\n })\n }\n };\n} else {\n // old school shim for old browsers\n module.exports = function inherits(ctor, superCtor) {\n if (superCtor) {\n ctor.super_ = superCtor\n var TempCtor = function () {}\n TempCtor.prototype = superCtor.prototype\n ctor.prototype = new TempCtor()\n ctor.prototype.constructor = ctor\n }\n }\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTcxNy5qcyIsIm1hcHBpbmdzIjoiQUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxPQUFPO0FBQ1A7QUFDQTtBQUNBLEVBQUU7QUFDRjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9pbmhlcml0cy9pbmhlcml0c19icm93c2VyLmpzPzNmYjUiXSwic291cmNlc0NvbnRlbnQiOlsiaWYgKHR5cGVvZiBPYmplY3QuY3JlYXRlID09PSAnZnVuY3Rpb24nKSB7XG4gIC8vIGltcGxlbWVudGF0aW9uIGZyb20gc3RhbmRhcmQgbm9kZS5qcyAndXRpbCcgbW9kdWxlXG4gIG1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaW5oZXJpdHMoY3Rvciwgc3VwZXJDdG9yKSB7XG4gICAgaWYgKHN1cGVyQ3Rvcikge1xuICAgICAgY3Rvci5zdXBlcl8gPSBzdXBlckN0b3JcbiAgICAgIGN0b3IucHJvdG90eXBlID0gT2JqZWN0LmNyZWF0ZShzdXBlckN0b3IucHJvdG90eXBlLCB7XG4gICAgICAgIGNvbnN0cnVjdG9yOiB7XG4gICAgICAgICAgdmFsdWU6IGN0b3IsXG4gICAgICAgICAgZW51bWVyYWJsZTogZmFsc2UsXG4gICAgICAgICAgd3JpdGFibGU6IHRydWUsXG4gICAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgICAgIH1cbiAgICAgIH0pXG4gICAgfVxuICB9O1xufSBlbHNlIHtcbiAgLy8gb2xkIHNjaG9vbCBzaGltIGZvciBvbGQgYnJvd3NlcnNcbiAgbW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBpbmhlcml0cyhjdG9yLCBzdXBlckN0b3IpIHtcbiAgICBpZiAoc3VwZXJDdG9yKSB7XG4gICAgICBjdG9yLnN1cGVyXyA9IHN1cGVyQ3RvclxuICAgICAgdmFyIFRlbXBDdG9yID0gZnVuY3Rpb24gKCkge31cbiAgICAgIFRlbXBDdG9yLnByb3RvdHlwZSA9IHN1cGVyQ3Rvci5wcm90b3R5cGVcbiAgICAgIGN0b3IucHJvdG90eXBlID0gbmV3IFRlbXBDdG9yKClcbiAgICAgIGN0b3IucHJvdG90eXBlLmNvbnN0cnVjdG9yID0gY3RvclxuICAgIH1cbiAgfVxufVxuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///5717\n")},9496:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\nvar has = __webpack_require__(7642);\nvar channel = __webpack_require__(7478)();\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar SLOT = {\n\tassert: function (O, slot) {\n\t\tif (!O || (typeof O !== 'object' && typeof O !== 'function')) {\n\t\t\tthrow new $TypeError('`O` is not an object');\n\t\t}\n\t\tif (typeof slot !== 'string') {\n\t\t\tthrow new $TypeError('`slot` must be a string');\n\t\t}\n\t\tchannel.assert(O);\n\t},\n\tget: function (O, slot) {\n\t\tif (!O || (typeof O !== 'object' && typeof O !== 'function')) {\n\t\t\tthrow new $TypeError('`O` is not an object');\n\t\t}\n\t\tif (typeof slot !== 'string') {\n\t\t\tthrow new $TypeError('`slot` must be a string');\n\t\t}\n\t\tvar slots = channel.get(O);\n\t\treturn slots && slots['$' + slot];\n\t},\n\thas: function (O, slot) {\n\t\tif (!O || (typeof O !== 'object' && typeof O !== 'function')) {\n\t\t\tthrow new $TypeError('`O` is not an object');\n\t\t}\n\t\tif (typeof slot !== 'string') {\n\t\t\tthrow new $TypeError('`slot` must be a string');\n\t\t}\n\t\tvar slots = channel.get(O);\n\t\treturn !!slots && has(slots, '$' + slot);\n\t},\n\tset: function (O, slot, V) {\n\t\tif (!O || (typeof O !== 'object' && typeof O !== 'function')) {\n\t\t\tthrow new $TypeError('`O` is not an object');\n\t\t}\n\t\tif (typeof slot !== 'string') {\n\t\t\tthrow new $TypeError('`slot` must be a string');\n\t\t}\n\t\tvar slots = channel.get(O);\n\t\tif (!slots) {\n\t\t\tslots = {};\n\t\t\tchannel.set(O, slots);\n\t\t}\n\t\tslots['$' + slot] = V;\n\t}\n};\n\nif (Object.freeze) {\n\tObject.freeze(SLOT);\n}\n\nmodule.exports = SLOT;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTQ5Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlO0FBQzFDLFVBQVUsbUJBQU8sQ0FBQyxJQUFLO0FBQ3ZCLGNBQWMsbUJBQU8sQ0FBQyxJQUFjOztBQUVwQzs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxFQUFFO0FBQ0Y7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsRUFBRTtBQUNGO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEVBQUU7QUFDRjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9pbnRlcm5hbC1zbG90L2luZGV4LmpzPzY1ZWIiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xudmFyIGhhcyA9IHJlcXVpcmUoJ2hhcycpO1xudmFyIGNoYW5uZWwgPSByZXF1aXJlKCdzaWRlLWNoYW5uZWwnKSgpO1xuXG52YXIgJFR5cGVFcnJvciA9IEdldEludHJpbnNpYygnJVR5cGVFcnJvciUnKTtcblxudmFyIFNMT1QgPSB7XG5cdGFzc2VydDogZnVuY3Rpb24gKE8sIHNsb3QpIHtcblx0XHRpZiAoIU8gfHwgKHR5cGVvZiBPICE9PSAnb2JqZWN0JyAmJiB0eXBlb2YgTyAhPT0gJ2Z1bmN0aW9uJykpIHtcblx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdgT2AgaXMgbm90IGFuIG9iamVjdCcpO1xuXHRcdH1cblx0XHRpZiAodHlwZW9mIHNsb3QgIT09ICdzdHJpbmcnKSB7XG5cdFx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignYHNsb3RgIG11c3QgYmUgYSBzdHJpbmcnKTtcblx0XHR9XG5cdFx0Y2hhbm5lbC5hc3NlcnQoTyk7XG5cdH0sXG5cdGdldDogZnVuY3Rpb24gKE8sIHNsb3QpIHtcblx0XHRpZiAoIU8gfHwgKHR5cGVvZiBPICE9PSAnb2JqZWN0JyAmJiB0eXBlb2YgTyAhPT0gJ2Z1bmN0aW9uJykpIHtcblx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdgT2AgaXMgbm90IGFuIG9iamVjdCcpO1xuXHRcdH1cblx0XHRpZiAodHlwZW9mIHNsb3QgIT09ICdzdHJpbmcnKSB7XG5cdFx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignYHNsb3RgIG11c3QgYmUgYSBzdHJpbmcnKTtcblx0XHR9XG5cdFx0dmFyIHNsb3RzID0gY2hhbm5lbC5nZXQoTyk7XG5cdFx0cmV0dXJuIHNsb3RzICYmIHNsb3RzWyckJyArIHNsb3RdO1xuXHR9LFxuXHRoYXM6IGZ1bmN0aW9uIChPLCBzbG90KSB7XG5cdFx0aWYgKCFPIHx8ICh0eXBlb2YgTyAhPT0gJ29iamVjdCcgJiYgdHlwZW9mIE8gIT09ICdmdW5jdGlvbicpKSB7XG5cdFx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignYE9gIGlzIG5vdCBhbiBvYmplY3QnKTtcblx0XHR9XG5cdFx0aWYgKHR5cGVvZiBzbG90ICE9PSAnc3RyaW5nJykge1xuXHRcdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ2BzbG90YCBtdXN0IGJlIGEgc3RyaW5nJyk7XG5cdFx0fVxuXHRcdHZhciBzbG90cyA9IGNoYW5uZWwuZ2V0KE8pO1xuXHRcdHJldHVybiAhIXNsb3RzICYmIGhhcyhzbG90cywgJyQnICsgc2xvdCk7XG5cdH0sXG5cdHNldDogZnVuY3Rpb24gKE8sIHNsb3QsIFYpIHtcblx0XHRpZiAoIU8gfHwgKHR5cGVvZiBPICE9PSAnb2JqZWN0JyAmJiB0eXBlb2YgTyAhPT0gJ2Z1bmN0aW9uJykpIHtcblx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdgT2AgaXMgbm90IGFuIG9iamVjdCcpO1xuXHRcdH1cblx0XHRpZiAodHlwZW9mIHNsb3QgIT09ICdzdHJpbmcnKSB7XG5cdFx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignYHNsb3RgIG11c3QgYmUgYSBzdHJpbmcnKTtcblx0XHR9XG5cdFx0dmFyIHNsb3RzID0gY2hhbm5lbC5nZXQoTyk7XG5cdFx0aWYgKCFzbG90cykge1xuXHRcdFx0c2xvdHMgPSB7fTtcblx0XHRcdGNoYW5uZWwuc2V0KE8sIHNsb3RzKTtcblx0XHR9XG5cdFx0c2xvdHNbJyQnICsgc2xvdF0gPSBWO1xuXHR9XG59O1xuXG5pZiAoT2JqZWN0LmZyZWV6ZSkge1xuXHRPYmplY3QuZnJlZXplKFNMT1QpO1xufVxuXG5tb2R1bGUuZXhwb3J0cyA9IFNMT1Q7XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///9496\n")},5320:function(module){"use strict";eval("\n\nvar fnToStr = Function.prototype.toString;\nvar reflectApply = typeof Reflect === 'object' && Reflect !== null && Reflect.apply;\nvar badArrayLike;\nvar isCallableMarker;\nif (typeof reflectApply === 'function' && typeof Object.defineProperty === 'function') {\n\ttry {\n\t\tbadArrayLike = Object.defineProperty({}, 'length', {\n\t\t\tget: function () {\n\t\t\t\tthrow isCallableMarker;\n\t\t\t}\n\t\t});\n\t\tisCallableMarker = {};\n\t\t// eslint-disable-next-line no-throw-literal\n\t\treflectApply(function () { throw 42; }, null, badArrayLike);\n\t} catch (_) {\n\t\tif (_ !== isCallableMarker) {\n\t\t\treflectApply = null;\n\t\t}\n\t}\n} else {\n\treflectApply = null;\n}\n\nvar constructorRegex = /^\\s*class\\b/;\nvar isES6ClassFn = function isES6ClassFunction(value) {\n\ttry {\n\t\tvar fnStr = fnToStr.call(value);\n\t\treturn constructorRegex.test(fnStr);\n\t} catch (e) {\n\t\treturn false; // not a function\n\t}\n};\n\nvar tryFunctionObject = function tryFunctionToStr(value) {\n\ttry {\n\t\tif (isES6ClassFn(value)) { return false; }\n\t\tfnToStr.call(value);\n\t\treturn true;\n\t} catch (e) {\n\t\treturn false;\n\t}\n};\nvar toStr = Object.prototype.toString;\nvar fnClass = '[object Function]';\nvar genClass = '[object GeneratorFunction]';\nvar hasToStringTag = typeof Symbol === 'function' && !!Symbol.toStringTag; // better: use `has-tostringtag`\n/* globals document: false */\nvar documentDotAll = typeof document === 'object' && typeof document.all === 'undefined' && document.all !== undefined ? document.all : {};\n\nmodule.exports = reflectApply\n\t? function isCallable(value) {\n\t\tif (value === documentDotAll) { return true; }\n\t\tif (!value) { return false; }\n\t\tif (typeof value !== 'function' && typeof value !== 'object') { return false; }\n\t\tif (typeof value === 'function' && !value.prototype) { return true; }\n\t\ttry {\n\t\t\treflectApply(value, null, badArrayLike);\n\t\t} catch (e) {\n\t\t\tif (e !== isCallableMarker) { return false; }\n\t\t}\n\t\treturn !isES6ClassFn(value);\n\t}\n\t: function isCallable(value) {\n\t\tif (value === documentDotAll) { return true; }\n\t\tif (!value) { return false; }\n\t\tif (typeof value !== 'function' && typeof value !== 'object') { return false; }\n\t\tif (typeof value === 'function' && !value.prototype) { return true; }\n\t\tif (hasToStringTag) { return tryFunctionObject(value); }\n\t\tif (isES6ClassFn(value)) { return false; }\n\t\tvar strClass = toStr.call(value);\n\t\treturn strClass === fnClass || strClass === genClass;\n\t};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTMyMC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSx5Q0FBeUM7QUFDekM7QUFDQTtBQUNBO0FBQ0EsR0FBRztBQUNIO0FBQ0E7QUFDQSw2QkFBNkIsV0FBVztBQUN4QyxHQUFHO0FBQ0g7QUFDQTtBQUNBO0FBQ0E7QUFDQSxFQUFFO0FBQ0Y7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsR0FBRztBQUNILGdCQUFnQjtBQUNoQjtBQUNBOztBQUVBO0FBQ0E7QUFDQSw2QkFBNkI7QUFDN0I7QUFDQTtBQUNBLEdBQUc7QUFDSDtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSwyRUFBMkU7QUFDM0U7QUFDQTs7QUFFQTtBQUNBO0FBQ0Esa0NBQWtDO0FBQ2xDLGdCQUFnQjtBQUNoQixrRUFBa0U7QUFDbEUseURBQXlEO0FBQ3pEO0FBQ0E7QUFDQSxJQUFJO0FBQ0osaUNBQWlDO0FBQ2pDO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esa0NBQWtDO0FBQ2xDLGdCQUFnQjtBQUNoQixrRUFBa0U7QUFDbEUseURBQXlEO0FBQ3pELHdCQUF3QjtBQUN4Qiw2QkFBNkI7QUFDN0I7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9pcy1jYWxsYWJsZS9pbmRleC5qcz8yMWQwIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGZuVG9TdHIgPSBGdW5jdGlvbi5wcm90b3R5cGUudG9TdHJpbmc7XG52YXIgcmVmbGVjdEFwcGx5ID0gdHlwZW9mIFJlZmxlY3QgPT09ICdvYmplY3QnICYmIFJlZmxlY3QgIT09IG51bGwgJiYgUmVmbGVjdC5hcHBseTtcbnZhciBiYWRBcnJheUxpa2U7XG52YXIgaXNDYWxsYWJsZU1hcmtlcjtcbmlmICh0eXBlb2YgcmVmbGVjdEFwcGx5ID09PSAnZnVuY3Rpb24nICYmIHR5cGVvZiBPYmplY3QuZGVmaW5lUHJvcGVydHkgPT09ICdmdW5jdGlvbicpIHtcblx0dHJ5IHtcblx0XHRiYWRBcnJheUxpa2UgPSBPYmplY3QuZGVmaW5lUHJvcGVydHkoe30sICdsZW5ndGgnLCB7XG5cdFx0XHRnZXQ6IGZ1bmN0aW9uICgpIHtcblx0XHRcdFx0dGhyb3cgaXNDYWxsYWJsZU1hcmtlcjtcblx0XHRcdH1cblx0XHR9KTtcblx0XHRpc0NhbGxhYmxlTWFya2VyID0ge307XG5cdFx0Ly8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLXRocm93LWxpdGVyYWxcblx0XHRyZWZsZWN0QXBwbHkoZnVuY3Rpb24gKCkgeyB0aHJvdyA0MjsgfSwgbnVsbCwgYmFkQXJyYXlMaWtlKTtcblx0fSBjYXRjaCAoXykge1xuXHRcdGlmIChfICE9PSBpc0NhbGxhYmxlTWFya2VyKSB7XG5cdFx0XHRyZWZsZWN0QXBwbHkgPSBudWxsO1xuXHRcdH1cblx0fVxufSBlbHNlIHtcblx0cmVmbGVjdEFwcGx5ID0gbnVsbDtcbn1cblxudmFyIGNvbnN0cnVjdG9yUmVnZXggPSAvXlxccypjbGFzc1xcYi87XG52YXIgaXNFUzZDbGFzc0ZuID0gZnVuY3Rpb24gaXNFUzZDbGFzc0Z1bmN0aW9uKHZhbHVlKSB7XG5cdHRyeSB7XG5cdFx0dmFyIGZuU3RyID0gZm5Ub1N0ci5jYWxsKHZhbHVlKTtcblx0XHRyZXR1cm4gY29uc3RydWN0b3JSZWdleC50ZXN0KGZuU3RyKTtcblx0fSBjYXRjaCAoZSkge1xuXHRcdHJldHVybiBmYWxzZTsgLy8gbm90IGEgZnVuY3Rpb25cblx0fVxufTtcblxudmFyIHRyeUZ1bmN0aW9uT2JqZWN0ID0gZnVuY3Rpb24gdHJ5RnVuY3Rpb25Ub1N0cih2YWx1ZSkge1xuXHR0cnkge1xuXHRcdGlmIChpc0VTNkNsYXNzRm4odmFsdWUpKSB7IHJldHVybiBmYWxzZTsgfVxuXHRcdGZuVG9TdHIuY2FsbCh2YWx1ZSk7XG5cdFx0cmV0dXJuIHRydWU7XG5cdH0gY2F0Y2ggKGUpIHtcblx0XHRyZXR1cm4gZmFsc2U7XG5cdH1cbn07XG52YXIgdG9TdHIgPSBPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nO1xudmFyIGZuQ2xhc3MgPSAnW29iamVjdCBGdW5jdGlvbl0nO1xudmFyIGdlbkNsYXNzID0gJ1tvYmplY3QgR2VuZXJhdG9yRnVuY3Rpb25dJztcbnZhciBoYXNUb1N0cmluZ1RhZyA9IHR5cGVvZiBTeW1ib2wgPT09ICdmdW5jdGlvbicgJiYgISFTeW1ib2wudG9TdHJpbmdUYWc7IC8vIGJldHRlcjogdXNlIGBoYXMtdG9zdHJpbmd0YWdgXG4vKiBnbG9iYWxzIGRvY3VtZW50OiBmYWxzZSAqL1xudmFyIGRvY3VtZW50RG90QWxsID0gdHlwZW9mIGRvY3VtZW50ID09PSAnb2JqZWN0JyAmJiB0eXBlb2YgZG9jdW1lbnQuYWxsID09PSAndW5kZWZpbmVkJyAmJiBkb2N1bWVudC5hbGwgIT09IHVuZGVmaW5lZCA/IGRvY3VtZW50LmFsbCA6IHt9O1xuXG5tb2R1bGUuZXhwb3J0cyA9IHJlZmxlY3RBcHBseVxuXHQ/IGZ1bmN0aW9uIGlzQ2FsbGFibGUodmFsdWUpIHtcblx0XHRpZiAodmFsdWUgPT09IGRvY3VtZW50RG90QWxsKSB7IHJldHVybiB0cnVlOyB9XG5cdFx0aWYgKCF2YWx1ZSkgeyByZXR1cm4gZmFsc2U7IH1cblx0XHRpZiAodHlwZW9mIHZhbHVlICE9PSAnZnVuY3Rpb24nICYmIHR5cGVvZiB2YWx1ZSAhPT0gJ29iamVjdCcpIHsgcmV0dXJuIGZhbHNlOyB9XG5cdFx0aWYgKHR5cGVvZiB2YWx1ZSA9PT0gJ2Z1bmN0aW9uJyAmJiAhdmFsdWUucHJvdG90eXBlKSB7IHJldHVybiB0cnVlOyB9XG5cdFx0dHJ5IHtcblx0XHRcdHJlZmxlY3RBcHBseSh2YWx1ZSwgbnVsbCwgYmFkQXJyYXlMaWtlKTtcblx0XHR9IGNhdGNoIChlKSB7XG5cdFx0XHRpZiAoZSAhPT0gaXNDYWxsYWJsZU1hcmtlcikgeyByZXR1cm4gZmFsc2U7IH1cblx0XHR9XG5cdFx0cmV0dXJuICFpc0VTNkNsYXNzRm4odmFsdWUpO1xuXHR9XG5cdDogZnVuY3Rpb24gaXNDYWxsYWJsZSh2YWx1ZSkge1xuXHRcdGlmICh2YWx1ZSA9PT0gZG9jdW1lbnREb3RBbGwpIHsgcmV0dXJuIHRydWU7IH1cblx0XHRpZiAoIXZhbHVlKSB7IHJldHVybiBmYWxzZTsgfVxuXHRcdGlmICh0eXBlb2YgdmFsdWUgIT09ICdmdW5jdGlvbicgJiYgdHlwZW9mIHZhbHVlICE9PSAnb2JqZWN0JykgeyByZXR1cm4gZmFsc2U7IH1cblx0XHRpZiAodHlwZW9mIHZhbHVlID09PSAnZnVuY3Rpb24nICYmICF2YWx1ZS5wcm90b3R5cGUpIHsgcmV0dXJuIHRydWU7IH1cblx0XHRpZiAoaGFzVG9TdHJpbmdUYWcpIHsgcmV0dXJuIHRyeUZ1bmN0aW9uT2JqZWN0KHZhbHVlKTsgfVxuXHRcdGlmIChpc0VTNkNsYXNzRm4odmFsdWUpKSB7IHJldHVybiBmYWxzZTsgfVxuXHRcdHZhciBzdHJDbGFzcyA9IHRvU3RyLmNhbGwodmFsdWUpO1xuXHRcdHJldHVybiBzdHJDbGFzcyA9PT0gZm5DbGFzcyB8fCBzdHJDbGFzcyA9PT0gZ2VuQ2xhc3M7XG5cdH07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///5320\n")},8923:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar getDay = Date.prototype.getDay;\nvar tryDateObject = function tryDateGetDayCall(value) {\n\ttry {\n\t\tgetDay.call(value);\n\t\treturn true;\n\t} catch (e) {\n\t\treturn false;\n\t}\n};\n\nvar toStr = Object.prototype.toString;\nvar dateClass = '[object Date]';\nvar hasToStringTag = __webpack_require__(6410)();\n\nmodule.exports = function isDateObject(value) {\n\tif (typeof value !== 'object' || value === null) {\n\t\treturn false;\n\t}\n\treturn hasToStringTag ? tryDateObject(value) : toStr.call(value) === dateClass;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODkyMy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsR0FBRztBQUNIO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0EscUJBQXFCLG1CQUFPLENBQUMsSUFBdUI7O0FBRXBEO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvaXMtZGF0ZS1vYmplY3QvaW5kZXguanM/MGU2NSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBnZXREYXkgPSBEYXRlLnByb3RvdHlwZS5nZXREYXk7XG52YXIgdHJ5RGF0ZU9iamVjdCA9IGZ1bmN0aW9uIHRyeURhdGVHZXREYXlDYWxsKHZhbHVlKSB7XG5cdHRyeSB7XG5cdFx0Z2V0RGF5LmNhbGwodmFsdWUpO1xuXHRcdHJldHVybiB0cnVlO1xuXHR9IGNhdGNoIChlKSB7XG5cdFx0cmV0dXJuIGZhbHNlO1xuXHR9XG59O1xuXG52YXIgdG9TdHIgPSBPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nO1xudmFyIGRhdGVDbGFzcyA9ICdbb2JqZWN0IERhdGVdJztcbnZhciBoYXNUb1N0cmluZ1RhZyA9IHJlcXVpcmUoJ2hhcy10b3N0cmluZ3RhZy9zaGFtcycpKCk7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaXNEYXRlT2JqZWN0KHZhbHVlKSB7XG5cdGlmICh0eXBlb2YgdmFsdWUgIT09ICdvYmplY3QnIHx8IHZhbHVlID09PSBudWxsKSB7XG5cdFx0cmV0dXJuIGZhbHNlO1xuXHR9XG5cdHJldHVybiBoYXNUb1N0cmluZ1RhZyA/IHRyeURhdGVPYmplY3QodmFsdWUpIDogdG9TdHIuY2FsbCh2YWx1ZSkgPT09IGRhdGVDbGFzcztcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///8923\n")},8420:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar callBound = __webpack_require__(1924);\nvar hasToStringTag = __webpack_require__(6410)();\nvar has;\nvar $exec;\nvar isRegexMarker;\nvar badStringifier;\n\nif (hasToStringTag) {\n\thas = callBound('Object.prototype.hasOwnProperty');\n\t$exec = callBound('RegExp.prototype.exec');\n\tisRegexMarker = {};\n\n\tvar throwRegexMarker = function () {\n\t\tthrow isRegexMarker;\n\t};\n\tbadStringifier = {\n\t\ttoString: throwRegexMarker,\n\t\tvalueOf: throwRegexMarker\n\t};\n\n\tif (typeof Symbol.toPrimitive === 'symbol') {\n\t\tbadStringifier[Symbol.toPrimitive] = throwRegexMarker;\n\t}\n}\n\nvar $toString = callBound('Object.prototype.toString');\nvar gOPD = Object.getOwnPropertyDescriptor;\nvar regexClass = '[object RegExp]';\n\nmodule.exports = hasToStringTag\n\t// eslint-disable-next-line consistent-return\n\t? function isRegex(value) {\n\t\tif (!value || typeof value !== 'object') {\n\t\t\treturn false;\n\t\t}\n\n\t\tvar descriptor = gOPD(value, 'lastIndex');\n\t\tvar hasLastIndexDataProperty = descriptor && has(descriptor, 'value');\n\t\tif (!hasLastIndexDataProperty) {\n\t\t\treturn false;\n\t\t}\n\n\t\ttry {\n\t\t\t$exec(value, badStringifier);\n\t\t} catch (e) {\n\t\t\treturn e === isRegexMarker;\n\t\t}\n\t}\n\t: function isRegex(value) {\n\t\t// In older browsers, typeof regex incorrectly returns 'function'\n\t\tif (!value || (typeof value !== 'object' && typeof value !== 'function')) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $toString(value) === regexClass;\n\t};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODQyMC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixnQkFBZ0IsbUJBQU8sQ0FBQyxJQUFxQjtBQUM3QyxxQkFBcUIsbUJBQU8sQ0FBQyxJQUF1QjtBQUNwRDtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBLElBQUk7QUFDSjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2lzLXJlZ2V4L2luZGV4LmpzP2Q4ZDgiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgY2FsbEJvdW5kID0gcmVxdWlyZSgnY2FsbC1iaW5kL2NhbGxCb3VuZCcpO1xudmFyIGhhc1RvU3RyaW5nVGFnID0gcmVxdWlyZSgnaGFzLXRvc3RyaW5ndGFnL3NoYW1zJykoKTtcbnZhciBoYXM7XG52YXIgJGV4ZWM7XG52YXIgaXNSZWdleE1hcmtlcjtcbnZhciBiYWRTdHJpbmdpZmllcjtcblxuaWYgKGhhc1RvU3RyaW5nVGFnKSB7XG5cdGhhcyA9IGNhbGxCb3VuZCgnT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eScpO1xuXHQkZXhlYyA9IGNhbGxCb3VuZCgnUmVnRXhwLnByb3RvdHlwZS5leGVjJyk7XG5cdGlzUmVnZXhNYXJrZXIgPSB7fTtcblxuXHR2YXIgdGhyb3dSZWdleE1hcmtlciA9IGZ1bmN0aW9uICgpIHtcblx0XHR0aHJvdyBpc1JlZ2V4TWFya2VyO1xuXHR9O1xuXHRiYWRTdHJpbmdpZmllciA9IHtcblx0XHR0b1N0cmluZzogdGhyb3dSZWdleE1hcmtlcixcblx0XHR2YWx1ZU9mOiB0aHJvd1JlZ2V4TWFya2VyXG5cdH07XG5cblx0aWYgKHR5cGVvZiBTeW1ib2wudG9QcmltaXRpdmUgPT09ICdzeW1ib2wnKSB7XG5cdFx0YmFkU3RyaW5naWZpZXJbU3ltYm9sLnRvUHJpbWl0aXZlXSA9IHRocm93UmVnZXhNYXJrZXI7XG5cdH1cbn1cblxudmFyICR0b1N0cmluZyA9IGNhbGxCb3VuZCgnT2JqZWN0LnByb3RvdHlwZS50b1N0cmluZycpO1xudmFyIGdPUEQgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yO1xudmFyIHJlZ2V4Q2xhc3MgPSAnW29iamVjdCBSZWdFeHBdJztcblxubW9kdWxlLmV4cG9ydHMgPSBoYXNUb1N0cmluZ1RhZ1xuXHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgY29uc2lzdGVudC1yZXR1cm5cblx0PyBmdW5jdGlvbiBpc1JlZ2V4KHZhbHVlKSB7XG5cdFx0aWYgKCF2YWx1ZSB8fCB0eXBlb2YgdmFsdWUgIT09ICdvYmplY3QnKSB7XG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0fVxuXG5cdFx0dmFyIGRlc2NyaXB0b3IgPSBnT1BEKHZhbHVlLCAnbGFzdEluZGV4Jyk7XG5cdFx0dmFyIGhhc0xhc3RJbmRleERhdGFQcm9wZXJ0eSA9IGRlc2NyaXB0b3IgJiYgaGFzKGRlc2NyaXB0b3IsICd2YWx1ZScpO1xuXHRcdGlmICghaGFzTGFzdEluZGV4RGF0YVByb3BlcnR5KSB7XG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0fVxuXG5cdFx0dHJ5IHtcblx0XHRcdCRleGVjKHZhbHVlLCBiYWRTdHJpbmdpZmllcik7XG5cdFx0fSBjYXRjaCAoZSkge1xuXHRcdFx0cmV0dXJuIGUgPT09IGlzUmVnZXhNYXJrZXI7XG5cdFx0fVxuXHR9XG5cdDogZnVuY3Rpb24gaXNSZWdleCh2YWx1ZSkge1xuXHRcdC8vIEluIG9sZGVyIGJyb3dzZXJzLCB0eXBlb2YgcmVnZXggaW5jb3JyZWN0bHkgcmV0dXJucyAnZnVuY3Rpb24nXG5cdFx0aWYgKCF2YWx1ZSB8fCAodHlwZW9mIHZhbHVlICE9PSAnb2JqZWN0JyAmJiB0eXBlb2YgdmFsdWUgIT09ICdmdW5jdGlvbicpKSB7XG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0fVxuXG5cdFx0cmV0dXJuICR0b1N0cmluZyh2YWx1ZSkgPT09IHJlZ2V4Q2xhc3M7XG5cdH07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///8420\n")},2636:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar toStr = Object.prototype.toString;\nvar hasSymbols = __webpack_require__(1405)();\n\nif (hasSymbols) {\n\tvar symToStr = Symbol.prototype.toString;\n\tvar symStringRegex = /^Symbol\\(.*\\)$/;\n\tvar isSymbolObject = function isRealSymbolObject(value) {\n\t\tif (typeof value.valueOf() !== 'symbol') {\n\t\t\treturn false;\n\t\t}\n\t\treturn symStringRegex.test(symToStr.call(value));\n\t};\n\n\tmodule.exports = function isSymbol(value) {\n\t\tif (typeof value === 'symbol') {\n\t\t\treturn true;\n\t\t}\n\t\tif (toStr.call(value) !== '[object Symbol]') {\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\treturn isSymbolObject(value);\n\t\t} catch (e) {\n\t\t\treturn false;\n\t\t}\n\t};\n} else {\n\n\tmodule.exports = function isSymbol(value) {\n\t\t// this environment does not support Symbols.\n\t\treturn false && 0;\n\t};\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjYzNi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBLGlCQUFpQixtQkFBTyxDQUFDLElBQWE7O0FBRXRDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxJQUFJO0FBQ0o7QUFDQTtBQUNBO0FBQ0EsRUFBRTs7QUFFRjtBQUNBO0FBQ0EsU0FBUyxNQUFLLElBQUksQ0FBSztBQUN2QjtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9pcy1zeW1ib2wvaW5kZXguanM/ZmVjNSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciB0b1N0ciA9IE9iamVjdC5wcm90b3R5cGUudG9TdHJpbmc7XG52YXIgaGFzU3ltYm9scyA9IHJlcXVpcmUoJ2hhcy1zeW1ib2xzJykoKTtcblxuaWYgKGhhc1N5bWJvbHMpIHtcblx0dmFyIHN5bVRvU3RyID0gU3ltYm9sLnByb3RvdHlwZS50b1N0cmluZztcblx0dmFyIHN5bVN0cmluZ1JlZ2V4ID0gL15TeW1ib2xcXCguKlxcKSQvO1xuXHR2YXIgaXNTeW1ib2xPYmplY3QgPSBmdW5jdGlvbiBpc1JlYWxTeW1ib2xPYmplY3QodmFsdWUpIHtcblx0XHRpZiAodHlwZW9mIHZhbHVlLnZhbHVlT2YoKSAhPT0gJ3N5bWJvbCcpIHtcblx0XHRcdHJldHVybiBmYWxzZTtcblx0XHR9XG5cdFx0cmV0dXJuIHN5bVN0cmluZ1JlZ2V4LnRlc3Qoc3ltVG9TdHIuY2FsbCh2YWx1ZSkpO1xuXHR9O1xuXG5cdG1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaXNTeW1ib2wodmFsdWUpIHtcblx0XHRpZiAodHlwZW9mIHZhbHVlID09PSAnc3ltYm9sJykge1xuXHRcdFx0cmV0dXJuIHRydWU7XG5cdFx0fVxuXHRcdGlmICh0b1N0ci5jYWxsKHZhbHVlKSAhPT0gJ1tvYmplY3QgU3ltYm9sXScpIHtcblx0XHRcdHJldHVybiBmYWxzZTtcblx0XHR9XG5cdFx0dHJ5IHtcblx0XHRcdHJldHVybiBpc1N5bWJvbE9iamVjdCh2YWx1ZSk7XG5cdFx0fSBjYXRjaCAoZSkge1xuXHRcdFx0cmV0dXJuIGZhbHNlO1xuXHRcdH1cblx0fTtcbn0gZWxzZSB7XG5cblx0bW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBpc1N5bWJvbCh2YWx1ZSkge1xuXHRcdC8vIHRoaXMgZW52aXJvbm1lbnQgZG9lcyBub3Qgc3VwcG9ydCBTeW1ib2xzLlxuXHRcdHJldHVybiBmYWxzZSAmJiB2YWx1ZTtcblx0fTtcbn1cbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2636\n")},9746:function(module){eval("module.exports = assert;\n\nfunction assert(val, msg) {\n if (!val)\n throw new Error(msg || 'Assertion failed');\n}\n\nassert.equal = function assertEqual(l, r, msg) {\n if (l != r)\n throw new Error(msg || ('Assertion failed: ' + l + ' != ' + r));\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTc0Ni5qcyIsIm1hcHBpbmdzIjoiQUFBQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvbWluaW1hbGlzdGljLWFzc2VydC9pbmRleC5qcz9kYTNlIl0sInNvdXJjZXNDb250ZW50IjpbIm1vZHVsZS5leHBvcnRzID0gYXNzZXJ0O1xuXG5mdW5jdGlvbiBhc3NlcnQodmFsLCBtc2cpIHtcbiAgaWYgKCF2YWwpXG4gICAgdGhyb3cgbmV3IEVycm9yKG1zZyB8fCAnQXNzZXJ0aW9uIGZhaWxlZCcpO1xufVxuXG5hc3NlcnQuZXF1YWwgPSBmdW5jdGlvbiBhc3NlcnRFcXVhbChsLCByLCBtc2cpIHtcbiAgaWYgKGwgIT0gcilcbiAgICB0aHJvdyBuZXcgRXJyb3IobXNnIHx8ICgnQXNzZXJ0aW9uIGZhaWxlZDogJyArIGwgKyAnICE9ICcgKyByKSk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///9746\n")},631:function(module,__unused_webpack_exports,__webpack_require__){eval("var hasMap = typeof Map === 'function' && Map.prototype;\nvar mapSizeDescriptor = Object.getOwnPropertyDescriptor && hasMap ? Object.getOwnPropertyDescriptor(Map.prototype, 'size') : null;\nvar mapSize = hasMap && mapSizeDescriptor && typeof mapSizeDescriptor.get === 'function' ? mapSizeDescriptor.get : null;\nvar mapForEach = hasMap && Map.prototype.forEach;\nvar hasSet = typeof Set === 'function' && Set.prototype;\nvar setSizeDescriptor = Object.getOwnPropertyDescriptor && hasSet ? Object.getOwnPropertyDescriptor(Set.prototype, 'size') : null;\nvar setSize = hasSet && setSizeDescriptor && typeof setSizeDescriptor.get === 'function' ? setSizeDescriptor.get : null;\nvar setForEach = hasSet && Set.prototype.forEach;\nvar hasWeakMap = typeof WeakMap === 'function' && WeakMap.prototype;\nvar weakMapHas = hasWeakMap ? WeakMap.prototype.has : null;\nvar hasWeakSet = typeof WeakSet === 'function' && WeakSet.prototype;\nvar weakSetHas = hasWeakSet ? WeakSet.prototype.has : null;\nvar hasWeakRef = typeof WeakRef === 'function' && WeakRef.prototype;\nvar weakRefDeref = hasWeakRef ? WeakRef.prototype.deref : null;\nvar booleanValueOf = Boolean.prototype.valueOf;\nvar objectToString = Object.prototype.toString;\nvar functionToString = Function.prototype.toString;\nvar match = String.prototype.match;\nvar bigIntValueOf = typeof BigInt === 'function' ? BigInt.prototype.valueOf : null;\nvar gOPS = Object.getOwnPropertySymbols;\nvar symToString = typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' ? Symbol.prototype.toString : null;\nvar hasShammedSymbols = typeof Symbol === 'function' && typeof Symbol.iterator === 'object';\nvar isEnumerable = Object.prototype.propertyIsEnumerable;\n\nvar gPO = (typeof Reflect === 'function' ? Reflect.getPrototypeOf : Object.getPrototypeOf) || (\n [].__proto__ === Array.prototype // eslint-disable-line no-proto\n ? function (O) {\n return O.__proto__; // eslint-disable-line no-proto\n }\n : null\n);\n\nvar inspectCustom = __webpack_require__(4654).custom;\nvar inspectSymbol = inspectCustom && isSymbol(inspectCustom) ? inspectCustom : null;\nvar toStringTag = typeof Symbol === 'function' && typeof Symbol.toStringTag !== 'undefined' ? Symbol.toStringTag : null;\n\nmodule.exports = function inspect_(obj, options, depth, seen) {\n var opts = options || {};\n\n if (has(opts, 'quoteStyle') && (opts.quoteStyle !== 'single' && opts.quoteStyle !== 'double')) {\n throw new TypeError('option \"quoteStyle\" must be \"single\" or \"double\"');\n }\n if (\n has(opts, 'maxStringLength') && (typeof opts.maxStringLength === 'number'\n ? opts.maxStringLength < 0 && opts.maxStringLength !== Infinity\n : opts.maxStringLength !== null\n )\n ) {\n throw new TypeError('option \"maxStringLength\", if provided, must be a positive integer, Infinity, or `null`');\n }\n var customInspect = has(opts, 'customInspect') ? opts.customInspect : true;\n if (typeof customInspect !== 'boolean' && customInspect !== 'symbol') {\n throw new TypeError('option \"customInspect\", if provided, must be `true`, `false`, or `\\'symbol\\'`');\n }\n\n if (\n has(opts, 'indent')\n && opts.indent !== null\n && opts.indent !== '\\t'\n && !(parseInt(opts.indent, 10) === opts.indent && opts.indent > 0)\n ) {\n throw new TypeError('options \"indent\" must be \"\\\\t\", an integer > 0, or `null`');\n }\n\n if (typeof obj === 'undefined') {\n return 'undefined';\n }\n if (obj === null) {\n return 'null';\n }\n if (typeof obj === 'boolean') {\n return obj ? 'true' : 'false';\n }\n\n if (typeof obj === 'string') {\n return inspectString(obj, opts);\n }\n if (typeof obj === 'number') {\n if (obj === 0) {\n return Infinity / obj > 0 ? '0' : '-0';\n }\n return String(obj);\n }\n if (typeof obj === 'bigint') {\n return String(obj) + 'n';\n }\n\n var maxDepth = typeof opts.depth === 'undefined' ? 5 : opts.depth;\n if (typeof depth === 'undefined') { depth = 0; }\n if (depth >= maxDepth && maxDepth > 0 && typeof obj === 'object') {\n return isArray(obj) ? '[Array]' : '[Object]';\n }\n\n var indent = getIndent(opts, depth);\n\n if (typeof seen === 'undefined') {\n seen = [];\n } else if (indexOf(seen, obj) >= 0) {\n return '[Circular]';\n }\n\n function inspect(value, from, noIndent) {\n if (from) {\n seen = seen.slice();\n seen.push(from);\n }\n if (noIndent) {\n var newOpts = {\n depth: opts.depth\n };\n if (has(opts, 'quoteStyle')) {\n newOpts.quoteStyle = opts.quoteStyle;\n }\n return inspect_(value, newOpts, depth + 1, seen);\n }\n return inspect_(value, opts, depth + 1, seen);\n }\n\n if (typeof obj === 'function') {\n var name = nameOf(obj);\n var keys = arrObjKeys(obj, inspect);\n return '[Function' + (name ? ': ' + name : ' (anonymous)') + ']' + (keys.length > 0 ? ' { ' + keys.join(', ') + ' }' : '');\n }\n if (isSymbol(obj)) {\n var symString = hasShammedSymbols ? String(obj).replace(/^(Symbol\\(.*\\))_[^)]*$/, '$1') : symToString.call(obj);\n return typeof obj === 'object' && !hasShammedSymbols ? markBoxed(symString) : symString;\n }\n if (isElement(obj)) {\n var s = '<' + String(obj.nodeName).toLowerCase();\n var attrs = obj.attributes || [];\n for (var i = 0; i < attrs.length; i++) {\n s += ' ' + attrs[i].name + '=' + wrapQuotes(quote(attrs[i].value), 'double', opts);\n }\n s += '>';\n if (obj.childNodes && obj.childNodes.length) { s += '...'; }\n s += '</' + String(obj.nodeName).toLowerCase() + '>';\n return s;\n }\n if (isArray(obj)) {\n if (obj.length === 0) { return '[]'; }\n var xs = arrObjKeys(obj, inspect);\n if (indent && !singleLineValues(xs)) {\n return '[' + indentedJoin(xs, indent) + ']';\n }\n return '[ ' + xs.join(', ') + ' ]';\n }\n if (isError(obj)) {\n var parts = arrObjKeys(obj, inspect);\n if (parts.length === 0) { return '[' + String(obj) + ']'; }\n return '{ [' + String(obj) + '] ' + parts.join(', ') + ' }';\n }\n if (typeof obj === 'object' && customInspect) {\n if (inspectSymbol && typeof obj[inspectSymbol] === 'function') {\n return obj[inspectSymbol]();\n } else if (customInspect !== 'symbol' && typeof obj.inspect === 'function') {\n return obj.inspect();\n }\n }\n if (isMap(obj)) {\n var mapParts = [];\n mapForEach.call(obj, function (value, key) {\n mapParts.push(inspect(key, obj, true) + ' => ' + inspect(value, obj));\n });\n return collectionOf('Map', mapSize.call(obj), mapParts, indent);\n }\n if (isSet(obj)) {\n var setParts = [];\n setForEach.call(obj, function (value) {\n setParts.push(inspect(value, obj));\n });\n return collectionOf('Set', setSize.call(obj), setParts, indent);\n }\n if (isWeakMap(obj)) {\n return weakCollectionOf('WeakMap');\n }\n if (isWeakSet(obj)) {\n return weakCollectionOf('WeakSet');\n }\n if (isWeakRef(obj)) {\n return weakCollectionOf('WeakRef');\n }\n if (isNumber(obj)) {\n return markBoxed(inspect(Number(obj)));\n }\n if (isBigInt(obj)) {\n return markBoxed(inspect(bigIntValueOf.call(obj)));\n }\n if (isBoolean(obj)) {\n return markBoxed(booleanValueOf.call(obj));\n }\n if (isString(obj)) {\n return markBoxed(inspect(String(obj)));\n }\n if (!isDate(obj) && !isRegExp(obj)) {\n var ys = arrObjKeys(obj, inspect);\n var isPlainObject = gPO ? gPO(obj) === Object.prototype : obj instanceof Object || obj.constructor === Object;\n var protoTag = obj instanceof Object ? '' : 'null prototype';\n var stringTag = !isPlainObject && toStringTag && Object(obj) === obj && toStringTag in obj ? toStr(obj).slice(8, -1) : protoTag ? 'Object' : '';\n var constructorTag = isPlainObject || typeof obj.constructor !== 'function' ? '' : obj.constructor.name ? obj.constructor.name + ' ' : '';\n var tag = constructorTag + (stringTag || protoTag ? '[' + [].concat(stringTag || [], protoTag || []).join(': ') + '] ' : '');\n if (ys.length === 0) { return tag + '{}'; }\n if (indent) {\n return tag + '{' + indentedJoin(ys, indent) + '}';\n }\n return tag + '{ ' + ys.join(', ') + ' }';\n }\n return String(obj);\n};\n\nfunction wrapQuotes(s, defaultStyle, opts) {\n var quoteChar = (opts.quoteStyle || defaultStyle) === 'double' ? '\"' : \"'\";\n return quoteChar + s + quoteChar;\n}\n\nfunction quote(s) {\n return String(s).replace(/\"/g, '"');\n}\n\nfunction isArray(obj) { return toStr(obj) === '[object Array]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\nfunction isDate(obj) { return toStr(obj) === '[object Date]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\nfunction isRegExp(obj) { return toStr(obj) === '[object RegExp]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\nfunction isError(obj) { return toStr(obj) === '[object Error]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\nfunction isString(obj) { return toStr(obj) === '[object String]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\nfunction isNumber(obj) { return toStr(obj) === '[object Number]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\nfunction isBoolean(obj) { return toStr(obj) === '[object Boolean]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); }\n\n// Symbol and BigInt do have Symbol.toStringTag by spec, so that can't be used to eliminate false positives\nfunction isSymbol(obj) {\n if (hasShammedSymbols) {\n return obj && typeof obj === 'object' && obj instanceof Symbol;\n }\n if (typeof obj === 'symbol') {\n return true;\n }\n if (!obj || typeof obj !== 'object' || !symToString) {\n return false;\n }\n try {\n symToString.call(obj);\n return true;\n } catch (e) {}\n return false;\n}\n\nfunction isBigInt(obj) {\n if (!obj || typeof obj !== 'object' || !bigIntValueOf) {\n return false;\n }\n try {\n bigIntValueOf.call(obj);\n return true;\n } catch (e) {}\n return false;\n}\n\nvar hasOwn = Object.prototype.hasOwnProperty || function (key) { return key in this; };\nfunction has(obj, key) {\n return hasOwn.call(obj, key);\n}\n\nfunction toStr(obj) {\n return objectToString.call(obj);\n}\n\nfunction nameOf(f) {\n if (f.name) { return f.name; }\n var m = match.call(functionToString.call(f), /^function\\s*([\\w$]+)/);\n if (m) { return m[1]; }\n return null;\n}\n\nfunction indexOf(xs, x) {\n if (xs.indexOf) { return xs.indexOf(x); }\n for (var i = 0, l = xs.length; i < l; i++) {\n if (xs[i] === x) { return i; }\n }\n return -1;\n}\n\nfunction isMap(x) {\n if (!mapSize || !x || typeof x !== 'object') {\n return false;\n }\n try {\n mapSize.call(x);\n try {\n setSize.call(x);\n } catch (s) {\n return true;\n }\n return x instanceof Map; // core-js workaround, pre-v2.5.0\n } catch (e) {}\n return false;\n}\n\nfunction isWeakMap(x) {\n if (!weakMapHas || !x || typeof x !== 'object') {\n return false;\n }\n try {\n weakMapHas.call(x, weakMapHas);\n try {\n weakSetHas.call(x, weakSetHas);\n } catch (s) {\n return true;\n }\n return x instanceof WeakMap; // core-js workaround, pre-v2.5.0\n } catch (e) {}\n return false;\n}\n\nfunction isWeakRef(x) {\n if (!weakRefDeref || !x || typeof x !== 'object') {\n return false;\n }\n try {\n weakRefDeref.call(x);\n return true;\n } catch (e) {}\n return false;\n}\n\nfunction isSet(x) {\n if (!setSize || !x || typeof x !== 'object') {\n return false;\n }\n try {\n setSize.call(x);\n try {\n mapSize.call(x);\n } catch (m) {\n return true;\n }\n return x instanceof Set; // core-js workaround, pre-v2.5.0\n } catch (e) {}\n return false;\n}\n\nfunction isWeakSet(x) {\n if (!weakSetHas || !x || typeof x !== 'object') {\n return false;\n }\n try {\n weakSetHas.call(x, weakSetHas);\n try {\n weakMapHas.call(x, weakMapHas);\n } catch (s) {\n return true;\n }\n return x instanceof WeakSet; // core-js workaround, pre-v2.5.0\n } catch (e) {}\n return false;\n}\n\nfunction isElement(x) {\n if (!x || typeof x !== 'object') { return false; }\n if (typeof HTMLElement !== 'undefined' && x instanceof HTMLElement) {\n return true;\n }\n return typeof x.nodeName === 'string' && typeof x.getAttribute === 'function';\n}\n\nfunction inspectString(str, opts) {\n if (str.length > opts.maxStringLength) {\n var remaining = str.length - opts.maxStringLength;\n var trailer = '... ' + remaining + ' more character' + (remaining > 1 ? 's' : '');\n return inspectString(str.slice(0, opts.maxStringLength), opts) + trailer;\n }\n // eslint-disable-next-line no-control-regex\n var s = str.replace(/(['\\\\])/g, '\\\\$1').replace(/[\\x00-\\x1f]/g, lowbyte);\n return wrapQuotes(s, 'single', opts);\n}\n\nfunction lowbyte(c) {\n var n = c.charCodeAt(0);\n var x = {\n 8: 'b',\n 9: 't',\n 10: 'n',\n 12: 'f',\n 13: 'r'\n }[n];\n if (x) { return '\\\\' + x; }\n return '\\\\x' + (n < 0x10 ? '0' : '') + n.toString(16).toUpperCase();\n}\n\nfunction markBoxed(str) {\n return 'Object(' + str + ')';\n}\n\nfunction weakCollectionOf(type) {\n return type + ' { ? }';\n}\n\nfunction collectionOf(type, size, entries, indent) {\n var joinedEntries = indent ? indentedJoin(entries, indent) : entries.join(', ');\n return type + ' (' + size + ') {' + joinedEntries + '}';\n}\n\nfunction singleLineValues(xs) {\n for (var i = 0; i < xs.length; i++) {\n if (indexOf(xs[i], '\\n') >= 0) {\n return false;\n }\n }\n return true;\n}\n\nfunction getIndent(opts, depth) {\n var baseIndent;\n if (opts.indent === '\\t') {\n baseIndent = '\\t';\n } else if (typeof opts.indent === 'number' && opts.indent > 0) {\n baseIndent = Array(opts.indent + 1).join(' ');\n } else {\n return null;\n }\n return {\n base: baseIndent,\n prev: Array(depth + 1).join(baseIndent)\n };\n}\n\nfunction indentedJoin(xs, indent) {\n if (xs.length === 0) { return ''; }\n var lineJoiner = '\\n' + indent.prev + indent.base;\n return lineJoiner + xs.join(',' + lineJoiner) + '\\n' + indent.prev;\n}\n\nfunction arrObjKeys(obj, inspect) {\n var isArr = isArray(obj);\n var xs = [];\n if (isArr) {\n xs.length = obj.length;\n for (var i = 0; i < obj.length; i++) {\n xs[i] = has(obj, i) ? inspect(obj[i], obj) : '';\n }\n }\n var syms = typeof gOPS === 'function' ? gOPS(obj) : [];\n var symMap;\n if (hasShammedSymbols) {\n symMap = {};\n for (var k = 0; k < syms.length; k++) {\n symMap['$' + syms[k]] = syms[k];\n }\n }\n\n for (var key in obj) { // eslint-disable-line no-restricted-syntax\n if (!has(obj, key)) { continue; } // eslint-disable-line no-restricted-syntax, no-continue\n if (isArr && String(Number(key)) === key && key < obj.length) { continue; } // eslint-disable-line no-restricted-syntax, no-continue\n if (hasShammedSymbols && symMap['$' + key] instanceof Symbol) {\n // this is to prevent shammed Symbols, which are stored as strings, from being included in the string key section\n continue; // eslint-disable-line no-restricted-syntax, no-continue\n } else if ((/[^\\w$]/).test(key)) {\n xs.push(inspect(key, obj) + ': ' + inspect(obj[key], obj));\n } else {\n xs.push(key + ': ' + inspect(obj[key], obj));\n }\n }\n if (typeof gOPS === 'function') {\n for (var j = 0; j < syms.length; j++) {\n if (isEnumerable.call(obj, syms[j])) {\n xs.push('[' + inspect(syms[j]) + ']: ' + inspect(obj[syms[j]], obj));\n }\n }\n }\n return xs;\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,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Q1Y7QUFDQTtBQUNBO0FBQ0E7QUFDQSx3QkFBd0IsaUJBQWlCO0FBQ3pDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9vYmplY3QtaW5zcGVjdC9pbmRleC5qcz8yNzE0Il0sInNvdXJjZXNDb250ZW50IjpbInZhciBoYXNNYXAgPSB0eXBlb2YgTWFwID09PSAnZnVuY3Rpb24nICYmIE1hcC5wcm90b3R5cGU7XG52YXIgbWFwU2l6ZURlc2NyaXB0b3IgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yICYmIGhhc01hcCA/IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3IoTWFwLnByb3RvdHlwZSwgJ3NpemUnKSA6IG51bGw7XG52YXIgbWFwU2l6ZSA9IGhhc01hcCAmJiBtYXBTaXplRGVzY3JpcHRvciAmJiB0eXBlb2YgbWFwU2l6ZURlc2NyaXB0b3IuZ2V0ID09PSAnZnVuY3Rpb24nID8gbWFwU2l6ZURlc2NyaXB0b3IuZ2V0IDogbnVsbDtcbnZhciBtYXBGb3JFYWNoID0gaGFzTWFwICYmIE1hcC5wcm90b3R5cGUuZm9yRWFjaDtcbnZhciBoYXNTZXQgPSB0eXBlb2YgU2V0ID09PSAnZnVuY3Rpb24nICYmIFNldC5wcm90b3R5cGU7XG52YXIgc2V0U2l6ZURlc2NyaXB0b3IgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yICYmIGhhc1NldCA/IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3IoU2V0LnByb3RvdHlwZSwgJ3NpemUnKSA6IG51bGw7XG52YXIgc2V0U2l6ZSA9IGhhc1NldCAmJiBzZXRTaXplRGVzY3JpcHRvciAmJiB0eXBlb2Ygc2V0U2l6ZURlc2NyaXB0b3IuZ2V0ID09PSAnZnVuY3Rpb24nID8gc2V0U2l6ZURlc2NyaXB0b3IuZ2V0IDogbnVsbDtcbnZhciBzZXRGb3JFYWNoID0gaGFzU2V0ICYmIFNldC5wcm90b3R5cGUuZm9yRWFjaDtcbnZhciBoYXNXZWFrTWFwID0gdHlwZW9mIFdlYWtNYXAgPT09ICdmdW5jdGlvbicgJiYgV2Vha01hcC5wcm90b3R5cGU7XG52YXIgd2Vha01hcEhhcyA9IGhhc1dlYWtNYXAgPyBXZWFrTWFwLnByb3RvdHlwZS5oYXMgOiBudWxsO1xudmFyIGhhc1dlYWtTZXQgPSB0eXBlb2YgV2Vha1NldCA9PT0gJ2Z1bmN0aW9uJyAmJiBXZWFrU2V0LnByb3RvdHlwZTtcbnZhciB3ZWFrU2V0SGFzID0gaGFzV2Vha1NldCA/IFdlYWtTZXQucHJvdG90eXBlLmhhcyA6IG51bGw7XG52YXIgaGFzV2Vha1JlZiA9IHR5cGVvZiBXZWFrUmVmID09PSAnZnVuY3Rpb24nICYmIFdlYWtSZWYucHJvdG90eXBlO1xudmFyIHdlYWtSZWZEZXJlZiA9IGhhc1dlYWtSZWYgPyBXZWFrUmVmLnByb3RvdHlwZS5kZXJlZiA6IG51bGw7XG52YXIgYm9vbGVhblZhbHVlT2YgPSBCb29sZWFuLnByb3RvdHlwZS52YWx1ZU9mO1xudmFyIG9iamVjdFRvU3RyaW5nID0gT2JqZWN0LnByb3RvdHlwZS50b1N0cmluZztcbnZhciBmdW5jdGlvblRvU3RyaW5nID0gRnVuY3Rpb24ucHJvdG90eXBlLnRvU3RyaW5nO1xudmFyIG1hdGNoID0gU3RyaW5nLnByb3RvdHlwZS5tYXRjaDtcbnZhciBiaWdJbnRWYWx1ZU9mID0gdHlwZW9mIEJpZ0ludCA9PT0gJ2Z1bmN0aW9uJyA/IEJpZ0ludC5wcm90b3R5cGUudmFsdWVPZiA6IG51bGw7XG52YXIgZ09QUyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eVN5bWJvbHM7XG52YXIgc3ltVG9TdHJpbmcgPSB0eXBlb2YgU3ltYm9sID09PSAnZnVuY3Rpb24nICYmIHR5cGVvZiBTeW1ib2wuaXRlcmF0b3IgPT09ICdzeW1ib2wnID8gU3ltYm9sLnByb3RvdHlwZS50b1N0cmluZyA6IG51bGw7XG52YXIgaGFzU2hhbW1lZFN5bWJvbHMgPSB0eXBlb2YgU3ltYm9sID09PSAnZnVuY3Rpb24nICYmIHR5cGVvZiBTeW1ib2wuaXRlcmF0b3IgPT09ICdvYmplY3QnO1xudmFyIGlzRW51bWVyYWJsZSA9IE9iamVjdC5wcm90b3R5cGUucHJvcGVydHlJc0VudW1lcmFibGU7XG5cbnZhciBnUE8gPSAodHlwZW9mIFJlZmxlY3QgPT09ICdmdW5jdGlvbicgPyBSZWZsZWN0LmdldFByb3RvdHlwZU9mIDogT2JqZWN0LmdldFByb3RvdHlwZU9mKSB8fCAoXG4gICAgW10uX19wcm90b19fID09PSBBcnJheS5wcm90b3R5cGUgLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1wcm90b1xuICAgICAgICA/IGZ1bmN0aW9uIChPKSB7XG4gICAgICAgICAgICByZXR1cm4gTy5fX3Byb3RvX187IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tcHJvdG9cbiAgICAgICAgfVxuICAgICAgICA6IG51bGxcbik7XG5cbnZhciBpbnNwZWN0Q3VzdG9tID0gcmVxdWlyZSgnLi91dGlsLmluc3BlY3QnKS5jdXN0b207XG52YXIgaW5zcGVjdFN5bWJvbCA9IGluc3BlY3RDdXN0b20gJiYgaXNTeW1ib2woaW5zcGVjdEN1c3RvbSkgPyBpbnNwZWN0Q3VzdG9tIDogbnVsbDtcbnZhciB0b1N0cmluZ1RhZyA9IHR5cGVvZiBTeW1ib2wgPT09ICdmdW5jdGlvbicgJiYgdHlwZW9mIFN5bWJvbC50b1N0cmluZ1RhZyAhPT0gJ3VuZGVmaW5lZCcgPyBTeW1ib2wudG9TdHJpbmdUYWcgOiBudWxsO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIGluc3BlY3RfKG9iaiwgb3B0aW9ucywgZGVwdGgsIHNlZW4pIHtcbiAgICB2YXIgb3B0cyA9IG9wdGlvbnMgfHwge307XG5cbiAgICBpZiAoaGFzKG9wdHMsICdxdW90ZVN0eWxlJykgJiYgKG9wdHMucXVvdGVTdHlsZSAhPT0gJ3NpbmdsZScgJiYgb3B0cy5xdW90ZVN0eWxlICE9PSAnZG91YmxlJykpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignb3B0aW9uIFwicXVvdGVTdHlsZVwiIG11c3QgYmUgXCJzaW5nbGVcIiBvciBcImRvdWJsZVwiJyk7XG4gICAgfVxuICAgIGlmIChcbiAgICAgICAgaGFzKG9wdHMsICdtYXhTdHJpbmdMZW5ndGgnKSAmJiAodHlwZW9mIG9wdHMubWF4U3RyaW5nTGVuZ3RoID09PSAnbnVtYmVyJ1xuICAgICAgICAgICAgPyBvcHRzLm1heFN0cmluZ0xlbmd0aCA8IDAgJiYgb3B0cy5tYXhTdHJpbmdMZW5ndGggIT09IEluZmluaXR5XG4gICAgICAgICAgICA6IG9wdHMubWF4U3RyaW5nTGVuZ3RoICE9PSBudWxsXG4gICAgICAgIClcbiAgICApIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignb3B0aW9uIFwibWF4U3RyaW5nTGVuZ3RoXCIsIGlmIHByb3ZpZGVkLCBtdXN0IGJlIGEgcG9zaXRpdmUgaW50ZWdlciwgSW5maW5pdHksIG9yIGBudWxsYCcpO1xuICAgIH1cbiAgICB2YXIgY3VzdG9tSW5zcGVjdCA9IGhhcyhvcHRzLCAnY3VzdG9tSW5zcGVjdCcpID8gb3B0cy5jdXN0b21JbnNwZWN0IDogdHJ1ZTtcbiAgICBpZiAodHlwZW9mIGN1c3RvbUluc3BlY3QgIT09ICdib29sZWFuJyAmJiBjdXN0b21JbnNwZWN0ICE9PSAnc3ltYm9sJykge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdvcHRpb24gXCJjdXN0b21JbnNwZWN0XCIsIGlmIHByb3ZpZGVkLCBtdXN0IGJlIGB0cnVlYCwgYGZhbHNlYCwgb3IgYFxcJ3N5bWJvbFxcJ2AnKTtcbiAgICB9XG5cbiAgICBpZiAoXG4gICAgICAgIGhhcyhvcHRzLCAnaW5kZW50JylcbiAgICAgICAgJiYgb3B0cy5pbmRlbnQgIT09IG51bGxcbiAgICAgICAgJiYgb3B0cy5pbmRlbnQgIT09ICdcXHQnXG4gICAgICAgICYmICEocGFyc2VJbnQob3B0cy5pbmRlbnQsIDEwKSA9PT0gb3B0cy5pbmRlbnQgJiYgb3B0cy5pbmRlbnQgPiAwKVxuICAgICkge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdvcHRpb25zIFwiaW5kZW50XCIgbXVzdCBiZSBcIlxcXFx0XCIsIGFuIGludGVnZXIgPiAwLCBvciBgbnVsbGAnKTtcbiAgICB9XG5cbiAgICBpZiAodHlwZW9mIG9iaiA9PT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgICAgcmV0dXJuICd1bmRlZmluZWQnO1xuICAgIH1cbiAgICBpZiAob2JqID09PSBudWxsKSB7XG4gICAgICAgIHJldHVybiAnbnVsbCc7XG4gICAgfVxuICAgIGlmICh0eXBlb2Ygb2JqID09PSAnYm9vbGVhbicpIHtcbiAgICAgICAgcmV0dXJuIG9iaiA/ICd0cnVlJyA6ICdmYWxzZSc7XG4gICAgfVxuXG4gICAgaWYgKHR5cGVvZiBvYmogPT09ICdzdHJpbmcnKSB7XG4gICAgICAgIHJldHVybiBpbnNwZWN0U3RyaW5nKG9iaiwgb3B0cyk7XG4gICAgfVxuICAgIGlmICh0eXBlb2Ygb2JqID09PSAnbnVtYmVyJykge1xuICAgICAgICBpZiAob2JqID09PSAwKSB7XG4gICAgICAgICAgICByZXR1cm4gSW5maW5pdHkgLyBvYmogPiAwID8gJzAnIDogJy0wJztcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gU3RyaW5nKG9iaik7XG4gICAgfVxuICAgIGlmICh0eXBlb2Ygb2JqID09PSAnYmlnaW50Jykge1xuICAgICAgICByZXR1cm4gU3RyaW5nKG9iaikgKyAnbic7XG4gICAgfVxuXG4gICAgdmFyIG1heERlcHRoID0gdHlwZW9mIG9wdHMuZGVwdGggPT09ICd1bmRlZmluZWQnID8gNSA6IG9wdHMuZGVwdGg7XG4gICAgaWYgKHR5cGVvZiBkZXB0aCA9PT0gJ3VuZGVmaW5lZCcpIHsgZGVwdGggPSAwOyB9XG4gICAgaWYgKGRlcHRoID49IG1heERlcHRoICYmIG1heERlcHRoID4gMCAmJiB0eXBlb2Ygb2JqID09PSAnb2JqZWN0Jykge1xuICAgICAgICByZXR1cm4gaXNBcnJheShvYmopID8gJ1tBcnJheV0nIDogJ1tPYmplY3RdJztcbiAgICB9XG5cbiAgICB2YXIgaW5kZW50ID0gZ2V0SW5kZW50KG9wdHMsIGRlcHRoKTtcblxuICAgIGlmICh0eXBlb2Ygc2VlbiA9PT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgICAgc2VlbiA9IFtdO1xuICAgIH0gZWxzZSBpZiAoaW5kZXhPZihzZWVuLCBvYmopID49IDApIHtcbiAgICAgICAgcmV0dXJuICdbQ2lyY3VsYXJdJztcbiAgICB9XG5cbiAgICBmdW5jdGlvbiBpbnNwZWN0KHZhbHVlLCBmcm9tLCBub0luZGVudCkge1xuICAgICAgICBpZiAoZnJvbSkge1xuICAgICAgICAgICAgc2VlbiA9IHNlZW4uc2xpY2UoKTtcbiAgICAgICAgICAgIHNlZW4ucHVzaChmcm9tKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAobm9JbmRlbnQpIHtcbiAgICAgICAgICAgIHZhciBuZXdPcHRzID0ge1xuICAgICAgICAgICAgICAgIGRlcHRoOiBvcHRzLmRlcHRoXG4gICAgICAgICAgICB9O1xuICAgICAgICAgICAgaWYgKGhhcyhvcHRzLCAncXVvdGVTdHlsZScpKSB7XG4gICAgICAgICAgICAgICAgbmV3T3B0cy5xdW90ZVN0eWxlID0gb3B0cy5xdW90ZVN0eWxlO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgcmV0dXJuIGluc3BlY3RfKHZhbHVlLCBuZXdPcHRzLCBkZXB0aCArIDEsIHNlZW4pO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBpbnNwZWN0Xyh2YWx1ZSwgb3B0cywgZGVwdGggKyAxLCBzZWVuKTtcbiAgICB9XG5cbiAgICBpZiAodHlwZW9mIG9iaiA9PT0gJ2Z1bmN0aW9uJykge1xuICAgICAgICB2YXIgbmFtZSA9IG5hbWVPZihvYmopO1xuICAgICAgICB2YXIga2V5cyA9IGFyck9iaktleXMob2JqLCBpbnNwZWN0KTtcbiAgICAgICAgcmV0dXJuICdbRnVuY3Rpb24nICsgKG5hbWUgPyAnOiAnICsgbmFtZSA6ICcgKGFub255bW91cyknKSArICddJyArIChrZXlzLmxlbmd0aCA+IDAgPyAnIHsgJyArIGtleXMuam9pbignLCAnKSArICcgfScgOiAnJyk7XG4gICAgfVxuICAgIGlmIChpc1N5bWJvbChvYmopKSB7XG4gICAgICAgIHZhciBzeW1TdHJpbmcgPSBoYXNTaGFtbWVkU3ltYm9scyA/IFN0cmluZyhvYmopLnJlcGxhY2UoL14oU3ltYm9sXFwoLipcXCkpX1teKV0qJC8sICckMScpIDogc3ltVG9TdHJpbmcuY2FsbChvYmopO1xuICAgICAgICByZXR1cm4gdHlwZW9mIG9iaiA9PT0gJ29iamVjdCcgJiYgIWhhc1NoYW1tZWRTeW1ib2xzID8gbWFya0JveGVkKHN5bVN0cmluZykgOiBzeW1TdHJpbmc7XG4gICAgfVxuICAgIGlmIChpc0VsZW1lbnQob2JqKSkge1xuICAgICAgICB2YXIgcyA9ICc8JyArIFN0cmluZyhvYmoubm9kZU5hbWUpLnRvTG93ZXJDYXNlKCk7XG4gICAgICAgIHZhciBhdHRycyA9IG9iai5hdHRyaWJ1dGVzIHx8IFtdO1xuICAgICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGF0dHJzLmxlbmd0aDsgaSsrKSB7XG4gICAgICAgICAgICBzICs9ICcgJyArIGF0dHJzW2ldLm5hbWUgKyAnPScgKyB3cmFwUXVvdGVzKHF1b3RlKGF0dHJzW2ldLnZhbHVlKSwgJ2RvdWJsZScsIG9wdHMpO1xuICAgICAgICB9XG4gICAgICAgIHMgKz0gJz4nO1xuICAgICAgICBpZiAob2JqLmNoaWxkTm9kZXMgJiYgb2JqLmNoaWxkTm9kZXMubGVuZ3RoKSB7IHMgKz0gJy4uLic7IH1cbiAgICAgICAgcyArPSAnPC8nICsgU3RyaW5nKG9iai5ub2RlTmFtZSkudG9Mb3dlckNhc2UoKSArICc+JztcbiAgICAgICAgcmV0dXJuIHM7XG4gICAgfVxuICAgIGlmIChpc0FycmF5KG9iaikpIHtcbiAgICAgICAgaWYgKG9iai5sZW5ndGggPT09IDApIHsgcmV0dXJuICdbXSc7IH1cbiAgICAgICAgdmFyIHhzID0gYXJyT2JqS2V5cyhvYmosIGluc3BlY3QpO1xuICAgICAgICBpZiAoaW5kZW50ICYmICFzaW5nbGVMaW5lVmFsdWVzKHhzKSkge1xuICAgICAgICAgICAgcmV0dXJuICdbJyArIGluZGVudGVkSm9pbih4cywgaW5kZW50KSArICddJztcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gJ1sgJyArIHhzLmpvaW4oJywgJykgKyAnIF0nO1xuICAgIH1cbiAgICBpZiAoaXNFcnJvcihvYmopKSB7XG4gICAgICAgIHZhciBwYXJ0cyA9IGFyck9iaktleXMob2JqLCBpbnNwZWN0KTtcbiAgICAgICAgaWYgKHBhcnRzLmxlbmd0aCA9PT0gMCkgeyByZXR1cm4gJ1snICsgU3RyaW5nKG9iaikgKyAnXSc7IH1cbiAgICAgICAgcmV0dXJuICd7IFsnICsgU3RyaW5nKG9iaikgKyAnXSAnICsgcGFydHMuam9pbignLCAnKSArICcgfSc7XG4gICAgfVxuICAgIGlmICh0eXBlb2Ygb2JqID09PSAnb2JqZWN0JyAmJiBjdXN0b21JbnNwZWN0KSB7XG4gICAgICAgIGlmIChpbnNwZWN0U3ltYm9sICYmIHR5cGVvZiBvYmpbaW5zcGVjdFN5bWJvbF0gPT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgICAgIHJldHVybiBvYmpbaW5zcGVjdFN5bWJvbF0oKTtcbiAgICAgICAgfSBlbHNlIGlmIChjdXN0b21JbnNwZWN0ICE9PSAnc3ltYm9sJyAmJiB0eXBlb2Ygb2JqLmluc3BlY3QgPT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgICAgIHJldHVybiBvYmouaW5zcGVjdCgpO1xuICAgICAgICB9XG4gICAgfVxuICAgIGlmIChpc01hcChvYmopKSB7XG4gICAgICAgIHZhciBtYXBQYXJ0cyA9IFtdO1xuICAgICAgICBtYXBGb3JFYWNoLmNhbGwob2JqLCBmdW5jdGlvbiAodmFsdWUsIGtleSkge1xuICAgICAgICAgICAgbWFwUGFydHMucHVzaChpbnNwZWN0KGtleSwgb2JqLCB0cnVlKSArICcgPT4gJyArIGluc3BlY3QodmFsdWUsIG9iaikpO1xuICAgICAgICB9KTtcbiAgICAgICAgcmV0dXJuIGNvbGxlY3Rpb25PZignTWFwJywgbWFwU2l6ZS5jYWxsKG9iaiksIG1hcFBhcnRzLCBpbmRlbnQpO1xuICAgIH1cbiAgICBpZiAoaXNTZXQob2JqKSkge1xuICAgICAgICB2YXIgc2V0UGFydHMgPSBbXTtcbiAgICAgICAgc2V0Rm9yRWFjaC5jYWxsKG9iaiwgZnVuY3Rpb24gKHZhbHVlKSB7XG4gICAgICAgICAgICBzZXRQYXJ0cy5wdXNoKGluc3BlY3QodmFsdWUsIG9iaikpO1xuICAgICAgICB9KTtcbiAgICAgICAgcmV0dXJuIGNvbGxlY3Rpb25PZignU2V0Jywgc2V0U2l6ZS5jYWxsKG9iaiksIHNldFBhcnRzLCBpbmRlbnQpO1xuICAgIH1cbiAgICBpZiAoaXNXZWFrTWFwKG9iaikpIHtcbiAgICAgICAgcmV0dXJuIHdlYWtDb2xsZWN0aW9uT2YoJ1dlYWtNYXAnKTtcbiAgICB9XG4gICAgaWYgKGlzV2Vha1NldChvYmopKSB7XG4gICAgICAgIHJldHVybiB3ZWFrQ29sbGVjdGlvbk9mKCdXZWFrU2V0Jyk7XG4gICAgfVxuICAgIGlmIChpc1dlYWtSZWYob2JqKSkge1xuICAgICAgICByZXR1cm4gd2Vha0NvbGxlY3Rpb25PZignV2Vha1JlZicpO1xuICAgIH1cbiAgICBpZiAoaXNOdW1iZXIob2JqKSkge1xuICAgICAgICByZXR1cm4gbWFya0JveGVkKGluc3BlY3QoTnVtYmVyKG9iaikpKTtcbiAgICB9XG4gICAgaWYgKGlzQmlnSW50KG9iaikpIHtcbiAgICAgICAgcmV0dXJuIG1hcmtCb3hlZChpbnNwZWN0KGJpZ0ludFZhbHVlT2YuY2FsbChvYmopKSk7XG4gICAgfVxuICAgIGlmIChpc0Jvb2xlYW4ob2JqKSkge1xuICAgICAgICByZXR1cm4gbWFya0JveGVkKGJvb2xlYW5WYWx1ZU9mLmNhbGwob2JqKSk7XG4gICAgfVxuICAgIGlmIChpc1N0cmluZyhvYmopKSB7XG4gICAgICAgIHJldHVybiBtYXJrQm94ZWQoaW5zcGVjdChTdHJpbmcob2JqKSkpO1xuICAgIH1cbiAgICBpZiAoIWlzRGF0ZShvYmopICYmICFpc1JlZ0V4cChvYmopKSB7XG4gICAgICAgIHZhciB5cyA9IGFyck9iaktleXMob2JqLCBpbnNwZWN0KTtcbiAgICAgICAgdmFyIGlzUGxhaW5PYmplY3QgPSBnUE8gPyBnUE8ob2JqKSA9PT0gT2JqZWN0LnByb3RvdHlwZSA6IG9iaiBpbnN0YW5jZW9mIE9iamVjdCB8fCBvYmouY29uc3RydWN0b3IgPT09IE9iamVjdDtcbiAgICAgICAgdmFyIHByb3RvVGFnID0gb2JqIGluc3RhbmNlb2YgT2JqZWN0ID8gJycgOiAnbnVsbCBwcm90b3R5cGUnO1xuICAgICAgICB2YXIgc3RyaW5nVGFnID0gIWlzUGxhaW5PYmplY3QgJiYgdG9TdHJpbmdUYWcgJiYgT2JqZWN0KG9iaikgPT09IG9iaiAmJiB0b1N0cmluZ1RhZyBpbiBvYmogPyB0b1N0cihvYmopLnNsaWNlKDgsIC0xKSA6IHByb3RvVGFnID8gJ09iamVjdCcgOiAnJztcbiAgICAgICAgdmFyIGNvbnN0cnVjdG9yVGFnID0gaXNQbGFpbk9iamVjdCB8fCB0eXBlb2Ygb2JqLmNvbnN0cnVjdG9yICE9PSAnZnVuY3Rpb24nID8gJycgOiBvYmouY29uc3RydWN0b3IubmFtZSA/IG9iai5jb25zdHJ1Y3Rvci5uYW1lICsgJyAnIDogJyc7XG4gICAgICAgIHZhciB0YWcgPSBjb25zdHJ1Y3RvclRhZyArIChzdHJpbmdUYWcgfHwgcHJvdG9UYWcgPyAnWycgKyBbXS5jb25jYXQoc3RyaW5nVGFnIHx8IFtdLCBwcm90b1RhZyB8fCBbXSkuam9pbignOiAnKSArICddICcgOiAnJyk7XG4gICAgICAgIGlmICh5cy5sZW5ndGggPT09IDApIHsgcmV0dXJuIHRhZyArICd7fSc7IH1cbiAgICAgICAgaWYgKGluZGVudCkge1xuICAgICAgICAgICAgcmV0dXJuIHRhZyArICd7JyArIGluZGVudGVkSm9pbih5cywgaW5kZW50KSArICd9JztcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gdGFnICsgJ3sgJyArIHlzLmpvaW4oJywgJykgKyAnIH0nO1xuICAgIH1cbiAgICByZXR1cm4gU3RyaW5nKG9iaik7XG59O1xuXG5mdW5jdGlvbiB3cmFwUXVvdGVzKHMsIGRlZmF1bHRTdHlsZSwgb3B0cykge1xuICAgIHZhciBxdW90ZUNoYXIgPSAob3B0cy5xdW90ZVN0eWxlIHx8IGRlZmF1bHRTdHlsZSkgPT09ICdkb3VibGUnID8gJ1wiJyA6IFwiJ1wiO1xuICAgIHJldHVybiBxdW90ZUNoYXIgKyBzICsgcXVvdGVDaGFyO1xufVxuXG5mdW5jdGlvbiBxdW90ZShzKSB7XG4gICAgcmV0dXJuIFN0cmluZyhzKS5yZXBsYWNlKC9cIi9nLCAnJnF1b3Q7Jyk7XG59XG5cbmZ1bmN0aW9uIGlzQXJyYXkob2JqKSB7IHJldHVybiB0b1N0cihvYmopID09PSAnW29iamVjdCBBcnJheV0nICYmICghdG9TdHJpbmdUYWcgfHwgISh0eXBlb2Ygb2JqID09PSAnb2JqZWN0JyAmJiB0b1N0cmluZ1RhZyBpbiBvYmopKTsgfVxuZnVuY3Rpb24gaXNEYXRlKG9iaikgeyByZXR1cm4gdG9TdHIob2JqKSA9PT0gJ1tvYmplY3QgRGF0ZV0nICYmICghdG9TdHJpbmdUYWcgfHwgISh0eXBlb2Ygb2JqID09PSAnb2JqZWN0JyAmJiB0b1N0cmluZ1RhZyBpbiBvYmopKTsgfVxuZnVuY3Rpb24gaXNSZWdFeHAob2JqKSB7IHJldHVybiB0b1N0cihvYmopID09PSAnW29iamVjdCBSZWdFeHBdJyAmJiAoIXRvU3RyaW5nVGFnIHx8ICEodHlwZW9mIG9iaiA9PT0gJ29iamVjdCcgJiYgdG9TdHJpbmdUYWcgaW4gb2JqKSk7IH1cbmZ1bmN0aW9uIGlzRXJyb3Iob2JqKSB7IHJldHVybiB0b1N0cihvYmopID09PSAnW29iamVjdCBFcnJvcl0nICYmICghdG9TdHJpbmdUYWcgfHwgISh0eXBlb2Ygb2JqID09PSAnb2JqZWN0JyAmJiB0b1N0cmluZ1RhZyBpbiBvYmopKTsgfVxuZnVuY3Rpb24gaXNTdHJpbmcob2JqKSB7IHJldHVybiB0b1N0cihvYmopID09PSAnW29iamVjdCBTdHJpbmddJyAmJiAoIXRvU3RyaW5nVGFnIHx8ICEodHlwZW9mIG9iaiA9PT0gJ29iamVjdCcgJiYgdG9TdHJpbmdUYWcgaW4gb2JqKSk7IH1cbmZ1bmN0aW9uIGlzTnVtYmVyKG9iaikgeyByZXR1cm4gdG9TdHIob2JqKSA9PT0gJ1tvYmplY3QgTnVtYmVyXScgJiYgKCF0b1N0cmluZ1RhZyB8fCAhKHR5cGVvZiBvYmogPT09ICdvYmplY3QnICYmIHRvU3RyaW5nVGFnIGluIG9iaikpOyB9XG5mdW5jdGlvbiBpc0Jvb2xlYW4ob2JqKSB7IHJldHVybiB0b1N0cihvYmopID09PSAnW29iamVjdCBCb29sZWFuXScgJiYgKCF0b1N0cmluZ1RhZyB8fCAhKHR5cGVvZiBvYmogPT09ICdvYmplY3QnICYmIHRvU3RyaW5nVGFnIGluIG9iaikpOyB9XG5cbi8vIFN5bWJvbCBhbmQgQmlnSW50IGRvIGhhdmUgU3ltYm9sLnRvU3RyaW5nVGFnIGJ5IHNwZWMsIHNvIHRoYXQgY2FuJ3QgYmUgdXNlZCB0byBlbGltaW5hdGUgZmFsc2UgcG9zaXRpdmVzXG5mdW5jdGlvbiBpc1N5bWJvbChvYmopIHtcbiAgICBpZiAoaGFzU2hhbW1lZFN5bWJvbHMpIHtcbiAgICAgICAgcmV0dXJuIG9iaiAmJiB0eXBlb2Ygb2JqID09PSAnb2JqZWN0JyAmJiBvYmogaW5zdGFuY2VvZiBTeW1ib2w7XG4gICAgfVxuICAgIGlmICh0eXBlb2Ygb2JqID09PSAnc3ltYm9sJykge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gICAgaWYgKCFvYmogfHwgdHlwZW9mIG9iaiAhPT0gJ29iamVjdCcgfHwgIXN5bVRvU3RyaW5nKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgdHJ5IHtcbiAgICAgICAgc3ltVG9TdHJpbmcuY2FsbChvYmopO1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9IGNhdGNoIChlKSB7fVxuICAgIHJldHVybiBmYWxzZTtcbn1cblxuZnVuY3Rpb24gaXNCaWdJbnQob2JqKSB7XG4gICAgaWYgKCFvYmogfHwgdHlwZW9mIG9iaiAhPT0gJ29iamVjdCcgfHwgIWJpZ0ludFZhbHVlT2YpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICB0cnkge1xuICAgICAgICBiaWdJbnRWYWx1ZU9mLmNhbGwob2JqKTtcbiAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgfSBjYXRjaCAoZSkge31cbiAgICByZXR1cm4gZmFsc2U7XG59XG5cbnZhciBoYXNPd24gPSBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5IHx8IGZ1bmN0aW9uIChrZXkpIHsgcmV0dXJuIGtleSBpbiB0aGlzOyB9O1xuZnVuY3Rpb24gaGFzKG9iaiwga2V5KSB7XG4gICAgcmV0dXJuIGhhc093bi5jYWxsKG9iaiwga2V5KTtcbn1cblxuZnVuY3Rpb24gdG9TdHIob2JqKSB7XG4gICAgcmV0dXJuIG9iamVjdFRvU3RyaW5nLmNhbGwob2JqKTtcbn1cblxuZnVuY3Rpb24gbmFtZU9mKGYpIHtcbiAgICBpZiAoZi5uYW1lKSB7IHJldHVybiBmLm5hbWU7IH1cbiAgICB2YXIgbSA9IG1hdGNoLmNhbGwoZnVuY3Rpb25Ub1N0cmluZy5jYWxsKGYpLCAvXmZ1bmN0aW9uXFxzKihbXFx3JF0rKS8pO1xuICAgIGlmIChtKSB7IHJldHVybiBtWzFdOyB9XG4gICAgcmV0dXJuIG51bGw7XG59XG5cbmZ1bmN0aW9uIGluZGV4T2YoeHMsIHgpIHtcbiAgICBpZiAoeHMuaW5kZXhPZikgeyByZXR1cm4geHMuaW5kZXhPZih4KTsgfVxuICAgIGZvciAodmFyIGkgPSAwLCBsID0geHMubGVuZ3RoOyBpIDwgbDsgaSsrKSB7XG4gICAgICAgIGlmICh4c1tpXSA9PT0geCkgeyByZXR1cm4gaTsgfVxuICAgIH1cbiAgICByZXR1cm4gLTE7XG59XG5cbmZ1bmN0aW9uIGlzTWFwKHgpIHtcbiAgICBpZiAoIW1hcFNpemUgfHwgIXggfHwgdHlwZW9mIHggIT09ICdvYmplY3QnKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgdHJ5IHtcbiAgICAgICAgbWFwU2l6ZS5jYWxsKHgpO1xuICAgICAgICB0cnkge1xuICAgICAgICAgICAgc2V0U2l6ZS5jYWxsKHgpO1xuICAgICAgICB9IGNhdGNoIChzKSB7XG4gICAgICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4geCBpbnN0YW5jZW9mIE1hcDsgLy8gY29yZS1qcyB3b3JrYXJvdW5kLCBwcmUtdjIuNS4wXG4gICAgfSBjYXRjaCAoZSkge31cbiAgICByZXR1cm4gZmFsc2U7XG59XG5cbmZ1bmN0aW9uIGlzV2Vha01hcCh4KSB7XG4gICAgaWYgKCF3ZWFrTWFwSGFzIHx8ICF4IHx8IHR5cGVvZiB4ICE9PSAnb2JqZWN0Jykge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHRyeSB7XG4gICAgICAgIHdlYWtNYXBIYXMuY2FsbCh4LCB3ZWFrTWFwSGFzKTtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIHdlYWtTZXRIYXMuY2FsbCh4LCB3ZWFrU2V0SGFzKTtcbiAgICAgICAgfSBjYXRjaCAocykge1xuICAgICAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIHggaW5zdGFuY2VvZiBXZWFrTWFwOyAvLyBjb3JlLWpzIHdvcmthcm91bmQsIHByZS12Mi41LjBcbiAgICB9IGNhdGNoIChlKSB7fVxuICAgIHJldHVybiBmYWxzZTtcbn1cblxuZnVuY3Rpb24gaXNXZWFrUmVmKHgpIHtcbiAgICBpZiAoIXdlYWtSZWZEZXJlZiB8fCAheCB8fCB0eXBlb2YgeCAhPT0gJ29iamVjdCcpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICB0cnkge1xuICAgICAgICB3ZWFrUmVmRGVyZWYuY2FsbCh4KTtcbiAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgfSBjYXRjaCAoZSkge31cbiAgICByZXR1cm4gZmFsc2U7XG59XG5cbmZ1bmN0aW9uIGlzU2V0KHgpIHtcbiAgICBpZiAoIXNldFNpemUgfHwgIXggfHwgdHlwZW9mIHggIT09ICdvYmplY3QnKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgdHJ5IHtcbiAgICAgICAgc2V0U2l6ZS5jYWxsKHgpO1xuICAgICAgICB0cnkge1xuICAgICAgICAgICAgbWFwU2l6ZS5jYWxsKHgpO1xuICAgICAgICB9IGNhdGNoIChtKSB7XG4gICAgICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4geCBpbnN0YW5jZW9mIFNldDsgLy8gY29yZS1qcyB3b3JrYXJvdW5kLCBwcmUtdjIuNS4wXG4gICAgfSBjYXRjaCAoZSkge31cbiAgICByZXR1cm4gZmFsc2U7XG59XG5cbmZ1bmN0aW9uIGlzV2Vha1NldCh4KSB7XG4gICAgaWYgKCF3ZWFrU2V0SGFzIHx8ICF4IHx8IHR5cGVvZiB4ICE9PSAnb2JqZWN0Jykge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHRyeSB7XG4gICAgICAgIHdlYWtTZXRIYXMuY2FsbCh4LCB3ZWFrU2V0SGFzKTtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIHdlYWtNYXBIYXMuY2FsbCh4LCB3ZWFrTWFwSGFzKTtcbiAgICAgICAgfSBjYXRjaCAocykge1xuICAgICAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIHggaW5zdGFuY2VvZiBXZWFrU2V0OyAvLyBjb3JlLWpzIHdvcmthcm91bmQsIHByZS12Mi41LjBcbiAgICB9IGNhdGNoIChlKSB7fVxuICAgIHJldHVybiBmYWxzZTtcbn1cblxuZnVuY3Rpb24gaXNFbGVtZW50KHgpIHtcbiAgICBpZiAoIXggfHwgdHlwZW9mIHggIT09ICdvYmplY3QnKSB7IHJldHVybiBmYWxzZTsgfVxuICAgIGlmICh0eXBlb2YgSFRNTEVsZW1lbnQgIT09ICd1bmRlZmluZWQnICYmIHggaW5zdGFuY2VvZiBIVE1MRWxlbWVudCkge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gICAgcmV0dXJuIHR5cGVvZiB4Lm5vZGVOYW1lID09PSAnc3RyaW5nJyAmJiB0eXBlb2YgeC5nZXRBdHRyaWJ1dGUgPT09ICdmdW5jdGlvbic7XG59XG5cbmZ1bmN0aW9uIGluc3BlY3RTdHJpbmcoc3RyLCBvcHRzKSB7XG4gICAgaWYgKHN0ci5sZW5ndGggPiBvcHRzLm1heFN0cmluZ0xlbmd0aCkge1xuICAgICAgICB2YXIgcmVtYWluaW5nID0gc3RyLmxlbmd0aCAtIG9wdHMubWF4U3RyaW5nTGVuZ3RoO1xuICAgICAgICB2YXIgdHJhaWxlciA9ICcuLi4gJyArIHJlbWFpbmluZyArICcgbW9yZSBjaGFyYWN0ZXInICsgKHJlbWFpbmluZyA+IDEgPyAncycgOiAnJyk7XG4gICAgICAgIHJldHVybiBpbnNwZWN0U3RyaW5nKHN0ci5zbGljZSgwLCBvcHRzLm1heFN0cmluZ0xlbmd0aCksIG9wdHMpICsgdHJhaWxlcjtcbiAgICB9XG4gICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLWNvbnRyb2wtcmVnZXhcbiAgICB2YXIgcyA9IHN0ci5yZXBsYWNlKC8oWydcXFxcXSkvZywgJ1xcXFwkMScpLnJlcGxhY2UoL1tcXHgwMC1cXHgxZl0vZywgbG93Ynl0ZSk7XG4gICAgcmV0dXJuIHdyYXBRdW90ZXMocywgJ3NpbmdsZScsIG9wdHMpO1xufVxuXG5mdW5jdGlvbiBsb3dieXRlKGMpIHtcbiAgICB2YXIgbiA9IGMuY2hhckNvZGVBdCgwKTtcbiAgICB2YXIgeCA9IHtcbiAgICAgICAgODogJ2InLFxuICAgICAgICA5OiAndCcsXG4gICAgICAgIDEwOiAnbicsXG4gICAgICAgIDEyOiAnZicsXG4gICAgICAgIDEzOiAncidcbiAgICB9W25dO1xuICAgIGlmICh4KSB7IHJldHVybiAnXFxcXCcgKyB4OyB9XG4gICAgcmV0dXJuICdcXFxceCcgKyAobiA8IDB4MTAgPyAnMCcgOiAnJykgKyBuLnRvU3RyaW5nKDE2KS50b1VwcGVyQ2FzZSgpO1xufVxuXG5mdW5jdGlvbiBtYXJrQm94ZWQoc3RyKSB7XG4gICAgcmV0dXJuICdPYmplY3QoJyArIHN0ciArICcpJztcbn1cblxuZnVuY3Rpb24gd2Vha0NvbGxlY3Rpb25PZih0eXBlKSB7XG4gICAgcmV0dXJuIHR5cGUgKyAnIHsgPyB9Jztcbn1cblxuZnVuY3Rpb24gY29sbGVjdGlvbk9mKHR5cGUsIHNpemUsIGVudHJpZXMsIGluZGVudCkge1xuICAgIHZhciBqb2luZWRFbnRyaWVzID0gaW5kZW50ID8gaW5kZW50ZWRKb2luKGVudHJpZXMsIGluZGVudCkgOiBlbnRyaWVzLmpvaW4oJywgJyk7XG4gICAgcmV0dXJuIHR5cGUgKyAnICgnICsgc2l6ZSArICcpIHsnICsgam9pbmVkRW50cmllcyArICd9Jztcbn1cblxuZnVuY3Rpb24gc2luZ2xlTGluZVZhbHVlcyh4cykge1xuICAgIGZvciAodmFyIGkgPSAwOyBpIDwgeHMubGVuZ3RoOyBpKyspIHtcbiAgICAgICAgaWYgKGluZGV4T2YoeHNbaV0sICdcXG4nKSA+PSAwKSB7XG4gICAgICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG5cbmZ1bmN0aW9uIGdldEluZGVudChvcHRzLCBkZXB0aCkge1xuICAgIHZhciBiYXNlSW5kZW50O1xuICAgIGlmIChvcHRzLmluZGVudCA9PT0gJ1xcdCcpIHtcbiAgICAgICAgYmFzZUluZGVudCA9ICdcXHQnO1xuICAgIH0gZWxzZSBpZiAodHlwZW9mIG9wdHMuaW5kZW50ID09PSAnbnVtYmVyJyAmJiBvcHRzLmluZGVudCA+IDApIHtcbiAgICAgICAgYmFzZUluZGVudCA9IEFycmF5KG9wdHMuaW5kZW50ICsgMSkuam9pbignICcpO1xuICAgIH0gZWxzZSB7XG4gICAgICAgIHJldHVybiBudWxsO1xuICAgIH1cbiAgICByZXR1cm4ge1xuICAgICAgICBiYXNlOiBiYXNlSW5kZW50LFxuICAgICAgICBwcmV2OiBBcnJheShkZXB0aCArIDEpLmpvaW4oYmFzZUluZGVudClcbiAgICB9O1xufVxuXG5mdW5jdGlvbiBpbmRlbnRlZEpvaW4oeHMsIGluZGVudCkge1xuICAgIGlmICh4cy5sZW5ndGggPT09IDApIHsgcmV0dXJuICcnOyB9XG4gICAgdmFyIGxpbmVKb2luZXIgPSAnXFxuJyArIGluZGVudC5wcmV2ICsgaW5kZW50LmJhc2U7XG4gICAgcmV0dXJuIGxpbmVKb2luZXIgKyB4cy5qb2luKCcsJyArIGxpbmVKb2luZXIpICsgJ1xcbicgKyBpbmRlbnQucHJldjtcbn1cblxuZnVuY3Rpb24gYXJyT2JqS2V5cyhvYmosIGluc3BlY3QpIHtcbiAgICB2YXIgaXNBcnIgPSBpc0FycmF5KG9iaik7XG4gICAgdmFyIHhzID0gW107XG4gICAgaWYgKGlzQXJyKSB7XG4gICAgICAgIHhzLmxlbmd0aCA9IG9iai5sZW5ndGg7XG4gICAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwgb2JqLmxlbmd0aDsgaSsrKSB7XG4gICAgICAgICAgICB4c1tpXSA9IGhhcyhvYmosIGkpID8gaW5zcGVjdChvYmpbaV0sIG9iaikgOiAnJztcbiAgICAgICAgfVxuICAgIH1cbiAgICB2YXIgc3ltcyA9IHR5cGVvZiBnT1BTID09PSAnZnVuY3Rpb24nID8gZ09QUyhvYmopIDogW107XG4gICAgdmFyIHN5bU1hcDtcbiAgICBpZiAoaGFzU2hhbW1lZFN5bWJvbHMpIHtcbiAgICAgICAgc3ltTWFwID0ge307XG4gICAgICAgIGZvciAodmFyIGsgPSAwOyBrIDwgc3ltcy5sZW5ndGg7IGsrKykge1xuICAgICAgICAgICAgc3ltTWFwWyckJyArIHN5bXNba11dID0gc3ltc1trXTtcbiAgICAgICAgfVxuICAgIH1cblxuICAgIGZvciAodmFyIGtleSBpbiBvYmopIHsgLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1yZXN0cmljdGVkLXN5bnRheFxuICAgICAgICBpZiAoIWhhcyhvYmosIGtleSkpIHsgY29udGludWU7IH0gLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1yZXN0cmljdGVkLXN5bnRheCwgbm8tY29udGludWVcbiAgICAgICAgaWYgKGlzQXJyICYmIFN0cmluZyhOdW1iZXIoa2V5KSkgPT09IGtleSAmJiBrZXkgPCBvYmoubGVuZ3RoKSB7IGNvbnRpbnVlOyB9IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tcmVzdHJpY3RlZC1zeW50YXgsIG5vLWNvbnRpbnVlXG4gICAgICAgIGlmIChoYXNTaGFtbWVkU3ltYm9scyAmJiBzeW1NYXBbJyQnICsga2V5XSBpbnN0YW5jZW9mIFN5bWJvbCkge1xuICAgICAgICAgICAgLy8gdGhpcyBpcyB0byBwcmV2ZW50IHNoYW1tZWQgU3ltYm9scywgd2hpY2ggYXJlIHN0b3JlZCBhcyBzdHJpbmdzLCBmcm9tIGJlaW5nIGluY2x1ZGVkIGluIHRoZSBzdHJpbmcga2V5IHNlY3Rpb25cbiAgICAgICAgICAgIGNvbnRpbnVlOyAvLyBlc2xpbnQtZGlzYWJsZS1saW5lIG5vLXJlc3RyaWN0ZWQtc3ludGF4LCBuby1jb250aW51ZVxuICAgICAgICB9IGVsc2UgaWYgKCgvW15cXHckXS8pLnRlc3Qoa2V5KSkge1xuICAgICAgICAgICAgeHMucHVzaChpbnNwZWN0KGtleSwgb2JqKSArICc6ICcgKyBpbnNwZWN0KG9ialtrZXldLCBvYmopKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIHhzLnB1c2goa2V5ICsgJzogJyArIGluc3BlY3Qob2JqW2tleV0sIG9iaikpO1xuICAgICAgICB9XG4gICAgfVxuICAgIGlmICh0eXBlb2YgZ09QUyA9PT0gJ2Z1bmN0aW9uJykge1xuICAgICAgICBmb3IgKHZhciBqID0gMDsgaiA8IHN5bXMubGVuZ3RoOyBqKyspIHtcbiAgICAgICAgICAgIGlmIChpc0VudW1lcmFibGUuY2FsbChvYmosIHN5bXNbal0pKSB7XG4gICAgICAgICAgICAgICAgeHMucHVzaCgnWycgKyBpbnNwZWN0KHN5bXNbal0pICsgJ106ICcgKyBpbnNwZWN0KG9ialtzeW1zW2pdXSwgb2JqKSk7XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIHhzO1xufVxuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///631\n")},8987:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar keysShim;\nif (!Object.keys) {\n\t// modified from https://github.com/es-shims/es5-shim\n\tvar has = Object.prototype.hasOwnProperty;\n\tvar toStr = Object.prototype.toString;\n\tvar isArgs = __webpack_require__(1414); // eslint-disable-line global-require\n\tvar isEnumerable = Object.prototype.propertyIsEnumerable;\n\tvar hasDontEnumBug = !isEnumerable.call({ toString: null }, 'toString');\n\tvar hasProtoEnumBug = isEnumerable.call(function () {}, 'prototype');\n\tvar dontEnums = [\n\t\t'toString',\n\t\t'toLocaleString',\n\t\t'valueOf',\n\t\t'hasOwnProperty',\n\t\t'isPrototypeOf',\n\t\t'propertyIsEnumerable',\n\t\t'constructor'\n\t];\n\tvar equalsConstructorPrototype = function (o) {\n\t\tvar ctor = o.constructor;\n\t\treturn ctor && ctor.prototype === o;\n\t};\n\tvar excludedKeys = {\n\t\t$applicationCache: true,\n\t\t$console: true,\n\t\t$external: true,\n\t\t$frame: true,\n\t\t$frameElement: true,\n\t\t$frames: true,\n\t\t$innerHeight: true,\n\t\t$innerWidth: true,\n\t\t$onmozfullscreenchange: true,\n\t\t$onmozfullscreenerror: true,\n\t\t$outerHeight: true,\n\t\t$outerWidth: true,\n\t\t$pageXOffset: true,\n\t\t$pageYOffset: true,\n\t\t$parent: true,\n\t\t$scrollLeft: true,\n\t\t$scrollTop: true,\n\t\t$scrollX: true,\n\t\t$scrollY: true,\n\t\t$self: true,\n\t\t$webkitIndexedDB: true,\n\t\t$webkitStorageInfo: true,\n\t\t$window: true\n\t};\n\tvar hasAutomationEqualityBug = (function () {\n\t\t/* global window */\n\t\tif (typeof window === 'undefined') { return false; }\n\t\tfor (var k in window) {\n\t\t\ttry {\n\t\t\t\tif (!excludedKeys['$' + k] && has.call(window, k) && window[k] !== null && typeof window[k] === 'object') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tequalsConstructorPrototype(window[k]);\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}());\n\tvar equalsConstructorPrototypeIfNotBuggy = function (o) {\n\t\t/* global window */\n\t\tif (typeof window === 'undefined' || !hasAutomationEqualityBug) {\n\t\t\treturn equalsConstructorPrototype(o);\n\t\t}\n\t\ttry {\n\t\t\treturn equalsConstructorPrototype(o);\n\t\t} catch (e) {\n\t\t\treturn false;\n\t\t}\n\t};\n\n\tkeysShim = function keys(object) {\n\t\tvar isObject = object !== null && typeof object === 'object';\n\t\tvar isFunction = toStr.call(object) === '[object Function]';\n\t\tvar isArguments = isArgs(object);\n\t\tvar isString = isObject && toStr.call(object) === '[object String]';\n\t\tvar theKeys = [];\n\n\t\tif (!isObject && !isFunction && !isArguments) {\n\t\t\tthrow new TypeError('Object.keys called on a non-object');\n\t\t}\n\n\t\tvar skipProto = hasProtoEnumBug && isFunction;\n\t\tif (isString && object.length > 0 && !has.call(object, 0)) {\n\t\t\tfor (var i = 0; i < object.length; ++i) {\n\t\t\t\ttheKeys.push(String(i));\n\t\t\t}\n\t\t}\n\n\t\tif (isArguments && object.length > 0) {\n\t\t\tfor (var j = 0; j < object.length; ++j) {\n\t\t\t\ttheKeys.push(String(j));\n\t\t\t}\n\t\t} else {\n\t\t\tfor (var name in object) {\n\t\t\t\tif (!(skipProto && name === 'prototype') && has.call(object, name)) {\n\t\t\t\t\ttheKeys.push(String(name));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (hasDontEnumBug) {\n\t\t\tvar skipConstructor = equalsConstructorPrototypeIfNotBuggy(object);\n\n\t\t\tfor (var k = 0; k < dontEnums.length; ++k) {\n\t\t\t\tif (!(skipConstructor && dontEnums[k] === 'constructor') && has.call(object, dontEnums[k])) {\n\t\t\t\t\ttheKeys.push(dontEnums[k]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn theKeys;\n\t};\n}\nmodule.exports = keysShim;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODk4Ny5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsY0FBYyxtQkFBTyxDQUFDLElBQWUsR0FBRztBQUN4QztBQUNBLDJDQUEyQyxnQkFBZ0I7QUFDM0QsdURBQXVEO0FBQ3ZEO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsdUNBQXVDO0FBQ3ZDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxPQUFPO0FBQ1A7QUFDQTtBQUNBO0FBQ0EsS0FBSztBQUNMO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsRUFBRTtBQUNGO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSTtBQUNKO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxtQkFBbUIsbUJBQW1CO0FBQ3RDO0FBQ0E7QUFDQTs7QUFFQTtBQUNBLG1CQUFtQixtQkFBbUI7QUFDdEM7QUFDQTtBQUNBLElBQUk7QUFDSjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQSxtQkFBbUIsc0JBQXNCO0FBQ3pDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvb2JqZWN0LWtleXMvaW1wbGVtZW50YXRpb24uanM/YjE4OSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBrZXlzU2hpbTtcbmlmICghT2JqZWN0LmtleXMpIHtcblx0Ly8gbW9kaWZpZWQgZnJvbSBodHRwczovL2dpdGh1Yi5jb20vZXMtc2hpbXMvZXM1LXNoaW1cblx0dmFyIGhhcyA9IE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHk7XG5cdHZhciB0b1N0ciA9IE9iamVjdC5wcm90b3R5cGUudG9TdHJpbmc7XG5cdHZhciBpc0FyZ3MgPSByZXF1aXJlKCcuL2lzQXJndW1lbnRzJyk7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgZ2xvYmFsLXJlcXVpcmVcblx0dmFyIGlzRW51bWVyYWJsZSA9IE9iamVjdC5wcm90b3R5cGUucHJvcGVydHlJc0VudW1lcmFibGU7XG5cdHZhciBoYXNEb250RW51bUJ1ZyA9ICFpc0VudW1lcmFibGUuY2FsbCh7IHRvU3RyaW5nOiBudWxsIH0sICd0b1N0cmluZycpO1xuXHR2YXIgaGFzUHJvdG9FbnVtQnVnID0gaXNFbnVtZXJhYmxlLmNhbGwoZnVuY3Rpb24gKCkge30sICdwcm90b3R5cGUnKTtcblx0dmFyIGRvbnRFbnVtcyA9IFtcblx0XHQndG9TdHJpbmcnLFxuXHRcdCd0b0xvY2FsZVN0cmluZycsXG5cdFx0J3ZhbHVlT2YnLFxuXHRcdCdoYXNPd25Qcm9wZXJ0eScsXG5cdFx0J2lzUHJvdG90eXBlT2YnLFxuXHRcdCdwcm9wZXJ0eUlzRW51bWVyYWJsZScsXG5cdFx0J2NvbnN0cnVjdG9yJ1xuXHRdO1xuXHR2YXIgZXF1YWxzQ29uc3RydWN0b3JQcm90b3R5cGUgPSBmdW5jdGlvbiAobykge1xuXHRcdHZhciBjdG9yID0gby5jb25zdHJ1Y3Rvcjtcblx0XHRyZXR1cm4gY3RvciAmJiBjdG9yLnByb3RvdHlwZSA9PT0gbztcblx0fTtcblx0dmFyIGV4Y2x1ZGVkS2V5cyA9IHtcblx0XHQkYXBwbGljYXRpb25DYWNoZTogdHJ1ZSxcblx0XHQkY29uc29sZTogdHJ1ZSxcblx0XHQkZXh0ZXJuYWw6IHRydWUsXG5cdFx0JGZyYW1lOiB0cnVlLFxuXHRcdCRmcmFtZUVsZW1lbnQ6IHRydWUsXG5cdFx0JGZyYW1lczogdHJ1ZSxcblx0XHQkaW5uZXJIZWlnaHQ6IHRydWUsXG5cdFx0JGlubmVyV2lkdGg6IHRydWUsXG5cdFx0JG9ubW96ZnVsbHNjcmVlbmNoYW5nZTogdHJ1ZSxcblx0XHQkb25tb3pmdWxsc2NyZWVuZXJyb3I6IHRydWUsXG5cdFx0JG91dGVySGVpZ2h0OiB0cnVlLFxuXHRcdCRvdXRlcldpZHRoOiB0cnVlLFxuXHRcdCRwYWdlWE9mZnNldDogdHJ1ZSxcblx0XHQkcGFnZVlPZmZzZXQ6IHRydWUsXG5cdFx0JHBhcmVudDogdHJ1ZSxcblx0XHQkc2Nyb2xsTGVmdDogdHJ1ZSxcblx0XHQkc2Nyb2xsVG9wOiB0cnVlLFxuXHRcdCRzY3JvbGxYOiB0cnVlLFxuXHRcdCRzY3JvbGxZOiB0cnVlLFxuXHRcdCRzZWxmOiB0cnVlLFxuXHRcdCR3ZWJraXRJbmRleGVkREI6IHRydWUsXG5cdFx0JHdlYmtpdFN0b3JhZ2VJbmZvOiB0cnVlLFxuXHRcdCR3aW5kb3c6IHRydWVcblx0fTtcblx0dmFyIGhhc0F1dG9tYXRpb25FcXVhbGl0eUJ1ZyA9IChmdW5jdGlvbiAoKSB7XG5cdFx0LyogZ2xvYmFsIHdpbmRvdyAqL1xuXHRcdGlmICh0eXBlb2Ygd2luZG93ID09PSAndW5kZWZpbmVkJykgeyByZXR1cm4gZmFsc2U7IH1cblx0XHRmb3IgKHZhciBrIGluIHdpbmRvdykge1xuXHRcdFx0dHJ5IHtcblx0XHRcdFx0aWYgKCFleGNsdWRlZEtleXNbJyQnICsga10gJiYgaGFzLmNhbGwod2luZG93LCBrKSAmJiB3aW5kb3dba10gIT09IG51bGwgJiYgdHlwZW9mIHdpbmRvd1trXSA9PT0gJ29iamVjdCcpIHtcblx0XHRcdFx0XHR0cnkge1xuXHRcdFx0XHRcdFx0ZXF1YWxzQ29uc3RydWN0b3JQcm90b3R5cGUod2luZG93W2tdKTtcblx0XHRcdFx0XHR9IGNhdGNoIChlKSB7XG5cdFx0XHRcdFx0XHRyZXR1cm4gdHJ1ZTtcblx0XHRcdFx0XHR9XG5cdFx0XHRcdH1cblx0XHRcdH0gY2F0Y2ggKGUpIHtcblx0XHRcdFx0cmV0dXJuIHRydWU7XG5cdFx0XHR9XG5cdFx0fVxuXHRcdHJldHVybiBmYWxzZTtcblx0fSgpKTtcblx0dmFyIGVxdWFsc0NvbnN0cnVjdG9yUHJvdG90eXBlSWZOb3RCdWdneSA9IGZ1bmN0aW9uIChvKSB7XG5cdFx0LyogZ2xvYmFsIHdpbmRvdyAqL1xuXHRcdGlmICh0eXBlb2Ygd2luZG93ID09PSAndW5kZWZpbmVkJyB8fCAhaGFzQXV0b21hdGlvbkVxdWFsaXR5QnVnKSB7XG5cdFx0XHRyZXR1cm4gZXF1YWxzQ29uc3RydWN0b3JQcm90b3R5cGUobyk7XG5cdFx0fVxuXHRcdHRyeSB7XG5cdFx0XHRyZXR1cm4gZXF1YWxzQ29uc3RydWN0b3JQcm90b3R5cGUobyk7XG5cdFx0fSBjYXRjaCAoZSkge1xuXHRcdFx0cmV0dXJuIGZhbHNlO1xuXHRcdH1cblx0fTtcblxuXHRrZXlzU2hpbSA9IGZ1bmN0aW9uIGtleXMob2JqZWN0KSB7XG5cdFx0dmFyIGlzT2JqZWN0ID0gb2JqZWN0ICE9PSBudWxsICYmIHR5cGVvZiBvYmplY3QgPT09ICdvYmplY3QnO1xuXHRcdHZhciBpc0Z1bmN0aW9uID0gdG9TdHIuY2FsbChvYmplY3QpID09PSAnW29iamVjdCBGdW5jdGlvbl0nO1xuXHRcdHZhciBpc0FyZ3VtZW50cyA9IGlzQXJncyhvYmplY3QpO1xuXHRcdHZhciBpc1N0cmluZyA9IGlzT2JqZWN0ICYmIHRvU3RyLmNhbGwob2JqZWN0KSA9PT0gJ1tvYmplY3QgU3RyaW5nXSc7XG5cdFx0dmFyIHRoZUtleXMgPSBbXTtcblxuXHRcdGlmICghaXNPYmplY3QgJiYgIWlzRnVuY3Rpb24gJiYgIWlzQXJndW1lbnRzKSB7XG5cdFx0XHR0aHJvdyBuZXcgVHlwZUVycm9yKCdPYmplY3Qua2V5cyBjYWxsZWQgb24gYSBub24tb2JqZWN0Jyk7XG5cdFx0fVxuXG5cdFx0dmFyIHNraXBQcm90byA9IGhhc1Byb3RvRW51bUJ1ZyAmJiBpc0Z1bmN0aW9uO1xuXHRcdGlmIChpc1N0cmluZyAmJiBvYmplY3QubGVuZ3RoID4gMCAmJiAhaGFzLmNhbGwob2JqZWN0LCAwKSkge1xuXHRcdFx0Zm9yICh2YXIgaSA9IDA7IGkgPCBvYmplY3QubGVuZ3RoOyArK2kpIHtcblx0XHRcdFx0dGhlS2V5cy5wdXNoKFN0cmluZyhpKSk7XG5cdFx0XHR9XG5cdFx0fVxuXG5cdFx0aWYgKGlzQXJndW1lbnRzICYmIG9iamVjdC5sZW5ndGggPiAwKSB7XG5cdFx0XHRmb3IgKHZhciBqID0gMDsgaiA8IG9iamVjdC5sZW5ndGg7ICsraikge1xuXHRcdFx0XHR0aGVLZXlzLnB1c2goU3RyaW5nKGopKTtcblx0XHRcdH1cblx0XHR9IGVsc2Uge1xuXHRcdFx0Zm9yICh2YXIgbmFtZSBpbiBvYmplY3QpIHtcblx0XHRcdFx0aWYgKCEoc2tpcFByb3RvICYmIG5hbWUgPT09ICdwcm90b3R5cGUnKSAmJiBoYXMuY2FsbChvYmplY3QsIG5hbWUpKSB7XG5cdFx0XHRcdFx0dGhlS2V5cy5wdXNoKFN0cmluZyhuYW1lKSk7XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHR9XG5cblx0XHRpZiAoaGFzRG9udEVudW1CdWcpIHtcblx0XHRcdHZhciBza2lwQ29uc3RydWN0b3IgPSBlcXVhbHNDb25zdHJ1Y3RvclByb3RvdHlwZUlmTm90QnVnZ3kob2JqZWN0KTtcblxuXHRcdFx0Zm9yICh2YXIgayA9IDA7IGsgPCBkb250RW51bXMubGVuZ3RoOyArK2spIHtcblx0XHRcdFx0aWYgKCEoc2tpcENvbnN0cnVjdG9yICYmIGRvbnRFbnVtc1trXSA9PT0gJ2NvbnN0cnVjdG9yJykgJiYgaGFzLmNhbGwob2JqZWN0LCBkb250RW51bXNba10pKSB7XG5cdFx0XHRcdFx0dGhlS2V5cy5wdXNoKGRvbnRFbnVtc1trXSk7XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHR9XG5cdFx0cmV0dXJuIHRoZUtleXM7XG5cdH07XG59XG5tb2R1bGUuZXhwb3J0cyA9IGtleXNTaGltO1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///8987\n")},2215:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar slice = Array.prototype.slice;\nvar isArgs = __webpack_require__(1414);\n\nvar origKeys = Object.keys;\nvar keysShim = origKeys ? function keys(o) { return origKeys(o); } : __webpack_require__(8987);\n\nvar originalKeys = Object.keys;\n\nkeysShim.shim = function shimObjectKeys() {\n\tif (Object.keys) {\n\t\tvar keysWorksWithArguments = (function () {\n\t\t\t// Safari 5.0 bug\n\t\t\tvar args = Object.keys(arguments);\n\t\t\treturn args && args.length === arguments.length;\n\t\t}(1, 2));\n\t\tif (!keysWorksWithArguments) {\n\t\t\tObject.keys = function keys(object) { // eslint-disable-line func-name-matching\n\t\t\t\tif (isArgs(object)) {\n\t\t\t\t\treturn originalKeys(slice.call(object));\n\t\t\t\t}\n\t\t\t\treturn originalKeys(object);\n\t\t\t};\n\t\t}\n\t} else {\n\t\tObject.keys = keysShim;\n\t}\n\treturn Object.keys || keysShim;\n};\n\nmodule.exports = keysShim;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjIxNS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBLGFBQWEsbUJBQU8sQ0FBQyxJQUFlOztBQUVwQztBQUNBLDZDQUE2QyxzQkFBc0IsRUFBRSxtQkFBTyxDQUFDLElBQWtCOztBQUUvRjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0g7QUFDQSx5Q0FBeUM7QUFDekM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsR0FBRztBQUNIO0FBQ0E7QUFDQTtBQUNBOztBQUVBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9vYmplY3Qta2V5cy9pbmRleC5qcz9kNmM3Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIHNsaWNlID0gQXJyYXkucHJvdG90eXBlLnNsaWNlO1xudmFyIGlzQXJncyA9IHJlcXVpcmUoJy4vaXNBcmd1bWVudHMnKTtcblxudmFyIG9yaWdLZXlzID0gT2JqZWN0LmtleXM7XG52YXIga2V5c1NoaW0gPSBvcmlnS2V5cyA/IGZ1bmN0aW9uIGtleXMobykgeyByZXR1cm4gb3JpZ0tleXMobyk7IH0gOiByZXF1aXJlKCcuL2ltcGxlbWVudGF0aW9uJyk7XG5cbnZhciBvcmlnaW5hbEtleXMgPSBPYmplY3Qua2V5cztcblxua2V5c1NoaW0uc2hpbSA9IGZ1bmN0aW9uIHNoaW1PYmplY3RLZXlzKCkge1xuXHRpZiAoT2JqZWN0LmtleXMpIHtcblx0XHR2YXIga2V5c1dvcmtzV2l0aEFyZ3VtZW50cyA9IChmdW5jdGlvbiAoKSB7XG5cdFx0XHQvLyBTYWZhcmkgNS4wIGJ1Z1xuXHRcdFx0dmFyIGFyZ3MgPSBPYmplY3Qua2V5cyhhcmd1bWVudHMpO1xuXHRcdFx0cmV0dXJuIGFyZ3MgJiYgYXJncy5sZW5ndGggPT09IGFyZ3VtZW50cy5sZW5ndGg7XG5cdFx0fSgxLCAyKSk7XG5cdFx0aWYgKCFrZXlzV29ya3NXaXRoQXJndW1lbnRzKSB7XG5cdFx0XHRPYmplY3Qua2V5cyA9IGZ1bmN0aW9uIGtleXMob2JqZWN0KSB7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgZnVuYy1uYW1lLW1hdGNoaW5nXG5cdFx0XHRcdGlmIChpc0FyZ3Mob2JqZWN0KSkge1xuXHRcdFx0XHRcdHJldHVybiBvcmlnaW5hbEtleXMoc2xpY2UuY2FsbChvYmplY3QpKTtcblx0XHRcdFx0fVxuXHRcdFx0XHRyZXR1cm4gb3JpZ2luYWxLZXlzKG9iamVjdCk7XG5cdFx0XHR9O1xuXHRcdH1cblx0fSBlbHNlIHtcblx0XHRPYmplY3Qua2V5cyA9IGtleXNTaGltO1xuXHR9XG5cdHJldHVybiBPYmplY3Qua2V5cyB8fCBrZXlzU2hpbTtcbn07XG5cbm1vZHVsZS5leHBvcnRzID0ga2V5c1NoaW07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///2215\n")},1414:function(module){"use strict";eval("\n\nvar toStr = Object.prototype.toString;\n\nmodule.exports = function isArguments(value) {\n\tvar str = toStr.call(value);\n\tvar isArgs = str === '[object Arguments]';\n\tif (!isArgs) {\n\t\tisArgs = str !== '[object Array]' &&\n\t\t\tvalue !== null &&\n\t\t\ttypeof value === 'object' &&\n\t\t\ttypeof value.length === 'number' &&\n\t\t\tvalue.length >= 0 &&\n\t\t\ttoStr.call(value.callee) === '[object Function]';\n\t}\n\treturn isArgs;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTQxNC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvb2JqZWN0LWtleXMvaXNBcmd1bWVudHMuanM/ZDRhYiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciB0b1N0ciA9IE9iamVjdC5wcm90b3R5cGUudG9TdHJpbmc7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaXNBcmd1bWVudHModmFsdWUpIHtcblx0dmFyIHN0ciA9IHRvU3RyLmNhbGwodmFsdWUpO1xuXHR2YXIgaXNBcmdzID0gc3RyID09PSAnW29iamVjdCBBcmd1bWVudHNdJztcblx0aWYgKCFpc0FyZ3MpIHtcblx0XHRpc0FyZ3MgPSBzdHIgIT09ICdbb2JqZWN0IEFycmF5XScgJiZcblx0XHRcdHZhbHVlICE9PSBudWxsICYmXG5cdFx0XHR0eXBlb2YgdmFsdWUgPT09ICdvYmplY3QnICYmXG5cdFx0XHR0eXBlb2YgdmFsdWUubGVuZ3RoID09PSAnbnVtYmVyJyAmJlxuXHRcdFx0dmFsdWUubGVuZ3RoID49IDAgJiZcblx0XHRcdHRvU3RyLmNhbGwodmFsdWUuY2FsbGVlKSA9PT0gJ1tvYmplY3QgRnVuY3Rpb25dJztcblx0fVxuXHRyZXR1cm4gaXNBcmdzO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///1414\n")},3697:function(module){"use strict";eval("\n\nvar $Object = Object;\nvar $TypeError = TypeError;\n\nmodule.exports = function flags() {\n\tif (this != null && this !== $Object(this)) {\n\t\tthrow new $TypeError('RegExp.prototype.flags getter called on non-object');\n\t}\n\tvar result = '';\n\tif (this.global) {\n\t\tresult += 'g';\n\t}\n\tif (this.ignoreCase) {\n\t\tresult += 'i';\n\t}\n\tif (this.multiline) {\n\t\tresult += 'm';\n\t}\n\tif (this.dotAll) {\n\t\tresult += 's';\n\t}\n\tif (this.unicode) {\n\t\tresult += 'u';\n\t}\n\tif (this.sticky) {\n\t\tresult += 'y';\n\t}\n\treturn result;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzY5Ny5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9yZWdleHAucHJvdG90eXBlLmZsYWdzL2ltcGxlbWVudGF0aW9uLmpzPzU3MDgiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgJE9iamVjdCA9IE9iamVjdDtcbnZhciAkVHlwZUVycm9yID0gVHlwZUVycm9yO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIGZsYWdzKCkge1xuXHRpZiAodGhpcyAhPSBudWxsICYmIHRoaXMgIT09ICRPYmplY3QodGhpcykpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignUmVnRXhwLnByb3RvdHlwZS5mbGFncyBnZXR0ZXIgY2FsbGVkIG9uIG5vbi1vYmplY3QnKTtcblx0fVxuXHR2YXIgcmVzdWx0ID0gJyc7XG5cdGlmICh0aGlzLmdsb2JhbCkge1xuXHRcdHJlc3VsdCArPSAnZyc7XG5cdH1cblx0aWYgKHRoaXMuaWdub3JlQ2FzZSkge1xuXHRcdHJlc3VsdCArPSAnaSc7XG5cdH1cblx0aWYgKHRoaXMubXVsdGlsaW5lKSB7XG5cdFx0cmVzdWx0ICs9ICdtJztcblx0fVxuXHRpZiAodGhpcy5kb3RBbGwpIHtcblx0XHRyZXN1bHQgKz0gJ3MnO1xuXHR9XG5cdGlmICh0aGlzLnVuaWNvZGUpIHtcblx0XHRyZXN1bHQgKz0gJ3UnO1xuXHR9XG5cdGlmICh0aGlzLnN0aWNreSkge1xuXHRcdHJlc3VsdCArPSAneSc7XG5cdH1cblx0cmV0dXJuIHJlc3VsdDtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3697\n")},2847:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar define = __webpack_require__(4289);\nvar callBind = __webpack_require__(5559);\n\nvar implementation = __webpack_require__(3697);\nvar getPolyfill = __webpack_require__(1721);\nvar shim = __webpack_require__(2753);\n\nvar flagsBound = callBind(implementation);\n\ndefine(flagsBound, {\n\tgetPolyfill: getPolyfill,\n\timplementation: implementation,\n\tshim: shim\n});\n\nmodule.exports = flagsBound;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjg0Ny5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixhQUFhLG1CQUFPLENBQUMsSUFBbUI7QUFDeEMsZUFBZSxtQkFBTyxDQUFDLElBQVc7O0FBRWxDLHFCQUFxQixtQkFBTyxDQUFDLElBQWtCO0FBQy9DLGtCQUFrQixtQkFBTyxDQUFDLElBQVk7QUFDdEMsV0FBVyxtQkFBTyxDQUFDLElBQVE7O0FBRTNCOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsQ0FBQzs7QUFFRCIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvcmVnZXhwLnByb3RvdHlwZS5mbGFncy9pbmRleC5qcz9lNzEwIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGRlZmluZSA9IHJlcXVpcmUoJ2RlZmluZS1wcm9wZXJ0aWVzJyk7XG52YXIgY2FsbEJpbmQgPSByZXF1aXJlKCdjYWxsLWJpbmQnKTtcblxudmFyIGltcGxlbWVudGF0aW9uID0gcmVxdWlyZSgnLi9pbXBsZW1lbnRhdGlvbicpO1xudmFyIGdldFBvbHlmaWxsID0gcmVxdWlyZSgnLi9wb2x5ZmlsbCcpO1xudmFyIHNoaW0gPSByZXF1aXJlKCcuL3NoaW0nKTtcblxudmFyIGZsYWdzQm91bmQgPSBjYWxsQmluZChpbXBsZW1lbnRhdGlvbik7XG5cbmRlZmluZShmbGFnc0JvdW5kLCB7XG5cdGdldFBvbHlmaWxsOiBnZXRQb2x5ZmlsbCxcblx0aW1wbGVtZW50YXRpb246IGltcGxlbWVudGF0aW9uLFxuXHRzaGltOiBzaGltXG59KTtcblxubW9kdWxlLmV4cG9ydHMgPSBmbGFnc0JvdW5kO1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///2847\n")},1721:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar implementation = __webpack_require__(3697);\n\nvar supportsDescriptors = __webpack_require__(4289).supportsDescriptors;\nvar $gOPD = Object.getOwnPropertyDescriptor;\nvar $TypeError = TypeError;\n\nmodule.exports = function getPolyfill() {\n\tif (!supportsDescriptors) {\n\t\tthrow new $TypeError('RegExp.prototype.flags requires a true ES5 environment that supports property descriptors');\n\t}\n\tif ((/a/mig).flags === 'gim') {\n\t\tvar descriptor = $gOPD(RegExp.prototype, 'flags');\n\t\tif (descriptor && typeof descriptor.get === 'function' && typeof (/a/).dotAll === 'boolean') {\n\t\t\treturn descriptor.get;\n\t\t}\n\t}\n\treturn implementation;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTcyMS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixxQkFBcUIsbUJBQU8sQ0FBQyxJQUFrQjs7QUFFL0MsMEJBQTBCLDZDQUFnRDtBQUMxRTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvcmVnZXhwLnByb3RvdHlwZS5mbGFncy9wb2x5ZmlsbC5qcz81N2VjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGltcGxlbWVudGF0aW9uID0gcmVxdWlyZSgnLi9pbXBsZW1lbnRhdGlvbicpO1xuXG52YXIgc3VwcG9ydHNEZXNjcmlwdG9ycyA9IHJlcXVpcmUoJ2RlZmluZS1wcm9wZXJ0aWVzJykuc3VwcG9ydHNEZXNjcmlwdG9ycztcbnZhciAkZ09QRCA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3I7XG52YXIgJFR5cGVFcnJvciA9IFR5cGVFcnJvcjtcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBnZXRQb2x5ZmlsbCgpIHtcblx0aWYgKCFzdXBwb3J0c0Rlc2NyaXB0b3JzKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ1JlZ0V4cC5wcm90b3R5cGUuZmxhZ3MgcmVxdWlyZXMgYSB0cnVlIEVTNSBlbnZpcm9ubWVudCB0aGF0IHN1cHBvcnRzIHByb3BlcnR5IGRlc2NyaXB0b3JzJyk7XG5cdH1cblx0aWYgKCgvYS9taWcpLmZsYWdzID09PSAnZ2ltJykge1xuXHRcdHZhciBkZXNjcmlwdG9yID0gJGdPUEQoUmVnRXhwLnByb3RvdHlwZSwgJ2ZsYWdzJyk7XG5cdFx0aWYgKGRlc2NyaXB0b3IgJiYgdHlwZW9mIGRlc2NyaXB0b3IuZ2V0ID09PSAnZnVuY3Rpb24nICYmIHR5cGVvZiAoL2EvKS5kb3RBbGwgPT09ICdib29sZWFuJykge1xuXHRcdFx0cmV0dXJuIGRlc2NyaXB0b3IuZ2V0O1xuXHRcdH1cblx0fVxuXHRyZXR1cm4gaW1wbGVtZW50YXRpb247XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///1721\n")},2753:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar supportsDescriptors = __webpack_require__(4289).supportsDescriptors;\nvar getPolyfill = __webpack_require__(1721);\nvar gOPD = Object.getOwnPropertyDescriptor;\nvar defineProperty = Object.defineProperty;\nvar TypeErr = TypeError;\nvar getProto = Object.getPrototypeOf;\nvar regex = /a/;\n\nmodule.exports = function shimFlags() {\n\tif (!supportsDescriptors || !getProto) {\n\t\tthrow new TypeErr('RegExp.prototype.flags requires a true ES5 environment that supports property descriptors');\n\t}\n\tvar polyfill = getPolyfill();\n\tvar proto = getProto(regex);\n\tvar descriptor = gOPD(proto, 'flags');\n\tif (!descriptor || descriptor.get !== polyfill) {\n\t\tdefineProperty(proto, 'flags', {\n\t\t\tconfigurable: true,\n\t\t\tenumerable: false,\n\t\t\tget: polyfill\n\t\t});\n\t}\n\treturn polyfill;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjc1My5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYiwwQkFBMEIsNkNBQWdEO0FBQzFFLGtCQUFrQixtQkFBTyxDQUFDLElBQVk7QUFDdEM7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0g7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9yZWdleHAucHJvdG90eXBlLmZsYWdzL3NoaW0uanM/MWM3ZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBzdXBwb3J0c0Rlc2NyaXB0b3JzID0gcmVxdWlyZSgnZGVmaW5lLXByb3BlcnRpZXMnKS5zdXBwb3J0c0Rlc2NyaXB0b3JzO1xudmFyIGdldFBvbHlmaWxsID0gcmVxdWlyZSgnLi9wb2x5ZmlsbCcpO1xudmFyIGdPUEQgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yO1xudmFyIGRlZmluZVByb3BlcnR5ID0gT2JqZWN0LmRlZmluZVByb3BlcnR5O1xudmFyIFR5cGVFcnIgPSBUeXBlRXJyb3I7XG52YXIgZ2V0UHJvdG8gPSBPYmplY3QuZ2V0UHJvdG90eXBlT2Y7XG52YXIgcmVnZXggPSAvYS87XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gc2hpbUZsYWdzKCkge1xuXHRpZiAoIXN1cHBvcnRzRGVzY3JpcHRvcnMgfHwgIWdldFByb3RvKSB7XG5cdFx0dGhyb3cgbmV3IFR5cGVFcnIoJ1JlZ0V4cC5wcm90b3R5cGUuZmxhZ3MgcmVxdWlyZXMgYSB0cnVlIEVTNSBlbnZpcm9ubWVudCB0aGF0IHN1cHBvcnRzIHByb3BlcnR5IGRlc2NyaXB0b3JzJyk7XG5cdH1cblx0dmFyIHBvbHlmaWxsID0gZ2V0UG9seWZpbGwoKTtcblx0dmFyIHByb3RvID0gZ2V0UHJvdG8ocmVnZXgpO1xuXHR2YXIgZGVzY3JpcHRvciA9IGdPUEQocHJvdG8sICdmbGFncycpO1xuXHRpZiAoIWRlc2NyaXB0b3IgfHwgZGVzY3JpcHRvci5nZXQgIT09IHBvbHlmaWxsKSB7XG5cdFx0ZGVmaW5lUHJvcGVydHkocHJvdG8sICdmbGFncycsIHtcblx0XHRcdGNvbmZpZ3VyYWJsZTogdHJ1ZSxcblx0XHRcdGVudW1lcmFibGU6IGZhbHNlLFxuXHRcdFx0Z2V0OiBwb2x5ZmlsbFxuXHRcdH0pO1xuXHR9XG5cdHJldHVybiBwb2x5ZmlsbDtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///2753\n")},7478:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\nvar callBound = __webpack_require__(1924);\nvar inspect = __webpack_require__(631);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar $WeakMap = GetIntrinsic('%WeakMap%', true);\nvar $Map = GetIntrinsic('%Map%', true);\n\nvar $weakMapGet = callBound('WeakMap.prototype.get', true);\nvar $weakMapSet = callBound('WeakMap.prototype.set', true);\nvar $weakMapHas = callBound('WeakMap.prototype.has', true);\nvar $mapGet = callBound('Map.prototype.get', true);\nvar $mapSet = callBound('Map.prototype.set', true);\nvar $mapHas = callBound('Map.prototype.has', true);\n\n/*\n * This function traverses the list returning the node corresponding to the\n * given key.\n *\n * That node is also moved to the head of the list, so that if it's accessed\n * again we don't need to traverse the whole list. By doing so, all the recently\n * used nodes can be accessed relatively quickly.\n */\nvar listGetNode = function (list, key) { // eslint-disable-line consistent-return\n\tfor (var prev = list, curr; (curr = prev.next) !== null; prev = curr) {\n\t\tif (curr.key === key) {\n\t\t\tprev.next = curr.next;\n\t\t\tcurr.next = list.next;\n\t\t\tlist.next = curr; // eslint-disable-line no-param-reassign\n\t\t\treturn curr;\n\t\t}\n\t}\n};\n\nvar listGet = function (objects, key) {\n\tvar node = listGetNode(objects, key);\n\treturn node && node.value;\n};\nvar listSet = function (objects, key, value) {\n\tvar node = listGetNode(objects, key);\n\tif (node) {\n\t\tnode.value = value;\n\t} else {\n\t\t// Prepend the new node to the beginning of the list\n\t\tobjects.next = { // eslint-disable-line no-param-reassign\n\t\t\tkey: key,\n\t\t\tnext: objects.next,\n\t\t\tvalue: value\n\t\t};\n\t}\n};\nvar listHas = function (objects, key) {\n\treturn !!listGetNode(objects, key);\n};\n\nmodule.exports = function getSideChannel() {\n\tvar $wm;\n\tvar $m;\n\tvar $o;\n\tvar channel = {\n\t\tassert: function (key) {\n\t\t\tif (!channel.has(key)) {\n\t\t\t\tthrow new $TypeError('Side channel does not contain ' + inspect(key));\n\t\t\t}\n\t\t},\n\t\tget: function (key) { // eslint-disable-line consistent-return\n\t\t\tif ($WeakMap && key && (typeof key === 'object' || typeof key === 'function')) {\n\t\t\t\tif ($wm) {\n\t\t\t\t\treturn $weakMapGet($wm, key);\n\t\t\t\t}\n\t\t\t} else if ($Map) {\n\t\t\t\tif ($m) {\n\t\t\t\t\treturn $mapGet($m, key);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif ($o) { // eslint-disable-line no-lonely-if\n\t\t\t\t\treturn listGet($o, key);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\thas: function (key) {\n\t\t\tif ($WeakMap && key && (typeof key === 'object' || typeof key === 'function')) {\n\t\t\t\tif ($wm) {\n\t\t\t\t\treturn $weakMapHas($wm, key);\n\t\t\t\t}\n\t\t\t} else if ($Map) {\n\t\t\t\tif ($m) {\n\t\t\t\t\treturn $mapHas($m, key);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif ($o) { // eslint-disable-line no-lonely-if\n\t\t\t\t\treturn listHas($o, key);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t},\n\t\tset: function (key, value) {\n\t\t\tif ($WeakMap && key && (typeof key === 'object' || typeof key === 'function')) {\n\t\t\t\tif (!$wm) {\n\t\t\t\t\t$wm = new $WeakMap();\n\t\t\t\t}\n\t\t\t\t$weakMapSet($wm, key, value);\n\t\t\t} else if ($Map) {\n\t\t\t\tif (!$m) {\n\t\t\t\t\t$m = new $Map();\n\t\t\t\t}\n\t\t\t\t$mapSet($m, key, value);\n\t\t\t} else {\n\t\t\t\tif (!$o) {\n\t\t\t\t\t/*\n\t\t\t\t\t * Initialize the linked list as an empty node, so that we don't have\n\t\t\t\t\t * to special-case handling of the first node: we can always refer to\n\t\t\t\t\t * it as (previous node).next, instead of something like (list).head\n\t\t\t\t\t */\n\t\t\t\t\t$o = { key: {}, next: null };\n\t\t\t\t}\n\t\t\t\tlistSet($o, key, value);\n\t\t\t}\n\t\t}\n\t};\n\treturn channel;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzQ3OC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlO0FBQzFDLGdCQUFnQixtQkFBTyxDQUFDLElBQXFCO0FBQzdDLGNBQWMsbUJBQU8sQ0FBQyxHQUFnQjs7QUFFdEM7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EseUNBQXlDO0FBQ3pDLDZCQUE2Qiw2QkFBNkI7QUFDMUQ7QUFDQTtBQUNBO0FBQ0EscUJBQXFCO0FBQ3JCO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0g7QUFDQSxtQkFBbUI7QUFDbkI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEdBQUc7QUFDSCx3QkFBd0I7QUFDeEI7QUFDQTtBQUNBO0FBQ0E7QUFDQSxLQUFLO0FBQ0w7QUFDQTtBQUNBO0FBQ0EsS0FBSztBQUNMLGNBQWM7QUFDZDtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0g7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEtBQUs7QUFDTDtBQUNBO0FBQ0E7QUFDQSxLQUFLO0FBQ0wsY0FBYztBQUNkO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsR0FBRztBQUNIO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEtBQUs7QUFDTDtBQUNBO0FBQ0E7QUFDQTtBQUNBLEtBQUs7QUFDTDtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxZQUFZLE9BQU87QUFDbkI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL3NpZGUtY2hhbm5lbC9pbmRleC5qcz81NDAyIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcbnZhciBjYWxsQm91bmQgPSByZXF1aXJlKCdjYWxsLWJpbmQvY2FsbEJvdW5kJyk7XG52YXIgaW5zcGVjdCA9IHJlcXVpcmUoJ29iamVjdC1pbnNwZWN0Jyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xudmFyICRXZWFrTWFwID0gR2V0SW50cmluc2ljKCclV2Vha01hcCUnLCB0cnVlKTtcbnZhciAkTWFwID0gR2V0SW50cmluc2ljKCclTWFwJScsIHRydWUpO1xuXG52YXIgJHdlYWtNYXBHZXQgPSBjYWxsQm91bmQoJ1dlYWtNYXAucHJvdG90eXBlLmdldCcsIHRydWUpO1xudmFyICR3ZWFrTWFwU2V0ID0gY2FsbEJvdW5kKCdXZWFrTWFwLnByb3RvdHlwZS5zZXQnLCB0cnVlKTtcbnZhciAkd2Vha01hcEhhcyA9IGNhbGxCb3VuZCgnV2Vha01hcC5wcm90b3R5cGUuaGFzJywgdHJ1ZSk7XG52YXIgJG1hcEdldCA9IGNhbGxCb3VuZCgnTWFwLnByb3RvdHlwZS5nZXQnLCB0cnVlKTtcbnZhciAkbWFwU2V0ID0gY2FsbEJvdW5kKCdNYXAucHJvdG90eXBlLnNldCcsIHRydWUpO1xudmFyICRtYXBIYXMgPSBjYWxsQm91bmQoJ01hcC5wcm90b3R5cGUuaGFzJywgdHJ1ZSk7XG5cbi8qXG4gKiBUaGlzIGZ1bmN0aW9uIHRyYXZlcnNlcyB0aGUgbGlzdCByZXR1cm5pbmcgdGhlIG5vZGUgY29ycmVzcG9uZGluZyB0byB0aGVcbiAqIGdpdmVuIGtleS5cbiAqXG4gKiBUaGF0IG5vZGUgaXMgYWxzbyBtb3ZlZCB0byB0aGUgaGVhZCBvZiB0aGUgbGlzdCwgc28gdGhhdCBpZiBpdCdzIGFjY2Vzc2VkXG4gKiBhZ2FpbiB3ZSBkb24ndCBuZWVkIHRvIHRyYXZlcnNlIHRoZSB3aG9sZSBsaXN0LiBCeSBkb2luZyBzbywgYWxsIHRoZSByZWNlbnRseVxuICogdXNlZCBub2RlcyBjYW4gYmUgYWNjZXNzZWQgcmVsYXRpdmVseSBxdWlja2x5LlxuICovXG52YXIgbGlzdEdldE5vZGUgPSBmdW5jdGlvbiAobGlzdCwga2V5KSB7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgY29uc2lzdGVudC1yZXR1cm5cblx0Zm9yICh2YXIgcHJldiA9IGxpc3QsIGN1cnI7IChjdXJyID0gcHJldi5uZXh0KSAhPT0gbnVsbDsgcHJldiA9IGN1cnIpIHtcblx0XHRpZiAoY3Vyci5rZXkgPT09IGtleSkge1xuXHRcdFx0cHJldi5uZXh0ID0gY3Vyci5uZXh0O1xuXHRcdFx0Y3Vyci5uZXh0ID0gbGlzdC5uZXh0O1xuXHRcdFx0bGlzdC5uZXh0ID0gY3VycjsgLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1wYXJhbS1yZWFzc2lnblxuXHRcdFx0cmV0dXJuIGN1cnI7XG5cdFx0fVxuXHR9XG59O1xuXG52YXIgbGlzdEdldCA9IGZ1bmN0aW9uIChvYmplY3RzLCBrZXkpIHtcblx0dmFyIG5vZGUgPSBsaXN0R2V0Tm9kZShvYmplY3RzLCBrZXkpO1xuXHRyZXR1cm4gbm9kZSAmJiBub2RlLnZhbHVlO1xufTtcbnZhciBsaXN0U2V0ID0gZnVuY3Rpb24gKG9iamVjdHMsIGtleSwgdmFsdWUpIHtcblx0dmFyIG5vZGUgPSBsaXN0R2V0Tm9kZShvYmplY3RzLCBrZXkpO1xuXHRpZiAobm9kZSkge1xuXHRcdG5vZGUudmFsdWUgPSB2YWx1ZTtcblx0fSBlbHNlIHtcblx0XHQvLyBQcmVwZW5kIHRoZSBuZXcgbm9kZSB0byB0aGUgYmVnaW5uaW5nIG9mIHRoZSBsaXN0XG5cdFx0b2JqZWN0cy5uZXh0ID0geyAvLyBlc2xpbnQtZGlzYWJsZS1saW5lIG5vLXBhcmFtLXJlYXNzaWduXG5cdFx0XHRrZXk6IGtleSxcblx0XHRcdG5leHQ6IG9iamVjdHMubmV4dCxcblx0XHRcdHZhbHVlOiB2YWx1ZVxuXHRcdH07XG5cdH1cbn07XG52YXIgbGlzdEhhcyA9IGZ1bmN0aW9uIChvYmplY3RzLCBrZXkpIHtcblx0cmV0dXJuICEhbGlzdEdldE5vZGUob2JqZWN0cywga2V5KTtcbn07XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gZ2V0U2lkZUNoYW5uZWwoKSB7XG5cdHZhciAkd207XG5cdHZhciAkbTtcblx0dmFyICRvO1xuXHR2YXIgY2hhbm5lbCA9IHtcblx0XHRhc3NlcnQ6IGZ1bmN0aW9uIChrZXkpIHtcblx0XHRcdGlmICghY2hhbm5lbC5oYXMoa2V5KSkge1xuXHRcdFx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignU2lkZSBjaGFubmVsIGRvZXMgbm90IGNvbnRhaW4gJyArIGluc3BlY3Qoa2V5KSk7XG5cdFx0XHR9XG5cdFx0fSxcblx0XHRnZXQ6IGZ1bmN0aW9uIChrZXkpIHsgLy8gZXNsaW50LWRpc2FibGUtbGluZSBjb25zaXN0ZW50LXJldHVyblxuXHRcdFx0aWYgKCRXZWFrTWFwICYmIGtleSAmJiAodHlwZW9mIGtleSA9PT0gJ29iamVjdCcgfHwgdHlwZW9mIGtleSA9PT0gJ2Z1bmN0aW9uJykpIHtcblx0XHRcdFx0aWYgKCR3bSkge1xuXHRcdFx0XHRcdHJldHVybiAkd2Vha01hcEdldCgkd20sIGtleSk7XG5cdFx0XHRcdH1cblx0XHRcdH0gZWxzZSBpZiAoJE1hcCkge1xuXHRcdFx0XHRpZiAoJG0pIHtcblx0XHRcdFx0XHRyZXR1cm4gJG1hcEdldCgkbSwga2V5KTtcblx0XHRcdFx0fVxuXHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0aWYgKCRvKSB7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tbG9uZWx5LWlmXG5cdFx0XHRcdFx0cmV0dXJuIGxpc3RHZXQoJG8sIGtleSk7XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHR9LFxuXHRcdGhhczogZnVuY3Rpb24gKGtleSkge1xuXHRcdFx0aWYgKCRXZWFrTWFwICYmIGtleSAmJiAodHlwZW9mIGtleSA9PT0gJ29iamVjdCcgfHwgdHlwZW9mIGtleSA9PT0gJ2Z1bmN0aW9uJykpIHtcblx0XHRcdFx0aWYgKCR3bSkge1xuXHRcdFx0XHRcdHJldHVybiAkd2Vha01hcEhhcygkd20sIGtleSk7XG5cdFx0XHRcdH1cblx0XHRcdH0gZWxzZSBpZiAoJE1hcCkge1xuXHRcdFx0XHRpZiAoJG0pIHtcblx0XHRcdFx0XHRyZXR1cm4gJG1hcEhhcygkbSwga2V5KTtcblx0XHRcdFx0fVxuXHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0aWYgKCRvKSB7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tbG9uZWx5LWlmXG5cdFx0XHRcdFx0cmV0dXJuIGxpc3RIYXMoJG8sIGtleSk7XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHRcdHJldHVybiBmYWxzZTtcblx0XHR9LFxuXHRcdHNldDogZnVuY3Rpb24gKGtleSwgdmFsdWUpIHtcblx0XHRcdGlmICgkV2Vha01hcCAmJiBrZXkgJiYgKHR5cGVvZiBrZXkgPT09ICdvYmplY3QnIHx8IHR5cGVvZiBrZXkgPT09ICdmdW5jdGlvbicpKSB7XG5cdFx0XHRcdGlmICghJHdtKSB7XG5cdFx0XHRcdFx0JHdtID0gbmV3ICRXZWFrTWFwKCk7XG5cdFx0XHRcdH1cblx0XHRcdFx0JHdlYWtNYXBTZXQoJHdtLCBrZXksIHZhbHVlKTtcblx0XHRcdH0gZWxzZSBpZiAoJE1hcCkge1xuXHRcdFx0XHRpZiAoISRtKSB7XG5cdFx0XHRcdFx0JG0gPSBuZXcgJE1hcCgpO1xuXHRcdFx0XHR9XG5cdFx0XHRcdCRtYXBTZXQoJG0sIGtleSwgdmFsdWUpO1xuXHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0aWYgKCEkbykge1xuXHRcdFx0XHRcdC8qXG5cdFx0XHRcdFx0ICogSW5pdGlhbGl6ZSB0aGUgbGlua2VkIGxpc3QgYXMgYW4gZW1wdHkgbm9kZSwgc28gdGhhdCB3ZSBkb24ndCBoYXZlXG5cdFx0XHRcdFx0ICogdG8gc3BlY2lhbC1jYXNlIGhhbmRsaW5nIG9mIHRoZSBmaXJzdCBub2RlOiB3ZSBjYW4gYWx3YXlzIHJlZmVyIHRvXG5cdFx0XHRcdFx0ICogaXQgYXMgKHByZXZpb3VzIG5vZGUpLm5leHQsIGluc3RlYWQgb2Ygc29tZXRoaW5nIGxpa2UgKGxpc3QpLmhlYWRcblx0XHRcdFx0XHQgKi9cblx0XHRcdFx0XHQkbyA9IHsga2V5OiB7fSwgbmV4dDogbnVsbCB9O1xuXHRcdFx0XHR9XG5cdFx0XHRcdGxpc3RTZXQoJG8sIGtleSwgdmFsdWUpO1xuXHRcdFx0fVxuXHRcdH1cblx0fTtcblx0cmV0dXJuIGNoYW5uZWw7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///7478\n")},9505:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar Call = __webpack_require__(581);\nvar Get = __webpack_require__(1391);\nvar GetMethod = __webpack_require__(7364);\nvar IsRegExp = __webpack_require__(840);\nvar ToString = __webpack_require__(6846);\nvar RequireObjectCoercible = __webpack_require__(9619);\nvar callBound = __webpack_require__(1924);\nvar hasSymbols = __webpack_require__(1405)();\nvar flagsGetter = __webpack_require__(2847);\n\nvar $indexOf = callBound('String.prototype.indexOf');\n\nvar regexpMatchAllPolyfill = __webpack_require__(6966);\n\nvar getMatcher = function getMatcher(regexp) { // eslint-disable-line consistent-return\n\tvar matcherPolyfill = regexpMatchAllPolyfill();\n\tif (hasSymbols && typeof Symbol.matchAll === 'symbol') {\n\t\tvar matcher = GetMethod(regexp, Symbol.matchAll);\n\t\tif (matcher === RegExp.prototype[Symbol.matchAll] && matcher !== matcherPolyfill) {\n\t\t\treturn matcherPolyfill;\n\t\t}\n\t\treturn matcher;\n\t}\n\t// fallback for pre-Symbol.matchAll environments\n\tif (IsRegExp(regexp)) {\n\t\treturn matcherPolyfill;\n\t}\n};\n\nmodule.exports = function matchAll(regexp) {\n\tvar O = RequireObjectCoercible(this);\n\n\tif (typeof regexp !== 'undefined' && regexp !== null) {\n\t\tvar isRegExp = IsRegExp(regexp);\n\t\tif (isRegExp) {\n\t\t\t// workaround for older engines that lack RegExp.prototype.flags\n\t\t\tvar flags = 'flags' in regexp ? Get(regexp, 'flags') : flagsGetter(regexp);\n\t\t\tRequireObjectCoercible(flags);\n\t\t\tif ($indexOf(ToString(flags), 'g') < 0) {\n\t\t\t\tthrow new TypeError('matchAll requires a global regular expression');\n\t\t\t}\n\t\t}\n\n\t\tvar matcher = getMatcher(regexp);\n\t\tif (typeof matcher !== 'undefined') {\n\t\t\treturn Call(matcher, regexp, [O]);\n\t\t}\n\t}\n\n\tvar S = ToString(O);\n\t// var rx = RegExpCreate(regexp, 'g');\n\tvar rx = new RegExp(regexp, 'g');\n\treturn Call(getMatcher(rx), rx, [S]);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTUwNS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixXQUFXLG1CQUFPLENBQUMsR0FBdUI7QUFDMUMsVUFBVSxtQkFBTyxDQUFDLElBQXNCO0FBQ3hDLGdCQUFnQixtQkFBTyxDQUFDLElBQTRCO0FBQ3BELGVBQWUsbUJBQU8sQ0FBQyxHQUEyQjtBQUNsRCxlQUFlLG1CQUFPLENBQUMsSUFBMkI7QUFDbEQsNkJBQTZCLG1CQUFPLENBQUMsSUFBeUM7QUFDOUUsZ0JBQWdCLG1CQUFPLENBQUMsSUFBcUI7QUFDN0MsaUJBQWlCLG1CQUFPLENBQUMsSUFBYTtBQUN0QyxrQkFBa0IsbUJBQU8sQ0FBQyxJQUF3Qjs7QUFFbEQ7O0FBRUEsNkJBQTZCLG1CQUFPLENBQUMsSUFBNEI7O0FBRWpFLCtDQUErQztBQUMvQztBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvc3RyaW5nLnByb3RvdHlwZS5tYXRjaGFsbC9pbXBsZW1lbnRhdGlvbi5qcz9jMTdkIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIENhbGwgPSByZXF1aXJlKCdlcy1hYnN0cmFjdC8yMDIxL0NhbGwnKTtcbnZhciBHZXQgPSByZXF1aXJlKCdlcy1hYnN0cmFjdC8yMDIxL0dldCcpO1xudmFyIEdldE1ldGhvZCA9IHJlcXVpcmUoJ2VzLWFic3RyYWN0LzIwMjEvR2V0TWV0aG9kJyk7XG52YXIgSXNSZWdFeHAgPSByZXF1aXJlKCdlcy1hYnN0cmFjdC8yMDIxL0lzUmVnRXhwJyk7XG52YXIgVG9TdHJpbmcgPSByZXF1aXJlKCdlcy1hYnN0cmFjdC8yMDIxL1RvU3RyaW5nJyk7XG52YXIgUmVxdWlyZU9iamVjdENvZXJjaWJsZSA9IHJlcXVpcmUoJ2VzLWFic3RyYWN0LzIwMjEvUmVxdWlyZU9iamVjdENvZXJjaWJsZScpO1xudmFyIGNhbGxCb3VuZCA9IHJlcXVpcmUoJ2NhbGwtYmluZC9jYWxsQm91bmQnKTtcbnZhciBoYXNTeW1ib2xzID0gcmVxdWlyZSgnaGFzLXN5bWJvbHMnKSgpO1xudmFyIGZsYWdzR2V0dGVyID0gcmVxdWlyZSgncmVnZXhwLnByb3RvdHlwZS5mbGFncycpO1xuXG52YXIgJGluZGV4T2YgPSBjYWxsQm91bmQoJ1N0cmluZy5wcm90b3R5cGUuaW5kZXhPZicpO1xuXG52YXIgcmVnZXhwTWF0Y2hBbGxQb2x5ZmlsbCA9IHJlcXVpcmUoJy4vcG9seWZpbGwtcmVnZXhwLW1hdGNoYWxsJyk7XG5cbnZhciBnZXRNYXRjaGVyID0gZnVuY3Rpb24gZ2V0TWF0Y2hlcihyZWdleHApIHsgLy8gZXNsaW50LWRpc2FibGUtbGluZSBjb25zaXN0ZW50LXJldHVyblxuXHR2YXIgbWF0Y2hlclBvbHlmaWxsID0gcmVnZXhwTWF0Y2hBbGxQb2x5ZmlsbCgpO1xuXHRpZiAoaGFzU3ltYm9scyAmJiB0eXBlb2YgU3ltYm9sLm1hdGNoQWxsID09PSAnc3ltYm9sJykge1xuXHRcdHZhciBtYXRjaGVyID0gR2V0TWV0aG9kKHJlZ2V4cCwgU3ltYm9sLm1hdGNoQWxsKTtcblx0XHRpZiAobWF0Y2hlciA9PT0gUmVnRXhwLnByb3RvdHlwZVtTeW1ib2wubWF0Y2hBbGxdICYmIG1hdGNoZXIgIT09IG1hdGNoZXJQb2x5ZmlsbCkge1xuXHRcdFx0cmV0dXJuIG1hdGNoZXJQb2x5ZmlsbDtcblx0XHR9XG5cdFx0cmV0dXJuIG1hdGNoZXI7XG5cdH1cblx0Ly8gZmFsbGJhY2sgZm9yIHByZS1TeW1ib2wubWF0Y2hBbGwgZW52aXJvbm1lbnRzXG5cdGlmIChJc1JlZ0V4cChyZWdleHApKSB7XG5cdFx0cmV0dXJuIG1hdGNoZXJQb2x5ZmlsbDtcblx0fVxufTtcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBtYXRjaEFsbChyZWdleHApIHtcblx0dmFyIE8gPSBSZXF1aXJlT2JqZWN0Q29lcmNpYmxlKHRoaXMpO1xuXG5cdGlmICh0eXBlb2YgcmVnZXhwICE9PSAndW5kZWZpbmVkJyAmJiByZWdleHAgIT09IG51bGwpIHtcblx0XHR2YXIgaXNSZWdFeHAgPSBJc1JlZ0V4cChyZWdleHApO1xuXHRcdGlmIChpc1JlZ0V4cCkge1xuXHRcdFx0Ly8gd29ya2Fyb3VuZCBmb3Igb2xkZXIgZW5naW5lcyB0aGF0IGxhY2sgUmVnRXhwLnByb3RvdHlwZS5mbGFnc1xuXHRcdFx0dmFyIGZsYWdzID0gJ2ZsYWdzJyBpbiByZWdleHAgPyBHZXQocmVnZXhwLCAnZmxhZ3MnKSA6IGZsYWdzR2V0dGVyKHJlZ2V4cCk7XG5cdFx0XHRSZXF1aXJlT2JqZWN0Q29lcmNpYmxlKGZsYWdzKTtcblx0XHRcdGlmICgkaW5kZXhPZihUb1N0cmluZyhmbGFncyksICdnJykgPCAwKSB7XG5cdFx0XHRcdHRocm93IG5ldyBUeXBlRXJyb3IoJ21hdGNoQWxsIHJlcXVpcmVzIGEgZ2xvYmFsIHJlZ3VsYXIgZXhwcmVzc2lvbicpO1xuXHRcdFx0fVxuXHRcdH1cblxuXHRcdHZhciBtYXRjaGVyID0gZ2V0TWF0Y2hlcihyZWdleHApO1xuXHRcdGlmICh0eXBlb2YgbWF0Y2hlciAhPT0gJ3VuZGVmaW5lZCcpIHtcblx0XHRcdHJldHVybiBDYWxsKG1hdGNoZXIsIHJlZ2V4cCwgW09dKTtcblx0XHR9XG5cdH1cblxuXHR2YXIgUyA9IFRvU3RyaW5nKE8pO1xuXHQvLyB2YXIgcnggPSBSZWdFeHBDcmVhdGUocmVnZXhwLCAnZycpO1xuXHR2YXIgcnggPSBuZXcgUmVnRXhwKHJlZ2V4cCwgJ2cnKTtcblx0cmV0dXJuIENhbGwoZ2V0TWF0Y2hlcihyeCksIHJ4LCBbU10pO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///9505\n")},4956:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar callBind = __webpack_require__(5559);\nvar define = __webpack_require__(4289);\n\nvar implementation = __webpack_require__(9505);\nvar getPolyfill = __webpack_require__(3447);\nvar shim = __webpack_require__(2376);\n\nvar boundMatchAll = callBind(implementation);\n\ndefine(boundMatchAll, {\n\tgetPolyfill: getPolyfill,\n\timplementation: implementation,\n\tshim: shim\n});\n\nmodule.exports = boundMatchAll;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDk1Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixlQUFlLG1CQUFPLENBQUMsSUFBVztBQUNsQyxhQUFhLG1CQUFPLENBQUMsSUFBbUI7O0FBRXhDLHFCQUFxQixtQkFBTyxDQUFDLElBQWtCO0FBQy9DLGtCQUFrQixtQkFBTyxDQUFDLElBQVk7QUFDdEMsV0FBVyxtQkFBTyxDQUFDLElBQVE7O0FBRTNCOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsQ0FBQzs7QUFFRCIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvc3RyaW5nLnByb3RvdHlwZS5tYXRjaGFsbC9pbmRleC5qcz9iMWNjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGNhbGxCaW5kID0gcmVxdWlyZSgnY2FsbC1iaW5kJyk7XG52YXIgZGVmaW5lID0gcmVxdWlyZSgnZGVmaW5lLXByb3BlcnRpZXMnKTtcblxudmFyIGltcGxlbWVudGF0aW9uID0gcmVxdWlyZSgnLi9pbXBsZW1lbnRhdGlvbicpO1xudmFyIGdldFBvbHlmaWxsID0gcmVxdWlyZSgnLi9wb2x5ZmlsbCcpO1xudmFyIHNoaW0gPSByZXF1aXJlKCcuL3NoaW0nKTtcblxudmFyIGJvdW5kTWF0Y2hBbGwgPSBjYWxsQmluZChpbXBsZW1lbnRhdGlvbik7XG5cbmRlZmluZShib3VuZE1hdGNoQWxsLCB7XG5cdGdldFBvbHlmaWxsOiBnZXRQb2x5ZmlsbCxcblx0aW1wbGVtZW50YXRpb246IGltcGxlbWVudGF0aW9uLFxuXHRzaGltOiBzaGltXG59KTtcblxubW9kdWxlLmV4cG9ydHMgPSBib3VuZE1hdGNoQWxsO1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///4956\n")},6966:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar hasSymbols = __webpack_require__(1405)();\nvar regexpMatchAll = __webpack_require__(7201);\n\nmodule.exports = function getRegExpMatchAllPolyfill() {\n\tif (!hasSymbols || typeof Symbol.matchAll !== 'symbol' || typeof RegExp.prototype[Symbol.matchAll] !== 'function') {\n\t\treturn regexpMatchAll;\n\t}\n\treturn RegExp.prototype[Symbol.matchAll];\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNjk2Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixpQkFBaUIsbUJBQU8sQ0FBQyxJQUFhO0FBQ3RDLHFCQUFxQixtQkFBTyxDQUFDLElBQW1COztBQUVoRDtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL3N0cmluZy5wcm90b3R5cGUubWF0Y2hhbGwvcG9seWZpbGwtcmVnZXhwLW1hdGNoYWxsLmpzPzZjMTgiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgaGFzU3ltYm9scyA9IHJlcXVpcmUoJ2hhcy1zeW1ib2xzJykoKTtcbnZhciByZWdleHBNYXRjaEFsbCA9IHJlcXVpcmUoJy4vcmVnZXhwLW1hdGNoYWxsJyk7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gZ2V0UmVnRXhwTWF0Y2hBbGxQb2x5ZmlsbCgpIHtcblx0aWYgKCFoYXNTeW1ib2xzIHx8IHR5cGVvZiBTeW1ib2wubWF0Y2hBbGwgIT09ICdzeW1ib2wnIHx8IHR5cGVvZiBSZWdFeHAucHJvdG90eXBlW1N5bWJvbC5tYXRjaEFsbF0gIT09ICdmdW5jdGlvbicpIHtcblx0XHRyZXR1cm4gcmVnZXhwTWF0Y2hBbGw7XG5cdH1cblx0cmV0dXJuIFJlZ0V4cC5wcm90b3R5cGVbU3ltYm9sLm1hdGNoQWxsXTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///6966\n")},3447:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar implementation = __webpack_require__(9505);\n\nmodule.exports = function getPolyfill() {\n\tif (String.prototype.matchAll) {\n\t\ttry {\n\t\t\t''.matchAll(RegExp.prototype);\n\t\t} catch (e) {\n\t\t\treturn String.prototype.matchAll;\n\t\t}\n\t}\n\treturn implementation;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzQ0Ny5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixxQkFBcUIsbUJBQU8sQ0FBQyxJQUFrQjs7QUFFL0M7QUFDQTtBQUNBO0FBQ0E7QUFDQSxJQUFJO0FBQ0o7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvc3RyaW5nLnByb3RvdHlwZS5tYXRjaGFsbC9wb2x5ZmlsbC5qcz9iOGExIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGltcGxlbWVudGF0aW9uID0gcmVxdWlyZSgnLi9pbXBsZW1lbnRhdGlvbicpO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIGdldFBvbHlmaWxsKCkge1xuXHRpZiAoU3RyaW5nLnByb3RvdHlwZS5tYXRjaEFsbCkge1xuXHRcdHRyeSB7XG5cdFx0XHQnJy5tYXRjaEFsbChSZWdFeHAucHJvdG90eXBlKTtcblx0XHR9IGNhdGNoIChlKSB7XG5cdFx0XHRyZXR1cm4gU3RyaW5nLnByb3RvdHlwZS5tYXRjaEFsbDtcblx0XHR9XG5cdH1cblx0cmV0dXJuIGltcGxlbWVudGF0aW9uO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///3447\n")},7201:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\n// var Construct = require('es-abstract/2021/Construct');\nvar CreateRegExpStringIterator = __webpack_require__(3937);\nvar Get = __webpack_require__(1391);\nvar Set = __webpack_require__(105);\nvar SpeciesConstructor = __webpack_require__(9655);\nvar ToLength = __webpack_require__(8305);\nvar ToString = __webpack_require__(6846);\nvar Type = __webpack_require__(3633);\nvar flagsGetter = __webpack_require__(2847);\n\nvar OrigRegExp = RegExp;\n\nvar supportsConstructingWithFlags = 'flags' in RegExp.prototype;\n\nvar constructRegexWithFlags = function constructRegex(C, R) {\n\tvar matcher;\n\t// workaround for older engines that lack RegExp.prototype.flags\n\tvar flags = 'flags' in R ? Get(R, 'flags') : ToString(flagsGetter(R));\n\tif (supportsConstructingWithFlags && typeof flags === 'string') {\n\t\tmatcher = new C(R, flags);\n\t} else if (C === OrigRegExp) {\n\t\t// workaround for older engines that can not construct a RegExp with flags\n\t\tmatcher = new C(R.source, flags);\n\t} else {\n\t\tmatcher = new C(R, flags);\n\t}\n\treturn { flags: flags, matcher: matcher };\n};\n\nvar regexMatchAll = function SymbolMatchAll(string) {\n\tvar R = this;\n\tif (Type(R) !== 'Object') {\n\t\tthrow new TypeError('\"this\" value must be an Object');\n\t}\n\tvar S = ToString(string);\n\tvar C = SpeciesConstructor(R, OrigRegExp);\n\n\tvar tmp = constructRegexWithFlags(C, R);\n\t// var flags = ToString(Get(R, 'flags'));\n\tvar flags = tmp.flags;\n\t// var matcher = Construct(C, [R, flags]);\n\tvar matcher = tmp.matcher;\n\n\tvar lastIndex = ToLength(Get(R, 'lastIndex'));\n\tSet(matcher, 'lastIndex', lastIndex, true);\n\tvar global = flags.indexOf('g') > -1;\n\tvar fullUnicode = flags.indexOf('u') > -1;\n\treturn CreateRegExpStringIterator(matcher, S, global, fullUnicode);\n};\n\nvar defineP = Object.defineProperty;\nvar gOPD = Object.getOwnPropertyDescriptor;\n\nif (defineP && gOPD) {\n\tvar desc = gOPD(regexMatchAll, 'name');\n\tif (desc && desc.configurable) {\n\t\tdefineP(regexMatchAll, 'name', { value: '[Symbol.matchAll]' });\n\t}\n}\n\nmodule.exports = regexMatchAll;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzIwMS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBLGlDQUFpQyxtQkFBTyxDQUFDLElBQTZDO0FBQ3RGLFVBQVUsbUJBQU8sQ0FBQyxJQUFzQjtBQUN4QyxVQUFVLG1CQUFPLENBQUMsR0FBc0I7QUFDeEMseUJBQXlCLG1CQUFPLENBQUMsSUFBcUM7QUFDdEUsZUFBZSxtQkFBTyxDQUFDLElBQTJCO0FBQ2xELGVBQWUsbUJBQU8sQ0FBQyxJQUEyQjtBQUNsRCxXQUFXLG1CQUFPLENBQUMsSUFBdUI7QUFDMUMsa0JBQWtCLG1CQUFPLENBQUMsSUFBd0I7O0FBRWxEOztBQUVBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEdBQUc7QUFDSDtBQUNBO0FBQ0EsR0FBRztBQUNIO0FBQ0E7QUFDQSxVQUFVO0FBQ1Y7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxtQ0FBbUMsNEJBQTRCO0FBQy9EO0FBQ0E7O0FBRUEiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL3N0cmluZy5wcm90b3R5cGUubWF0Y2hhbGwvcmVnZXhwLW1hdGNoYWxsLmpzP2ZhODkiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG4vLyB2YXIgQ29uc3RydWN0ID0gcmVxdWlyZSgnZXMtYWJzdHJhY3QvMjAyMS9Db25zdHJ1Y3QnKTtcbnZhciBDcmVhdGVSZWdFeHBTdHJpbmdJdGVyYXRvciA9IHJlcXVpcmUoJ2VzLWFic3RyYWN0LzIwMjEvQ3JlYXRlUmVnRXhwU3RyaW5nSXRlcmF0b3InKTtcbnZhciBHZXQgPSByZXF1aXJlKCdlcy1hYnN0cmFjdC8yMDIxL0dldCcpO1xudmFyIFNldCA9IHJlcXVpcmUoJ2VzLWFic3RyYWN0LzIwMjEvU2V0Jyk7XG52YXIgU3BlY2llc0NvbnN0cnVjdG9yID0gcmVxdWlyZSgnZXMtYWJzdHJhY3QvMjAyMS9TcGVjaWVzQ29uc3RydWN0b3InKTtcbnZhciBUb0xlbmd0aCA9IHJlcXVpcmUoJ2VzLWFic3RyYWN0LzIwMjEvVG9MZW5ndGgnKTtcbnZhciBUb1N0cmluZyA9IHJlcXVpcmUoJ2VzLWFic3RyYWN0LzIwMjEvVG9TdHJpbmcnKTtcbnZhciBUeXBlID0gcmVxdWlyZSgnZXMtYWJzdHJhY3QvMjAyMS9UeXBlJyk7XG52YXIgZmxhZ3NHZXR0ZXIgPSByZXF1aXJlKCdyZWdleHAucHJvdG90eXBlLmZsYWdzJyk7XG5cbnZhciBPcmlnUmVnRXhwID0gUmVnRXhwO1xuXG52YXIgc3VwcG9ydHNDb25zdHJ1Y3RpbmdXaXRoRmxhZ3MgPSAnZmxhZ3MnIGluIFJlZ0V4cC5wcm90b3R5cGU7XG5cbnZhciBjb25zdHJ1Y3RSZWdleFdpdGhGbGFncyA9IGZ1bmN0aW9uIGNvbnN0cnVjdFJlZ2V4KEMsIFIpIHtcblx0dmFyIG1hdGNoZXI7XG5cdC8vIHdvcmthcm91bmQgZm9yIG9sZGVyIGVuZ2luZXMgdGhhdCBsYWNrIFJlZ0V4cC5wcm90b3R5cGUuZmxhZ3Ncblx0dmFyIGZsYWdzID0gJ2ZsYWdzJyBpbiBSID8gR2V0KFIsICdmbGFncycpIDogVG9TdHJpbmcoZmxhZ3NHZXR0ZXIoUikpO1xuXHRpZiAoc3VwcG9ydHNDb25zdHJ1Y3RpbmdXaXRoRmxhZ3MgJiYgdHlwZW9mIGZsYWdzID09PSAnc3RyaW5nJykge1xuXHRcdG1hdGNoZXIgPSBuZXcgQyhSLCBmbGFncyk7XG5cdH0gZWxzZSBpZiAoQyA9PT0gT3JpZ1JlZ0V4cCkge1xuXHRcdC8vIHdvcmthcm91bmQgZm9yIG9sZGVyIGVuZ2luZXMgdGhhdCBjYW4gbm90IGNvbnN0cnVjdCBhIFJlZ0V4cCB3aXRoIGZsYWdzXG5cdFx0bWF0Y2hlciA9IG5ldyBDKFIuc291cmNlLCBmbGFncyk7XG5cdH0gZWxzZSB7XG5cdFx0bWF0Y2hlciA9IG5ldyBDKFIsIGZsYWdzKTtcblx0fVxuXHRyZXR1cm4geyBmbGFnczogZmxhZ3MsIG1hdGNoZXI6IG1hdGNoZXIgfTtcbn07XG5cbnZhciByZWdleE1hdGNoQWxsID0gZnVuY3Rpb24gU3ltYm9sTWF0Y2hBbGwoc3RyaW5nKSB7XG5cdHZhciBSID0gdGhpcztcblx0aWYgKFR5cGUoUikgIT09ICdPYmplY3QnKSB7XG5cdFx0dGhyb3cgbmV3IFR5cGVFcnJvcignXCJ0aGlzXCIgdmFsdWUgbXVzdCBiZSBhbiBPYmplY3QnKTtcblx0fVxuXHR2YXIgUyA9IFRvU3RyaW5nKHN0cmluZyk7XG5cdHZhciBDID0gU3BlY2llc0NvbnN0cnVjdG9yKFIsIE9yaWdSZWdFeHApO1xuXG5cdHZhciB0bXAgPSBjb25zdHJ1Y3RSZWdleFdpdGhGbGFncyhDLCBSKTtcblx0Ly8gdmFyIGZsYWdzID0gVG9TdHJpbmcoR2V0KFIsICdmbGFncycpKTtcblx0dmFyIGZsYWdzID0gdG1wLmZsYWdzO1xuXHQvLyB2YXIgbWF0Y2hlciA9IENvbnN0cnVjdChDLCBbUiwgZmxhZ3NdKTtcblx0dmFyIG1hdGNoZXIgPSB0bXAubWF0Y2hlcjtcblxuXHR2YXIgbGFzdEluZGV4ID0gVG9MZW5ndGgoR2V0KFIsICdsYXN0SW5kZXgnKSk7XG5cdFNldChtYXRjaGVyLCAnbGFzdEluZGV4JywgbGFzdEluZGV4LCB0cnVlKTtcblx0dmFyIGdsb2JhbCA9IGZsYWdzLmluZGV4T2YoJ2cnKSA+IC0xO1xuXHR2YXIgZnVsbFVuaWNvZGUgPSBmbGFncy5pbmRleE9mKCd1JykgPiAtMTtcblx0cmV0dXJuIENyZWF0ZVJlZ0V4cFN0cmluZ0l0ZXJhdG9yKG1hdGNoZXIsIFMsIGdsb2JhbCwgZnVsbFVuaWNvZGUpO1xufTtcblxudmFyIGRlZmluZVAgPSBPYmplY3QuZGVmaW5lUHJvcGVydHk7XG52YXIgZ09QRCA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3I7XG5cbmlmIChkZWZpbmVQICYmIGdPUEQpIHtcblx0dmFyIGRlc2MgPSBnT1BEKHJlZ2V4TWF0Y2hBbGwsICduYW1lJyk7XG5cdGlmIChkZXNjICYmIGRlc2MuY29uZmlndXJhYmxlKSB7XG5cdFx0ZGVmaW5lUChyZWdleE1hdGNoQWxsLCAnbmFtZScsIHsgdmFsdWU6ICdbU3ltYm9sLm1hdGNoQWxsXScgfSk7XG5cdH1cbn1cblxubW9kdWxlLmV4cG9ydHMgPSByZWdleE1hdGNoQWxsO1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///7201\n")},2376:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar define = __webpack_require__(4289);\nvar hasSymbols = __webpack_require__(1405)();\nvar getPolyfill = __webpack_require__(3447);\nvar regexpMatchAllPolyfill = __webpack_require__(6966);\n\nvar defineP = Object.defineProperty;\nvar gOPD = Object.getOwnPropertyDescriptor;\n\nmodule.exports = function shimMatchAll() {\n\tvar polyfill = getPolyfill();\n\tdefine(\n\t\tString.prototype,\n\t\t{ matchAll: polyfill },\n\t\t{ matchAll: function () { return String.prototype.matchAll !== polyfill; } }\n\t);\n\tif (hasSymbols) {\n\t\t// eslint-disable-next-line no-restricted-properties\n\t\tvar symbol = Symbol.matchAll || (Symbol['for'] ? Symbol['for']('Symbol.matchAll') : Symbol('Symbol.matchAll'));\n\t\tdefine(\n\t\t\tSymbol,\n\t\t\t{ matchAll: symbol },\n\t\t\t{ matchAll: function () { return Symbol.matchAll !== symbol; } }\n\t\t);\n\n\t\tif (defineP && gOPD) {\n\t\t\tvar desc = gOPD(Symbol, symbol);\n\t\t\tif (!desc || desc.configurable) {\n\t\t\t\tdefineP(Symbol, symbol, {\n\t\t\t\t\tconfigurable: false,\n\t\t\t\t\tenumerable: false,\n\t\t\t\t\tvalue: symbol,\n\t\t\t\t\twritable: false\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tvar regexpMatchAll = regexpMatchAllPolyfill();\n\t\tvar func = {};\n\t\tfunc[symbol] = regexpMatchAll;\n\t\tvar predicate = {};\n\t\tpredicate[symbol] = function () {\n\t\t\treturn RegExp.prototype[symbol] !== regexpMatchAll;\n\t\t};\n\t\tdefine(RegExp.prototype, func, predicate);\n\t}\n\treturn polyfill;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjM3Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixhQUFhLG1CQUFPLENBQUMsSUFBbUI7QUFDeEMsaUJBQWlCLG1CQUFPLENBQUMsSUFBYTtBQUN0QyxrQkFBa0IsbUJBQU8sQ0FBQyxJQUFZO0FBQ3RDLDZCQUE2QixtQkFBTyxDQUFDLElBQTRCOztBQUVqRTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSSxvQkFBb0I7QUFDeEIsSUFBSSx3QkFBd0I7QUFDNUI7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsS0FBSyxrQkFBa0I7QUFDdkIsS0FBSyx3QkFBd0I7QUFDN0I7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEtBQUs7QUFDTDtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL3N0cmluZy5wcm90b3R5cGUubWF0Y2hhbGwvc2hpbS5qcz85Yzg4Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGRlZmluZSA9IHJlcXVpcmUoJ2RlZmluZS1wcm9wZXJ0aWVzJyk7XG52YXIgaGFzU3ltYm9scyA9IHJlcXVpcmUoJ2hhcy1zeW1ib2xzJykoKTtcbnZhciBnZXRQb2x5ZmlsbCA9IHJlcXVpcmUoJy4vcG9seWZpbGwnKTtcbnZhciByZWdleHBNYXRjaEFsbFBvbHlmaWxsID0gcmVxdWlyZSgnLi9wb2x5ZmlsbC1yZWdleHAtbWF0Y2hhbGwnKTtcblxudmFyIGRlZmluZVAgPSBPYmplY3QuZGVmaW5lUHJvcGVydHk7XG52YXIgZ09QRCA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3I7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gc2hpbU1hdGNoQWxsKCkge1xuXHR2YXIgcG9seWZpbGwgPSBnZXRQb2x5ZmlsbCgpO1xuXHRkZWZpbmUoXG5cdFx0U3RyaW5nLnByb3RvdHlwZSxcblx0XHR7IG1hdGNoQWxsOiBwb2x5ZmlsbCB9LFxuXHRcdHsgbWF0Y2hBbGw6IGZ1bmN0aW9uICgpIHsgcmV0dXJuIFN0cmluZy5wcm90b3R5cGUubWF0Y2hBbGwgIT09IHBvbHlmaWxsOyB9IH1cblx0KTtcblx0aWYgKGhhc1N5bWJvbHMpIHtcblx0XHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tcmVzdHJpY3RlZC1wcm9wZXJ0aWVzXG5cdFx0dmFyIHN5bWJvbCA9IFN5bWJvbC5tYXRjaEFsbCB8fCAoU3ltYm9sWydmb3InXSA/IFN5bWJvbFsnZm9yJ10oJ1N5bWJvbC5tYXRjaEFsbCcpIDogU3ltYm9sKCdTeW1ib2wubWF0Y2hBbGwnKSk7XG5cdFx0ZGVmaW5lKFxuXHRcdFx0U3ltYm9sLFxuXHRcdFx0eyBtYXRjaEFsbDogc3ltYm9sIH0sXG5cdFx0XHR7IG1hdGNoQWxsOiBmdW5jdGlvbiAoKSB7IHJldHVybiBTeW1ib2wubWF0Y2hBbGwgIT09IHN5bWJvbDsgfSB9XG5cdFx0KTtcblxuXHRcdGlmIChkZWZpbmVQICYmIGdPUEQpIHtcblx0XHRcdHZhciBkZXNjID0gZ09QRChTeW1ib2wsIHN5bWJvbCk7XG5cdFx0XHRpZiAoIWRlc2MgfHwgZGVzYy5jb25maWd1cmFibGUpIHtcblx0XHRcdFx0ZGVmaW5lUChTeW1ib2wsIHN5bWJvbCwge1xuXHRcdFx0XHRcdGNvbmZpZ3VyYWJsZTogZmFsc2UsXG5cdFx0XHRcdFx0ZW51bWVyYWJsZTogZmFsc2UsXG5cdFx0XHRcdFx0dmFsdWU6IHN5bWJvbCxcblx0XHRcdFx0XHR3cml0YWJsZTogZmFsc2Vcblx0XHRcdFx0fSk7XG5cdFx0XHR9XG5cdFx0fVxuXG5cdFx0dmFyIHJlZ2V4cE1hdGNoQWxsID0gcmVnZXhwTWF0Y2hBbGxQb2x5ZmlsbCgpO1xuXHRcdHZhciBmdW5jID0ge307XG5cdFx0ZnVuY1tzeW1ib2xdID0gcmVnZXhwTWF0Y2hBbGw7XG5cdFx0dmFyIHByZWRpY2F0ZSA9IHt9O1xuXHRcdHByZWRpY2F0ZVtzeW1ib2xdID0gZnVuY3Rpb24gKCkge1xuXHRcdFx0cmV0dXJuIFJlZ0V4cC5wcm90b3R5cGVbc3ltYm9sXSAhPT0gcmVnZXhwTWF0Y2hBbGw7XG5cdFx0fTtcblx0XHRkZWZpbmUoUmVnRXhwLnByb3RvdHlwZSwgZnVuYywgcHJlZGljYXRlKTtcblx0fVxuXHRyZXR1cm4gcG9seWZpbGw7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///2376\n")},4654:function(){},4766:function(module){eval('!function(t,e){ true?module.exports=e():0}(self,(function(){return(()=>{var t={426:(t,e,n)=>{var r=n(529);function o(t,e,n){Array.isArray(t)?t.push(e):t[n]=e}t.exports=function(t){var e,n,i,u=[];if(Array.isArray(t))n=[],e=t.length-1;else{if("object"!=typeof t||null===t)throw new TypeError("Expecting an Array or an Object, but `"+(null===t?"null":typeof t)+"` provided.");n={},i=Object.keys(t),e=i.length-1}return function n(c,a){var l,s,f,d;for(s=i?i[a]:a,Array.isArray(t[s])||(void 0===t[s]?t[s]=[]:t[s]=[t[s]]),l=0;l<t[s].length;l++)o((d=c,f=Array.isArray(d)?[].concat(d):r(d)),t[s][l],s),a>=e?u.push(f):n(f,a+1)}(n,0),u}},529:t=>{t.exports=function(){for(var t={},n=0;n<arguments.length;n++){var r=arguments[n];for(var o in r)e.call(r,o)&&(t[o]=r[o])}return t};var e=Object.prototype.hasOwnProperty}},e={};function n(r){var o=e[r];if(void 0!==o)return o.exports;var i=e[r]={exports:{}};return t[r](i,i.exports,n),i.exports}n.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var r in e)n.o(e,r)&&!n.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),n.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var r={};return(()=>{"use strict";n.r(r),n.d(r,{default:()=>X,getCssSelector:()=>Q});var t,e,o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol?"symbol":typeof t};function i(t){return null!=t&&"object"===(void 0===t?"undefined":o(t))&&1===t.nodeType&&"object"===o(t.style)&&"object"===o(t.ownerDocument)}function u(t="unknown problem",...e){console.warn(`CssSelectorGenerator: ${t}`,...e)}!function(t){t.NONE="none",t.DESCENDANT="descendant",t.CHILD="child"}(t||(t={})),function(t){t.id="id",t.class="class",t.tag="tag",t.attribute="attribute",t.nthchild="nthchild",t.nthoftype="nthoftype"}(e||(e={}));const c={selectors:[e.id,e.class,e.tag,e.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function a(t){return t instanceof RegExp}function l(t){return["string","function"].includes(typeof t)||a(t)}function s(t){return Array.isArray(t)?t.filter(l):[]}function f(t){const e=[Node.DOCUMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE,Node.ELEMENT_NODE];return function(t){return t instanceof Node}(t)&&e.includes(t.nodeType)}function d(t,e){if(f(t))return t.contains(e)||u("element root mismatch","Provided root does not contain the element. This will most likely result in producing a fallback selector using element\'s real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will nto work as intended."),t;const n=e.getRootNode({composed:!1});return f(n)?(n!==document&&u("shadow root inferred","You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended."),n):e.ownerDocument.querySelector(":root")}function p(t){return"number"==typeof t?t:Number.POSITIVE_INFINITY}function m(t=[]){const[e=[],...n]=t;return 0===n.length?e:n.reduce(((t,e)=>t.filter((t=>e.includes(t)))),e)}function h(t){return[].concat(...t)}function y(t){const e=t.map((t=>{if(a(t))return e=>t.test(e);if("function"==typeof t)return e=>{const n=t(e);return"boolean"!=typeof n?(u("pattern matcher function invalid","Provided pattern matching function does not return boolean. It\'s result will be ignored.",t),!1):n};if("string"==typeof t){const e=new RegExp("^"+t.replace(/[|\\\\{}()[\\]^$+?.]/g,"\\\\$&").replace(/\\*/g,".+")+"$");return t=>e.test(t)}return u("pattern matcher invalid","Pattern matching only accepts strings, regular expressions and/or functions. This item is invalid and will be ignored.",t),()=>!1}));return t=>e.some((e=>e(t)))}function g(t,e,n){const r=Array.from(d(n,t[0]).querySelectorAll(e));return r.length===t.length&&t.every((t=>r.includes(t)))}function b(t,e){e=null!=e?e:function(t){return t.ownerDocument.querySelector(":root")}(t);const n=[];let r=t;for(;i(r)&&r!==e;)n.push(r),r=r.parentElement;return n}function v(t,e){return m(t.map((t=>b(t,e))))}const N={[t.NONE]:{type:t.NONE,value:""},[t.DESCENDANT]:{type:t.DESCENDANT,value:" > "},[t.CHILD]:{type:t.CHILD,value:" "}},S=new RegExp(["^$","\\\\s","^\\\\d"].join("|")),E=new RegExp(["^$","^\\\\d"].join("|")),w=[e.nthoftype,e.tag,e.id,e.class,e.attribute,e.nthchild];var x=n(426),A=n.n(x);const C=y(["class","id","ng-*"]);function O({nodeName:t}){return`[${t}]`}function T({nodeName:t,nodeValue:e}){return`[${t}=\'${Y(e)}\']`}function I({nodeName:t}){return!C(t)}function j(t){const e=Array.from(t.attributes).filter(I);return[...e.map(O),...e.map(T)]}function D(t){return(t.getAttribute("class")||"").trim().split(/\\s+/).filter((t=>!E.test(t))).map((t=>`.${Y(t)}`))}function $(t){const e=t.getAttribute("id")||"",n=`#${Y(e)}`,r=t.getRootNode({composed:!1});return!S.test(e)&&g([t],n,r)?[n]:[]}function P(t){const e=t.parentNode;if(e){const n=Array.from(e.childNodes).filter(i).indexOf(t);if(n>-1)return[`:nth-child(${n+1})`]}return[]}function R(t){return[Y(t.tagName.toLowerCase())]}function _(t){const e=[...new Set(h(t.map(R)))];return 0===e.length||e.length>1?[]:[e[0]]}function k(t){const e=_([t])[0],n=t.parentElement;if(n){const r=Array.from(n.children).filter((t=>t.tagName.toLowerCase()===e)).indexOf(t);if(r>-1)return[`${e}:nth-of-type(${r+1})`]}return[]}function M(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){const n=[];let r=0,o=q(1);for(;o.length<=t.length&&r<e;)r+=1,n.push(o.map((e=>t[e]))),o=L(o,t.length-1);return n}function L(t=[],e=0){const n=t.length;if(0===n)return[];const r=[...t];r[n-1]+=1;for(let t=n-1;t>=0;t--)if(r[t]>e){if(0===t)return q(n+1);r[t-1]++,r[t]=r[t-1]+1}return r[n-1]>e?q(n+1):r}function q(t=1){return Array.from(Array(t).keys())}const F=":".charCodeAt(0).toString(16).toUpperCase(),V=/[ !"#$%&\'()\\[\\]{|}<>*+,./;=?@^`~\\\\]/;function Y(t=""){var e,n;return null!==(n=null===(e=null===CSS||void 0===CSS?void 0:CSS.escape)||void 0===e?void 0:e.call(CSS,t))&&void 0!==n?n:function(t=""){return t.split("").map((t=>":"===t?`\\\\${F} `:V.test(t)?`\\\\${t}`:escape(t).replace(/%/g,"\\\\"))).join("")}(t)}const B={tag:_,id:function(t){return 0===t.length||t.length>1?[]:$(t[0])},class:function(t){return m(t.map(D))},attribute:function(t){return m(t.map(j))},nthchild:function(t){return m(t.map(P))},nthoftype:function(t){return m(t.map(k))}},G={tag:R,id:$,class:D,attribute:j,nthchild:P,nthoftype:k};function W(t){return t.includes(e.tag)||t.includes(e.nthoftype)?[...t]:[...t,e.tag]}function H(t={}){const n=[...w];return t[e.tag]&&t[e.nthoftype]&&n.splice(n.indexOf(e.tag),1),n.map((e=>{return(r=t)[n=e]?r[n].join(""):"";var n,r})).join("")}function U(t,e,n="",r){const o=function(t,e){return""===e?t:function(t,e){return[...t.map((t=>e+" "+t)),...t.map((t=>e+" > "+t))]}(t,e)}(function(t,e,n){const r=h(function(t,e){return function(t){const{selectors:e,combineBetweenSelectors:n,includeTag:r,maxCandidates:o}=t,i=n?M(e,{maxResults:o}):e.map((t=>[t]));return r?i.map(W):i}(e).map((e=>function(t,e){const n={};return t.forEach((t=>{const r=e[t];r.length>0&&(n[t]=r)})),A()(n).map(H)}(e,t))).filter((t=>t.length>0))}(function(t,e){const{blacklist:n,whitelist:r,combineWithinSelector:o,maxCombinations:i}=e,u=y(n),c=y(r);return function(t){const{selectors:e,includeTag:n}=t,r=[].concat(e);return n&&!r.includes("tag")&&r.push("tag"),r}(e).reduce(((e,n)=>{const r=function(t=[],e){return t.sort(((t,n)=>{const r=e(t),o=e(n);return r&&!o?-1:!r&&o?1:0}))}(function(t=[],e,n){return t.filter((t=>n(t)||!e(t)))}(function(t,e){var n;return(null!==(n=B[e])&&void 0!==n?n:()=>[])(t)}(t,n),u,c),c);return e[n]=o?M(r,{maxResults:i}):r.map((t=>[t])),e}),{})}(t,n),n));return[...new Set(r)]}(t,r.root,r),n);for(const e of o)if(g(t,e,r.root))return e;return null}function z(t){return{value:t,include:!1}}function J({selectors:t,operator:n}){let r=[...w];t[e.tag]&&t[e.nthoftype]&&(r=r.filter((t=>t!==e.tag)));let o="";return r.forEach((e=>{(t[e]||[]).forEach((({value:t,include:e})=>{e&&(o+=t)}))})),n.value+o}function K(n){return[":root",...b(n).reverse().map((n=>{const r=function(e,n,r=t.NONE){const o={};return n.forEach((t=>{Reflect.set(o,t,function(t,e){return G[e](t)}(e,t).map(z))})),{element:e,operator:N[r],selectors:o}}(n,[e.nthchild],t.DESCENDANT);return r.selectors.nthchild.forEach((t=>{t.include=!0})),r})).map(J)].join("")}function Q(t,n={}){const r=function(t){const e=(Array.isArray(t)?t:[t]).filter(i);return[...new Set(e)]}(t),o=function(t,n={}){const r=Object.assign(Object.assign({},c),n);return{selectors:(o=r.selectors,Array.isArray(o)?o.filter((t=>{return n=e,r=t,Object.values(n).includes(r);var n,r})):[]),whitelist:s(r.whitelist),blacklist:s(r.blacklist),root:d(r.root,t),combineWithinSelector:!!r.combineWithinSelector,combineBetweenSelectors:!!r.combineBetweenSelectors,includeTag:!!r.includeTag,maxCombinations:p(r.maxCombinations),maxCandidates:p(r.maxCandidates)};var o}(r[0],n);let u="",a=o.root;function l(){return function(t,e,n="",r){if(0===t.length)return null;const o=[t.length>1?t:[],...v(t,e).map((t=>[t]))];for(const t of o){const e=U(t,0,n,r);if(e)return{foundElements:t,selector:e}}return null}(r,a,u,o)}let f=l();for(;f;){const{foundElements:t,selector:e}=f;if(g(r,e,o.root))return e;a=t[0],u=e,f=l()}return r.length>1?r.map((t=>Q(t,o))).join(", "):function(t){return t.map(K).join(", ")}(r)}const X=Q})(),r})()}));//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,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QiwwQkFBMEIsR0FBRyxvQkFBb0Isa0NBQWtDLGVBQWUsTUFBTSxnREFBZ0QsY0FBYyxtQkFBbUIsYUFBYSxvQkFBb0IsSUFBSSxFQUFFLFVBQVUsc0JBQXNCLGdCQUFnQiwyQ0FBMkMsWUFBWSxjQUFjLE9BQU8sb0JBQW9CLFlBQVksdUJBQXVCLEVBQUUsYUFBYSx1REFBdUQsU0FBUyxzQkFBc0Isc0JBQXNCLGtCQUFrQixJQUFJLFVBQVUsR0FBRyxhQUFhLGNBQWMsMENBQTBDLCtCQUErQixXQUFXLHNCQUFzQiw4QkFBOEIsZUFBZSxjQUFjLElBQUkscUNBQXFDLDhCQUE4Qix5Q0FBeUMsYUFBYSxLQUFLLG9CQUFvQixpQkFBaUIsRUFBRSxvQkFBb0IsMkNBQTJDLHNCQUFzQixxQkFBcUIsRUFBRSxzQ0FBc0MsT0FBTyxPQUFPLHdEQUF3RCw0Q0FBNEMsUUFBUSwrUUFBK1EsTUFBTSxTQUFTLGtCQUFrQixhQUFhLDRCQUE0Qiw0QkFBNEIsa0RBQWtELGtCQUFrQixtQkFBbUIsWUFBWSw0QkFBNEIsWUFBWSxVQUFVLFVBQVUsS0FBSyxFQUFFLEVBQUUsTUFBTSwyQkFBMkIsR0FBRywwQkFBMEIsaUJBQWlCLDREQUE0RCwyQkFBMkIsSUFBSSxVQUFVLE1BQU0sSUFBSSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvY3NzLXNlbGVjdG9yLWdlbmVyYXRvci9idWlsZC9pbmRleC5qcz8xZTQyIl0sInNvdXJjZXNDb250ZW50IjpbIiFmdW5jdGlvbih0LGUpe1wib2JqZWN0XCI9PXR5cGVvZiBleHBvcnRzJiZcIm9iamVjdFwiPT10eXBlb2YgbW9kdWxlP21vZHVsZS5leHBvcnRzPWUoKTpcImZ1bmN0aW9uXCI9PXR5cGVvZiBkZWZpbmUmJmRlZmluZS5hbWQ/ZGVmaW5lKFtdLGUpOlwib2JqZWN0XCI9PXR5cGVvZiBleHBvcnRzP2V4cG9ydHMuQ3NzU2VsZWN0b3JHZW5lcmF0b3I9ZSgpOnQuQ3NzU2VsZWN0b3JHZW5lcmF0b3I9ZSgpfShzZWxmLChmdW5jdGlvbigpe3JldHVybigoKT0+e3ZhciB0PXs0MjY6KHQsZSxuKT0+e3ZhciByPW4oNTI5KTtmdW5jdGlvbiBvKHQsZSxuKXtBcnJheS5pc0FycmF5KHQpP3QucHVzaChlKTp0W25dPWV9dC5leHBvcnRzPWZ1bmN0aW9uKHQpe3ZhciBlLG4saSx1PVtdO2lmKEFycmF5LmlzQXJyYXkodCkpbj1bXSxlPXQubGVuZ3RoLTE7ZWxzZXtpZihcIm9iamVjdFwiIT10eXBlb2YgdHx8bnVsbD09PXQpdGhyb3cgbmV3IFR5cGVFcnJvcihcIkV4cGVjdGluZyBhbiBBcnJheSBvciBhbiBPYmplY3QsIGJ1dCBgXCIrKG51bGw9PT10P1wibnVsbFwiOnR5cGVvZiB0KStcImAgcHJvdmlkZWQuXCIpO249e30saT1PYmplY3Qua2V5cyh0KSxlPWkubGVuZ3RoLTF9cmV0dXJuIGZ1bmN0aW9uIG4oYyxhKXt2YXIgbCxzLGYsZDtmb3Iocz1pP2lbYV06YSxBcnJheS5pc0FycmF5KHRbc10pfHwodm9pZCAwPT09dFtzXT90W3NdPVtdOnRbc109W3Rbc11dKSxsPTA7bDx0W3NdLmxlbmd0aDtsKyspbygoZD1jLGY9QXJyYXkuaXNBcnJheShkKT9bXS5jb25jYXQoZCk6cihkKSksdFtzXVtsXSxzKSxhPj1lP3UucHVzaChmKTpuKGYsYSsxKX0obiwwKSx1fX0sNTI5OnQ9Pnt0LmV4cG9ydHM9ZnVuY3Rpb24oKXtmb3IodmFyIHQ9e30sbj0wO248YXJndW1lbnRzLmxlbmd0aDtuKyspe3ZhciByPWFyZ3VtZW50c1tuXTtmb3IodmFyIG8gaW4gcillLmNhbGwocixvKSYmKHRbb109cltvXSl9cmV0dXJuIHR9O3ZhciBlPU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHl9fSxlPXt9O2Z1bmN0aW9uIG4ocil7dmFyIG89ZVtyXTtpZih2b2lkIDAhPT1vKXJldHVybiBvLmV4cG9ydHM7dmFyIGk9ZVtyXT17ZXhwb3J0czp7fX07cmV0dXJuIHRbcl0oaSxpLmV4cG9ydHMsbiksaS5leHBvcnRzfW4ubj10PT57dmFyIGU9dCYmdC5fX2VzTW9kdWxlPygpPT50LmRlZmF1bHQ6KCk9PnQ7cmV0dXJuIG4uZChlLHthOmV9KSxlfSxuLmQ9KHQsZSk9Pntmb3IodmFyIHIgaW4gZSluLm8oZSxyKSYmIW4ubyh0LHIpJiZPYmplY3QuZGVmaW5lUHJvcGVydHkodCxyLHtlbnVtZXJhYmxlOiEwLGdldDplW3JdfSl9LG4ubz0odCxlKT0+T2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHQsZSksbi5yPXQ9PntcInVuZGVmaW5lZFwiIT10eXBlb2YgU3ltYm9sJiZTeW1ib2wudG9TdHJpbmdUYWcmJk9iamVjdC5kZWZpbmVQcm9wZXJ0eSh0LFN5bWJvbC50b1N0cmluZ1RhZyx7dmFsdWU6XCJNb2R1bGVcIn0pLE9iamVjdC5kZWZpbmVQcm9wZXJ0eSh0LFwiX19lc01vZHVsZVwiLHt2YWx1ZTohMH0pfTt2YXIgcj17fTtyZXR1cm4oKCk9PntcInVzZSBzdHJpY3RcIjtuLnIociksbi5kKHIse2RlZmF1bHQ6KCk9PlgsZ2V0Q3NzU2VsZWN0b3I6KCk9PlF9KTt2YXIgdCxlLG89XCJmdW5jdGlvblwiPT10eXBlb2YgU3ltYm9sJiZcInN5bWJvbFwiPT10eXBlb2YgU3ltYm9sLml0ZXJhdG9yP2Z1bmN0aW9uKHQpe3JldHVybiB0eXBlb2YgdH06ZnVuY3Rpb24odCl7cmV0dXJuIHQmJlwiZnVuY3Rpb25cIj09dHlwZW9mIFN5bWJvbCYmdC5jb25zdHJ1Y3Rvcj09PVN5bWJvbD9cInN5bWJvbFwiOnR5cGVvZiB0fTtmdW5jdGlvbiBpKHQpe3JldHVybiBudWxsIT10JiZcIm9iamVjdFwiPT09KHZvaWQgMD09PXQ/XCJ1bmRlZmluZWRcIjpvKHQpKSYmMT09PXQubm9kZVR5cGUmJlwib2JqZWN0XCI9PT1vKHQuc3R5bGUpJiZcIm9iamVjdFwiPT09byh0Lm93bmVyRG9jdW1lbnQpfWZ1bmN0aW9uIHUodD1cInVua25vd24gcHJvYmxlbVwiLC4uLmUpe2NvbnNvbGUud2FybihgQ3NzU2VsZWN0b3JHZW5lcmF0b3I6ICR7dH1gLC4uLmUpfSFmdW5jdGlvbih0KXt0Lk5PTkU9XCJub25lXCIsdC5ERVNDRU5EQU5UPVwiZGVzY2VuZGFudFwiLHQuQ0hJTEQ9XCJjaGlsZFwifSh0fHwodD17fSkpLGZ1bmN0aW9uKHQpe3QuaWQ9XCJpZFwiLHQuY2xhc3M9XCJjbGFzc1wiLHQudGFnPVwidGFnXCIsdC5hdHRyaWJ1dGU9XCJhdHRyaWJ1dGVcIix0Lm50aGNoaWxkPVwibnRoY2hpbGRcIix0Lm50aG9mdHlwZT1cIm50aG9mdHlwZVwifShlfHwoZT17fSkpO2NvbnN0IGM9e3NlbGVjdG9yczpbZS5pZCxlLmNsYXNzLGUudGFnLGUuYXR0cmlidXRlXSxpbmNsdWRlVGFnOiExLHdoaXRlbGlzdDpbXSxibGFja2xpc3Q6W10sY29tYmluZVdpdGhpblNlbGVjdG9yOiEwLGNvbWJpbmVCZXR3ZWVuU2VsZWN0b3JzOiEwLHJvb3Q6bnVsbCxtYXhDb21iaW5hdGlvbnM6TnVtYmVyLlBPU0lUSVZFX0lORklOSVRZLG1heENhbmRpZGF0ZXM6TnVtYmVyLlBPU0lUSVZFX0lORklOSVRZfTtmdW5jdGlvbiBhKHQpe3JldHVybiB0IGluc3RhbmNlb2YgUmVnRXhwfWZ1bmN0aW9uIGwodCl7cmV0dXJuW1wic3RyaW5nXCIsXCJmdW5jdGlvblwiXS5pbmNsdWRlcyh0eXBlb2YgdCl8fGEodCl9ZnVuY3Rpb24gcyh0KXtyZXR1cm4gQXJyYXkuaXNBcnJheSh0KT90LmZpbHRlcihsKTpbXX1mdW5jdGlvbiBmKHQpe2NvbnN0IGU9W05vZGUuRE9DVU1FTlRfTk9ERSxOb2RlLkRPQ1VNRU5UX0ZSQUdNRU5UX05PREUsTm9kZS5FTEVNRU5UX05PREVdO3JldHVybiBmdW5jdGlvbih0KXtyZXR1cm4gdCBpbnN0YW5jZW9mIE5vZGV9KHQpJiZlLmluY2x1ZGVzKHQubm9kZVR5cGUpfWZ1bmN0aW9uIGQodCxlKXtpZihmKHQpKXJldHVybiB0LmNvbnRhaW5zKGUpfHx1KFwiZWxlbWVudCByb290IG1pc21hdGNoXCIsXCJQcm92aWRlZCByb290IGRvZXMgbm90IGNvbnRhaW4gdGhlIGVsZW1lbnQuIFRoaXMgd2lsbCBtb3N0IGxpa2VseSByZXN1bHQgaW4gcHJvZHVjaW5nIGEgZmFsbGJhY2sgc2VsZWN0b3IgdXNpbmcgZWxlbWVudCdzIHJlYWwgcm9vdCBub2RlLiBJZiB5b3UgcGxhbiB0byB1c2UgdGhlIHNlbGVjdG9yIHVzaW5nIHByb3ZpZGVkIHJvb3QgKGUuZy4gYHJvb3QucXVlcnlTZWxlY3RvcmApLCBpdCB3aWxsIG50byB3b3JrIGFzIGludGVuZGVkLlwiKSx0O2NvbnN0IG49ZS5nZXRSb290Tm9kZSh7Y29tcG9zZWQ6ITF9KTtyZXR1cm4gZihuKT8obiE9PWRvY3VtZW50JiZ1KFwic2hhZG93IHJvb3QgaW5mZXJyZWRcIixcIllvdSBkaWQgbm90IHByb3ZpZGUgYSByb290IGFuZCB0aGUgZWxlbWVudCBpcyBhIGNoaWxkIG9mIFNoYWRvdyBET00uIFRoaXMgd2lsbCBwcm9kdWNlIGEgc2VsZWN0b3IgdXNpbmcgU2hhZG93Um9vdCBhcyBhIHJvb3QuIElmIHlvdSBwbGFuIHRvIHVzZSB0aGUgc2VsZWN0b3IgdXNpbmcgZG9jdW1lbnQgYXMgYSByb290IChlLmcuIGBkb2N1bWVudC5xdWVyeVNlbGVjdG9yYCksIGl0IHdpbGwgbm90IHdvcmsgYXMgaW50ZW5kZWQuXCIpLG4pOmUub3duZXJEb2N1bWVudC5xdWVyeVNlbGVjdG9yKFwiOnJvb3RcIil9ZnVuY3Rpb24gcCh0KXtyZXR1cm5cIm51bWJlclwiPT10eXBlb2YgdD90Ok51bWJlci5QT1NJVElWRV9JTkZJTklUWX1mdW5jdGlvbiBtKHQ9W10pe2NvbnN0W2U9W10sLi4ubl09dDtyZXR1cm4gMD09PW4ubGVuZ3RoP2U6bi5yZWR1Y2UoKCh0LGUpPT50LmZpbHRlcigodD0+ZS5pbmNsdWRlcyh0KSkpKSxlKX1mdW5jdGlvbiBoKHQpe3JldHVybltdLmNvbmNhdCguLi50KX1mdW5jdGlvbiB5KHQpe2NvbnN0IGU9dC5tYXAoKHQ9PntpZihhKHQpKXJldHVybiBlPT50LnRlc3QoZSk7aWYoXCJmdW5jdGlvblwiPT10eXBlb2YgdClyZXR1cm4gZT0+e2NvbnN0IG49dChlKTtyZXR1cm5cImJvb2xlYW5cIiE9dHlwZW9mIG4/KHUoXCJwYXR0ZXJuIG1hdGNoZXIgZnVuY3Rpb24gaW52YWxpZFwiLFwiUHJvdmlkZWQgcGF0dGVybiBtYXRjaGluZyBmdW5jdGlvbiBkb2VzIG5vdCByZXR1cm4gYm9vbGVhbi4gSXQncyByZXN1bHQgd2lsbCBiZSBpZ25vcmVkLlwiLHQpLCExKTpufTtpZihcInN0cmluZ1wiPT10eXBlb2YgdCl7Y29uc3QgZT1uZXcgUmVnRXhwKFwiXlwiK3QucmVwbGFjZSgvW3xcXFxce30oKVtcXF1eJCs/Ll0vZyxcIlxcXFwkJlwiKS5yZXBsYWNlKC9cXCovZyxcIi4rXCIpK1wiJFwiKTtyZXR1cm4gdD0+ZS50ZXN0KHQpfXJldHVybiB1KFwicGF0dGVybiBtYXRjaGVyIGludmFsaWRcIixcIlBhdHRlcm4gbWF0Y2hpbmcgb25seSBhY2NlcHRzIHN0cmluZ3MsIHJlZ3VsYXIgZXhwcmVzc2lvbnMgYW5kL29yIGZ1bmN0aW9ucy4gVGhpcyBpdGVtIGlzIGludmFsaWQgYW5kIHdpbGwgYmUgaWdub3JlZC5cIix0KSwoKT0+ITF9KSk7cmV0dXJuIHQ9PmUuc29tZSgoZT0+ZSh0KSkpfWZ1bmN0aW9uIGcodCxlLG4pe2NvbnN0IHI9QXJyYXkuZnJvbShkKG4sdFswXSkucXVlcnlTZWxlY3RvckFsbChlKSk7cmV0dXJuIHIubGVuZ3RoPT09dC5sZW5ndGgmJnQuZXZlcnkoKHQ9PnIuaW5jbHVkZXModCkpKX1mdW5jdGlvbiBiKHQsZSl7ZT1udWxsIT1lP2U6ZnVuY3Rpb24odCl7cmV0dXJuIHQub3duZXJEb2N1bWVudC5xdWVyeVNlbGVjdG9yKFwiOnJvb3RcIil9KHQpO2NvbnN0IG49W107bGV0IHI9dDtmb3IoO2kocikmJnIhPT1lOyluLnB1c2gocikscj1yLnBhcmVudEVsZW1lbnQ7cmV0dXJuIG59ZnVuY3Rpb24gdih0LGUpe3JldHVybiBtKHQubWFwKCh0PT5iKHQsZSkpKSl9Y29uc3QgTj17W3QuTk9ORV06e3R5cGU6dC5OT05FLHZhbHVlOlwiXCJ9LFt0LkRFU0NFTkRBTlRdOnt0eXBlOnQuREVTQ0VOREFOVCx2YWx1ZTpcIiA+IFwifSxbdC5DSElMRF06e3R5cGU6dC5DSElMRCx2YWx1ZTpcIiBcIn19LFM9bmV3IFJlZ0V4cChbXCJeJFwiLFwiXFxcXHNcIixcIl5cXFxcZFwiXS5qb2luKFwifFwiKSksRT1uZXcgUmVnRXhwKFtcIl4kXCIsXCJeXFxcXGRcIl0uam9pbihcInxcIikpLHc9W2UubnRob2Z0eXBlLGUudGFnLGUuaWQsZS5jbGFzcyxlLmF0dHJpYnV0ZSxlLm50aGNoaWxkXTt2YXIgeD1uKDQyNiksQT1uLm4oeCk7Y29uc3QgQz15KFtcImNsYXNzXCIsXCJpZFwiLFwibmctKlwiXSk7ZnVuY3Rpb24gTyh7bm9kZU5hbWU6dH0pe3JldHVybmBbJHt0fV1gfWZ1bmN0aW9uIFQoe25vZGVOYW1lOnQsbm9kZVZhbHVlOmV9KXtyZXR1cm5gWyR7dH09JyR7WShlKX0nXWB9ZnVuY3Rpb24gSSh7bm9kZU5hbWU6dH0pe3JldHVybiFDKHQpfWZ1bmN0aW9uIGoodCl7Y29uc3QgZT1BcnJheS5mcm9tKHQuYXR0cmlidXRlcykuZmlsdGVyKEkpO3JldHVyblsuLi5lLm1hcChPKSwuLi5lLm1hcChUKV19ZnVuY3Rpb24gRCh0KXtyZXR1cm4odC5nZXRBdHRyaWJ1dGUoXCJjbGFzc1wiKXx8XCJcIikudHJpbSgpLnNwbGl0KC9cXHMrLykuZmlsdGVyKCh0PT4hRS50ZXN0KHQpKSkubWFwKCh0PT5gLiR7WSh0KX1gKSl9ZnVuY3Rpb24gJCh0KXtjb25zdCBlPXQuZ2V0QXR0cmlidXRlKFwiaWRcIil8fFwiXCIsbj1gIyR7WShlKX1gLHI9dC5nZXRSb290Tm9kZSh7Y29tcG9zZWQ6ITF9KTtyZXR1cm4hUy50ZXN0KGUpJiZnKFt0XSxuLHIpP1tuXTpbXX1mdW5jdGlvbiBQKHQpe2NvbnN0IGU9dC5wYXJlbnROb2RlO2lmKGUpe2NvbnN0IG49QXJyYXkuZnJvbShlLmNoaWxkTm9kZXMpLmZpbHRlcihpKS5pbmRleE9mKHQpO2lmKG4+LTEpcmV0dXJuW2A6bnRoLWNoaWxkKCR7bisxfSlgXX1yZXR1cm5bXX1mdW5jdGlvbiBSKHQpe3JldHVybltZKHQudGFnTmFtZS50b0xvd2VyQ2FzZSgpKV19ZnVuY3Rpb24gXyh0KXtjb25zdCBlPVsuLi5uZXcgU2V0KGgodC5tYXAoUikpKV07cmV0dXJuIDA9PT1lLmxlbmd0aHx8ZS5sZW5ndGg+MT9bXTpbZVswXV19ZnVuY3Rpb24gayh0KXtjb25zdCBlPV8oW3RdKVswXSxuPXQucGFyZW50RWxlbWVudDtpZihuKXtjb25zdCByPUFycmF5LmZyb20obi5jaGlsZHJlbikuZmlsdGVyKCh0PT50LnRhZ05hbWUudG9Mb3dlckNhc2UoKT09PWUpKS5pbmRleE9mKHQpO2lmKHI+LTEpcmV0dXJuW2Ake2V9Om50aC1vZi10eXBlKCR7cisxfSlgXX1yZXR1cm5bXX1mdW5jdGlvbiBNKHQ9W10se21heFJlc3VsdHM6ZT1OdW1iZXIuUE9TSVRJVkVfSU5GSU5JVFl9PXt9KXtjb25zdCBuPVtdO2xldCByPTAsbz1xKDEpO2Zvcig7by5sZW5ndGg8PXQubGVuZ3RoJiZyPGU7KXIrPTEsbi5wdXNoKG8ubWFwKChlPT50W2VdKSkpLG89TChvLHQubGVuZ3RoLTEpO3JldHVybiBufWZ1bmN0aW9uIEwodD1bXSxlPTApe2NvbnN0IG49dC5sZW5ndGg7aWYoMD09PW4pcmV0dXJuW107Y29uc3Qgcj1bLi4udF07cltuLTFdKz0xO2ZvcihsZXQgdD1uLTE7dD49MDt0LS0paWYoclt0XT5lKXtpZigwPT09dClyZXR1cm4gcShuKzEpO3JbdC0xXSsrLHJbdF09clt0LTFdKzF9cmV0dXJuIHJbbi0xXT5lP3EobisxKTpyfWZ1bmN0aW9uIHEodD0xKXtyZXR1cm4gQXJyYXkuZnJvbShBcnJheSh0KS5rZXlzKCkpfWNvbnN0IEY9XCI6XCIuY2hhckNvZGVBdCgwKS50b1N0cmluZygxNikudG9VcHBlckNhc2UoKSxWPS9bICFcIiMkJSYnKClcXFtcXF17fH08PiorLC4vOz0/QF5gflxcXFxdLztmdW5jdGlvbiBZKHQ9XCJcIil7dmFyIGUsbjtyZXR1cm4gbnVsbCE9PShuPW51bGw9PT0oZT1udWxsPT09Q1NTfHx2b2lkIDA9PT1DU1M/dm9pZCAwOkNTUy5lc2NhcGUpfHx2b2lkIDA9PT1lP3ZvaWQgMDplLmNhbGwoQ1NTLHQpKSYmdm9pZCAwIT09bj9uOmZ1bmN0aW9uKHQ9XCJcIil7cmV0dXJuIHQuc3BsaXQoXCJcIikubWFwKCh0PT5cIjpcIj09PXQ/YFxcXFwke0Z9IGA6Vi50ZXN0KHQpP2BcXFxcJHt0fWA6ZXNjYXBlKHQpLnJlcGxhY2UoLyUvZyxcIlxcXFxcIikpKS5qb2luKFwiXCIpfSh0KX1jb25zdCBCPXt0YWc6XyxpZDpmdW5jdGlvbih0KXtyZXR1cm4gMD09PXQubGVuZ3RofHx0Lmxlbmd0aD4xP1tdOiQodFswXSl9LGNsYXNzOmZ1bmN0aW9uKHQpe3JldHVybiBtKHQubWFwKEQpKX0sYXR0cmlidXRlOmZ1bmN0aW9uKHQpe3JldHVybiBtKHQubWFwKGopKX0sbnRoY2hpbGQ6ZnVuY3Rpb24odCl7cmV0dXJuIG0odC5tYXAoUCkpfSxudGhvZnR5cGU6ZnVuY3Rpb24odCl7cmV0dXJuIG0odC5tYXAoaykpfX0sRz17dGFnOlIsaWQ6JCxjbGFzczpELGF0dHJpYnV0ZTpqLG50aGNoaWxkOlAsbnRob2Z0eXBlOmt9O2Z1bmN0aW9uIFcodCl7cmV0dXJuIHQuaW5jbHVkZXMoZS50YWcpfHx0LmluY2x1ZGVzKGUubnRob2Z0eXBlKT9bLi4udF06Wy4uLnQsZS50YWddfWZ1bmN0aW9uIEgodD17fSl7Y29uc3Qgbj1bLi4ud107cmV0dXJuIHRbZS50YWddJiZ0W2UubnRob2Z0eXBlXSYmbi5zcGxpY2Uobi5pbmRleE9mKGUudGFnKSwxKSxuLm1hcCgoZT0+e3JldHVybihyPXQpW249ZV0/cltuXS5qb2luKFwiXCIpOlwiXCI7dmFyIG4scn0pKS5qb2luKFwiXCIpfWZ1bmN0aW9uIFUodCxlLG49XCJcIixyKXtjb25zdCBvPWZ1bmN0aW9uKHQsZSl7cmV0dXJuXCJcIj09PWU/dDpmdW5jdGlvbih0LGUpe3JldHVyblsuLi50Lm1hcCgodD0+ZStcIiBcIit0KSksLi4udC5tYXAoKHQ9PmUrXCIgPiBcIit0KSldfSh0LGUpfShmdW5jdGlvbih0LGUsbil7Y29uc3Qgcj1oKGZ1bmN0aW9uKHQsZSl7cmV0dXJuIGZ1bmN0aW9uKHQpe2NvbnN0e3NlbGVjdG9yczplLGNvbWJpbmVCZXR3ZWVuU2VsZWN0b3JzOm4saW5jbHVkZVRhZzpyLG1heENhbmRpZGF0ZXM6b309dCxpPW4/TShlLHttYXhSZXN1bHRzOm99KTplLm1hcCgodD0+W3RdKSk7cmV0dXJuIHI/aS5tYXAoVyk6aX0oZSkubWFwKChlPT5mdW5jdGlvbih0LGUpe2NvbnN0IG49e307cmV0dXJuIHQuZm9yRWFjaCgodD0+e2NvbnN0IHI9ZVt0XTtyLmxlbmd0aD4wJiYoblt0XT1yKX0pKSxBKCkobikubWFwKEgpfShlLHQpKSkuZmlsdGVyKCh0PT50Lmxlbmd0aD4wKSl9KGZ1bmN0aW9uKHQsZSl7Y29uc3R7YmxhY2tsaXN0Om4sd2hpdGVsaXN0OnIsY29tYmluZVdpdGhpblNlbGVjdG9yOm8sbWF4Q29tYmluYXRpb25zOml9PWUsdT15KG4pLGM9eShyKTtyZXR1cm4gZnVuY3Rpb24odCl7Y29uc3R7c2VsZWN0b3JzOmUsaW5jbHVkZVRhZzpufT10LHI9W10uY29uY2F0KGUpO3JldHVybiBuJiYhci5pbmNsdWRlcyhcInRhZ1wiKSYmci5wdXNoKFwidGFnXCIpLHJ9KGUpLnJlZHVjZSgoKGUsbik9Pntjb25zdCByPWZ1bmN0aW9uKHQ9W10sZSl7cmV0dXJuIHQuc29ydCgoKHQsbik9Pntjb25zdCByPWUodCksbz1lKG4pO3JldHVybiByJiYhbz8tMTohciYmbz8xOjB9KSl9KGZ1bmN0aW9uKHQ9W10sZSxuKXtyZXR1cm4gdC5maWx0ZXIoKHQ9Pm4odCl8fCFlKHQpKSl9KGZ1bmN0aW9uKHQsZSl7dmFyIG47cmV0dXJuKG51bGwhPT0obj1CW2VdKSYmdm9pZCAwIT09bj9uOigpPT5bXSkodCl9KHQsbiksdSxjKSxjKTtyZXR1cm4gZVtuXT1vP00ocix7bWF4UmVzdWx0czppfSk6ci5tYXAoKHQ9Plt0XSkpLGV9KSx7fSl9KHQsbiksbikpO3JldHVyblsuLi5uZXcgU2V0KHIpXX0odCxyLnJvb3Qsciksbik7Zm9yKGNvbnN0IGUgb2YgbylpZihnKHQsZSxyLnJvb3QpKXJldHVybiBlO3JldHVybiBudWxsfWZ1bmN0aW9uIHoodCl7cmV0dXJue3ZhbHVlOnQsaW5jbHVkZTohMX19ZnVuY3Rpb24gSih7c2VsZWN0b3JzOnQsb3BlcmF0b3I6bn0pe2xldCByPVsuLi53XTt0W2UudGFnXSYmdFtlLm50aG9mdHlwZV0mJihyPXIuZmlsdGVyKCh0PT50IT09ZS50YWcpKSk7bGV0IG89XCJcIjtyZXR1cm4gci5mb3JFYWNoKChlPT57KHRbZV18fFtdKS5mb3JFYWNoKCgoe3ZhbHVlOnQsaW5jbHVkZTplfSk9PntlJiYobys9dCl9KSl9KSksbi52YWx1ZStvfWZ1bmN0aW9uIEsobil7cmV0dXJuW1wiOnJvb3RcIiwuLi5iKG4pLnJldmVyc2UoKS5tYXAoKG49Pntjb25zdCByPWZ1bmN0aW9uKGUsbixyPXQuTk9ORSl7Y29uc3Qgbz17fTtyZXR1cm4gbi5mb3JFYWNoKCh0PT57UmVmbGVjdC5zZXQobyx0LGZ1bmN0aW9uKHQsZSl7cmV0dXJuIEdbZV0odCl9KGUsdCkubWFwKHopKX0pKSx7ZWxlbWVudDplLG9wZXJhdG9yOk5bcl0sc2VsZWN0b3JzOm99fShuLFtlLm50aGNoaWxkXSx0LkRFU0NFTkRBTlQpO3JldHVybiByLnNlbGVjdG9ycy5udGhjaGlsZC5mb3JFYWNoKCh0PT57dC5pbmNsdWRlPSEwfSkpLHJ9KSkubWFwKEopXS5qb2luKFwiXCIpfWZ1bmN0aW9uIFEodCxuPXt9KXtjb25zdCByPWZ1bmN0aW9uKHQpe2NvbnN0IGU9KEFycmF5LmlzQXJyYXkodCk/dDpbdF0pLmZpbHRlcihpKTtyZXR1cm5bLi4ubmV3IFNldChlKV19KHQpLG89ZnVuY3Rpb24odCxuPXt9KXtjb25zdCByPU9iamVjdC5hc3NpZ24oT2JqZWN0LmFzc2lnbih7fSxjKSxuKTtyZXR1cm57c2VsZWN0b3JzOihvPXIuc2VsZWN0b3JzLEFycmF5LmlzQXJyYXkobyk/by5maWx0ZXIoKHQ9PntyZXR1cm4gbj1lLHI9dCxPYmplY3QudmFsdWVzKG4pLmluY2x1ZGVzKHIpO3ZhciBuLHJ9KSk6W10pLHdoaXRlbGlzdDpzKHIud2hpdGVsaXN0KSxibGFja2xpc3Q6cyhyLmJsYWNrbGlzdCkscm9vdDpkKHIucm9vdCx0KSxjb21iaW5lV2l0aGluU2VsZWN0b3I6ISFyLmNvbWJpbmVXaXRoaW5TZWxlY3Rvcixjb21iaW5lQmV0d2VlblNlbGVjdG9yczohIXIuY29tYmluZUJldHdlZW5TZWxlY3RvcnMsaW5jbHVkZVRhZzohIXIuaW5jbHVkZVRhZyxtYXhDb21iaW5hdGlvbnM6cChyLm1heENvbWJpbmF0aW9ucyksbWF4Q2FuZGlkYXRlczpwKHIubWF4Q2FuZGlkYXRlcyl9O3ZhciBvfShyWzBdLG4pO2xldCB1PVwiXCIsYT1vLnJvb3Q7ZnVuY3Rpb24gbCgpe3JldHVybiBmdW5jdGlvbih0LGUsbj1cIlwiLHIpe2lmKDA9PT10Lmxlbmd0aClyZXR1cm4gbnVsbDtjb25zdCBvPVt0Lmxlbmd0aD4xP3Q6W10sLi4udih0LGUpLm1hcCgodD0+W3RdKSldO2Zvcihjb25zdCB0IG9mIG8pe2NvbnN0IGU9VSh0LDAsbixyKTtpZihlKXJldHVybntmb3VuZEVsZW1lbnRzOnQsc2VsZWN0b3I6ZX19cmV0dXJuIG51bGx9KHIsYSx1LG8pfWxldCBmPWwoKTtmb3IoO2Y7KXtjb25zdHtmb3VuZEVsZW1lbnRzOnQsc2VsZWN0b3I6ZX09ZjtpZihnKHIsZSxvLnJvb3QpKXJldHVybiBlO2E9dFswXSx1PWUsZj1sKCl9cmV0dXJuIHIubGVuZ3RoPjE/ci5tYXAoKHQ9PlEodCxvKSkpLmpvaW4oXCIsIFwiKTpmdW5jdGlvbih0KXtyZXR1cm4gdC5tYXAoSykuam9pbihcIiwgXCIpfShyKX1jb25zdCBYPVF9KSgpLHJ9KSgpfSkpOyJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///4766\n')},7912:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $Array = GetIntrinsic('%Array%');\n\n// eslint-disable-next-line global-require\nvar toStr = !$Array.isArray && __webpack_require__(1924)('Object.prototype.toString');\n\n// https://ecma-international.org/ecma-262/6.0/#sec-isarray\n\nmodule.exports = $Array.isArray || function IsArray(argument) {\n\treturn toStr(argument) === '[object Array]';\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzkxMi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQTtBQUNBLCtCQUErQixtQkFBTyxDQUFDLElBQXFCOztBQUU1RDs7QUFFQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjAvSXNBcnJheS5qcz83MGQ4Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRBcnJheSA9IEdldEludHJpbnNpYygnJUFycmF5JScpO1xuXG4vLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgZ2xvYmFsLXJlcXVpcmVcbnZhciB0b1N0ciA9ICEkQXJyYXkuaXNBcnJheSAmJiByZXF1aXJlKCdjYWxsLWJpbmQvY2FsbEJvdW5kJykoJ09iamVjdC5wcm90b3R5cGUudG9TdHJpbmcnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWlzYXJyYXlcblxubW9kdWxlLmV4cG9ydHMgPSAkQXJyYXkuaXNBcnJheSB8fCBmdW5jdGlvbiBJc0FycmF5KGFyZ3VtZW50KSB7XG5cdHJldHVybiB0b1N0cihhcmd1bWVudCkgPT09ICdbb2JqZWN0IEFycmF5XSc7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///7912\n")},4200:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar CodePointAt = __webpack_require__(2432);\nvar IsIntegralNumber = __webpack_require__(7312);\nvar Type = __webpack_require__(3633);\n\nvar MAX_SAFE_INTEGER = __webpack_require__(1645);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\n// https://ecma-international.org/ecma-262/12.0/#sec-advancestringindex\n\nmodule.exports = function AdvanceStringIndex(S, index, unicode) {\n\tif (Type(S) !== 'String') {\n\t\tthrow new $TypeError('Assertion failed: `S` must be a String');\n\t}\n\tif (!IsIntegralNumber(index) || index < 0 || index > MAX_SAFE_INTEGER) {\n\t\tthrow new $TypeError('Assertion failed: `length` must be an integer >= 0 and <= 2**53');\n\t}\n\tif (Type(unicode) !== 'Boolean') {\n\t\tthrow new $TypeError('Assertion failed: `unicode` must be a Boolean');\n\t}\n\tif (!unicode) {\n\t\treturn index + 1;\n\t}\n\tvar length = S.length;\n\tif ((index + 1) >= length) {\n\t\treturn index + 1;\n\t}\n\tvar cp = CodePointAt(S, index);\n\treturn index + cp['[[CodeUnitCount]]'];\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDIwMC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQyxrQkFBa0IsbUJBQU8sQ0FBQyxJQUFlO0FBQ3pDLHVCQUF1QixtQkFBTyxDQUFDLElBQW9CO0FBQ25ELFdBQVcsbUJBQU8sQ0FBQyxJQUFROztBQUUzQix1QkFBdUIsbUJBQU8sQ0FBQyxJQUEyQjs7QUFFMUQ7O0FBRUE7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9BZHZhbmNlU3RyaW5nSW5kZXguanM/YTg1YiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciBDb2RlUG9pbnRBdCA9IHJlcXVpcmUoJy4vQ29kZVBvaW50QXQnKTtcbnZhciBJc0ludGVncmFsTnVtYmVyID0gcmVxdWlyZSgnLi9Jc0ludGVncmFsTnVtYmVyJyk7XG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG52YXIgTUFYX1NBRkVfSU5URUdFUiA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvbWF4U2FmZUludGVnZXInKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG5cbi8vIGh0dHBzOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi8xMi4wLyNzZWMtYWR2YW5jZXN0cmluZ2luZGV4XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gQWR2YW5jZVN0cmluZ0luZGV4KFMsIGluZGV4LCB1bmljb2RlKSB7XG5cdGlmIChUeXBlKFMpICE9PSAnU3RyaW5nJykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBgU2AgbXVzdCBiZSBhIFN0cmluZycpO1xuXHR9XG5cdGlmICghSXNJbnRlZ3JhbE51bWJlcihpbmRleCkgfHwgaW5kZXggPCAwIHx8IGluZGV4ID4gTUFYX1NBRkVfSU5URUdFUikge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBgbGVuZ3RoYCBtdXN0IGJlIGFuIGludGVnZXIgPj0gMCBhbmQgPD0gMioqNTMnKTtcblx0fVxuXHRpZiAoVHlwZSh1bmljb2RlKSAhPT0gJ0Jvb2xlYW4nKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IGB1bmljb2RlYCBtdXN0IGJlIGEgQm9vbGVhbicpO1xuXHR9XG5cdGlmICghdW5pY29kZSkge1xuXHRcdHJldHVybiBpbmRleCArIDE7XG5cdH1cblx0dmFyIGxlbmd0aCA9IFMubGVuZ3RoO1xuXHRpZiAoKGluZGV4ICsgMSkgPj0gbGVuZ3RoKSB7XG5cdFx0cmV0dXJuIGluZGV4ICsgMTtcblx0fVxuXHR2YXIgY3AgPSBDb2RlUG9pbnRBdChTLCBpbmRleCk7XG5cdHJldHVybiBpbmRleCArIGNwWydbW0NvZGVVbml0Q291bnRdXSddO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///4200\n")},581:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\nvar callBound = __webpack_require__(1924);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar IsArray = __webpack_require__(6975);\n\nvar $apply = GetIntrinsic('%Reflect.apply%', true) || callBound('%Function.prototype.apply%');\n\n// https://ecma-international.org/ecma-262/6.0/#sec-call\n\nmodule.exports = function Call(F, V) {\n\tvar argumentsList = arguments.length > 2 ? arguments[2] : [];\n\tif (!IsArray(argumentsList)) {\n\t\tthrow new $TypeError('Assertion failed: optional `argumentsList`, if provided, must be a List');\n\t}\n\treturn $apply(F, V, argumentsList);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTgxLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWU7QUFDMUMsZ0JBQWdCLG1CQUFPLENBQUMsSUFBcUI7O0FBRTdDOztBQUVBLGNBQWMsbUJBQU8sQ0FBQyxJQUFXOztBQUVqQzs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9DYWxsLmpzP2Y4M2YiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xudmFyIGNhbGxCb3VuZCA9IHJlcXVpcmUoJ2NhbGwtYmluZC9jYWxsQm91bmQnKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG5cbnZhciBJc0FycmF5ID0gcmVxdWlyZSgnLi9Jc0FycmF5Jyk7XG5cbnZhciAkYXBwbHkgPSBHZXRJbnRyaW5zaWMoJyVSZWZsZWN0LmFwcGx5JScsIHRydWUpIHx8IGNhbGxCb3VuZCgnJUZ1bmN0aW9uLnByb3RvdHlwZS5hcHBseSUnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWNhbGxcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBDYWxsKEYsIFYpIHtcblx0dmFyIGFyZ3VtZW50c0xpc3QgPSBhcmd1bWVudHMubGVuZ3RoID4gMiA/IGFyZ3VtZW50c1syXSA6IFtdO1xuXHRpZiAoIUlzQXJyYXkoYXJndW1lbnRzTGlzdCkpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogb3B0aW9uYWwgYGFyZ3VtZW50c0xpc3RgLCBpZiBwcm92aWRlZCwgbXVzdCBiZSBhIExpc3QnKTtcblx0fVxuXHRyZXR1cm4gJGFwcGx5KEYsIFYsIGFyZ3VtZW50c0xpc3QpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///581\n")},2432:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar callBound = __webpack_require__(1924);\nvar isLeadingSurrogate = __webpack_require__(9544);\nvar isTrailingSurrogate = __webpack_require__(5424);\n\nvar Type = __webpack_require__(3633);\nvar UTF16SurrogatePairToCodePoint = __webpack_require__(4857);\n\nvar $charAt = callBound('String.prototype.charAt');\nvar $charCodeAt = callBound('String.prototype.charCodeAt');\n\n// https://ecma-international.org/ecma-262/12.0/#sec-codepointat\n\nmodule.exports = function CodePointAt(string, position) {\n\tif (Type(string) !== 'String') {\n\t\tthrow new $TypeError('Assertion failed: `string` must be a String');\n\t}\n\tvar size = string.length;\n\tif (position < 0 || position >= size) {\n\t\tthrow new $TypeError('Assertion failed: `position` must be >= 0, and < the length of `string`');\n\t}\n\tvar first = $charCodeAt(string, position);\n\tvar cp = $charAt(string, position);\n\tvar firstIsLeading = isLeadingSurrogate(first);\n\tvar firstIsTrailing = isTrailingSurrogate(first);\n\tif (!firstIsLeading && !firstIsTrailing) {\n\t\treturn {\n\t\t\t'[[CodePoint]]': cp,\n\t\t\t'[[CodeUnitCount]]': 1,\n\t\t\t'[[IsUnpairedSurrogate]]': false\n\t\t};\n\t}\n\tif (firstIsTrailing || (position + 1 === size)) {\n\t\treturn {\n\t\t\t'[[CodePoint]]': cp,\n\t\t\t'[[CodeUnitCount]]': 1,\n\t\t\t'[[IsUnpairedSurrogate]]': true\n\t\t};\n\t}\n\tvar second = $charCodeAt(string, position + 1);\n\tif (!isTrailingSurrogate(second)) {\n\t\treturn {\n\t\t\t'[[CodePoint]]': cp,\n\t\t\t'[[CodeUnitCount]]': 1,\n\t\t\t'[[IsUnpairedSurrogate]]': true\n\t\t};\n\t}\n\n\treturn {\n\t\t'[[CodePoint]]': UTF16SurrogatePairToCodePoint(first, second),\n\t\t'[[CodeUnitCount]]': 2,\n\t\t'[[IsUnpairedSurrogate]]': false\n\t};\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjQzMi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBLGdCQUFnQixtQkFBTyxDQUFDLElBQXFCO0FBQzdDLHlCQUF5QixtQkFBTyxDQUFDLElBQStCO0FBQ2hFLDBCQUEwQixtQkFBTyxDQUFDLElBQWdDOztBQUVsRSxXQUFXLG1CQUFPLENBQUMsSUFBUTtBQUMzQixvQ0FBb0MsbUJBQU8sQ0FBQyxJQUFpQzs7QUFFN0U7QUFDQTs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvQ29kZVBvaW50QXQuanM/NTNmOCJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xudmFyIGNhbGxCb3VuZCA9IHJlcXVpcmUoJ2NhbGwtYmluZC9jYWxsQm91bmQnKTtcbnZhciBpc0xlYWRpbmdTdXJyb2dhdGUgPSByZXF1aXJlKCcuLi9oZWxwZXJzL2lzTGVhZGluZ1N1cnJvZ2F0ZScpO1xudmFyIGlzVHJhaWxpbmdTdXJyb2dhdGUgPSByZXF1aXJlKCcuLi9oZWxwZXJzL2lzVHJhaWxpbmdTdXJyb2dhdGUnKTtcblxudmFyIFR5cGUgPSByZXF1aXJlKCcuL1R5cGUnKTtcbnZhciBVVEYxNlN1cnJvZ2F0ZVBhaXJUb0NvZGVQb2ludCA9IHJlcXVpcmUoJy4vVVRGMTZTdXJyb2dhdGVQYWlyVG9Db2RlUG9pbnQnKTtcblxudmFyICRjaGFyQXQgPSBjYWxsQm91bmQoJ1N0cmluZy5wcm90b3R5cGUuY2hhckF0Jyk7XG52YXIgJGNoYXJDb2RlQXQgPSBjYWxsQm91bmQoJ1N0cmluZy5wcm90b3R5cGUuY2hhckNvZGVBdCcpO1xuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvMTIuMC8jc2VjLWNvZGVwb2ludGF0XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gQ29kZVBvaW50QXQoc3RyaW5nLCBwb3NpdGlvbikge1xuXHRpZiAoVHlwZShzdHJpbmcpICE9PSAnU3RyaW5nJykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBgc3RyaW5nYCBtdXN0IGJlIGEgU3RyaW5nJyk7XG5cdH1cblx0dmFyIHNpemUgPSBzdHJpbmcubGVuZ3RoO1xuXHRpZiAocG9zaXRpb24gPCAwIHx8IHBvc2l0aW9uID49IHNpemUpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogYHBvc2l0aW9uYCBtdXN0IGJlID49IDAsIGFuZCA8IHRoZSBsZW5ndGggb2YgYHN0cmluZ2AnKTtcblx0fVxuXHR2YXIgZmlyc3QgPSAkY2hhckNvZGVBdChzdHJpbmcsIHBvc2l0aW9uKTtcblx0dmFyIGNwID0gJGNoYXJBdChzdHJpbmcsIHBvc2l0aW9uKTtcblx0dmFyIGZpcnN0SXNMZWFkaW5nID0gaXNMZWFkaW5nU3Vycm9nYXRlKGZpcnN0KTtcblx0dmFyIGZpcnN0SXNUcmFpbGluZyA9IGlzVHJhaWxpbmdTdXJyb2dhdGUoZmlyc3QpO1xuXHRpZiAoIWZpcnN0SXNMZWFkaW5nICYmICFmaXJzdElzVHJhaWxpbmcpIHtcblx0XHRyZXR1cm4ge1xuXHRcdFx0J1tbQ29kZVBvaW50XV0nOiBjcCxcblx0XHRcdCdbW0NvZGVVbml0Q291bnRdXSc6IDEsXG5cdFx0XHQnW1tJc1VucGFpcmVkU3Vycm9nYXRlXV0nOiBmYWxzZVxuXHRcdH07XG5cdH1cblx0aWYgKGZpcnN0SXNUcmFpbGluZyB8fCAocG9zaXRpb24gKyAxID09PSBzaXplKSkge1xuXHRcdHJldHVybiB7XG5cdFx0XHQnW1tDb2RlUG9pbnRdXSc6IGNwLFxuXHRcdFx0J1tbQ29kZVVuaXRDb3VudF1dJzogMSxcblx0XHRcdCdbW0lzVW5wYWlyZWRTdXJyb2dhdGVdXSc6IHRydWVcblx0XHR9O1xuXHR9XG5cdHZhciBzZWNvbmQgPSAkY2hhckNvZGVBdChzdHJpbmcsIHBvc2l0aW9uICsgMSk7XG5cdGlmICghaXNUcmFpbGluZ1N1cnJvZ2F0ZShzZWNvbmQpKSB7XG5cdFx0cmV0dXJuIHtcblx0XHRcdCdbW0NvZGVQb2ludF1dJzogY3AsXG5cdFx0XHQnW1tDb2RlVW5pdENvdW50XV0nOiAxLFxuXHRcdFx0J1tbSXNVbnBhaXJlZFN1cnJvZ2F0ZV1dJzogdHJ1ZVxuXHRcdH07XG5cdH1cblxuXHRyZXR1cm4ge1xuXHRcdCdbW0NvZGVQb2ludF1dJzogVVRGMTZTdXJyb2dhdGVQYWlyVG9Db2RlUG9pbnQoZmlyc3QsIHNlY29uZCksXG5cdFx0J1tbQ29kZVVuaXRDb3VudF1dJzogMixcblx0XHQnW1tJc1VucGFpcmVkU3Vycm9nYXRlXV0nOiBmYWxzZVxuXHR9O1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2432\n")},2658:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-createiterresultobject\n\nmodule.exports = function CreateIterResultObject(value, done) {\n\tif (Type(done) !== 'Boolean') {\n\t\tthrow new $TypeError('Assertion failed: Type(done) is not Boolean');\n\t}\n\treturn {\n\t\tvalue: value,\n\t\tdone: done\n\t};\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjY1OC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSxXQUFXLG1CQUFPLENBQUMsSUFBUTs7QUFFM0I7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL0NyZWF0ZUl0ZXJSZXN1bHRPYmplY3QuanM/NDk1YSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xuXG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtY3JlYXRlaXRlcnJlc3VsdG9iamVjdFxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIENyZWF0ZUl0ZXJSZXN1bHRPYmplY3QodmFsdWUsIGRvbmUpIHtcblx0aWYgKFR5cGUoZG9uZSkgIT09ICdCb29sZWFuJykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBUeXBlKGRvbmUpIGlzIG5vdCBCb29sZWFuJyk7XG5cdH1cblx0cmV0dXJuIHtcblx0XHR2YWx1ZTogdmFsdWUsXG5cdFx0ZG9uZTogZG9uZVxuXHR9O1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2658\n")},7730:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar DefineOwnProperty = __webpack_require__(3682);\n\nvar FromPropertyDescriptor = __webpack_require__(8334);\nvar IsDataDescriptor = __webpack_require__(3746);\nvar IsPropertyKey = __webpack_require__(4305);\nvar SameValue = __webpack_require__(484);\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-createmethodproperty\n\nmodule.exports = function CreateMethodProperty(O, P, V) {\n\tif (Type(O) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: Type(O) is not Object');\n\t}\n\n\tif (!IsPropertyKey(P)) {\n\t\tthrow new $TypeError('Assertion failed: IsPropertyKey(P) is not true');\n\t}\n\n\tvar newDesc = {\n\t\t'[[Configurable]]': true,\n\t\t'[[Enumerable]]': false,\n\t\t'[[Value]]': V,\n\t\t'[[Writable]]': true\n\t};\n\treturn DefineOwnProperty(\n\t\tIsDataDescriptor,\n\t\tSameValue,\n\t\tFromPropertyDescriptor,\n\t\tO,\n\t\tP,\n\t\tnewDesc\n\t);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzczMC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSx3QkFBd0IsbUJBQU8sQ0FBQyxJQUE4Qjs7QUFFOUQsNkJBQTZCLG1CQUFPLENBQUMsSUFBMEI7QUFDL0QsdUJBQXVCLG1CQUFPLENBQUMsSUFBb0I7QUFDbkQsb0JBQW9CLG1CQUFPLENBQUMsSUFBaUI7QUFDN0MsZ0JBQWdCLG1CQUFPLENBQUMsR0FBYTtBQUNyQyxXQUFXLG1CQUFPLENBQUMsSUFBUTs7QUFFM0I7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9DcmVhdGVNZXRob2RQcm9wZXJ0eS5qcz9iODljIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG5cbnZhciBEZWZpbmVPd25Qcm9wZXJ0eSA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvRGVmaW5lT3duUHJvcGVydHknKTtcblxudmFyIEZyb21Qcm9wZXJ0eURlc2NyaXB0b3IgPSByZXF1aXJlKCcuL0Zyb21Qcm9wZXJ0eURlc2NyaXB0b3InKTtcbnZhciBJc0RhdGFEZXNjcmlwdG9yID0gcmVxdWlyZSgnLi9Jc0RhdGFEZXNjcmlwdG9yJyk7XG52YXIgSXNQcm9wZXJ0eUtleSA9IHJlcXVpcmUoJy4vSXNQcm9wZXJ0eUtleScpO1xudmFyIFNhbWVWYWx1ZSA9IHJlcXVpcmUoJy4vU2FtZVZhbHVlJyk7XG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtY3JlYXRlbWV0aG9kcHJvcGVydHlcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBDcmVhdGVNZXRob2RQcm9wZXJ0eShPLCBQLCBWKSB7XG5cdGlmIChUeXBlKE8pICE9PSAnT2JqZWN0Jykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBUeXBlKE8pIGlzIG5vdCBPYmplY3QnKTtcblx0fVxuXG5cdGlmICghSXNQcm9wZXJ0eUtleShQKSkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBJc1Byb3BlcnR5S2V5KFApIGlzIG5vdCB0cnVlJyk7XG5cdH1cblxuXHR2YXIgbmV3RGVzYyA9IHtcblx0XHQnW1tDb25maWd1cmFibGVdXSc6IHRydWUsXG5cdFx0J1tbRW51bWVyYWJsZV1dJzogZmFsc2UsXG5cdFx0J1tbVmFsdWVdXSc6IFYsXG5cdFx0J1tbV3JpdGFibGVdXSc6IHRydWVcblx0fTtcblx0cmV0dXJuIERlZmluZU93blByb3BlcnR5KFxuXHRcdElzRGF0YURlc2NyaXB0b3IsXG5cdFx0U2FtZVZhbHVlLFxuXHRcdEZyb21Qcm9wZXJ0eURlc2NyaXB0b3IsXG5cdFx0Tyxcblx0XHRQLFxuXHRcdG5ld0Rlc2Ncblx0KTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///7730\n")},3937:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\nvar hasSymbols = __webpack_require__(1405)();\n\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar IteratorPrototype = GetIntrinsic('%IteratorPrototype%', true);\nvar $defineProperty = GetIntrinsic('%Object.defineProperty%', true);\n\nvar AdvanceStringIndex = __webpack_require__(4200);\nvar CreateIterResultObject = __webpack_require__(2658);\nvar CreateMethodProperty = __webpack_require__(7730);\nvar Get = __webpack_require__(1391);\nvar OrdinaryObjectCreate = __webpack_require__(953);\nvar RegExpExec = __webpack_require__(6258);\nvar Set = __webpack_require__(105);\nvar ToLength = __webpack_require__(8305);\nvar ToString = __webpack_require__(6846);\nvar Type = __webpack_require__(3633);\n\nvar SLOT = __webpack_require__(9496);\n\nvar RegExpStringIterator = function RegExpStringIterator(R, S, global, fullUnicode) {\n\tif (Type(S) !== 'String') {\n\t\tthrow new $TypeError('`S` must be a string');\n\t}\n\tif (Type(global) !== 'Boolean') {\n\t\tthrow new $TypeError('`global` must be a boolean');\n\t}\n\tif (Type(fullUnicode) !== 'Boolean') {\n\t\tthrow new $TypeError('`fullUnicode` must be a boolean');\n\t}\n\tSLOT.set(this, '[[IteratingRegExp]]', R);\n\tSLOT.set(this, '[[IteratedString]]', S);\n\tSLOT.set(this, '[[Global]]', global);\n\tSLOT.set(this, '[[Unicode]]', fullUnicode);\n\tSLOT.set(this, '[[Done]]', false);\n};\n\nif (IteratorPrototype) {\n\tRegExpStringIterator.prototype = OrdinaryObjectCreate(IteratorPrototype);\n}\n\nvar RegExpStringIteratorNext = function next() {\n\tvar O = this; // eslint-disable-line no-invalid-this\n\tif (Type(O) !== 'Object') {\n\t\tthrow new $TypeError('receiver must be an object');\n\t}\n\tif (\n\t\t!(O instanceof RegExpStringIterator)\n || !SLOT.has(O, '[[IteratingRegExp]]')\n || !SLOT.has(O, '[[IteratedString]]')\n || !SLOT.has(O, '[[Global]]')\n || !SLOT.has(O, '[[Unicode]]')\n || !SLOT.has(O, '[[Done]]')\n\t) {\n\t\tthrow new $TypeError('\"this\" value must be a RegExpStringIterator instance');\n\t}\n\tif (SLOT.get(O, '[[Done]]')) {\n\t\treturn CreateIterResultObject(undefined, true);\n\t}\n\tvar R = SLOT.get(O, '[[IteratingRegExp]]');\n\tvar S = SLOT.get(O, '[[IteratedString]]');\n\tvar global = SLOT.get(O, '[[Global]]');\n\tvar fullUnicode = SLOT.get(O, '[[Unicode]]');\n\tvar match = RegExpExec(R, S);\n\tif (match === null) {\n\t\tSLOT.set(O, '[[Done]]', true);\n\t\treturn CreateIterResultObject(undefined, true);\n\t}\n\tif (global) {\n\t\tvar matchStr = ToString(Get(match, '0'));\n\t\tif (matchStr === '') {\n\t\t\tvar thisIndex = ToLength(Get(R, 'lastIndex'));\n\t\t\tvar nextIndex = AdvanceStringIndex(S, thisIndex, fullUnicode);\n\t\t\tSet(R, 'lastIndex', nextIndex, true);\n\t\t}\n\t\treturn CreateIterResultObject(match, false);\n\t}\n\tSLOT.set(O, '[[Done]]', true);\n\treturn CreateIterResultObject(match, false);\n};\nCreateMethodProperty(RegExpStringIterator.prototype, 'next', RegExpStringIteratorNext);\n\nif (hasSymbols) {\n\tif (Symbol.toStringTag) {\n\t\tif ($defineProperty) {\n\t\t\t$defineProperty(RegExpStringIterator.prototype, Symbol.toStringTag, {\n\t\t\t\tconfigurable: true,\n\t\t\t\tenumerable: false,\n\t\t\t\tvalue: 'RegExp String Iterator',\n\t\t\t\twritable: false\n\t\t\t});\n\t\t} else {\n\t\t\tRegExpStringIterator.prototype[Symbol.toStringTag] = 'RegExp String Iterator';\n\t\t}\n\t}\n\n\tif (Symbol.iterator && typeof RegExpStringIterator.prototype[Symbol.iterator] !== 'function') {\n\t\tvar iteratorFn = function SymbolIterator() {\n\t\t\treturn this;\n\t\t};\n\t\tCreateMethodProperty(RegExpStringIterator.prototype, Symbol.iterator, iteratorFn);\n\t}\n}\n\n// https://262.ecma-international.org/11.0/#sec-createregexpstringiterator\nmodule.exports = function CreateRegExpStringIterator(R, S, global, fullUnicode) {\n\t// assert R.global === global && R.unicode === fullUnicode?\n\treturn new RegExpStringIterator(R, S, global, fullUnicode);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzkzNy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlO0FBQzFDLGlCQUFpQixtQkFBTyxDQUFDLElBQWE7O0FBRXRDO0FBQ0E7QUFDQTs7QUFFQSx5QkFBeUIsbUJBQU8sQ0FBQyxJQUFzQjtBQUN2RCw2QkFBNkIsbUJBQU8sQ0FBQyxJQUEwQjtBQUMvRCwyQkFBMkIsbUJBQU8sQ0FBQyxJQUF3QjtBQUMzRCxVQUFVLG1CQUFPLENBQUMsSUFBTztBQUN6QiwyQkFBMkIsbUJBQU8sQ0FBQyxHQUF3QjtBQUMzRCxpQkFBaUIsbUJBQU8sQ0FBQyxJQUFjO0FBQ3ZDLFVBQVUsbUJBQU8sQ0FBQyxHQUFPO0FBQ3pCLGVBQWUsbUJBQU8sQ0FBQyxJQUFZO0FBQ25DLGVBQWUsbUJBQU8sQ0FBQyxJQUFZO0FBQ25DLFdBQVcsbUJBQU8sQ0FBQyxJQUFROztBQUUzQixXQUFXLG1CQUFPLENBQUMsSUFBZTs7QUFFbEM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0EsZUFBZTtBQUNmO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLElBQUk7QUFDSixJQUFJO0FBQ0o7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvQ3JlYXRlUmVnRXhwU3RyaW5nSXRlcmF0b3IuanM/MGUzOSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG52YXIgaGFzU3ltYm9scyA9IHJlcXVpcmUoJ2hhcy1zeW1ib2xzJykoKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG52YXIgSXRlcmF0b3JQcm90b3R5cGUgPSBHZXRJbnRyaW5zaWMoJyVJdGVyYXRvclByb3RvdHlwZSUnLCB0cnVlKTtcbnZhciAkZGVmaW5lUHJvcGVydHkgPSBHZXRJbnRyaW5zaWMoJyVPYmplY3QuZGVmaW5lUHJvcGVydHklJywgdHJ1ZSk7XG5cbnZhciBBZHZhbmNlU3RyaW5nSW5kZXggPSByZXF1aXJlKCcuL0FkdmFuY2VTdHJpbmdJbmRleCcpO1xudmFyIENyZWF0ZUl0ZXJSZXN1bHRPYmplY3QgPSByZXF1aXJlKCcuL0NyZWF0ZUl0ZXJSZXN1bHRPYmplY3QnKTtcbnZhciBDcmVhdGVNZXRob2RQcm9wZXJ0eSA9IHJlcXVpcmUoJy4vQ3JlYXRlTWV0aG9kUHJvcGVydHknKTtcbnZhciBHZXQgPSByZXF1aXJlKCcuL0dldCcpO1xudmFyIE9yZGluYXJ5T2JqZWN0Q3JlYXRlID0gcmVxdWlyZSgnLi9PcmRpbmFyeU9iamVjdENyZWF0ZScpO1xudmFyIFJlZ0V4cEV4ZWMgPSByZXF1aXJlKCcuL1JlZ0V4cEV4ZWMnKTtcbnZhciBTZXQgPSByZXF1aXJlKCcuL1NldCcpO1xudmFyIFRvTGVuZ3RoID0gcmVxdWlyZSgnLi9Ub0xlbmd0aCcpO1xudmFyIFRvU3RyaW5nID0gcmVxdWlyZSgnLi9Ub1N0cmluZycpO1xudmFyIFR5cGUgPSByZXF1aXJlKCcuL1R5cGUnKTtcblxudmFyIFNMT1QgPSByZXF1aXJlKCdpbnRlcm5hbC1zbG90Jyk7XG5cbnZhciBSZWdFeHBTdHJpbmdJdGVyYXRvciA9IGZ1bmN0aW9uIFJlZ0V4cFN0cmluZ0l0ZXJhdG9yKFIsIFMsIGdsb2JhbCwgZnVsbFVuaWNvZGUpIHtcblx0aWYgKFR5cGUoUykgIT09ICdTdHJpbmcnKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ2BTYCBtdXN0IGJlIGEgc3RyaW5nJyk7XG5cdH1cblx0aWYgKFR5cGUoZ2xvYmFsKSAhPT0gJ0Jvb2xlYW4nKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ2BnbG9iYWxgIG11c3QgYmUgYSBib29sZWFuJyk7XG5cdH1cblx0aWYgKFR5cGUoZnVsbFVuaWNvZGUpICE9PSAnQm9vbGVhbicpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignYGZ1bGxVbmljb2RlYCBtdXN0IGJlIGEgYm9vbGVhbicpO1xuXHR9XG5cdFNMT1Quc2V0KHRoaXMsICdbW0l0ZXJhdGluZ1JlZ0V4cF1dJywgUik7XG5cdFNMT1Quc2V0KHRoaXMsICdbW0l0ZXJhdGVkU3RyaW5nXV0nLCBTKTtcblx0U0xPVC5zZXQodGhpcywgJ1tbR2xvYmFsXV0nLCBnbG9iYWwpO1xuXHRTTE9ULnNldCh0aGlzLCAnW1tVbmljb2RlXV0nLCBmdWxsVW5pY29kZSk7XG5cdFNMT1Quc2V0KHRoaXMsICdbW0RvbmVdXScsIGZhbHNlKTtcbn07XG5cbmlmIChJdGVyYXRvclByb3RvdHlwZSkge1xuXHRSZWdFeHBTdHJpbmdJdGVyYXRvci5wcm90b3R5cGUgPSBPcmRpbmFyeU9iamVjdENyZWF0ZShJdGVyYXRvclByb3RvdHlwZSk7XG59XG5cbnZhciBSZWdFeHBTdHJpbmdJdGVyYXRvck5leHQgPSBmdW5jdGlvbiBuZXh0KCkge1xuXHR2YXIgTyA9IHRoaXM7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8taW52YWxpZC10aGlzXG5cdGlmIChUeXBlKE8pICE9PSAnT2JqZWN0Jykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdyZWNlaXZlciBtdXN0IGJlIGFuIG9iamVjdCcpO1xuXHR9XG5cdGlmIChcblx0XHQhKE8gaW5zdGFuY2VvZiBSZWdFeHBTdHJpbmdJdGVyYXRvcilcbiAgICAgICAgfHwgIVNMT1QuaGFzKE8sICdbW0l0ZXJhdGluZ1JlZ0V4cF1dJylcbiAgICAgICAgfHwgIVNMT1QuaGFzKE8sICdbW0l0ZXJhdGVkU3RyaW5nXV0nKVxuICAgICAgICB8fCAhU0xPVC5oYXMoTywgJ1tbR2xvYmFsXV0nKVxuICAgICAgICB8fCAhU0xPVC5oYXMoTywgJ1tbVW5pY29kZV1dJylcbiAgICAgICAgfHwgIVNMT1QuaGFzKE8sICdbW0RvbmVdXScpXG5cdCkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdcInRoaXNcIiB2YWx1ZSBtdXN0IGJlIGEgUmVnRXhwU3RyaW5nSXRlcmF0b3IgaW5zdGFuY2UnKTtcblx0fVxuXHRpZiAoU0xPVC5nZXQoTywgJ1tbRG9uZV1dJykpIHtcblx0XHRyZXR1cm4gQ3JlYXRlSXRlclJlc3VsdE9iamVjdCh1bmRlZmluZWQsIHRydWUpO1xuXHR9XG5cdHZhciBSID0gU0xPVC5nZXQoTywgJ1tbSXRlcmF0aW5nUmVnRXhwXV0nKTtcblx0dmFyIFMgPSBTTE9ULmdldChPLCAnW1tJdGVyYXRlZFN0cmluZ11dJyk7XG5cdHZhciBnbG9iYWwgPSBTTE9ULmdldChPLCAnW1tHbG9iYWxdXScpO1xuXHR2YXIgZnVsbFVuaWNvZGUgPSBTTE9ULmdldChPLCAnW1tVbmljb2RlXV0nKTtcblx0dmFyIG1hdGNoID0gUmVnRXhwRXhlYyhSLCBTKTtcblx0aWYgKG1hdGNoID09PSBudWxsKSB7XG5cdFx0U0xPVC5zZXQoTywgJ1tbRG9uZV1dJywgdHJ1ZSk7XG5cdFx0cmV0dXJuIENyZWF0ZUl0ZXJSZXN1bHRPYmplY3QodW5kZWZpbmVkLCB0cnVlKTtcblx0fVxuXHRpZiAoZ2xvYmFsKSB7XG5cdFx0dmFyIG1hdGNoU3RyID0gVG9TdHJpbmcoR2V0KG1hdGNoLCAnMCcpKTtcblx0XHRpZiAobWF0Y2hTdHIgPT09ICcnKSB7XG5cdFx0XHR2YXIgdGhpc0luZGV4ID0gVG9MZW5ndGgoR2V0KFIsICdsYXN0SW5kZXgnKSk7XG5cdFx0XHR2YXIgbmV4dEluZGV4ID0gQWR2YW5jZVN0cmluZ0luZGV4KFMsIHRoaXNJbmRleCwgZnVsbFVuaWNvZGUpO1xuXHRcdFx0U2V0KFIsICdsYXN0SW5kZXgnLCBuZXh0SW5kZXgsIHRydWUpO1xuXHRcdH1cblx0XHRyZXR1cm4gQ3JlYXRlSXRlclJlc3VsdE9iamVjdChtYXRjaCwgZmFsc2UpO1xuXHR9XG5cdFNMT1Quc2V0KE8sICdbW0RvbmVdXScsIHRydWUpO1xuXHRyZXR1cm4gQ3JlYXRlSXRlclJlc3VsdE9iamVjdChtYXRjaCwgZmFsc2UpO1xufTtcbkNyZWF0ZU1ldGhvZFByb3BlcnR5KFJlZ0V4cFN0cmluZ0l0ZXJhdG9yLnByb3RvdHlwZSwgJ25leHQnLCBSZWdFeHBTdHJpbmdJdGVyYXRvck5leHQpO1xuXG5pZiAoaGFzU3ltYm9scykge1xuXHRpZiAoU3ltYm9sLnRvU3RyaW5nVGFnKSB7XG5cdFx0aWYgKCRkZWZpbmVQcm9wZXJ0eSkge1xuXHRcdFx0JGRlZmluZVByb3BlcnR5KFJlZ0V4cFN0cmluZ0l0ZXJhdG9yLnByb3RvdHlwZSwgU3ltYm9sLnRvU3RyaW5nVGFnLCB7XG5cdFx0XHRcdGNvbmZpZ3VyYWJsZTogdHJ1ZSxcblx0XHRcdFx0ZW51bWVyYWJsZTogZmFsc2UsXG5cdFx0XHRcdHZhbHVlOiAnUmVnRXhwIFN0cmluZyBJdGVyYXRvcicsXG5cdFx0XHRcdHdyaXRhYmxlOiBmYWxzZVxuXHRcdFx0fSk7XG5cdFx0fSBlbHNlIHtcblx0XHRcdFJlZ0V4cFN0cmluZ0l0ZXJhdG9yLnByb3RvdHlwZVtTeW1ib2wudG9TdHJpbmdUYWddID0gJ1JlZ0V4cCBTdHJpbmcgSXRlcmF0b3InO1xuXHRcdH1cblx0fVxuXG5cdGlmIChTeW1ib2wuaXRlcmF0b3IgJiYgdHlwZW9mIFJlZ0V4cFN0cmluZ0l0ZXJhdG9yLnByb3RvdHlwZVtTeW1ib2wuaXRlcmF0b3JdICE9PSAnZnVuY3Rpb24nKSB7XG5cdFx0dmFyIGl0ZXJhdG9yRm4gPSBmdW5jdGlvbiBTeW1ib2xJdGVyYXRvcigpIHtcblx0XHRcdHJldHVybiB0aGlzO1xuXHRcdH07XG5cdFx0Q3JlYXRlTWV0aG9kUHJvcGVydHkoUmVnRXhwU3RyaW5nSXRlcmF0b3IucHJvdG90eXBlLCBTeW1ib2wuaXRlcmF0b3IsIGl0ZXJhdG9yRm4pO1xuXHR9XG59XG5cbi8vIGh0dHBzOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvMTEuMC8jc2VjLWNyZWF0ZXJlZ2V4cHN0cmluZ2l0ZXJhdG9yXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIENyZWF0ZVJlZ0V4cFN0cmluZ0l0ZXJhdG9yKFIsIFMsIGdsb2JhbCwgZnVsbFVuaWNvZGUpIHtcblx0Ly8gYXNzZXJ0IFIuZ2xvYmFsID09PSBnbG9iYWwgJiYgUi51bmljb2RlID09PSBmdWxsVW5pY29kZT9cblx0cmV0dXJuIG5ldyBSZWdFeHBTdHJpbmdJdGVyYXRvcihSLCBTLCBnbG9iYWwsIGZ1bGxVbmljb2RlKTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3937\n")},3950:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar isPropertyDescriptor = __webpack_require__(2435);\nvar DefineOwnProperty = __webpack_require__(3682);\n\nvar FromPropertyDescriptor = __webpack_require__(8334);\nvar IsAccessorDescriptor = __webpack_require__(9527);\nvar IsDataDescriptor = __webpack_require__(3746);\nvar IsPropertyKey = __webpack_require__(4305);\nvar SameValue = __webpack_require__(484);\nvar ToPropertyDescriptor = __webpack_require__(9916);\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-definepropertyorthrow\n\nmodule.exports = function DefinePropertyOrThrow(O, P, desc) {\n\tif (Type(O) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: Type(O) is not Object');\n\t}\n\n\tif (!IsPropertyKey(P)) {\n\t\tthrow new $TypeError('Assertion failed: IsPropertyKey(P) is not true');\n\t}\n\n\tvar Desc = isPropertyDescriptor({\n\t\tType: Type,\n\t\tIsDataDescriptor: IsDataDescriptor,\n\t\tIsAccessorDescriptor: IsAccessorDescriptor\n\t}, desc) ? desc : ToPropertyDescriptor(desc);\n\tif (!isPropertyDescriptor({\n\t\tType: Type,\n\t\tIsDataDescriptor: IsDataDescriptor,\n\t\tIsAccessorDescriptor: IsAccessorDescriptor\n\t}, Desc)) {\n\t\tthrow new $TypeError('Assertion failed: Desc is not a valid Property Descriptor');\n\t}\n\n\treturn DefineOwnProperty(\n\t\tIsDataDescriptor,\n\t\tSameValue,\n\t\tFromPropertyDescriptor,\n\t\tO,\n\t\tP,\n\t\tDesc\n\t);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzk1MC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSwyQkFBMkIsbUJBQU8sQ0FBQyxJQUFpQztBQUNwRSx3QkFBd0IsbUJBQU8sQ0FBQyxJQUE4Qjs7QUFFOUQsNkJBQTZCLG1CQUFPLENBQUMsSUFBMEI7QUFDL0QsMkJBQTJCLG1CQUFPLENBQUMsSUFBd0I7QUFDM0QsdUJBQXVCLG1CQUFPLENBQUMsSUFBb0I7QUFDbkQsb0JBQW9CLG1CQUFPLENBQUMsSUFBaUI7QUFDN0MsZ0JBQWdCLG1CQUFPLENBQUMsR0FBYTtBQUNyQywyQkFBMkIsbUJBQU8sQ0FBQyxJQUF3QjtBQUMzRCxXQUFXLG1CQUFPLENBQUMsSUFBUTs7QUFFM0I7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsRUFBRTtBQUNGO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsRUFBRTtBQUNGO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL0RlZmluZVByb3BlcnR5T3JUaHJvdy5qcz8wYTEwIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG5cbnZhciBpc1Byb3BlcnR5RGVzY3JpcHRvciA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvaXNQcm9wZXJ0eURlc2NyaXB0b3InKTtcbnZhciBEZWZpbmVPd25Qcm9wZXJ0eSA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvRGVmaW5lT3duUHJvcGVydHknKTtcblxudmFyIEZyb21Qcm9wZXJ0eURlc2NyaXB0b3IgPSByZXF1aXJlKCcuL0Zyb21Qcm9wZXJ0eURlc2NyaXB0b3InKTtcbnZhciBJc0FjY2Vzc29yRGVzY3JpcHRvciA9IHJlcXVpcmUoJy4vSXNBY2Nlc3NvckRlc2NyaXB0b3InKTtcbnZhciBJc0RhdGFEZXNjcmlwdG9yID0gcmVxdWlyZSgnLi9Jc0RhdGFEZXNjcmlwdG9yJyk7XG52YXIgSXNQcm9wZXJ0eUtleSA9IHJlcXVpcmUoJy4vSXNQcm9wZXJ0eUtleScpO1xudmFyIFNhbWVWYWx1ZSA9IHJlcXVpcmUoJy4vU2FtZVZhbHVlJyk7XG52YXIgVG9Qcm9wZXJ0eURlc2NyaXB0b3IgPSByZXF1aXJlKCcuL1RvUHJvcGVydHlEZXNjcmlwdG9yJyk7XG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtZGVmaW5lcHJvcGVydHlvcnRocm93XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gRGVmaW5lUHJvcGVydHlPclRocm93KE8sIFAsIGRlc2MpIHtcblx0aWYgKFR5cGUoTykgIT09ICdPYmplY3QnKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IFR5cGUoTykgaXMgbm90IE9iamVjdCcpO1xuXHR9XG5cblx0aWYgKCFJc1Byb3BlcnR5S2V5KFApKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IElzUHJvcGVydHlLZXkoUCkgaXMgbm90IHRydWUnKTtcblx0fVxuXG5cdHZhciBEZXNjID0gaXNQcm9wZXJ0eURlc2NyaXB0b3Ioe1xuXHRcdFR5cGU6IFR5cGUsXG5cdFx0SXNEYXRhRGVzY3JpcHRvcjogSXNEYXRhRGVzY3JpcHRvcixcblx0XHRJc0FjY2Vzc29yRGVzY3JpcHRvcjogSXNBY2Nlc3NvckRlc2NyaXB0b3Jcblx0fSwgZGVzYykgPyBkZXNjIDogVG9Qcm9wZXJ0eURlc2NyaXB0b3IoZGVzYyk7XG5cdGlmICghaXNQcm9wZXJ0eURlc2NyaXB0b3Ioe1xuXHRcdFR5cGU6IFR5cGUsXG5cdFx0SXNEYXRhRGVzY3JpcHRvcjogSXNEYXRhRGVzY3JpcHRvcixcblx0XHRJc0FjY2Vzc29yRGVzY3JpcHRvcjogSXNBY2Nlc3NvckRlc2NyaXB0b3Jcblx0fSwgRGVzYykpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogRGVzYyBpcyBub3QgYSB2YWxpZCBQcm9wZXJ0eSBEZXNjcmlwdG9yJyk7XG5cdH1cblxuXHRyZXR1cm4gRGVmaW5lT3duUHJvcGVydHkoXG5cdFx0SXNEYXRhRGVzY3JpcHRvcixcblx0XHRTYW1lVmFsdWUsXG5cdFx0RnJvbVByb3BlcnR5RGVzY3JpcHRvcixcblx0XHRPLFxuXHRcdFAsXG5cdFx0RGVzY1xuXHQpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///3950\n")},8334:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar assertRecord = __webpack_require__(2188);\n\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-frompropertydescriptor\n\nmodule.exports = function FromPropertyDescriptor(Desc) {\n\tif (typeof Desc === 'undefined') {\n\t\treturn Desc;\n\t}\n\n\tassertRecord(Type, 'Property Descriptor', 'Desc', Desc);\n\n\tvar obj = {};\n\tif ('[[Value]]' in Desc) {\n\t\tobj.value = Desc['[[Value]]'];\n\t}\n\tif ('[[Writable]]' in Desc) {\n\t\tobj.writable = Desc['[[Writable]]'];\n\t}\n\tif ('[[Get]]' in Desc) {\n\t\tobj.get = Desc['[[Get]]'];\n\t}\n\tif ('[[Set]]' in Desc) {\n\t\tobj.set = Desc['[[Set]]'];\n\t}\n\tif ('[[Enumerable]]' in Desc) {\n\t\tobj.enumerable = Desc['[[Enumerable]]'];\n\t}\n\tif ('[[Configurable]]' in Desc) {\n\t\tobj.configurable = Desc['[[Configurable]]'];\n\t}\n\treturn obj;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODMzNC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxJQUF5Qjs7QUFFcEQsV0FBVyxtQkFBTyxDQUFDLElBQVE7O0FBRTNCOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Gcm9tUHJvcGVydHlEZXNjcmlwdG9yLmpzPzFkZDciXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgYXNzZXJ0UmVjb3JkID0gcmVxdWlyZSgnLi4vaGVscGVycy9hc3NlcnRSZWNvcmQnKTtcblxudmFyIFR5cGUgPSByZXF1aXJlKCcuL1R5cGUnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWZyb21wcm9wZXJ0eWRlc2NyaXB0b3JcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBGcm9tUHJvcGVydHlEZXNjcmlwdG9yKERlc2MpIHtcblx0aWYgKHR5cGVvZiBEZXNjID09PSAndW5kZWZpbmVkJykge1xuXHRcdHJldHVybiBEZXNjO1xuXHR9XG5cblx0YXNzZXJ0UmVjb3JkKFR5cGUsICdQcm9wZXJ0eSBEZXNjcmlwdG9yJywgJ0Rlc2MnLCBEZXNjKTtcblxuXHR2YXIgb2JqID0ge307XG5cdGlmICgnW1tWYWx1ZV1dJyBpbiBEZXNjKSB7XG5cdFx0b2JqLnZhbHVlID0gRGVzY1snW1tWYWx1ZV1dJ107XG5cdH1cblx0aWYgKCdbW1dyaXRhYmxlXV0nIGluIERlc2MpIHtcblx0XHRvYmoud3JpdGFibGUgPSBEZXNjWydbW1dyaXRhYmxlXV0nXTtcblx0fVxuXHRpZiAoJ1tbR2V0XV0nIGluIERlc2MpIHtcblx0XHRvYmouZ2V0ID0gRGVzY1snW1tHZXRdXSddO1xuXHR9XG5cdGlmICgnW1tTZXRdXScgaW4gRGVzYykge1xuXHRcdG9iai5zZXQgPSBEZXNjWydbW1NldF1dJ107XG5cdH1cblx0aWYgKCdbW0VudW1lcmFibGVdXScgaW4gRGVzYykge1xuXHRcdG9iai5lbnVtZXJhYmxlID0gRGVzY1snW1tFbnVtZXJhYmxlXV0nXTtcblx0fVxuXHRpZiAoJ1tbQ29uZmlndXJhYmxlXV0nIGluIERlc2MpIHtcblx0XHRvYmouY29uZmlndXJhYmxlID0gRGVzY1snW1tDb25maWd1cmFibGVdXSddO1xuXHR9XG5cdHJldHVybiBvYmo7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///8334\n")},1391:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar inspect = __webpack_require__(631);\n\nvar IsPropertyKey = __webpack_require__(4305);\nvar Type = __webpack_require__(3633);\n\n/**\n * 7.3.1 Get (O, P) - https://ecma-international.org/ecma-262/6.0/#sec-get-o-p\n * 1. Assert: Type(O) is Object.\n * 2. Assert: IsPropertyKey(P) is true.\n * 3. Return O.[[Get]](P, O).\n */\n\nmodule.exports = function Get(O, P) {\n\t// 7.3.1.1\n\tif (Type(O) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: Type(O) is not Object');\n\t}\n\t// 7.3.1.2\n\tif (!IsPropertyKey(P)) {\n\t\tthrow new $TypeError('Assertion failed: IsPropertyKey(P) is not true, got ' + inspect(P));\n\t}\n\t// 7.3.1.3\n\treturn O[P];\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTM5MS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSxjQUFjLG1CQUFPLENBQUMsR0FBZ0I7O0FBRXRDLG9CQUFvQixtQkFBTyxDQUFDLElBQWlCO0FBQzdDLFdBQVcsbUJBQU8sQ0FBQyxJQUFROztBQUUzQjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL0dldC5qcz9hODc1Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG5cbnZhciBpbnNwZWN0ID0gcmVxdWlyZSgnb2JqZWN0LWluc3BlY3QnKTtcblxudmFyIElzUHJvcGVydHlLZXkgPSByZXF1aXJlKCcuL0lzUHJvcGVydHlLZXknKTtcbnZhciBUeXBlID0gcmVxdWlyZSgnLi9UeXBlJyk7XG5cbi8qKlxuICogNy4zLjEgR2V0IChPLCBQKSAtIGh0dHBzOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi82LjAvI3NlYy1nZXQtby1wXG4gKiAxLiBBc3NlcnQ6IFR5cGUoTykgaXMgT2JqZWN0LlxuICogMi4gQXNzZXJ0OiBJc1Byb3BlcnR5S2V5KFApIGlzIHRydWUuXG4gKiAzLiBSZXR1cm4gTy5bW0dldF1dKFAsIE8pLlxuICovXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gR2V0KE8sIFApIHtcblx0Ly8gNy4zLjEuMVxuXHRpZiAoVHlwZShPKSAhPT0gJ09iamVjdCcpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogVHlwZShPKSBpcyBub3QgT2JqZWN0Jyk7XG5cdH1cblx0Ly8gNy4zLjEuMlxuXHRpZiAoIUlzUHJvcGVydHlLZXkoUCkpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogSXNQcm9wZXJ0eUtleShQKSBpcyBub3QgdHJ1ZSwgZ290ICcgKyBpbnNwZWN0KFApKTtcblx0fVxuXHQvLyA3LjMuMS4zXG5cdHJldHVybiBPW1BdO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///1391\n")},7364:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar GetV = __webpack_require__(8509);\nvar IsCallable = __webpack_require__(1787);\nvar IsPropertyKey = __webpack_require__(4305);\n\n/**\n * 7.3.9 - https://ecma-international.org/ecma-262/6.0/#sec-getmethod\n * 1. Assert: IsPropertyKey(P) is true.\n * 2. Let func be GetV(O, P).\n * 3. ReturnIfAbrupt(func).\n * 4. If func is either undefined or null, return undefined.\n * 5. If IsCallable(func) is false, throw a TypeError exception.\n * 6. Return func.\n */\n\nmodule.exports = function GetMethod(O, P) {\n\t// 7.3.9.1\n\tif (!IsPropertyKey(P)) {\n\t\tthrow new $TypeError('Assertion failed: IsPropertyKey(P) is not true');\n\t}\n\n\t// 7.3.9.2\n\tvar func = GetV(O, P);\n\n\t// 7.3.9.4\n\tif (func == null) {\n\t\treturn void 0;\n\t}\n\n\t// 7.3.9.5\n\tif (!IsCallable(func)) {\n\t\tthrow new $TypeError(P + 'is not a function');\n\t}\n\n\t// 7.3.9.6\n\treturn func;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzM2NC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSxXQUFXLG1CQUFPLENBQUMsSUFBUTtBQUMzQixpQkFBaUIsbUJBQU8sQ0FBQyxJQUFjO0FBQ3ZDLG9CQUFvQixtQkFBTyxDQUFDLElBQWlCOztBQUU3QztBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9HZXRNZXRob2QuanM/NmZiMyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xuXG52YXIgR2V0ViA9IHJlcXVpcmUoJy4vR2V0VicpO1xudmFyIElzQ2FsbGFibGUgPSByZXF1aXJlKCcuL0lzQ2FsbGFibGUnKTtcbnZhciBJc1Byb3BlcnR5S2V5ID0gcmVxdWlyZSgnLi9Jc1Byb3BlcnR5S2V5Jyk7XG5cbi8qKlxuICogNy4zLjkgLSBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtZ2V0bWV0aG9kXG4gKiAxLiBBc3NlcnQ6IElzUHJvcGVydHlLZXkoUCkgaXMgdHJ1ZS5cbiAqIDIuIExldCBmdW5jIGJlIEdldFYoTywgUCkuXG4gKiAzLiBSZXR1cm5JZkFicnVwdChmdW5jKS5cbiAqIDQuIElmIGZ1bmMgaXMgZWl0aGVyIHVuZGVmaW5lZCBvciBudWxsLCByZXR1cm4gdW5kZWZpbmVkLlxuICogNS4gSWYgSXNDYWxsYWJsZShmdW5jKSBpcyBmYWxzZSwgdGhyb3cgYSBUeXBlRXJyb3IgZXhjZXB0aW9uLlxuICogNi4gUmV0dXJuIGZ1bmMuXG4gKi9cblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBHZXRNZXRob2QoTywgUCkge1xuXHQvLyA3LjMuOS4xXG5cdGlmICghSXNQcm9wZXJ0eUtleShQKSkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBJc1Byb3BlcnR5S2V5KFApIGlzIG5vdCB0cnVlJyk7XG5cdH1cblxuXHQvLyA3LjMuOS4yXG5cdHZhciBmdW5jID0gR2V0VihPLCBQKTtcblxuXHQvLyA3LjMuOS40XG5cdGlmIChmdW5jID09IG51bGwpIHtcblx0XHRyZXR1cm4gdm9pZCAwO1xuXHR9XG5cblx0Ly8gNy4zLjkuNVxuXHRpZiAoIUlzQ2FsbGFibGUoZnVuYykpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcihQICsgJ2lzIG5vdCBhIGZ1bmN0aW9uJyk7XG5cdH1cblxuXHQvLyA3LjMuOS42XG5cdHJldHVybiBmdW5jO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///7364\n")},8509:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar IsPropertyKey = __webpack_require__(4305);\nvar ToObject = __webpack_require__(821);\n\n/**\n * 7.3.2 GetV (V, P)\n * 1. Assert: IsPropertyKey(P) is true.\n * 2. Let O be ToObject(V).\n * 3. ReturnIfAbrupt(O).\n * 4. Return O.[[Get]](P, V).\n */\n\nmodule.exports = function GetV(V, P) {\n\t// 7.3.2.1\n\tif (!IsPropertyKey(P)) {\n\t\tthrow new $TypeError('Assertion failed: IsPropertyKey(P) is not true');\n\t}\n\n\t// 7.3.2.2-3\n\tvar O = ToObject(V);\n\n\t// 7.3.2.4\n\treturn O[P];\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODUwOS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSxvQkFBb0IsbUJBQU8sQ0FBQyxJQUFpQjtBQUM3QyxlQUFlLG1CQUFPLENBQUMsR0FBWTs7QUFFbkM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9HZXRWLmpzPzcwYjIiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJFR5cGVFcnJvciA9IEdldEludHJpbnNpYygnJVR5cGVFcnJvciUnKTtcblxudmFyIElzUHJvcGVydHlLZXkgPSByZXF1aXJlKCcuL0lzUHJvcGVydHlLZXknKTtcbnZhciBUb09iamVjdCA9IHJlcXVpcmUoJy4vVG9PYmplY3QnKTtcblxuLyoqXG4gKiA3LjMuMiBHZXRWIChWLCBQKVxuICogMS4gQXNzZXJ0OiBJc1Byb3BlcnR5S2V5KFApIGlzIHRydWUuXG4gKiAyLiBMZXQgTyBiZSBUb09iamVjdChWKS5cbiAqIDMuIFJldHVybklmQWJydXB0KE8pLlxuICogNC4gUmV0dXJuIE8uW1tHZXRdXShQLCBWKS5cbiAqL1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIEdldFYoViwgUCkge1xuXHQvLyA3LjMuMi4xXG5cdGlmICghSXNQcm9wZXJ0eUtleShQKSkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBJc1Byb3BlcnR5S2V5KFApIGlzIG5vdCB0cnVlJyk7XG5cdH1cblxuXHQvLyA3LjMuMi4yLTNcblx0dmFyIE8gPSBUb09iamVjdChWKTtcblxuXHQvLyA3LjMuMi40XG5cdHJldHVybiBPW1BdO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///8509\n")},9527:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar has = __webpack_require__(7642);\n\nvar assertRecord = __webpack_require__(2188);\n\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-isaccessordescriptor\n\nmodule.exports = function IsAccessorDescriptor(Desc) {\n\tif (typeof Desc === 'undefined') {\n\t\treturn false;\n\t}\n\n\tassertRecord(Type, 'Property Descriptor', 'Desc', Desc);\n\n\tif (!has(Desc, '[[Get]]') && !has(Desc, '[[Set]]')) {\n\t\treturn false;\n\t}\n\n\treturn true;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTUyNy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixVQUFVLG1CQUFPLENBQUMsSUFBSzs7QUFFdkIsbUJBQW1CLG1CQUFPLENBQUMsSUFBeUI7O0FBRXBELFdBQVcsbUJBQU8sQ0FBQyxJQUFROztBQUUzQjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Jc0FjY2Vzc29yRGVzY3JpcHRvci5qcz84YWI1Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGhhcyA9IHJlcXVpcmUoJ2hhcycpO1xuXG52YXIgYXNzZXJ0UmVjb3JkID0gcmVxdWlyZSgnLi4vaGVscGVycy9hc3NlcnRSZWNvcmQnKTtcblxudmFyIFR5cGUgPSByZXF1aXJlKCcuL1R5cGUnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWlzYWNjZXNzb3JkZXNjcmlwdG9yXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gSXNBY2Nlc3NvckRlc2NyaXB0b3IoRGVzYykge1xuXHRpZiAodHlwZW9mIERlc2MgPT09ICd1bmRlZmluZWQnKSB7XG5cdFx0cmV0dXJuIGZhbHNlO1xuXHR9XG5cblx0YXNzZXJ0UmVjb3JkKFR5cGUsICdQcm9wZXJ0eSBEZXNjcmlwdG9yJywgJ0Rlc2MnLCBEZXNjKTtcblxuXHRpZiAoIWhhcyhEZXNjLCAnW1tHZXRdXScpICYmICFoYXMoRGVzYywgJ1tbU2V0XV0nKSkge1xuXHRcdHJldHVybiBmYWxzZTtcblx0fVxuXG5cdHJldHVybiB0cnVlO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///9527\n")},6975:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $Array = GetIntrinsic('%Array%');\n\n// eslint-disable-next-line global-require\nvar toStr = !$Array.isArray && __webpack_require__(1924)('Object.prototype.toString');\n\n// https://ecma-international.org/ecma-262/6.0/#sec-isarray\n\nmodule.exports = $Array.isArray || function IsArray(argument) {\n\treturn toStr(argument) === '[object Array]';\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNjk3NS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQTtBQUNBLCtCQUErQixtQkFBTyxDQUFDLElBQXFCOztBQUU1RDs7QUFFQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvSXNBcnJheS5qcz9jMTI1Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRBcnJheSA9IEdldEludHJpbnNpYygnJUFycmF5JScpO1xuXG4vLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgZ2xvYmFsLXJlcXVpcmVcbnZhciB0b1N0ciA9ICEkQXJyYXkuaXNBcnJheSAmJiByZXF1aXJlKCdjYWxsLWJpbmQvY2FsbEJvdW5kJykoJ09iamVjdC5wcm90b3R5cGUudG9TdHJpbmcnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWlzYXJyYXlcblxubW9kdWxlLmV4cG9ydHMgPSAkQXJyYXkuaXNBcnJheSB8fCBmdW5jdGlvbiBJc0FycmF5KGFyZ3VtZW50KSB7XG5cdHJldHVybiB0b1N0cihhcmd1bWVudCkgPT09ICdbb2JqZWN0IEFycmF5XSc7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///6975\n")},1787:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\n// http://262.ecma-international.org/5.1/#sec-9.11\n\nmodule.exports = __webpack_require__(5320);\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTc4Ny5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQSwwQ0FBdUMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvSXNDYWxsYWJsZS5qcz81NTA4Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxuLy8gaHR0cDovLzI2Mi5lY21hLWludGVybmF0aW9uYWwub3JnLzUuMS8jc2VjLTkuMTFcblxubW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKCdpcy1jYWxsYWJsZScpO1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///1787\n")},1974:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(4445);\n\nvar $construct = GetIntrinsic('%Reflect.construct%', true);\n\nvar DefinePropertyOrThrow = __webpack_require__(3950);\ntry {\n\tDefinePropertyOrThrow({}, '', { '[[Get]]': function () {} });\n} catch (e) {\n\t// Accessor properties aren't supported\n\tDefinePropertyOrThrow = null;\n}\n\n// https://ecma-international.org/ecma-262/6.0/#sec-isconstructor\n\nif (DefinePropertyOrThrow && $construct) {\n\tvar isConstructorMarker = {};\n\tvar badArrayLike = {};\n\tDefinePropertyOrThrow(badArrayLike, 'length', {\n\t\t'[[Get]]': function () {\n\t\t\tthrow isConstructorMarker;\n\t\t},\n\t\t'[[Enumerable]]': true\n\t});\n\n\tmodule.exports = function IsConstructor(argument) {\n\t\ttry {\n\t\t\t// `Reflect.construct` invokes `IsConstructor(target)` before `Get(args, 'length')`:\n\t\t\t$construct(argument, badArrayLike);\n\t\t} catch (err) {\n\t\t\treturn err === isConstructorMarker;\n\t\t}\n\t};\n} else {\n\tmodule.exports = function IsConstructor(argument) {\n\t\t// unfortunately there's no way to truly check this without try/catch `new argument` in old environments\n\t\treturn typeof argument === 'function' && !!argument.prototype;\n\t};\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTk3NC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxJQUFvQjs7QUFFL0M7O0FBRUEsNEJBQTRCLG1CQUFPLENBQUMsSUFBeUI7QUFDN0Q7QUFDQSx5QkFBeUIsUUFBUSwyQkFBMkI7QUFDNUQsRUFBRTtBQUNGO0FBQ0E7QUFDQTs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0g7QUFDQSxFQUFFOztBQUVGO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBSTtBQUNKO0FBQ0E7QUFDQTtBQUNBLEVBQUU7QUFDRjtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL0lzQ29uc3RydWN0b3IuanM/ZTc0NyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCcuLi9HZXRJbnRyaW5zaWMuanMnKTtcblxudmFyICRjb25zdHJ1Y3QgPSBHZXRJbnRyaW5zaWMoJyVSZWZsZWN0LmNvbnN0cnVjdCUnLCB0cnVlKTtcblxudmFyIERlZmluZVByb3BlcnR5T3JUaHJvdyA9IHJlcXVpcmUoJy4vRGVmaW5lUHJvcGVydHlPclRocm93Jyk7XG50cnkge1xuXHREZWZpbmVQcm9wZXJ0eU9yVGhyb3coe30sICcnLCB7ICdbW0dldF1dJzogZnVuY3Rpb24gKCkge30gfSk7XG59IGNhdGNoIChlKSB7XG5cdC8vIEFjY2Vzc29yIHByb3BlcnRpZXMgYXJlbid0IHN1cHBvcnRlZFxuXHREZWZpbmVQcm9wZXJ0eU9yVGhyb3cgPSBudWxsO1xufVxuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtaXNjb25zdHJ1Y3RvclxuXG5pZiAoRGVmaW5lUHJvcGVydHlPclRocm93ICYmICRjb25zdHJ1Y3QpIHtcblx0dmFyIGlzQ29uc3RydWN0b3JNYXJrZXIgPSB7fTtcblx0dmFyIGJhZEFycmF5TGlrZSA9IHt9O1xuXHREZWZpbmVQcm9wZXJ0eU9yVGhyb3coYmFkQXJyYXlMaWtlLCAnbGVuZ3RoJywge1xuXHRcdCdbW0dldF1dJzogZnVuY3Rpb24gKCkge1xuXHRcdFx0dGhyb3cgaXNDb25zdHJ1Y3Rvck1hcmtlcjtcblx0XHR9LFxuXHRcdCdbW0VudW1lcmFibGVdXSc6IHRydWVcblx0fSk7XG5cblx0bW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBJc0NvbnN0cnVjdG9yKGFyZ3VtZW50KSB7XG5cdFx0dHJ5IHtcblx0XHRcdC8vIGBSZWZsZWN0LmNvbnN0cnVjdGAgaW52b2tlcyBgSXNDb25zdHJ1Y3Rvcih0YXJnZXQpYCBiZWZvcmUgYEdldChhcmdzLCAnbGVuZ3RoJylgOlxuXHRcdFx0JGNvbnN0cnVjdChhcmd1bWVudCwgYmFkQXJyYXlMaWtlKTtcblx0XHR9IGNhdGNoIChlcnIpIHtcblx0XHRcdHJldHVybiBlcnIgPT09IGlzQ29uc3RydWN0b3JNYXJrZXI7XG5cdFx0fVxuXHR9O1xufSBlbHNlIHtcblx0bW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBJc0NvbnN0cnVjdG9yKGFyZ3VtZW50KSB7XG5cdFx0Ly8gdW5mb3J0dW5hdGVseSB0aGVyZSdzIG5vIHdheSB0byB0cnVseSBjaGVjayB0aGlzIHdpdGhvdXQgdHJ5L2NhdGNoIGBuZXcgYXJndW1lbnRgIGluIG9sZCBlbnZpcm9ubWVudHNcblx0XHRyZXR1cm4gdHlwZW9mIGFyZ3VtZW50ID09PSAnZnVuY3Rpb24nICYmICEhYXJndW1lbnQucHJvdG90eXBlO1xuXHR9O1xufVxuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///1974\n")},3746:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar has = __webpack_require__(7642);\n\nvar assertRecord = __webpack_require__(2188);\n\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-isdatadescriptor\n\nmodule.exports = function IsDataDescriptor(Desc) {\n\tif (typeof Desc === 'undefined') {\n\t\treturn false;\n\t}\n\n\tassertRecord(Type, 'Property Descriptor', 'Desc', Desc);\n\n\tif (!has(Desc, '[[Value]]') && !has(Desc, '[[Writable]]')) {\n\t\treturn false;\n\t}\n\n\treturn true;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzc0Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixVQUFVLG1CQUFPLENBQUMsSUFBSzs7QUFFdkIsbUJBQW1CLG1CQUFPLENBQUMsSUFBeUI7O0FBRXBELFdBQVcsbUJBQU8sQ0FBQyxJQUFROztBQUUzQjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Jc0RhdGFEZXNjcmlwdG9yLmpzP2EwYmEiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgaGFzID0gcmVxdWlyZSgnaGFzJyk7XG5cbnZhciBhc3NlcnRSZWNvcmQgPSByZXF1aXJlKCcuLi9oZWxwZXJzL2Fzc2VydFJlY29yZCcpO1xuXG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtaXNkYXRhZGVzY3JpcHRvclxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIElzRGF0YURlc2NyaXB0b3IoRGVzYykge1xuXHRpZiAodHlwZW9mIERlc2MgPT09ICd1bmRlZmluZWQnKSB7XG5cdFx0cmV0dXJuIGZhbHNlO1xuXHR9XG5cblx0YXNzZXJ0UmVjb3JkKFR5cGUsICdQcm9wZXJ0eSBEZXNjcmlwdG9yJywgJ0Rlc2MnLCBEZXNjKTtcblxuXHRpZiAoIWhhcyhEZXNjLCAnW1tWYWx1ZV1dJykgJiYgIWhhcyhEZXNjLCAnW1tXcml0YWJsZV1dJykpIHtcblx0XHRyZXR1cm4gZmFsc2U7XG5cdH1cblxuXHRyZXR1cm4gdHJ1ZTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3746\n")},7312:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar abs = __webpack_require__(4908);\nvar floor = __webpack_require__(375);\nvar Type = __webpack_require__(3633);\n\nvar $isNaN = __webpack_require__(9086);\nvar $isFinite = __webpack_require__(2633);\n\n// https://tc39.es/ecma262/#sec-isintegralnumber\n\nmodule.exports = function IsIntegralNumber(argument) {\n\tif (Type(argument) !== 'Number' || $isNaN(argument) || !$isFinite(argument)) {\n\t\treturn false;\n\t}\n\tvar absValue = abs(argument);\n\treturn floor(absValue) === absValue;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzMxMi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixVQUFVLG1CQUFPLENBQUMsSUFBTztBQUN6QixZQUFZLG1CQUFPLENBQUMsR0FBUztBQUM3QixXQUFXLG1CQUFPLENBQUMsSUFBUTs7QUFFM0IsYUFBYSxtQkFBTyxDQUFDLElBQWtCO0FBQ3ZDLGdCQUFnQixtQkFBTyxDQUFDLElBQXFCOztBQUU3Qzs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Jc0ludGVncmFsTnVtYmVyLmpzPzZhYjEiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgYWJzID0gcmVxdWlyZSgnLi9hYnMnKTtcbnZhciBmbG9vciA9IHJlcXVpcmUoJy4vZmxvb3InKTtcbnZhciBUeXBlID0gcmVxdWlyZSgnLi9UeXBlJyk7XG5cbnZhciAkaXNOYU4gPSByZXF1aXJlKCcuLi9oZWxwZXJzL2lzTmFOJyk7XG52YXIgJGlzRmluaXRlID0gcmVxdWlyZSgnLi4vaGVscGVycy9pc0Zpbml0ZScpO1xuXG4vLyBodHRwczovL3RjMzkuZXMvZWNtYTI2Mi8jc2VjLWlzaW50ZWdyYWxudW1iZXJcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBJc0ludGVncmFsTnVtYmVyKGFyZ3VtZW50KSB7XG5cdGlmIChUeXBlKGFyZ3VtZW50KSAhPT0gJ051bWJlcicgfHwgJGlzTmFOKGFyZ3VtZW50KSB8fCAhJGlzRmluaXRlKGFyZ3VtZW50KSkge1xuXHRcdHJldHVybiBmYWxzZTtcblx0fVxuXHR2YXIgYWJzVmFsdWUgPSBhYnMoYXJndW1lbnQpO1xuXHRyZXR1cm4gZmxvb3IoYWJzVmFsdWUpID09PSBhYnNWYWx1ZTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///7312\n")},4305:function(module){"use strict";eval("\n\n// https://ecma-international.org/ecma-262/6.0/#sec-ispropertykey\n\nmodule.exports = function IsPropertyKey(argument) {\n\treturn typeof argument === 'string' || typeof argument === 'symbol';\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDMwNS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvSXNQcm9wZXJ0eUtleS5qcz9lMzE3Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWlzcHJvcGVydHlrZXlcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBJc1Byb3BlcnR5S2V5KGFyZ3VtZW50KSB7XG5cdHJldHVybiB0eXBlb2YgYXJndW1lbnQgPT09ICdzdHJpbmcnIHx8IHR5cGVvZiBhcmd1bWVudCA9PT0gJ3N5bWJvbCc7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///4305\n")},840:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $match = GetIntrinsic('%Symbol.match%', true);\n\nvar hasRegExpMatcher = __webpack_require__(8420);\n\nvar ToBoolean = __webpack_require__(9731);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-isregexp\n\nmodule.exports = function IsRegExp(argument) {\n\tif (!argument || typeof argument !== 'object') {\n\t\treturn false;\n\t}\n\tif ($match) {\n\t\tvar isRegExp = argument[$match];\n\t\tif (typeof isRegExp !== 'undefined') {\n\t\t\treturn ToBoolean(isRegExp);\n\t\t}\n\t}\n\treturn hasRegExpMatcher(argument);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODQwLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWU7O0FBRTFDOztBQUVBLHVCQUF1QixtQkFBTyxDQUFDLElBQVU7O0FBRXpDLGdCQUFnQixtQkFBTyxDQUFDLElBQWE7O0FBRXJDOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Jc1JlZ0V4cC5qcz8xMjA2Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRtYXRjaCA9IEdldEludHJpbnNpYygnJVN5bWJvbC5tYXRjaCUnLCB0cnVlKTtcblxudmFyIGhhc1JlZ0V4cE1hdGNoZXIgPSByZXF1aXJlKCdpcy1yZWdleCcpO1xuXG52YXIgVG9Cb29sZWFuID0gcmVxdWlyZSgnLi9Ub0Jvb2xlYW4nKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLWlzcmVnZXhwXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gSXNSZWdFeHAoYXJndW1lbnQpIHtcblx0aWYgKCFhcmd1bWVudCB8fCB0eXBlb2YgYXJndW1lbnQgIT09ICdvYmplY3QnKSB7XG5cdFx0cmV0dXJuIGZhbHNlO1xuXHR9XG5cdGlmICgkbWF0Y2gpIHtcblx0XHR2YXIgaXNSZWdFeHAgPSBhcmd1bWVudFskbWF0Y2hdO1xuXHRcdGlmICh0eXBlb2YgaXNSZWdFeHAgIT09ICd1bmRlZmluZWQnKSB7XG5cdFx0XHRyZXR1cm4gVG9Cb29sZWFuKGlzUmVnRXhwKTtcblx0XHR9XG5cdH1cblx0cmV0dXJuIGhhc1JlZ0V4cE1hdGNoZXIoYXJndW1lbnQpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///840\n")},953:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $ObjectCreate = GetIntrinsic('%Object.create%', true);\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar $SyntaxError = GetIntrinsic('%SyntaxError%');\n\nvar IsArray = __webpack_require__(6975);\nvar Type = __webpack_require__(3633);\n\nvar hasProto = !({ __proto__: null } instanceof Object);\n\n// https://262.ecma-international.org/6.0/#sec-objectcreate\n\nmodule.exports = function OrdinaryObjectCreate(proto) {\n\tif (proto !== null && Type(proto) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: `proto` must be null or an object');\n\t}\n\tvar additionalInternalSlotsList = arguments.length < 2 ? [] : arguments[1];\n\tif (!IsArray(additionalInternalSlotsList)) {\n\t\tthrow new $TypeError('Assertion failed: `additionalInternalSlotsList` must be an Array');\n\t}\n\t// var internalSlotsList = ['[[Prototype]]', '[[Extensible]]'];\n\tif (additionalInternalSlotsList.length > 0) {\n\t\tthrow new $SyntaxError('es-abstract does not yet support internal slots');\n\t\t// internalSlotsList.push(...additionalInternalSlotsList);\n\t}\n\t// var O = MakeBasicObject(internalSlotsList);\n\t// setProto(O, proto);\n\t// return O;\n\n\tif ($ObjectCreate) {\n\t\treturn $ObjectCreate(proto);\n\t}\n\tif (hasProto) {\n\t\treturn { __proto__: proto };\n\t}\n\n\tif (proto === null) {\n\t\tthrow new $SyntaxError('native Object.create support is required to create null objects');\n\t}\n\tvar T = function T() {};\n\tT.prototype = proto;\n\treturn new T();\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTUzLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWU7O0FBRTFDO0FBQ0E7QUFDQTs7QUFFQSxjQUFjLG1CQUFPLENBQUMsSUFBVztBQUNqQyxXQUFXLG1CQUFPLENBQUMsSUFBUTs7QUFFM0IsbUJBQW1CLGtCQUFrQjs7QUFFckM7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQSxXQUFXO0FBQ1g7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvT3JkaW5hcnlPYmplY3RDcmVhdGUuanM/NzRmZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkT2JqZWN0Q3JlYXRlID0gR2V0SW50cmluc2ljKCclT2JqZWN0LmNyZWF0ZSUnLCB0cnVlKTtcbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xudmFyICRTeW50YXhFcnJvciA9IEdldEludHJpbnNpYygnJVN5bnRheEVycm9yJScpO1xuXG52YXIgSXNBcnJheSA9IHJlcXVpcmUoJy4vSXNBcnJheScpO1xudmFyIFR5cGUgPSByZXF1aXJlKCcuL1R5cGUnKTtcblxudmFyIGhhc1Byb3RvID0gISh7IF9fcHJvdG9fXzogbnVsbCB9IGluc3RhbmNlb2YgT2JqZWN0KTtcblxuLy8gaHR0cHM6Ly8yNjIuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy82LjAvI3NlYy1vYmplY3RjcmVhdGVcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBPcmRpbmFyeU9iamVjdENyZWF0ZShwcm90bykge1xuXHRpZiAocHJvdG8gIT09IG51bGwgJiYgVHlwZShwcm90bykgIT09ICdPYmplY3QnKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IGBwcm90b2AgbXVzdCBiZSBudWxsIG9yIGFuIG9iamVjdCcpO1xuXHR9XG5cdHZhciBhZGRpdGlvbmFsSW50ZXJuYWxTbG90c0xpc3QgPSBhcmd1bWVudHMubGVuZ3RoIDwgMiA/IFtdIDogYXJndW1lbnRzWzFdO1xuXHRpZiAoIUlzQXJyYXkoYWRkaXRpb25hbEludGVybmFsU2xvdHNMaXN0KSkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBgYWRkaXRpb25hbEludGVybmFsU2xvdHNMaXN0YCBtdXN0IGJlIGFuIEFycmF5Jyk7XG5cdH1cblx0Ly8gdmFyIGludGVybmFsU2xvdHNMaXN0ID0gWydbW1Byb3RvdHlwZV1dJywgJ1tbRXh0ZW5zaWJsZV1dJ107XG5cdGlmIChhZGRpdGlvbmFsSW50ZXJuYWxTbG90c0xpc3QubGVuZ3RoID4gMCkge1xuXHRcdHRocm93IG5ldyAkU3ludGF4RXJyb3IoJ2VzLWFic3RyYWN0IGRvZXMgbm90IHlldCBzdXBwb3J0IGludGVybmFsIHNsb3RzJyk7XG5cdFx0Ly8gaW50ZXJuYWxTbG90c0xpc3QucHVzaCguLi5hZGRpdGlvbmFsSW50ZXJuYWxTbG90c0xpc3QpO1xuXHR9XG5cdC8vIHZhciBPID0gTWFrZUJhc2ljT2JqZWN0KGludGVybmFsU2xvdHNMaXN0KTtcblx0Ly8gc2V0UHJvdG8oTywgcHJvdG8pO1xuXHQvLyByZXR1cm4gTztcblxuXHRpZiAoJE9iamVjdENyZWF0ZSkge1xuXHRcdHJldHVybiAkT2JqZWN0Q3JlYXRlKHByb3RvKTtcblx0fVxuXHRpZiAoaGFzUHJvdG8pIHtcblx0XHRyZXR1cm4geyBfX3Byb3RvX186IHByb3RvIH07XG5cdH1cblxuXHRpZiAocHJvdG8gPT09IG51bGwpIHtcblx0XHR0aHJvdyBuZXcgJFN5bnRheEVycm9yKCduYXRpdmUgT2JqZWN0LmNyZWF0ZSBzdXBwb3J0IGlzIHJlcXVpcmVkIHRvIGNyZWF0ZSBudWxsIG9iamVjdHMnKTtcblx0fVxuXHR2YXIgVCA9IGZ1bmN0aW9uIFQoKSB7fTtcblx0VC5wcm90b3R5cGUgPSBwcm90bztcblx0cmV0dXJuIG5ldyBUKCk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///953\n")},6258:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar regexExec = __webpack_require__(1924)('RegExp.prototype.exec');\n\nvar Call = __webpack_require__(581);\nvar Get = __webpack_require__(1391);\nvar IsCallable = __webpack_require__(1787);\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-regexpexec\n\nmodule.exports = function RegExpExec(R, S) {\n\tif (Type(R) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: `R` must be an Object');\n\t}\n\tif (Type(S) !== 'String') {\n\t\tthrow new $TypeError('Assertion failed: `S` must be a String');\n\t}\n\tvar exec = Get(R, 'exec');\n\tif (IsCallable(exec)) {\n\t\tvar result = Call(exec, R, [S]);\n\t\tif (result === null || Type(result) === 'Object') {\n\t\t\treturn result;\n\t\t}\n\t\tthrow new $TypeError('\"exec\" method must return `null` or an Object');\n\t}\n\treturn regexExec(R, S);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNjI1OC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQSxnQkFBZ0IsbUJBQU8sQ0FBQyxJQUFxQjs7QUFFN0MsV0FBVyxtQkFBTyxDQUFDLEdBQVE7QUFDM0IsVUFBVSxtQkFBTyxDQUFDLElBQU87QUFDekIsaUJBQWlCLG1CQUFPLENBQUMsSUFBYztBQUN2QyxXQUFXLG1CQUFPLENBQUMsSUFBUTs7QUFFM0I7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9SZWdFeHBFeGVjLmpzPzU4ZGMiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJFR5cGVFcnJvciA9IEdldEludHJpbnNpYygnJVR5cGVFcnJvciUnKTtcblxudmFyIHJlZ2V4RXhlYyA9IHJlcXVpcmUoJ2NhbGwtYmluZC9jYWxsQm91bmQnKSgnUmVnRXhwLnByb3RvdHlwZS5leGVjJyk7XG5cbnZhciBDYWxsID0gcmVxdWlyZSgnLi9DYWxsJyk7XG52YXIgR2V0ID0gcmVxdWlyZSgnLi9HZXQnKTtcbnZhciBJc0NhbGxhYmxlID0gcmVxdWlyZSgnLi9Jc0NhbGxhYmxlJyk7XG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG4vLyBodHRwczovL2VjbWEtaW50ZXJuYXRpb25hbC5vcmcvZWNtYS0yNjIvNi4wLyNzZWMtcmVnZXhwZXhlY1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFJlZ0V4cEV4ZWMoUiwgUykge1xuXHRpZiAoVHlwZShSKSAhPT0gJ09iamVjdCcpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogYFJgIG11c3QgYmUgYW4gT2JqZWN0Jyk7XG5cdH1cblx0aWYgKFR5cGUoUykgIT09ICdTdHJpbmcnKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IGBTYCBtdXN0IGJlIGEgU3RyaW5nJyk7XG5cdH1cblx0dmFyIGV4ZWMgPSBHZXQoUiwgJ2V4ZWMnKTtcblx0aWYgKElzQ2FsbGFibGUoZXhlYykpIHtcblx0XHR2YXIgcmVzdWx0ID0gQ2FsbChleGVjLCBSLCBbU10pO1xuXHRcdGlmIChyZXN1bHQgPT09IG51bGwgfHwgVHlwZShyZXN1bHQpID09PSAnT2JqZWN0Jykge1xuXHRcdFx0cmV0dXJuIHJlc3VsdDtcblx0XHR9XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ1wiZXhlY1wiIG1ldGhvZCBtdXN0IHJldHVybiBgbnVsbGAgb3IgYW4gT2JqZWN0Jyk7XG5cdH1cblx0cmV0dXJuIHJlZ2V4RXhlYyhSLCBTKTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///6258\n")},9619:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nmodule.exports = __webpack_require__(4559);\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTYxOS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYiwwQ0FBcUQiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvUmVxdWlyZU9iamVjdENvZXJjaWJsZS5qcz8wMWJjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxubW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKCcuLi81L0NoZWNrT2JqZWN0Q29lcmNpYmxlJyk7XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///9619\n")},484:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar $isNaN = __webpack_require__(9086);\n\n// http://262.ecma-international.org/5.1/#sec-9.12\n\nmodule.exports = function SameValue(x, y) {\n\tif (x === y) { // 0 === -0, but they are not identical.\n\t\tif (x === 0) { return 1 / x === 1 / y; }\n\t\treturn true;\n\t}\n\treturn $isNaN(x) && $isNaN(y);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDg0LmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLGFBQWEsbUJBQU8sQ0FBQyxJQUFrQjs7QUFFdkM7O0FBRUE7QUFDQSxnQkFBZ0I7QUFDaEIsaUJBQWlCO0FBQ2pCO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL1NhbWVWYWx1ZS5qcz80MzZlIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyICRpc05hTiA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvaXNOYU4nKTtcblxuLy8gaHR0cDovLzI2Mi5lY21hLWludGVybmF0aW9uYWwub3JnLzUuMS8jc2VjLTkuMTJcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBTYW1lVmFsdWUoeCwgeSkge1xuXHRpZiAoeCA9PT0geSkgeyAvLyAwID09PSAtMCwgYnV0IHRoZXkgYXJlIG5vdCBpZGVudGljYWwuXG5cdFx0aWYgKHggPT09IDApIHsgcmV0dXJuIDEgLyB4ID09PSAxIC8geTsgfVxuXHRcdHJldHVybiB0cnVlO1xuXHR9XG5cdHJldHVybiAkaXNOYU4oeCkgJiYgJGlzTmFOKHkpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///484\n")},105:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar IsPropertyKey = __webpack_require__(4305);\nvar SameValue = __webpack_require__(484);\nvar Type = __webpack_require__(3633);\n\n// IE 9 does not throw in strict mode when writability/configurability/extensibility is violated\nvar noThrowOnStrictViolation = (function () {\n\ttry {\n\t\tdelete [].length;\n\t\treturn true;\n\t} catch (e) {\n\t\treturn false;\n\t}\n}());\n\n// https://ecma-international.org/ecma-262/6.0/#sec-set-o-p-v-throw\n\nmodule.exports = function Set(O, P, V, Throw) {\n\tif (Type(O) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: `O` must be an Object');\n\t}\n\tif (!IsPropertyKey(P)) {\n\t\tthrow new $TypeError('Assertion failed: `P` must be a Property Key');\n\t}\n\tif (Type(Throw) !== 'Boolean') {\n\t\tthrow new $TypeError('Assertion failed: `Throw` must be a Boolean');\n\t}\n\tif (Throw) {\n\t\tO[P] = V; // eslint-disable-line no-param-reassign\n\t\tif (noThrowOnStrictViolation && !SameValue(O[P], V)) {\n\t\t\tthrow new $TypeError('Attempted to assign to readonly property.');\n\t\t}\n\t\treturn true;\n\t}\n\ttry {\n\t\tO[P] = V; // eslint-disable-line no-param-reassign\n\t\treturn noThrowOnStrictViolation ? SameValue(O[P], V) : true;\n\t} catch (e) {\n\t\treturn false;\n\t}\n\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTA1LmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWU7O0FBRTFDOztBQUVBLG9CQUFvQixtQkFBTyxDQUFDLElBQWlCO0FBQzdDLGdCQUFnQixtQkFBTyxDQUFDLEdBQWE7QUFDckMsV0FBVyxtQkFBTyxDQUFDLElBQVE7O0FBRTNCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxHQUFHO0FBQ0g7QUFDQTtBQUNBLENBQUM7O0FBRUQ7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLFlBQVk7QUFDWjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxZQUFZO0FBQ1o7QUFDQSxHQUFHO0FBQ0g7QUFDQTs7QUFFQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9TZXQuanM/MjdmZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xuXG52YXIgSXNQcm9wZXJ0eUtleSA9IHJlcXVpcmUoJy4vSXNQcm9wZXJ0eUtleScpO1xudmFyIFNhbWVWYWx1ZSA9IHJlcXVpcmUoJy4vU2FtZVZhbHVlJyk7XG52YXIgVHlwZSA9IHJlcXVpcmUoJy4vVHlwZScpO1xuXG4vLyBJRSA5IGRvZXMgbm90IHRocm93IGluIHN0cmljdCBtb2RlIHdoZW4gd3JpdGFiaWxpdHkvY29uZmlndXJhYmlsaXR5L2V4dGVuc2liaWxpdHkgaXMgdmlvbGF0ZWRcbnZhciBub1Rocm93T25TdHJpY3RWaW9sYXRpb24gPSAoZnVuY3Rpb24gKCkge1xuXHR0cnkge1xuXHRcdGRlbGV0ZSBbXS5sZW5ndGg7XG5cdFx0cmV0dXJuIHRydWU7XG5cdH0gY2F0Y2ggKGUpIHtcblx0XHRyZXR1cm4gZmFsc2U7XG5cdH1cbn0oKSk7XG5cbi8vIGh0dHBzOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi82LjAvI3NlYy1zZXQtby1wLXYtdGhyb3dcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBTZXQoTywgUCwgViwgVGhyb3cpIHtcblx0aWYgKFR5cGUoTykgIT09ICdPYmplY3QnKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IGBPYCBtdXN0IGJlIGFuIE9iamVjdCcpO1xuXHR9XG5cdGlmICghSXNQcm9wZXJ0eUtleShQKSkge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBgUGAgbXVzdCBiZSBhIFByb3BlcnR5IEtleScpO1xuXHR9XG5cdGlmIChUeXBlKFRocm93KSAhPT0gJ0Jvb2xlYW4nKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Fzc2VydGlvbiBmYWlsZWQ6IGBUaHJvd2AgbXVzdCBiZSBhIEJvb2xlYW4nKTtcblx0fVxuXHRpZiAoVGhyb3cpIHtcblx0XHRPW1BdID0gVjsgLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1wYXJhbS1yZWFzc2lnblxuXHRcdGlmIChub1Rocm93T25TdHJpY3RWaW9sYXRpb24gJiYgIVNhbWVWYWx1ZShPW1BdLCBWKSkge1xuXHRcdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0F0dGVtcHRlZCB0byBhc3NpZ24gdG8gcmVhZG9ubHkgcHJvcGVydHkuJyk7XG5cdFx0fVxuXHRcdHJldHVybiB0cnVlO1xuXHR9XG5cdHRyeSB7XG5cdFx0T1tQXSA9IFY7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tcGFyYW0tcmVhc3NpZ25cblx0XHRyZXR1cm4gbm9UaHJvd09uU3RyaWN0VmlvbGF0aW9uID8gU2FtZVZhbHVlKE9bUF0sIFYpIDogdHJ1ZTtcblx0fSBjYXRjaCAoZSkge1xuXHRcdHJldHVybiBmYWxzZTtcblx0fVxuXG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///105\n")},9655:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $species = GetIntrinsic('%Symbol.species%', true);\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar IsConstructor = __webpack_require__(1974);\nvar Type = __webpack_require__(3633);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-speciesconstructor\n\nmodule.exports = function SpeciesConstructor(O, defaultConstructor) {\n\tif (Type(O) !== 'Object') {\n\t\tthrow new $TypeError('Assertion failed: Type(O) is not Object');\n\t}\n\tvar C = O.constructor;\n\tif (typeof C === 'undefined') {\n\t\treturn defaultConstructor;\n\t}\n\tif (Type(C) !== 'Object') {\n\t\tthrow new $TypeError('O.constructor is not an Object');\n\t}\n\tvar S = $species ? C[$species] : void 0;\n\tif (S == null) {\n\t\treturn defaultConstructor;\n\t}\n\tif (IsConstructor(S)) {\n\t\treturn S;\n\t}\n\tthrow new $TypeError('no constructor found');\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTY1NS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBOztBQUVBLG9CQUFvQixtQkFBTyxDQUFDLElBQWlCO0FBQzdDLFdBQVcsbUJBQU8sQ0FBQyxJQUFROztBQUUzQjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL1NwZWNpZXNDb25zdHJ1Y3Rvci5qcz82MzAxIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRzcGVjaWVzID0gR2V0SW50cmluc2ljKCclU3ltYm9sLnNwZWNpZXMlJywgdHJ1ZSk7XG52YXIgJFR5cGVFcnJvciA9IEdldEludHJpbnNpYygnJVR5cGVFcnJvciUnKTtcblxudmFyIElzQ29uc3RydWN0b3IgPSByZXF1aXJlKCcuL0lzQ29uc3RydWN0b3InKTtcbnZhciBUeXBlID0gcmVxdWlyZSgnLi9UeXBlJyk7XG5cbi8vIGh0dHBzOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi82LjAvI3NlYy1zcGVjaWVzY29uc3RydWN0b3JcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBTcGVjaWVzQ29uc3RydWN0b3IoTywgZGVmYXVsdENvbnN0cnVjdG9yKSB7XG5cdGlmIChUeXBlKE8pICE9PSAnT2JqZWN0Jykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdBc3NlcnRpb24gZmFpbGVkOiBUeXBlKE8pIGlzIG5vdCBPYmplY3QnKTtcblx0fVxuXHR2YXIgQyA9IE8uY29uc3RydWN0b3I7XG5cdGlmICh0eXBlb2YgQyA9PT0gJ3VuZGVmaW5lZCcpIHtcblx0XHRyZXR1cm4gZGVmYXVsdENvbnN0cnVjdG9yO1xuXHR9XG5cdGlmIChUeXBlKEMpICE9PSAnT2JqZWN0Jykge1xuXHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdPLmNvbnN0cnVjdG9yIGlzIG5vdCBhbiBPYmplY3QnKTtcblx0fVxuXHR2YXIgUyA9ICRzcGVjaWVzID8gQ1skc3BlY2llc10gOiB2b2lkIDA7XG5cdGlmIChTID09IG51bGwpIHtcblx0XHRyZXR1cm4gZGVmYXVsdENvbnN0cnVjdG9yO1xuXHR9XG5cdGlmIChJc0NvbnN0cnVjdG9yKFMpKSB7XG5cdFx0cmV0dXJuIFM7XG5cdH1cblx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ25vIGNvbnN0cnVjdG9yIGZvdW5kJyk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///9655\n")},9731:function(module){"use strict";eval("\n\n// http://262.ecma-international.org/5.1/#sec-9.2\n\nmodule.exports = function ToBoolean(value) { return !!value; };\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTczMS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQSw2Q0FBNkMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvVG9Cb29sZWFuLmpzPzNhMGQiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG4vLyBodHRwOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvNS4xLyNzZWMtOS4yXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVG9Cb29sZWFuKHZhbHVlKSB7IHJldHVybiAhIXZhbHVlOyB9O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///9731\n")},751:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar ES5ToInteger = __webpack_require__(775);\n\nvar ToNumber = __webpack_require__(5631);\n\n// https://www.ecma-international.org/ecma-262/11.0/#sec-tointeger\n\nmodule.exports = function ToInteger(value) {\n\tvar number = ToNumber(value);\n\tif (number !== 0) {\n\t\tnumber = ES5ToInteger(number);\n\t}\n\treturn number === 0 ? 0 : number;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzUxLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWdCOztBQUUzQyxlQUFlLG1CQUFPLENBQUMsSUFBWTs7QUFFbkM7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvVG9JbnRlZ2VyT3JJbmZpbml0eS5qcz84MmNjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEVTNVRvSW50ZWdlciA9IHJlcXVpcmUoJy4uLzUvVG9JbnRlZ2VyJyk7XG5cbnZhciBUb051bWJlciA9IHJlcXVpcmUoJy4vVG9OdW1iZXInKTtcblxuLy8gaHR0cHM6Ly93d3cuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi8xMS4wLyNzZWMtdG9pbnRlZ2VyXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVG9JbnRlZ2VyKHZhbHVlKSB7XG5cdHZhciBudW1iZXIgPSBUb051bWJlcih2YWx1ZSk7XG5cdGlmIChudW1iZXIgIT09IDApIHtcblx0XHRudW1iZXIgPSBFUzVUb0ludGVnZXIobnVtYmVyKTtcblx0fVxuXHRyZXR1cm4gbnVtYmVyID09PSAwID8gMCA6IG51bWJlcjtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///751\n")},8305:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar MAX_SAFE_INTEGER = __webpack_require__(1645);\n\nvar ToIntegerOrInfinity = __webpack_require__(751);\n\nmodule.exports = function ToLength(argument) {\n\tvar len = ToIntegerOrInfinity(argument);\n\tif (len <= 0) { return 0; } // includes converting -0 to +0\n\tif (len > MAX_SAFE_INTEGER) { return MAX_SAFE_INTEGER; }\n\treturn len;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODMwNS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYix1QkFBdUIsbUJBQU8sQ0FBQyxJQUEyQjs7QUFFMUQsMEJBQTBCLG1CQUFPLENBQUMsR0FBdUI7O0FBRXpEO0FBQ0E7QUFDQSxpQkFBaUIsWUFBWTtBQUM3QiwrQkFBK0I7QUFDL0I7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Ub0xlbmd0aC5qcz9mZWRlIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIE1BWF9TQUZFX0lOVEVHRVIgPSByZXF1aXJlKCcuLi9oZWxwZXJzL21heFNhZmVJbnRlZ2VyJyk7XG5cbnZhciBUb0ludGVnZXJPckluZmluaXR5ID0gcmVxdWlyZSgnLi9Ub0ludGVnZXJPckluZmluaXR5Jyk7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVG9MZW5ndGgoYXJndW1lbnQpIHtcblx0dmFyIGxlbiA9IFRvSW50ZWdlck9ySW5maW5pdHkoYXJndW1lbnQpO1xuXHRpZiAobGVuIDw9IDApIHsgcmV0dXJuIDA7IH0gLy8gaW5jbHVkZXMgY29udmVydGluZyAtMCB0byArMFxuXHRpZiAobGVuID4gTUFYX1NBRkVfSU5URUdFUikgeyByZXR1cm4gTUFYX1NBRkVfSU5URUdFUjsgfVxuXHRyZXR1cm4gbGVuO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///8305\n")},5631:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar $Number = GetIntrinsic('%Number%');\nvar $RegExp = GetIntrinsic('%RegExp%');\nvar $parseInteger = GetIntrinsic('%parseInt%');\n\nvar callBound = __webpack_require__(1924);\nvar regexTester = __webpack_require__(823);\nvar isPrimitive = __webpack_require__(4790);\n\nvar $strSlice = callBound('String.prototype.slice');\nvar isBinary = regexTester(/^0b[01]+$/i);\nvar isOctal = regexTester(/^0o[0-7]+$/i);\nvar isInvalidHexLiteral = regexTester(/^[-+]0x[0-9a-f]+$/i);\nvar nonWS = ['\\u0085', '\\u200b', '\\ufffe'].join('');\nvar nonWSregex = new $RegExp('[' + nonWS + ']', 'g');\nvar hasNonWS = regexTester(nonWSregex);\n\n// whitespace from: https://es5.github.io/#x15.5.4.20\n// implementation from https://github.com/es-shims/es5-shim/blob/v3.4.0/es5-shim.js#L1304-L1324\nvar ws = [\n\t'\\x09\\x0A\\x0B\\x0C\\x0D\\x20\\xA0\\u1680\\u180E\\u2000\\u2001\\u2002\\u2003',\n\t'\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200A\\u202F\\u205F\\u3000\\u2028',\n\t'\\u2029\\uFEFF'\n].join('');\nvar trimRegex = new RegExp('(^[' + ws + ']+)|([' + ws + ']+$)', 'g');\nvar $replace = callBound('String.prototype.replace');\nvar $trim = function (value) {\n\treturn $replace(value, trimRegex, '');\n};\n\nvar ToPrimitive = __webpack_require__(4607);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-tonumber\n\nmodule.exports = function ToNumber(argument) {\n\tvar value = isPrimitive(argument) ? argument : ToPrimitive(argument, $Number);\n\tif (typeof value === 'symbol') {\n\t\tthrow new $TypeError('Cannot convert a Symbol value to a number');\n\t}\n\tif (typeof value === 'bigint') {\n\t\tthrow new $TypeError('Conversion from \\'BigInt\\' to \\'number\\' is not allowed.');\n\t}\n\tif (typeof value === 'string') {\n\t\tif (isBinary(value)) {\n\t\t\treturn ToNumber($parseInteger($strSlice(value, 2), 2));\n\t\t} else if (isOctal(value)) {\n\t\t\treturn ToNumber($parseInteger($strSlice(value, 2), 8));\n\t\t} else if (hasNonWS(value) || isInvalidHexLiteral(value)) {\n\t\t\treturn NaN;\n\t\t}\n\t\tvar trimmed = $trim(value);\n\t\tif (trimmed !== value) {\n\t\t\treturn ToNumber(trimmed);\n\t\t}\n\n\t}\n\treturn $Number(value);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTYzMS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBO0FBQ0E7QUFDQTs7QUFFQSxnQkFBZ0IsbUJBQU8sQ0FBQyxJQUFxQjtBQUM3QyxrQkFBa0IsbUJBQU8sQ0FBQyxHQUF3QjtBQUNsRCxrQkFBa0IsbUJBQU8sQ0FBQyxJQUF3Qjs7QUFFbEQ7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBLGtCQUFrQixtQkFBTyxDQUFDLElBQWU7O0FBRXpDOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxJQUFJO0FBQ0o7QUFDQSxJQUFJO0FBQ0o7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Ub051bWJlci5qcz81YWM0Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG52YXIgJE51bWJlciA9IEdldEludHJpbnNpYygnJU51bWJlciUnKTtcbnZhciAkUmVnRXhwID0gR2V0SW50cmluc2ljKCclUmVnRXhwJScpO1xudmFyICRwYXJzZUludGVnZXIgPSBHZXRJbnRyaW5zaWMoJyVwYXJzZUludCUnKTtcblxudmFyIGNhbGxCb3VuZCA9IHJlcXVpcmUoJ2NhbGwtYmluZC9jYWxsQm91bmQnKTtcbnZhciByZWdleFRlc3RlciA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvcmVnZXhUZXN0ZXInKTtcbnZhciBpc1ByaW1pdGl2ZSA9IHJlcXVpcmUoJy4uL2hlbHBlcnMvaXNQcmltaXRpdmUnKTtcblxudmFyICRzdHJTbGljZSA9IGNhbGxCb3VuZCgnU3RyaW5nLnByb3RvdHlwZS5zbGljZScpO1xudmFyIGlzQmluYXJ5ID0gcmVnZXhUZXN0ZXIoL14wYlswMV0rJC9pKTtcbnZhciBpc09jdGFsID0gcmVnZXhUZXN0ZXIoL14wb1swLTddKyQvaSk7XG52YXIgaXNJbnZhbGlkSGV4TGl0ZXJhbCA9IHJlZ2V4VGVzdGVyKC9eWy0rXTB4WzAtOWEtZl0rJC9pKTtcbnZhciBub25XUyA9IFsnXFx1MDA4NScsICdcXHUyMDBiJywgJ1xcdWZmZmUnXS5qb2luKCcnKTtcbnZhciBub25XU3JlZ2V4ID0gbmV3ICRSZWdFeHAoJ1snICsgbm9uV1MgKyAnXScsICdnJyk7XG52YXIgaGFzTm9uV1MgPSByZWdleFRlc3Rlcihub25XU3JlZ2V4KTtcblxuLy8gd2hpdGVzcGFjZSBmcm9tOiBodHRwczovL2VzNS5naXRodWIuaW8vI3gxNS41LjQuMjBcbi8vIGltcGxlbWVudGF0aW9uIGZyb20gaHR0cHM6Ly9naXRodWIuY29tL2VzLXNoaW1zL2VzNS1zaGltL2Jsb2IvdjMuNC4wL2VzNS1zaGltLmpzI0wxMzA0LUwxMzI0XG52YXIgd3MgPSBbXG5cdCdcXHgwOVxceDBBXFx4MEJcXHgwQ1xceDBEXFx4MjBcXHhBMFxcdTE2ODBcXHUxODBFXFx1MjAwMFxcdTIwMDFcXHUyMDAyXFx1MjAwMycsXG5cdCdcXHUyMDA0XFx1MjAwNVxcdTIwMDZcXHUyMDA3XFx1MjAwOFxcdTIwMDlcXHUyMDBBXFx1MjAyRlxcdTIwNUZcXHUzMDAwXFx1MjAyOCcsXG5cdCdcXHUyMDI5XFx1RkVGRidcbl0uam9pbignJyk7XG52YXIgdHJpbVJlZ2V4ID0gbmV3IFJlZ0V4cCgnKF5bJyArIHdzICsgJ10rKXwoWycgKyB3cyArICddKyQpJywgJ2cnKTtcbnZhciAkcmVwbGFjZSA9IGNhbGxCb3VuZCgnU3RyaW5nLnByb3RvdHlwZS5yZXBsYWNlJyk7XG52YXIgJHRyaW0gPSBmdW5jdGlvbiAodmFsdWUpIHtcblx0cmV0dXJuICRyZXBsYWNlKHZhbHVlLCB0cmltUmVnZXgsICcnKTtcbn07XG5cbnZhciBUb1ByaW1pdGl2ZSA9IHJlcXVpcmUoJy4vVG9QcmltaXRpdmUnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLXRvbnVtYmVyXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVG9OdW1iZXIoYXJndW1lbnQpIHtcblx0dmFyIHZhbHVlID0gaXNQcmltaXRpdmUoYXJndW1lbnQpID8gYXJndW1lbnQgOiBUb1ByaW1pdGl2ZShhcmd1bWVudCwgJE51bWJlcik7XG5cdGlmICh0eXBlb2YgdmFsdWUgPT09ICdzeW1ib2wnKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0Nhbm5vdCBjb252ZXJ0IGEgU3ltYm9sIHZhbHVlIHRvIGEgbnVtYmVyJyk7XG5cdH1cblx0aWYgKHR5cGVvZiB2YWx1ZSA9PT0gJ2JpZ2ludCcpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQ29udmVyc2lvbiBmcm9tIFxcJ0JpZ0ludFxcJyB0byBcXCdudW1iZXJcXCcgaXMgbm90IGFsbG93ZWQuJyk7XG5cdH1cblx0aWYgKHR5cGVvZiB2YWx1ZSA9PT0gJ3N0cmluZycpIHtcblx0XHRpZiAoaXNCaW5hcnkodmFsdWUpKSB7XG5cdFx0XHRyZXR1cm4gVG9OdW1iZXIoJHBhcnNlSW50ZWdlcigkc3RyU2xpY2UodmFsdWUsIDIpLCAyKSk7XG5cdFx0fSBlbHNlIGlmIChpc09jdGFsKHZhbHVlKSkge1xuXHRcdFx0cmV0dXJuIFRvTnVtYmVyKCRwYXJzZUludGVnZXIoJHN0clNsaWNlKHZhbHVlLCAyKSwgOCkpO1xuXHRcdH0gZWxzZSBpZiAoaGFzTm9uV1ModmFsdWUpIHx8IGlzSW52YWxpZEhleExpdGVyYWwodmFsdWUpKSB7XG5cdFx0XHRyZXR1cm4gTmFOO1xuXHRcdH1cblx0XHR2YXIgdHJpbW1lZCA9ICR0cmltKHZhbHVlKTtcblx0XHRpZiAodHJpbW1lZCAhPT0gdmFsdWUpIHtcblx0XHRcdHJldHVybiBUb051bWJlcih0cmltbWVkKTtcblx0XHR9XG5cblx0fVxuXHRyZXR1cm4gJE51bWJlcih2YWx1ZSk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///5631\n")},821:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $Object = GetIntrinsic('%Object%');\n\nvar RequireObjectCoercible = __webpack_require__(9619);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-toobject\n\nmodule.exports = function ToObject(value) {\n\tRequireObjectCoercible(value);\n\treturn $Object(value);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODIxLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWU7O0FBRTFDOztBQUVBLDZCQUE2QixtQkFBTyxDQUFDLElBQTBCOztBQUUvRDs7QUFFQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Ub09iamVjdC5qcz81Mzc0Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRPYmplY3QgPSBHZXRJbnRyaW5zaWMoJyVPYmplY3QlJyk7XG5cbnZhciBSZXF1aXJlT2JqZWN0Q29lcmNpYmxlID0gcmVxdWlyZSgnLi9SZXF1aXJlT2JqZWN0Q29lcmNpYmxlJyk7XG5cbi8vIGh0dHBzOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi82LjAvI3NlYy10b29iamVjdFxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFRvT2JqZWN0KHZhbHVlKSB7XG5cdFJlcXVpcmVPYmplY3RDb2VyY2libGUodmFsdWUpO1xuXHRyZXR1cm4gJE9iamVjdCh2YWx1ZSk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///821\n")},4607:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar toPrimitive = __webpack_require__(1503);\n\n// https://ecma-international.org/ecma-262/6.0/#sec-toprimitive\n\nmodule.exports = function ToPrimitive(input) {\n\tif (arguments.length > 1) {\n\t\treturn toPrimitive(input, arguments[1]);\n\t}\n\treturn toPrimitive(input);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDYwNy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixrQkFBa0IsbUJBQU8sQ0FBQyxJQUF3Qjs7QUFFbEQ7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL1RvUHJpbWl0aXZlLmpzP2I1MGMiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgdG9QcmltaXRpdmUgPSByZXF1aXJlKCdlcy10by1wcmltaXRpdmUvZXMyMDE1Jyk7XG5cbi8vIGh0dHBzOi8vZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy9lY21hLTI2Mi82LjAvI3NlYy10b3ByaW1pdGl2ZVxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFRvUHJpbWl0aXZlKGlucHV0KSB7XG5cdGlmIChhcmd1bWVudHMubGVuZ3RoID4gMSkge1xuXHRcdHJldHVybiB0b1ByaW1pdGl2ZShpbnB1dCwgYXJndW1lbnRzWzFdKTtcblx0fVxuXHRyZXR1cm4gdG9QcmltaXRpdmUoaW5wdXQpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///4607\n")},9916:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar has = __webpack_require__(7642);\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nvar Type = __webpack_require__(3633);\nvar ToBoolean = __webpack_require__(9731);\nvar IsCallable = __webpack_require__(1787);\n\n// https://262.ecma-international.org/5.1/#sec-8.10.5\n\nmodule.exports = function ToPropertyDescriptor(Obj) {\n\tif (Type(Obj) !== 'Object') {\n\t\tthrow new $TypeError('ToPropertyDescriptor requires an object');\n\t}\n\n\tvar desc = {};\n\tif (has(Obj, 'enumerable')) {\n\t\tdesc['[[Enumerable]]'] = ToBoolean(Obj.enumerable);\n\t}\n\tif (has(Obj, 'configurable')) {\n\t\tdesc['[[Configurable]]'] = ToBoolean(Obj.configurable);\n\t}\n\tif (has(Obj, 'value')) {\n\t\tdesc['[[Value]]'] = Obj.value;\n\t}\n\tif (has(Obj, 'writable')) {\n\t\tdesc['[[Writable]]'] = ToBoolean(Obj.writable);\n\t}\n\tif (has(Obj, 'get')) {\n\t\tvar getter = Obj.get;\n\t\tif (typeof getter !== 'undefined' && !IsCallable(getter)) {\n\t\t\tthrow new $TypeError('getter must be a function');\n\t\t}\n\t\tdesc['[[Get]]'] = getter;\n\t}\n\tif (has(Obj, 'set')) {\n\t\tvar setter = Obj.set;\n\t\tif (typeof setter !== 'undefined' && !IsCallable(setter)) {\n\t\t\tthrow new $TypeError('setter must be a function');\n\t\t}\n\t\tdesc['[[Set]]'] = setter;\n\t}\n\n\tif ((has(desc, '[[Get]]') || has(desc, '[[Set]]')) && (has(desc, '[[Value]]') || has(desc, '[[Writable]]'))) {\n\t\tthrow new $TypeError('Invalid property descriptor. Cannot both specify accessors and a value or writable attribute');\n\t}\n\treturn desc;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTkxNi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixVQUFVLG1CQUFPLENBQUMsSUFBSzs7QUFFdkIsbUJBQW1CLG1CQUFPLENBQUMsR0FBZTs7QUFFMUM7O0FBRUEsV0FBVyxtQkFBTyxDQUFDLElBQVE7QUFDM0IsZ0JBQWdCLG1CQUFPLENBQUMsSUFBYTtBQUNyQyxpQkFBaUIsbUJBQU8sQ0FBQyxJQUFjOztBQUV2Qzs7QUFFQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Ub1Byb3BlcnR5RGVzY3JpcHRvci5qcz8yMTJjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGhhcyA9IHJlcXVpcmUoJ2hhcycpO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJFR5cGVFcnJvciA9IEdldEludHJpbnNpYygnJVR5cGVFcnJvciUnKTtcblxudmFyIFR5cGUgPSByZXF1aXJlKCcuL1R5cGUnKTtcbnZhciBUb0Jvb2xlYW4gPSByZXF1aXJlKCcuL1RvQm9vbGVhbicpO1xudmFyIElzQ2FsbGFibGUgPSByZXF1aXJlKCcuL0lzQ2FsbGFibGUnKTtcblxuLy8gaHR0cHM6Ly8yNjIuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy81LjEvI3NlYy04LjEwLjVcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBUb1Byb3BlcnR5RGVzY3JpcHRvcihPYmopIHtcblx0aWYgKFR5cGUoT2JqKSAhPT0gJ09iamVjdCcpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignVG9Qcm9wZXJ0eURlc2NyaXB0b3IgcmVxdWlyZXMgYW4gb2JqZWN0Jyk7XG5cdH1cblxuXHR2YXIgZGVzYyA9IHt9O1xuXHRpZiAoaGFzKE9iaiwgJ2VudW1lcmFibGUnKSkge1xuXHRcdGRlc2NbJ1tbRW51bWVyYWJsZV1dJ10gPSBUb0Jvb2xlYW4oT2JqLmVudW1lcmFibGUpO1xuXHR9XG5cdGlmIChoYXMoT2JqLCAnY29uZmlndXJhYmxlJykpIHtcblx0XHRkZXNjWydbW0NvbmZpZ3VyYWJsZV1dJ10gPSBUb0Jvb2xlYW4oT2JqLmNvbmZpZ3VyYWJsZSk7XG5cdH1cblx0aWYgKGhhcyhPYmosICd2YWx1ZScpKSB7XG5cdFx0ZGVzY1snW1tWYWx1ZV1dJ10gPSBPYmoudmFsdWU7XG5cdH1cblx0aWYgKGhhcyhPYmosICd3cml0YWJsZScpKSB7XG5cdFx0ZGVzY1snW1tXcml0YWJsZV1dJ10gPSBUb0Jvb2xlYW4oT2JqLndyaXRhYmxlKTtcblx0fVxuXHRpZiAoaGFzKE9iaiwgJ2dldCcpKSB7XG5cdFx0dmFyIGdldHRlciA9IE9iai5nZXQ7XG5cdFx0aWYgKHR5cGVvZiBnZXR0ZXIgIT09ICd1bmRlZmluZWQnICYmICFJc0NhbGxhYmxlKGdldHRlcikpIHtcblx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdnZXR0ZXIgbXVzdCBiZSBhIGZ1bmN0aW9uJyk7XG5cdFx0fVxuXHRcdGRlc2NbJ1tbR2V0XV0nXSA9IGdldHRlcjtcblx0fVxuXHRpZiAoaGFzKE9iaiwgJ3NldCcpKSB7XG5cdFx0dmFyIHNldHRlciA9IE9iai5zZXQ7XG5cdFx0aWYgKHR5cGVvZiBzZXR0ZXIgIT09ICd1bmRlZmluZWQnICYmICFJc0NhbGxhYmxlKHNldHRlcikpIHtcblx0XHRcdHRocm93IG5ldyAkVHlwZUVycm9yKCdzZXR0ZXIgbXVzdCBiZSBhIGZ1bmN0aW9uJyk7XG5cdFx0fVxuXHRcdGRlc2NbJ1tbU2V0XV0nXSA9IHNldHRlcjtcblx0fVxuXG5cdGlmICgoaGFzKGRlc2MsICdbW0dldF1dJykgfHwgaGFzKGRlc2MsICdbW1NldF1dJykpICYmIChoYXMoZGVzYywgJ1tbVmFsdWVdXScpIHx8IGhhcyhkZXNjLCAnW1tXcml0YWJsZV1dJykpKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ0ludmFsaWQgcHJvcGVydHkgZGVzY3JpcHRvci4gQ2Fubm90IGJvdGggc3BlY2lmeSBhY2Nlc3NvcnMgYW5kIGEgdmFsdWUgb3Igd3JpdGFibGUgYXR0cmlidXRlJyk7XG5cdH1cblx0cmV0dXJuIGRlc2M7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///9916\n")},6846:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $String = GetIntrinsic('%String%');\nvar $TypeError = GetIntrinsic('%TypeError%');\n\n// https://ecma-international.org/ecma-262/6.0/#sec-tostring\n\nmodule.exports = function ToString(argument) {\n\tif (typeof argument === 'symbol') {\n\t\tthrow new $TypeError('Cannot convert a Symbol value to a string');\n\t}\n\treturn $String(argument);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNjg0Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBOztBQUVBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9Ub1N0cmluZy5qcz8xNTk0Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRTdHJpbmcgPSBHZXRJbnRyaW5zaWMoJyVTdHJpbmclJyk7XG52YXIgJFR5cGVFcnJvciA9IEdldEludHJpbnNpYygnJVR5cGVFcnJvciUnKTtcblxuLy8gaHR0cHM6Ly9lY21hLWludGVybmF0aW9uYWwub3JnL2VjbWEtMjYyLzYuMC8jc2VjLXRvc3RyaW5nXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVG9TdHJpbmcoYXJndW1lbnQpIHtcblx0aWYgKHR5cGVvZiBhcmd1bWVudCA9PT0gJ3N5bWJvbCcpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQ2Fubm90IGNvbnZlcnQgYSBTeW1ib2wgdmFsdWUgdG8gYSBzdHJpbmcnKTtcblx0fVxuXHRyZXR1cm4gJFN0cmluZyhhcmd1bWVudCk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///6846\n")},3633:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar ES5Type = __webpack_require__(3951);\n\n// https://262.ecma-international.org/11.0/#sec-ecmascript-data-types-and-values\n\nmodule.exports = function Type(x) {\n\tif (typeof x === 'symbol') {\n\t\treturn 'Symbol';\n\t}\n\tif (typeof x === 'bigint') {\n\t\treturn 'BigInt';\n\t}\n\treturn ES5Type(x);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzYzMy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixjQUFjLG1CQUFPLENBQUMsSUFBVzs7QUFFakM7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC8yMDIxL1R5cGUuanM/YTdmMyJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBFUzVUeXBlID0gcmVxdWlyZSgnLi4vNS9UeXBlJyk7XG5cbi8vIGh0dHBzOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvMTEuMC8jc2VjLWVjbWFzY3JpcHQtZGF0YS10eXBlcy1hbmQtdmFsdWVzXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gVHlwZSh4KSB7XG5cdGlmICh0eXBlb2YgeCA9PT0gJ3N5bWJvbCcpIHtcblx0XHRyZXR1cm4gJ1N5bWJvbCc7XG5cdH1cblx0aWYgKHR5cGVvZiB4ID09PSAnYmlnaW50Jykge1xuXHRcdHJldHVybiAnQmlnSW50Jztcblx0fVxuXHRyZXR1cm4gRVM1VHlwZSh4KTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3633\n")},4857:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar $fromCharCode = GetIntrinsic('%String.fromCharCode%');\n\nvar isLeadingSurrogate = __webpack_require__(9544);\nvar isTrailingSurrogate = __webpack_require__(5424);\n\n// https://tc39.es/ecma262/2020/#sec-utf16decodesurrogatepair\n\nmodule.exports = function UTF16DecodeSurrogatePair(lead, trail) {\n\tif (!isLeadingSurrogate(lead) || !isTrailingSurrogate(trail)) {\n\t\tthrow new $TypeError('Assertion failed: `lead` must be a leading surrogate char code, and `trail` must be a trailing surrogate char code');\n\t}\n\t// var cp = (lead - 0xD800) * 0x400 + (trail - 0xDC00) + 0x10000;\n\treturn $fromCharCode(lead) + $fromCharCode(trail);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDg1Ny5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBOztBQUVBLHlCQUF5QixtQkFBTyxDQUFDLElBQStCO0FBQ2hFLDBCQUEwQixtQkFBTyxDQUFDLElBQWdDOztBQUVsRTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvMjAyMS9VVEYxNlN1cnJvZ2F0ZVBhaXJUb0NvZGVQb2ludC5qcz9iMGE0Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIEdldEludHJpbnNpYyA9IHJlcXVpcmUoJ2dldC1pbnRyaW5zaWMnKTtcblxudmFyICRUeXBlRXJyb3IgPSBHZXRJbnRyaW5zaWMoJyVUeXBlRXJyb3IlJyk7XG52YXIgJGZyb21DaGFyQ29kZSA9IEdldEludHJpbnNpYygnJVN0cmluZy5mcm9tQ2hhckNvZGUlJyk7XG5cbnZhciBpc0xlYWRpbmdTdXJyb2dhdGUgPSByZXF1aXJlKCcuLi9oZWxwZXJzL2lzTGVhZGluZ1N1cnJvZ2F0ZScpO1xudmFyIGlzVHJhaWxpbmdTdXJyb2dhdGUgPSByZXF1aXJlKCcuLi9oZWxwZXJzL2lzVHJhaWxpbmdTdXJyb2dhdGUnKTtcblxuLy8gaHR0cHM6Ly90YzM5LmVzL2VjbWEyNjIvMjAyMC8jc2VjLXV0ZjE2ZGVjb2Rlc3Vycm9nYXRlcGFpclxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFVURjE2RGVjb2RlU3Vycm9nYXRlUGFpcihsZWFkLCB0cmFpbCkge1xuXHRpZiAoIWlzTGVhZGluZ1N1cnJvZ2F0ZShsZWFkKSB8fCAhaXNUcmFpbGluZ1N1cnJvZ2F0ZSh0cmFpbCkpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignQXNzZXJ0aW9uIGZhaWxlZDogYGxlYWRgIG11c3QgYmUgYSBsZWFkaW5nIHN1cnJvZ2F0ZSBjaGFyIGNvZGUsIGFuZCBgdHJhaWxgIG11c3QgYmUgYSB0cmFpbGluZyBzdXJyb2dhdGUgY2hhciBjb2RlJyk7XG5cdH1cblx0Ly8gdmFyIGNwID0gKGxlYWQgLSAweEQ4MDApICogMHg0MDAgKyAodHJhaWwgLSAweERDMDApICsgMHgxMDAwMDtcblx0cmV0dXJuICRmcm9tQ2hhckNvZGUobGVhZCkgKyAkZnJvbUNoYXJDb2RlKHRyYWlsKTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///4857\n")},4908:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $abs = GetIntrinsic('%Math.abs%');\n\n// http://262.ecma-international.org/5.1/#sec-5.2\n\nmodule.exports = function abs(x) {\n\treturn $abs(x);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDkwOC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQTs7QUFFQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvYWJzLmpzPzZjMmEiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJGFicyA9IEdldEludHJpbnNpYygnJU1hdGguYWJzJScpO1xuXG4vLyBodHRwOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvNS4xLyNzZWMtNS4yXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gYWJzKHgpIHtcblx0cmV0dXJuICRhYnMoeCk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///4908\n")},375:function(module){"use strict";eval("\n\n// var modulo = require('./modulo');\nvar $floor = Math.floor;\n\n// http://262.ecma-international.org/5.1/#sec-5.2\n\nmodule.exports = function floor(x) {\n\t// return x - modulo(x, 1);\n\treturn $floor(x);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzc1LmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViO0FBQ0E7O0FBRUE7O0FBRUE7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzIwMjEvZmxvb3IuanM/ODY4NCJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbi8vIHZhciBtb2R1bG8gPSByZXF1aXJlKCcuL21vZHVsbycpO1xudmFyICRmbG9vciA9IE1hdGguZmxvb3I7XG5cbi8vIGh0dHA6Ly8yNjIuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy81LjEvI3NlYy01LjJcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBmbG9vcih4KSB7XG5cdC8vIHJldHVybiB4IC0gbW9kdWxvKHgsIDEpO1xuXHRyZXR1cm4gJGZsb29yKHgpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///375\n")},4559:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\n\n// http://262.ecma-international.org/5.1/#sec-9.10\n\nmodule.exports = function CheckObjectCoercible(value, optMessage) {\n\tif (value == null) {\n\t\tthrow new $TypeError(optMessage || ('Cannot call method on ' + value));\n\t}\n\treturn value;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDU1OS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzUvQ2hlY2tPYmplY3RDb2VyY2libGUuanM/NWM5MiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xuXG4vLyBodHRwOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvNS4xLyNzZWMtOS4xMFxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIENoZWNrT2JqZWN0Q29lcmNpYmxlKHZhbHVlLCBvcHRNZXNzYWdlKSB7XG5cdGlmICh2YWx1ZSA9PSBudWxsKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3Iob3B0TWVzc2FnZSB8fCAoJ0Nhbm5vdCBjYWxsIG1ldGhvZCBvbiAnICsgdmFsdWUpKTtcblx0fVxuXHRyZXR1cm4gdmFsdWU7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///4559\n")},775:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar abs = __webpack_require__(7890);\nvar floor = __webpack_require__(2748);\nvar ToNumber = __webpack_require__(7709);\n\nvar $isNaN = __webpack_require__(9086);\nvar $isFinite = __webpack_require__(2633);\nvar $sign = __webpack_require__(8111);\n\n// http://262.ecma-international.org/5.1/#sec-9.4\n\nmodule.exports = function ToInteger(value) {\n\tvar number = ToNumber(value);\n\tif ($isNaN(number)) { return 0; }\n\tif (number === 0 || !$isFinite(number)) { return number; }\n\treturn $sign(number) * floor(abs(number));\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzc1LmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLFVBQVUsbUJBQU8sQ0FBQyxJQUFPO0FBQ3pCLFlBQVksbUJBQU8sQ0FBQyxJQUFTO0FBQzdCLGVBQWUsbUJBQU8sQ0FBQyxJQUFZOztBQUVuQyxhQUFhLG1CQUFPLENBQUMsSUFBa0I7QUFDdkMsZ0JBQWdCLG1CQUFPLENBQUMsSUFBcUI7QUFDN0MsWUFBWSxtQkFBTyxDQUFDLElBQWlCOztBQUVyQzs7QUFFQTtBQUNBO0FBQ0EsdUJBQXVCO0FBQ3ZCLDJDQUEyQztBQUMzQztBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC81L1RvSW50ZWdlci5qcz81YThjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGFicyA9IHJlcXVpcmUoJy4vYWJzJyk7XG52YXIgZmxvb3IgPSByZXF1aXJlKCcuL2Zsb29yJyk7XG52YXIgVG9OdW1iZXIgPSByZXF1aXJlKCcuL1RvTnVtYmVyJyk7XG5cbnZhciAkaXNOYU4gPSByZXF1aXJlKCcuLi9oZWxwZXJzL2lzTmFOJyk7XG52YXIgJGlzRmluaXRlID0gcmVxdWlyZSgnLi4vaGVscGVycy9pc0Zpbml0ZScpO1xudmFyICRzaWduID0gcmVxdWlyZSgnLi4vaGVscGVycy9zaWduJyk7XG5cbi8vIGh0dHA6Ly8yNjIuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy81LjEvI3NlYy05LjRcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBUb0ludGVnZXIodmFsdWUpIHtcblx0dmFyIG51bWJlciA9IFRvTnVtYmVyKHZhbHVlKTtcblx0aWYgKCRpc05hTihudW1iZXIpKSB7IHJldHVybiAwOyB9XG5cdGlmIChudW1iZXIgPT09IDAgfHwgISRpc0Zpbml0ZShudW1iZXIpKSB7IHJldHVybiBudW1iZXI7IH1cblx0cmV0dXJuICRzaWduKG51bWJlcikgKiBmbG9vcihhYnMobnVtYmVyKSk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///775\n")},7709:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar ToPrimitive = __webpack_require__(1950);\n\n// http://262.ecma-international.org/5.1/#sec-9.3\n\nmodule.exports = function ToNumber(value) {\n\tvar prim = ToPrimitive(value, Number);\n\tif (typeof prim !== 'string') {\n\t\treturn +prim; // eslint-disable-line no-implicit-coercion\n\t}\n\n\t// eslint-disable-next-line no-control-regex\n\tvar trimmed = prim.replace(/^[ \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\u0085]+|[ \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\u0085]+$/g, '');\n\tif ((/^0[ob]|^[+-]0x/).test(trimmed)) {\n\t\treturn NaN;\n\t}\n\n\treturn +trimmed; // eslint-disable-line no-implicit-coercion\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzcwOS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixrQkFBa0IsbUJBQU8sQ0FBQyxJQUFlOztBQUV6Qzs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxnQkFBZ0I7QUFDaEI7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQSxrQkFBa0I7QUFDbEIiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzUvVG9OdW1iZXIuanM/ZTU2YiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBUb1ByaW1pdGl2ZSA9IHJlcXVpcmUoJy4vVG9QcmltaXRpdmUnKTtcblxuLy8gaHR0cDovLzI2Mi5lY21hLWludGVybmF0aW9uYWwub3JnLzUuMS8jc2VjLTkuM1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFRvTnVtYmVyKHZhbHVlKSB7XG5cdHZhciBwcmltID0gVG9QcmltaXRpdmUodmFsdWUsIE51bWJlcik7XG5cdGlmICh0eXBlb2YgcHJpbSAhPT0gJ3N0cmluZycpIHtcblx0XHRyZXR1cm4gK3ByaW07IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8taW1wbGljaXQtY29lcmNpb25cblx0fVxuXG5cdC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1jb250cm9sLXJlZ2V4XG5cdHZhciB0cmltbWVkID0gcHJpbS5yZXBsYWNlKC9eWyBcXHRcXHgwYlxcZlxceGEwXFx1ZmVmZlxcblxcclxcdTIwMjhcXHUyMDI5XFx1MTY4MFxcdTE4MGVcXHUyMDAwXFx1MjAwMVxcdTIwMDJcXHUyMDAzXFx1MjAwNFxcdTIwMDVcXHUyMDA2XFx1MjAwN1xcdTIwMDhcXHUyMDA5XFx1MjAwYVxcdTIwMmZcXHUyMDVmXFx1MzAwMFxcdTAwODVdK3xbIFxcdFxceDBiXFxmXFx4YTBcXHVmZWZmXFxuXFxyXFx1MjAyOFxcdTIwMjlcXHUxNjgwXFx1MTgwZVxcdTIwMDBcXHUyMDAxXFx1MjAwMlxcdTIwMDNcXHUyMDA0XFx1MjAwNVxcdTIwMDZcXHUyMDA3XFx1MjAwOFxcdTIwMDlcXHUyMDBhXFx1MjAyZlxcdTIwNWZcXHUzMDAwXFx1MDA4NV0rJC9nLCAnJyk7XG5cdGlmICgoL14wW29iXXxeWystXTB4LykudGVzdCh0cmltbWVkKSkge1xuXHRcdHJldHVybiBOYU47XG5cdH1cblxuXHRyZXR1cm4gK3RyaW1tZWQ7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8taW1wbGljaXQtY29lcmNpb25cbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///7709\n")},1950:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\n// http://262.ecma-international.org/5.1/#sec-9.1\n\nmodule.exports = __webpack_require__(2116);\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTk1MC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQSwwQ0FBK0MiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzUvVG9QcmltaXRpdmUuanM/ZmEwZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbi8vIGh0dHA6Ly8yNjIuZWNtYS1pbnRlcm5hdGlvbmFsLm9yZy81LjEvI3NlYy05LjFcblxubW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKCdlcy10by1wcmltaXRpdmUvZXM1Jyk7XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///1950\n")},3951:function(module){"use strict";eval("\n\n// https://262.ecma-international.org/5.1/#sec-8\n\nmodule.exports = function Type(x) {\n\tif (x === null) {\n\t\treturn 'Null';\n\t}\n\tif (typeof x === 'undefined') {\n\t\treturn 'Undefined';\n\t}\n\tif (typeof x === 'function' || typeof x === 'object') {\n\t\treturn 'Object';\n\t}\n\tif (typeof x === 'number') {\n\t\treturn 'Number';\n\t}\n\tif (typeof x === 'boolean') {\n\t\treturn 'Boolean';\n\t}\n\tif (typeof x === 'string') {\n\t\treturn 'String';\n\t}\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzk1MS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC81L1R5cGUuanM/ODYzZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbi8vIGh0dHBzOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvNS4xLyNzZWMtOFxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIFR5cGUoeCkge1xuXHRpZiAoeCA9PT0gbnVsbCkge1xuXHRcdHJldHVybiAnTnVsbCc7XG5cdH1cblx0aWYgKHR5cGVvZiB4ID09PSAndW5kZWZpbmVkJykge1xuXHRcdHJldHVybiAnVW5kZWZpbmVkJztcblx0fVxuXHRpZiAodHlwZW9mIHggPT09ICdmdW5jdGlvbicgfHwgdHlwZW9mIHggPT09ICdvYmplY3QnKSB7XG5cdFx0cmV0dXJuICdPYmplY3QnO1xuXHR9XG5cdGlmICh0eXBlb2YgeCA9PT0gJ251bWJlcicpIHtcblx0XHRyZXR1cm4gJ051bWJlcic7XG5cdH1cblx0aWYgKHR5cGVvZiB4ID09PSAnYm9vbGVhbicpIHtcblx0XHRyZXR1cm4gJ0Jvb2xlYW4nO1xuXHR9XG5cdGlmICh0eXBlb2YgeCA9PT0gJ3N0cmluZycpIHtcblx0XHRyZXR1cm4gJ1N0cmluZyc7XG5cdH1cbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///3951\n")},7890:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $abs = GetIntrinsic('%Math.abs%');\n\n// http://262.ecma-international.org/5.1/#sec-5.2\n\nmodule.exports = function abs(x) {\n\treturn $abs(x);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNzg5MC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQTs7QUFFQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0LzUvYWJzLmpzPzRjNmUiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJGFicyA9IEdldEludHJpbnNpYygnJU1hdGguYWJzJScpO1xuXG4vLyBodHRwOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvNS4xLyNzZWMtNS4yXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gYWJzKHgpIHtcblx0cmV0dXJuICRhYnMoeCk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///7890\n")},2748:function(module){"use strict";eval("\n\n// var modulo = require('./modulo');\nvar $floor = Math.floor;\n\n// http://262.ecma-international.org/5.1/#sec-5.2\n\nmodule.exports = function floor(x) {\n\t// return x - modulo(x, 1);\n\treturn $floor(x);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjc0OC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBOztBQUVBOztBQUVBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC81L2Zsb29yLmpzPzlkZGYiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG4vLyB2YXIgbW9kdWxvID0gcmVxdWlyZSgnLi9tb2R1bG8nKTtcbnZhciAkZmxvb3IgPSBNYXRoLmZsb29yO1xuXG4vLyBodHRwOi8vMjYyLmVjbWEtaW50ZXJuYXRpb25hbC5vcmcvNS4xLyNzZWMtNS4yXG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gZmxvb3IoeCkge1xuXHQvLyByZXR1cm4geCAtIG1vZHVsbyh4LCAxKTtcblx0cmV0dXJuICRmbG9vcih4KTtcbn07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///2748\n")},4445:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\n// TODO: remove, semver-major\n\nmodule.exports = __webpack_require__(210);\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDQ0NS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjs7QUFFQSx5Q0FBeUMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L0dldEludHJpbnNpYy5qcz83MWZjIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxuLy8gVE9ETzogcmVtb3ZlLCBzZW12ZXItbWFqb3JcblxubW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///4445\n")},3682:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $defineProperty = GetIntrinsic('%Object.defineProperty%', true);\n\nif ($defineProperty) {\n\ttry {\n\t\t$defineProperty({}, 'a', { value: 1 });\n\t} catch (e) {\n\t\t// IE 8 has a broken defineProperty\n\t\t$defineProperty = null;\n\t}\n}\n\n// node v0.6 has a bug where array lengths can be Set but not Defined\nvar hasArrayLengthDefineBug = Object.defineProperty && Object.defineProperty([], 'length', { value: 1 }).length === 0;\n\n// eslint-disable-next-line global-require\nvar isArray = hasArrayLengthDefineBug && __webpack_require__(7912); // this does not depend on any other AOs.\n\nvar callBound = __webpack_require__(1924);\n\nvar $isEnumerable = callBound('Object.prototype.propertyIsEnumerable');\n\n// eslint-disable-next-line max-params\nmodule.exports = function DefineOwnProperty(IsDataDescriptor, SameValue, FromPropertyDescriptor, O, P, desc) {\n\tif (!$defineProperty) {\n\t\tif (!IsDataDescriptor(desc)) {\n\t\t\t// ES3 does not support getters/setters\n\t\t\treturn false;\n\t\t}\n\t\tif (!desc['[[Configurable]]'] || !desc['[[Writable]]']) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// fallback for ES3\n\t\tif (P in O && $isEnumerable(O, P) !== !!desc['[[Enumerable]]']) {\n\t\t\t// a non-enumerable existing property\n\t\t\treturn false;\n\t\t}\n\n\t\t// property does not exist at all, or exists but is enumerable\n\t\tvar V = desc['[[Value]]'];\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tO[P] = V; // will use [[Define]]\n\t\treturn SameValue(O[P], V);\n\t}\n\tif (\n\t\thasArrayLengthDefineBug\n\t\t&& P === 'length'\n\t\t&& '[[Value]]' in desc\n\t\t&& isArray(O)\n\t\t&& O.length !== desc['[[Value]]']\n\t) {\n\t\t// eslint-disable-next-line no-param-reassign\n\t\tO.length = desc['[[Value]]'];\n\t\treturn O.length === desc['[[Value]]'];\n\t}\n\n\t$defineProperty(O, P, FromPropertyDescriptor(desc));\n\treturn true;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMzY4Mi5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQzs7QUFFQTtBQUNBO0FBQ0Esb0JBQW9CLFNBQVMsVUFBVTtBQUN2QyxHQUFHO0FBQ0g7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQSw2RkFBNkYsVUFBVTs7QUFFdkc7QUFDQSx5Q0FBeUMsbUJBQU8sQ0FBQyxJQUFpQixHQUFHOztBQUVyRSxnQkFBZ0IsbUJBQU8sQ0FBQyxJQUFxQjs7QUFFN0M7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxZQUFZO0FBQ1o7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC9oZWxwZXJzL0RlZmluZU93blByb3BlcnR5LmpzPzExOTQiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJGRlZmluZVByb3BlcnR5ID0gR2V0SW50cmluc2ljKCclT2JqZWN0LmRlZmluZVByb3BlcnR5JScsIHRydWUpO1xuXG5pZiAoJGRlZmluZVByb3BlcnR5KSB7XG5cdHRyeSB7XG5cdFx0JGRlZmluZVByb3BlcnR5KHt9LCAnYScsIHsgdmFsdWU6IDEgfSk7XG5cdH0gY2F0Y2ggKGUpIHtcblx0XHQvLyBJRSA4IGhhcyBhIGJyb2tlbiBkZWZpbmVQcm9wZXJ0eVxuXHRcdCRkZWZpbmVQcm9wZXJ0eSA9IG51bGw7XG5cdH1cbn1cblxuLy8gbm9kZSB2MC42IGhhcyBhIGJ1ZyB3aGVyZSBhcnJheSBsZW5ndGhzIGNhbiBiZSBTZXQgYnV0IG5vdCBEZWZpbmVkXG52YXIgaGFzQXJyYXlMZW5ndGhEZWZpbmVCdWcgPSBPYmplY3QuZGVmaW5lUHJvcGVydHkgJiYgT2JqZWN0LmRlZmluZVByb3BlcnR5KFtdLCAnbGVuZ3RoJywgeyB2YWx1ZTogMSB9KS5sZW5ndGggPT09IDA7XG5cbi8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBnbG9iYWwtcmVxdWlyZVxudmFyIGlzQXJyYXkgPSBoYXNBcnJheUxlbmd0aERlZmluZUJ1ZyAmJiByZXF1aXJlKCcuLi8yMDIwL0lzQXJyYXknKTsgLy8gdGhpcyBkb2VzIG5vdCBkZXBlbmQgb24gYW55IG90aGVyIEFPcy5cblxudmFyIGNhbGxCb3VuZCA9IHJlcXVpcmUoJ2NhbGwtYmluZC9jYWxsQm91bmQnKTtcblxudmFyICRpc0VudW1lcmFibGUgPSBjYWxsQm91bmQoJ09iamVjdC5wcm90b3R5cGUucHJvcGVydHlJc0VudW1lcmFibGUnKTtcblxuLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG1heC1wYXJhbXNcbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gRGVmaW5lT3duUHJvcGVydHkoSXNEYXRhRGVzY3JpcHRvciwgU2FtZVZhbHVlLCBGcm9tUHJvcGVydHlEZXNjcmlwdG9yLCBPLCBQLCBkZXNjKSB7XG5cdGlmICghJGRlZmluZVByb3BlcnR5KSB7XG5cdFx0aWYgKCFJc0RhdGFEZXNjcmlwdG9yKGRlc2MpKSB7XG5cdFx0XHQvLyBFUzMgZG9lcyBub3Qgc3VwcG9ydCBnZXR0ZXJzL3NldHRlcnNcblx0XHRcdHJldHVybiBmYWxzZTtcblx0XHR9XG5cdFx0aWYgKCFkZXNjWydbW0NvbmZpZ3VyYWJsZV1dJ10gfHwgIWRlc2NbJ1tbV3JpdGFibGVdXSddKSB7XG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0fVxuXG5cdFx0Ly8gZmFsbGJhY2sgZm9yIEVTM1xuXHRcdGlmIChQIGluIE8gJiYgJGlzRW51bWVyYWJsZShPLCBQKSAhPT0gISFkZXNjWydbW0VudW1lcmFibGVdXSddKSB7XG5cdFx0XHQvLyBhIG5vbi1lbnVtZXJhYmxlIGV4aXN0aW5nIHByb3BlcnR5XG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0fVxuXG5cdFx0Ly8gcHJvcGVydHkgZG9lcyBub3QgZXhpc3QgYXQgYWxsLCBvciBleGlzdHMgYnV0IGlzIGVudW1lcmFibGVcblx0XHR2YXIgViA9IGRlc2NbJ1tbVmFsdWVdXSddO1xuXHRcdC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1wYXJhbS1yZWFzc2lnblxuXHRcdE9bUF0gPSBWOyAvLyB3aWxsIHVzZSBbW0RlZmluZV1dXG5cdFx0cmV0dXJuIFNhbWVWYWx1ZShPW1BdLCBWKTtcblx0fVxuXHRpZiAoXG5cdFx0aGFzQXJyYXlMZW5ndGhEZWZpbmVCdWdcblx0XHQmJiBQID09PSAnbGVuZ3RoJ1xuXHRcdCYmICdbW1ZhbHVlXV0nIGluIGRlc2Ncblx0XHQmJiBpc0FycmF5KE8pXG5cdFx0JiYgTy5sZW5ndGggIT09IGRlc2NbJ1tbVmFsdWVdXSddXG5cdCkge1xuXHRcdC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1wYXJhbS1yZWFzc2lnblxuXHRcdE8ubGVuZ3RoID0gZGVzY1snW1tWYWx1ZV1dJ107XG5cdFx0cmV0dXJuIE8ubGVuZ3RoID09PSBkZXNjWydbW1ZhbHVlXV0nXTtcblx0fVxuXG5cdCRkZWZpbmVQcm9wZXJ0eShPLCBQLCBGcm9tUHJvcGVydHlEZXNjcmlwdG9yKGRlc2MpKTtcblx0cmV0dXJuIHRydWU7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///3682\n")},2188:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $TypeError = GetIntrinsic('%TypeError%');\nvar $SyntaxError = GetIntrinsic('%SyntaxError%');\n\nvar has = __webpack_require__(7642);\n\nvar predicates = {\n\t// https://262.ecma-international.org/6.0/#sec-property-descriptor-specification-type\n\t'Property Descriptor': function isPropertyDescriptor(Type, Desc) {\n\t\tif (Type(Desc) !== 'Object') {\n\t\t\treturn false;\n\t\t}\n\t\tvar allowed = {\n\t\t\t'[[Configurable]]': true,\n\t\t\t'[[Enumerable]]': true,\n\t\t\t'[[Get]]': true,\n\t\t\t'[[Set]]': true,\n\t\t\t'[[Value]]': true,\n\t\t\t'[[Writable]]': true\n\t\t};\n\n\t\tfor (var key in Desc) { // eslint-disable-line\n\t\t\tif (has(Desc, key) && !allowed[key]) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\tvar isData = has(Desc, '[[Value]]');\n\t\tvar IsAccessor = has(Desc, '[[Get]]') || has(Desc, '[[Set]]');\n\t\tif (isData && IsAccessor) {\n\t\t\tthrow new $TypeError('Property Descriptors may not be both accessor and data descriptors');\n\t\t}\n\t\treturn true;\n\t}\n};\n\nmodule.exports = function assertRecord(Type, recordType, argumentName, value) {\n\tvar predicate = predicates[recordType];\n\tif (typeof predicate !== 'function') {\n\t\tthrow new $SyntaxError('unknown record type: ' + recordType);\n\t}\n\tif (!predicate(Type, value)) {\n\t\tthrow new $TypeError(argumentName + ' must be a ' + recordType);\n\t}\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjE4OC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBOztBQUVBLFVBQVUsbUJBQU8sQ0FBQyxJQUFLOztBQUV2QjtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBLDBCQUEwQjtBQUMxQjtBQUNBO0FBQ0E7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvaGVscGVycy9hc3NlcnRSZWNvcmQuanM/NzlhMiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xudmFyICRTeW50YXhFcnJvciA9IEdldEludHJpbnNpYygnJVN5bnRheEVycm9yJScpO1xuXG52YXIgaGFzID0gcmVxdWlyZSgnaGFzJyk7XG5cbnZhciBwcmVkaWNhdGVzID0ge1xuXHQvLyBodHRwczovLzI2Mi5lY21hLWludGVybmF0aW9uYWwub3JnLzYuMC8jc2VjLXByb3BlcnR5LWRlc2NyaXB0b3Itc3BlY2lmaWNhdGlvbi10eXBlXG5cdCdQcm9wZXJ0eSBEZXNjcmlwdG9yJzogZnVuY3Rpb24gaXNQcm9wZXJ0eURlc2NyaXB0b3IoVHlwZSwgRGVzYykge1xuXHRcdGlmIChUeXBlKERlc2MpICE9PSAnT2JqZWN0Jykge1xuXHRcdFx0cmV0dXJuIGZhbHNlO1xuXHRcdH1cblx0XHR2YXIgYWxsb3dlZCA9IHtcblx0XHRcdCdbW0NvbmZpZ3VyYWJsZV1dJzogdHJ1ZSxcblx0XHRcdCdbW0VudW1lcmFibGVdXSc6IHRydWUsXG5cdFx0XHQnW1tHZXRdXSc6IHRydWUsXG5cdFx0XHQnW1tTZXRdXSc6IHRydWUsXG5cdFx0XHQnW1tWYWx1ZV1dJzogdHJ1ZSxcblx0XHRcdCdbW1dyaXRhYmxlXV0nOiB0cnVlXG5cdFx0fTtcblxuXHRcdGZvciAodmFyIGtleSBpbiBEZXNjKSB7IC8vIGVzbGludC1kaXNhYmxlLWxpbmVcblx0XHRcdGlmIChoYXMoRGVzYywga2V5KSAmJiAhYWxsb3dlZFtrZXldKSB7XG5cdFx0XHRcdHJldHVybiBmYWxzZTtcblx0XHRcdH1cblx0XHR9XG5cblx0XHR2YXIgaXNEYXRhID0gaGFzKERlc2MsICdbW1ZhbHVlXV0nKTtcblx0XHR2YXIgSXNBY2Nlc3NvciA9IGhhcyhEZXNjLCAnW1tHZXRdXScpIHx8IGhhcyhEZXNjLCAnW1tTZXRdXScpO1xuXHRcdGlmIChpc0RhdGEgJiYgSXNBY2Nlc3Nvcikge1xuXHRcdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoJ1Byb3BlcnR5IERlc2NyaXB0b3JzIG1heSBub3QgYmUgYm90aCBhY2Nlc3NvciBhbmQgZGF0YSBkZXNjcmlwdG9ycycpO1xuXHRcdH1cblx0XHRyZXR1cm4gdHJ1ZTtcblx0fVxufTtcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBhc3NlcnRSZWNvcmQoVHlwZSwgcmVjb3JkVHlwZSwgYXJndW1lbnROYW1lLCB2YWx1ZSkge1xuXHR2YXIgcHJlZGljYXRlID0gcHJlZGljYXRlc1tyZWNvcmRUeXBlXTtcblx0aWYgKHR5cGVvZiBwcmVkaWNhdGUgIT09ICdmdW5jdGlvbicpIHtcblx0XHR0aHJvdyBuZXcgJFN5bnRheEVycm9yKCd1bmtub3duIHJlY29yZCB0eXBlOiAnICsgcmVjb3JkVHlwZSk7XG5cdH1cblx0aWYgKCFwcmVkaWNhdGUoVHlwZSwgdmFsdWUpKSB7XG5cdFx0dGhyb3cgbmV3ICRUeXBlRXJyb3IoYXJndW1lbnROYW1lICsgJyBtdXN0IGJlIGEgJyArIHJlY29yZFR5cGUpO1xuXHR9XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///2188\n")},2633:function(module){"use strict";eval("\n\nvar $isNaN = Number.isNaN || function (a) { return a !== a; };\n\nmodule.exports = Number.isFinite || function (x) { return typeof x === 'number' && !$isNaN(x) && x !== Infinity && x !== -Infinity; };\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjYzMy5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYiw0Q0FBNEM7O0FBRTVDLG1EQUFtRCIsInNvdXJjZXMiOlsid2VicGFjazovL3JlYWRpdW0tanMvLi9ub2RlX21vZHVsZXMvZXMtYWJzdHJhY3QvaGVscGVycy9pc0Zpbml0ZS5qcz80YjU2Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyICRpc05hTiA9IE51bWJlci5pc05hTiB8fCBmdW5jdGlvbiAoYSkgeyByZXR1cm4gYSAhPT0gYTsgfTtcblxubW9kdWxlLmV4cG9ydHMgPSBOdW1iZXIuaXNGaW5pdGUgfHwgZnVuY3Rpb24gKHgpIHsgcmV0dXJuIHR5cGVvZiB4ID09PSAnbnVtYmVyJyAmJiAhJGlzTmFOKHgpICYmIHggIT09IEluZmluaXR5ICYmIHggIT09IC1JbmZpbml0eTsgfTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2633\n")},9544:function(module){"use strict";eval("\n\nmodule.exports = function isLeadingSurrogate(charCode) {\n\treturn typeof charCode === 'number' && charCode >= 0xD800 && charCode <= 0xDBFF;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTU0NC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvaXNMZWFkaW5nU3Vycm9nYXRlLmpzPzc0NzIiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIGlzTGVhZGluZ1N1cnJvZ2F0ZShjaGFyQ29kZSkge1xuXHRyZXR1cm4gdHlwZW9mIGNoYXJDb2RlID09PSAnbnVtYmVyJyAmJiBjaGFyQ29kZSA+PSAweEQ4MDAgJiYgY2hhckNvZGUgPD0gMHhEQkZGO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///9544\n")},9086:function(module){"use strict";eval("\n\nmodule.exports = Number.isNaN || function isNaN(a) {\n\treturn a !== a;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiOTA4Ni5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvaXNOYU4uanM/MGVjZSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbm1vZHVsZS5leHBvcnRzID0gTnVtYmVyLmlzTmFOIHx8IGZ1bmN0aW9uIGlzTmFOKGEpIHtcblx0cmV0dXJuIGEgIT09IGE7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///9086\n")},4790:function(module){"use strict";eval("\n\nmodule.exports = function isPrimitive(value) {\n\treturn value === null || (typeof value !== 'function' && typeof value !== 'object');\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNDc5MC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvaXNQcmltaXRpdmUuanM/NjJkZiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaXNQcmltaXRpdmUodmFsdWUpIHtcblx0cmV0dXJuIHZhbHVlID09PSBudWxsIHx8ICh0eXBlb2YgdmFsdWUgIT09ICdmdW5jdGlvbicgJiYgdHlwZW9mIHZhbHVlICE9PSAnb2JqZWN0Jyk7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///4790\n")},2435:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar has = __webpack_require__(7642);\nvar $TypeError = GetIntrinsic('%TypeError%');\n\nmodule.exports = function IsPropertyDescriptor(ES, Desc) {\n\tif (ES.Type(Desc) !== 'Object') {\n\t\treturn false;\n\t}\n\tvar allowed = {\n\t\t'[[Configurable]]': true,\n\t\t'[[Enumerable]]': true,\n\t\t'[[Get]]': true,\n\t\t'[[Set]]': true,\n\t\t'[[Value]]': true,\n\t\t'[[Writable]]': true\n\t};\n\n\tfor (var key in Desc) { // eslint-disable-line no-restricted-syntax\n\t\tif (has(Desc, key) && !allowed[key]) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tif (ES.IsDataDescriptor(Desc) && ES.IsAccessorDescriptor(Desc)) {\n\t\tthrow new $TypeError('Property Descriptors may not be both accessor and data descriptors');\n\t}\n\treturn true;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMjQzNS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQyxVQUFVLG1CQUFPLENBQUMsSUFBSztBQUN2Qjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUEseUJBQXlCO0FBQ3pCO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvaXNQcm9wZXJ0eURlc2NyaXB0b3IuanM/MDc4YiJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciBoYXMgPSByZXF1aXJlKCdoYXMnKTtcbnZhciAkVHlwZUVycm9yID0gR2V0SW50cmluc2ljKCclVHlwZUVycm9yJScpO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIElzUHJvcGVydHlEZXNjcmlwdG9yKEVTLCBEZXNjKSB7XG5cdGlmIChFUy5UeXBlKERlc2MpICE9PSAnT2JqZWN0Jykge1xuXHRcdHJldHVybiBmYWxzZTtcblx0fVxuXHR2YXIgYWxsb3dlZCA9IHtcblx0XHQnW1tDb25maWd1cmFibGVdXSc6IHRydWUsXG5cdFx0J1tbRW51bWVyYWJsZV1dJzogdHJ1ZSxcblx0XHQnW1tHZXRdXSc6IHRydWUsXG5cdFx0J1tbU2V0XV0nOiB0cnVlLFxuXHRcdCdbW1ZhbHVlXV0nOiB0cnVlLFxuXHRcdCdbW1dyaXRhYmxlXV0nOiB0cnVlXG5cdH07XG5cblx0Zm9yICh2YXIga2V5IGluIERlc2MpIHsgLy8gZXNsaW50LWRpc2FibGUtbGluZSBuby1yZXN0cmljdGVkLXN5bnRheFxuXHRcdGlmIChoYXMoRGVzYywga2V5KSAmJiAhYWxsb3dlZFtrZXldKSB7XG5cdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0fVxuXHR9XG5cblx0aWYgKEVTLklzRGF0YURlc2NyaXB0b3IoRGVzYykgJiYgRVMuSXNBY2Nlc3NvckRlc2NyaXB0b3IoRGVzYykpIHtcblx0XHR0aHJvdyBuZXcgJFR5cGVFcnJvcignUHJvcGVydHkgRGVzY3JpcHRvcnMgbWF5IG5vdCBiZSBib3RoIGFjY2Vzc29yIGFuZCBkYXRhIGRlc2NyaXB0b3JzJyk7XG5cdH1cblx0cmV0dXJuIHRydWU7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///2435\n")},5424:function(module){"use strict";eval("\n\nmodule.exports = function isTrailingSurrogate(charCode) {\n\treturn typeof charCode === 'number' && charCode >= 0xDC00 && charCode <= 0xDFFF;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNTQyNC5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvaXNUcmFpbGluZ1N1cnJvZ2F0ZS5qcz82ZTE0Il0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBpc1RyYWlsaW5nU3Vycm9nYXRlKGNoYXJDb2RlKSB7XG5cdHJldHVybiB0eXBlb2YgY2hhckNvZGUgPT09ICdudW1iZXInICYmIGNoYXJDb2RlID49IDB4REMwMCAmJiBjaGFyQ29kZSA8PSAweERGRkY7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///5424\n")},1645:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $Math = GetIntrinsic('%Math%');\nvar $Number = GetIntrinsic('%Number%');\n\nmodule.exports = $Number.MAX_SAFE_INTEGER || $Math.pow(2, 53) - 1;\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMTY0NS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYixtQkFBbUIsbUJBQU8sQ0FBQyxHQUFlOztBQUUxQztBQUNBOztBQUVBIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vcmVhZGl1bS1qcy8uL25vZGVfbW9kdWxlcy9lcy1hYnN0cmFjdC9oZWxwZXJzL21heFNhZmVJbnRlZ2VyLmpzP2YzOTkiXSwic291cmNlc0NvbnRlbnQiOlsiJ3VzZSBzdHJpY3QnO1xuXG52YXIgR2V0SW50cmluc2ljID0gcmVxdWlyZSgnZ2V0LWludHJpbnNpYycpO1xuXG52YXIgJE1hdGggPSBHZXRJbnRyaW5zaWMoJyVNYXRoJScpO1xudmFyICROdW1iZXIgPSBHZXRJbnRyaW5zaWMoJyVOdW1iZXIlJyk7XG5cbm1vZHVsZS5leHBvcnRzID0gJE51bWJlci5NQVhfU0FGRV9JTlRFR0VSIHx8ICRNYXRoLnBvdygyLCA1MykgLSAxO1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///1645\n")},823:function(module,__unused_webpack_exports,__webpack_require__){"use strict";eval("\n\nvar GetIntrinsic = __webpack_require__(210);\n\nvar $test = GetIntrinsic('RegExp.prototype.test');\n\nvar callBind = __webpack_require__(5559);\n\nmodule.exports = function regexTester(regex) {\n\treturn callBind($test, regex);\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODIzLmpzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLG1CQUFtQixtQkFBTyxDQUFDLEdBQWU7O0FBRTFDOztBQUVBLGVBQWUsbUJBQU8sQ0FBQyxJQUFXOztBQUVsQztBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvcmVnZXhUZXN0ZXIuanM/YWVjOSJdLCJzb3VyY2VzQ29udGVudCI6WyIndXNlIHN0cmljdCc7XG5cbnZhciBHZXRJbnRyaW5zaWMgPSByZXF1aXJlKCdnZXQtaW50cmluc2ljJyk7XG5cbnZhciAkdGVzdCA9IEdldEludHJpbnNpYygnUmVnRXhwLnByb3RvdHlwZS50ZXN0Jyk7XG5cbnZhciBjYWxsQmluZCA9IHJlcXVpcmUoJ2NhbGwtYmluZCcpO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIHJlZ2V4VGVzdGVyKHJlZ2V4KSB7XG5cdHJldHVybiBjYWxsQmluZCgkdGVzdCwgcmVnZXgpO1xufTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///823\n")},8111:function(module){"use strict";eval("\n\nmodule.exports = function sign(number) {\n\treturn number >= 0 ? 1 : -1;\n};\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODExMS5qcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9yZWFkaXVtLWpzLy4vbm9kZV9tb2R1bGVzL2VzLWFic3RyYWN0L2hlbHBlcnMvc2lnbi5qcz80MjdlIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBzaWduKG51bWJlcikge1xuXHRyZXR1cm4gbnVtYmVyID49IDAgPyAxIDogLTE7XG59O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///8111\n")}},__webpack_module_cache__={};function __webpack_require__(Q){var t=__webpack_module_cache__[Q];if(void 0!==t)return t.exports;var n=__webpack_module_cache__[Q]={exports:{}};return __webpack_modules__[Q](n,n.exports,__webpack_require__),n.exports}__webpack_require__.n=function(Q){var t=Q&&Q.__esModule?function(){return Q.default}:function(){return Q};return __webpack_require__.d(t,{a:t}),t},__webpack_require__.d=function(Q,t){for(var n in t)__webpack_require__.o(t,n)&&!__webpack_require__.o(Q,n)&&Object.defineProperty(Q,n,{enumerable:!0,get:t[n]})},__webpack_require__.o=function(Q,t){return Object.prototype.hasOwnProperty.call(Q,t)};var __webpack_exports__=__webpack_require__(5232)})(); \ No newline at end of file +!function(){var u={1844:function(u,t){"use strict";function e(u){return u.split("").reverse().join("")}function r(u){return(u|-u)>>31&1}function n(u,t,e,n){var o=u.P[e],D=u.M[e],i=n>>>31,a=t[e]|i,F=a|D,c=(a&o)+o^o|a,l=D|~(c|o),f=o&c,s=r(l&u.lastRowMask[e])-r(f&u.lastRowMask[e]);return l<<=1,f<<=1,o=(f|=i)|~(F|(l|=r(n)-i)),D=l&F,u.P[e]=o,u.M[e]=D,s}function o(u,t,e){if(0===t.length)return[];e=Math.min(e,t.length);var r=[],o=32,D=Math.ceil(t.length/o)-1,i={P:new Uint32Array(D+1),M:new Uint32Array(D+1),lastRowMask:new Uint32Array(D+1)};i.lastRowMask.fill(1<<31),i.lastRowMask[D]=1<<(t.length-1)%o;for(var a=new Uint32Array(D+1),F=new Map,c=[],l=0;l<256;l++)c.push(a);for(var f=0;f<t.length;f+=1){var s=t.charCodeAt(f);if(!F.has(s)){var A=new Uint32Array(D+1);F.set(s,A),s<c.length&&(c[s]=A);for(var p=0;p<=D;p+=1){A[p]=0;for(var E=0;E<o;E+=1){var C=p*o+E;C>=t.length||t.charCodeAt(C)===s&&(A[p]|=1<<E)}}}}var y=Math.max(0,Math.ceil(e/o)-1),d=new Uint32Array(D+1);for(p=0;p<=y;p+=1)d[p]=(p+1)*o;for(d[D]=t.length,p=0;p<=y;p+=1)i.P[p]=-1,i.M[p]=0;for(var B=0;B<u.length;B+=1){var h=u.charCodeAt(B);A=void 0,h<c.length?A=c[h]:void 0===(A=F.get(h))&&(A=a);var g=0;for(p=0;p<=y;p+=1)g=n(i,A,p,g),d[p]+=g;if(d[y]-g<=e&&y<D&&(1&A[y+1]||g<0)){y+=1,i.P[y]=-1,i.M[y]=0;var m=y===D?t.length%o:o;d[y]=d[y-1]+m-g+n(i,A,y,g)}else for(;y>0&&d[y]>=e+o;)y-=1;y===D&&d[y]<=e&&(d[y]<e&&r.splice(0,r.length),r.push({start:-1,end:B+1,errors:d[y]}),e=d[y])}return r}t.Z=function(u,t,r){return function(u,t,r){var n=e(t);return r.map((function(r){var D=Math.max(0,r.end-t.length-r.errors);return{start:o(e(u.slice(D,r.end)),n,r.errors).reduce((function(u,t){return r.end-t.end<u?r.end-t.end:u}),r.end),end:r.end,errors:r.errors}}))}(u,t,o(u,t,r))}},3099:function(u,t,e){"use strict";var r=e(2870),n=e(2755),o=n(r("String.prototype.indexOf"));u.exports=function(u,t){var e=r(u,!!t);return"function"==typeof e&&o(u,".prototype.")>-1?n(e):e}},2755:function(u,t,e){"use strict";var r=e(3569),n=e(2870),o=n("%Function.prototype.apply%"),D=n("%Function.prototype.call%"),i=n("%Reflect.apply%",!0)||r.call(D,o),a=n("%Object.getOwnPropertyDescriptor%",!0),F=n("%Object.defineProperty%",!0),c=n("%Math.max%");if(F)try{F({},"a",{value:1})}catch(u){F=null}u.exports=function(u){var t=i(r,D,arguments);return a&&F&&a(t,"length").configurable&&F(t,"length",{value:1+c(0,u.length-(arguments.length-1))}),t};var l=function(){return i(r,o,arguments)};F?F(u.exports,"apply",{value:l}):u.exports.apply=l},6663:function(u,t,e){"use strict";var r=e(229)(),n=e(2870),o=r&&n("%Object.defineProperty%",!0),D=n("%SyntaxError%"),i=n("%TypeError%"),a=e(658);u.exports=function(u,t,e){if(!u||"object"!=typeof u&&"function"!=typeof u)throw new i("`obj` must be an object or a function`");if("string"!=typeof t&&"symbol"!=typeof t)throw new i("`property` must be a string or a symbol`");if(arguments.length>3&&"boolean"!=typeof arguments[3]&&null!==arguments[3])throw new i("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&"boolean"!=typeof arguments[4]&&null!==arguments[4])throw new i("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&"boolean"!=typeof arguments[5]&&null!==arguments[5])throw new i("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&"boolean"!=typeof arguments[6])throw new i("`loose`, if provided, must be a boolean");var r=arguments.length>3?arguments[3]:null,n=arguments.length>4?arguments[4]:null,F=arguments.length>5?arguments[5]:null,c=arguments.length>6&&arguments[6],l=!!a&&a(u,t);if(o)o(u,t,{configurable:null===F&&l?l.configurable:!F,enumerable:null===r&&l?l.enumerable:!r,value:e,writable:null===n&&l?l.writable:!n});else{if(!c&&(r||n||F))throw new D("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.");u[t]=e}}},9722:function(u,t,e){"use strict";var r=e(2051),n="function"==typeof Symbol&&"symbol"==typeof Symbol("foo"),o=Object.prototype.toString,D=Array.prototype.concat,i=e(6663),a=e(229)(),F=function(u,t,e,r){if(t in u)if(!0===r){if(u[t]===e)return}else if("function"!=typeof(n=r)||"[object Function]"!==o.call(n)||!r())return;var n;a?i(u,t,e,!0):i(u,t,e)},c=function(u,t){var e=arguments.length>2?arguments[2]:{},o=r(t);n&&(o=D.call(o,Object.getOwnPropertySymbols(t)));for(var i=0;i<o.length;i+=1)F(u,o[i],t[o[i]],e[o[i]])};c.supportsDescriptors=!!a,u.exports=c},2263:function(u,t,e){"use strict";var r=e(2870)("%Object.defineProperty%",!0),n=e(3060)(),o=e(9545),D=n?Symbol.toStringTag:null;u.exports=function(u,t){var e=arguments.length>2&&arguments[2]&&arguments[2].force;!D||!e&&o(u,D)||(r?r(u,D,{configurable:!0,enumerable:!1,value:t,writable:!1}):u[D]=t)}},7358:function(u,t,e){"use strict";var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator,n=e(7959),o=e(3655),D=e(455),i=e(8760);u.exports=function(u){if(n(u))return u;var t,e="default";if(arguments.length>1&&(arguments[1]===String?e="string":arguments[1]===Number&&(e="number")),r&&(Symbol.toPrimitive?t=function(u,t){var e=u[t];if(null!=e){if(!o(e))throw new TypeError(e+" returned for property "+t+" of object "+u+" is not a function");return e}}(u,Symbol.toPrimitive):i(u)&&(t=Symbol.prototype.valueOf)),void 0!==t){var a=t.call(u,e);if(n(a))return a;throw new TypeError("unable to convert exotic object to primitive")}return"default"===e&&(D(u)||i(u))&&(e="string"),function(u,t){if(null==u)throw new TypeError("Cannot call method on "+u);if("string"!=typeof t||"number"!==t&&"string"!==t)throw new TypeError('hint must be "string" or "number"');var e,r,D,i="string"===t?["toString","valueOf"]:["valueOf","toString"];for(D=0;D<i.length;++D)if(e=u[i[D]],o(e)&&(r=e.call(u),n(r)))return r;throw new TypeError("No default value")}(u,"default"===e?"number":e)}},7959:function(u){"use strict";u.exports=function(u){return null===u||"function"!=typeof u&&"object"!=typeof u}},8640:function(u){"use strict";var t=Array.prototype.slice,e=Object.prototype.toString;u.exports=function(u){var r=this;if("function"!=typeof r||"[object Function]"!==e.call(r))throw new TypeError("Function.prototype.bind called on incompatible "+r);for(var n,o=t.call(arguments,1),D=Math.max(0,r.length-o.length),i=[],a=0;a<D;a++)i.push("$"+a);if(n=Function("binder","return function ("+i.join(",")+"){ return binder.apply(this,arguments); }")((function(){if(this instanceof n){var e=r.apply(this,o.concat(t.call(arguments)));return Object(e)===e?e:this}return r.apply(u,o.concat(t.call(arguments)))})),r.prototype){var F=function(){};F.prototype=r.prototype,n.prototype=new F,F.prototype=null}return n}},3569:function(u,t,e){"use strict";var r=e(8640);u.exports=Function.prototype.bind||r},5610:function(u){"use strict";var t=function(){return"string"==typeof function(){}.name},e=Object.getOwnPropertyDescriptor;if(e)try{e([],"length")}catch(u){e=null}t.functionsHaveConfigurableNames=function(){if(!t()||!e)return!1;var u=e((function(){}),"name");return!!u&&!!u.configurable};var r=Function.prototype.bind;t.boundFunctionsHaveNames=function(){return t()&&"function"==typeof r&&""!==function(){}.bind().name},u.exports=t},2870:function(u,t,e){"use strict";var r,n=SyntaxError,o=Function,D=TypeError,i=function(u){try{return o('"use strict"; return ('+u+").constructor;")()}catch(u){}},a=Object.getOwnPropertyDescriptor;if(a)try{a({},"")}catch(u){a=null}var F=function(){throw new D},c=a?function(){try{return F}catch(u){try{return a(arguments,"callee").get}catch(u){return F}}}():F,l=e(1143)(),f=e(3413)(),s=Object.getPrototypeOf||(f?function(u){return u.__proto__}:null),A={},p="undefined"!=typeof Uint8Array&&s?s(Uint8Array):r,E={"%AggregateError%":"undefined"==typeof AggregateError?r:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?r:ArrayBuffer,"%ArrayIteratorPrototype%":l&&s?s([][Symbol.iterator]()):r,"%AsyncFromSyncIteratorPrototype%":r,"%AsyncFunction%":A,"%AsyncGenerator%":A,"%AsyncGeneratorFunction%":A,"%AsyncIteratorPrototype%":A,"%Atomics%":"undefined"==typeof Atomics?r:Atomics,"%BigInt%":"undefined"==typeof BigInt?r:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?r:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?r:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?r:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":Error,"%eval%":eval,"%EvalError%":EvalError,"%Float32Array%":"undefined"==typeof Float32Array?r:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?r:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?r:FinalizationRegistry,"%Function%":o,"%GeneratorFunction%":A,"%Int8Array%":"undefined"==typeof Int8Array?r:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?r:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?r:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":l&&s?s(s([][Symbol.iterator]())):r,"%JSON%":"object"==typeof JSON?JSON:r,"%Map%":"undefined"==typeof Map?r:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&l&&s?s((new Map)[Symbol.iterator]()):r,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?r:Promise,"%Proxy%":"undefined"==typeof Proxy?r:Proxy,"%RangeError%":RangeError,"%ReferenceError%":ReferenceError,"%Reflect%":"undefined"==typeof Reflect?r:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?r:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&l&&s?s((new Set)[Symbol.iterator]()):r,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?r:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":l&&s?s(""[Symbol.iterator]()):r,"%Symbol%":l?Symbol:r,"%SyntaxError%":n,"%ThrowTypeError%":c,"%TypedArray%":p,"%TypeError%":D,"%Uint8Array%":"undefined"==typeof Uint8Array?r:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?r:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?r:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?r:Uint32Array,"%URIError%":URIError,"%WeakMap%":"undefined"==typeof WeakMap?r:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?r:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?r:WeakSet};if(s)try{null.error}catch(u){var C=s(s(u));E["%Error.prototype%"]=C}var y=function u(t){var e;if("%AsyncFunction%"===t)e=i("async function () {}");else if("%GeneratorFunction%"===t)e=i("function* () {}");else if("%AsyncGeneratorFunction%"===t)e=i("async function* () {}");else if("%AsyncGenerator%"===t){var r=u("%AsyncGeneratorFunction%");r&&(e=r.prototype)}else if("%AsyncIteratorPrototype%"===t){var n=u("%AsyncGenerator%");n&&s&&(e=s(n.prototype))}return E[t]=e,e},d={"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},B=e(3569),h=e(9545),g=B.call(Function.call,Array.prototype.concat),m=B.call(Function.apply,Array.prototype.splice),b=B.call(Function.call,String.prototype.replace),v=B.call(Function.call,String.prototype.slice),w=B.call(Function.call,RegExp.prototype.exec),S=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,x=/\\(\\)?/g,O=function(u,t){var e,r=u;if(h(d,r)&&(r="%"+(e=d[r])[0]+"%"),h(E,r)){var o=E[r];if(o===A&&(o=y(r)),void 0===o&&!t)throw new D("intrinsic "+u+" exists, but is not available. Please file an issue!");return{alias:e,name:r,value:o}}throw new n("intrinsic "+u+" does not exist!")};u.exports=function(u,t){if("string"!=typeof u||0===u.length)throw new D("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof t)throw new D('"allowMissing" argument must be a boolean');if(null===w(/^%?[^%]*%?$/,u))throw new n("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var e=function(u){var t=v(u,0,1),e=v(u,-1);if("%"===t&&"%"!==e)throw new n("invalid intrinsic syntax, expected closing `%`");if("%"===e&&"%"!==t)throw new n("invalid intrinsic syntax, expected opening `%`");var r=[];return b(u,S,(function(u,t,e,n){r[r.length]=e?b(n,x,"$1"):t||u})),r}(u),r=e.length>0?e[0]:"",o=O("%"+r+"%",t),i=o.name,F=o.value,c=!1,l=o.alias;l&&(r=l[0],m(e,g([0,1],l)));for(var f=1,s=!0;f<e.length;f+=1){var A=e[f],p=v(A,0,1),C=v(A,-1);if(('"'===p||"'"===p||"`"===p||'"'===C||"'"===C||"`"===C)&&p!==C)throw new n("property names with quotes must have matching quotes");if("constructor"!==A&&s||(c=!0),h(E,i="%"+(r+="."+A)+"%"))F=E[i];else if(null!=F){if(!(A in F)){if(!t)throw new D("base intrinsic for "+u+" exists, but the property is not available.");return}if(a&&f+1>=e.length){var y=a(F,A);F=(s=!!y)&&"get"in y&&!("originalValue"in y.get)?y.get:F[A]}else s=h(F,A),F=F[A];s&&!c&&(E[i]=F)}}return F}},658:function(u,t,e){"use strict";var r=e(2870)("%Object.getOwnPropertyDescriptor%",!0);if(r)try{r([],"length")}catch(u){r=null}u.exports=r},229:function(u,t,e){"use strict";var r=e(2870)("%Object.defineProperty%",!0),n=function(){if(r)try{return r({},"a",{value:1}),!0}catch(u){return!1}return!1};n.hasArrayLengthDefineBug=function(){if(!n())return null;try{return 1!==r([],"length",{value:1}).length}catch(u){return!0}},u.exports=n},3413:function(u){"use strict";var t={foo:{}},e=Object;u.exports=function(){return{__proto__:t}.foo===t.foo&&!({__proto__:null}instanceof e)}},1143:function(u,t,e){"use strict";var r="undefined"!=typeof Symbol&&Symbol,n=e(9985);u.exports=function(){return"function"==typeof r&&"function"==typeof Symbol&&"symbol"==typeof r("foo")&&"symbol"==typeof Symbol("bar")&&n()}},9985:function(u){"use strict";u.exports=function(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var u={},t=Symbol("test"),e=Object(t);if("string"==typeof t)return!1;if("[object Symbol]"!==Object.prototype.toString.call(t))return!1;if("[object Symbol]"!==Object.prototype.toString.call(e))return!1;for(t in u[t]=42,u)return!1;if("function"==typeof Object.keys&&0!==Object.keys(u).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(u).length)return!1;var r=Object.getOwnPropertySymbols(u);if(1!==r.length||r[0]!==t)return!1;if(!Object.prototype.propertyIsEnumerable.call(u,t))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var n=Object.getOwnPropertyDescriptor(u,t);if(42!==n.value||!0!==n.enumerable)return!1}return!0}},3060:function(u,t,e){"use strict";var r=e(9985);u.exports=function(){return r()&&!!Symbol.toStringTag}},9545:function(u){"use strict";var t={}.hasOwnProperty,e=Function.prototype.call;u.exports=e.bind?e.bind(t):function(u,r){return e.call(t,u,r)}},7284:function(u,t,e){"use strict";var r=e(2870),n=e(9545),o=e(5714)(),D=r("%TypeError%"),i={assert:function(u,t){if(!u||"object"!=typeof u&&"function"!=typeof u)throw new D("`O` is not an object");if("string"!=typeof t)throw new D("`slot` must be a string");if(o.assert(u),!i.has(u,t))throw new D("`"+t+"` is not present on `O`")},get:function(u,t){if(!u||"object"!=typeof u&&"function"!=typeof u)throw new D("`O` is not an object");if("string"!=typeof t)throw new D("`slot` must be a string");var e=o.get(u);return e&&e["$"+t]},has:function(u,t){if(!u||"object"!=typeof u&&"function"!=typeof u)throw new D("`O` is not an object");if("string"!=typeof t)throw new D("`slot` must be a string");var e=o.get(u);return!!e&&n(e,"$"+t)},set:function(u,t,e){if(!u||"object"!=typeof u&&"function"!=typeof u)throw new D("`O` is not an object");if("string"!=typeof t)throw new D("`slot` must be a string");var r=o.get(u);r||(r={},o.set(u,r)),r["$"+t]=e}};Object.freeze&&Object.freeze(i),u.exports=i},3655:function(u){"use strict";var t,e,r=Function.prototype.toString,n="object"==typeof Reflect&&null!==Reflect&&Reflect.apply;if("function"==typeof n&&"function"==typeof Object.defineProperty)try{t=Object.defineProperty({},"length",{get:function(){throw e}}),e={},n((function(){throw 42}),null,t)}catch(u){u!==e&&(n=null)}else n=null;var o=/^\s*class\b/,D=function(u){try{var t=r.call(u);return o.test(t)}catch(u){return!1}},i=function(u){try{return!D(u)&&(r.call(u),!0)}catch(u){return!1}},a=Object.prototype.toString,F="function"==typeof Symbol&&!!Symbol.toStringTag,c=!(0 in[,]),l=function(){return!1};if("object"==typeof document){var f=document.all;a.call(f)===a.call(document.all)&&(l=function(u){if((c||!u)&&(void 0===u||"object"==typeof u))try{var t=a.call(u);return("[object HTMLAllCollection]"===t||"[object HTML document.all class]"===t||"[object HTMLCollection]"===t||"[object Object]"===t)&&null==u("")}catch(u){}return!1})}u.exports=n?function(u){if(l(u))return!0;if(!u)return!1;if("function"!=typeof u&&"object"!=typeof u)return!1;try{n(u,null,t)}catch(u){if(u!==e)return!1}return!D(u)&&i(u)}:function(u){if(l(u))return!0;if(!u)return!1;if("function"!=typeof u&&"object"!=typeof u)return!1;if(F)return i(u);if(D(u))return!1;var t=a.call(u);return!("[object Function]"!==t&&"[object GeneratorFunction]"!==t&&!/^\[object HTML/.test(t))&&i(u)}},455:function(u,t,e){"use strict";var r=Date.prototype.getDay,n=Object.prototype.toString,o=e(3060)();u.exports=function(u){return"object"==typeof u&&null!==u&&(o?function(u){try{return r.call(u),!0}catch(u){return!1}}(u):"[object Date]"===n.call(u))}},5494:function(u,t,e){"use strict";var r,n,o,D,i=e(3099),a=e(3060)();if(a){r=i("Object.prototype.hasOwnProperty"),n=i("RegExp.prototype.exec"),o={};var F=function(){throw o};D={toString:F,valueOf:F},"symbol"==typeof Symbol.toPrimitive&&(D[Symbol.toPrimitive]=F)}var c=i("Object.prototype.toString"),l=Object.getOwnPropertyDescriptor;u.exports=a?function(u){if(!u||"object"!=typeof u)return!1;var t=l(u,"lastIndex");if(!t||!r(t,"value"))return!1;try{n(u,D)}catch(u){return u===o}}:function(u){return!(!u||"object"!=typeof u&&"function"!=typeof u)&&"[object RegExp]"===c(u)}},8760:function(u,t,e){"use strict";var r=Object.prototype.toString;if(e(1143)()){var n=Symbol.prototype.toString,o=/^Symbol\(.*\)$/;u.exports=function(u){if("symbol"==typeof u)return!0;if("[object Symbol]"!==r.call(u))return!1;try{return function(u){return"symbol"==typeof u.valueOf()&&o.test(n.call(u))}(u)}catch(u){return!1}}}else u.exports=function(u){return!1}},4538:function(u,t,e){var r="function"==typeof Map&&Map.prototype,n=Object.getOwnPropertyDescriptor&&r?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,o=r&&n&&"function"==typeof n.get?n.get:null,D=r&&Map.prototype.forEach,i="function"==typeof Set&&Set.prototype,a=Object.getOwnPropertyDescriptor&&i?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,F=i&&a&&"function"==typeof a.get?a.get:null,c=i&&Set.prototype.forEach,l="function"==typeof WeakMap&&WeakMap.prototype?WeakMap.prototype.has:null,f="function"==typeof WeakSet&&WeakSet.prototype?WeakSet.prototype.has:null,s="function"==typeof WeakRef&&WeakRef.prototype?WeakRef.prototype.deref:null,A=Boolean.prototype.valueOf,p=Object.prototype.toString,E=Function.prototype.toString,C=String.prototype.match,y=String.prototype.slice,d=String.prototype.replace,B=String.prototype.toUpperCase,h=String.prototype.toLowerCase,g=RegExp.prototype.test,m=Array.prototype.concat,b=Array.prototype.join,v=Array.prototype.slice,w=Math.floor,S="function"==typeof BigInt?BigInt.prototype.valueOf:null,x=Object.getOwnPropertySymbols,O="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,j="function"==typeof Symbol&&"object"==typeof Symbol.iterator,P="function"==typeof Symbol&&Symbol.toStringTag&&(Symbol.toStringTag,1)?Symbol.toStringTag:null,T=Object.prototype.propertyIsEnumerable,I=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(u){return u.__proto__}:null);function R(u,t){if(u===1/0||u===-1/0||u!=u||u&&u>-1e3&&u<1e3||g.call(/e/,t))return t;var e=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof u){var r=u<0?-w(-u):w(u);if(r!==u){var n=String(r),o=y.call(t,n.length+1);return d.call(n,e,"$&_")+"."+d.call(d.call(o,/([0-9]{3})/g,"$&_"),/_$/,"")}}return d.call(t,e,"$&_")}var N=e(7002),M=N.custom,k=W(M)?M:null;function $(u,t,e){var r="double"===(e.quoteStyle||t)?'"':"'";return r+u+r}function L(u){return d.call(String(u),/"/g,""")}function U(u){return!("[object Array]"!==H(u)||P&&"object"==typeof u&&P in u)}function _(u){return!("[object RegExp]"!==H(u)||P&&"object"==typeof u&&P in u)}function W(u){if(j)return u&&"object"==typeof u&&u instanceof Symbol;if("symbol"==typeof u)return!0;if(!u||"object"!=typeof u||!O)return!1;try{return O.call(u),!0}catch(u){}return!1}u.exports=function u(t,e,r,n){var i=e||{};if(V(i,"quoteStyle")&&"single"!==i.quoteStyle&&"double"!==i.quoteStyle)throw new TypeError('option "quoteStyle" must be "single" or "double"');if(V(i,"maxStringLength")&&("number"==typeof i.maxStringLength?i.maxStringLength<0&&i.maxStringLength!==1/0:null!==i.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var a=!V(i,"customInspect")||i.customInspect;if("boolean"!=typeof a&&"symbol"!==a)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(V(i,"indent")&&null!==i.indent&&"\t"!==i.indent&&!(parseInt(i.indent,10)===i.indent&&i.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(V(i,"numericSeparator")&&"boolean"!=typeof i.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var p=i.numericSeparator;if(void 0===t)return"undefined";if(null===t)return"null";if("boolean"==typeof t)return t?"true":"false";if("string"==typeof t)return X(t,i);if("number"==typeof t){if(0===t)return 1/0/t>0?"0":"-0";var B=String(t);return p?R(t,B):B}if("bigint"==typeof t){var g=String(t)+"n";return p?R(t,g):g}var w=void 0===i.depth?5:i.depth;if(void 0===r&&(r=0),r>=w&&w>0&&"object"==typeof t)return U(t)?"[Array]":"[Object]";var x,M=function(u,t){var e;if("\t"===u.indent)e="\t";else{if(!("number"==typeof u.indent&&u.indent>0))return null;e=b.call(Array(u.indent+1)," ")}return{base:e,prev:b.call(Array(t+1),e)}}(i,r);if(void 0===n)n=[];else if(q(n,t)>=0)return"[Circular]";function G(t,e,o){if(e&&(n=v.call(n)).push(e),o){var D={depth:i.depth};return V(i,"quoteStyle")&&(D.quoteStyle=i.quoteStyle),u(t,D,r+1,n)}return u(t,i,r+1,n)}if("function"==typeof t&&!_(t)){var Y=function(u){if(u.name)return u.name;var t=C.call(E.call(u),/^function\s*([\w$]+)/);return t?t[1]:null}(t),uu=Q(t,G);return"[Function"+(Y?": "+Y:" (anonymous)")+"]"+(uu.length>0?" { "+b.call(uu,", ")+" }":"")}if(W(t)){var tu=j?d.call(String(t),/^(Symbol\(.*\))_[^)]*$/,"$1"):O.call(t);return"object"!=typeof t||j?tu:z(tu)}if((x=t)&&"object"==typeof x&&("undefined"!=typeof HTMLElement&&x instanceof HTMLElement||"string"==typeof x.nodeName&&"function"==typeof x.getAttribute)){for(var eu="<"+h.call(String(t.nodeName)),ru=t.attributes||[],nu=0;nu<ru.length;nu++)eu+=" "+ru[nu].name+"="+$(L(ru[nu].value),"double",i);return eu+=">",t.childNodes&&t.childNodes.length&&(eu+="..."),eu+"</"+h.call(String(t.nodeName))+">"}if(U(t)){if(0===t.length)return"[]";var ou=Q(t,G);return M&&!function(u){for(var t=0;t<u.length;t++)if(q(u[t],"\n")>=0)return!1;return!0}(ou)?"["+Z(ou,M)+"]":"[ "+b.call(ou,", ")+" ]"}if(function(u){return!("[object Error]"!==H(u)||P&&"object"==typeof u&&P in u)}(t)){var Du=Q(t,G);return"cause"in Error.prototype||!("cause"in t)||T.call(t,"cause")?0===Du.length?"["+String(t)+"]":"{ ["+String(t)+"] "+b.call(Du,", ")+" }":"{ ["+String(t)+"] "+b.call(m.call("[cause]: "+G(t.cause),Du),", ")+" }"}if("object"==typeof t&&a){if(k&&"function"==typeof t[k]&&N)return N(t,{depth:w-r});if("symbol"!==a&&"function"==typeof t.inspect)return t.inspect()}if(function(u){if(!o||!u||"object"!=typeof u)return!1;try{o.call(u);try{F.call(u)}catch(u){return!0}return u instanceof Map}catch(u){}return!1}(t)){var iu=[];return D&&D.call(t,(function(u,e){iu.push(G(e,t,!0)+" => "+G(u,t))})),K("Map",o.call(t),iu,M)}if(function(u){if(!F||!u||"object"!=typeof u)return!1;try{F.call(u);try{o.call(u)}catch(u){return!0}return u instanceof Set}catch(u){}return!1}(t)){var au=[];return c&&c.call(t,(function(u){au.push(G(u,t))})),K("Set",F.call(t),au,M)}if(function(u){if(!l||!u||"object"!=typeof u)return!1;try{l.call(u,l);try{f.call(u,f)}catch(u){return!0}return u instanceof WeakMap}catch(u){}return!1}(t))return J("WeakMap");if(function(u){if(!f||!u||"object"!=typeof u)return!1;try{f.call(u,f);try{l.call(u,l)}catch(u){return!0}return u instanceof WeakSet}catch(u){}return!1}(t))return J("WeakSet");if(function(u){if(!s||!u||"object"!=typeof u)return!1;try{return s.call(u),!0}catch(u){}return!1}(t))return J("WeakRef");if(function(u){return!("[object Number]"!==H(u)||P&&"object"==typeof u&&P in u)}(t))return z(G(Number(t)));if(function(u){if(!u||"object"!=typeof u||!S)return!1;try{return S.call(u),!0}catch(u){}return!1}(t))return z(G(S.call(t)));if(function(u){return!("[object Boolean]"!==H(u)||P&&"object"==typeof u&&P in u)}(t))return z(A.call(t));if(function(u){return!("[object String]"!==H(u)||P&&"object"==typeof u&&P in u)}(t))return z(G(String(t)));if(!function(u){return!("[object Date]"!==H(u)||P&&"object"==typeof u&&P in u)}(t)&&!_(t)){var Fu=Q(t,G),cu=I?I(t)===Object.prototype:t instanceof Object||t.constructor===Object,lu=t instanceof Object?"":"null prototype",fu=!cu&&P&&Object(t)===t&&P in t?y.call(H(t),8,-1):lu?"Object":"",su=(cu||"function"!=typeof t.constructor?"":t.constructor.name?t.constructor.name+" ":"")+(fu||lu?"["+b.call(m.call([],fu||[],lu||[]),": ")+"] ":"");return 0===Fu.length?su+"{}":M?su+"{"+Z(Fu,M)+"}":su+"{ "+b.call(Fu,", ")+" }"}return String(t)};var G=Object.prototype.hasOwnProperty||function(u){return u in this};function V(u,t){return G.call(u,t)}function H(u){return p.call(u)}function q(u,t){if(u.indexOf)return u.indexOf(t);for(var e=0,r=u.length;e<r;e++)if(u[e]===t)return e;return-1}function X(u,t){if(u.length>t.maxStringLength){var e=u.length-t.maxStringLength,r="... "+e+" more character"+(e>1?"s":"");return X(y.call(u,0,t.maxStringLength),t)+r}return $(d.call(d.call(u,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,Y),"single",t)}function Y(u){var t=u.charCodeAt(0),e={8:"b",9:"t",10:"n",12:"f",13:"r"}[t];return e?"\\"+e:"\\x"+(t<16?"0":"")+B.call(t.toString(16))}function z(u){return"Object("+u+")"}function J(u){return u+" { ? }"}function K(u,t,e,r){return u+" ("+t+") {"+(r?Z(e,r):b.call(e,", "))+"}"}function Z(u,t){if(0===u.length)return"";var e="\n"+t.prev+t.base;return e+b.call(u,","+e)+"\n"+t.prev}function Q(u,t){var e=U(u),r=[];if(e){r.length=u.length;for(var n=0;n<u.length;n++)r[n]=V(u,n)?t(u[n],u):""}var o,D="function"==typeof x?x(u):[];if(j){o={};for(var i=0;i<D.length;i++)o["$"+D[i]]=D[i]}for(var a in u)V(u,a)&&(e&&String(Number(a))===a&&a<u.length||j&&o["$"+a]instanceof Symbol||(g.call(/[^\w$]/,a)?r.push(t(a,u)+": "+t(u[a],u)):r.push(a+": "+t(u[a],u))));if("function"==typeof x)for(var F=0;F<D.length;F++)T.call(u,D[F])&&r.push("["+t(D[F])+"]: "+t(u[D[F]],u));return r}},9121:function(u,t,e){"use strict";var r;if(!Object.keys){var n=Object.prototype.hasOwnProperty,o=Object.prototype.toString,D=e(999),i=Object.prototype.propertyIsEnumerable,a=!i.call({toString:null},"toString"),F=i.call((function(){}),"prototype"),c=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],l=function(u){var t=u.constructor;return t&&t.prototype===u},f={$applicationCache:!0,$console:!0,$external:!0,$frame:!0,$frameElement:!0,$frames:!0,$innerHeight:!0,$innerWidth:!0,$onmozfullscreenchange:!0,$onmozfullscreenerror:!0,$outerHeight:!0,$outerWidth:!0,$pageXOffset:!0,$pageYOffset:!0,$parent:!0,$scrollLeft:!0,$scrollTop:!0,$scrollX:!0,$scrollY:!0,$self:!0,$webkitIndexedDB:!0,$webkitStorageInfo:!0,$window:!0},s=function(){if("undefined"==typeof window)return!1;for(var u in window)try{if(!f["$"+u]&&n.call(window,u)&&null!==window[u]&&"object"==typeof window[u])try{l(window[u])}catch(u){return!0}}catch(u){return!0}return!1}();r=function(u){var t=null!==u&&"object"==typeof u,e="[object Function]"===o.call(u),r=D(u),i=t&&"[object String]"===o.call(u),f=[];if(!t&&!e&&!r)throw new TypeError("Object.keys called on a non-object");var A=F&&e;if(i&&u.length>0&&!n.call(u,0))for(var p=0;p<u.length;++p)f.push(String(p));if(r&&u.length>0)for(var E=0;E<u.length;++E)f.push(String(E));else for(var C in u)A&&"prototype"===C||!n.call(u,C)||f.push(String(C));if(a)for(var y=function(u){if("undefined"==typeof window||!s)return l(u);try{return l(u)}catch(u){return!1}}(u),d=0;d<c.length;++d)y&&"constructor"===c[d]||!n.call(u,c[d])||f.push(c[d]);return f}}u.exports=r},2051:function(u,t,e){"use strict";var r=Array.prototype.slice,n=e(999),o=Object.keys,D=o?function(u){return o(u)}:e(9121),i=Object.keys;D.shim=function(){if(Object.keys){var u=function(){var u=Object.keys(arguments);return u&&u.length===arguments.length}(1,2);u||(Object.keys=function(u){return n(u)?i(r.call(u)):i(u)})}else Object.keys=D;return Object.keys||D},u.exports=D},999:function(u){"use strict";var t=Object.prototype.toString;u.exports=function(u){var e=t.call(u),r="[object Arguments]"===e;return r||(r="[object Array]"!==e&&null!==u&&"object"==typeof u&&"number"==typeof u.length&&u.length>=0&&"[object Function]"===t.call(u.callee)),r}},9766:function(u,t,e){"use strict";var r=e(8921),n=Object,o=TypeError;u.exports=r((function(){if(null!=this&&this!==n(this))throw new o("RegExp.prototype.flags getter called on non-object");var u="";return this.hasIndices&&(u+="d"),this.global&&(u+="g"),this.ignoreCase&&(u+="i"),this.multiline&&(u+="m"),this.dotAll&&(u+="s"),this.unicode&&(u+="u"),this.unicodeSets&&(u+="v"),this.sticky&&(u+="y"),u}),"get flags",!0)},483:function(u,t,e){"use strict";var r=e(9722),n=e(2755),o=e(9766),D=e(5113),i=e(7299),a=n(D());r(a,{getPolyfill:D,implementation:o,shim:i}),u.exports=a},5113:function(u,t,e){"use strict";var r=e(9766),n=e(9722).supportsDescriptors,o=Object.getOwnPropertyDescriptor;u.exports=function(){if(n&&"gim"===/a/gim.flags){var u=o(RegExp.prototype,"flags");if(u&&"function"==typeof u.get&&"boolean"==typeof RegExp.prototype.dotAll&&"boolean"==typeof RegExp.prototype.hasIndices){var t="",e={};if(Object.defineProperty(e,"hasIndices",{get:function(){t+="d"}}),Object.defineProperty(e,"sticky",{get:function(){t+="y"}}),"dy"===t)return u.get}}return r}},7299:function(u,t,e){"use strict";var r=e(9722).supportsDescriptors,n=e(5113),o=Object.getOwnPropertyDescriptor,D=Object.defineProperty,i=TypeError,a=Object.getPrototypeOf,F=/a/;u.exports=function(){if(!r||!a)throw new i("RegExp.prototype.flags requires a true ES5 environment that supports property descriptors");var u=n(),t=a(F),e=o(t,"flags");return e&&e.get===u||D(t,"flags",{configurable:!0,enumerable:!1,get:u}),u}},7582:function(u,t,e){"use strict";var r=e(3099),n=e(2870),o=e(5494),D=r("RegExp.prototype.exec"),i=n("%TypeError%");u.exports=function(u){if(!o(u))throw new i("`regex` must be a RegExp");return function(t){return null!==D(u,t)}}},8921:function(u,t,e){"use strict";var r=e(6663),n=e(229)(),o=e(5610).functionsHaveConfigurableNames(),D=TypeError;u.exports=function(u,t){if("function"!=typeof u)throw new D("`fn` is not a function");return arguments.length>2&&!!arguments[2]&&!o||(n?r(u,"name",t,!0,!0):r(u,"name",t)),u}},5714:function(u,t,e){"use strict";var r=e(2870),n=e(3099),o=e(4538),D=r("%TypeError%"),i=r("%WeakMap%",!0),a=r("%Map%",!0),F=n("WeakMap.prototype.get",!0),c=n("WeakMap.prototype.set",!0),l=n("WeakMap.prototype.has",!0),f=n("Map.prototype.get",!0),s=n("Map.prototype.set",!0),A=n("Map.prototype.has",!0),p=function(u,t){for(var e,r=u;null!==(e=r.next);r=e)if(e.key===t)return r.next=e.next,e.next=u.next,u.next=e,e};u.exports=function(){var u,t,e,r={assert:function(u){if(!r.has(u))throw new D("Side channel does not contain "+o(u))},get:function(r){if(i&&r&&("object"==typeof r||"function"==typeof r)){if(u)return F(u,r)}else if(a){if(t)return f(t,r)}else if(e)return function(u,t){var e=p(u,t);return e&&e.value}(e,r)},has:function(r){if(i&&r&&("object"==typeof r||"function"==typeof r)){if(u)return l(u,r)}else if(a){if(t)return A(t,r)}else if(e)return function(u,t){return!!p(u,t)}(e,r);return!1},set:function(r,n){i&&r&&("object"==typeof r||"function"==typeof r)?(u||(u=new i),c(u,r,n)):a?(t||(t=new a),s(t,r,n)):(e||(e={key:{},next:null}),function(u,t,e){var r=p(u,t);r?r.value=e:u.next={key:t,next:u.next,value:e}}(e,r,n))}};return r}},3073:function(u,t,e){"use strict";var r=e(7113),n=e(151),o=e(1959),D=e(9497),i=e(5128),a=e(6751),F=e(3099),c=e(1143)(),l=e(483),f=F("String.prototype.indexOf"),s=e(2009),A=function(u){var t=s();if(c&&"symbol"==typeof Symbol.matchAll){var e=o(u,Symbol.matchAll);return e===RegExp.prototype[Symbol.matchAll]&&e!==t?t:e}if(D(u))return t};u.exports=function(u){var t=a(this);if(null!=u){if(D(u)){var e="flags"in u?n(u,"flags"):l(u);if(a(e),f(i(e),"g")<0)throw new TypeError("matchAll requires a global regular expression")}var o=A(u);if(void 0!==o)return r(o,u,[t])}var F=i(t),c=new RegExp(u,"g");return r(A(c),c,[F])}},5155:function(u,t,e){"use strict";var r=e(2755),n=e(9722),o=e(3073),D=e(1794),i=e(3911),a=r(o);n(a,{getPolyfill:D,implementation:o,shim:i}),u.exports=a},2009:function(u,t,e){"use strict";var r=e(1143)(),n=e(8012);u.exports=function(){return r&&"symbol"==typeof Symbol.matchAll&&"function"==typeof RegExp.prototype[Symbol.matchAll]?RegExp.prototype[Symbol.matchAll]:n}},1794:function(u,t,e){"use strict";var r=e(3073);u.exports=function(){if(String.prototype.matchAll)try{"".matchAll(RegExp.prototype)}catch(u){return String.prototype.matchAll}return r}},8012:function(u,t,e){"use strict";var r=e(1398),n=e(151),o=e(8322),D=e(2449),i=e(3995),a=e(5128),F=e(1874),c=e(483),l=e(8921),f=e(3099)("String.prototype.indexOf"),s=RegExp,A="flags"in RegExp.prototype,p=l((function(u){var t=this;if("Object"!==F(t))throw new TypeError('"this" value must be an Object');var e=a(u),l=function(u,t){var e="flags"in t?n(t,"flags"):a(c(t));return{flags:e,matcher:new u(A&&"string"==typeof e?t:u===s?t.source:t,e)}}(D(t,s),t),p=l.flags,E=l.matcher,C=i(n(t,"lastIndex"));o(E,"lastIndex",C,!0);var y=f(p,"g")>-1,d=f(p,"u")>-1;return r(E,e,y,d)}),"[Symbol.matchAll]",!0);u.exports=p},3911:function(u,t,e){"use strict";var r=e(9722),n=e(1143)(),o=e(1794),D=e(2009),i=Object.defineProperty,a=Object.getOwnPropertyDescriptor;u.exports=function(){var u=o();if(r(String.prototype,{matchAll:u},{matchAll:function(){return String.prototype.matchAll!==u}}),n){var t=Symbol.matchAll||(Symbol.for?Symbol.for("Symbol.matchAll"):Symbol("Symbol.matchAll"));if(r(Symbol,{matchAll:t},{matchAll:function(){return Symbol.matchAll!==t}}),i&&a){var e=a(Symbol,t);e&&!e.configurable||i(Symbol,t,{configurable:!1,enumerable:!1,value:t,writable:!1})}var F=D(),c={};c[t]=F;var l={};l[t]=function(){return RegExp.prototype[t]!==F},r(RegExp.prototype,c,l)}return u}},8125:function(u,t,e){"use strict";var r=e(6751),n=e(5128),o=e(3099)("String.prototype.replace"),D=/^\s$/.test(""),i=D?/^[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+/:/^[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+/,a=D?/[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+$/:/[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+$/;u.exports=function(){var u=n(r(this));return o(o(u,i,""),a,"")}},9434:function(u,t,e){"use strict";var r=e(2755),n=e(9722),o=e(6751),D=e(8125),i=e(3228),a=e(818),F=r(i()),c=function(u){return o(u),F(u)};n(c,{getPolyfill:i,implementation:D,shim:a}),u.exports=c},3228:function(u,t,e){"use strict";var r=e(8125);u.exports=function(){return String.prototype.trim&&""==="".trim()&&""==="".trim()&&"_"==="_".trim()&&"_"==="_".trim()?String.prototype.trim:r}},818:function(u,t,e){"use strict";var r=e(9722),n=e(3228);u.exports=function(){var u=n();return r(String.prototype,{trim:u},{trim:function(){return String.prototype.trim!==u}}),u}},7002:function(){},1510:function(u,t,e){"use strict";var r=e(2870),n=e(6318),o=e(1874),D=e(2990),i=e(5674),a=r("%TypeError%");u.exports=function(u,t,e){if("String"!==o(u))throw new a("Assertion failed: `S` must be a String");if(!D(t)||t<0||t>i)throw new a("Assertion failed: `length` must be an integer >= 0 and <= 2**53");if("Boolean"!==o(e))throw new a("Assertion failed: `unicode` must be a Boolean");return e?t+1>=u.length?t+1:t+n(u,t)["[[CodeUnitCount]]"]:t+1}},7113:function(u,t,e){"use strict";var r=e(2870),n=e(3099),o=r("%TypeError%"),D=e(6287),i=r("%Reflect.apply%",!0)||n("Function.prototype.apply");u.exports=function(u,t){var e=arguments.length>2?arguments[2]:[];if(!D(e))throw new o("Assertion failed: optional `argumentsList`, if provided, must be a List");return i(u,t,e)}},6318:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(3099),o=e(5541),D=e(959),i=e(1874),a=e(1751),F=n("String.prototype.charAt"),c=n("String.prototype.charCodeAt");u.exports=function(u,t){if("String"!==i(u))throw new r("Assertion failed: `string` must be a String");var e=u.length;if(t<0||t>=e)throw new r("Assertion failed: `position` must be >= 0, and < the length of `string`");var n=c(u,t),l=F(u,t),f=o(n),s=D(n);if(!f&&!s)return{"[[CodePoint]]":l,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!1};if(s||t+1===e)return{"[[CodePoint]]":l,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!0};var A=c(u,t+1);return D(A)?{"[[CodePoint]]":a(n,A),"[[CodeUnitCount]]":2,"[[IsUnpairedSurrogate]]":!1}:{"[[CodePoint]]":l,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!0}}},5702:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(1874);u.exports=function(u,t){if("Boolean"!==n(t))throw new r("Assertion failed: Type(done) is not Boolean");return{value:u,done:t}}},6782:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(2860),o=e(8357),D=e(3301),i=e(6284),a=e(8277),F=e(1874);u.exports=function(u,t,e){if("Object"!==F(u))throw new r("Assertion failed: Type(O) is not Object");if(!i(t))throw new r("Assertion failed: IsPropertyKey(P) is not true");return n(D,a,o,u,t,{"[[Configurable]]":!0,"[[Enumerable]]":!1,"[[Value]]":e,"[[Writable]]":!0})}},1398:function(u,t,e){"use strict";var r=e(2870),n=e(1143)(),o=r("%TypeError%"),D=r("%IteratorPrototype%",!0),i=e(1510),a=e(5702),F=e(6782),c=e(151),l=e(5716),f=e(3500),s=e(8322),A=e(3995),p=e(5128),E=e(1874),C=e(7284),y=e(2263),d=function(u,t,e,r){if("String"!==E(t))throw new o("`S` must be a string");if("Boolean"!==E(e))throw new o("`global` must be a boolean");if("Boolean"!==E(r))throw new o("`fullUnicode` must be a boolean");C.set(this,"[[IteratingRegExp]]",u),C.set(this,"[[IteratedString]]",t),C.set(this,"[[Global]]",e),C.set(this,"[[Unicode]]",r),C.set(this,"[[Done]]",!1)};D&&(d.prototype=l(D)),F(d.prototype,"next",(function(){var u=this;if("Object"!==E(u))throw new o("receiver must be an object");if(!(u instanceof d&&C.has(u,"[[IteratingRegExp]]")&&C.has(u,"[[IteratedString]]")&&C.has(u,"[[Global]]")&&C.has(u,"[[Unicode]]")&&C.has(u,"[[Done]]")))throw new o('"this" value must be a RegExpStringIterator instance');if(C.get(u,"[[Done]]"))return a(void 0,!0);var t=C.get(u,"[[IteratingRegExp]]"),e=C.get(u,"[[IteratedString]]"),r=C.get(u,"[[Global]]"),n=C.get(u,"[[Unicode]]"),D=f(t,e);if(null===D)return C.set(u,"[[Done]]",!0),a(void 0,!0);if(r){if(""===p(c(D,"0"))){var F=A(c(t,"lastIndex")),l=i(e,F,n);s(t,"lastIndex",l,!0)}return a(D,!1)}return C.set(u,"[[Done]]",!0),a(D,!1)})),n&&(y(d.prototype,"RegExp String Iterator"),Symbol.iterator&&"function"!=typeof d.prototype[Symbol.iterator])&&F(d.prototype,Symbol.iterator,(function(){return this})),u.exports=function(u,t,e,r){return new d(u,t,e,r)}},3645:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(7999),o=e(2860),D=e(8357),i=e(8355),a=e(3301),F=e(6284),c=e(8277),l=e(7628),f=e(1874);u.exports=function(u,t,e){if("Object"!==f(u))throw new r("Assertion failed: Type(O) is not Object");if(!F(t))throw new r("Assertion failed: IsPropertyKey(P) is not true");var s=n({Type:f,IsDataDescriptor:a,IsAccessorDescriptor:i},e)?e:l(e);if(!n({Type:f,IsDataDescriptor:a,IsAccessorDescriptor:i},s))throw new r("Assertion failed: Desc is not a valid Property Descriptor");return o(a,c,D,u,t,s)}},8357:function(u,t,e){"use strict";var r=e(1489),n=e(1598),o=e(1874);u.exports=function(u){return void 0!==u&&r(o,"Property Descriptor","Desc",u),n(u)}},151:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(4538),o=e(6284),D=e(1874);u.exports=function(u,t){if("Object"!==D(u))throw new r("Assertion failed: Type(O) is not Object");if(!o(t))throw new r("Assertion failed: IsPropertyKey(P) is not true, got "+n(t));return u[t]}},1959:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(9374),o=e(7304),D=e(6284),i=e(4538);u.exports=function(u,t){if(!D(t))throw new r("Assertion failed: IsPropertyKey(P) is not true");var e=n(u,t);if(null!=e){if(!o(e))throw new r(i(t)+" is not a function: "+i(e));return e}}},9374:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(4538),o=e(6284);u.exports=function(u,t){if(!o(t))throw new r("Assertion failed: IsPropertyKey(P) is not true, got "+n(t));return u[t]}},8355:function(u,t,e){"use strict";var r=e(9545),n=e(1874),o=e(1489);u.exports=function(u){return void 0!==u&&(o(n,"Property Descriptor","Desc",u),!(!r(u,"[[Get]]")&&!r(u,"[[Set]]")))}},6287:function(u,t,e){"use strict";u.exports=e(2403)},7304:function(u,t,e){"use strict";u.exports=e(3655)},4791:function(u,t,e){"use strict";var r=e(6740)("%Reflect.construct%",!0),n=e(3645);try{n({},"",{"[[Get]]":function(){}})}catch(u){n=null}if(n&&r){var o={},D={};n(D,"length",{"[[Get]]":function(){throw o},"[[Enumerable]]":!0}),u.exports=function(u){try{r(u,D)}catch(u){return u===o}}}else u.exports=function(u){return"function"==typeof u&&!!u.prototype}},3301:function(u,t,e){"use strict";var r=e(9545),n=e(1874),o=e(1489);u.exports=function(u){return void 0!==u&&(o(n,"Property Descriptor","Desc",u),!(!r(u,"[[Value]]")&&!r(u,"[[Writable]]")))}},6284:function(u){"use strict";u.exports=function(u){return"string"==typeof u||"symbol"==typeof u}},9497:function(u,t,e){"use strict";var r=e(2870)("%Symbol.match%",!0),n=e(5494),o=e(5695);u.exports=function(u){if(!u||"object"!=typeof u)return!1;if(r){var t=u[r];if(void 0!==t)return o(t)}return n(u)}},5716:function(u,t,e){"use strict";var r=e(2870),n=r("%Object.create%",!0),o=r("%TypeError%"),D=r("%SyntaxError%"),i=e(6287),a=e(1874),F=e(7735),c=e(7284),l=e(3413)();u.exports=function(u){if(null!==u&&"Object"!==a(u))throw new o("Assertion failed: `proto` must be null or an object");var t,e=arguments.length<2?[]:arguments[1];if(!i(e))throw new o("Assertion failed: `additionalInternalSlotsList` must be an Array");if(n)t=n(u);else if(l)t={__proto__:u};else{if(null===u)throw new D("native Object.create support is required to create null objects");var r=function(){};r.prototype=u,t=new r}return e.length>0&&F(e,(function(u){c.set(t,u,void 0)})),t}},3500:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(3099)("RegExp.prototype.exec"),o=e(7113),D=e(151),i=e(7304),a=e(1874);u.exports=function(u,t){if("Object"!==a(u))throw new r("Assertion failed: `R` must be an Object");if("String"!==a(t))throw new r("Assertion failed: `S` must be a String");var e=D(u,"exec");if(i(e)){var F=o(e,u,[t]);if(null===F||"Object"===a(F))return F;throw new r('"exec" method must return `null` or an Object')}return n(u,t)}},6751:function(u,t,e){"use strict";u.exports=e(9572)},8277:function(u,t,e){"use strict";var r=e(159);u.exports=function(u,t){return u===t?0!==u||1/u==1/t:r(u)&&r(t)}},8322:function(u,t,e){"use strict";var r=e(2870)("%TypeError%"),n=e(6284),o=e(8277),D=e(1874),i=function(){try{return delete[].length,!0}catch(u){return!1}}();u.exports=function(u,t,e,a){if("Object"!==D(u))throw new r("Assertion failed: `O` must be an Object");if(!n(t))throw new r("Assertion failed: `P` must be a Property Key");if("Boolean"!==D(a))throw new r("Assertion failed: `Throw` must be a Boolean");if(a){if(u[t]=e,i&&!o(u[t],e))throw new r("Attempted to assign to readonly property.");return!0}try{return u[t]=e,!i||o(u[t],e)}catch(u){return!1}}},2449:function(u,t,e){"use strict";var r=e(2870),n=r("%Symbol.species%",!0),o=r("%TypeError%"),D=e(4791),i=e(1874);u.exports=function(u,t){if("Object"!==i(u))throw new o("Assertion failed: Type(O) is not Object");var e=u.constructor;if(void 0===e)return t;if("Object"!==i(e))throw new o("O.constructor is not an Object");var r=n?e[n]:void 0;if(null==r)return t;if(D(r))return r;throw new o("no constructor found")}},6207:function(u,t,e){"use strict";var r=e(2870),n=r("%Number%"),o=r("%RegExp%"),D=r("%TypeError%"),i=r("%parseInt%"),a=e(3099),F=e(7582),c=a("String.prototype.slice"),l=F(/^0b[01]+$/i),f=F(/^0o[0-7]+$/i),s=F(/^[-+]0x[0-9a-f]+$/i),A=F(new o("["+[" ","",""].join("")+"]","g")),p=e(9434),E=e(1874);u.exports=function u(t){if("String"!==E(t))throw new D("Assertion failed: `argument` is not a String");if(l(t))return n(i(c(t,2),2));if(f(t))return n(i(c(t,2),8));if(A(t)||s(t))return NaN;var e=p(t);return e!==t?u(e):n(t)}},5695:function(u){"use strict";u.exports=function(u){return!!u}},1200:function(u,t,e){"use strict";var r=e(6542),n=e(5693),o=e(159),D=e(1117);u.exports=function(u){var t=r(u);return o(t)||0===t?0:D(t)?n(t):t}},3995:function(u,t,e){"use strict";var r=e(5674),n=e(1200);u.exports=function(u){var t=n(u);return t<=0?0:t>r?r:t}},6542:function(u,t,e){"use strict";var r=e(2870),n=r("%TypeError%"),o=r("%Number%"),D=e(8606),i=e(703),a=e(6207);u.exports=function(u){var t=D(u)?u:i(u,o);if("symbol"==typeof t)throw new n("Cannot convert a Symbol value to a number");if("bigint"==typeof t)throw new n("Conversion from 'BigInt' to 'number' is not allowed.");return"string"==typeof t?a(t):o(t)}},703:function(u,t,e){"use strict";var r=e(7358);u.exports=function(u){return arguments.length>1?r(u,arguments[1]):r(u)}},7628:function(u,t,e){"use strict";var r=e(9545),n=e(2870)("%TypeError%"),o=e(1874),D=e(5695),i=e(7304);u.exports=function(u){if("Object"!==o(u))throw new n("ToPropertyDescriptor requires an object");var t={};if(r(u,"enumerable")&&(t["[[Enumerable]]"]=D(u.enumerable)),r(u,"configurable")&&(t["[[Configurable]]"]=D(u.configurable)),r(u,"value")&&(t["[[Value]]"]=u.value),r(u,"writable")&&(t["[[Writable]]"]=D(u.writable)),r(u,"get")){var e=u.get;if(void 0!==e&&!i(e))throw new n("getter must be a function");t["[[Get]]"]=e}if(r(u,"set")){var a=u.set;if(void 0!==a&&!i(a))throw new n("setter must be a function");t["[[Set]]"]=a}if((r(t,"[[Get]]")||r(t,"[[Set]]"))&&(r(t,"[[Value]]")||r(t,"[[Writable]]")))throw new n("Invalid property descriptor. Cannot both specify accessors and a value or writable attribute");return t}},5128:function(u,t,e){"use strict";var r=e(2870),n=r("%String%"),o=r("%TypeError%");u.exports=function(u){if("symbol"==typeof u)throw new o("Cannot convert a Symbol value to a string");return n(u)}},1874:function(u,t,e){"use strict";var r=e(6101);u.exports=function(u){return"symbol"==typeof u?"Symbol":"bigint"==typeof u?"BigInt":r(u)}},1751:function(u,t,e){"use strict";var r=e(2870),n=r("%TypeError%"),o=r("%String.fromCharCode%"),D=e(5541),i=e(959);u.exports=function(u,t){if(!D(u)||!i(t))throw new n("Assertion failed: `lead` must be a leading surrogate char code, and `trail` must be a trailing surrogate char code");return o(u)+o(t)}},3567:function(u,t,e){"use strict";var r=e(1874),n=Math.floor;u.exports=function(u){return"BigInt"===r(u)?u:n(u)}},5693:function(u,t,e){"use strict";var r=e(2870),n=e(3567),o=r("%TypeError%");u.exports=function(u){if("number"!=typeof u&&"bigint"!=typeof u)throw new o("argument must be a Number or a BigInt");var t=u<0?-n(-u):n(u);return 0===t?0:t}},9572:function(u,t,e){"use strict";var r=e(2870)("%TypeError%");u.exports=function(u,t){if(null==u)throw new r(t||"Cannot call method on "+u);return u}},6101:function(u){"use strict";u.exports=function(u){return null===u?"Null":void 0===u?"Undefined":"function"==typeof u||"object"==typeof u?"Object":"number"==typeof u?"Number":"boolean"==typeof u?"Boolean":"string"==typeof u?"String":void 0}},6740:function(u,t,e){"use strict";u.exports=e(2870)},2860:function(u,t,e){"use strict";var r=e(229),n=e(2870),o=r()&&n("%Object.defineProperty%",!0),D=r.hasArrayLengthDefineBug(),i=D&&e(2403),a=e(3099)("Object.prototype.propertyIsEnumerable");u.exports=function(u,t,e,r,n,F){if(!o){if(!u(F))return!1;if(!F["[[Configurable]]"]||!F["[[Writable]]"])return!1;if(n in r&&a(r,n)!==!!F["[[Enumerable]]"])return!1;var c=F["[[Value]]"];return r[n]=c,t(r[n],c)}return D&&"length"===n&&"[[Value]]"in F&&i(r)&&r.length!==F["[[Value]]"]?(r.length=F["[[Value]]"],r.length===F["[[Value]]"]):(o(r,n,e(F)),!0)}},2403:function(u,t,e){"use strict";var r=e(2870)("%Array%"),n=!r.isArray&&e(3099)("Object.prototype.toString");u.exports=r.isArray||function(u){return"[object Array]"===n(u)}},1489:function(u,t,e){"use strict";var r=e(2870),n=r("%TypeError%"),o=r("%SyntaxError%"),D=e(9545),i=e(2990),a={"Property Descriptor":function(u){var t={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};if(!u)return!1;for(var e in u)if(D(u,e)&&!t[e])return!1;var r=D(u,"[[Value]]"),o=D(u,"[[Get]]")||D(u,"[[Set]]");if(r&&o)throw new n("Property Descriptors may not be both accessor and data descriptors");return!0},"Match Record":e(900),"Iterator Record":function(u){return D(u,"[[Iterator]]")&&D(u,"[[NextMethod]]")&&D(u,"[[Done]]")},"PromiseCapability Record":function(u){return!!u&&D(u,"[[Resolve]]")&&"function"==typeof u["[[Resolve]]"]&&D(u,"[[Reject]]")&&"function"==typeof u["[[Reject]]"]&&D(u,"[[Promise]]")&&u["[[Promise]]"]&&"function"==typeof u["[[Promise]]"].then},"AsyncGeneratorRequest Record":function(u){return!!u&&D(u,"[[Completion]]")&&D(u,"[[Capability]]")&&a["PromiseCapability Record"](u["[[Capability]]"])},"RegExp Record":function(u){return u&&D(u,"[[IgnoreCase]]")&&"boolean"==typeof u["[[IgnoreCase]]"]&&D(u,"[[Multiline]]")&&"boolean"==typeof u["[[Multiline]]"]&&D(u,"[[DotAll]]")&&"boolean"==typeof u["[[DotAll]]"]&&D(u,"[[Unicode]]")&&"boolean"==typeof u["[[Unicode]]"]&&D(u,"[[CapturingGroupsCount]]")&&"number"==typeof u["[[CapturingGroupsCount]]"]&&i(u["[[CapturingGroupsCount]]"])&&u["[[CapturingGroupsCount]]"]>=0}};u.exports=function(u,t,e,r){var D=a[t];if("function"!=typeof D)throw new o("unknown record type: "+t);if("Object"!==u(r)||!D(r))throw new n(e+" must be a "+t)}},7735:function(u){"use strict";u.exports=function(u,t){for(var e=0;e<u.length;e+=1)t(u[e],e,u)}},1598:function(u){"use strict";u.exports=function(u){if(void 0===u)return u;var t={};return"[[Value]]"in u&&(t.value=u["[[Value]]"]),"[[Writable]]"in u&&(t.writable=!!u["[[Writable]]"]),"[[Get]]"in u&&(t.get=u["[[Get]]"]),"[[Set]]"in u&&(t.set=u["[[Set]]"]),"[[Enumerable]]"in u&&(t.enumerable=!!u["[[Enumerable]]"]),"[[Configurable]]"in u&&(t.configurable=!!u["[[Configurable]]"]),t}},1117:function(u,t,e){"use strict";var r=e(159);u.exports=function(u){return("number"==typeof u||"bigint"==typeof u)&&!r(u)&&u!==1/0&&u!==-1/0}},2990:function(u,t,e){"use strict";var r=e(2870),n=r("%Math.abs%"),o=r("%Math.floor%"),D=e(159),i=e(1117);u.exports=function(u){if("number"!=typeof u||D(u)||!i(u))return!1;var t=n(u);return o(t)===t}},5541:function(u){"use strict";u.exports=function(u){return"number"==typeof u&&u>=55296&&u<=56319}},900:function(u,t,e){"use strict";var r=e(9545);u.exports=function(u){return r(u,"[[StartIndex]]")&&r(u,"[[EndIndex]]")&&u["[[StartIndex]]"]>=0&&u["[[EndIndex]]"]>=u["[[StartIndex]]"]&&String(parseInt(u["[[StartIndex]]"],10))===String(u["[[StartIndex]]"])&&String(parseInt(u["[[EndIndex]]"],10))===String(u["[[EndIndex]]"])}},159:function(u){"use strict";u.exports=Number.isNaN||function(u){return u!=u}},8606:function(u){"use strict";u.exports=function(u){return null===u||"function"!=typeof u&&"object"!=typeof u}},7999:function(u,t,e){"use strict";var r=e(2870),n=e(9545),o=r("%TypeError%");u.exports=function(u,t){if("Object"!==u.Type(t))return!1;var e={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};for(var r in t)if(n(t,r)&&!e[r])return!1;if(u.IsDataDescriptor(t)&&u.IsAccessorDescriptor(t))throw new o("Property Descriptors may not be both accessor and data descriptors");return!0}},959:function(u){"use strict";u.exports=function(u){return"number"==typeof u&&u>=56320&&u<=57343}},5674:function(u){"use strict";u.exports=Number.MAX_SAFE_INTEGER||9007199254740991}},t={};function e(r){var n=t[r];if(void 0!==n)return n.exports;var o=t[r]={exports:{}};return u[r](o,o.exports,e),o.exports}e.n=function(u){var t=u&&u.__esModule?function(){return u.default}:function(){return u};return e.d(t,{a:t}),t},e.d=function(u,t){for(var r in t)e.o(t,r)&&!e.o(u,r)&&Object.defineProperty(u,r,{enumerable:!0,get:t[r]})},e.o=function(u,t){return Object.prototype.hasOwnProperty.call(u,t)},function(){"use strict";var u=e(1844);function t(t,e,r){for(var n=0,o=[];-1!==n;)-1!==(n=t.indexOf(e,n))&&(o.push({start:n,end:n+e.length,errors:0}),n+=1);return o.length>0?o:(0,u.Z)(t,e,r)}function r(u,e){return 0===e.length||0===u.length?0:1-t(u,e,e.length)[0].errors/e.length}function n(u){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(u){return typeof u}:function(u){return u&&"function"==typeof Symbol&&u.constructor===Symbol&&u!==Symbol.prototype?"symbol":typeof u},n(u)}function o(u,t){(null==t||t>u.length)&&(t=u.length);for(var e=0,r=new Array(t);e<t;e++)r[e]=u[e];return r}function D(u,t){if(!(u instanceof t))throw new TypeError("Cannot call a class as a function")}function i(u,t){for(var e=0;e<t.length;e++){var r=t[e];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(u,(void 0,o=function(u,t){if("object"!==n(u)||null===u)return u;var e=u[Symbol.toPrimitive];if(void 0!==e){var r=e.call(u,"string");if("object"!==n(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(u)}(r.key),"symbol"===n(o)?o:String(o)),r)}var o}function a(u,t,e){return t&&i(u.prototype,t),e&&i(u,e),Object.defineProperty(u,"prototype",{writable:!1}),u}function F(u){switch(u.nodeType){case Node.ELEMENT_NODE:case Node.TEXT_NODE:return u.textContent.length;default:return 0}}function c(u){for(var t=u.previousSibling,e=0;t;)e+=F(t),t=t.previousSibling;return e}function l(u){for(var t=arguments.length,e=new Array(t>1?t-1:0),r=1;r<t;r++)e[r-1]=arguments[r];for(var n,o=e.shift(),D=u.ownerDocument.createNodeIterator(u,NodeFilter.SHOW_TEXT),i=[],a=D.nextNode(),F=0;void 0!==o&&a;)F+(n=a).data.length>o?(i.push({node:n,offset:o-F}),o=e.shift()):(a=D.nextNode(),F+=n.data.length);for(;void 0!==o&&n&&F===o;)i.push({node:n,offset:n.data.length}),o=e.shift();if(void 0!==o)throw new RangeError("Offset exceeds text length");return i}var f=function(){function u(t,e){if(D(this,u),e<0)throw new Error("Offset is invalid");this.element=t,this.offset=e}return a(u,[{key:"relativeTo",value:function(t){if(!t.contains(this.element))throw new Error("Parent is not an ancestor of current element");for(var e=this.element,r=this.offset;e!==t;)r+=c(e),e=e.parentElement;return new u(e,r)}},{key:"resolve",value:function(){var u=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};try{return l(this.element,this.offset)[0]}catch(n){if(0===this.offset&&void 0!==u.direction){var t=document.createTreeWalker(this.element.getRootNode(),NodeFilter.SHOW_TEXT);t.currentNode=this.element;var e=1===u.direction,r=e?t.nextNode():t.previousNode();if(!r)throw n;return{node:r,offset:e?0:r.data.length}}throw n}}}],[{key:"fromCharOffset",value:function(t,e){switch(t.nodeType){case Node.TEXT_NODE:return u.fromPoint(t,e);case Node.ELEMENT_NODE:return new u(t,e);default:throw new Error("Node is not an element or text node")}}},{key:"fromPoint",value:function(t,e){switch(t.nodeType){case Node.TEXT_NODE:if(e<0||e>t.data.length)throw new Error("Text node offset is out of range");if(!t.parentElement)throw new Error("Text node has no parent");var r=c(t)+e;return new u(t.parentElement,r);case Node.ELEMENT_NODE:if(e<0||e>t.childNodes.length)throw new Error("Child node offset is out of range");for(var n=0,o=0;o<e;o++)n+=F(t.childNodes[o]);return new u(t,n);default:throw new Error("Point is not in an element or text node")}}}]),u}(),s=function(){function u(t,e){D(this,u),this.start=t,this.end=e}return a(u,[{key:"relativeTo",value:function(t){return new u(this.start.relativeTo(t),this.end.relativeTo(t))}},{key:"toRange",value:function(){var u,t,e,r;if(this.start.element===this.end.element&&this.start.offset<=this.end.offset){var n=(e=l(this.start.element,this.start.offset,this.end.offset),r=2,function(u){if(Array.isArray(u))return u}(e)||function(u,t){var e=null==u?null:"undefined"!=typeof Symbol&&u[Symbol.iterator]||u["@@iterator"];if(null!=e){var r,n,o,D,i=[],a=!0,F=!1;try{if(o=(e=e.call(u)).next,0===t){if(Object(e)!==e)return;a=!1}else for(;!(a=(r=o.call(e)).done)&&(i.push(r.value),i.length!==t);a=!0);}catch(u){F=!0,n=u}finally{try{if(!a&&null!=e.return&&(D=e.return(),Object(D)!==D))return}finally{if(F)throw n}}return i}}(e,r)||function(u,t){if(u){if("string"==typeof u)return o(u,t);var e=Object.prototype.toString.call(u).slice(8,-1);return"Object"===e&&u.constructor&&(e=u.constructor.name),"Map"===e||"Set"===e?Array.from(u):"Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e)?o(u,t):void 0}}(e,r)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}());u=n[0],t=n[1]}else u=this.start.resolve({direction:1}),t=this.end.resolve({direction:2});var D=new Range;return D.setStart(u.node,u.offset),D.setEnd(t.node,t.offset),D}}],[{key:"fromRange",value:function(t){return new u(f.fromPoint(t.startContainer,t.startOffset),f.fromPoint(t.endContainer,t.endOffset))}},{key:"fromOffsets",value:function(t,e,r){return new u(new f(t,e),new f(t,r))}}]),u}();function A(u){return A="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(u){return typeof u}:function(u){return u&&"function"==typeof Symbol&&u.constructor===Symbol&&u!==Symbol.prototype?"symbol":typeof u},A(u)}function p(u,t){var e=Object.keys(u);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(u);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(u,t).enumerable}))),e.push.apply(e,r)}return e}function E(u){for(var t=1;t<arguments.length;t++){var e=null!=arguments[t]?arguments[t]:{};t%2?p(Object(e),!0).forEach((function(t){var r,n,o;r=u,n=t,o=e[t],(n=B(n))in r?Object.defineProperty(r,n,{value:o,enumerable:!0,configurable:!0,writable:!0}):r[n]=o})):Object.getOwnPropertyDescriptors?Object.defineProperties(u,Object.getOwnPropertyDescriptors(e)):p(Object(e)).forEach((function(t){Object.defineProperty(u,t,Object.getOwnPropertyDescriptor(e,t))}))}return u}function C(u,t){if(!(u instanceof t))throw new TypeError("Cannot call a class as a function")}function y(u,t){for(var e=0;e<t.length;e++){var r=t[e];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(u,B(r.key),r)}}function d(u,t,e){return t&&y(u.prototype,t),e&&y(u,e),Object.defineProperty(u,"prototype",{writable:!1}),u}function B(u){var t=function(u,t){if("object"!==A(u)||null===u)return u;var e=u[Symbol.toPrimitive];if(void 0!==e){var r=e.call(u,"string");if("object"!==A(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(u)}(u);return"symbol"===A(t)?t:String(t)}var h=function(){function u(t,e,r){C(this,u),this.root=t,this.start=e,this.end=r}return d(u,[{key:"toSelector",value:function(){return{type:"TextPositionSelector",start:this.start,end:this.end}}},{key:"toRange",value:function(){return s.fromOffsets(this.root,this.start,this.end).toRange()}}],[{key:"fromRange",value:function(t,e){var r=s.fromRange(e).relativeTo(t);return new u(t,r.start.offset,r.end.offset)}},{key:"fromSelector",value:function(t,e){return new u(t,e.start,e.end)}}]),u}(),g=function(){function u(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};C(this,u),this.root=t,this.exact=e,this.context=r}return d(u,[{key:"toSelector",value:function(){return{type:"TextQuoteSelector",exact:this.exact,prefix:this.context.prefix,suffix:this.context.suffix}}},{key:"toRange",value:function(){var u=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.toPositionAnchor(u).toRange()}},{key:"toPositionAnchor",value:function(){var u=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},e=function(u,e){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(0===e.length)return null;var o=Math.min(256,e.length/2),D=t(u,e,o);if(0===D.length)return null;var i=function(t){var o=1-t.errors/e.length,D=n.prefix?r(u.slice(Math.max(0,t.start-n.prefix.length),t.start),n.prefix):1,i=n.suffix?r(u.slice(t.end,t.end+n.suffix.length),n.suffix):1,a=1;return"number"==typeof n.hint&&(a=1-Math.abs(t.start-n.hint)/u.length),(50*o+20*D+20*i+2*a)/92},a=D.map((function(u){return{start:u.start,end:u.end,score:i(u)}}));return a.sort((function(u,t){return t.score-u.score})),a[0]}(this.root.textContent,this.exact,E(E({},this.context),{},{hint:u.hint}));if(!e)throw new Error("Quote not found");return new h(this.root,e.start,e.end)}}],[{key:"fromRange",value:function(t,e){var r=t.textContent,n=s.fromRange(e).relativeTo(t),o=n.start.offset,D=n.end.offset;return new u(t,r.slice(o,D),{prefix:r.slice(Math.max(0,o-32),o),suffix:r.slice(D,Math.min(r.length,D+32))})}},{key:"fromSelector",value:function(t,e){var r=e.prefix,n=e.suffix;return new u(t,e.exact,{prefix:r,suffix:n})}}]),u}();function m(u,t){(null==t||t>u.length)&&(t=u.length);for(var e=0,r=new Array(t);e<t;e++)r[e]=u[e];return r}window.addEventListener("error",(function(u){Android.logError(u.message,u.filename,u.lineno)}),!1),window.addEventListener("load",(function(){new ResizeObserver((function(){var u;u=Android.getViewportWidth(),b=u/window.devicePixelRatio,T("--RS__viewportWidth","calc("+u+"px / "+window.devicePixelRatio+")"),function(){var u="readium-virtual-page",t=document.getElementById(u);if(v()||2!=parseInt(window.getComputedStyle(document.documentElement).getPropertyValue("column-count")))t&&t.remove();else{var e=document.scrollingElement.scrollWidth/b;Math.round(2*e)/2%1>.1&&(t?t.remove():((t=document.createElement("div")).setAttribute("id",u),t.style.breakBefore="column",t.innerHTML="​",document.body.appendChild(t)))}}(),j()})).observe(document.body)}),!1);var b=1;function v(){var u=document.documentElement.style;return"readium-scroll-on"==u.getPropertyValue("--USER__view").trim()||"readium-scroll-on"==u.getPropertyValue("--USER__scroll").trim()}function w(){return"rtl"==document.body.dir.toLowerCase()}function S(u){return v()?document.scrollingElement.scrollTop=u.top+window.scrollY:document.scrollingElement.scrollLeft=O(u.left+window.scrollX),!0}function x(u){if(v())throw"Called scrollToOffset() with scroll mode enabled. This can only be used in paginated mode.";var t=window.scrollX;return document.scrollingElement.scrollLeft=O(u),Math.abs(t-u)/b>.01}function O(u){var t=u+(w()?-1:1);return t-t%b}function j(){if(!v()){var u=window.scrollX,t=(w()?-1:1)*(b/2);document.scrollingElement.scrollLeft=O(u+t)}}function P(u){try{var t,e=u.locations,r=u.text;if(r&&r.highlight)return e&&e.cssSelector&&(t=document.querySelector(e.cssSelector)),t||(t=document.body),new g(t,r.highlight,{prefix:r.before,suffix:r.after}).toRange();if(e){var n=null;if(!n&&e.cssSelector&&(n=document.querySelector(e.cssSelector)),!n&&e.fragments){var o,D=function(u,t){var e="undefined"!=typeof Symbol&&u[Symbol.iterator]||u["@@iterator"];if(!e){if(Array.isArray(u)||(e=function(u,t){if(u){if("string"==typeof u)return m(u,t);var e=Object.prototype.toString.call(u).slice(8,-1);return"Object"===e&&u.constructor&&(e=u.constructor.name),"Map"===e||"Set"===e?Array.from(u):"Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e)?m(u,t):void 0}}(u))||t&&u&&"number"==typeof u.length){e&&(u=e);var r=0,n=function(){};return{s:n,n:function(){return r>=u.length?{done:!0}:{done:!1,value:u[r++]}},e:function(u){throw u},f:n}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,D=!0,i=!1;return{s:function(){e=e.call(u)},n:function(){var u=e.next();return D=u.done,u},e:function(u){i=!0,o=u},f:function(){try{D||null==e.return||e.return()}finally{if(i)throw o}}}}(e.fragments);try{for(D.s();!(o=D.n()).done;){var i=o.value;if(n=document.getElementById(i))break}}catch(u){D.e(u)}finally{D.f()}}if(n){var a=document.createRange();return a.setStartBefore(n),a.setEndAfter(n),a}}}catch(u){N(u)}return null}function T(u,t){null===t||""===t?I(u):document.documentElement.style.setProperty(u,t,"important")}function I(u){document.documentElement.style.removeProperty(u)}function R(){var u=Array.prototype.slice.call(arguments).join(" ");Android.log(u)}function N(u){Android.logError(u,"",0)}function M(u,t){var e="undefined"!=typeof Symbol&&u[Symbol.iterator]||u["@@iterator"];if(!e){if(Array.isArray(u)||(e=function(u,t){if(u){if("string"==typeof u)return k(u,t);var e=Object.prototype.toString.call(u).slice(8,-1);return"Object"===e&&u.constructor&&(e=u.constructor.name),"Map"===e||"Set"===e?Array.from(u):"Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e)?k(u,t):void 0}}(u))||t&&u&&"number"==typeof u.length){e&&(u=e);var r=0,n=function(){};return{s:n,n:function(){return r>=u.length?{done:!0}:{done:!1,value:u[r++]}},e:function(u){throw u},f:n}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,D=!0,i=!1;return{s:function(){e=e.call(u)},n:function(){var u=e.next();return D=u.done,u},e:function(u){i=!0,o=u},f:function(){try{D||null==e.return||e.return()}finally{if(i)throw o}}}}function k(u,t){(null==t||t>u.length)&&(t=u.length);for(var e=0,r=new Array(t);e<t;e++)r[e]=u[e];return r}var $=!1;function L(u){var t=window.devicePixelRatio,e=u.width*t,r=u.height*t,n=u.left*t,o=u.top*t;return{width:e,height:r,left:n,top:o,right:n+e,bottom:o+r}}function U(u,t){var e,r=[],n=M(u.getClientRects());try{for(n.s();!(e=n.n()).done;){var o=e.value;r.push({bottom:o.bottom,height:o.height,left:o.left,right:o.right,top:o.top,width:o.width})}}catch(u){n.e(u)}finally{n.f()}for(var D=function(u,t){var e,r=new Set(u),n=M(u);try{for(n.s();!(e=n.n()).done;){var o=e.value;if(o.width>1&&o.height>1){var D,i=M(u);try{for(i.s();!(D=i.n()).done;){var a=D.value;if(o!==a&&r.has(a)&&G(a,o,1)){z("CLIENT RECT: remove contained"),r.delete(o);break}}}catch(u){i.e(u)}finally{i.f()}}else z("CLIENT RECT: remove tiny"),r.delete(o)}}catch(u){n.e(u)}finally{n.f()}return Array.from(r)}(_(r,1,t)),i=H(D),a=i.length-1;a>=0;a--){var F=i[a];if(!(F.width*F.height>4)){if(!(i.length>1)){z("CLIENT RECT: remove small, but keep otherwise empty!");break}z("CLIENT RECT: remove small"),i.splice(a,1)}}return z("CLIENT RECT: reduced ".concat(r.length," --\x3e ").concat(i.length)),i}function _(u,t,e){for(var r=0;r<u.length;r++)for(var n,o=function(){var n=u[r],o=u[D];if(n===o)return z("mergeTouchingRects rect1 === rect2 ??!"),0;var i=Y(n.top,o.top,t)&&Y(n.bottom,o.bottom,t),a=Y(n.left,o.left,t)&&Y(n.right,o.right,t);if((a&&!e||i&&!a)&&X(n,o,t)){z("CLIENT RECT: merging two into one, VERTICAL: ".concat(i," HORIZONTAL: ").concat(a," (").concat(e,")"));var F=u.filter((function(u){return u!==n&&u!==o})),c=W(n,o);return F.push(c),{v:_(F,t,e)}}},D=r+1;D<u.length;D++)if(0!==(n=o())&&n)return n.v;return u}function W(u,t){var e=Math.min(u.left,t.left),r=Math.max(u.right,t.right),n=Math.min(u.top,t.top),o=Math.max(u.bottom,t.bottom);return{bottom:o,height:o-n,left:e,right:r,top:n,width:r-e}}function G(u,t,e){return V(u,t.left,t.top,e)&&V(u,t.right,t.top,e)&&V(u,t.left,t.bottom,e)&&V(u,t.right,t.bottom,e)}function V(u,t,e,r){return(u.left<t||Y(u.left,t,r))&&(u.right>t||Y(u.right,t,r))&&(u.top<e||Y(u.top,e,r))&&(u.bottom>e||Y(u.bottom,e,r))}function H(u){for(var t=0;t<u.length;t++)for(var e,r=function(){var e=u[t],r=u[n];if(e===r)return z("replaceOverlapingRects rect1 === rect2 ??!"),0;if(X(e,r,-1)){var o,D=[],i=q(e,r);if(1===i.length)D=i,o=e;else{var a=q(r,e);i.length<a.length?(D=i,o=e):(D=a,o=r)}z("CLIENT RECT: overlap, cut one rect into ".concat(D.length));var F=u.filter((function(u){return u!==o}));return Array.prototype.push.apply(F,D),{v:H(F)}}},n=t+1;n<u.length;n++)if(0!==(e=r())&&e)return e.v;return u}function q(u,t){var e=function(u,t){var e=Math.max(u.left,t.left),r=Math.min(u.right,t.right),n=Math.max(u.top,t.top),o=Math.min(u.bottom,t.bottom);return{bottom:o,height:Math.max(0,o-n),left:e,right:r,top:n,width:Math.max(0,r-e)}}(t,u);if(0===e.height||0===e.width)return[u];var r=[],n={bottom:u.bottom,height:0,left:u.left,right:e.left,top:u.top,width:0};n.width=n.right-n.left,n.height=n.bottom-n.top,0!==n.height&&0!==n.width&&r.push(n);var o={bottom:e.top,height:0,left:e.left,right:e.right,top:u.top,width:0};o.width=o.right-o.left,o.height=o.bottom-o.top,0!==o.height&&0!==o.width&&r.push(o);var D={bottom:u.bottom,height:0,left:e.left,right:e.right,top:e.bottom,width:0};D.width=D.right-D.left,D.height=D.bottom-D.top,0!==D.height&&0!==D.width&&r.push(D);var i={bottom:u.bottom,height:0,left:e.right,right:u.right,top:u.top,width:0};return i.width=i.right-i.left,i.height=i.bottom-i.top,0!==i.height&&0!==i.width&&r.push(i),r}function X(u,t,e){return(u.left<t.right||e>=0&&Y(u.left,t.right,e))&&(t.left<u.right||e>=0&&Y(t.left,u.right,e))&&(u.top<t.bottom||e>=0&&Y(u.top,t.bottom,e))&&(t.top<u.bottom||e>=0&&Y(t.top,u.bottom,e))}function Y(u,t,e){return Math.abs(u-t)<=e}function z(){$&&R.apply(null,arguments)}function J(u,t){var e="undefined"!=typeof Symbol&&u[Symbol.iterator]||u["@@iterator"];if(!e){if(Array.isArray(u)||(e=Z(u))||t&&u&&"number"==typeof u.length){e&&(u=e);var r=0,n=function(){};return{s:n,n:function(){return r>=u.length?{done:!0}:{done:!1,value:u[r++]}},e:function(u){throw u},f:n}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,D=!0,i=!1;return{s:function(){e=e.call(u)},n:function(){var u=e.next();return D=u.done,u},e:function(u){i=!0,o=u},f:function(){try{D||null==e.return||e.return()}finally{if(i)throw o}}}}function K(u,t){return function(u){if(Array.isArray(u))return u}(u)||function(u,t){var e=null==u?null:"undefined"!=typeof Symbol&&u[Symbol.iterator]||u["@@iterator"];if(null!=e){var r,n,o,D,i=[],a=!0,F=!1;try{if(o=(e=e.call(u)).next,0===t){if(Object(e)!==e)return;a=!1}else for(;!(a=(r=o.call(e)).done)&&(i.push(r.value),i.length!==t);a=!0);}catch(u){F=!0,n=u}finally{try{if(!a&&null!=e.return&&(D=e.return(),Object(D)!==D))return}finally{if(F)throw n}}return i}}(u,t)||Z(u,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Z(u,t){if(u){if("string"==typeof u)return Q(u,t);var e=Object.prototype.toString.call(u).slice(8,-1);return"Object"===e&&u.constructor&&(e=u.constructor.name),"Map"===e||"Set"===e?Array.from(u):"Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e)?Q(u,t):void 0}}function Q(u,t){(null==t||t>u.length)&&(t=u.length);for(var e=0,r=new Array(t);e<t;e++)r[e]=u[e];return r}var uu,tu,eu=new Map,ru=new Map,nu=0;function ou(u){return u&&u instanceof Element}window.addEventListener("load",(function(){var u=document.body,t={width:0,height:0};new ResizeObserver((function(){t.width===u.clientWidth&&t.height===u.clientHeight||(t={width:u.clientWidth,height:u.clientHeight},ru.forEach((function(u){u.requestLayout()})))})).observe(u)}),!1),function(u){u.NONE="none",u.DESCENDANT="descendant",u.CHILD="child"}(uu||(uu={})),function(u){u.id="id",u.class="class",u.tag="tag",u.attribute="attribute",u.nthchild="nthchild",u.nthoftype="nthoftype"}(tu||(tu={}));const Du="CssSelectorGenerator";function iu(u="unknown problem",...t){console.warn(`${Du}: ${u}`,...t)}const au={selectors:[tu.id,tu.class,tu.tag,tu.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function Fu(u){return u instanceof RegExp}function cu(u){return["string","function"].includes(typeof u)||Fu(u)}function lu(u){return Array.isArray(u)?u.filter(cu):[]}function fu(u){const t=[Node.DOCUMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE,Node.ELEMENT_NODE];return function(u){return u instanceof Node}(u)&&t.includes(u.nodeType)}function su(u,t){if(fu(u))return u.contains(t)||iu("element root mismatch","Provided root does not contain the element. This will most likely result in producing a fallback selector using element's real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will nto work as intended."),u;const e=t.getRootNode({composed:!1});return fu(e)?(e!==document&&iu("shadow root inferred","You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended."),e):t.ownerDocument.querySelector(":root")}function Au(u){return"number"==typeof u?u:Number.POSITIVE_INFINITY}function pu(u=[]){const[t=[],...e]=u;return 0===e.length?t:e.reduce(((u,t)=>u.filter((u=>t.includes(u)))),t)}function Eu(u){return[].concat(...u)}function Cu(u){const t=u.map((u=>{if(Fu(u))return t=>u.test(t);if("function"==typeof u)return t=>{const e=u(t);return"boolean"!=typeof e?(iu("pattern matcher function invalid","Provided pattern matching function does not return boolean. It's result will be ignored.",u),!1):e};if("string"==typeof u){const t=new RegExp("^"+u.replace(/[|\\{}()[\]^$+?.]/g,"\\$&").replace(/\*/g,".+")+"$");return u=>t.test(u)}return iu("pattern matcher invalid","Pattern matching only accepts strings, regular expressions and/or functions. This item is invalid and will be ignored.",u),()=>!1}));return u=>t.some((t=>t(u)))}function yu(u,t,e){const r=Array.from(su(e,u[0]).querySelectorAll(t));return r.length===u.length&&u.every((u=>r.includes(u)))}function du(u,t){t=null!=t?t:function(u){return u.ownerDocument.querySelector(":root")}(u);const e=[];let r=u;for(;ou(r)&&r!==t;)e.push(r),r=r.parentElement;return e}function Bu(u,t){return pu(u.map((u=>du(u,t))))}const hu=" > ",gu=" ",mu={[uu.NONE]:{type:uu.NONE,value:""},[uu.DESCENDANT]:{type:uu.DESCENDANT,value:hu},[uu.CHILD]:{type:uu.CHILD,value:gu}},bu=new RegExp(["^$","\\s"].join("|")),vu=new RegExp(["^$"].join("|")),wu=[tu.nthoftype,tu.tag,tu.id,tu.class,tu.attribute,tu.nthchild],Su=Cu(["class","id","ng-*"]);function xu({nodeName:u}){return`[${u}]`}function Ou({nodeName:u,nodeValue:t}){return`[${u}='${Wu(t)}']`}function ju(u){const t=Array.from(u.attributes).filter((t=>function({nodeName:u},t){const e=t.tagName.toLowerCase();return!(["input","option"].includes(e)&&"value"===u||Su(u))}(t,u)));return[...t.map(xu),...t.map(Ou)]}function Pu(u){return(u.getAttribute("class")||"").trim().split(/\s+/).filter((u=>!vu.test(u))).map((u=>`.${Wu(u)}`))}function Tu(u){const t=u.getAttribute("id")||"",e=`#${Wu(t)}`,r=u.getRootNode({composed:!1});return!bu.test(t)&&yu([u],e,r)?[e]:[]}function Iu(u){const t=u.parentNode;if(t){const e=Array.from(t.childNodes).filter(ou).indexOf(u);if(e>-1)return[`:nth-child(${e+1})`]}return[]}function Ru(u){return[Wu(u.tagName.toLowerCase())]}function Nu(u){const t=[...new Set(Eu(u.map(Ru)))];return 0===t.length||t.length>1?[]:[t[0]]}function Mu(u){const t=Nu([u])[0],e=u.parentElement;if(e){const r=Array.from(e.children).filter((u=>u.tagName.toLowerCase()===t)),n=r.indexOf(u);if(n>-1)return[`${t}:nth-of-type(${n+1})`]}return[]}function ku(u=[],{maxResults:t=Number.POSITIVE_INFINITY}={}){const e=[];let r=0,n=Lu(1);for(;n.length<=u.length&&r<t;)r+=1,e.push(n.map((t=>u[t]))),n=$u(n,u.length-1);return e}function $u(u=[],t=0){const e=u.length;if(0===e)return[];const r=[...u];r[e-1]+=1;for(let u=e-1;u>=0;u--)if(r[u]>t){if(0===u)return Lu(e+1);r[u-1]++,r[u]=r[u-1]+1}return r[e-1]>t?Lu(e+1):r}function Lu(u=1){return Array.from(Array(u).keys())}const Uu=":".charCodeAt(0).toString(16).toUpperCase(),_u=/[ !"#$%&'()\[\]{|}<>*+,./;=?@^`~\\]/;function Wu(u=""){var t,e;return null!==(e=null===(t=null===CSS||void 0===CSS?void 0:CSS.escape)||void 0===t?void 0:t.call(CSS,u))&&void 0!==e?e:function(u=""){return u.split("").map((u=>":"===u?`\\${Uu} `:_u.test(u)?`\\${u}`:escape(u).replace(/%/g,"\\"))).join("")}(u)}const Gu={tag:Nu,id:function(u){return 0===u.length||u.length>1?[]:Tu(u[0])},class:function(u){return pu(u.map(Pu))},attribute:function(u){return pu(u.map(ju))},nthchild:function(u){return pu(u.map(Iu))},nthoftype:function(u){return pu(u.map(Mu))}},Vu={tag:Ru,id:Tu,class:Pu,attribute:ju,nthchild:Iu,nthoftype:Mu};function Hu(u){return u.includes(tu.tag)||u.includes(tu.nthoftype)?[...u]:[...u,tu.tag]}function qu(u={}){const t=[...wu];return u[tu.tag]&&u[tu.nthoftype]&&t.splice(t.indexOf(tu.tag),1),t.map((t=>{return(r=u)[e=t]?r[e].join(""):"";var e,r})).join("")}function Xu(u,t,e="",r){const n=function(u,t){return""===t?u:function(u,t){return[...u.map((u=>t+gu+u)),...u.map((u=>t+hu+u))]}(u,t)}(function(u,t,e){const r=function(u,t){const{blacklist:e,whitelist:r,combineWithinSelector:n,maxCombinations:o}=t,D=Cu(e),i=Cu(r);return function(u){const{selectors:t,includeTag:e}=u,r=[].concat(t);return e&&!r.includes("tag")&&r.push("tag"),r}(t).reduce(((t,e)=>{const r=function(u,t){var e;return(null!==(e=Gu[t])&&void 0!==e?e:()=>[])(u)}(u,e),a=function(u=[],t,e){return u.filter((u=>e(u)||!t(u)))}(r,D,i),F=function(u=[],t){return u.sort(((u,e)=>{const r=t(u),n=t(e);return r&&!n?-1:!r&&n?1:0}))}(a,i);return t[e]=n?ku(F,{maxResults:o}):F.map((u=>[u])),t}),{})}(u,e),n=function(u,t){return function(u){const{selectors:t,combineBetweenSelectors:e,includeTag:r,maxCandidates:n}=u,o=e?ku(t,{maxResults:n}):t.map((u=>[u]));return r?o.map(Hu):o}(t).map((t=>function(u,t){const e={};return u.forEach((u=>{const r=t[u];r.length>0&&(e[u]=r)})),function(u={}){let t=[];return Object.entries(u).forEach((([u,e])=>{t=e.flatMap((e=>0===t.length?[{[u]:e}]:t.map((t=>Object.assign(Object.assign({},t),{[u]:e})))))})),t}(e).map(qu)}(t,u))).filter((u=>u.length>0))}(r,e),o=Eu(n);return[...new Set(o)]}(u,r.root,r),e);for(const t of n)if(yu(u,t,r.root))return t;return null}function Yu(u){return{value:u,include:!1}}function zu({selectors:u,operator:t}){let e=[...wu];u[tu.tag]&&u[tu.nthoftype]&&(e=e.filter((u=>u!==tu.tag)));let r="";return e.forEach((t=>{(u[t]||[]).forEach((({value:u,include:t})=>{t&&(r+=u)}))})),t.value+r}function Ju(u){return[":root",...du(u).reverse().map((u=>{const t=function(u,t,e=uu.NONE){const r={};return t.forEach((t=>{Reflect.set(r,t,function(u,t){return Vu[t](u)}(u,t).map(Yu))})),{element:u,operator:mu[e],selectors:r}}(u,[tu.nthchild],uu.DESCENDANT);return t.selectors.nthchild.forEach((u=>{u.include=!0})),t})).map(zu)].join("")}function Ku(u,t={}){const e=function(u){const t=(Array.isArray(u)?u:[u]).filter(ou);return[...new Set(t)]}(u),r=function(u,t={}){const e=Object.assign(Object.assign({},au),t);return{selectors:(r=e.selectors,Array.isArray(r)?r.filter((u=>{return t=tu,e=u,Object.values(t).includes(e);var t,e})):[]),whitelist:lu(e.whitelist),blacklist:lu(e.blacklist),root:su(e.root,u),combineWithinSelector:!!e.combineWithinSelector,combineBetweenSelectors:!!e.combineBetweenSelectors,includeTag:!!e.includeTag,maxCombinations:Au(e.maxCombinations),maxCandidates:Au(e.maxCandidates)};var r}(e[0],t);let n="",o=r.root;function D(){return function(u,t,e="",r){if(0===u.length)return null;const n=[u.length>1?u:[],...Bu(u,t).map((u=>[u]))];for(const u of n){const t=Xu(u,0,e,r);if(t)return{foundElements:u,selector:t}}return null}(e,o,n,r)}let i=D();for(;i;){const{foundElements:u,selector:t}=i;if(yu(e,t,r.root))return t;o=u[0],n=t,i=D()}return e.length>1?e.map((u=>Ku(u,r))).join(", "):function(u){return u.map(Ju).join(", ")}(e)}function Zu(u){return null==u?null:-1!=["a","audio","button","canvas","details","input","label","option","select","submit","textarea","video"].indexOf(u.nodeName.toLowerCase())||u.hasAttribute("contenteditable")&&"false"!=u.getAttribute("contenteditable").toLowerCase()?u.outerHTML:u.parentElement?Zu(u.parentElement):null}function Qu(u){for(var t=0;t<u.children.length;t++){var e=u.children[t];if(!tt(e)&&ut(e))return Qu(e)}return u}function ut(u){if(readium.isFixedLayout)return!0;if(u===document.body||u===document.documentElement)return!0;if(!document||!document.documentElement||!document.body)return!1;var t=u.getBoundingClientRect();return v()?t.bottom>0&&t.top<window.innerHeight:t.right>0&&t.left<window.innerWidth}function tt(u){var t=getComputedStyle(u);if(t){if("block"!=t.getPropertyValue("display"))return!0;if("0"===t.getPropertyValue("opacity"))return!0}return!1}function et(u){if(window.getSelection().isCollapsed){var t=window.devicePixelRatio,e={defaultPrevented:u.defaultPrevented,x:u.clientX*t,y:u.clientY*t,targetElement:u.target.outerHTML,interactiveElement:Zu(u.target)};(function(u,t){if(0===ru.size)return!1;var e=function(){var t,e=J(ru);try{for(e.s();!(t=e.n()).done;){var r,n=K(t.value,2),o=n[0],D=J(n[1].items.reverse());try{for(D.s();!(r=D.n()).done;){var i=r.value;if(i.clickableElements){var a,F=J(i.clickableElements);try{for(F.s();!(a=F.n()).done;){var c=a.value,l=c.getBoundingClientRect().toJSON();if(V(l,u.clientX,u.clientY,1))return{group:o,item:i,element:c,rect:l}}}catch(u){F.e(u)}finally{F.f()}}}}catch(u){D.e(u)}finally{D.f()}}}catch(u){e.e(u)}finally{e.f()}}();return!!e&&Android.onDecorationActivated(JSON.stringify({id:e.item.decoration.id,group:e.group,rect:L(e.item.range.getBoundingClientRect()),click:t}))})(u,e)||Android.onTap(JSON.stringify(e))&&(u.stopPropagation(),u.preventDefault())}}function rt(u){return u.defaultPrevented||null!=Zu(document.activeElement)}function nt(u){u.stopPropagation(),u.preventDefault()}function ot(u,t){if(!u.repeat){var e={type:t,code:u.code,characters:String.fromCharCode(u.keyCode),alt:u.altKey,control:u.ctrlKey,shift:u.shiftKey,meta:u.metaKey};Android.onKey(JSON.stringify(e))}}window.addEventListener("DOMContentLoaded",(function(){document.addEventListener("click",et,!1),function(u){u.addEventListener("touchstart",(function(u){e=!0;var n=u.touches[0].clientX*r,o=u.touches[0].clientY*r;t={defaultPrevented:u.defaultPrevented,startX:n,startY:o,currentX:n,currentY:o,offsetX:0,offsetY:0,interactiveElement:Zu(u.target)}}),{passive:!1}),u.addEventListener("touchend",(function(u){t&&(Android.onDragEnd(JSON.stringify(t))&&(u.stopPropagation(),u.preventDefault()),t=void 0)}),{passive:!1}),u.addEventListener("touchmove",(function(u){if(t){t.currentX=u.touches[0].clientX*r,t.currentY=u.touches[0].clientY*r,t.offsetX=t.currentX-t.startX,t.offsetY=t.currentY-t.startY;var n=!1;e?(Math.abs(t.offsetX)>=6||Math.abs(t.offsetY)>=6)&&(e=!1,n=Android.onDragStart(JSON.stringify(t))):n=Android.onDragMove(JSON.stringify(t)),n&&(u.stopPropagation(),u.preventDefault())}}),{passive:!1});var t=void 0,e=!1,r=window.devicePixelRatio}(document)})),window.addEventListener("keydown",(function(u){rt(u)||(nt(u),ot(u,"down"))})),window.addEventListener("keyup",(function(u){rt(u)||(nt(u),ot(u,"up"))}));var Dt=e(5155);e.n(Dt)().shim();var it=!0;function at(){it&&R.apply(null,arguments)}window.addEventListener("load",(function(){var u=!1;document.addEventListener("selectionchange",(function(){var t=window.getSelection().isCollapsed;t&&u?(u=!1,Android.onSelectionEnd(),j()):t||u||(u=!0,Android.onSelectionStart())}))}),!1),window.readium={scrollToId:function(u){var t=document.getElementById(u);return!!t&&S(t.getBoundingClientRect())},scrollToPosition:function(u){if(u<0||u>1)throw"scrollToPosition() must be given a position from 0.0 to 1.0";var t;v()?(t=document.scrollingElement.scrollHeight*u,document.scrollingElement.scrollTop=t):(t=document.scrollingElement.scrollWidth*u*(w()?-1:1),document.scrollingElement.scrollLeft=O(t))},scrollToText:function(u){var t=P({text:u});return!!t&&(function(u){S(u.getBoundingClientRect())}(t),!0)},scrollLeft:function(){var u=document.scrollingElement.scrollWidth,t=window.scrollX-b,e=w()?-(u-b):0;return x(Math.max(t,e))},scrollRight:function(){var u=document.scrollingElement.scrollWidth,t=window.scrollX+b,e=w()?0:u-b;return x(Math.min(t,e))},scrollToStart:function(){v()?(document.scrollingElement.scrollTop=0,window.scrollTo(0,0)):document.scrollingElement.scrollLeft=0},scrollToEnd:function(){if(v())document.scrollingElement.scrollTop=document.body.scrollHeight,window.scrollTo(0,document.body.scrollHeight);else{var u=w()?-1:1;document.scrollingElement.scrollLeft=O(document.scrollingElement.scrollWidth*u)}},setCSSProperties:function(u){for(var t in u)T(t,u[t])},setProperty:T,removeProperty:I,getCurrentSelection:function(){var u=function(){var u=window.getSelection();if(u&&!u.isCollapsed){var t=u.toString();if(0!==t.trim().replace(/\n/g," ").replace(/\s\s+/g," ").length&&u.anchorNode&&u.focusNode){var e=1===u.rangeCount?u.getRangeAt(0):function(u,t,e,r){var n=new Range;if(n.setStart(u,t),n.setEnd(e,r),!n.collapsed)return n;at(">>> createOrderedRange COLLAPSED ... RANGE REVERSE?");var o=new Range;if(o.setStart(e,r),o.setEnd(u,t),!o.collapsed)return at(">>> createOrderedRange RANGE REVERSE OK."),n;at(">>> createOrderedRange RANGE REVERSE ALSO COLLAPSED?!")}(u.anchorNode,u.anchorOffset,u.focusNode,u.focusOffset);if(e&&!e.collapsed){var r=document.body.textContent,n=s.fromRange(e).relativeTo(document.body),o=n.start.offset,D=n.end.offset,i=r.slice(Math.max(0,o-200),o),a=i.search(/(?:[\0-@\[-`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u036F\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482-\u0489\u0530\u0557\u0558\u055A-\u055F\u0589-\u05CF\u05EB-\u05EE\u05F3-\u061F\u064B-\u066D\u0670\u06D4\u06D6-\u06E4\u06E7-\u06ED\u06F0-\u06F9\u06FD\u06FE\u0700-\u070F\u0711\u0730-\u074C\u07A6-\u07B0\u07B2-\u07C9\u07EB-\u07F3\u07F6-\u07F9\u07FB-\u07FF\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u083F\u0859-\u085F\u086B-\u086F\u0888\u088F-\u089F\u08CA-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962-\u0970\u0981-\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA-\u09BC\u09BE-\u09CD\u09CF-\u09DB\u09DE\u09E2-\u09EF\u09F2-\u09FB\u09FD-\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A-\u0A58\u0A5D\u0A5F-\u0A71\u0A75-\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA-\u0ABC\u0ABE-\u0ACF\u0AD1-\u0ADF\u0AE2-\u0AF8\u0AFA-\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A-\u0B3C\u0B3E-\u0B5B\u0B5E\u0B62-\u0B70\u0B72-\u0B82\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BCF\u0BD1-\u0C04\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C3E-\u0C57\u0C5B\u0C5C\u0C5E\u0C5F\u0C62-\u0C7F\u0C81-\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA-\u0CBC\u0CBE-\u0CDC\u0CDF\u0CE2-\u0CF0\u0CF3-\u0D03\u0D0D\u0D11\u0D3B\u0D3C\u0D3E-\u0D4D\u0D4F-\u0D53\u0D57-\u0D5E\u0D62-\u0D79\u0D80-\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0E00\u0E31\u0E34-\u0E3F\u0E47-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EB1\u0EB4-\u0EBC\u0EBE\u0EBF\u0EC5\u0EC7-\u0EDB\u0EE0-\u0EFF\u0F01-\u0F3F\u0F48\u0F6D-\u0F87\u0F8D-\u0FFF\u102B-\u103E\u1040-\u104F\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F-\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16F0\u16F9-\u16FF\u1712-\u171E\u1732-\u173F\u1752-\u175F\u176D\u1771-\u177F\u17B4-\u17D6\u17D8-\u17DB\u17DD-\u181F\u1879-\u187F\u1885\u1886\u18A9\u18AB-\u18AF\u18F6-\u18FF\u191F-\u194F\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19FF\u1A17-\u1A1F\u1A55-\u1AA6\u1AA8-\u1B04\u1B34-\u1B44\u1B4D-\u1B82\u1BA1-\u1BAD\u1BB0-\u1BB9\u1BE6-\u1BFF\u1C24-\u1C4C\u1C50-\u1C59\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CE8\u1CED\u1CF4\u1CF7-\u1CF9\u1CFB-\u1CFF\u1DC0-\u1DFF\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u2182\u2185-\u2BFF\u2CE5-\u2CEA\u2CEF-\u2CF1\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7F\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF-\u2E2E\u2E30-\u3004\u3007-\u3030\u3036-\u303A\u303D-\u3040\u3097-\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31C0-\u31EF\u3200-\u33FF\u4DC0-\u4DFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA620-\uA629\uA62C-\uA63F\uA66F-\uA67E\uA69E\uA69F\uA6E6-\uA716\uA720\uA721\uA789\uA78A\uA7CB-\uA7CF\uA7D2\uA7D4\uA7DA-\uA7F1\uA802\uA806\uA80B\uA823-\uA83F\uA874-\uA881\uA8B4-\uA8F1\uA8F8-\uA8FA\uA8FC\uA8FF-\uA909\uA926-\uA92F\uA947-\uA95F\uA97D-\uA983\uA9B3-\uA9CE\uA9D0-\uA9DF\uA9E5\uA9F0-\uA9F9\uA9FF\uAA29-\uAA3F\uAA43\uAA4C-\uAA5F\uAA77-\uAA79\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAC3-\uAADA\uAADE\uAADF\uAAEB-\uAAF1\uAAF5-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB6A-\uAB6F\uABE3-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB1E\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFE6F\uFE75\uFEFD-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEFF\uDF20-\uDF2C\uDF41\uDF4A-\uDF4F\uDF76-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0-\uDFFF]|\uD801[\uDC9E-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDD6F\uDD7B\uDD8B\uDD93\uDD96\uDDA2\uDDB2\uDDBA\uDDBD-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDF7F\uDF86\uDFB1\uDFBB-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE01-\uDE0F\uDE14\uDE18\uDE36-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE5-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD24-\uDE7F\uDEAA-\uDEAF\uDEB2-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF46-\uDF6F\uDF82-\uDFAF\uDFC5-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC00-\uDC02\uDC38-\uDC70\uDC73\uDC74\uDC76-\uDC82\uDCB0-\uDCCF\uDCE9-\uDD02\uDD27-\uDD43\uDD45\uDD46\uDD48-\uDD4F\uDD73-\uDD75\uDD77-\uDD82\uDDB3-\uDDC0\uDDC5-\uDDD9\uDDDB\uDDDD-\uDDFF\uDE12\uDE2C-\uDE3E\uDE41-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEDF-\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A-\uDF3C\uDF3E-\uDF4F\uDF51-\uDF5C\uDF62-\uDFFF]|\uD805[\uDC35-\uDC46\uDC4B-\uDC5E\uDC62-\uDC7F\uDCB0-\uDCC3\uDCC6\uDCC8-\uDD7F\uDDAF-\uDDD7\uDDDC-\uDDFF\uDE30-\uDE43\uDE45-\uDE7F\uDEAB-\uDEB7\uDEB9-\uDEFF\uDF1B-\uDF3F\uDF47-\uDFFF]|\uD806[\uDC2C-\uDC9F\uDCE0-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD30-\uDD3E\uDD40\uDD42-\uDD9F\uDDA8\uDDA9\uDDD1-\uDDE0\uDDE2\uDDE4-\uDDFF\uDE01-\uDE0A\uDE33-\uDE39\uDE3B-\uDE4F\uDE51-\uDE5B\uDE8A-\uDE9C\uDE9E-\uDEAF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC2F-\uDC3F\uDC41-\uDC71\uDC90-\uDCFF\uDD07\uDD0A\uDD31-\uDD45\uDD47-\uDD5F\uDD66\uDD69\uDD8A-\uDD97\uDD99-\uDEDF\uDEF3-\uDF01\uDF03\uDF11\uDF34-\uDFAF\uDFB1-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC00-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80E-\uD810\uD812-\uD819\uD824-\uD82A\uD82D\uD82E\uD830-\uD834\uD836\uD83C-\uD83F\uD87C\uD87D\uD87F\uD889-\uDBFF][\uDC00-\uDFFF]|\uD80B[\uDC00-\uDF8F\uDFF1-\uDFFF]|\uD80D[\uDC30-\uDC40\uDC47-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F-\uDE6F\uDEBF-\uDECF\uDEEE-\uDEFF\uDF30-\uDF3F\uDF44-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4F\uDF51-\uDF92\uDFA0-\uDFDF\uDFE2\uDFE4-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82B[\uDC00-\uDFEF\uDFF4\uDFFC\uDFFF]|\uD82C[\uDD23-\uDD31\uDD33-\uDD4F\uDD53\uDD54\uDD56-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC-\uDFFF]|\uD837[\uDC00-\uDEFF\uDF1F-\uDF24\uDF2B-\uDFFF]|\uD838[\uDC00-\uDC2F\uDC6E-\uDCFF\uDD2D-\uDD36\uDD3E-\uDD4D\uDD4F-\uDE8F\uDEAE-\uDEBF\uDEEC-\uDFFF]|\uD839[\uDC00-\uDCCF\uDCEC-\uDFDF\uDFE7\uDFEC\uDFEF\uDFFF]|\uD83A[\uDCC5-\uDCFF\uDD44-\uDD4A\uDD4C-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD869[\uDEE0-\uDEFF]|\uD86D[\uDF3A-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFEF]|\uD87B[\uDE5E-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDF4F]|\uD888[\uDFB0-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])(?:[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE3F\uDE40\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDF02\uDF04-\uDF10\uDF12-\uDF33\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883\uD885-\uD887][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F\uDC41-\uDC46]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD32\uDD50-\uDD52\uDD55\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E\uDF25-\uDF2A]|\uD838[\uDC30-\uDC6D\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDCD0-\uDCEB\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF39\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0\uDFF0-\uDFFF]|\uD87B[\uDC00-\uDE5D]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A\uDF50-\uDFFF]|\uD888[\uDC00-\uDFAF])/g);-1!==a&&(i=i.slice(a+1));var F=r.slice(D,Math.min(r.length,D+200)),c=Array.from(F.matchAll(/(?:[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE3F\uDE40\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDF02\uDF04-\uDF10\uDF12-\uDF33\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883\uD885-\uD887][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F\uDC41-\uDC46]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD32\uDD50-\uDD52\uDD55\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E\uDF25-\uDF2A]|\uD838[\uDC30-\uDC6D\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDCD0-\uDCEB\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF39\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0\uDFF0-\uDFFF]|\uD87B[\uDC00-\uDE5D]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A\uDF50-\uDFFF]|\uD888[\uDC00-\uDFAF])(?:[\0-@\[-`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u036F\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482-\u0489\u0530\u0557\u0558\u055A-\u055F\u0589-\u05CF\u05EB-\u05EE\u05F3-\u061F\u064B-\u066D\u0670\u06D4\u06D6-\u06E4\u06E7-\u06ED\u06F0-\u06F9\u06FD\u06FE\u0700-\u070F\u0711\u0730-\u074C\u07A6-\u07B0\u07B2-\u07C9\u07EB-\u07F3\u07F6-\u07F9\u07FB-\u07FF\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u083F\u0859-\u085F\u086B-\u086F\u0888\u088F-\u089F\u08CA-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962-\u0970\u0981-\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA-\u09BC\u09BE-\u09CD\u09CF-\u09DB\u09DE\u09E2-\u09EF\u09F2-\u09FB\u09FD-\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A-\u0A58\u0A5D\u0A5F-\u0A71\u0A75-\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA-\u0ABC\u0ABE-\u0ACF\u0AD1-\u0ADF\u0AE2-\u0AF8\u0AFA-\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A-\u0B3C\u0B3E-\u0B5B\u0B5E\u0B62-\u0B70\u0B72-\u0B82\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BCF\u0BD1-\u0C04\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C3E-\u0C57\u0C5B\u0C5C\u0C5E\u0C5F\u0C62-\u0C7F\u0C81-\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA-\u0CBC\u0CBE-\u0CDC\u0CDF\u0CE2-\u0CF0\u0CF3-\u0D03\u0D0D\u0D11\u0D3B\u0D3C\u0D3E-\u0D4D\u0D4F-\u0D53\u0D57-\u0D5E\u0D62-\u0D79\u0D80-\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0E00\u0E31\u0E34-\u0E3F\u0E47-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EB1\u0EB4-\u0EBC\u0EBE\u0EBF\u0EC5\u0EC7-\u0EDB\u0EE0-\u0EFF\u0F01-\u0F3F\u0F48\u0F6D-\u0F87\u0F8D-\u0FFF\u102B-\u103E\u1040-\u104F\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F-\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16F0\u16F9-\u16FF\u1712-\u171E\u1732-\u173F\u1752-\u175F\u176D\u1771-\u177F\u17B4-\u17D6\u17D8-\u17DB\u17DD-\u181F\u1879-\u187F\u1885\u1886\u18A9\u18AB-\u18AF\u18F6-\u18FF\u191F-\u194F\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19FF\u1A17-\u1A1F\u1A55-\u1AA6\u1AA8-\u1B04\u1B34-\u1B44\u1B4D-\u1B82\u1BA1-\u1BAD\u1BB0-\u1BB9\u1BE6-\u1BFF\u1C24-\u1C4C\u1C50-\u1C59\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CE8\u1CED\u1CF4\u1CF7-\u1CF9\u1CFB-\u1CFF\u1DC0-\u1DFF\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u2182\u2185-\u2BFF\u2CE5-\u2CEA\u2CEF-\u2CF1\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7F\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF-\u2E2E\u2E30-\u3004\u3007-\u3030\u3036-\u303A\u303D-\u3040\u3097-\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31C0-\u31EF\u3200-\u33FF\u4DC0-\u4DFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA620-\uA629\uA62C-\uA63F\uA66F-\uA67E\uA69E\uA69F\uA6E6-\uA716\uA720\uA721\uA789\uA78A\uA7CB-\uA7CF\uA7D2\uA7D4\uA7DA-\uA7F1\uA802\uA806\uA80B\uA823-\uA83F\uA874-\uA881\uA8B4-\uA8F1\uA8F8-\uA8FA\uA8FC\uA8FF-\uA909\uA926-\uA92F\uA947-\uA95F\uA97D-\uA983\uA9B3-\uA9CE\uA9D0-\uA9DF\uA9E5\uA9F0-\uA9F9\uA9FF\uAA29-\uAA3F\uAA43\uAA4C-\uAA5F\uAA77-\uAA79\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAC3-\uAADA\uAADE\uAADF\uAAEB-\uAAF1\uAAF5-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB6A-\uAB6F\uABE3-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB1E\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFE6F\uFE75\uFEFD-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEFF\uDF20-\uDF2C\uDF41\uDF4A-\uDF4F\uDF76-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0-\uDFFF]|\uD801[\uDC9E-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDD6F\uDD7B\uDD8B\uDD93\uDD96\uDDA2\uDDB2\uDDBA\uDDBD-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDF7F\uDF86\uDFB1\uDFBB-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE01-\uDE0F\uDE14\uDE18\uDE36-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE5-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD24-\uDE7F\uDEAA-\uDEAF\uDEB2-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF46-\uDF6F\uDF82-\uDFAF\uDFC5-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC00-\uDC02\uDC38-\uDC70\uDC73\uDC74\uDC76-\uDC82\uDCB0-\uDCCF\uDCE9-\uDD02\uDD27-\uDD43\uDD45\uDD46\uDD48-\uDD4F\uDD73-\uDD75\uDD77-\uDD82\uDDB3-\uDDC0\uDDC5-\uDDD9\uDDDB\uDDDD-\uDDFF\uDE12\uDE2C-\uDE3E\uDE41-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEDF-\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A-\uDF3C\uDF3E-\uDF4F\uDF51-\uDF5C\uDF62-\uDFFF]|\uD805[\uDC35-\uDC46\uDC4B-\uDC5E\uDC62-\uDC7F\uDCB0-\uDCC3\uDCC6\uDCC8-\uDD7F\uDDAF-\uDDD7\uDDDC-\uDDFF\uDE30-\uDE43\uDE45-\uDE7F\uDEAB-\uDEB7\uDEB9-\uDEFF\uDF1B-\uDF3F\uDF47-\uDFFF]|\uD806[\uDC2C-\uDC9F\uDCE0-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD30-\uDD3E\uDD40\uDD42-\uDD9F\uDDA8\uDDA9\uDDD1-\uDDE0\uDDE2\uDDE4-\uDDFF\uDE01-\uDE0A\uDE33-\uDE39\uDE3B-\uDE4F\uDE51-\uDE5B\uDE8A-\uDE9C\uDE9E-\uDEAF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC2F-\uDC3F\uDC41-\uDC71\uDC90-\uDCFF\uDD07\uDD0A\uDD31-\uDD45\uDD47-\uDD5F\uDD66\uDD69\uDD8A-\uDD97\uDD99-\uDEDF\uDEF3-\uDF01\uDF03\uDF11\uDF34-\uDFAF\uDFB1-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC00-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80E-\uD810\uD812-\uD819\uD824-\uD82A\uD82D\uD82E\uD830-\uD834\uD836\uD83C-\uD83F\uD87C\uD87D\uD87F\uD889-\uDBFF][\uDC00-\uDFFF]|\uD80B[\uDC00-\uDF8F\uDFF1-\uDFFF]|\uD80D[\uDC30-\uDC40\uDC47-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F-\uDE6F\uDEBF-\uDECF\uDEEE-\uDEFF\uDF30-\uDF3F\uDF44-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4F\uDF51-\uDF92\uDFA0-\uDFDF\uDFE2\uDFE4-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82B[\uDC00-\uDFEF\uDFF4\uDFFC\uDFFF]|\uD82C[\uDD23-\uDD31\uDD33-\uDD4F\uDD53\uDD54\uDD56-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC-\uDFFF]|\uD837[\uDC00-\uDEFF\uDF1F-\uDF24\uDF2B-\uDFFF]|\uD838[\uDC00-\uDC2F\uDC6E-\uDCFF\uDD2D-\uDD36\uDD3E-\uDD4D\uDD4F-\uDE8F\uDEAE-\uDEBF\uDEEC-\uDFFF]|\uD839[\uDC00-\uDCCF\uDCEC-\uDFDF\uDFE7\uDFEC\uDFEF\uDFFF]|\uD83A[\uDCC5-\uDCFF\uDD44-\uDD4A\uDD4C-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD869[\uDEE0-\uDEFF]|\uD86D[\uDF3A-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFEF]|\uD87B[\uDE5E-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDF4F]|\uD888[\uDFB0-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g)).pop();return void 0!==c&&c.index>1&&(F=F.slice(0,c.index+1)),{highlight:t,before:i,after:F}}at("$$$$$$$$$$$$$$$$$ CANNOT GET NON-COLLAPSED SELECTION RANGE?!")}}}();return u?{text:u,rect:function(){try{var u=window.getSelection();if(!u)return;return L(u.getRangeAt(0).getBoundingClientRect())}catch(u){return N(u),null}}()}:null},registerDecorationTemplates:function(u){for(var t="",e=0,r=Object.entries(u);e<r.length;e++){var n=K(r[e],2),o=n[0],D=n[1];eu.set(o,D),D.stylesheet&&(t+=D.stylesheet+"\n")}if(t){var i=document.createElement("style");i.innerHTML=t,document.getElementsByTagName("head")[0].appendChild(i)}},getDecorations:function(u){var t=ru.get(u);return t||(t=function(u,t){var e=[],r=0,n=null;function o(t){var n=u+"-"+r++,o=P(t.locator);if(o){var D={id:n,decoration:t,range:o};e.push(D),i(D)}else R("Can't locate DOM range for decoration",t)}function D(u){var t=e.findIndex((function(t){return t.decoration.id===u}));if(-1!==t){var r=e[t];e.splice(t,1),r.clickableElements=null,r.container&&(r.container.remove(),r.container=null)}}function i(e){var r=(n||((n=document.createElement("div")).setAttribute("id",u),n.setAttribute("data-group",t),n.style.setProperty("pointer-events","none"),document.body.append(n)),n),o=eu.get(e.decoration.style);if(o){var D=document.createElement("div");D.setAttribute("id",e.id),D.setAttribute("data-style",e.decoration.style),D.style.setProperty("pointer-events","none");var i,a=window.innerWidth,F=parseInt(getComputedStyle(document.documentElement).getPropertyValue("column-count")),c=a/(F||1),l=document.scrollingElement,f=l.scrollLeft,s=l.scrollTop,A=e.range.getBoundingClientRect();try{var p=document.createElement("template");p.innerHTML=e.decoration.element.trim(),i=p.content.firstElementChild}catch(u){return void N('Invalid decoration element "'.concat(e.decoration.element,'": ').concat(u.message))}if("boxes"===o.layout){var E,C=U(e.range,!0),y=J(C=C.sort((function(u,t){return u.top<t.top?-1:u.top>t.top?1:0})));try{for(y.s();!(E=y.n()).done;){var d=E.value,B=i.cloneNode(!0);B.style.setProperty("pointer-events","none"),g(B,d,A),D.append(B)}}catch(u){y.e(u)}finally{y.f()}}else if("bounds"===o.layout){var h=i.cloneNode(!0);h.style.setProperty("pointer-events","none"),g(h,A,A),D.append(h)}r.append(D),e.container=D,e.clickableElements=Array.from(D.querySelectorAll("[data-activable='1']")),0===e.clickableElements.length&&(e.clickableElements=Array.from(D.children))}else N("Unknown decoration style: ".concat(e.decoration.style));function g(u,t,e){if(u.style.position="absolute","wrap"===o.width)u.style.width="".concat(t.width,"px"),u.style.height="".concat(t.height,"px"),u.style.left="".concat(t.left+f,"px"),u.style.top="".concat(t.top+s,"px");else if("viewport"===o.width){u.style.width="".concat(a,"px"),u.style.height="".concat(t.height,"px");var r=Math.floor(t.left/a)*a;u.style.left="".concat(r+f,"px"),u.style.top="".concat(t.top+s,"px")}else if("bounds"===o.width)u.style.width="".concat(e.width,"px"),u.style.height="".concat(t.height,"px"),u.style.left="".concat(e.left+f,"px"),u.style.top="".concat(t.top+s,"px");else if("page"===o.width){u.style.width="".concat(c,"px"),u.style.height="".concat(t.height,"px");var n=Math.floor(t.left/c)*c;u.style.left="".concat(n+f,"px"),u.style.top="".concat(t.top+s,"px")}}}function a(){n&&(n.remove(),n=null)}return{add:o,remove:D,update:function(u){D(u.id),o(u)},clear:function(){a(),e.length=0},items:e,requestLayout:function(){a(),e.forEach((function(u){return i(u)}))}}}("r2-decoration-"+nu++,u),ru.set(u,t)),t},findFirstVisibleLocator:function(){var u=Qu(document.body);return{href:"#",type:"application/xhtml+xml",locations:{cssSelector:Ku(u)},text:{highlight:u.textContent}}}},window.readium.isReflowable=!0,document.addEventListener("DOMContentLoaded",(function(){var u=document.createElement("meta");u.setAttribute("name","viewport"),u.setAttribute("content","width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no"),document.head.appendChild(u)}))}()}(); +//# sourceMappingURL=readium-reflowable.js.map \ No newline at end of file diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/DecorableNavigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/DecorableNavigator.kt index 4798609188..b49146ef80 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/DecorableNavigator.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/DecorableNavigator.kt @@ -21,12 +21,13 @@ import org.json.JSONObject import org.readium.r2.shared.JSONable import org.readium.r2.shared.extensions.JSONParceler import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.util.Url /** * A navigator able to render arbitrary decorations over a publication. */ @ExperimentalDecorator -interface DecorableNavigator : Navigator { +public interface DecorableNavigator : Navigator { /** * Declares the current state of the decorations in the given decoration [group]. * @@ -35,7 +36,7 @@ interface DecorableNavigator : Navigator { * Name each decoration group as you see fit. A good practice is to use the name of the feature * requiring decorations, e.g. annotation, search, tts, etc. */ - suspend fun applyDecorations(decorations: List<Decoration>, group: String) + public suspend fun applyDecorations(decorations: List<Decoration>, group: String) /** * Indicates whether the Navigator supports the given decoration [style] class. @@ -44,19 +45,19 @@ interface DecorableNavigator : Navigator { * particular feature before enabling it. For example, underlining an audiobook does not make * sense, so an Audiobook Navigator would not support the `underline` decoration style. */ - fun <T : Decoration.Style> supportsDecorationStyle(style: KClass<T>): Boolean + public fun <T : Decoration.Style> supportsDecorationStyle(style: KClass<T>): Boolean /** * Registers a new [listener] for decoration interactions in the given [group]. */ - fun addDecorationListener(group: String, listener: Listener) + public fun addDecorationListener(group: String, listener: Listener) /** * Removes the given [listener] for all decoration interactions. */ - fun removeDecorationListener(listener: Listener) + public fun removeDecorationListener(listener: Listener) - interface Listener { + public interface Listener { /** * Called when the user activates a decoration, e.g. with a click or tap. @@ -64,7 +65,7 @@ interface DecorableNavigator : Navigator { * @param event Holds the metadata about the interaction event. * @return Whether the listener handled the interaction. */ - fun onDecorationActivated(event: OnActivatedEvent): Boolean + public fun onDecorationActivated(event: OnActivatedEvent): Boolean } /** @@ -77,7 +78,7 @@ interface DecorableNavigator : Navigator { * @param point Event point of the interaction, in the coordinate of the navigator view. This is * only useful in the context of a VisualNavigator. */ - data class OnActivatedEvent( + public data class OnActivatedEvent( val decoration: Decoration, val group: String, val rect: RectF? = null, @@ -98,7 +99,7 @@ interface DecorableNavigator : Navigator { */ @Parcelize @ExperimentalDecorator -data class Decoration( +public data class Decoration( val id: DecorationId, val locator: Locator, val style: Style, @@ -112,30 +113,31 @@ data class Decoration( * It is media type agnostic, meaning that each Navigator will translate the style into a set of * rendering instructions which makes sense for the resource type. */ - interface Style : Parcelable { + public interface Style : Parcelable { @Parcelize - data class Highlight( + public data class Highlight( @ColorInt override val tint: Int, override val isActive: Boolean = false ) : Style, Tinted, Activable + @Parcelize - data class Underline( + public data class Underline( @ColorInt override val tint: Int, override val isActive: Boolean = false ) : Style, Tinted, Activable /** A type of [Style] which has a tint color. */ - interface Tinted { - @get:ColorInt val tint: Int + public interface Tinted { + @get:ColorInt public val tint: Int } /** A type of [Style] which can be in an "active" state. */ - interface Activable { - val isActive: Boolean + public interface Activable { + public val isActive: Boolean } } - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { put("id", id) put("locator", locator.toJSON()) putOpt("style", style::class.qualifiedName) @@ -144,15 +146,15 @@ data class Decoration( /** Unique identifier for a decoration. */ @ExperimentalDecorator -typealias DecorationId = String +public typealias DecorationId = String /** Represents an atomic change in a list of [Decoration] objects. */ @ExperimentalDecorator -sealed class DecorationChange { - data class Added(val decoration: Decoration) : DecorationChange() - data class Updated(val decoration: Decoration) : DecorationChange() - data class Moved(val id: DecorationId, val fromPosition: Int, val toPosition: Int) : DecorationChange() - data class Removed(val id: DecorationId) : DecorationChange() +public sealed class DecorationChange { + public data class Added(val decoration: Decoration) : DecorationChange() + public data class Updated(val decoration: Decoration) : DecorationChange() + public data class Moved(val id: DecorationId, val fromPosition: Int, val toPosition: Int) : DecorationChange() + public data class Removed(val id: DecorationId) : DecorationChange() } /** @@ -161,7 +163,9 @@ sealed class DecorationChange { * The changes need to be applied in the same order, one by one. */ @ExperimentalDecorator -suspend fun List<Decoration>.changesByHref(target: List<Decoration>): Map<String, List<DecorationChange>> = withContext(Dispatchers.Default) { +public suspend fun List<Decoration>.changesByHref(target: List<Decoration>): Map<Url, List<DecorationChange>> = withContext( + Dispatchers.Default +) { val source = this@changesByHref val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { override fun getOldListSize(): Int = source.size @@ -179,7 +183,7 @@ suspend fun List<Decoration>.changesByHref(target: List<Decoration>): Map<String } }) - val changes = mutableMapOf<String, List<DecorationChange>>() + val changes = mutableMapOf<Url, List<DecorationChange>>() fun registerChange(change: DecorationChange, locator: Locator) { val resourceChanges = changes[locator.href] ?: emptyList() @@ -203,7 +207,14 @@ suspend fun List<Decoration>.changesByHref(target: List<Decoration>): Map<String override fun onMoved(fromPosition: Int, toPosition: Int) { val decoration = target[toPosition] - registerChange(DecorationChange.Moved(decoration.id, fromPosition = fromPosition, toPosition = toPosition), decoration.locator) + registerChange( + DecorationChange.Moved( + decoration.id, + fromPosition = fromPosition, + toPosition = toPosition + ), + decoration.locator + ) } override fun onChanged(position: Int, count: Int, payload: Any?) { diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/Deprecated.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/Deprecated.kt new file mode 100644 index 0000000000..62fbe79ddd --- /dev/null +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/Deprecated.kt @@ -0,0 +1,103 @@ +/* + * Module: r2-navigator-kotlin + * Developers: Aferdita Muriqi + * + * Copyright (c) 2019. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.navigator + +import kotlin.time.Duration +import kotlinx.coroutines.flow.Flow +import org.readium.r2.navigator.media.MediaPlayback + +@Deprecated("Use navigator fragments.", level = DeprecationLevel.ERROR) +public interface IR2Activity + +@Deprecated("Use TtsNavigator.", level = DeprecationLevel.ERROR) +public interface IR2TTS + +/** + * A navigator rendering an audio or video publication. + */ +@Deprecated("Use the new readium-navigator-media modules.") +@OptIn(ExperimentalAudiobook::class) +public interface MediaNavigator : Navigator { + + /** + * Current playback information. + */ + public val playback: Flow<MediaPlayback> + + /** + * Indicates whether the navigator is currently playing. + */ + public val isPlaying: Boolean + + /** + * Sets the speed of the media playback. + * + * Normal speed is 1.0 and 0.0 is incorrect. + */ + public fun setPlaybackRate(rate: Double) + + /** + * Resumes or start the playback at the current location. + */ + public fun play() + + /** + * Pauses the playback. + */ + public fun pause() + + /** + * Toggles the playback. + * Can be useful as a handler for play/pause button. + */ + public fun playPause() + + /** + * Stops the playback. + * + * Compared to [pause], the navigator may clear its state in whatever way is appropriate. For + * example, recovering a player's resources. + */ + public fun stop() + + /** + * Seeks to the given time in the current resource. + */ + public fun seekTo(position: Duration) + + /** + * Seeks relatively from the current position in the current resource. + */ + public fun seekRelative(offset: Duration) + + public interface Listener : Navigator.Listener +} + +/** + * Moves to the left content portion (eg. page) relative to the reading progression direction. + */ +@Deprecated( + "Use a DirectionalNavigationAdapter or goFoward and goBackward.", + level = DeprecationLevel.ERROR +) +public fun VisualNavigator.goLeft(animated: Boolean = false, completion: () -> Unit = {}): Boolean { + throw NotImplementedError() +} + +/** + * Moves to the right content portion (eg. page) relative to the reading progression direction. + */ +@Deprecated( + "Use a DirectionalNavigationAdapter or goFoward and goBackward.", + level = DeprecationLevel.ERROR +) +public fun VisualNavigator.goRight(animated: Boolean = false, completion: () -> Unit = {}): Boolean { + throw NotImplementedError() +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/DummyPublication.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/DummyPublication.kt index 4a03933ae1..5927d98d1f 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/DummyPublication.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/DummyPublication.kt @@ -5,7 +5,7 @@ import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Metadata import org.readium.r2.shared.publication.Publication -object RestorationNotSupportedException : Exception( +public object RestorationNotSupportedException : Exception( "Restoration of the navigator fragment after process death is not supported. You must pop it from the back stack or finish the host Activity before `onResume`." ) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/GlobalVars.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/GlobalVars.kt index 875e3791ae..97207f501c 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/GlobalVars.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/GlobalVars.kt @@ -16,5 +16,5 @@ package org.readium.r2.navigator /** * Global Parameters */ -@Deprecated("Use Publication::localBaseUrlOf() instead") -const val BASE_URL = "http://127.0.0.1" +@Deprecated("Use Publication::localBaseUrlOf() instead", level = DeprecationLevel.ERROR) +public const val BASE_URL: String = "http://127.0.0.1" diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/HyperlinkNavigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/HyperlinkNavigator.kt new file mode 100644 index 0000000000..14b5c23f19 --- /dev/null +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/HyperlinkNavigator.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.navigator + +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.util.AbsoluteUrl + +/** + * A navigator supporting hyperlinks. + */ +@ExperimentalReadiumApi +public interface HyperlinkNavigator : Navigator { + + @ExperimentalReadiumApi + public interface Listener : Navigator.Listener { + + /** + * Called when a link to an internal resource was clicked in the navigator. + * + * You can use this callback to perform custom navigation like opening a new window + * or other operations. + * + * By returning false the navigator wont try to open the link itself and it is up + * to the calling app to decide how to display the link. + */ + @ExperimentalReadiumApi + public fun shouldFollowInternalLink(link: Link): Boolean { return true } + + /** + * Called when a link to an external URL was activated in the navigator. + * + * If it is an HTTP URL, you should open it with a `CustomTabsIntent` or `WebView`, for + * example: + * + * ```kotlin + * override fun onExternalLinkActivated(url: AbsoluteUrl) { + * if (!url.isHttp) return + * + * val context = requireActivity() + * val uri = url.toUri() + * + * try { + * CustomTabsIntent.Builder() + * .build() + * .launchUrl(context, uri) + * } catch (e: ActivityNotFoundException) { + * context.startActivity(Intent(Intent.ACTION_VIEW, uri)) + * } + * } + * ``` + */ + @ExperimentalReadiumApi + public fun onExternalLinkActivated(url: AbsoluteUrl) + } +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/IR2Activity.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/IR2Activity.kt deleted file mode 100644 index 9304103cf8..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/IR2Activity.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Module: r2-navigator-kotlin - * Developers: Aferdita Muriqi - * - * Copyright (c) 2019. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.navigator - -import android.content.SharedPreferences -import android.view.View -import org.readium.r2.navigator.pager.R2ViewPager -import org.readium.r2.shared.publication.Publication - -interface IR2Activity { - - val publication: Publication - val preferences: SharedPreferences - val publicationIdentifier: String - val publicationFileName: String - val publicationPath: String - val bookId: Long - val resourcePager: R2ViewPager? - get() = null - val allowToggleActionBar: Boolean - get() = true - - fun toggleActionBar() {} - fun toggleActionBar(v: View? = null) {} - fun nextResource(v: View? = null) {} - fun previousResource(v: View? = null) {} - fun onPageChanged(pageIndex: Int, totalPages: Int, url: String) {} - fun onPageEnded(end: Boolean) {} - fun onPageLoaded() {} - fun highlightActivated(id: String) {} - fun highlightAnnotationMarkActivated(id: String) {} -} - -interface IR2TTS { - fun playTextChanged(text: String) {} - fun playStateChanged(playing: Boolean) {} - fun dismissScreenReader() {} -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt index 498415acb4..77d2819224 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt @@ -6,19 +6,11 @@ package org.readium.r2.navigator -import android.graphics.PointF -import kotlin.time.Duration -import kotlin.time.ExperimentalTime -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import org.readium.r2.navigator.media.MediaPlayback -import org.readium.r2.navigator.preferences.Axis -import org.readium.r2.navigator.preferences.ReadingProgression -import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.ReadingProgression as PublicationReadingProgression +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError /** * Base interface for a navigator rendering a publication. @@ -32,48 +24,36 @@ import org.readium.r2.shared.publication.ReadingProgression as PublicationReadin * and provide the initial location when creating the navigator. * - **User accessibility settings should override the behavior when needed** (eg. disabling * animated transition, even when requested by the caller). - * - **The navigator is the single source of truth for the current location.** So for example, the - * TTS should observe the position from the navigator instead of having the reading app move - * manually both the navigator and the TTS reader when the user skips forward. + * - **The navigator is the single source of truth for the current location.** * - **The navigator should only provide a minimal gestures/interactions set.** For example, * scrolling through a web view or zooming a fixed image is expected from the user. But additional * interactions such as tapping/clicking the edge of the page to skip to the next one should be * implemented by the reading app, and not the navigator. */ -interface Navigator { - - /** - * Publication rendered by this navigator. - */ - val publication: Publication +public interface Navigator { /** * Current position in the publication. * Can be used to save a bookmark to the current position. */ - val currentLocator: StateFlow<Locator> + public val currentLocator: StateFlow<Locator> /** * Moves to the position in the publication corresponding to the given [Locator]. */ - fun go(locator: Locator, animated: Boolean = false, completion: () -> Unit = {}): Boolean + public fun go(locator: Locator, animated: Boolean = false, completion: () -> Unit = {}): Boolean /** * Moves to the position in the publication targeted by the given link. */ - fun go(link: Link, animated: Boolean = false, completion: () -> Unit = {}): Boolean + public fun go(link: Link, animated: Boolean = false, completion: () -> Unit = {}): Boolean - /** - * Moves to the next content portion (eg. page) in the reading progression direction. - */ - fun goForward(animated: Boolean = false, completion: () -> Unit = {}): Boolean + public interface Listener { - /** - * Moves to the previous content portion (eg. page) in the reading progression direction. - */ - fun goBackward(animated: Boolean = false, completion: () -> Unit = {}): Boolean - - interface Listener { + /** + * Called when a publication resource failed to be loaded. + */ + public fun onResourceLoadFailed(href: Url, error: ReadError) {} /** * Called when the navigator jumps to an explicit location, which might break the linear @@ -85,200 +65,25 @@ interface Navigator { * You can use this callback to implement a navigation history by differentiating between * continuous and discontinuous moves. */ - fun onJumpToLocator(locator: Locator) {} + public fun onJumpToLocator(locator: Locator) {} } - @Deprecated("Use [currentLocator.value] instead", ReplaceWith("currentLocator.value")) - val currentLocation: Locator? get() = currentLocator.value - @Deprecated("Use [VisualNavigator.Listener] instead", ReplaceWith("VisualNavigator.Listener")) - interface VisualListener : VisualNavigator.Listener + @Deprecated( + "Use [currentLocator.value] instead", + ReplaceWith("currentLocator.value"), + level = DeprecationLevel.ERROR + ) + public val currentLocation: Locator? get() = currentLocator.value + + @Deprecated( + "Use [VisualNavigator.Listener] instead", + ReplaceWith("VisualNavigator.Listener"), + level = DeprecationLevel.ERROR + ) + public interface VisualListener : VisualNavigator.Listener } -interface NavigatorDelegate { - @Deprecated("Observe [currentLocator] instead") - fun locationDidChange(navigator: Navigator? = null, locator: Locator) {} -} - -/** - * A navigator rendering the publication visually on-screen. - */ -interface VisualNavigator : Navigator { - - /** - * Current presentation rendered by the navigator. - */ - @ExperimentalReadiumApi - val presentation: StateFlow<Presentation> - - /** - * Returns the [Locator] to the first content element that begins on the current screen. - */ - @ExperimentalReadiumApi - suspend fun firstVisibleElementLocator(): Locator? = - currentLocator.value - - @ExperimentalReadiumApi - interface Presentation { - /** - * Horizontal direction of progression across resources. - */ - val readingProgression: ReadingProgression - - /** - * If the overflow of the content is managed through scroll instead of pagination. - */ - val scroll: Boolean - - /** - * Main axis along which the resources are laid out. - */ - val axis: Axis - } - - interface Listener : Navigator.Listener { - - /** - * Called when a link to an internal resource was clicked in the navigator. - * - * You can use this callback to perform custom navigation like opening a new window - * or other operations. - * - * By returning false the navigator wont try to open the link itself and it is up - * to the calling app to decide how to display the link. - */ - fun shouldJumpToLink(link: Link): Boolean { return true } - - /** - * Called when the user tapped the content, but nothing handled the event internally (eg. - * by following an internal link). - * - * Can be used in the reading app to toggle the navigation bars, or switch to the - * previous/next page if the tapped occurred on the edges. - * - * The [point] is relative to the navigator's view. - */ - fun onTap(point: PointF): Boolean = false - - /** - * Called when the user starts dragging the content, but nothing handled the event - * internally. - * - * The points are relative to the navigator's view. - */ - @ExperimentalDragGesture - fun onDragStart(startPoint: PointF, offset: PointF): Boolean = false - - /** - * Called when the user continues dragging the content, but nothing handled the event - * internally. - * - * The points are relative to the navigator's view. - */ - @ExperimentalDragGesture - fun onDragMove(startPoint: PointF, offset: PointF): Boolean = false - - /** - * Called when the user stops dragging the content, but nothing handled the event - * internally. - * - * The points are relative to the navigator's view. - */ - @ExperimentalDragGesture - fun onDragEnd(startPoint: PointF, offset: PointF): Boolean = false - } - - /** - * Current reading progression direction. - */ - @Deprecated("Use `presentation.value.readingProgression` instead", ReplaceWith("presentation.value.readingProgression")) - val readingProgression: PublicationReadingProgression -} - -/** - * Moves to the left content portion (eg. page) relative to the reading progression direction. - */ -@OptIn(ExperimentalReadiumApi::class) -fun VisualNavigator.goLeft(animated: Boolean = false, completion: () -> Unit = {}): Boolean { - return when (presentation.value.readingProgression) { - ReadingProgression.LTR -> - goBackward(animated = animated, completion = completion) - - ReadingProgression.RTL -> - goForward(animated = animated, completion = completion) - } -} - -/** - * Moves to the right content portion (eg. page) relative to the reading progression direction. - */ -@OptIn(ExperimentalReadiumApi::class) -fun VisualNavigator.goRight(animated: Boolean = false, completion: () -> Unit = {}): Boolean { - return when (presentation.value.readingProgression) { - ReadingProgression.LTR -> - goForward(animated = animated, completion = completion) - - ReadingProgression.RTL -> - goBackward(animated = animated, completion = completion) - } -} - -/** - * A navigator rendering an audio or video publication. - */ -@OptIn(ExperimentalTime::class) -@ExperimentalAudiobook -interface MediaNavigator : Navigator { - - /** - * Current playback information. - */ - val playback: Flow<MediaPlayback> - - /** - * Indicates whether the navigator is currently playing. - */ - val isPlaying: Boolean - - /** - * Sets the speed of the media playback. - * - * Normal speed is 1.0 and 0.0 is incorrect. - */ - fun setPlaybackRate(rate: Double) - - /** - * Resumes or start the playback at the current location. - */ - fun play() - - /** - * Pauses the playback. - */ - fun pause() - - /** - * Toggles the playback. - * Can be useful as a handler for play/pause button. - */ - fun playPause() - - /** - * Stops the playback. - * - * Compared to [pause], the navigator may clear its state in whatever way is appropriate. For - * example, recovering a player's resources. - */ - fun stop() - - /** - * Seeks to the given time in the current resource. - */ - fun seekTo(position: Duration) - - /** - * Seeks relatively from the current position in the current resource. - */ - fun seekRelative(offset: Duration) - - interface Listener : Navigator.Listener +public interface NavigatorDelegate { + @Deprecated("Observe [currentLocator] instead", level = DeprecationLevel.ERROR) + public fun locationDidChange(navigator: Navigator? = null, locator: Locator) {} } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/NavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/NavigatorFragment.kt new file mode 100644 index 0000000000..cb945295b6 --- /dev/null +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/NavigatorFragment.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.navigator + +import android.os.Bundle +import androidx.fragment.app.Fragment +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.services.isRestricted + +public abstract class NavigatorFragment internal constructor( + protected val publication: Publication +) : Fragment(), Navigator { + + override fun onCreate(savedInstanceState: Bundle?) { + require(!publication.isRestricted) { "The provided publication is restricted. Check that any DRM was properly unlocked using a Content Protection." } + + super.onCreate(savedInstanceState) + } +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/OptIn.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/OptIn.kt index 38702fa7de..f0b7e67406 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/OptIn.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/OptIn.kt @@ -11,21 +11,36 @@ package org.readium.r2.navigator message = "Support for the Decorator API is still experimental. The API may be changed in the future without notice." ) @Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY) -annotation class ExperimentalDecorator +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS, + AnnotationTarget.PROPERTY +) +public annotation class ExperimentalDecorator @RequiresOptIn( level = RequiresOptIn.Level.WARNING, message = "The new Audiobook navigator is still experimental. The API may be changed in the future without notice." ) @Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY) -annotation class ExperimentalAudiobook +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS, + AnnotationTarget.PROPERTY +) +public annotation class ExperimentalAudiobook @RequiresOptIn( level = RequiresOptIn.Level.WARNING, message = "The new dragging gesture is still experimental. The API may be changed in the future without notice." ) @Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY) -annotation class ExperimentalDragGesture +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS, + AnnotationTarget.PROPERTY +) +public annotation class ExperimentalDragGesture diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt index 58cfd18d27..817f0d8c9b 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt @@ -35,36 +35,41 @@ import org.json.JSONObject import org.jsoup.Jsoup import org.jsoup.safety.Safelist import org.readium.r2.navigator.extensions.optRectF +import org.readium.r2.navigator.input.InputModifier +import org.readium.r2.navigator.input.Key +import org.readium.r2.navigator.input.KeyEvent +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.extensions.optNullableString import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.extensions.tryOrNull -import org.readium.r2.shared.fetcher.Resource import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.ReadingProgression -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.decodeString +import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.use import timber.log.Timber -@OptIn(ExperimentalDecorator::class) -open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(context, attrs) { +@OptIn(ExperimentalDecorator::class, ExperimentalReadiumApi::class) +internal open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(context, attrs) { interface Listener { val readingProgression: ReadingProgression fun onResourceLoaded(link: Link?, webView: R2BasicWebView, url: String?) {} - fun onPageLoaded() - fun onPageChanged(pageIndex: Int, totalPages: Int, url: String) - fun onPageEnded(end: Boolean) - fun onScroll() - fun onTap(point: PointF): Boolean - fun onDragStart(event: DragEvent): Boolean - fun onDragMove(event: DragEvent): Boolean - fun onDragEnd(event: DragEvent): Boolean + fun onPageLoaded() {} + fun onPageChanged(pageIndex: Int, totalPages: Int, url: String) {} + fun onPageEnded(end: Boolean) {} + fun onTap(point: PointF): Boolean = false + fun onDragStart(event: DragEvent): Boolean = false + fun onDragMove(event: DragEvent): Boolean = false + fun onDragEnd(event: DragEvent): Boolean = false + fun onKey(event: KeyEvent): Boolean = false fun onDecorationActivated(id: DecorationId, group: String, rect: RectF, point: PointF): Boolean = false - fun onProgressionChanged() - fun onHighlightActivated(id: String) - fun onHighlightAnnotationMarkActivated(id: String) + fun onProgressionChanged() {} fun goForward(animated: Boolean = false, completion: () -> Unit = {}): Boolean = false fun goBackward(animated: Boolean = false, completion: () -> Unit = {}): Boolean = false @@ -75,12 +80,15 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte @InternalReadiumApi fun javascriptInterfacesForResource(link: Link): Map<String, Any?> = emptyMap() + @InternalReadiumApi fun shouldOverrideUrlLoading(webView: WebView, request: WebResourceRequest): Boolean = false + @InternalReadiumApi fun shouldInterceptRequest(webView: WebView, request: WebResourceRequest): WebResourceResponse? = null + @InternalReadiumApi - fun resourceAtUrl(url: String): Resource? = null + fun resourceAtUrl(url: Url): Resource? = null /** * Requests to load the next resource in the reading order. @@ -90,15 +98,24 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte */ @InternalReadiumApi fun goToNextResource(jump: Boolean, animated: Boolean): Boolean = false + @InternalReadiumApi fun goToPreviousResource(jump: Boolean, animated: Boolean): Boolean = false + + @Deprecated("Not available anymore", level = DeprecationLevel.ERROR) + fun onScroll() {} + + @Deprecated("Not available anymore", level = DeprecationLevel.ERROR) + fun onHighlightActivated(id: String) {} + + @Deprecated("Not available anymore", level = DeprecationLevel.ERROR) + fun onHighlightAnnotationMarkActivated(id: String) {} } var listener: Listener? = null internal var preferences: SharedPreferences? = null - internal var useLegacySettings: Boolean = false - var resourceUrl: String? = null + var resourceUrl: Url? = null internal val scrollModeFlow = MutableStateFlow(false) @@ -181,13 +198,9 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte super.destroy() } - @android.webkit.JavascriptInterface open fun scrollRight(animated: Boolean = false) { uiScope.launch { val listener = listener ?: return@launch - listener.onScroll() - - val isRtl = (listener.readingProgression == ReadingProgression.RTL) fun goRight(jump: Boolean) { if (listener.readingProgression == ReadingProgression.RTL) { @@ -216,11 +229,9 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte } } - @android.webkit.JavascriptInterface open fun scrollLeft(animated: Boolean = false) { uiScope.launch { val listener = listener ?: return@launch - listener.onScroll() fun goLeft(jump: Boolean) { if (listener.readingProgression == ReadingProgression.RTL) { @@ -249,10 +260,7 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte } } - /** - * Called from the JS code when a tap is detected. - * If the JS indicates the tap is being handled within the web view, don't take action, - * + /* * Returns whether the web view should prevent the default behavior for this tap. */ @android.webkit.JavascriptInterface @@ -277,23 +285,7 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte return handleFootnote(event.targetElement) } - // Skips to previous/next pages if the tap is on the content edges. - val clientWidth = computeHorizontalScrollExtent() - val thresholdRange = 0.0..(0.2 * clientWidth) - - // FIXME: Call listener.onTap if scrollLeft|Right fails - return when { - useLegacySettings && thresholdRange.contains(event.point.x) -> { - scrollLeft(false) - true - } - useLegacySettings && thresholdRange.contains(clientWidth - event.point.x) -> { - scrollRight(false) - true - } - else -> - runBlocking(uiScope.coroutineContext) { listener?.onTap(event.point) ?: false } - } + return runBlocking(uiScope.coroutineContext) { listener?.onTap(event.point) ?: false } } /** @@ -347,22 +339,21 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte val href = tryOrNull { Jsoup.parse(html) } ?.select("a[epub:type=noteref]")?.first() ?.attr("href") + ?.let { Url(it) } ?: return false - val id = href.substringAfter("#", missingDelimiterValue = "") - .takeIf { it.isNotBlank() } - ?: return false + val id = href.fragment ?: return false - val absoluteUrl = Href(href, baseHref = resourceUrl).percentEncodedString - .substringBefore("#") + val absoluteUrl = resourceUrl.resolve(href).removeFragment() val aside = runBlocking { tryOrLog { listener?.resourceAtUrl(absoluteUrl) ?.use { res -> - res.readAsString() + res.read() + .flatMap { it.decodeString() } .map { Jsoup.parse(it) } - .getOrThrow() + .getOrNull() } ?.select("#$id") ?.first()?.html() @@ -375,7 +366,7 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater // Inflate the custom layout/view - val customView = inflater.inflate(R.layout.popup_footnote, null) + val customView = inflater.inflate(R.layout.readium_navigator_popup_footnote, null) // Initialize a new instance of popup window val mPopupWindow = PopupWindow( @@ -388,9 +379,7 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte // Set an elevation value for popup window // Call requires API level 21 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mPopupWindow.elevation = 5.0f - } + mPopupWindow.elevation = 5.0f val textView = customView.findViewById(R.id.footnote) as TextView if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -439,6 +428,23 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte return runBlocking(uiScope.coroutineContext) { listener?.onDragEnd(event) ?: false } } + @android.webkit.JavascriptInterface + fun onKey(eventJson: String): Boolean { + val jsonObject = JSONObject(eventJson) + val event = KeyEvent( + type = when (jsonObject.optString("type")) { + "down" -> KeyEvent.Type.Down + "up" -> KeyEvent.Type.Up + else -> return false + }, + key = Key(jsonObject.optString("code")), + modifiers = inputModifiers(jsonObject), + characters = jsonObject.optNullableString("characters")?.takeUnless { it.isBlank() } + ) + + return listener?.onKey(event) ?: false + } + @android.webkit.JavascriptInterface fun onSelectionStart() { isSelecting = true @@ -464,9 +470,6 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte fun fromJSONObject(obj: JSONObject?): DragEvent? { obj ?: return null - val x = obj.optDouble("x").toFloat() - val y = obj.optDouble("y").toFloat() - return DragEvent( defaultPrevented = obj.optBoolean("defaultPrevented"), startPoint = PointF( @@ -503,20 +506,6 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte Timber.d("JavaScript: $message") } - @android.webkit.JavascriptInterface - fun highlightActivated(id: String) { - uiScope.launch { - listener?.onHighlightActivated(id) - } - } - - @android.webkit.JavascriptInterface - fun highlightAnnotationMarkActivated(id: String) { - uiScope.launch { - listener?.onHighlightAnnotationMarkActivated(id) - } - } - fun Boolean.toInt() = if (this) 1 else 0 fun scrollToStart() { @@ -607,7 +596,7 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte } internal fun shouldOverrideUrlLoading(request: WebResourceRequest): Boolean { - if (resourceUrl == request.url?.toString()) return false + if (resourceUrl == request.url.toUrl()) return false return listener?.shouldOverrideUrlLoading(this, request) ?: false } @@ -654,7 +643,10 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte ?: return super.startActionMode(callback, type) val parent = parent ?: return null - val wrapper = Callback2Wrapper(customCallback, callback2 = callback as? ActionMode.Callback2) + val wrapper = Callback2Wrapper( + customCallback, + callback2 = callback as? ActionMode.Callback2 + ) return parent.startActionModeForChild(this, wrapper, type) } @@ -668,3 +660,19 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte ?: super.onGetContentRect(mode, view, outRect) } } + +private fun inputModifiers(json: JSONObject): Set<InputModifier> = + buildSet { + if (json.optBoolean("alt")) { + add(InputModifier.Alt) + } + if (json.optBoolean("control")) { + add(InputModifier.Control) + } + if (json.optBoolean("shift")) { + add(InputModifier.Shift) + } + if (json.optBoolean("meta")) { + add(InputModifier.Meta) + } + } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/R2WebView.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/R2WebView.kt index d1822037d0..509e768786 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/R2WebView.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/R2WebView.kt @@ -11,6 +11,7 @@ package org.readium.r2.navigator import android.content.Context import android.graphics.Rect +import android.os.Build import android.util.AttributeSet import android.view.* import android.view.animation.Interpolator @@ -31,7 +32,7 @@ import timber.log.Timber * Created by Aferdita Muriqi on 12/2/17. */ -class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, attrs) { +internal class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, attrs) { init { initWebPager() @@ -39,7 +40,6 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, private val uiScope = CoroutineScope(Dispatchers.Main) - @android.webkit.JavascriptInterface override fun scrollRight(animated: Boolean) { super.scrollRight(animated) uiScope.launch { @@ -52,7 +52,6 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, } } - @android.webkit.JavascriptInterface override fun scrollLeft(animated: Boolean) { super.scrollLeft(animated) uiScope.launch { @@ -65,8 +64,6 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, } } - private val USE_CACHE = false - private val MAX_SETTLE_DURATION = 600 // ms private val MIN_DISTANCE_FOR_FLING = 25 // dips private val MIN_FLING_VELOCITY = 400 // dips @@ -107,8 +104,6 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, private val mFirstOffset = -java.lang.Float.MAX_VALUE private val mLastOffset = java.lang.Float.MAX_VALUE - private var mScrollingCacheEnabled: Boolean = false - private var mIsBeingDragged: Boolean = false private var mGutterSize: Int = 30 private var mTouchSlop: Int = 0 @@ -136,6 +131,7 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, * Determines speed during touch scrolling */ private var mVelocityTracker: VelocityTracker? = null + /** Initial velocity of the current movement. */ private var mInitialVelocity: Int? = null private var mMinimumVelocity: Int = 0 @@ -189,7 +185,13 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, private fun initWebPager() { setWillNotDraw(false) descendantFocusability = ViewGroup.FOCUS_AFTER_DESCENDANTS - isFocusable = true + + // Disable the focus overlay appearing when interacting with the keyboard. + // https://developer.android.com/about/versions/oreo/android-8.0-changes#ian + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + defaultFocusHighlightEnabled = false + } + val context = context mScroller = Scroller(context, sInterpolator) val configuration = ViewConfiguration.get(context) @@ -245,7 +247,9 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, val count = childCount while (i < count) { val childInsets = ViewCompat - .dispatchApplyWindowInsets(getChildAt(i), applied).getInsets(WindowInsetsCompat.Type.systemBars()) + .dispatchApplyWindowInsets(getChildAt(i), applied).getInsets( + WindowInsetsCompat.Type.systemBars() + ) // Now keep track of any consumed by tracking each dimension's min // value res.left = min( @@ -269,7 +273,10 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, // Now return a new WindowInsets, using the consumed window insets return WindowInsetsCompat.Builder(applied) - .setInsets(WindowInsetsCompat.Type.systemBars(), Insets.of(res.left, res.top, res.right, res.bottom)) + .setInsets( + WindowInsetsCompat.Type.systemBars(), + Insets.of(res.left, res.top, res.right, res.bottom) + ) .build() } } @@ -391,7 +398,6 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, sx = if (mIsScrollStarted) mScroller!!.currX else mScroller!!.startX // And abort the current scrolling. mScroller!!.abortAnimation() - setScrollingCacheEnabled(false) } else { sx = scrollX } @@ -404,7 +410,6 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, return } - setScrollingCacheEnabled(true) setScrollState(SCROLL_STATE_SETTLING) val halfWidth = width / 2 @@ -430,7 +435,6 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, } private fun infoForPosition(position: Int): ItemInfo { - val ii = ItemInfo() ii.position = position ii.offset = (position * (1 / numPages)).toFloat() @@ -530,7 +534,8 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, } childLeft += scrollX child.layout( - childLeft, childTop, + childLeft, + childTop, childLeft + child.measuredWidth, childTop + child.measuredHeight ) @@ -550,7 +555,7 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, } override fun computeScroll() { - if (!useLegacySettings && scrollMode) { + if (scrollMode) { return super.computeScroll() } @@ -657,8 +662,6 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, private fun completeScroll(postEvents: Boolean) { val needPopulate = mScrollState == SCROLL_STATE_SETTLING if (needPopulate) { - // Done with scroll, no longer want to cache view drawing. - setScrollingCacheEnabled(false) val wasScrolling = !mScroller!!.isFinished if (wasScrolling) { mScroller!!.abortAnimation() @@ -684,7 +687,6 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, } override fun onTouchEvent(ev: MotionEvent): Boolean { - if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain() } @@ -692,7 +694,6 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, val action = ev.action when (action and MotionEvent.ACTION_MASK) { - MotionEvent.ACTION_DOWN -> { mScroller?.let { scroller -> mHasAbortedScroller = !scroller.isFinished @@ -714,18 +715,18 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, if (!isSelecting && !mIsBeingDragged) { mInitialVelocity = getCurrentXVelocity() val pointerIndex = ev.findPointerIndex(mActivePointerId) - val x = ev.getX(pointerIndex) + val x = ev.safeGetX(pointerIndex) val xDiff = abs(x - mLastMotionX) if (xDiff > mTouchSlop) { if (DEBUG) Timber.v("Starting drag!") mIsBeingDragged = true - mLastMotionX = if (x - mInitialMotionX > 0) + mLastMotionX = if (x - mInitialMotionX > 0) { mInitialMotionX + mTouchSlop - else + } else { mInitialMotionX - mTouchSlop + } setScrollState(SCROLL_STATE_DRAGGING) - setScrollingCacheEnabled(true) } } } @@ -735,8 +736,8 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, mHasAbortedScroller = false val activePointerIndex = ev.findPointerIndex(mActivePointerId) - val x = ev.getX(activePointerIndex) - val y = ev.getY(activePointerIndex) + val x = ev.safeGetX(activePointerIndex) + val y = ev.safeGetY(activePointerIndex) if (scrollMode) { val totalDelta = (y - mInitialMotionY).toInt() @@ -785,13 +786,13 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, } MotionEvent.ACTION_POINTER_DOWN -> { val index = ev.actionIndex - val x = ev.getX(index) + val x = ev.safeGetX(index) mLastMotionX = x mActivePointerId = ev.getPointerId(index) } MotionEvent.ACTION_POINTER_UP -> { onSecondaryPointerUp(ev) - mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId)) + mLastMotionX = ev.safeGetX(ev.findPointerIndex(mActivePointerId)) } } @@ -860,8 +861,11 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, val isCancelled = (initialVelocity * currentVelocity) <= 0 return if (!isCancelled && abs(deltaX) > mFlingDistance && abs(currentVelocity) > mMinimumVelocity) { - if (currentVelocity >= 0) currentPage - 1 - else currentPage + 1 + if (currentVelocity >= 0) { + currentPage - 1 + } else { + currentPage + 1 + } } else { currentPage } @@ -874,27 +878,12 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. val newPointerIndex = if (pointerIndex == 0) 1 else 0 - mLastMotionX = ev.getX(newPointerIndex) + mLastMotionX = ev.safeGetX(newPointerIndex) mActivePointerId = ev.getPointerId(newPointerIndex) mVelocityTracker?.clear() } } - private fun setScrollingCacheEnabled(enabled: Boolean) { - if (mScrollingCacheEnabled != enabled) { - mScrollingCacheEnabled = enabled - if (USE_CACHE) { - val size = childCount - for (i in 0 until size) { - val child = getChildAt(i) - if (child.visibility != View.GONE) { - child.isDrawingCacheEnabled = enabled - } - } - } - } - } - override fun dispatchKeyEvent(event: KeyEvent): Boolean { // Let the focused view and/or our descendants get the key first return super.dispatchKeyEvent(event) || executeKeyEvent(event) @@ -917,7 +906,10 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, } else { arrowScroll(View.FOCUS_LEFT) } - KeyEvent.KEYCODE_DPAD_RIGHT -> handled = if (event.hasModifiers(KeyEvent.META_ALT_ON)) { + KeyEvent.KEYCODE_DPAD_RIGHT -> handled = if (event.hasModifiers( + KeyEvent.META_ALT_ON + ) + ) { pageRight() } else { arrowScroll(View.FOCUS_RIGHT) @@ -964,7 +956,12 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, sb.append(" => ").append(parent.javaClass.simpleName) parent = parent.parent } - if (DEBUG) Timber.e("arrowScroll tried to find focus based on non-child current focused view %s", sb.toString()) + if (DEBUG) { + Timber.e( + "arrowScroll tried to find focus based on non-child current focused view %s", + sb.toString() + ) + } currentFocused = null } } @@ -972,7 +969,8 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, var handled = false val nextFocused = FocusFinder.getInstance().findNextFocus( - this, currentFocused, + this, + currentFocused, direction ) if (nextFocused != null && nextFocused !== currentFocused) { @@ -1094,3 +1092,15 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context, } } } + +/** + * May crash with java.lang.IllegalArgumentException: pointerIndex out of range + */ +private fun MotionEvent.safeGetX(pointerIndex: Int): Float = + try { getX(pointerIndex) } catch (e: IllegalArgumentException) { 0F } + +/** + * May crash with java.lang.IllegalArgumentException: pointerIndex out of range + */ +private fun MotionEvent.safeGetY(pointerIndex: Int): Float = + try { getY(pointerIndex) } catch (e: IllegalArgumentException) { 0F } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/SelectableNavigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/SelectableNavigator.kt index 4e60a87a8c..47db897f81 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/SelectableNavigator.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/SelectableNavigator.kt @@ -12,13 +12,13 @@ import org.readium.r2.shared.publication.Locator /** * A navigator supporting user selection. */ -interface SelectableNavigator : Navigator { +public interface SelectableNavigator : Navigator { /** Currently selected content. */ - suspend fun currentSelection(): Selection? + public suspend fun currentSelection(): Selection? /** Clears the current selection. */ - fun clearSelection() + public fun clearSelection() } /** @@ -28,7 +28,7 @@ interface SelectableNavigator : Navigator { * @param rect Frame of the bounding rect for the selection, in the coordinate of the navigator * view. This is only useful in the context of a VisualNavigator. */ -data class Selection( +public data class Selection( val locator: Locator, - val rect: RectF?, + val rect: RectF? ) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/SimplePresentation.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/SimpleOverflow.kt similarity index 85% rename from readium/navigator/src/main/java/org/readium/r2/navigator/SimplePresentation.kt rename to readium/navigator/src/main/java/org/readium/r2/navigator/SimpleOverflow.kt index c9b217eefc..afbe6f3604 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/SimplePresentation.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/SimpleOverflow.kt @@ -13,8 +13,8 @@ import org.readium.r2.shared.InternalReadiumApi @InternalReadiumApi @OptIn(ExperimentalReadiumApi::class) -data class SimplePresentation( +public data class SimpleOverflow( override val readingProgression: ReadingProgression, override val scroll: Boolean, - override val axis: Axis, -) : VisualNavigator.Presentation + override val axis: Axis +) : OverflowableNavigator.Overflow diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/VisualNavigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/VisualNavigator.kt new file mode 100644 index 0000000000..18c2796251 --- /dev/null +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/VisualNavigator.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.navigator + +import android.graphics.PointF +import android.view.View +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.readium.r2.navigator.input.InputListener +import org.readium.r2.navigator.preferences.Axis +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.publication.ReadingProgression as PublicationReadingProgression + +/** + * A navigator rendering the publication visually on-screen. + */ +public interface VisualNavigator : Navigator { + + @Deprecated( + "Renamed to OverflowableNavigator.Overflow", + level = DeprecationLevel.ERROR + ) + @OptIn(ExperimentalReadiumApi::class) + public interface Presentation { + /** + * Horizontal direction of progression across resources. + */ + public val readingProgression: ReadingProgression + + /** + * If the overflow of the content is managed through scroll instead of pagination. + */ + public val scroll: Boolean + + /** + * Main axis along which the resources are laid out. + */ + public val axis: Axis + } + + /** + * View displaying the publication. + */ + public val publicationView: View + + /** + * Current presentation rendered by the navigator. + */ + @Deprecated( + "Moved to OverflowableNavigator.overflow", + level = DeprecationLevel.ERROR + ) + public val presentation: StateFlow<Any> get() = MutableStateFlow(Any()) + + /** + * Returns the [Locator] to the first content element that begins on the current screen. + */ + @ExperimentalReadiumApi + public suspend fun firstVisibleElementLocator(): Locator? = + currentLocator.value + + /** + * Adds a new [InputListener] to receive touch, mouse or keyboard events. + * + * Registration order is critical, as listeners may consume the events and prevent others from + * receiving them. + */ + @ExperimentalReadiumApi + public fun addInputListener(listener: InputListener) + + /** + * Removes a previously registered [InputListener]. + */ + @ExperimentalReadiumApi + public fun removeInputListener(listener: InputListener) + + public interface Listener : Navigator.Listener { + + /** + * Called when a link to an internal resource was clicked in the navigator. + * + * You can use this callback to perform custom navigation like opening a new window + * or other operations. + * + * By returning false the navigator wont try to open the link itself and it is up + * to the calling app to decide how to display the link. + */ + @Deprecated( + "Use `HyperlinkNavigator.Listener.shouldFollowInternalLink` instead", + replaceWith = ReplaceWith("shouldFollowInternalLink(link)"), + level = DeprecationLevel.ERROR + ) + public fun shouldJumpToLink(link: Link): Boolean { return true } + + @Deprecated("Use `addInputListener` instead", level = DeprecationLevel.ERROR) + public fun onTap(point: PointF): Boolean = false + + @Deprecated("Use `addInputListener` instead", level = DeprecationLevel.ERROR) + public fun onDragStart(startPoint: PointF, offset: PointF): Boolean = false + + @Deprecated("Use `addInputListener` instead", level = DeprecationLevel.ERROR) + public fun onDragMove(startPoint: PointF, offset: PointF): Boolean = false + + @Deprecated("Use `addInputListener` instead", level = DeprecationLevel.ERROR) + public fun onDragEnd(startPoint: PointF, offset: PointF): Boolean = false + } + + /** + * Current reading progression direction. + */ + @Deprecated( + "Use `presentation.value.readingProgression` instead", + ReplaceWith("presentation.value.readingProgression"), + level = DeprecationLevel.ERROR + ) + public val readingProgression: PublicationReadingProgression + + /** + * Moves to the next content portion (eg. page) in the reading progression direction. + */ + @Deprecated( + "Moved to OverflowableNavigator", + level = DeprecationLevel.ERROR + ) + public fun goForward(animated: Boolean = false, completion: () -> Unit = {}): Boolean + + /** + * Moves to the previous content portion (eg. page) in the reading progression direction. + */ + @Deprecated( + "Moved to OverflowableNavigator", + level = DeprecationLevel.ERROR + ) + public fun goBackward(animated: Boolean = false, completion: () -> Unit = {}): Boolean +} + +/** + * A [VisualNavigator] with content that can extend beyond the viewport. + * + * The user typically navigates through the publication by scrolling or tapping the viewport edges. + */ +@ExperimentalReadiumApi +public interface OverflowableNavigator : VisualNavigator { + + @ExperimentalReadiumApi + public interface Listener : VisualNavigator.Listener + + /** + * Current presentation rendered by the navigator. + */ + @ExperimentalReadiumApi + public val overflow: StateFlow<Overflow> + + @ExperimentalReadiumApi + public interface Overflow { + /** + * Horizontal direction of progression across resources. + */ + public val readingProgression: ReadingProgression + + /** + * If the overflow of the content is managed through scroll instead of pagination. + */ + public val scroll: Boolean + + /** + * Main axis along which the resources are laid out. + */ + public val axis: Axis + } + + /** + * Moves to the next content portion (eg. page) in the reading progression direction. + */ + public override fun goForward(animated: Boolean, completion: () -> Unit): Boolean + + /** + * Moves to the previous content portion (eg. page) in the reading progression direction. + */ + public override fun goBackward(animated: Boolean, completion: () -> Unit): Boolean +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/audiobook/Deprecated.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/audiobook/Deprecated.kt new file mode 100644 index 0000000000..bdd68570f0 --- /dev/null +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/audiobook/Deprecated.kt @@ -0,0 +1,21 @@ +package org.readium.r2.navigator.audiobook + +import android.media.MediaPlayer +import org.readium.r2.shared.publication.Link + +@Deprecated("Build your own UI upon AudiobookNavigator instead.", level = DeprecationLevel.ERROR) +public open class R2AudiobookActivity + +@Deprecated("Use the new MediaNavigator API.", level = DeprecationLevel.ERROR) +public class R2MediaPlayer(private var items: List<Link>) : + MediaPlayer.OnPreparedListener { + + override fun onPrepared(mp: MediaPlayer?) { + } +} + +@Deprecated("Use the new MediaNavigator API.", level = DeprecationLevel.ERROR) +public interface MediaPlayerCallback { + public fun onPrepared() + public fun onComplete(index: Int, currentPosition: Int, duration: Int) +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/audiobook/R2AudiobookActivity.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/audiobook/R2AudiobookActivity.kt deleted file mode 100644 index d0aa2c25ec..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/audiobook/R2AudiobookActivity.kt +++ /dev/null @@ -1,446 +0,0 @@ -package org.readium.r2.navigator.audiobook - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.net.Uri -import android.os.Bundle -import android.os.Handler -import android.widget.SeekBar -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat -import androidx.lifecycle.Lifecycle -import java.util.concurrent.TimeUnit -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import org.readium.r2.navigator.BuildConfig.DEBUG -import org.readium.r2.navigator.IR2Activity -import org.readium.r2.navigator.NavigatorDelegate -import org.readium.r2.navigator.R -import org.readium.r2.navigator.VisualNavigator -import org.readium.r2.navigator.databinding.ActivityR2AudiobookBinding -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.extensions.getPublication -import org.readium.r2.shared.publication.* -import org.readium.r2.shared.publication.services.isRestricted -import timber.log.Timber - -open class R2AudiobookActivity : AppCompatActivity(), CoroutineScope, IR2Activity, MediaPlayerCallback, VisualNavigator { - - override val currentLocator: StateFlow<Locator> get() = _currentLocator - private val _currentLocator = MutableStateFlow(Locator(href = "#", type = "")) - private lateinit var binding: ActivityR2AudiobookBinding - - private fun notifyCurrentLocation() { - val locator = publication.readingOrder[currentResource].let { resource -> - val progression = mediaPlayer - .takeIf { it.duration > 0 } - ?.let { it.currentPosition / it.duration } - ?: 0.0 - - // FIXME: Add totalProgression - Locator( - href = resource.href, - type = resource.type ?: "audio/*", - title = resource.title, - locations = Locator.Locations( - fragments = listOf( - "t=${TimeUnit.MILLISECONDS.toSeconds(mediaPlayer.currentPosition.toLong())}" - ), - progression = progression - ) - ) - } - - if (locator == _currentLocator.value) { - return - } - - _currentLocator.value = locator - navigatorDelegate?.locationDidChange(navigator = this, locator = locator) - } - - override fun go(locator: Locator, animated: Boolean, completion: () -> Unit): Boolean { - loadedInitialLocator = true - currentResource = publication.readingOrder.indexOfFirstWithHref(locator.href) ?: return false - mediaPlayer.goTo(currentResource) - seek(locator.locations) - - binding.playPause.callOnClick() - notifyCurrentLocation() - - return true - } - - override fun go(link: Link, animated: Boolean, completion: () -> Unit): Boolean { - val locator = publication.locatorFromLink(link) ?: return false - return go(locator) - } - - override fun goForward(animated: Boolean, completion: () -> Unit): Boolean { - if (currentResource < publication.readingOrder.size - 1) { - currentResource++ - } - - mediaPlayer.next() - binding.playPause.callOnClick() - return true - } - - override fun goBackward(animated: Boolean, completion: () -> Unit): Boolean { - if (currentResource > 0) { - currentResource-- - } - - mediaPlayer.previous() - binding.playPause.callOnClick() - return true - } - - @Deprecated("Use `presentation.value.readingProgression` instead", replaceWith = ReplaceWith("presentation.value.readingProgression")) - override val readingProgression: ReadingProgression - get() = TODO("not implemented") // To change initializer of created properties use File | Settings | File Templates. - - @ExperimentalReadiumApi - override val presentation: StateFlow<VisualNavigator.Presentation> - get() = TODO("Not yet implemented") - - /** - * Context of this scope. - */ - override val coroutineContext: CoroutineContext - get() = Dispatchers.Main - - override lateinit var preferences: SharedPreferences - override lateinit var publication: Publication - override lateinit var publicationIdentifier: String - override lateinit var publicationFileName: String - override lateinit var publicationPath: String - override var bookId: Long = -1 - - var currentResource = 0 - - var startTime = 0.0 - private var finalTime = 0.0 - - private val forwardTime = 10000 - private val backwardTime = 10000 - - lateinit var mediaPlayer: R2MediaPlayer - private var loadedInitialLocator = false - - protected var navigatorDelegate: NavigatorDelegate? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityR2AudiobookBinding.inflate(layoutInflater) - setContentView(binding.root) - - preferences = getSharedPreferences("org.readium.r2.settings", Context.MODE_PRIVATE) - - publicationPath = intent.getStringExtra("publicationPath") ?: throw Exception("publicationPath required") - publicationFileName = intent.getStringExtra("publicationFileName") ?: throw Exception("publicationFileName required") - val baseUrl = intent.getStringExtra("baseUrl") ?: throw Exception("Intent extra `baseUrl` is required. Provide the URL returned by Server.addPublication()") - - publication = intent.getPublication(this) - publicationIdentifier = publication.metadata.identifier ?: publication.metadata.title - - require(!publication.isRestricted) { "The provided publication is restricted. Check that any DRM was properly unlocked using a Content Protection." } - - title = null - - val readingOrderOverHttp = publication.readingOrder.map { it.withBaseUrl(baseUrl) } - mediaPlayer = R2MediaPlayer(readingOrderOverHttp, this) - - Handler(mainLooper).postDelayed({ - - if (this.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { - - if (!loadedInitialLocator) { - go(publication.readingOrder.first()) - } - - binding.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - /** - * Notification that the progress level has changed. Clients can use the fromUser parameter - * to distinguish user-initiated changes from those that occurred programmatically. - * - * @param seekBar The SeekBar whose progress has changed - * @param progress The current progress level. This will be in the range min..max where min - * @param fromUser True if the progress change was initiated by the user. - */ - override fun onProgressChanged( - seekBar: SeekBar?, - progress: Int, - fromUser: Boolean - ) { - if (!fromUser) { - return - } - mediaPlayer.seekTo(progress) - if (DEBUG) Timber.d("progress $progress") - } - - /** - * Notification that the user has started a touch gesture. Clients may want to use this - * to disable advancing the seekbar. - * @param seekBar The SeekBar in which the touch gesture began - */ - override fun onStartTrackingTouch(seekBar: SeekBar?) { - // do nothing - isSeekTracking = true - if (DEBUG) Timber.d("start tracking") - } - - /** - * Notification that the user has finished a touch gesture. Clients may want to use this - * to re-enable advancing the seekbar. - * @param seekBar The SeekBar in which the touch gesture began - */ - override fun onStopTrackingTouch(seekBar: SeekBar?) { - // do nothing - isSeekTracking = false - if (DEBUG) Timber.d("stop tracking") - } - }) - - binding.playPause.setOnClickListener { - mediaPlayer.let { - if (it.isPlaying) { - it.pause() - } else { - if (it.isPaused) { - it.resume() - } else { - it.startPlayer() - } - Handler(mainLooper).postDelayed(updateSeekTime, 100) - } - this.updateUI() - } - } - - binding.playPause.callOnClick() - - binding.fastForward.setOnClickListener { - if (startTime.toInt() + forwardTime <= finalTime) { - startTime += forwardTime - mediaPlayer.seekTo(startTime) - } - } - - binding.fastBack.setOnClickListener { - if (startTime.toInt() - backwardTime > 0) { - startTime -= backwardTime - mediaPlayer.seekTo(startTime) - } - } - - binding.nextChapter.setOnClickListener { - goForward(false) {} - } - - binding.prevChapter.setOnClickListener { - goBackward(false) {} - } - } - }, 100) - } - - private fun updateUI() { - - if (currentResource == publication.readingOrder.size - 1) { - binding.nextChapter.isEnabled = false - binding.nextChapter.alpha = .5f - } else { - binding.nextChapter.isEnabled = true - binding.nextChapter.alpha = 1.0f - } - if (currentResource == 0) { - binding.prevChapter.isEnabled = false - binding.prevChapter.alpha = .5f - } else { - binding.prevChapter.isEnabled = true - binding.prevChapter.alpha = 1.0f - } - - val current = publication.readingOrder[currentResource] - binding.chapterView.text = current.title - - if (mediaPlayer.isPlaying) { - binding.playPause.setImageDrawable(ContextCompat.getDrawable(this@R2AudiobookActivity, R.drawable.ic_pause_white_24dp)) - } else { - binding.playPause.setImageDrawable(ContextCompat.getDrawable(this@R2AudiobookActivity, R.drawable.ic_play_arrow_white_24dp)) - } - - finalTime = mediaPlayer.duration - startTime = mediaPlayer.currentPosition - - binding.seekBar.max = finalTime.toInt() - - binding.chapterTime.text = String.format( - "%d:%d", - TimeUnit.MILLISECONDS.toMinutes(finalTime.toLong()), - TimeUnit.MILLISECONDS.toSeconds(finalTime.toLong()) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(finalTime.toLong())) - ) - - binding.progressTime.text = String.format( - "%d:%d", - TimeUnit.MILLISECONDS.toMinutes(startTime.toLong()), - TimeUnit.MILLISECONDS.toSeconds(startTime.toLong()) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(startTime.toLong())) - ) - - binding.seekBar.progress = startTime.toInt() - - notifyCurrentLocation() - } - - private fun seek(locations: Locator.Locations) { - if (!mediaPlayer.isPrepared) { - pendingSeekLocation = locations - return - } - - pendingSeekLocation = null - - val time = locations.fragments.firstOrNull()?.let { - var time = it - if (time.startsWith("#t=") || time.startsWith("t=")) { - time = time.substring(time.indexOf('=') + 1) - } - time - } - time?.let { - mediaPlayer.seekTo(TimeUnit.SECONDS.toMillis(it.toLong()).toInt()) - } ?: run { - val progression = locations.progression - val duration = mediaPlayer.duration - Timber.d("progression used") - if (progression != null) { - Timber.d("ready to seek") - mediaPlayer.seekTo(progression * duration) - } - } - } - - private var pendingSeekLocation: Locator.Locations? = null - var isSeekTracking = false - - private fun seekIfNeeded() { - pendingSeekLocation?.let { locations -> - seek(locations) - } - } - - override fun onPrepared() { - seekIfNeeded() - Handler(mainLooper).postDelayed(updateSeekTime, 100) - updateUI() - } - - override fun onComplete(index: Int, currentPosition: Int, duration: Int) { - if (currentResource == index && currentPosition > 0 && currentResource < publication.readingOrder.size - 1 && currentPosition >= duration - 200 && !isSeekTracking) { - Handler(mainLooper).postDelayed({ - if (currentResource < publication.readingOrder.size - 1) { - currentResource++ - } - mediaPlayer.next() - binding.playPause.callOnClick() - }, 100) - } else if (currentPosition > 0 && currentResource == publication.readingOrder.size - 1) { - mediaPlayer.pause() - binding.playPause.setImageDrawable(ContextCompat.getDrawable(this@R2AudiobookActivity, R.drawable.ic_play_arrow_white_24dp)) - } else { - mediaPlayer.pause() - binding.playPause.setImageDrawable(ContextCompat.getDrawable(this@R2AudiobookActivity, R.drawable.ic_play_arrow_white_24dp)) - } - } - - private val updateSeekTime = object : Runnable { - override fun run() { - if (mediaPlayer.isPrepared) { - mediaPlayer.let { - startTime = it.mediaPlayer.currentPosition.toDouble() - } - binding.progressTime.text = String.format( - "%d:%d", - TimeUnit.MILLISECONDS.toMinutes(startTime.toLong()), - TimeUnit.MILLISECONDS.toSeconds(startTime.toLong()) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(startTime.toLong())) - ) - binding.seekBar.progress = startTime.toInt() - - notifyCurrentLocation() - - Handler(mainLooper).postDelayed(this, 100) - } - } - } - - override fun finish() { - setResult(Activity.RESULT_OK, intent) - super.finish() - } - - override fun onResume() { - super.onResume() - mediaPlayer.resume() - } - - override fun onPause() { - super.onPause() - mediaPlayer.pause() - } - - override fun onStop() { - super.onStop() - mediaPlayer.stop() - } - - override fun onDestroy() { - super.onDestroy() - mediaPlayer.stop() - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == 2 && resultCode == Activity.RESULT_OK) { - if (data != null) { - val locator = data.getParcelableExtra("locator") as? Locator - - locator?.let { - // href is the link to the page in the toc - var href = locator.href - - if (href.indexOf("#") > 0) { - href = href.substring(0, href.indexOf("#")) - } - - var index = 0 - for (resource in publication.readingOrder) { - if (resource.href.endsWith(href)) { - currentResource = index - break - } - index++ - } - go(locator) - } - } - } - } -} - -internal fun Link.withBaseUrl(baseUrl: String): Link { - // Already an absolute URL? - if (Uri.parse(href).scheme != null) { - return this - } - - check(!baseUrl.endsWith("/")) - check(href.startsWith("/")) - return copy(href = baseUrl + href) -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/audiobook/R2MediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/audiobook/R2MediaPlayer.kt deleted file mode 100644 index 1347a278b4..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/audiobook/R2MediaPlayer.kt +++ /dev/null @@ -1,153 +0,0 @@ -package org.readium.r2.navigator.audiobook - -import android.app.ProgressDialog -import android.media.MediaPlayer -import android.media.MediaPlayer.OnPreparedListener -import android.net.Uri -import java.io.IOException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.readium.r2.shared.publication.Link - -class R2MediaPlayer(private var items: List<Link>, private var callback: MediaPlayerCallback) : OnPreparedListener { - - private val uiScope = CoroutineScope(Dispatchers.Main) - - var progress: ProgressDialog? = null - - var mediaPlayer: MediaPlayer = MediaPlayer() - - val isPlaying: Boolean - get() = mediaPlayer.isPlaying - - val duration: Double - get() = mediaPlayer.duration.toDouble() // if (isPrepared) {mediaPlayer.duration.toDouble()}else {0.0} - - val currentPosition: Double - get() = mediaPlayer.currentPosition.toDouble() // if (isPrepared) {mediaPlayer.currentPosition.toDouble()}else {0.0} - - var isPaused: Boolean - var isPrepared: Boolean - - private var index: Int - - init { - isPaused = false - isPrepared = false - index = 0 - if (mediaPlayer.isPlaying) { - mediaPlayer.stop() - mediaPlayer.release() - } - toggleProgress(true) - } - - /** - * Called when the media file is ready for playback. - * - * @param mp the MediaPlayer that is ready for playback - */ - override fun onPrepared(mp: MediaPlayer?) { - toggleProgress(false) - this.start() - isPrepared = true - callback.onPrepared() - } - - fun startPlayer() { - mediaPlayer.reset() - try { - mediaPlayer.setDataSource(Uri.parse(items[index].href).toString()) - mediaPlayer.setOnPreparedListener(this) - mediaPlayer.prepareAsync() - toggleProgress(true) - } catch (e: IllegalArgumentException) { - e.printStackTrace() - } catch (e: IllegalStateException) { - e.printStackTrace() - } catch (e: IOException) { - e.printStackTrace() - } - } - - private fun toggleProgress(show: Boolean) { - uiScope.launch { - if (show) progress?.show() - else progress?.hide() - } - } - - fun seekTo(progression: Any) { - when (progression) { - is Double -> mediaPlayer.seekTo(progression.toInt()) - is Int -> mediaPlayer.seekTo(progression) - else -> mediaPlayer.seekTo(progression.toString().toInt()) - } - } - - fun stop() { - if (isPrepared) { - mediaPlayer.stop() - isPrepared = false - } - } - - fun pause() { - if (isPrepared) { - mediaPlayer.pause() - isPaused = true - } - } - - fun start() { - mediaPlayer.start() - isPaused = false - isPrepared = false - mediaPlayer.setOnCompletionListener { - callback.onComplete(index, it.currentPosition, it.duration) - } - } - - fun resume() { - if (isPrepared) { - mediaPlayer.start() - isPaused = false - } - } - - fun goTo(index: Int) { - this.index = index - isPaused = false - isPrepared = false - if (mediaPlayer.isPlaying) { - mediaPlayer.stop() - } - toggleProgress(true) - } - - fun previous() { - index -= 1 - isPaused = false - isPrepared = false - if (mediaPlayer.isPlaying) { - mediaPlayer.stop() - } - toggleProgress(true) - } - - fun next() { - index += 1 - isPaused = false - isPrepared = false - if (mediaPlayer.isPlaying) { - mediaPlayer.stop() - } - toggleProgress(true) - } -} - -interface MediaPlayerCallback { - fun onPrepared() - fun onComplete(index: Int, currentPosition: Int, duration: Int) -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/cbz/R2CbzActivity.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/cbz/R2CbzActivity.kt index 1ef8a4b0c8..4cb2af80f1 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/cbz/R2CbzActivity.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/cbz/R2CbzActivity.kt @@ -9,226 +9,8 @@ package org.readium.r2.navigator.cbz -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.graphics.PointF -import android.os.Build -import android.os.Bundle -import android.view.View -import android.view.WindowManager -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.Observer -import androidx.lifecycle.asLiveData -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import org.readium.r2.navigator.IR2Activity -import org.readium.r2.navigator.NavigatorDelegate -import org.readium.r2.navigator.R -import org.readium.r2.navigator.VisualNavigator -import org.readium.r2.navigator.image.ImageNavigatorFragment -import org.readium.r2.navigator.pager.R2PagerAdapter -import org.readium.r2.navigator.pager.R2ViewPager -import org.readium.r2.navigator.util.CompositeFragmentFactory -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.extensions.getPublication -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.ReadingProgression - -open class R2CbzActivity : AppCompatActivity(), CoroutineScope, IR2Activity, VisualNavigator, ImageNavigatorFragment.Listener { - - private val navigatorFragment: ImageNavigatorFragment - get() = supportFragmentManager.findFragmentById(R.id.image_navigator) as ImageNavigatorFragment - - protected var navigatorDelegate: NavigatorDelegate? = null - - protected val positions: List<Locator> get() = navigatorFragment.positions - - val currentPagerPosition: Int get() = navigatorFragment.currentPagerPosition - - override val currentLocator: StateFlow<Locator> - get() = navigatorFragment.currentLocator - - @ExperimentalReadiumApi - override val presentation: StateFlow<VisualNavigator.Presentation> - get() = navigatorFragment.presentation - - override fun go(locator: Locator, animated: Boolean, completion: () -> Unit): Boolean { - return navigatorFragment.go(locator, animated, completion) - } - - override fun go(link: Link, animated: Boolean, completion: () -> Unit): Boolean { - return navigatorFragment.go(link, animated, completion) - } - - override fun goForward(animated: Boolean, completion: () -> Unit): Boolean { - return navigatorFragment.goForward(animated, completion) - } - - override fun goBackward(animated: Boolean, completion: () -> Unit): Boolean { - return navigatorFragment.goBackward(animated, completion) - } - - @Deprecated("Use `presentation.value.readingProgression` instead", replaceWith = ReplaceWith("presentation.value.readingProgression")) - override val readingProgression: ReadingProgression - get() = navigatorFragment.readingProgression - - /** - * Context of this scope. - */ - override val coroutineContext: CoroutineContext - get() = Dispatchers.Main - - override lateinit var preferences: SharedPreferences - override lateinit var resourcePager: R2ViewPager - override lateinit var publicationPath: String - override lateinit var publication: Publication - override lateinit var publicationIdentifier: String - override lateinit var publicationFileName: String - - override var bookId: Long = -1 - - var resources: List<String> = emptyList() - lateinit var adapter: R2PagerAdapter - - override fun onCreate(savedInstanceState: Bundle?) { - preferences = getSharedPreferences("org.readium.r2.settings", Context.MODE_PRIVATE) - - publicationPath = intent.getStringExtra("publicationPath") ?: throw Exception("publicationPath required") - publicationFileName = intent.getStringExtra("publicationFileName") ?: throw Exception("publicationFileName required") - publication = intent.getPublication(this) - - publicationIdentifier = publication.metadata.identifier ?: publication.metadata.title - title = publication.metadata.title - - val initialLocator = intent.getParcelableExtra("locator") as? Locator - - // This must be done before the call to super.onCreate, including by reading apps. - // Because they may want to set their own factories, let's use a CompositeFragmentFactory that retains - // previously set factories. - supportFragmentManager.fragmentFactory = CompositeFragmentFactory( - supportFragmentManager.fragmentFactory, - ImageNavigatorFragment.createFactory(publication, initialLocator = initialLocator, listener = this) - ) - - super.onCreate(savedInstanceState) - - setContentView(R.layout.activity_r2_image) - - resourcePager = navigatorFragment.resourcePager - - navigatorFragment.currentLocator.asLiveData().observe( - this, - Observer { locator -> - locator ?: return@Observer - @Suppress("DEPRECATION") - navigatorDelegate?.locationDidChange(this, locator) - } - ) - - // Add support for display cutout. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - } - } - - override fun finish() { - setResult(Activity.RESULT_OK, intent) - super.finish() - } - - @Deprecated("Use goForward instead", replaceWith = ReplaceWith("goForward()"), level = DeprecationLevel.ERROR) - override fun nextResource(v: View?) { - navigatorFragment.goForward() - } - - @Deprecated("Use goBackward instead", replaceWith = ReplaceWith("goBackward()"), level = DeprecationLevel.ERROR) - override fun previousResource(v: View?) { - navigatorFragment.goBackward() - } - - @Suppress("DEPRECATION") - override fun toggleActionBar() { - if (allowToggleActionBar) { - launch { - if (supportActionBar!!.isShowing) { - resourcePager.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - or View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar - or View.SYSTEM_UI_FLAG_IMMERSIVE - ) - } else { - resourcePager.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - ) - } - } - } - } - - override fun toggleActionBar(v: View?) { - toggleActionBar() - } - - override fun onTap(point: PointF): Boolean { - val viewWidth = navigatorFragment.requireView().width - val leftRange = 0.0..(0.2 * viewWidth) - - when { - leftRange.contains(point.x) -> navigatorFragment.goBackward(animated = true) - leftRange.contains(viewWidth - point.x) -> navigatorFragment.goForward(animated = true) - else -> toggleActionBar() - } - - return true - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == 2 && resultCode == Activity.RESULT_OK) { - if (data != null) { - - val locator = data.getParcelableExtra("locator") as? Locator - - locator?.let { - fun setCurrent(resources: List<String>) { - for (index in 0 until resources.count()) { - val resource = resources[index] - if (resource.endsWith(locator.href)) { - resourcePager.currentItem = index - break - } - } - } - - go(locator) - - setCurrent(resources) - } - - @Suppress("DEPRECATION") - if (supportActionBar!!.isShowing && allowToggleActionBar) { - resourcePager.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - or View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar - or View.SYSTEM_UI_FLAG_IMMERSIVE - ) - } - } - } - super.onActivityResult(requestCode, resultCode, data) - } -} +@Deprecated( + "Use ImageNavigatorFragment in your own activity instead.", + level = DeprecationLevel.ERROR +) +public open class R2CbzActivity diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/divina/R2DiViNaActivity.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/divina/R2DiViNaActivity.kt index 5bc5237657..e4c655e3fb 100755 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/divina/R2DiViNaActivity.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/divina/R2DiViNaActivity.kt @@ -9,110 +9,8 @@ package org.readium.r2.navigator.divina -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.content.SharedPreferences -import android.os.Bundle -import android.view.View -import android.webkit.WebView -import androidx.appcompat.app.AppCompatActivity -import androidx.webkit.WebViewClientCompat -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.readium.r2.navigator.IR2Activity -import org.readium.r2.navigator.R2BasicWebView -import org.readium.r2.navigator.VisualNavigator -import org.readium.r2.navigator.databinding.ActivityR2DivinaBinding -import org.readium.r2.shared.extensions.getPublication -import org.readium.r2.shared.publication.Publication - -open class R2DiViNaActivity : AppCompatActivity(), CoroutineScope, IR2Activity, VisualNavigator.Listener { - - /** - * Context of this scope. - */ - override val coroutineContext: CoroutineContext - get() = Dispatchers.Main - - override lateinit var preferences: SharedPreferences - override lateinit var publication: Publication - override lateinit var publicationIdentifier: String - override lateinit var publicationPath: String - override lateinit var publicationFileName: String - override var bookId: Long = -1 - - lateinit var divinaWebView: R2BasicWebView - - private lateinit var binding: ActivityR2DivinaBinding - - @SuppressLint("SetJavaScriptEnabled") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityR2DivinaBinding.inflate(layoutInflater) - setContentView(binding.root) - - preferences = getSharedPreferences("org.readium.r2.settings", Context.MODE_PRIVATE) - divinaWebView = binding.divinaWebView - // divinaWebView.listener = this - - publication = intent.getPublication(this) - publicationPath = intent.getStringExtra("publicationPath") ?: throw Exception("publicationPath required") - publicationFileName = intent.getStringExtra("publicationFileName") ?: throw Exception("publicationFileName required") - - publicationIdentifier = publication.metadata.identifier ?: "" - title = publication.metadata.title - - // Set up divinaWebView to enable JavaScript and access to local URLs - divinaWebView.settings.javaScriptEnabled = true - divinaWebView.settings.allowFileAccess = true - divinaWebView.settings.allowFileAccessFromFileURLs = true - divinaWebView.webViewClient = object : WebViewClientCompat() { - - override fun onPageFinished(view: WebView?, url: String?) { - super.onPageFinished(view, url) - // Define the JS toggleMenu function that will call Android's toggleActionBar -// divinaWebView.evaluateJavascript("window.androidObj = function AndroidClass(){};", null) -// divinaWebView.evaluateJavascript("window.androidObj.toggleMenu = function() { Android.toggleMenu() };", null) - - // Now launch the DiViNa player for the folderPath = publicationPath - divinaWebView.evaluateJavascript("if (player) { player.openDiViNaFromPath('$publicationPath'); };", null) - } - } - divinaWebView.loadUrl("file:///android_asset/readium/divina/divinaPlayer.html") - } - - @Suppress("DEPRECATION") - override fun toggleActionBar() { - launch { - if (supportActionBar!!.isShowing) { - divinaWebView.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - or View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar - or View.SYSTEM_UI_FLAG_IMMERSIVE - ) - } else { - divinaWebView.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - ) - } - } - } - - override fun finish() { - setResult(Activity.RESULT_OK, intent) - super.finish() - } - - override fun onDestroy() { - super.onDestroy() - divinaWebView.evaluateJavascript("if (player) { player.destroy(); };", null) - } -} +@Deprecated( + "Use ImageNavigatorFragment in your own activity to get a basic support for DiViNa.", + level = DeprecationLevel.ERROR +) +public open class R2DiViNaActivity diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/Deprecated.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/Deprecated.kt new file mode 100644 index 0000000000..356baf0bd9 --- /dev/null +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/Deprecated.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.navigator.epub + +import org.readium.r2.shared.publication.Locator + +@Deprecated( + "Use EpubNavigatorFragment in your own activity instead.", + level = DeprecationLevel.ERROR +) +public open class R2EpubActivity + +@Deprecated("Use Decorator API instead.", level = DeprecationLevel.ERROR) +public interface IR2Highlightable + +@Deprecated("Use Decorator API instead.", level = DeprecationLevel.ERROR) +public data class Highlight( + val id: String +) + +@Deprecated("Use Decorator API instead.", level = DeprecationLevel.ERROR) +public enum class Style { + Highlight, Underline, Strikethrough +} + +@Deprecated("Use navigator fragments.", level = DeprecationLevel.ERROR) +public interface IR2Selectable { + public fun currentSelection(callback: (Locator?) -> Unit) +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubDefaults.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubDefaults.kt index f0d2a62206..705e4a67d2 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubDefaults.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubDefaults.kt @@ -18,7 +18,7 @@ import org.readium.r2.shared.util.Language * @see EpubPreferences */ @ExperimentalReadiumApi -data class EpubDefaults( +public data class EpubDefaults( val columnCount: ColumnCount? = null, val fontSize: Double? = null, val fontWeight: Double? = null, diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFactory.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFactory.kt index 41714aec1c..3d909a2310 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFactory.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFactory.kt @@ -6,6 +6,8 @@ package org.readium.r2.navigator.epub +import androidx.fragment.app.FragmentFactory +import org.readium.r2.navigator.ExperimentalDecorator import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator @@ -20,7 +22,7 @@ import org.readium.r2.shared.publication.presentation.presentation * @param configuration Configuration of the factory to create. */ @ExperimentalReadiumApi -class EpubNavigatorFactory( +public class EpubNavigatorFactory( private val publication: Publication, private val configuration: Configuration = Configuration() ) { @@ -30,24 +32,37 @@ class EpubNavigatorFactory( * * @param defaults navigator fallbacks for some preferences */ - data class Configuration( - val defaults: EpubDefaults = EpubDefaults(), + public data class Configuration( + val defaults: EpubDefaults = EpubDefaults() ) private val layout: EpubLayout = publication.metadata.presentation.layout ?: EpubLayout.REFLOWABLE - fun createFragmentFactory( + /** + * Creates a factory for [EpubNavigatorFragment]. + * + * @param initialLocator The first location which should be visible when rendering the + * publication. Can be used to restore the last reading location. + * @param readingOrder Custom reading order to override the publication's one. + * @param initialPreferences The set of preferences that should be initially applied to the + * navigator. + * @param listener Optional listener to implement to observe navigator events. + * @param paginationListener Optional listener to implement to observe events related to + * pagination. + * @param configuration Additional configuration. + */ + @OptIn(ExperimentalDecorator::class) + public fun createFragmentFactory( initialLocator: Locator?, readingOrder: List<Link>? = null, initialPreferences: EpubPreferences = EpubPreferences(), listener: EpubNavigatorFragment.Listener? = null, paginationListener: EpubNavigatorFragment.PaginationListener? = null, - configuration: EpubNavigatorFragment.Configuration = EpubNavigatorFragment.Configuration(), - ) = org.readium.r2.navigator.util.createFragmentFactory { + configuration: EpubNavigatorFragment.Configuration = EpubNavigatorFragment.Configuration() + ): FragmentFactory = org.readium.r2.navigator.util.createFragmentFactory { EpubNavigatorFragment( publication = publication, - baseUrl = null, initialLocator = initialLocator, readingOrder = readingOrder, initialPreferences = initialPreferences, @@ -59,8 +74,8 @@ class EpubNavigatorFactory( ) } - fun createPreferencesEditor( - currentPreferences: EpubPreferences, + public fun createPreferencesEditor( + currentPreferences: EpubPreferences ): EpubPreferencesEditor = EpubPreferencesEditor( initialPreferences = currentPreferences, diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt index 571bcaa24e..fa62e9aa54 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt @@ -21,6 +21,7 @@ import android.webkit.WebResourceResponse import android.webkit.WebView import androidx.collection.forEach import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentFactory @@ -28,7 +29,7 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import androidx.lifecycle.whenStarted +import androidx.lifecycle.withStarted import androidx.viewpager.widget.ViewPager import kotlin.math.ceil import kotlin.reflect.KClass @@ -41,16 +42,35 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.json.JSONObject -import org.readium.r2.navigator.* -import org.readium.r2.navigator.databinding.ActivityR2ViewpagerBinding +import org.readium.r2.navigator.DecorableNavigator +import org.readium.r2.navigator.Decoration +import org.readium.r2.navigator.DecorationId +import org.readium.r2.navigator.ExperimentalDecorator +import org.readium.r2.navigator.HyperlinkNavigator +import org.readium.r2.navigator.NavigatorFragment +import org.readium.r2.navigator.OverflowableNavigator +import org.readium.r2.navigator.R +import org.readium.r2.navigator.R2BasicWebView +import org.readium.r2.navigator.RestorationNotSupportedException +import org.readium.r2.navigator.SelectableNavigator +import org.readium.r2.navigator.Selection +import org.readium.r2.navigator.databinding.ReadiumNavigatorViewpagerBinding +import org.readium.r2.navigator.dummyPublication import org.readium.r2.navigator.epub.EpubNavigatorViewModel.RunScriptCommand import org.readium.r2.navigator.epub.css.FontFamilyDeclaration import org.readium.r2.navigator.epub.css.MutableFontFamilyDeclaration import org.readium.r2.navigator.epub.css.RsProperties import org.readium.r2.navigator.epub.css.buildFontFamilyDeclaration +import org.readium.r2.navigator.extensions.normalizeLocator import org.readium.r2.navigator.extensions.optRectF import org.readium.r2.navigator.extensions.positionsByResource import org.readium.r2.navigator.html.HtmlDecorationTemplates +import org.readium.r2.navigator.input.CompositeInputListener +import org.readium.r2.navigator.input.DragEvent +import org.readium.r2.navigator.input.InputListener +import org.readium.r2.navigator.input.KeyEvent +import org.readium.r2.navigator.input.KeyInterceptorView +import org.readium.r2.navigator.input.TapEvent import org.readium.r2.navigator.pager.R2EpubPageFragment import org.readium.r2.navigator.pager.R2PagerAdapter import org.readium.r2.navigator.pager.R2PagerAdapter.PageResource @@ -62,24 +82,25 @@ import org.readium.r2.navigator.util.createFragmentFactory import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.extensions.tryOrLog -import org.readium.r2.shared.fetcher.Resource +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.ReadingProgression as PublicationReadingProgression import org.readium.r2.shared.publication.epub.EpubLayout import org.readium.r2.shared.publication.presentation.presentation -import org.readium.r2.shared.publication.services.isRestricted import org.readium.r2.shared.publication.services.positionsByReadingOrder -import org.readium.r2.shared.util.launchWebBrowser +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.toAbsoluteUrl /** * Factory for a [JavascriptInterface] which will be injected in the web views. * * Return `null` if you don't want to inject the interface for the given resource. */ -typealias JavascriptInterfaceFactory = (resource: Link) -> Any? +public typealias JavascriptInterfaceFactory = (resource: Link) -> Any? /** * Navigator for EPUB publications. @@ -87,9 +108,8 @@ typealias JavascriptInterfaceFactory = (resource: Link) -> Any? * To use this [Fragment], create a factory with `EpubNavigatorFragment.createFactory()`. */ @OptIn(ExperimentalDecorator::class, ExperimentalReadiumApi::class, DelicateReadiumApi::class) -class EpubNavigatorFragment internal constructor( - override val publication: Publication, - private val baseUrl: String?, +public class EpubNavigatorFragment internal constructor( + publication: Publication, private val initialLocator: Locator?, readingOrder: List<Link>?, private val initialPreferences: EpubPreferences, @@ -97,8 +117,13 @@ class EpubNavigatorFragment internal constructor( internal val paginationListener: PaginationListener?, epubLayout: EpubLayout, private val defaults: EpubDefaults, - configuration: Configuration, -) : Fragment(), VisualNavigator, SelectableNavigator, DecorableNavigator, Configurable<EpubSettings, EpubPreferences> { + configuration: Configuration +) : NavigatorFragment(publication), + OverflowableNavigator, + SelectableNavigator, + DecorableNavigator, + HyperlinkNavigator, + Configurable<EpubSettings, EpubPreferences> { // Make a copy to prevent the user from modifying the configuration after initialization. internal val config: Configuration = configuration.copy().apply { @@ -111,7 +136,7 @@ class EpubNavigatorFragment internal constructor( } } - data class Configuration internal constructor( + public data class Configuration internal constructor( /** * Patterns for asset paths which will be available to EPUB resources under @@ -180,12 +205,12 @@ class EpubNavigatorFragment internal constructor( internal var fontFamilyDeclarations: List<FontFamilyDeclaration>, internal var javascriptInterfaces: Map<String, JavascriptInterfaceFactory> ) { - constructor( + public constructor( servedAssets: List<String> = emptyList(), readiumCssRsProperties: RsProperties = RsProperties(), decorationTemplates: HtmlDecorationTemplates = HtmlDecorationTemplates.defaultTemplates(), selectionActionModeCallback: ActionMode.Callback? = null, - shouldApplyInsetsPadding: Boolean? = true, + shouldApplyInsetsPadding: Boolean? = true ) : this( servedAssets = servedAssets, readiumCssRsProperties = readiumCssRsProperties, @@ -203,7 +228,7 @@ class EpubNavigatorFragment internal constructor( * Return `null` in [factory] to prevent adding the Javascript interface for a given * resource. */ - fun registerJavascriptInterface(name: String, factory: JavascriptInterfaceFactory) { + public fun registerJavascriptInterface(name: String, factory: JavascriptInterfaceFactory) { javascriptInterfaces += name to factory } @@ -214,7 +239,7 @@ class EpubNavigatorFragment internal constructor( * symbols are missing from [fontFamily]. */ @ExperimentalReadiumApi - fun addFontFamilyDeclaration( + public fun addFontFamilyDeclaration( fontFamily: FontFamily, alternates: List<FontFamily> = emptyList(), builderAction: (MutableFontFamilyDeclaration).() -> Unit @@ -226,24 +251,18 @@ class EpubNavigatorFragment internal constructor( ) } - companion object { - operator fun invoke(builder: Configuration.() -> Unit): Configuration = + public companion object { + public operator fun invoke(builder: Configuration.() -> Unit): Configuration = Configuration().apply(builder) } } - interface PaginationListener { - fun onPageChanged(pageIndex: Int, totalPages: Int, locator: Locator) {} - fun onPageLoaded() {} + public interface PaginationListener { + public fun onPageChanged(pageIndex: Int, totalPages: Int, locator: Locator) {} + public fun onPageLoaded() {} } - interface Listener : VisualNavigator.Listener - - init { - require(!publication.isRestricted) { "The provided publication is restricted. Check that any DRM was properly unlocked using a Content Protection." } - } - - override val presentation: StateFlow<VisualNavigator.Presentation> get() = viewModel.presentation + public interface Listener : OverflowableNavigator.Listener, HyperlinkNavigator.Listener // Configurable @@ -258,7 +277,7 @@ class EpubNavigatorFragment internal constructor( * * Note that this only work with reflowable resources. */ - suspend fun evaluateJavascript(script: String): String? { + public suspend fun evaluateJavascript(script: String): String? { val page = currentReflowablePageFragment ?: return null page.awaitLoaded() val webView = page.webView ?: return null @@ -267,8 +286,9 @@ class EpubNavigatorFragment internal constructor( private val viewModel: EpubNavigatorViewModel by viewModels { EpubNavigatorViewModel.createFactory( - requireActivity().application, publication, - baseUrl = baseUrl, config = this.config, + requireActivity().application, + publication, + config = this.config, initialPreferences = initialPreferences, listener = listener, layout = epubLayout, @@ -279,29 +299,30 @@ class EpubNavigatorFragment internal constructor( private val readingOrder: List<Link> = readingOrder ?: publication.readingOrder private val positionsByReadingOrder: List<List<Locator>> = - if (readingOrder != null) emptyList() - else runBlocking { publication.positionsByReadingOrder() } + if (readingOrder != null) { + emptyList() + } else { + runBlocking { publication.positionsByReadingOrder() } + } internal lateinit var positions: List<Locator> - lateinit var resourcePager: R2ViewPager + + internal lateinit var resourcePager: R2ViewPager private lateinit var resourcesSingle: List<PageResource> private lateinit var resourcesDouble: List<PageResource> - @Deprecated("Migrate to the new Settings API (see migration guide)") - val preferences: SharedPreferences get() = viewModel.preferences - - internal lateinit var publicationIdentifier: String + @Deprecated( + "Migrate to the new Settings API (see migration guide)", + level = DeprecationLevel.ERROR + ) + public val preferences: SharedPreferences get() = throw NotImplementedError() internal var currentPagerPosition: Int = 0 internal lateinit var adapter: R2PagerAdapter private lateinit var currentActivity: FragmentActivity - internal var navigatorDelegate: NavigatorDelegate? = null - - private val r2Activity: R2EpubActivity? get() = activity as? R2EpubActivity - - private var _binding: ActivityR2ViewpagerBinding? = null + private var _binding: ReadiumNavigatorViewpagerBinding? = null private val binding get() = _binding!! override fun onCreateView( @@ -310,11 +331,10 @@ class EpubNavigatorFragment internal constructor( savedInstanceState: Bundle? ): View { currentActivity = requireActivity() - _binding = ActivityR2ViewpagerBinding.inflate(inflater, container, false) - val view = binding.root + _binding = ReadiumNavigatorViewpagerBinding.inflate(inflater, container, false) + var view: View = binding.root positions = positionsByReadingOrder.flatten() - publicationIdentifier = publication.metadata.identifier ?: publication.metadata.title when (viewModel.layout) { EpubLayout.REFLOWABLE -> { @@ -353,7 +373,7 @@ class EpubNavigatorFragment internal constructor( leftLink = doublePageLeft, leftUrl = viewModel.urlTo(doublePageLeft), rightLink = doublePageRight, - rightUrl = viewModel.urlTo(doublePageRight), + rightUrl = viewModel.urlTo(doublePageRight) ) ) doublePageLeft = null @@ -362,7 +382,12 @@ class EpubNavigatorFragment internal constructor( } // add last page if there is only a left page remaining if (doublePageLeft != null) { - resourcesDouble.add(PageResource.EpubFxl(leftLink = doublePageLeft, leftUrl = viewModel.urlTo(doublePageLeft))) + resourcesDouble.add( + PageResource.EpubFxl( + leftLink = doublePageLeft, + leftUrl = viewModel.urlTo(doublePageLeft) + ) + ) } this.resourcesSingle = resourcesSingle @@ -373,11 +398,6 @@ class EpubNavigatorFragment internal constructor( resourcePager = binding.resourcePager resetResourcePager() - @Suppress("DEPRECATION") - if (viewModel.useLegacySettings && publication.cssStyle == ReadingProgression.RTL.value) { - resourcePager.direction = PublicationReadingProgression.RTL - } - resourcePager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() { override fun onPageSelected(position: Int) { @@ -409,6 +429,11 @@ class EpubNavigatorFragment internal constructor( } }) + // Fixed layout publications cannot intercept JS events yet. + if (publication.metadata.presentation.layout == EpubLayout.FIXED) { + view = KeyInterceptorView(view, inputListener) + } + return view } @@ -422,11 +447,13 @@ class EpubNavigatorFragment internal constructor( resourcePager = R2ViewPager(requireContext()) resourcePager.id = R.id.resourcePager - resourcePager.type = when (publication.metadata.presentation.layout) { - EpubLayout.REFLOWABLE, null -> Publication.TYPE.EPUB - EpubLayout.FIXED -> Publication.TYPE.FXL + resourcePager.publicationType = when (publication.metadata.presentation.layout) { + EpubLayout.REFLOWABLE, null -> R2ViewPager.PublicationType.EPUB + EpubLayout.FIXED -> R2ViewPager.PublicationType.FXL } resourcePager.setBackgroundColor(viewModel.settings.value.effectiveBackgroundColor) + // Let the page views handle the keyboard events. + resourcePager.isFocusable = false parent.addView(resourcePager) @@ -452,7 +479,7 @@ class EpubNavigatorFragment internal constructor( } adapter.listener = PagerAdapterListener() resourcePager.adapter = adapter - resourcePager.direction = readingProgression + resourcePager.direction = overflow.value.readingProgression resourcePager.layoutDirection = when (settings.value.readingProgression) { ReadingProgression.RTL -> LayoutDirection.RTL ReadingProgression.LTR -> LayoutDirection.LTR @@ -479,10 +506,17 @@ class EpubNavigatorFragment internal constructor( } viewLifecycleOwner.lifecycleScope.launch { - whenStarted { + withStarted { // Restore the last locator before a configuration change (e.g. screen rotation), or the // initial locator when given. - val locator = savedInstanceState?.getParcelable("locator") ?: initialLocator + val locator = savedInstanceState?.let { + BundleCompat.getParcelable( + it, + "locator", + Locator::class.java + ) + } + ?: initialLocator if (locator != null) { go(locator) } @@ -501,9 +535,6 @@ class EpubNavigatorFragment internal constructor( EpubNavigatorViewModel.Event.InvalidateViewPager -> { invalidateResourcePager() } - is EpubNavigatorViewModel.Event.OpenExternalLink -> { - launchWebBrowser(requireContext(), event.url) - } } } @@ -558,18 +589,24 @@ class EpubNavigatorFragment internal constructor( notifyCurrentLocation() } + @OptIn(DelicateReadiumApi::class) override fun go(locator: Locator, animated: Boolean, completion: () -> Unit): Boolean { + @Suppress("NAME_SHADOWING") + val locator = publication.normalizeLocator(locator) + listener?.onJumpToLocator(locator) - val href = locator.href - // Remove anchor - .substringBefore("#") + val href = locator.href.removeFragment() fun setCurrent(resources: List<PageResource>) { val page = resources.withIndex().firstOrNull { (_, res) -> when (res) { - is PageResource.EpubReflowable -> res.link.href == href - is PageResource.EpubFxl -> res.leftUrl?.endsWith(href) == true || res.rightUrl?.endsWith(href) == true + is PageResource.EpubReflowable -> + res.link.url() == href + is PageResource.EpubFxl -> + res.leftUrl?.toString()?.endsWith(href.toString()) == true || res.rightUrl?.toString()?.endsWith( + href.toString() + ) == true else -> false } } ?: return @@ -584,7 +621,6 @@ class EpubNavigatorFragment internal constructor( if (publication.metadata.presentation.layout != EpubLayout.FIXED) { setCurrent(resourcesSingle) } else { - when (viewModel.dualPageMode) { // FIXME: Properly implement DualPage.AUTO depending on the device orientation. DualPage.OFF, DualPage.AUTO -> { @@ -630,6 +666,32 @@ class EpubNavigatorFragment internal constructor( } } + // VisualNavigator + + override val publicationView: View + get() = requireView() + + override val overflow: StateFlow<OverflowableNavigator.Overflow> + get() = viewModel.overflow + + @Deprecated( + "Use `presentation.value.readingProgression` instead", + replaceWith = ReplaceWith("presentation.value.readingProgression"), + level = DeprecationLevel.ERROR + ) + override val readingProgression: PublicationReadingProgression + get() = throw NotImplementedError() + + private val inputListener = CompositeInputListener() + + override fun addInputListener(listener: InputListener) { + inputListener.add(listener) + } + + override fun removeInputListener(listener: InputListener) { + inputListener.remove(listener) + } + // SelectableNavigator override suspend fun currentSelection(): Selection? { @@ -678,7 +740,12 @@ class EpubNavigatorFragment internal constructor( viewModel.removeDecorationListener(listener) } + @OptIn(DelicateReadiumApi::class) override suspend fun applyDecorations(decorations: List<Decoration>, group: String) { + @Suppress("NAME_SHADOWING") + val decorations = decorations + .map { it.copy(locator = publication.normalizeLocator(it.locator)) } + run(viewModel.applyDecorations(decorations, group)) } @@ -686,10 +753,9 @@ class EpubNavigatorFragment internal constructor( internal val webViewListener: R2BasicWebView.Listener = WebViewListener() - @OptIn(ExperimentalDragGesture::class) private inner class WebViewListener : R2BasicWebView.Listener { - override val readingProgression: PublicationReadingProgression + override val readingProgression: ReadingProgression get() = viewModel.readingProgression override fun onResourceLoaded(link: Link?, webView: R2BasicWebView, url: String?) { @@ -697,57 +763,36 @@ class EpubNavigatorFragment internal constructor( } override fun onPageLoaded() { - r2Activity?.onPageLoaded() paginationListener?.onPageLoaded() notifyCurrentLocation() } - override fun onPageChanged(pageIndex: Int, totalPages: Int, url: String) { - r2Activity?.onPageChanged(pageIndex = pageIndex, totalPages = totalPages, url = url) - } - - override fun onPageEnded(end: Boolean) { - r2Activity?.onPageEnded(end) - } - override fun javascriptInterfacesForResource(link: Link): Map<String, Any?> = config.javascriptInterfaces.mapValues { (_, factory) -> factory(link) } - @Suppress("DEPRECATION") - override fun onScroll() { - val activity = r2Activity ?: return - if (activity.supportActionBar?.isShowing == true && activity.allowToggleActionBar) { - resourcePager.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - or View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar - or View.SYSTEM_UI_FLAG_IMMERSIVE - ) - } - } - override fun onTap(point: PointF): Boolean = - listener?.onTap(point.adjustedToViewport()) ?: false + inputListener.onTap(TapEvent(point)) override fun onDragStart(event: R2BasicWebView.DragEvent): Boolean = - listener?.onDragStart( - startPoint = event.startPoint.adjustedToViewport(), - offset = event.offset - ) ?: false + onDrag(DragEvent.Type.Start, event) override fun onDragMove(event: R2BasicWebView.DragEvent): Boolean = - listener?.onDragMove( - startPoint = event.startPoint.adjustedToViewport(), - offset = event.offset - ) ?: false + onDrag(DragEvent.Type.Move, event) override fun onDragEnd(event: R2BasicWebView.DragEvent): Boolean = - listener?.onDragEnd( - startPoint = event.startPoint.adjustedToViewport(), - offset = event.offset - ) ?: false + onDrag(DragEvent.Type.End, event) + + private fun onDrag(type: DragEvent.Type, event: R2BasicWebView.DragEvent): Boolean = + inputListener.onDrag( + DragEvent( + type = type, + start = event.startPoint.adjustedToViewport(), + offset = event.offset + ) + ) + + override fun onKey(event: KeyEvent): Boolean = + inputListener.onKey(event) override fun onDecorationActivated( id: DecorationId, @@ -766,14 +811,6 @@ class EpubNavigatorFragment internal constructor( notifyCurrentLocation() } - override fun onHighlightActivated(id: String) { - r2Activity?.highlightActivated(id) - } - - override fun onHighlightAnnotationMarkActivated(id: String) { - r2Activity?.highlightAnnotationMarkActivated(id) - } - override fun goToPreviousResource(jump: Boolean, animated: Boolean): Boolean { return this@EpubNavigatorFragment.goToPreviousResource(jump = jump, animated = animated) } @@ -789,7 +826,7 @@ class EpubNavigatorFragment internal constructor( * Prevents opening external links in the web view and handles internal links. */ override fun shouldOverrideUrlLoading(webView: WebView, request: WebResourceRequest): Boolean { - val url = request.url ?: return false + val url = request.url.toAbsoluteUrl() ?: return false viewModel.navigateToUrl(url) return true } @@ -797,7 +834,7 @@ class EpubNavigatorFragment internal constructor( override fun shouldInterceptRequest(webView: WebView, request: WebResourceRequest): WebResourceResponse? = viewModel.shouldInterceptRequest(request) - override fun resourceAtUrl(url: String): Resource? = + override fun resourceAtUrl(url: Url): Resource? = viewModel.internalLinkFromUrl(url) ?.let { publication.get(it) } } @@ -896,8 +933,11 @@ class EpubNavigatorFragment internal constructor( ?.let { publication.locatorFromLink(it) } private val r2PagerAdapter: R2PagerAdapter? - get() = if (::resourcePager.isInitialized) resourcePager.adapter as? R2PagerAdapter - else null + get() = if (::resourcePager.isInitialized) { + resourcePager.adapter as? R2PagerAdapter + } else { + null + } private val currentReflowablePageFragment: R2EpubPageFragment? get() = currentFragment as? R2EpubPageFragment @@ -912,21 +952,18 @@ class EpubNavigatorFragment internal constructor( * Returns the reflowable page fragment matching the given href, if it is already loaded in the * view pager. */ - private fun loadedFragmentForHref(href: String): R2EpubPageFragment? { + private fun loadedFragmentForHref(href: Url): R2EpubPageFragment? { val adapter = r2PagerAdapter ?: return null adapter.mFragments.forEach { _, fragment -> val pageFragment = fragment as? R2EpubPageFragment ?: return@forEach val link = pageFragment.link ?: return@forEach - if (link.href == href) { + if (link.url() == href) { return pageFragment } } return null } - override val readingProgression: PublicationReadingProgression - get() = viewModel.readingProgression - override val currentLocator: StateFlow<Locator> get() = _currentLocator private val _currentLocator = MutableStateFlow( initialLocator @@ -944,8 +981,8 @@ class EpubNavigatorFragment internal constructor( val resource = readingOrder[resourcePager.currentItem] return currentReflowablePageFragment?.webView?.findFirstVisibleLocator() ?.copy( - href = resource.href, - type = resource.type ?: MediaType.XHTML.toString() + href = resource.url(), + mediaType = resource.mediaType ?: MediaType.XHTML ) } @@ -958,9 +995,9 @@ class EpubNavigatorFragment internal constructor( /** * Mapping between reading order hrefs and the table of contents title. */ - private val tableOfContentsTitleByHref: Map<String, String> by lazy { - fun fulfill(linkList: List<Link>): MutableMap<String, String> { - var result: MutableMap<String, String> = mutableMapOf() + private val tableOfContentsTitleByHref: Map<Href, String> by lazy { + fun fulfill(linkList: List<Link>): MutableMap<Href, String> { + var result: MutableMap<Href, String> = mutableMapOf() for (link in linkList) { val title = link.title ?: "" @@ -971,7 +1008,7 @@ class EpubNavigatorFragment internal constructor( val subResult = fulfill(link.children) - result = (subResult + result) as MutableMap<String, String> + result = (subResult + result) as MutableMap<Href, String> } return result @@ -984,7 +1021,6 @@ class EpubNavigatorFragment internal constructor( // Make sure viewLifecycleOwner is accessible. view ?: return - val navigator = this debounceLocationNotificationJob?.cancel() debounceLocationNotificationJob = viewLifecycleOwner.lifecycleScope.launch { delay(100L) @@ -1002,18 +1038,22 @@ class EpubNavigatorFragment internal constructor( } ?: 0.0 val link = when (val pageResource = adapter.getResource(resourcePager.currentItem)) { - is PageResource.EpubFxl -> checkNotNull(pageResource.leftLink ?: pageResource.rightLink) + is PageResource.EpubFxl -> checkNotNull( + pageResource.leftLink ?: pageResource.rightLink + ) is PageResource.EpubReflowable -> pageResource.link - else -> throw IllegalStateException("Expected EpubFxl or EpubReflowable page resources") + else -> throw IllegalStateException( + "Expected EpubFxl or EpubReflowable page resources" + ) } - val positionLocator = publication.positionsByResource[link.href]?.let { positions -> + val positionLocator = publication.positionsByResource[link.url()]?.let { positions -> val index = ceil(progression * (positions.size - 1)).toInt() positions.getOrNull(index) } val currentLocator = Locator( - href = link.href, - type = link.type ?: MediaType.XHTML.toString(), + href = link.url(), + mediaType = link.mediaType ?: MediaType.XHTML, title = tableOfContentsTitleByHref[link.href] ?: positionLocator?.title ?: link.title, locations = (positionLocator?.locations ?: Locator.Locations()).copy( progression = progression @@ -1024,8 +1064,6 @@ class EpubNavigatorFragment internal constructor( _currentLocator.value = currentLocator // Deprecated notifications - @Suppress("DEPRECATION") - navigatorDelegate?.locationDidChange(navigator = navigator, locator = currentLocator) reflowableWebView?.let { paginationListener?.onPageChanged( pageIndex = it.mCurItem, @@ -1036,7 +1074,7 @@ class EpubNavigatorFragment internal constructor( } } - companion object { + public companion object { /** * Creates a factory for [EpubNavigatorFragment]. @@ -1049,9 +1087,16 @@ class EpubNavigatorFragment internal constructor( * @param readingOrder Custom order of resources to display. Used for example to display a * non-linear resource on its own. * @param listener Optional listener to implement to observe events, such as user taps. + * @param readingOrder Custom order of resources to display. Used for example to display a + * non-linear resource on its own. * @param config Additional configuration. */ - fun createFactory( + @Deprecated( + "Use `EpubNavigatorFactory().createFragmentFactory()` instead", + level = DeprecationLevel.ERROR + ) + @Suppress("UNUSED_PARAMETER") + public fun createFactory( publication: Publication, baseUrl: String? = null, initialLocator: Locator? = null, @@ -1060,16 +1105,7 @@ class EpubNavigatorFragment internal constructor( paginationListener: PaginationListener? = null, config: Configuration = Configuration(), initialPreferences: EpubPreferences = EpubPreferences() - ): FragmentFactory = - createFragmentFactory { - EpubNavigatorFragment( - publication, baseUrl, initialLocator, readingOrder, initialPreferences, - listener, paginationListener, - epubLayout = publication.metadata.presentation.layout ?: EpubLayout.REFLOWABLE, - defaults = EpubDefaults(), - configuration = config, - ) - } + ): FragmentFactory { throw NotImplementedError() } /** * Creates a factory for a dummy [EpubNavigatorFragment]. @@ -1077,11 +1113,10 @@ class EpubNavigatorFragment internal constructor( * Used when Android restore the [EpubNavigatorFragment] after the process was killed. You * need to make sure the fragment is removed from the screen before [onResume] is called. */ - fun createDummyFactory(): FragmentFactory = createFragmentFactory { + public fun createDummyFactory(): FragmentFactory = createFragmentFactory { EpubNavigatorFragment( publication = dummyPublication, - baseUrl = null, - initialLocator = Locator(href = "#", type = "application/xhtml+xml"), + initialLocator = Locator(href = Url("#")!!, mediaType = MediaType.XHTML), readingOrder = null, initialPreferences = EpubPreferences(), listener = null, @@ -1094,8 +1129,10 @@ class EpubNavigatorFragment internal constructor( /** * Returns a URL to the application asset at [path], served in the web views. + * + * Returns null if the given [path] is not valid or an absolute URL. */ - fun assetUrl(path: String): String = + public fun assetUrl(path: String): Url? = WebViewServer.assetUrl(path) } } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt index 8d33368173..6b66d5ee0b 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt @@ -9,11 +9,8 @@ package org.readium.r2.navigator.epub import android.app.Application -import android.content.Context -import android.content.SharedPreferences import android.graphics.PointF import android.graphics.RectF -import android.net.Uri import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import androidx.lifecycle.AndroidViewModel @@ -29,17 +26,16 @@ import org.readium.r2.navigator.epub.extensions.javascriptForGroup import org.readium.r2.navigator.html.HtmlDecorationTemplates import org.readium.r2.navigator.preferences.* import org.readium.r2.navigator.util.createViewModelFactory -import org.readium.r2.shared.COLUMN_COUNT_REF import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.SCROLL_REF -import org.readium.r2.shared.extensions.addPrefix import org.readium.r2.shared.extensions.mapStateIn +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.ReadingProgression as PublicationReadingProgression import org.readium.r2.shared.publication.epub.EpubLayout -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.RelativeUrl +import org.readium.r2.shared.util.Url internal enum class DualPage { AUTO, OFF, ON @@ -52,17 +48,11 @@ internal class EpubNavigatorViewModel( val config: EpubNavigatorFragment.Configuration, initialPreferences: EpubPreferences, val layout: EpubLayout, - val listener: VisualNavigator.Listener?, + val listener: EpubNavigatorFragment.Listener?, private val defaults: EpubDefaults, - baseUrl: String?, - private val server: WebViewServer?, + private val server: WebViewServer ) : AndroidViewModel(application) { - val useLegacySettings: Boolean = (server == null) - - val preferences: SharedPreferences = - application.getSharedPreferences("org.readium.r2.settings", Context.MODE_PRIVATE) - // Make a copy to prevent new decoration templates from being registered after initializing // the navigator. private val decorationTemplates: HtmlDecorationTemplates = config.decorationTemplates.copy() @@ -71,14 +61,14 @@ internal class EpubNavigatorViewModel( sealed class Scope { object CurrentResource : Scope() object LoadedResources : Scope() - data class Resource(val href: String) : Scope() + data class Resource(val href: Url) : Scope() data class WebView(val webView: R2BasicWebView) : Scope() } } sealed class Event { data class OpenInternalLink(val target: Link) : Event() - data class OpenExternalLink(val url: Uri) : Event() + /** Refreshes all the resources in the view pager. */ object InvalidateViewPager : Event() data class RunScript(val command: RunScriptCommand) : Event() @@ -95,30 +85,23 @@ internal class EpubNavigatorViewModel( val settings: StateFlow<EpubSettings> = _settings.asStateFlow() - val presentation: StateFlow<VisualNavigator.Presentation> = _settings + val overflow: StateFlow<OverflowableNavigator.Overflow> = _settings .mapStateIn(viewModelScope) { settings -> - SimplePresentation( + SimpleOverflow( readingProgression = settings.readingProgression, scroll = settings.scroll, - axis = if (settings.scroll && !settings.verticalText) Axis.VERTICAL - else Axis.HORIZONTAL + axis = if (settings.scroll && !settings.verticalText) { + Axis.VERTICAL + } else { + Axis.HORIZONTAL + } ) } - private val googleFonts: List<FontFamily> = - if (useLegacySettings) - listOf( - FontFamily.LITERATA, FontFamily.PT_SERIF, FontFamily.ROBOTO, - FontFamily.SOURCE_SANS_PRO, FontFamily.VOLLKORN - ) - else - emptyList() - private val css = MutableStateFlow( ReadiumCss( rsProperties = config.readiumCssRsProperties, fontFamilyDeclarations = config.fontFamilyDeclarations, - googleFonts = googleFonts, assetsBaseHref = WebViewServer.assetsBaseHref ).update(settings.value, useReadiumCssFontSize = config.useReadiumCssFontSize) ) @@ -165,7 +148,7 @@ internal class EpubNavigatorViewModel( if (link != null) { for ((group, decorations) in decorations) { val changes = decorations - .filter { it.locator.href == link.href } + .filter { it.locator.href == link.url() } .map { DecorationChange.Added(it) } val groupScript = changes.javascriptForGroup(group, decorationTemplates) ?: continue @@ -178,56 +161,44 @@ internal class EpubNavigatorViewModel( // Serving resources - val baseUrl: String = - baseUrl?.let { it.removeSuffix("/") + "/" } - ?: publication.linkWithRel("self")?.href + val baseUrl: AbsoluteUrl = + (publication.baseUrl as? AbsoluteUrl) ?: WebViewServer.publicationBaseHref /** * Generates the URL to the given publication link. */ - fun urlTo(link: Link): String = - with(link) { - // Already an absolute URL? - if (Uri.parse(href).scheme != null) { - href - } else { - Href( - href = href.removePrefix("/"), - baseHref = baseUrl - ).percentEncodedString - } - } + fun urlTo(link: Link): AbsoluteUrl = + baseUrl.resolve(link.url()) /** * Intercepts and handles web view navigation to [url]. */ - fun navigateToUrl(url: Uri) = viewModelScope.launch { - val href = url.toString() - val link = internalLinkFromUrl(href) + fun navigateToUrl(url: AbsoluteUrl) = viewModelScope.launch { + val link = internalLinkFromUrl(url) if (link != null) { - if (listener?.shouldJumpToLink(link) == true) { + if (listener == null || listener.shouldFollowInternalLink(link)) { _events.send(Event.OpenInternalLink(link)) } } else { - _events.send(Event.OpenExternalLink(url)) + listener?.onExternalLinkActivated(url) } } /** * Gets the publication [Link] targeted by the given [url]. */ - fun internalLinkFromUrl(url: String): Link? { - if (!url.startsWith(baseUrl)) return null + fun internalLinkFromUrl(url: Url): Link? { + val href = (baseUrl.relativize(url) as? RelativeUrl) + ?: return null - val href = url.removePrefix(baseUrl).addPrefix("/") return publication.linkWithHref(href) - // Query parameters must be kept as they might be relevant for the fetcher. - ?.copy(href = href) + // Query parameters must be kept as they might be relevant for the container. + ?.copy(href = Href(href)) } fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? = - server?.shouldInterceptRequest(request, css.value) + server.shouldInterceptRequest(request, css.value) fun submitPreferences(preferences: EpubPreferences) = viewModelScope.launch { val oldSettings = settings.value @@ -255,37 +226,23 @@ internal class EpubNavigatorViewModel( /** * Effective reading progression. */ - val readingProgression: PublicationReadingProgression get() = - if (useLegacySettings) { - publication.metadata.effectiveReadingProgression - } else when (settings.value.readingProgression) { - ReadingProgression.LTR -> PublicationReadingProgression.LTR - ReadingProgression.RTL -> PublicationReadingProgression.RTL - } + val readingProgression: ReadingProgression get() = + settings.value.readingProgression /** * Indicates whether the dual page mode is enabled. */ val dualPageMode: DualPage get() = - if (useLegacySettings) { - @Suppress("DEPRECATION") - when (preferences.getInt(COLUMN_COUNT_REF, 0)) { - 1 -> DualPage.OFF - 2 -> DualPage.ON - else -> DualPage.AUTO + when (layout) { + EpubLayout.FIXED -> when (settings.value.spread) { + Spread.AUTO -> DualPage.AUTO + Spread.ALWAYS -> DualPage.ON + Spread.NEVER -> DualPage.OFF } - } else { - when (layout) { - EpubLayout.FIXED -> when (settings.value.spread) { - Spread.AUTO -> DualPage.AUTO - Spread.ALWAYS -> DualPage.ON - Spread.NEVER -> DualPage.OFF - } - EpubLayout.REFLOWABLE -> when (settings.value.columnCount) { - ColumnCount.ONE -> DualPage.OFF - ColumnCount.TWO -> DualPage.ON - ColumnCount.AUTO -> DualPage.AUTO - } + EpubLayout.REFLOWABLE -> when (settings.value.columnCount) { + ColumnCount.ONE -> DualPage.OFF + ColumnCount.TWO -> DualPage.ON + ColumnCount.AUTO -> DualPage.AUTO } } @@ -293,14 +250,8 @@ internal class EpubNavigatorViewModel( * Indicates whether the navigator is scrollable instead of paginated. */ val isScrollEnabled: StateFlow<Boolean> get() = - if (useLegacySettings) { - @Suppress("DEPRECATION") - val scroll = preferences.getBoolean(SCROLL_REF, false) - MutableStateFlow(scroll) - } else { - settings.mapStateIn(viewModelScope) { - if (layout == EpubLayout.REFLOWABLE) it.scroll else false - } + settings.mapStateIn(viewModelScope) { + if (layout == EpubLayout.REFLOWABLE) it.scroll else false } // Selection @@ -369,7 +320,10 @@ internal class EpubNavigatorViewModel( ?: return false val event = DecorableNavigator.OnActivatedEvent( - decoration = decoration, group = group, rect = rect, point = point + decoration = decoration, + group = group, + rect = rect, + point = point ) for (listener in listeners) { if (listener.onDecorationActivated(event)) { @@ -385,30 +339,30 @@ internal class EpubNavigatorViewModel( fun createFactory( application: Application, publication: Publication, - baseUrl: String?, layout: EpubLayout, - listener: VisualNavigator.Listener?, + listener: EpubNavigatorFragment.Listener?, defaults: EpubDefaults, config: EpubNavigatorFragment.Configuration, initialPreferences: EpubPreferences ) = createViewModelFactory { EpubNavigatorViewModel( - application, publication, config, initialPreferences, layout, listener, + application, + publication, + config, + initialPreferences, + layout, + listener, defaults = defaults, - baseUrl = baseUrl, - server = if (baseUrl != null) null - else WebViewServer( - application, publication, + server = WebViewServer( + application, + publication, servedAssets = config.servedAssets, - disableSelectionWhenProtected = config.disableSelectionWhenProtected + disableSelectionWhenProtected = config.disableSelectionWhenProtected, + onResourceLoadFailed = { url, error -> + listener?.onResourceLoadFailed(url, error) + } ) ) } } } - -private val FontFamily.Companion.LITERATA: FontFamily get() = FontFamily("Literata") -private val FontFamily.Companion.PT_SERIF: FontFamily get() = FontFamily("PT Serif") -private val FontFamily.Companion.ROBOTO: FontFamily get() = FontFamily("Roboto") -private val FontFamily.Companion.SOURCE_SANS_PRO: FontFamily get() = FontFamily("Source Sans Pro") -private val FontFamily.Companion.VOLLKORN: FontFamily get() = FontFamily("Vollkorn") diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferences.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferences.kt index cb739ae57f..7eaf181bd0 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferences.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferences.kt @@ -49,7 +49,7 @@ import org.readium.r2.shared.util.Language */ @ExperimentalReadiumApi @Serializable -data class EpubPreferences( +public data class EpubPreferences( val backgroundColor: Color? = null, val columnCount: ColumnCount? = null, val fontFamily: FontFamily? = null, @@ -116,120 +116,128 @@ data class EpubPreferences( verticalText = other.verticalText ?: verticalText, wordSpacing = other.wordSpacing ?: wordSpacing ) -} - -/** - * Loads the preferences from the legacy EPUB settings stored in the [SharedPreferences] with - * given [sharedPreferencesName]. - * - * This can be used to migrate the legacy settings to the new [EpubPreferences] format. - * - * If you changed the `fontFamilyValues` in the original Test App `UserSettings`, pass it to - * [fontFamilies] to migrate the font family properly. - */ -@ExperimentalReadiumApi -fun EpubPreferences.Companion.fromLegacyEpubSettings( - context: Context, - sharedPreferencesName: String = "org.readium.r2.settings", - fontFamilies: List<String> = listOf( - "Original", "PT Serif", "Roboto", "Source Sans Pro", "Vollkorn", "OpenDyslexic", - "AccessibleDfA", "IA Writer Duospace" - ) -): EpubPreferences { - - val sp: SharedPreferences = - context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE) - - val fontFamily = sp - .takeIf { it.contains("fontFamily") } - ?.getInt("fontFamily", 0) - ?.let { fontFamilies.getOrNull(it) } - ?.takeUnless { it == "Original" } - ?.let { FontFamily(it) } - - val theme = sp - .takeIf { sp.contains("appearance") } - ?.getInt("appearance", 0) - ?.let { - when (it) { - 0 -> Theme.LIGHT - 1 -> Theme.SEPIA - 2 -> Theme.DARK - else -> null - } - } - - val scroll = sp - .takeIf { sp.contains("scroll") } - ?.getBoolean("scroll", false) - - val colCount = sp - .takeIf { sp.contains("colCount") } - ?.getInt("colCount", 0) - ?.let { - when (it) { - 0 -> ColumnCount.AUTO - 1 -> ColumnCount.ONE - 2 -> ColumnCount.TWO - else -> null - } - } - val pageMargins = sp - .takeIf { sp.contains("pageMargins") } - ?.getFloat("pageMargins", 1.0f) - ?.toDouble() - - val fontSize = sp - .takeIf { sp.contains("fontSize") } - ?.let { sp.getFloat("fontSize", 0f) } - ?.toDouble() - ?.let { it / 100 } - - val textAlign = sp - .takeIf { sp.contains("textAlign") } - ?.getInt("textAlign", 0) - ?.let { - when (it) { - 0 -> TextAlign.JUSTIFY - 1 -> TextAlign.START - else -> null - } + public companion object { + + /** + * Loads the preferences from the legacy EPUB settings stored in the [SharedPreferences] with + * given [sharedPreferencesName]. + * + * This can be used to migrate the legacy settings to the new [EpubPreferences] format. + * + * If you changed the `fontFamilyValues` in the original Test App `UserSettings`, pass it to + * [fontFamilies] to migrate the font family properly. + */ + @ExperimentalReadiumApi + public fun fromLegacyEpubSettings( + context: Context, + sharedPreferencesName: String = "org.readium.r2.settings", + fontFamilies: List<String> = listOf( + "Original", + "PT Serif", + "Roboto", + "Source Sans Pro", + "Vollkorn", + "OpenDyslexic", + "AccessibleDfA", + "IA Writer Duospace" + ) + ): EpubPreferences { + val sp: SharedPreferences = + context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE) + + val fontFamily = sp + .takeIf { it.contains("fontFamily") } + ?.getInt("fontFamily", 0) + ?.let { fontFamilies.getOrNull(it) } + ?.takeUnless { it == "Original" } + ?.let { FontFamily(it) } + + val theme = sp + .takeIf { sp.contains("appearance") } + ?.getInt("appearance", 0) + ?.let { + when (it) { + 0 -> Theme.LIGHT + 1 -> Theme.SEPIA + 2 -> Theme.DARK + else -> null + } + } + + val scroll = sp + .takeIf { sp.contains("scroll") } + ?.getBoolean("scroll", false) + + val colCount = sp + .takeIf { sp.contains("colCount") } + ?.getInt("colCount", 0) + ?.let { + when (it) { + 0 -> ColumnCount.AUTO + 1 -> ColumnCount.ONE + 2 -> ColumnCount.TWO + else -> null + } + } + + val pageMargins = sp + .takeIf { sp.contains("pageMargins") } + ?.getFloat("pageMargins", 1.0f) + ?.toDouble() + + val fontSize = sp + .takeIf { sp.contains("fontSize") } + ?.let { sp.getFloat("fontSize", 0f) } + ?.toDouble() + ?.let { it / 100 } + + val textAlign = sp + .takeIf { sp.contains("textAlign") } + ?.getInt("textAlign", 0) + ?.let { + when (it) { + 0 -> TextAlign.JUSTIFY + 1 -> TextAlign.START + else -> null + } + } + + val wordSpacing = sp + .takeIf { sp.contains("wordSpacing") } + ?.getFloat("wordSpacing", 0f) + ?.toDouble() + + val letterSpacing = sp + .takeIf { sp.contains("letterSpacing") } + ?.getFloat("letterSpacing", 0f) + ?.toDouble() + ?.let { it * 2 } + + val lineHeight = sp + .takeIf { sp.contains("lineHeight") } + ?.getFloat("lineHeight", 1.2f) + ?.toDouble() + + // Note that in the legacy preferences storage, "advanced settings" was incorrectly synonym to + // "publisher styles", hence we don't need to flip the value. + val publisherStyles = sp + .takeIf { sp.contains("advancedSettings") } + ?.getBoolean("advancedSettings", false) + + return EpubPreferences( + fontFamily = fontFamily, + theme = theme, + scroll = scroll, + columnCount = colCount, + pageMargins = pageMargins, + fontSize = fontSize, + textAlign = textAlign, + wordSpacing = wordSpacing, + letterSpacing = letterSpacing, + lineHeight = lineHeight, + publisherStyles = publisherStyles + ) } - - val wordSpacing = sp - .takeIf { sp.contains("wordSpacing") } - ?.getFloat("wordSpacing", 0f) - ?.toDouble() - - val letterSpacing = sp - .takeIf { sp.contains("letterSpacing") } - ?.getFloat("letterSpacing", 0f) - ?.toDouble() - ?.let { it * 2 } - - val lineHeight = sp - .takeIf { sp.contains("lineHeight") } - ?.getFloat("lineHeight", 1.2f) - ?.toDouble() - - // Note that in the legacy preferences storage, "advanced settings" was incorrectly synonym to - // "publisher styles", hence we don't need to flip the value. - val publisherStyles = sp - .takeIf { sp.contains("advancedSettings") } - ?.getBoolean("advancedSettings", false) - - return EpubPreferences( - fontFamily = fontFamily, - theme = theme, - scroll = scroll, - columnCount = colCount, - pageMargins = pageMargins, - fontSize = fontSize, - textAlign = textAlign, - wordSpacing = wordSpacing, - letterSpacing = letterSpacing, - lineHeight = lineHeight, - publisherStyles = publisherStyles - ) + } } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferencesEditor.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferencesEditor.kt index f045ea843e..04b4772f29 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferencesEditor.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferencesEditor.kt @@ -22,10 +22,10 @@ import org.readium.r2.shared.util.Language * or ranges. */ @ExperimentalReadiumApi -class EpubPreferencesEditor internal constructor( +public class EpubPreferencesEditor internal constructor( initialPreferences: EpubPreferences, publicationMetadata: Metadata, - val layout: EpubLayout, + public val layout: EpubLayout, defaults: EpubDefaults ) : PreferencesEditor<EpubPreferences> { @@ -59,12 +59,16 @@ class EpubPreferencesEditor internal constructor( * * When unset, the current [theme] background color is effective. */ - val backgroundColor: Preference<Color> = + public val backgroundColor: Preference<Color> = PreferenceDelegate( getValue = { preferences.backgroundColor }, - getEffectiveValue = { state.settings.backgroundColor ?: Color((theme.value ?: theme.effectiveValue).backgroundColor) }, + getEffectiveValue = { + state.settings.backgroundColor ?: Color( + (theme.value ?: theme.effectiveValue).backgroundColor + ) + }, getIsEffective = { preferences.backgroundColor != null }, - updateValue = { value -> updateValues { it.copy(backgroundColor = value) } }, + updateValue = { value -> updateValues { it.copy(backgroundColor = value) } } ) /** @@ -74,13 +78,13 @@ class EpubPreferencesEditor internal constructor( * - the publication is reflowable * - [scroll] is off */ - val columnCount: EnumPreference<ColumnCount> = + public val columnCount: EnumPreference<ColumnCount> = EnumPreferenceDelegate( getValue = { preferences.columnCount }, getEffectiveValue = { state.settings.columnCount }, getIsEffective = { layout == EpubLayout.REFLOWABLE && !state.settings.scroll }, updateValue = { value -> updateValues { it.copy(columnCount = value) } }, - supportedValues = listOf(ColumnCount.AUTO, ColumnCount.ONE, ColumnCount.TWO), + supportedValues = listOf(ColumnCount.AUTO, ColumnCount.ONE, ColumnCount.TWO) ) /** @@ -88,7 +92,7 @@ class EpubPreferencesEditor internal constructor( * * Only effective with reflowable publications. */ - val fontFamily: Preference<FontFamily?> = + public val fontFamily: Preference<FontFamily?> = PreferenceDelegate( getValue = { preferences.fontFamily }, getEffectiveValue = { state.settings.fontFamily }, @@ -103,7 +107,7 @@ class EpubPreferencesEditor internal constructor( * * Only effective with reflowable publications. */ - val fontSize: RangePreference<Double> = + public val fontSize: RangePreference<Double> = RangePreferenceDelegate( getValue = { preferences.fontSize }, getEffectiveValue = { state.settings.fontSize }, @@ -111,7 +115,7 @@ class EpubPreferencesEditor internal constructor( updateValue = { value -> updateValues { it.copy(fontSize = value) } }, supportedRange = 0.1..5.0, progressionStrategy = DoubleIncrement(0.1), - valueFormatter = percentFormatter(), + valueFormatter = percentFormatter() ) /** @@ -122,7 +126,7 @@ class EpubPreferencesEditor internal constructor( * * Only effective with reflowable publications. */ - val fontWeight: RangePreference<Double> = + public val fontWeight: RangePreference<Double> = RangePreferenceDelegate( getValue = { preferences.fontWeight }, getEffectiveValue = { state.settings.fontWeight ?: 1.0 }, @@ -141,12 +145,15 @@ class EpubPreferencesEditor internal constructor( * - [publisherStyles] is off * - the layout is LTR */ - val hyphens: Preference<Boolean> = + public val hyphens: Preference<Boolean> = PreferenceDelegate( getValue = { preferences.hyphens }, - getEffectiveValue = { state.settings.hyphens ?: false }, + getEffectiveValue = { + state.settings.hyphens + ?: (state.settings.textAlign == TextAlign.JUSTIFY) + }, getIsEffective = ::isHyphensEffective, - updateValue = { value -> updateValues { it.copy(hyphens = value) } }, + updateValue = { value -> updateValues { it.copy(hyphens = value) } } ) /** @@ -156,13 +163,13 @@ class EpubPreferencesEditor internal constructor( * - the publication is reflowable * - the [theme] is set to [Theme.DARK] */ - val imageFilter: EnumPreference<ImageFilter?> = + public val imageFilter: EnumPreference<ImageFilter?> = EnumPreferenceDelegate( getValue = { preferences.imageFilter }, getEffectiveValue = { state.settings.imageFilter }, getIsEffective = { state.settings.theme == Theme.DARK }, updateValue = { value -> updateValues { it.copy(imageFilter = value) } }, - supportedValues = listOf(ImageFilter.DARKEN, ImageFilter.INVERT), + supportedValues = listOf(ImageFilter.DARKEN, ImageFilter.INVERT) ) /** @@ -170,12 +177,12 @@ class EpubPreferencesEditor internal constructor( * * This has an impact on the resolved layout (e.g. LTR, RTL). */ - val language: Preference<Language?> = + public val language: Preference<Language?> = PreferenceDelegate( getValue = { preferences.language }, getEffectiveValue = { state.settings.language }, getIsEffective = { true }, - updateValue = { value -> updateValues { it.copy(language = value) } }, + updateValue = { value -> updateValues { it.copy(language = value) } } ) /** @@ -186,7 +193,7 @@ class EpubPreferencesEditor internal constructor( * - [publisherStyles] is off * - the layout is LTR */ - val letterSpacing: RangePreference<Double> = + public val letterSpacing: RangePreference<Double> = RangePreferenceDelegate( getValue = { preferences.letterSpacing }, getEffectiveValue = { state.settings.letterSpacing ?: 0.0 }, @@ -194,7 +201,7 @@ class EpubPreferencesEditor internal constructor( updateValue = { value -> updateValues { it.copy(letterSpacing = value) } }, supportedRange = 0.0..1.0, progressionStrategy = DoubleIncrement(0.1), - valueFormatter = percentFormatter(), + valueFormatter = percentFormatter() ) /** @@ -205,12 +212,12 @@ class EpubPreferencesEditor internal constructor( * - [publisherStyles] is off * - the layout is RTL */ - val ligatures: Preference<Boolean> = + public val ligatures: Preference<Boolean> = PreferenceDelegate( getValue = { preferences.ligatures }, getEffectiveValue = { state.settings.ligatures ?: false }, getIsEffective = ::isLigaturesEffective, - updateValue = { value -> updateValues { it.copy(ligatures = value) } }, + updateValue = { value -> updateValues { it.copy(ligatures = value) } } ) /** @@ -220,7 +227,7 @@ class EpubPreferencesEditor internal constructor( * - the publication is reflowable * - [publisherStyles] is off */ - val lineHeight: RangePreference<Double> = + public val lineHeight: RangePreference<Double> = RangePreferenceDelegate( getValue = { preferences.lineHeight }, getEffectiveValue = { state.settings.lineHeight ?: 1.2 }, @@ -228,7 +235,7 @@ class EpubPreferencesEditor internal constructor( updateValue = { value -> updateValues { it.copy(lineHeight = value) } }, supportedRange = 1.0..2.0, progressionStrategy = DoubleIncrement(0.1), - valueFormatter = { it.format(5) }, + valueFormatter = { it.format(5) } ) /** @@ -236,7 +243,7 @@ class EpubPreferencesEditor internal constructor( * * Only effective with reflowable publications. */ - val pageMargins: RangePreference<Double> = + public val pageMargins: RangePreference<Double> = RangePreferenceDelegate( getValue = { preferences.pageMargins }, getEffectiveValue = { state.settings.pageMargins }, @@ -244,7 +251,7 @@ class EpubPreferencesEditor internal constructor( updateValue = { value -> updateValues { it.copy(pageMargins = value) } }, supportedRange = 0.0..4.0, progressionStrategy = DoubleIncrement(0.3), - valueFormatter = { it.format(5) }, + valueFormatter = { it.format(5) } ) /** @@ -255,7 +262,7 @@ class EpubPreferencesEditor internal constructor( * - [publisherStyles] is off * - the layout is LTR or RTL */ - val paragraphIndent: RangePreference<Double> = + public val paragraphIndent: RangePreference<Double> = RangePreferenceDelegate( getValue = { preferences.paragraphIndent }, getEffectiveValue = { state.settings.paragraphIndent ?: 0.0 }, @@ -263,7 +270,7 @@ class EpubPreferencesEditor internal constructor( updateValue = { value -> updateValues { it.copy(paragraphIndent = value) } }, supportedRange = 0.0..3.0, progressionStrategy = DoubleIncrement(0.2), - valueFormatter = percentFormatter(), + valueFormatter = percentFormatter() ) /** @@ -273,7 +280,7 @@ class EpubPreferencesEditor internal constructor( * - the publication is reflowable * - [publisherStyles] is off */ - val paragraphSpacing: RangePreference<Double> = + public val paragraphSpacing: RangePreference<Double> = RangePreferenceDelegate( getValue = { preferences.paragraphSpacing }, getEffectiveValue = { state.settings.paragraphSpacing ?: 0.0 }, @@ -281,7 +288,7 @@ class EpubPreferencesEditor internal constructor( updateValue = { value -> updateValues { it.copy(paragraphSpacing = value) } }, supportedRange = 0.0..2.0, progressionStrategy = DoubleIncrement(0.1), - valueFormatter = percentFormatter(), + valueFormatter = percentFormatter() ) /** @@ -290,12 +297,12 @@ class EpubPreferencesEditor internal constructor( * * Only effective with reflowable publications. */ - val publisherStyles: Preference<Boolean> = + public val publisherStyles: Preference<Boolean> = PreferenceDelegate( getValue = { preferences.publisherStyles }, getEffectiveValue = { state.settings.publisherStyles }, getIsEffective = { layout == EpubLayout.REFLOWABLE }, - updateValue = { value -> updateValues { it.copy(publisherStyles = value) } }, + updateValue = { value -> updateValues { it.copy(publisherStyles = value) } } ) /** @@ -303,13 +310,13 @@ class EpubPreferencesEditor internal constructor( * * This can be changed to influence directly the layout (e.g. LTR or RTL). */ - val readingProgression: EnumPreference<ReadingProgression> = + public val readingProgression: EnumPreference<ReadingProgression> = EnumPreferenceDelegate( getValue = { preferences.readingProgression }, getEffectiveValue = { state.settings.readingProgression }, getIsEffective = { true }, updateValue = { value -> updateValues { it.copy(readingProgression = value) } }, - supportedValues = listOf(ReadingProgression.LTR, ReadingProgression.RTL), + supportedValues = listOf(ReadingProgression.LTR, ReadingProgression.RTL) ) /** @@ -318,12 +325,12 @@ class EpubPreferencesEditor internal constructor( * * Only effective with reflowable publications. */ - val scroll: Preference<Boolean> = + public val scroll: Preference<Boolean> = PreferenceDelegate( getValue = { preferences.scroll }, getEffectiveValue = { state.settings.scroll }, getIsEffective = { layout == EpubLayout.REFLOWABLE }, - updateValue = { value -> updateValues { it.copy(scroll = value) } }, + updateValue = { value -> updateValues { it.copy(scroll = value) } } ) /** @@ -332,13 +339,13 @@ class EpubPreferencesEditor internal constructor( * * Only effective with fixed-layout publications. */ - val spread: EnumPreference<Spread> = + public val spread: EnumPreference<Spread> = EnumPreferenceDelegate( getValue = { preferences.spread }, getEffectiveValue = { state.settings.spread }, getIsEffective = { layout == EpubLayout.FIXED }, updateValue = { value -> updateValues { it.copy(spread = value) } }, - supportedValues = listOf(Spread.NEVER, Spread.ALWAYS), + supportedValues = listOf(Spread.NEVER, Spread.ALWAYS) ) /** @@ -349,13 +356,18 @@ class EpubPreferencesEditor internal constructor( * - [publisherStyles] is off * - the layout is LTR or RTL */ - val textAlign: EnumPreference<TextAlign?> = + public val textAlign: EnumPreference<TextAlign?> = EnumPreferenceDelegate( getValue = { preferences.textAlign }, getEffectiveValue = { state.settings.textAlign }, getIsEffective = ::isTextAlignEffective, updateValue = { value -> updateValues { it.copy(textAlign = value) } }, - supportedValues = listOf(TextAlign.START, TextAlign.LEFT, TextAlign.RIGHT, TextAlign.JUSTIFY), + supportedValues = listOf( + TextAlign.START, + TextAlign.LEFT, + TextAlign.RIGHT, + TextAlign.JUSTIFY + ) ) /** @@ -364,10 +376,14 @@ class EpubPreferencesEditor internal constructor( * When unset, the current [theme] text color is effective. * Only effective with reflowable publications. */ - val textColor: Preference<Color> = + public val textColor: Preference<Color> = PreferenceDelegate( getValue = { preferences.textColor }, - getEffectiveValue = { state.settings.textColor ?: Color((theme.value ?: theme.effectiveValue).contentColor) }, + getEffectiveValue = { + state.settings.textColor ?: Color( + (theme.value ?: theme.effectiveValue).contentColor + ) + }, getIsEffective = { layout == EpubLayout.REFLOWABLE && preferences.textColor != null }, updateValue = { value -> updateValues { it.copy(textColor = value) } } ) @@ -377,7 +393,7 @@ class EpubPreferencesEditor internal constructor( * * Only effective with reflowable publications. */ - val textNormalization: Preference<Boolean> = + public val textNormalization: Preference<Boolean> = PreferenceDelegate( getValue = { preferences.textNormalization }, getEffectiveValue = { state.settings.textNormalization }, @@ -390,13 +406,13 @@ class EpubPreferencesEditor internal constructor( * * Only effective with reflowable publications. */ - val theme: EnumPreference<Theme> = + public val theme: EnumPreference<Theme> = EnumPreferenceDelegate( getValue = { preferences.theme }, getEffectiveValue = { state.settings.theme }, getIsEffective = { layout == EpubLayout.REFLOWABLE }, updateValue = { value -> updateValues { it.copy(theme = value) } }, - supportedValues = listOf(Theme.LIGHT, Theme.DARK, Theme.SEPIA), + supportedValues = listOf(Theme.LIGHT, Theme.DARK, Theme.SEPIA) ) /** @@ -406,7 +422,7 @@ class EpubPreferencesEditor internal constructor( * - the publication is reflowable * - [publisherStyles] is off */ - val typeScale: RangePreference<Double> = + public val typeScale: RangePreference<Double> = RangePreferenceDelegate( getValue = { preferences.typeScale }, getEffectiveValue = { state.settings.typeScale ?: 1.2 }, @@ -414,7 +430,7 @@ class EpubPreferencesEditor internal constructor( updateValue = { value -> updateValues { it.copy(typeScale = value) } }, valueFormatter = { it.format(5) }, supportedRange = 1.0..2.0, - progressionStrategy = StepsProgression(1.0, 1.067, 1.125, 1.2, 1.25, 1.333, 1.414, 1.5, 1.618), + progressionStrategy = StepsProgression(1.0, 1.067, 1.125, 1.2, 1.25, 1.333, 1.414, 1.5, 1.618) ) /** @@ -423,12 +439,12 @@ class EpubPreferencesEditor internal constructor( * * Only effective with reflowable publications. */ - val verticalText: Preference<Boolean> = + public val verticalText: Preference<Boolean> = PreferenceDelegate( getValue = { preferences.verticalText }, getEffectiveValue = { state.settings.verticalText }, getIsEffective = { layout == EpubLayout.REFLOWABLE }, - updateValue = { value -> updateValues { it.copy(verticalText = value) } }, + updateValue = { value -> updateValues { it.copy(verticalText = value) } } ) /** @@ -438,7 +454,7 @@ class EpubPreferencesEditor internal constructor( * - the publication is reflowable * - the layout is LTR */ - val wordSpacing: RangePreference<Double> = + public val wordSpacing: RangePreference<Double> = RangePreferenceDelegate( getValue = { preferences.wordSpacing }, getEffectiveValue = { state.settings.wordSpacing ?: 0.0 }, @@ -446,7 +462,7 @@ class EpubPreferencesEditor internal constructor( updateValue = { value -> updateValues { it.copy(wordSpacing = value) } }, supportedRange = 0.0..1.0, progressionStrategy = DoubleIncrement(0.1), - valueFormatter = percentFormatter(), + valueFormatter = percentFormatter() ) private fun percentFormatter(): (Double) -> String = @@ -471,7 +487,7 @@ class EpubPreferencesEditor internal constructor( private fun isHyphensEffective() = layout == EpubLayout.REFLOWABLE && state.layout.stylesheets == Layout.Stylesheets.Default && !state.settings.publisherStyles && - preferences.hyphens != null + (preferences.hyphens != null || state.settings.textAlign == TextAlign.JUSTIFY) private fun isLetterSpacingEffective() = layout == EpubLayout.REFLOWABLE && state.layout.stylesheets == Layout.Stylesheets.Default && diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferencesFilters.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferencesFilters.kt index fe07a1b1e2..795f405f4b 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferencesFilters.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferencesFilters.kt @@ -13,14 +13,14 @@ import org.readium.r2.shared.ExperimentalReadiumApi * Suggested filter to keep only shared [EpubPreferences]. */ @ExperimentalReadiumApi -object EpubSharedPreferencesFilter : PreferencesFilter<EpubPreferences> { +public object EpubSharedPreferencesFilter : PreferencesFilter<EpubPreferences> { override fun filter(preferences: EpubPreferences): EpubPreferences = preferences.copy( readingProgression = null, language = null, spread = null, - verticalText = null, + verticalText = null ) } @@ -28,13 +28,13 @@ object EpubSharedPreferencesFilter : PreferencesFilter<EpubPreferences> { * Suggested filter to keep only publication-specific [EpubPreferences]. */ @ExperimentalReadiumApi -object EpubPublicationPreferencesFilter : PreferencesFilter<EpubPreferences> { +public object EpubPublicationPreferencesFilter : PreferencesFilter<EpubPreferences> { override fun filter(preferences: EpubPreferences): EpubPreferences = EpubPreferences( readingProgression = preferences.readingProgression, language = preferences.language, spread = preferences.spread, - verticalText = preferences.verticalText, + verticalText = preferences.verticalText ) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferencesSerializer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferencesSerializer.kt index a4a9fbd406..bd187265fd 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferencesSerializer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubPreferencesSerializer.kt @@ -14,7 +14,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi * JSON serializer of [EpubPreferences]. */ @ExperimentalReadiumApi -class EpubPreferencesSerializer : PreferencesSerializer<EpubPreferences> { +public class EpubPreferencesSerializer : PreferencesSerializer<EpubPreferences> { override fun serialize(preferences: EpubPreferences): String = Json.encodeToString(EpubPreferences.serializer(), preferences) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubSettings.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubSettings.kt index f439ba1071..202cba0818 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubSettings.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubSettings.kt @@ -36,7 +36,7 @@ import org.readium.r2.shared.util.Language * @see EpubPreferences */ @ExperimentalReadiumApi -data class EpubSettings( +public data class EpubSettings( val backgroundColor: Color?, val columnCount: ColumnCount, val fontFamily: FontFamily?, @@ -66,7 +66,6 @@ data class EpubSettings( @OptIn(ExperimentalReadiumApi::class) internal fun ReadiumCss.update(settings: EpubSettings, useReadiumCssFontSize: Boolean): ReadiumCss { - fun resolveFontStack(fontFamily: String): List<String> = buildList { add(fontFamily) @@ -109,8 +108,11 @@ internal fun ReadiumCss.update(settings: EpubSettings, useReadiumCssFontSize: Bo backgroundColor = backgroundColor?.toCss(), fontOverride = (fontFamily != null || textNormalization), fontFamily = fontFamily?.toCss(), - fontSize = if (useReadiumCssFontSize) Length.Percent(fontSize) - else null, + fontSize = if (useReadiumCssFontSize) { + Length.Percent(fontSize) + } else { + null + }, advancedSettings = !publisherStyles, typeScale = typeScale, textAlign = when (textAlign) { @@ -130,9 +132,11 @@ internal fun ReadiumCss.update(settings: EpubSettings, useReadiumCssFontSize: Bo a11yNormalize = textNormalization, overrides = mapOf( "font-weight" to - if (fontWeight != null) + if (fontWeight != null) { (FontWeight.NORMAL.value * fontWeight).toInt().coerceIn(1, 1000).toString() - else "" + } else { + "" + } ) ) ) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubSettingsResolver.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubSettingsResolver.kt index dd18f81ca4..a4e3286fac 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubSettingsResolver.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubSettingsResolver.kt @@ -53,7 +53,7 @@ internal class EpubSettingsResolver( theme = theme, typeScale = preferences.typeScale ?: defaults.typeScale, verticalText = verticalText, - wordSpacing = preferences.wordSpacing ?: defaults.wordSpacing, + wordSpacing = preferences.wordSpacing ?: defaults.wordSpacing ) } @@ -64,6 +64,7 @@ internal class EpubSettingsResolver( val rpPref = preferences.readingProgression val langPref = preferences.language val metadataLanguage = metadata.language + val metadataReadingProgression = metadata.readingProgression // Compute language according to the following rule: // preference value > metadata value > default value > null @@ -80,10 +81,10 @@ internal class EpubSettingsResolver( rpPref langPref != null -> if (langPref.isRtl) ReadingProgression.RTL else ReadingProgression.LTR - metadata.readingProgression.isHorizontal == true -> - when (metadata.readingProgression) { + metadataReadingProgression != null -> + when (metadataReadingProgression) { PublicationReadingProgression.RTL -> ReadingProgression.RTL - else -> ReadingProgression.LTR + PublicationReadingProgression.LTR -> ReadingProgression.LTR } metadataLanguage != null -> if (metadataLanguage.isRtl) ReadingProgression.RTL else ReadingProgression.LTR diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt index d93b172c4a..92faf7169d 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt @@ -8,12 +8,16 @@ package org.readium.r2.navigator.epub import org.readium.r2.navigator.epub.css.ReadiumCss import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.TransformingResource import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.epub.EpubLayout import org.readium.r2.shared.publication.presentation.presentation import org.readium.r2.shared.publication.services.isProtected +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.TransformingResource import timber.log.Timber /** @@ -24,23 +28,30 @@ import timber.log.Timber @OptIn(ExperimentalReadiumApi::class) internal fun Resource.injectHtml( publication: Publication, + mediaType: MediaType, css: ReadiumCss, - baseHref: String, + baseHref: AbsoluteUrl, disableSelectionWhenProtected: Boolean ): Resource = TransformingResource(this) { bytes -> - val link = link() - check(link.mediaType.isHtml) + if (!mediaType.isHtml) { + return@TransformingResource Try.success(bytes) + } - var content = bytes.toString(link.mediaType.charset ?: Charsets.UTF_8).trim() + var content = bytes.toString(mediaType.charset ?: Charsets.UTF_8).trim() val injectables = mutableListOf<String>() - val baseUri = baseHref.removeSuffix("/") if (publication.metadata.presentation.layout == EpubLayout.REFLOWABLE) { content = css.injectHtml(content) - injectables.add(script("$baseUri/readium/scripts/readium-reflowable.js")) + injectables.add( + script( + baseHref.resolve(Url("readium/scripts/readium-reflowable.js")!!) + ) + ) } else { - injectables.add(script("$baseUri/readium/scripts/readium-fixed.js")) + injectables.add( + script(baseHref.resolve(Url("readium/scripts/readium-fixed.js")!!)) + ) } // Disable the text selection if the publication is protected. @@ -60,15 +71,15 @@ internal fun Resource.injectHtml( val headEndIndex = content.indexOf("</head>", 0, true) if (headEndIndex == -1) { - Timber.e("</head> closing tag not found in ${link.href}") + Timber.e("</head> closing tag not found in resource with href: $sourceUrl") } else { content = StringBuilder(content) .insert(headEndIndex, "\n" + injectables.joinToString("\n") + "\n") .toString() } - content.toByteArray() + Try.success(content.toByteArray()) } -private fun script(src: String): String = +private fun script(src: Url): String = """<script type="text/javascript" src="$src"></script>""" diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/IR2Highlightable.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/IR2Highlightable.kt deleted file mode 100644 index 09f1c3ed42..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/IR2Highlightable.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.epub - -import android.graphics.Rect -import org.readium.r2.shared.publication.Locator - -interface IR2Highlightable { - fun showHighlight(highlight: Highlight) - - fun showHighlights(highlights: Array<Highlight>) - - fun hideHighlightWithID(id: String) - - fun hideAllHighlights() - - fun rectangleForHighlightWithID(id: String, callback: (Rect?) -> Unit) - - fun rectangleForHighlightAnnotationMarkWithID(id: String): Rect? - - fun registerHighlightAnnotationMarkStyle(name: String, css: String) - - fun highlightActivated(id: String) - - fun highlightAnnotationMarkActivated(id: String) -} - -data class Highlight( - val id: String, - val locator: Locator, - val color: Int, - val style: Style, - val annotationMarkStyle: String? = null -) - -enum class Style { - highlight, underline, strikethrough -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/IR2Selectable.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/IR2Selectable.kt deleted file mode 100644 index b91ee3454c..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/IR2Selectable.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.epub - -import org.readium.r2.shared.publication.Locator - -interface IR2Selectable { - fun currentSelection(callback: (Locator?) -> Unit) -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/R2EpubActivity.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/R2EpubActivity.kt deleted file mode 100644 index fb0e46b428..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/R2EpubActivity.kt +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.epub - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.graphics.Color -import android.graphics.PointF -import android.graphics.Rect -import android.os.Bundle -import android.util.DisplayMetrics -import android.view.ActionMode -import android.view.View -import android.view.WindowManager -import androidx.appcompat.app.AppCompatActivity -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.StateFlow -import org.json.JSONException -import org.json.JSONObject -import org.readium.r2.navigator.* -import org.readium.r2.navigator.pager.R2EpubPageFragment -import org.readium.r2.navigator.pager.R2PagerAdapter -import org.readium.r2.navigator.pager.R2ViewPager -import org.readium.r2.navigator.util.CompositeFragmentFactory -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.extensions.getPublication -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.ReadingProgression - -open class R2EpubActivity : AppCompatActivity(), IR2Activity, IR2Selectable, IR2Highlightable, IR2TTS, CoroutineScope, VisualNavigator, EpubNavigatorFragment.Listener { - - /** - * Context of this scope. - */ - override val coroutineContext: CoroutineContext - get() = Dispatchers.Main - - override lateinit var preferences: SharedPreferences - override lateinit var publicationPath: String - override lateinit var publicationFileName: String - override lateinit var publication: Publication - override lateinit var publicationIdentifier: String - override var bookId: Long = -1 - protected lateinit var baseUrl: String - - override var allowToggleActionBar = true - - protected var navigatorDelegate: NavigatorDelegate? = null - - val adapter: R2PagerAdapter get() = - resourcePager.adapter as R2PagerAdapter - - override val resourcePager: R2ViewPager get() = - navigatorFragment().resourcePager - - private val currentFragment: R2EpubPageFragment? get() = - adapter.mFragments.get(adapter.getItemId(resourcePager.currentItem)) as? R2EpubPageFragment - - // For backward compatibility, we expose these properties only through the `R2EpubActivity`. - val positions: List<Locator> get() = navigatorFragment().positions - val currentPagerPosition: Int get() = navigatorFragment().currentPagerPosition - - override val currentLocator: StateFlow<Locator> - get() = navigatorFragment().currentLocator - - /** - * Locates the [EpubNavigatorFragment] instance. - * - * Reading apps may override this method to provide their own path to the navigator fragment. - */ - open fun navigatorFragment(): EpubNavigatorFragment = - supportFragmentManager.findFragmentByTag(getString(R.string.epub_navigator_tag)) as EpubNavigatorFragment - - override fun onCreate(savedInstanceState: Bundle?) { - preferences = getSharedPreferences("org.readium.r2.settings", Context.MODE_PRIVATE) - - publication = intent.getPublication(this) - publicationPath = intent.getStringExtra("publicationPath") ?: throw Exception("publicationPath required") - publicationFileName = intent.getStringExtra("publicationFileName") ?: throw Exception("publicationFileName required") - publicationIdentifier = publication.metadata.identifier ?: publication.metadata.title - baseUrl = intent.getStringExtra("baseUrl") ?: throw Exception("Intent extra `baseUrl` is required. Provide the URL returned by Server.addPublication()") - - val initialLocator = intent.getParcelableExtra("locator") as? Locator - - // This must be done before the call to super.onCreate, including by reading apps. - // Because they may want to set their own factories, let's use a CompositeFragmentFactory that retains - // previously set factories. - supportFragmentManager.fragmentFactory = CompositeFragmentFactory( - supportFragmentManager.fragmentFactory, - EpubNavigatorFragment.createFactory(publication, baseUrl = baseUrl, initialLocator = initialLocator, listener = this) - ) - - super.onCreate(savedInstanceState) - - setContentView(R.layout.activity_r2_epub) - - title = null - - // Add support for display cutout. - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { - window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - } - } - - override fun finish() { - setResult(Activity.RESULT_OK, intent) - super.finish() - } - - override fun onActionModeStarted(mode: ActionMode?) { - mode?.menu?.clear() - super.onActionModeStarted(mode) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == 2 && resultCode == Activity.RESULT_OK) { - val locator = data?.getParcelableExtra("locator") as? Locator - if (locator != null) { - go(locator) - } - } - super.onActivityResult(requestCode, resultCode, data) - } - - @Suppress("DEPRECATION") - override fun toggleActionBar() { - if (allowToggleActionBar) { - if (supportActionBar!!.isShowing) { - resourcePager.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - or View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar - or View.SYSTEM_UI_FLAG_IMMERSIVE - ) - } else { - resourcePager.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - ) - } - } - } - - @Deprecated("Use `presentation.value.readingProgression` instead", replaceWith = ReplaceWith("presentation.value.readingProgression")) - override val readingProgression: ReadingProgression - get() = navigatorFragment().readingProgression - - @ExperimentalReadiumApi - override val presentation: StateFlow<VisualNavigator.Presentation> - get() = navigatorFragment().presentation - - @Suppress("DEPRECATION") - override fun go(locator: Locator, animated: Boolean, completion: () -> Unit): Boolean { - navigatorFragment().go(locator, animated, completion) - - if (allowToggleActionBar && supportActionBar!!.isShowing) { - resourcePager.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - or View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar - or View.SYSTEM_UI_FLAG_IMMERSIVE - ) - } - - return true - } - - override fun go(link: Link, animated: Boolean, completion: () -> Unit): Boolean { - return navigatorFragment().go(link, animated, completion) - } - - override fun goForward(animated: Boolean, completion: () -> Unit): Boolean { - return navigatorFragment().goForward(animated, completion) - } - - override fun goBackward(animated: Boolean, completion: () -> Unit): Boolean { - return navigatorFragment().goBackward(animated, completion) - } - - override fun onTap(point: PointF): Boolean { - toggleActionBar() - return super.onTap(point) - } - - override fun currentSelection(callback: (Locator?) -> Unit) { - currentFragment?.webView?.getCurrentSelectionInfo { - val selection = JSONObject(it) - val resource = publication.readingOrder[resourcePager.currentItem] - val resourceHref = resource.href - val resourceType = resource.type ?: "" - val locations = Locator.Locations.fromJSON(selection.getJSONObject("locations")) - val text = Locator.Text.fromJSON(selection.getJSONObject("text")) - - val locator = Locator( - href = resourceHref, - type = resourceType, - locations = locations, - text = text - ) - callback(locator) - } - } - - override fun showHighlight(highlight: Highlight) { - currentFragment?.webView?.run { - val colorJson = JSONObject().apply { - put("red", Color.red(highlight.color)) - put("green", Color.green(highlight.color)) - put("blue", Color.blue(highlight.color)) - } - createHighlight(highlight.locator.toJSON().toString(), colorJson.toString()) { - if (highlight.annotationMarkStyle.isNullOrEmpty().not()) - createAnnotation(highlight.id) - } - } - } - - override fun showHighlights(highlights: Array<Highlight>) { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun hideHighlightWithID(id: String) { - currentFragment?.webView?.destroyHighlight(id) - currentFragment?.webView?.destroyHighlight(id.replace("HIGHLIGHT", "ANNOTATION")) - } - - override fun hideAllHighlights() { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun rectangleForHighlightWithID(id: String, callback: (Rect?) -> Unit) { - currentFragment?.webView?.rectangleForHighlightWithID(id) { - val rect = - try { - with(JSONObject(it)) { - val display = windowManager.defaultDisplay - val metrics = DisplayMetrics() - display.getMetrics(metrics) - val left = getDouble("left") - val width = getDouble("width") - val top = getDouble("top") * metrics.density - val height = getDouble("height") * metrics.density - Rect(left.toInt(), top.toInt(), width.toInt() + left.toInt(), top.toInt() + height.toInt()) - } - } catch (e: JSONException) { - null - } - callback(rect) - } - } - - override fun rectangleForHighlightAnnotationMarkWithID(id: String): Rect? { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun registerHighlightAnnotationMarkStyle(name: String, css: String) { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun highlightActivated(id: String) { - } - - override fun highlightAnnotationMarkActivated(id: String) { - } - - fun createHighlight(color: Int, callback: (Highlight) -> Unit) { - currentSelection { locator -> - val colorJson = JSONObject().apply { - put("red", Color.red(color)) - put("green", Color.green(color)) - put("blue", Color.blue(color)) - } - - currentFragment?.webView?.createHighlight(locator?.toJSON().toString(), colorJson.toString()) { - val json = JSONObject(it) - val id = json.getString("id") - callback( - Highlight( - id, - locator!!, - color, - Style.highlight - ) - ) - } - } - } - - fun createAnnotation(highlight: Highlight?, callback: (Highlight) -> Unit) { - if (highlight != null) { - currentFragment?.webView?.createAnnotation(highlight.id) - callback(highlight) - } else { - createHighlight(Color.rgb(150, 150, 150)) { - createAnnotation(it) { highlight -> - callback(highlight) - } - } - } - } -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt index 602444cf12..7a4047c264 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt @@ -7,7 +7,6 @@ package org.readium.r2.navigator.epub import android.app.Application -import android.content.res.AssetManager import android.os.PatternMatcher import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse @@ -16,16 +15,19 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.navigator.epub.css.ReadiumCss import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.ResourceInputStream -import org.readium.r2.shared.fetcher.StringResource -import org.readium.r2.shared.fetcher.fallback +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.asInputStream import org.readium.r2.shared.util.http.HttpHeaders import org.readium.r2.shared.util.http.HttpRange -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.StringResource +import org.readium.r2.shared.util.resource.fallback /** * Serves the publication resources and application assets in the EPUB navigator web views. @@ -35,18 +37,17 @@ internal class WebViewServer( private val application: Application, private val publication: Publication, servedAssets: List<String>, - private val disableSelectionWhenProtected: Boolean + private val disableSelectionWhenProtected: Boolean, + private val onResourceLoadFailed: (Url, ReadError) -> Unit ) { companion object { - val publicationBaseHref = "https://readium/publication/" - val assetsBaseHref = "https://readium/assets/" + val publicationBaseHref = AbsoluteUrl("https://readium/publication/")!! + val assetsBaseHref = AbsoluteUrl("https://readium/assets/")!! - fun assetUrl(path: String): String = - Href(path, baseHref = assetsBaseHref).percentEncodedString + fun assetUrl(path: String): Url? = + Url.fromDecodedPath(path)?.let { assetsBaseHref.resolve(it) } } - private val assetManager: AssetManager = application.assets - /** * Serves the requests of the navigator web views. * @@ -59,8 +60,11 @@ internal class WebViewServer( return when { path.startsWith("/publication/") -> { + val href = Url.fromDecodedPath(path.removePrefix("/publication/")) + ?: return null + servePublicationResource( - href = path.removePrefix("/publication"), + href = href, range = HttpHeaders(request.requestHeaders).range, css = css ) @@ -77,47 +81,80 @@ internal class WebViewServer( * * If the [Resource] is an HTML document, injects the required JavaScript and CSS files. */ - private fun servePublicationResource(href: String, range: HttpRange?, css: ReadiumCss): WebResourceResponse { + private fun servePublicationResource(href: Url, range: HttpRange?, css: ReadiumCss): WebResourceResponse { val link = publication.linkWithHref(href) // Query parameters must be kept as they might be relevant for the fetcher. - ?.copy(href = href) + ?.copy(href = Href(href)) ?: Link(href = href) - var resource = publication.get(link) - .fallback { errorResource(link, error = it) } - if (link.mediaType.isHtml) { - resource = resource.injectHtml( - publication, css, - baseHref = assetsBaseHref, - disableSelectionWhenProtected = disableSelectionWhenProtected + // Drop anchor because it is meant to be interpreted by the client. + val urlWithoutAnchor = href.removeFragment() + + var resource = publication + .get(urlWithoutAnchor) + ?.fallback { + onResourceLoadFailed(urlWithoutAnchor, it) + errorResource() + } ?: run { + val error = ReadError.Decoding( + "Resource not found at $urlWithoutAnchor in publication." ) + onResourceLoadFailed(urlWithoutAnchor, error) + errorResource() } + link.mediaType + ?.takeIf { it.isHtml } + ?.let { + resource = resource.injectHtml( + publication, + mediaType = it, + css, + baseHref = assetsBaseHref, + disableSelectionWhenProtected = disableSelectionWhenProtected + ) + } + val headers = mutableMapOf( - "Accept-Ranges" to "bytes", + "Accept-Ranges" to "bytes" ) if (range == null) { - return WebResourceResponse(link.type, null, 200, "OK", headers, ResourceInputStream(resource)) + return WebResourceResponse( + link.mediaType?.toString(), + null, + 200, + "OK", + headers, + resource.asInputStream() + ) } else { // Byte range request - val stream = ResourceInputStream(resource) + val stream = resource.asInputStream() val length = stream.available() val longRange = range.toLongRange(length.toLong()) headers["Content-Range"] = "bytes ${longRange.first}-${longRange.last}/$length" // Content-Length will automatically be filled by the WebView using the Content-Range header. -// headers["Content-Length"] = (longRange.last - longRange.first + 1).toString() - return WebResourceResponse(link.type, null, 206, "Partial Content", headers, stream) + // headers["Content-Length"] = (longRange.last - longRange.first + 1).toString() + // Weirdly, the WebView will call itself stream.skip to skip to the requested range. + return WebResourceResponse( + link.mediaType?.toString(), + null, + 206, + "Partial Content", + headers, + stream + ) } } - - private fun errorResource(link: Link, error: Resource.Exception): Resource = - StringResource(link.copy(type = MediaType.XHTML.toString())) { + private fun errorResource(): Resource = + StringResource { withContext(Dispatchers.IO) { - assetManager - .open("readium/error.xhtml").bufferedReader() - .use { it.readText() } - .replace("\${error}", error.getUserMessage(application)) - .replace("\${href}", link.href) + Try.success( + application.assets + .open("readium/error.xhtml") + .bufferedReader() + .use { it.readText() } + ) } } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/FontFamilyDeclaration.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/FontFamilyDeclaration.kt index f1fb492a07..96af0884cd 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/FontFamilyDeclaration.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/FontFamilyDeclaration.kt @@ -8,6 +8,7 @@ package org.readium.r2.navigator.epub.css import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.Either +import org.readium.r2.shared.util.Url /** * Build a declaration for [fontFamily] using [builderAction]. @@ -36,16 +37,6 @@ internal data class FontFamilyDeclaration( val fontFaces: List<FontFaceDeclaration> ) -/** - * Build a font face declaration for [fontFamily]. - */ -@ExperimentalReadiumApi -internal fun buildFontFaceDeclaration( - fontFamily: String, - builderAction: (MutableFontFaceDeclaration).() -> Unit -) = - MutableFontFaceDeclaration(fontFamily).apply(builderAction).toFontFaceDeclaration() - /** * An immutable font face declaration. */ @@ -54,17 +45,17 @@ internal data class FontFaceDeclaration( val fontFamily: String, val sources: List<FontFaceSource>, var fontStyle: FontStyle? = null, - var fontWeight: Either<FontWeight, ClosedRange<Int>>? = null, + var fontWeight: Either<FontWeight, ClosedRange<Int>>? = null ) { - fun links(urlNormalizer: (String) -> String): List<String> = + fun links(urlNormalizer: (Url) -> Url): List<String> = sources .filter { it.preload } .map { """<link rel="preload" href="${urlNormalizer(it.href)}" as="font" crossorigin="" />""" } - fun toCss(urlNormalizer: (String) -> String): String { + fun toCss(urlNormalizer: (Url) -> Url): String { val descriptors = buildMap { set("font-family", """"$fontFamily"""") @@ -99,7 +90,7 @@ internal data class FontFaceDeclaration( * `<link rel="preload">`. */ internal data class FontFaceSource( - val href: String, + val href: Url, val preload: Boolean = false ) @@ -107,13 +98,13 @@ internal data class FontFaceSource( * A mutable font family declaration. */ @ExperimentalReadiumApi -data class MutableFontFamilyDeclaration internal constructor( +public data class MutableFontFamilyDeclaration internal constructor( private val fontFamily: String, private val alternates: List<String>, private val fontFaces: MutableList<FontFaceDeclaration> = mutableListOf() ) { - fun addFontFace(builderAction: MutableFontFaceDeclaration.() -> Unit) { + public fun addFontFace(builderAction: MutableFontFaceDeclaration.() -> Unit) { val fontFace = MutableFontFaceDeclaration(fontFamily).apply(builderAction) fontFaces.add(fontFace.toFontFaceDeclaration()) } @@ -128,41 +119,55 @@ data class MutableFontFamilyDeclaration internal constructor( * A mutable font face declaration. */ @ExperimentalReadiumApi -data class MutableFontFaceDeclaration internal constructor( +public data class MutableFontFaceDeclaration internal constructor( private val fontFamily: String, private val sources: MutableList<FontFaceSource> = mutableListOf(), private var fontStyle: FontStyle? = null, private var fontWeight: Either<FontWeight, ClosedRange<Int>>? = null ) { + /** + * Add a source for the font face. + * + * @param path Path to the font file. + * @param preload Indicates whether this source will be declared for preloading in the HTML + * using `<link rel="preload">`. + */ + public fun addSource(path: String, preload: Boolean = false) { + val url = requireNotNull(Url.fromDecodedPath(path)) { + "Invalid font path: $path" + } + addSource(url, preload = preload) + } + /** * Add a source for the font face. * * @param preload Indicates whether this source will be declared for preloading in the HTML * using `<link rel="preload">`. */ - fun addSource(href: String, preload: Boolean = false) { + public fun addSource(href: Url, preload: Boolean = false) { this.sources.add(FontFaceSource(href = href, preload = preload)) } /** * Set the font style of the font face. */ - fun setFontStyle(fontStyle: FontStyle) { + public fun setFontStyle(fontStyle: FontStyle) { this.fontStyle = fontStyle } /** * Set the font weight of the font face. */ - fun setFontWeight(fontWeight: FontWeight) { + public fun setFontWeight(fontWeight: FontWeight) { this.fontWeight = Either(fontWeight) } /** * Set the font weight range of a variable font face. */ - fun setFontWeight(range: ClosedRange<Int>) { + public fun setFontWeight(range: ClosedRange<Int>) { require(range.start >= 1) require(range.endInclusive <= 1000) this.fontWeight = Either(range) @@ -176,7 +181,7 @@ data class MutableFontFaceDeclaration internal constructor( * Styles that a font can be styled with. */ @ExperimentalReadiumApi -enum class FontStyle { +public enum class FontStyle { NORMAL, ITALIC; } @@ -187,7 +192,7 @@ enum class FontStyle { * See https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-weight#common_weight_name_mapping */ @ExperimentalReadiumApi -enum class FontWeight(val value: Int) { +public enum class FontWeight(public val value: Int) { THIN(100), EXTRA_LIGHT(200), LIGHT(300), diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/Layout.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/Layout.kt index 2b638ec17e..420be17e12 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/Layout.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/Layout.kt @@ -21,7 +21,7 @@ import org.readium.r2.shared.util.Language internal data class Layout( val language: Language? = null, val stylesheets: Stylesheets = Stylesheets.Default, - val readingProgression: ReadingProgression = ReadingProgression.LTR, + val readingProgression: ReadingProgression = ReadingProgression.LTR ) { /** * Readium CSS stylesheet variants. @@ -29,8 +29,10 @@ internal data class Layout( enum class Stylesheets(val folder: String?, val htmlDir: HtmlDir) { /** Left to right */ Default(null, HtmlDir.Ltr), + /** Right to left */ Rtl("rtl", HtmlDir.Rtl), + /** * Asian language, laid out vertically. * @@ -38,6 +40,7 @@ internal data class Layout( * https://github.com/readium/readium-css/tree/master/css/dist#vertical */ CjkVertical("cjk-vertical", HtmlDir.Unspecified), + /** Asian language, laid out horizontally */ CjkHorizontal("cjk-horizontal", HtmlDir.Ltr); } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/Properties.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/Properties.kt index 304ecac70b..88ba71e686 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/Properties.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/Properties.kt @@ -4,6 +4,8 @@ * available in the top-level LICENSE file of the project. */ +@file:OptIn(ExperimentalReadiumApi::class) + package org.readium.r2.navigator.epub.css import androidx.annotation.ColorInt @@ -16,8 +18,8 @@ import org.readium.r2.shared.util.Either * Holds a set of Readium CSS properties applied together. */ @ExperimentalReadiumApi -interface Properties : Cssable { - fun toCssProperties(): Map<String, String?> +public interface Properties : Cssable { + public fun toCssProperties(): Map<String, String?> override fun toCss(): String? { val props = toCssProperties() @@ -88,7 +90,7 @@ interface Properties : Cssable { * subscripts. Requires: fontOverride */ @ExperimentalReadiumApi -data class UserProperties( +public data class UserProperties( // View mode val view: View? = null, @@ -125,7 +127,7 @@ data class UserProperties( // Accessibility val a11yNormalize: Boolean? = null, - val overrides: Map<String, String?> = emptyMap(), + val overrides: Map<String, String?> = emptyMap() ) : Properties { override fun toCssProperties(): Map<String, String?> = buildMap { @@ -229,7 +231,7 @@ data class UserProperties( * The value can be another variable e.g. var(-RS__monospaceTf). */ @ExperimentalReadiumApi -data class RsProperties( +public data class RsProperties( // Pagination val colWidth: Length? = null, val colCount: ColCount? = null, @@ -280,7 +282,7 @@ data class RsProperties( val compFontFamily: List<String>? = null, val codeFontFamily: List<String>? = null, - val overrides: Map<String, String?> = emptyMap(), + val overrides: Map<String, String?> = emptyMap() ) : Properties { override fun toCssProperties(): Map<String, String?> = buildMap { @@ -345,7 +347,7 @@ data class RsProperties( /** User view. */ @ExperimentalReadiumApi -enum class View(private val css: String) : Cssable { +public enum class View(private val css: String) : Cssable { PAGED("readium-paged-on"), SCROLL("readium-scroll-on"); @@ -354,7 +356,7 @@ enum class View(private val css: String) : Cssable { /** Reading mode. */ @ExperimentalReadiumApi -enum class Appearance(private val css: String?) : Cssable { +public enum class Appearance(private val css: String?) : Cssable { NIGHT("readium-night-on"), SEPIA("readium-sepia-on"); @@ -363,9 +365,9 @@ enum class Appearance(private val css: String?) : Cssable { /** CSS color. */ @ExperimentalReadiumApi -interface Color : Cssable { +public interface Color : Cssable { - data class Rgb(val red: kotlin.Int, val green: kotlin.Int, val blue: kotlin.Int) : Color { + public data class Rgb(val red: kotlin.Int, val green: kotlin.Int, val blue: kotlin.Int) : Color { init { require(red in 0..255) require(green in 0..255) @@ -376,7 +378,7 @@ interface Color : Cssable { } @JvmInline - value class Hex(val color: String) : Color { + public value class Hex(public val color: String) : Color { init { require(Regex("^#(?:[0-9a-fA-F]{3}){1,2}$").matches(color)) } @@ -385,7 +387,7 @@ interface Color : Cssable { } @JvmInline - value class Int(@ColorInt val color: kotlin.Int) : Color { + public value class Int(@ColorInt public val color: kotlin.Int) : Color { override fun toCss(): String = String.format("#%06X", 0xFFFFFF and color) } @@ -393,102 +395,102 @@ interface Color : Cssable { /** CSS length dimension. */ @ExperimentalReadiumApi -interface Length : Cssable { +public interface Length : Cssable { /** Absolute CSS length. */ - interface Absolute : Length + public interface Absolute : Length /** Centimeters */ @JvmInline - value class Cm(val value: Double) : Absolute { + public value class Cm(public val value: Double) : Absolute { override fun toCss(): String = value.toCss("cm") } /** Millimeters */ @JvmInline - value class Mm(val value: Double) : Absolute { + public value class Mm(public val value: Double) : Absolute { override fun toCss(): String = value.toCss("mm") } /** Inches */ @JvmInline - value class In(val value: Double) : Absolute { + public value class In(public val value: Double) : Absolute { override fun toCss(): String = value.toCss("in") } /** Pixels */ @JvmInline - value class Px(val value: Double) : Absolute { + public value class Px(public val value: Double) : Absolute { override fun toCss(): String = value.toCss("px") } /** Points */ @JvmInline - value class Pt(val value: Double) : Absolute { + public value class Pt(public val value: Double) : Absolute { override fun toCss(): String = value.toCss("pt") } /** Picas */ @JvmInline - value class Pc(val value: Double) : Absolute { + public value class Pc(public val value: Double) : Absolute { override fun toCss(): String = value.toCss("pc") } /** Relative CSS length. */ - interface Relative : Length + public interface Relative : Length /** Relative to the font-size of the element. */ @JvmInline - value class Em(val value: Double) : Relative { + public value class Em(public val value: Double) : Relative { override fun toCss(): String = value.toCss("em") } /** Relative to the width of the "0" (zero). */ @JvmInline - value class Ch(val value: Double) : Relative { + public value class Ch(public val value: Double) : Relative { override fun toCss(): String = value.toCss("ch") } /** Relative to font-size of the root element. */ @JvmInline - value class Rem(val value: Double) : Relative { + public value class Rem(public val value: Double) : Relative { override fun toCss(): String = value.toCss("rem") } /** Relative to 1% of the width of the viewport. */ @JvmInline - value class Vw(val value: Double) : Relative { + public value class Vw(public val value: Double) : Relative { override fun toCss(): String = value.toCss("vw") } /** Relative to 1% of the height of the viewport. */ @JvmInline - value class Vh(val value: Double) : Relative { + public value class Vh(public val value: Double) : Relative { override fun toCss(): String = value.toCss("vh") } /** Relative to 1% of viewport's smaller dimension. */ @JvmInline - value class VMin(val value: Double) : Relative { + public value class VMin(public val value: Double) : Relative { override fun toCss(): String = value.toCss("vmin") } /** Relative to 1% of viewport's larger dimension. */ @JvmInline - value class VMax(val value: Double) : Relative { + public value class VMax(public val value: Double) : Relative { override fun toCss(): String = value.toCss("vmax") } /** Relative to the parent element. */ @JvmInline - value class Percent(val value: Double) : Relative { + public value class Percent(public val value: Double) : Relative { override fun toCss(): String = (value * 100).toCss("%") } } /** Number of CSS columns. */ @ExperimentalReadiumApi -enum class ColCount(private val css: String) : Cssable { +public enum class ColCount(private val css: String) : Cssable { AUTO("auto"), ONE("1"), TWO("2"); @@ -498,7 +500,7 @@ enum class ColCount(private val css: String) : Cssable { /** CSS text alignment. */ @ExperimentalReadiumApi -enum class TextAlign(private val css: String) : Cssable { +public enum class TextAlign(private val css: String) : Cssable { START("start"), LEFT("left"), RIGHT("right"), @@ -509,7 +511,7 @@ enum class TextAlign(private val css: String) : Cssable { /** CSS hyphenation. */ @ExperimentalReadiumApi -enum class Hyphens(private val css: String) : Cssable { +public enum class Hyphens(private val css: String) : Cssable { NONE("none"), AUTO("auto"); @@ -518,7 +520,7 @@ enum class Hyphens(private val css: String) : Cssable { /** CSS ligatures. */ @ExperimentalReadiumApi -enum class Ligatures(private val css: String) : Cssable { +public enum class Ligatures(private val css: String) : Cssable { NONE("none"), COMMON("common-ligatures"); @@ -527,7 +529,7 @@ enum class Ligatures(private val css: String) : Cssable { /** CSS box sizing. */ @ExperimentalReadiumApi -enum class BoxSizing(private val css: String) : Cssable { +public enum class BoxSizing(private val css: String) : Cssable { CONTENT_BOX("content-box"), BORDER_BOX("border-box"); @@ -535,8 +537,8 @@ enum class BoxSizing(private val css: String) : Cssable { } @ExperimentalReadiumApi -fun interface Cssable { - fun toCss(): String? +public fun interface Cssable { + public fun toCss(): String? } private fun MutableMap<String, String?>.putCss(name: String, value: Cssable?) { @@ -558,8 +560,11 @@ private fun MutableMap<String, String?>.putCss(name: String, strings: List<Strin /** Readium CSS boolean flag. */ private fun flag(name: String, value: Boolean?) = Cssable { - if (value == true) "readium-$name-on" - else null + if (value == true) { + "readium-$name-on" + } else { + null + } } /** diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/ReadiumCss.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/ReadiumCss.kt index 53a829eadf..e0c9b4394c 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/ReadiumCss.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/css/ReadiumCss.kt @@ -13,6 +13,7 @@ import org.jsoup.nodes.Element import org.readium.r2.navigator.preferences.FontFamily import org.readium.r2.navigator.preferences.ReadingProgression import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.Url @ExperimentalReadiumApi internal data class ReadiumCss( @@ -21,7 +22,7 @@ internal data class ReadiumCss( val userProperties: UserProperties = UserProperties(), val fontFamilyDeclarations: List<FontFamilyDeclaration> = emptyList(), val googleFonts: List<FontFamily> = emptyList(), - val assetsBaseHref: String + val assetsBaseHref: Url ) { /** @@ -46,8 +47,6 @@ internal data class ReadiumCss( */ private fun injectStyles(content: StringBuilder) { val hasStyles = content.hasStyles() - val assetsBaseHref = assetsBaseHref.removeSuffix("/") - val stylesheetsFolder = assetsBaseHref + "/readium/readium-css/" + (layout.stylesheets.folder?.plus("/") ?: "") val headBeforeIndex = content.indexForOpeningTag("head") content.insert( @@ -55,7 +54,7 @@ internal data class ReadiumCss( "\n" + buildList { addAll(fontsInjectableLinks) - add(stylesheetLink(stylesheetsFolder + "ReadiumCSS-before.css")) + add(stylesheetLink(beforeCss)) // Fix Readium CSS issue with the positioning of <audio> elements. // https://github.com/readium/readium-css/issues/94 @@ -71,11 +70,11 @@ internal data class ReadiumCss( :root[style], :root { overflow: visible !important; } :root[style] > body, :root > body { overflow: visible !important; } </style> - """.trimMargin() + """.trimMargin() ) if (!hasStyles) { - add(stylesheetLink(stylesheetsFolder + "ReadiumCSS-default.css")) + add(stylesheetLink(defaultCss)) } }.joinToString("\n") + "\n" ) @@ -84,7 +83,7 @@ internal data class ReadiumCss( content.insert( endHeadIndex, "\n" + buildList { - add(stylesheetLink(stylesheetsFolder + "ReadiumCSS-after.css")) + add(stylesheetLink(afterCss)) if (fontsInjectableCss.isNotEmpty()) { add( @@ -99,6 +98,24 @@ internal data class ReadiumCss( ) } + private val stylesheetsFolder by lazy { + assetsBaseHref.resolve( + Url("readium/readium-css/${layout.stylesheets.folder?.plus("/") ?: ""}")!! + ) + } + + private val beforeCss by lazy { + stylesheetsFolder.resolve(Url("ReadiumCSS-before.css")!!) + } + + private val afterCss by lazy { + stylesheetsFolder.resolve(Url("ReadiumCSS-after.css")!!) + } + + private val defaultCss by lazy { + stylesheetsFolder.resolve(Url("ReadiumCSS-default.css")!!) + } + /** * Generates the font face declarations from the declared font families. */ @@ -131,8 +148,8 @@ internal data class ReadiumCss( .flatMap { it.links(::normalizeAssetUrl) } } - private fun normalizeAssetUrl(url: String): String = - assetsBaseHref.removeSuffix("/") + "/" + url.removePrefix("/") + private fun normalizeAssetUrl(url: Url): Url = + assetsBaseHref.resolve(url) /** * Returns whether the [String] receiver has any CSS styles. @@ -142,10 +159,12 @@ internal data class ReadiumCss( private fun CharSequence.hasStyles(): Boolean { return indexOf("<link ", 0, true) != -1 || indexOf(" style=", 0, true) != -1 || - Regex("<style.*?>", setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)).containsMatchIn(this) + Regex("<style.*?>", setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)).containsMatchIn( + this + ) } - private fun stylesheetLink(href: String): String = + private fun stylesheetLink(href: Url): String = """<link rel="stylesheet" type="text/css" href="$href"/>""" /** @@ -206,7 +225,10 @@ internal data class ReadiumCss( val body = document.body() if (body.hasLang()) { - content.insert(content.indexForTagAttributes("html"), " xml:lang=\"${body.lang() ?: language}\"") + content.insert( + content.indexForTagAttributes("html"), + " xml:lang=\"${body.lang() ?: language}\"" + ) } else { val injectable = " xml:lang=\"$language\"" content.insert(content.indexForTagAttributes("html"), injectable) @@ -234,4 +256,7 @@ internal data class ReadiumCss( ) + tag.length + 1 } -private val dirRegex = Regex("""(<(?:html|body)[^\>]*)\s+dir=[\"']\w*[\"']""", setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE)) +private val dirRegex = Regex( + """(<(?:html|body)[^\>]*)\s+dir=[\"']\w*[\"']""", + setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE) +) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLLayout.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLLayout.kt index 48f1ed9c64..ec130cc2df 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLLayout.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLLayout.kt @@ -31,7 +31,7 @@ import kotlin.math.roundToInt import kotlin.math.roundToLong import org.readium.r2.shared.extensions.equalsDelta -class R2FXLLayout : FrameLayout { +internal class R2FXLLayout : FrameLayout { private var scaleDetector: ScaleGestureDetector? = null private var gestureDetector: GestureDetector? = null @@ -67,8 +67,10 @@ class R2FXLLayout : FrameLayout { // allow parent views to intercept any touch events that we do not consume var isAllowParentInterceptOnEdge = true + // allow parent views to intercept any touch events that we do not consume even if we are in a scaled state var isAllowParentInterceptOnScaled = false + // minimum scale of the content var minScale = 1.0f set(minScale) { @@ -77,6 +79,7 @@ class R2FXLLayout : FrameLayout { maxScale = this.minScale } } + // maximum scale of the content var maxScale = 3.0f set(maxScale) { @@ -181,7 +184,11 @@ class R2FXLLayout : FrameLayout { init(context) } - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { init(context) } @@ -242,7 +249,9 @@ class R2FXLLayout : FrameLayout { override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { return if (isScrollingAllowed) { super.onInterceptTouchEvent(ev) - } else isAllowZoom + } else { + isAllowZoom + } } @SuppressLint("ClickableViewAccessibility") @@ -299,8 +308,7 @@ class R2FXLLayout : FrameLayout { dispatchOnLongTap(e) } } - - override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { + override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { var consumed = false if (e2.pointerCount == 1 && !scaleDetector!!.isInProgress) { // only drag if we have one pointer and aren't already scaling @@ -319,7 +327,7 @@ class R2FXLLayout : FrameLayout { return consumed } - override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { + override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { val scale = scale val newScale = scale.coerceIn(minScale, maxScale) if (newScale.equalsDelta(scale)) { @@ -366,8 +374,9 @@ class R2FXLLayout : FrameLayout { override fun onScale(detector: ScaleGestureDetector): Boolean { val scale = scale * detector.scaleFactor val scaleFactor = detector.scaleFactor - if (java.lang.Float.isNaN(scaleFactor) || java.lang.Float.isInfinite(scaleFactor)) + if (java.lang.Float.isNaN(scaleFactor) || java.lang.Float.isInfinite(scaleFactor)) { return false + } internalScale(scale, focusX, focusY) zoomDispatcher.onZoom(scale) @@ -485,7 +494,13 @@ class R2FXLLayout : FrameLayout { val child = getChildAt(0) if (child != null) { - R2FXLUtils.setRect(drawRect, child.left.toFloat(), child.top.toFloat(), child.right.toFloat(), child.bottom.toFloat()) + R2FXLUtils.setRect( + drawRect, + child.left.toFloat(), + child.top.toFloat(), + child.right.toFloat(), + child.bottom.toFloat() + ) scaledPointsToScreenPoints(drawRect) } else { // If no child is added, then center the drawrect, and let it be empty @@ -566,7 +581,12 @@ class R2FXLLayout : FrameLayout { mTargetX = p.x mTargetY = p.y if (scale) { - scaleMatrix.setScale(mZoomStart, mZoomStart, this@R2FXLLayout.focusX, this@R2FXLLayout.focusY) + scaleMatrix.setScale( + mZoomStart, + mZoomStart, + this@R2FXLLayout.focusX, + this@R2FXLLayout.focusY + ) matrixUpdated() } if (doTranslate()) { @@ -594,7 +614,6 @@ class R2FXLLayout : FrameLayout { } override fun run() { - if (mCancelled || !doScale() && !doTranslate()) { return } @@ -634,7 +653,6 @@ class R2FXLLayout : FrameLayout { private var mFinished = false internal fun fling(velocityX: Int, velocityY: Int) { - val startX = viewPortRect.left.roundToInt() val minX: Int val maxX: Int @@ -682,7 +700,6 @@ class R2FXLLayout : FrameLayout { override fun run() { if (!mScroller.isFinished && mScroller.computeScrollOffset()) { - val newX = mScroller.currX val newY = mScroller.currY diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLOnDoubleTapListener.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLOnDoubleTapListener.kt index 6c033a2bbf..3b091ee2b4 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLOnDoubleTapListener.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLOnDoubleTapListener.kt @@ -9,7 +9,7 @@ package org.readium.r2.navigator.epub.fxl -class R2FXLOnDoubleTapListener(private var threeStep: Boolean) : R2FXLLayout.OnDoubleTapListener { +internal class R2FXLOnDoubleTapListener(private var threeStep: Boolean) : R2FXLLayout.OnDoubleTapListener { override fun onDoubleTap(view: R2FXLLayout, info: R2FXLLayout.TapInfo): Boolean { try { diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLScroller.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLScroller.kt index 333a860c73..18efad2d8a 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLScroller.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLScroller.kt @@ -12,7 +12,7 @@ package org.readium.r2.navigator.epub.fxl import android.content.Context import android.widget.OverScroller -abstract class R2FXLScroller { +internal abstract class R2FXLScroller { abstract val isFinished: Boolean abstract val currX: Int diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLUtils.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLUtils.kt index 8668ee20e0..542ba17036 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLUtils.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/fxl/R2FXLUtils.kt @@ -14,7 +14,7 @@ import android.graphics.RectF import kotlin.math.roundToInt import kotlin.math.roundToLong -object R2FXLUtils { +internal object R2FXLUtils { /** * Round and set the values on the rectangle @@ -43,7 +43,12 @@ object R2FXLUtils { * @param bottom bottom */ fun setRect(rect: RectF, left: Float, top: Float, right: Float, bottom: Float) { - rect.set(left.roundToLong().toFloat(), top.roundToLong().toFloat(), right.roundToLong().toFloat(), bottom.roundToLong().toFloat()) + rect.set( + left.roundToLong().toFloat(), + top.roundToLong().toFloat(), + right.roundToLong().toFloat(), + bottom.roundToLong().toFloat() + ) } /** diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Duration.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Duration.kt index c788f2aeae..dd6ea19e67 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Duration.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Duration.kt @@ -11,9 +11,11 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit import kotlin.time.ExperimentalTime +import org.readium.r2.shared.InternalReadiumApi @ExperimentalTime -internal fun List<Duration>.sum(): Duration = +@InternalReadiumApi +public fun List<Duration>.sum(): Duration = fold(0.seconds) { a, b -> a + b } @JvmName("sumNullable") diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Extensions.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Extensions.kt index 47591a77dc..233a2f12ee 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Extensions.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Extensions.kt @@ -13,11 +13,11 @@ import androidx.core.view.ViewCompat /** returns true if the resolved layout direction of the content view in this * activity is ViewCompat.LAYOUT_DIRECTION_RTL. Otherwise false. */ -fun Activity.layoutDirectionIsRTL(): Boolean { +internal fun Activity.layoutDirectionIsRTL(): Boolean { return ViewCompat.getLayoutDirection(findViewById(android.R.id.content)) == ViewCompat.LAYOUT_DIRECTION_RTL } @ColorInt -fun Context.color(@ColorRes id: Int): Int { +internal fun Context.color(@ColorRes id: Int): Int { return ContextCompat.getColor(this, id) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/JSON.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/JSON.kt index 6832ea10f3..97d2db0402 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/JSON.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/JSON.kt @@ -12,7 +12,7 @@ import org.json.JSONObject /** * Parses a [RectF] from its JSON representation. */ -fun JSONObject.optRectF(name: String): RectF? = +internal fun JSONObject.optRectF(name: String): RectF? = optJSONObject(name)?.let { json -> val left = json.optDouble("left").toFloat() val top = json.optDouble("top").toFloat() diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Locator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Locator.kt index 38c1d8928f..df8621dcec 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Locator.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Locator.kt @@ -12,7 +12,6 @@ package org.readium.r2.navigator.extensions import java.util.* import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -import kotlin.time.ExperimentalTime import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.Locator @@ -23,8 +22,7 @@ import org.readium.r2.shared.publication.Locator /** * All named parameters found in the fragments, such as `p=5`. */ -@InternalReadiumApi -val Locator.Locations.fragmentParameters: Map<String, String> get() = +internal val Locator.Locations.fragmentParameters: Map<String, String> get() = fragments // Concatenates fragments together, after dropping any # .map { it.removePrefix("#") } @@ -62,13 +60,13 @@ internal val Locator.Locations.page: Int? get() = * * https://www.w3.org/TR/media-frags/ */ -internal val Locator.Locations.time: Duration? get() = +@InternalReadiumApi +public val Locator.Locations.time: Duration? get() = fragmentParameters["t"]?.toIntOrNull()?.seconds /** * Computes the time position from the resource duration. */ -@OptIn(ExperimentalTime::class) internal fun Locator.Locations.timeWithDuration(duration: Duration?): Duration? = - let(duration, progression) { d, p -> (p * d.inSeconds).seconds } + let(duration, progression) { d, p -> (p * d.inWholeSeconds).seconds } ?: time diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Number.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Number.kt index f2a4bc4be1..52d0eeb2ad 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Number.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Number.kt @@ -10,7 +10,7 @@ import java.text.NumberFormat import org.readium.r2.shared.InternalReadiumApi @InternalReadiumApi -fun Number.format(maximumFractionDigits: Int, percent: Boolean = false): String { +public fun Number.format(maximumFractionDigits: Int, percent: Boolean = false): String { val format = if (percent) NumberFormat.getPercentInstance() else NumberFormat.getNumberInstance() format.maximumFractionDigits = maximumFractionDigits return format.format(this) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Publication.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Publication.kt index 4ca319438e..70aa053e6a 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Publication.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/extensions/Publication.kt @@ -9,28 +9,43 @@ package org.readium.r2.navigator.extensions -import java.net.URL import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.extensions.tryOrNull +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.positions - -/** Computes an absolute URL to the given HREF. */ -internal fun Publication.urlToHref(href: String): URL? { - val baseUrl = this.baseUrl?.toString()?.removeSuffix("/") - val urlString = if (baseUrl != null && href.startsWith("/")) { - "$baseUrl$href" - } else { - href - } - return tryOrNull { URL(urlString) } -} +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Url // These extensions will be removed in the next release, with `PositionsService`. -internal val Publication.positionsSync: List<Locator> - get() = runBlocking { positions() } - -internal val Publication.positionsByResource: Map<String, List<Locator>> +internal val Publication.positionsByResource: Map<Url, List<Locator>> get() = runBlocking { positions().groupBy { it.href } } + +/** + * Historically, we used to have "absolute" HREFs in the manifest: + * - starting with a `/` for packaged publications. + * - resolved to the `self` link for remote publications. + * + * We removed the normalization and now use relative HREFs everywhere, but we still need to support + * the locators created with the old absolute HREFs. + */ +@DelicateReadiumApi +public fun Publication.normalizeLocator(locator: Locator): Locator { + val self = (baseUrl as? AbsoluteUrl) + + return if (self == null) { // Packaged publication + locator.copy( + href = Url(locator.href.toString().removePrefix("/")) + ?: return locator + ) + } else { // Remote publication + // Check that the locator HREF relative to `self` exists int he manifest. + val relativeHref = self.relativize(locator.href) + if (linkWithHref(relativeHref) != null) { + locator.copy(href = relativeHref) + } else { + locator + } + } +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/html/HtmlDecorationTemplate.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/html/HtmlDecorationTemplate.kt index eafd2084a0..e3600596b6 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/html/HtmlDecorationTemplate.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/html/HtmlDecorationTemplate.kt @@ -37,11 +37,11 @@ import org.readium.r2.shared.JSONable * your app name. r2- and readium- are reserved by the Readium toolkit. */ @ExperimentalDecorator -data class HtmlDecorationTemplate( +public data class HtmlDecorationTemplate( val layout: Layout, val width: Width = Width.WRAP, val element: (Decoration) -> String = { "<div/>" }, - val stylesheet: String? = null, + val stylesheet: String? = null ) : JSONable { /** @@ -49,9 +49,10 @@ data class HtmlDecorationTemplate( * DOM range. */ @Parcelize - enum class Layout(val value: String) : Parcelable { + public enum class Layout(public val value: String) : Parcelable { /** A single HTML element covering the smallest region containing all CSS border boxes. */ BOUNDS("bounds"), + /** One HTML element for each CSS border box (e.g. line of text). */ BOXES("boxes"); } @@ -60,13 +61,16 @@ data class HtmlDecorationTemplate( * Indicates how the width of each created HTML element expands in the viewport. */ @Parcelize - enum class Width(val value: String) : Parcelable { + public enum class Width(public val value: String) : Parcelable { /** Smallest width fitting the CSS border box. */ WRAP("wrap"), + /** Fills the bounds layout. */ BOUNDS("bounds"), + /** Fills the anchor page, useful for dual page. */ VIEWPORT("viewport"), + /** Fills the whole viewport. */ PAGE("page"); } @@ -78,21 +82,43 @@ data class HtmlDecorationTemplate( val bottom: Int = 0 ) - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { put("layout", layout.value) put("width", width.value) putOpt("stylesheet", stylesheet) } - companion object { + public companion object { /** Creates a new decoration template for the highlight style. */ - fun highlight(@ColorInt defaultTint: Int, lineWeight: Int, cornerRadius: Int, alpha: Double): HtmlDecorationTemplate = - createTemplate(asHighlight = true, defaultTint = defaultTint, lineWeight = lineWeight, cornerRadius = cornerRadius, alpha = alpha) + public fun highlight( + @ColorInt defaultTint: Int, + lineWeight: Int, + cornerRadius: Int, + alpha: Double + ): HtmlDecorationTemplate = + createTemplate( + asHighlight = true, + defaultTint = defaultTint, + lineWeight = lineWeight, + cornerRadius = cornerRadius, + alpha = alpha + ) /** Creates a new decoration template for the underline style. */ - fun underline(@ColorInt defaultTint: Int, lineWeight: Int, cornerRadius: Int, alpha: Double): HtmlDecorationTemplate = - createTemplate(asHighlight = false, defaultTint = defaultTint, lineWeight = lineWeight, cornerRadius = cornerRadius, alpha = alpha) + public fun underline( + @ColorInt defaultTint: Int, + lineWeight: Int, + cornerRadius: Int, + alpha: Double + ): HtmlDecorationTemplate = + createTemplate( + asHighlight = false, + defaultTint = defaultTint, + lineWeight = lineWeight, + cornerRadius = cornerRadius, + alpha = alpha + ) /** * @param asHighlight When true, the non active style is of an highlight. Otherwise, it is @@ -143,40 +169,56 @@ data class HtmlDecorationTemplate( } @ExperimentalDecorator -class HtmlDecorationTemplates private constructor( +public class HtmlDecorationTemplates private constructor( internal val styles: MutableMap<KClass<*>, HtmlDecorationTemplate> = mutableMapOf() ) : JSONable { - operator fun <S : Style> get(style: KClass<S>): HtmlDecorationTemplate? = + public operator fun <S : Style> get(style: KClass<S>): HtmlDecorationTemplate? = styles[style] - operator fun <S : Style> set(style: KClass<S>, template: HtmlDecorationTemplate) { + public operator fun <S : Style> set(style: KClass<S>, template: HtmlDecorationTemplate) { styles[style] = template } - override fun toJSON() = JSONObject( + override fun toJSON(): JSONObject = JSONObject( styles.entries.associate { it.key.qualifiedName!! to it.value.toJSON() } ) - fun copy() = HtmlDecorationTemplates(styles.toMutableMap()) + public fun copy(): HtmlDecorationTemplates = HtmlDecorationTemplates(styles.toMutableMap()) - companion object { - operator fun invoke(build: HtmlDecorationTemplates.() -> Unit): HtmlDecorationTemplates = + public companion object { + public operator fun invoke(build: HtmlDecorationTemplates.() -> Unit): HtmlDecorationTemplates = HtmlDecorationTemplates().apply(build) /** * Creates the default list of decoration styles with associated HTML templates. */ - fun defaultTemplates( + public fun defaultTemplates( @ColorInt defaultTint: Int = Color.YELLOW, lineWeight: Int = 2, cornerRadius: Int = 3, alpha: Double = 0.3 - ) = HtmlDecorationTemplates { - set(Style.Highlight::class, HtmlDecorationTemplate.highlight(defaultTint = defaultTint, lineWeight = lineWeight, cornerRadius = cornerRadius, alpha = alpha)) - set(Style.Underline::class, HtmlDecorationTemplate.underline(defaultTint = defaultTint, lineWeight = lineWeight, cornerRadius = cornerRadius, alpha = alpha)) + ): HtmlDecorationTemplates = HtmlDecorationTemplates { + set( + Style.Highlight::class, + HtmlDecorationTemplate.highlight( + defaultTint = defaultTint, + lineWeight = lineWeight, + cornerRadius = cornerRadius, + alpha = alpha + ) + ) + set( + Style.Underline::class, + HtmlDecorationTemplate.underline( + defaultTint = defaultTint, + lineWeight = lineWeight, + cornerRadius = cornerRadius, + alpha = alpha + ) + ) } } } @@ -186,10 +228,10 @@ class HtmlDecorationTemplates private constructor( * * @param alpha When set, overrides the actual color alpha. */ -fun @receiver:ColorInt Int.toCss(alpha: Double? = null): String { +public fun @receiver:ColorInt Int.toCss(alpha: Double? = null): String { val r = Color.red(this) val g = Color.green(this) val b = Color.blue(this) - val a = alpha ?: Color.alpha(this).toDouble() / 255 + val a = alpha ?: (Color.alpha(this).toDouble() / 255) return "rgba($r, $g, $b, $a)" } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt index 6aaca39a90..446e53bf48 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt @@ -13,52 +13,54 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentFactory import androidx.viewpager.widget.ViewPager -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.runBlocking +import org.readium.r2.navigator.NavigatorFragment +import org.readium.r2.navigator.OverflowableNavigator import org.readium.r2.navigator.RestorationNotSupportedException -import org.readium.r2.navigator.SimplePresentation +import org.readium.r2.navigator.SimpleOverflow import org.readium.r2.navigator.VisualNavigator -import org.readium.r2.navigator.databinding.ActivityR2ViewpagerBinding +import org.readium.r2.navigator.databinding.ReadiumNavigatorViewpagerBinding import org.readium.r2.navigator.dummyPublication import org.readium.r2.navigator.extensions.layoutDirectionIsRTL +import org.readium.r2.navigator.extensions.normalizeLocator +import org.readium.r2.navigator.input.CompositeInputListener +import org.readium.r2.navigator.input.InputListener +import org.readium.r2.navigator.input.KeyInterceptorView +import org.readium.r2.navigator.input.TapEvent import org.readium.r2.navigator.pager.R2CbzPageFragment import org.readium.r2.navigator.pager.R2PagerAdapter import org.readium.r2.navigator.pager.R2ViewPager import org.readium.r2.navigator.preferences.Axis import org.readium.r2.navigator.preferences.ReadingProgression import org.readium.r2.navigator.util.createFragmentFactory +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.ReadingProgression as PublicationReadingProgression import org.readium.r2.shared.publication.indexOfFirstWithHref -import org.readium.r2.shared.publication.services.isRestricted import org.readium.r2.shared.publication.services.positions +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType /** * Navigator for bitmap-based publications, such as CBZ. */ -@OptIn(ExperimentalReadiumApi::class) -class ImageNavigatorFragment private constructor( - override val publication: Publication, +@OptIn(ExperimentalReadiumApi::class, DelicateReadiumApi::class) +public class ImageNavigatorFragment private constructor( + publication: Publication, private val initialLocator: Locator? = null, internal val listener: Listener? = null -) : Fragment(), CoroutineScope by MainScope(), VisualNavigator { +) : NavigatorFragment(publication), OverflowableNavigator { - interface Listener : VisualNavigator.Listener - - init { - require(!publication.isRestricted) { "The provided publication is restricted. Check that any DRM was properly unlocked using a Content Protection." } - } + public interface Listener : VisualNavigator.Listener internal lateinit var positions: List<Locator> internal lateinit var resourcePager: R2ViewPager @@ -70,35 +72,23 @@ class ImageNavigatorFragment private constructor( override val currentLocator: StateFlow<Locator> get() = _currentLocator private val _currentLocator = MutableStateFlow( - initialLocator + initialLocator?.let { publication.normalizeLocator(it) } ?: requireNotNull(publication.locatorFromLink(publication.readingOrder.first())) ) internal var currentPagerPosition: Int = 0 internal var resources: List<String> = emptyList() - private var _binding: ActivityR2ViewpagerBinding? = null + private var _binding: ReadiumNavigatorViewpagerBinding? = null private val binding get() = _binding!! - override val readingProgression: PublicationReadingProgression = - publication.metadata.effectiveReadingProgression - - @ExperimentalReadiumApi - override val presentation: StateFlow<VisualNavigator.Presentation> = - MutableStateFlow( - SimplePresentation( - readingProgression = when (readingProgression) { - PublicationReadingProgression.RTL -> ReadingProgression.RTL - else -> ReadingProgression.LTR - }, - scroll = false, - axis = Axis.HORIZONTAL - ) - ).asStateFlow() - override fun onCreate(savedInstanceState: Bundle?) { childFragmentManager.fragmentFactory = createFragmentFactory { - R2CbzPageFragment(publication) { x, y -> this.listener?.onTap(PointF(x, y)) } + R2CbzPageFragment(publication) { x, y -> + inputListener.onTap( + TapEvent(PointF(x, y)) + ) + } } super.onCreate(savedInstanceState) } @@ -109,12 +99,15 @@ class ImageNavigatorFragment private constructor( savedInstanceState: Bundle? ): View { currentActivity = requireActivity() - _binding = ActivityR2ViewpagerBinding.inflate(inflater, container, false) + _binding = ReadiumNavigatorViewpagerBinding.inflate(inflater, container, false) val view = binding.root - preferences = requireContext().getSharedPreferences("org.readium.r2.settings", Context.MODE_PRIVATE) + preferences = requireContext().getSharedPreferences( + "org.readium.r2.settings", + Context.MODE_PRIVATE + ) resourcePager = binding.resourcePager - resourcePager.type = Publication.TYPE.CBZ + resourcePager.publicationType = R2ViewPager.PublicationType.CBZ positions = runBlocking { publication.positions() } @@ -146,7 +139,7 @@ class ImageNavigatorFragment private constructor( go(initialLocator) } - return view + return KeyInterceptorView(view, inputListener) } override fun onStart() { @@ -170,15 +163,23 @@ class ImageNavigatorFragment private constructor( _binding = null } - @Deprecated("Use goForward instead", replaceWith = ReplaceWith("goForward()"), level = DeprecationLevel.ERROR) + @Deprecated( + "Use goForward instead", + replaceWith = ReplaceWith("goForward()"), + level = DeprecationLevel.ERROR + ) @Suppress("UNUSED_PARAMETER") - fun nextResource(v: View?) { + public fun nextResource(v: View?) { goForward() } - @Deprecated("Use goBackward instead", replaceWith = ReplaceWith("goBackward()"), level = DeprecationLevel.ERROR) + @Deprecated( + "Use goBackward instead", + replaceWith = ReplaceWith("goBackward()"), + level = DeprecationLevel.ERROR + ) @Suppress("UNUSED_PARAMETER") - fun previousResource(v: View?) { + public fun previousResource(v: View?) { goBackward() } @@ -191,6 +192,9 @@ class ImageNavigatorFragment private constructor( } override fun go(locator: Locator, animated: Boolean, completion: () -> Unit): Boolean { + @Suppress("NAME_SHADOWING") + val locator = publication.normalizeLocator(locator) + val resourceIndex = publication.readingOrder.indexOfFirstWithHref(locator.href) ?: return false @@ -234,7 +238,44 @@ class ImageNavigatorFragment private constructor( return current != resourcePager.currentItem } - companion object { + // VisualNavigator + + override val publicationView: View + get() = requireView() + + @Suppress("DEPRECATION") + @Deprecated( + "Use `presentation.value.readingProgression` instead", + replaceWith = ReplaceWith("presentation.value.readingProgression"), + level = DeprecationLevel.ERROR + ) + override val readingProgression: PublicationReadingProgression = + publication.metadata.effectiveReadingProgression + + @ExperimentalReadiumApi + override val overflow: StateFlow<OverflowableNavigator.Overflow> = + MutableStateFlow( + SimpleOverflow( + readingProgression = when (publication.metadata.readingProgression) { + PublicationReadingProgression.RTL -> ReadingProgression.RTL + else -> ReadingProgression.LTR + }, + scroll = false, + axis = Axis.HORIZONTAL + ) + ).asStateFlow() + + private val inputListener = CompositeInputListener() + + override fun addInputListener(listener: InputListener) { + inputListener.add(listener) + } + + override fun removeInputListener(listener: InputListener) { + inputListener.remove(listener) + } + + public companion object { /** * Factory for [ImageNavigatorFragment]. @@ -244,7 +285,7 @@ class ImageNavigatorFragment private constructor( * publication. Can be used to restore the last reading location. * @param listener Optional listener to implement to observe events, such as user taps. */ - fun createFactory( + public fun createFactory( publication: Publication, initialLocator: Locator? = null, listener: Listener? = null @@ -257,10 +298,10 @@ class ImageNavigatorFragment private constructor( * Used when Android restore the [ImageNavigatorFragment] after the process was killed. You * need to make sure the fragment is removed from the screen before `onResume` is called. */ - fun createDummyFactory(): FragmentFactory = createFragmentFactory { + public fun createDummyFactory(): FragmentFactory = createFragmentFactory { ImageNavigatorFragment( publication = dummyPublication, - initialLocator = Locator(href = "#", type = "image/jpg"), + initialLocator = Locator(href = Url("#")!!, mediaType = MediaType.JPEG), listener = null ) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/input/DragEvent.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/input/DragEvent.kt new file mode 100644 index 0000000000..2b380db610 --- /dev/null +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/input/DragEvent.kt @@ -0,0 +1,16 @@ +package org.readium.r2.navigator.input + +import android.graphics.PointF + +/** + * Represents a drag event emitted by a navigator from a [start] point moved by an [offset]. + * + * All the points are relative to the navigator view. + */ +public data class DragEvent( + val type: Type, + val start: PointF, + val offset: PointF +) { + public enum class Type { Start, Move, End } +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/input/InputListener.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/input/InputListener.kt new file mode 100644 index 0000000000..12c0e0a1e7 --- /dev/null +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/input/InputListener.kt @@ -0,0 +1,44 @@ +package org.readium.r2.navigator.input + +import org.readium.r2.shared.ExperimentalReadiumApi + +@ExperimentalReadiumApi +public interface InputListener { + /** + * Called when the user tapped the content, but nothing handled the event internally (eg. + * by following an internal link). + */ + public fun onTap(event: TapEvent): Boolean = false + + /** + * Called when the user dragged the content, but nothing handled the event internally. + */ + public fun onDrag(event: DragEvent): Boolean = false + + /** + * Called when the user pressed or released a key, but nothing handled the event internally. + */ + public fun onKey(event: KeyEvent): Boolean = false +} + +@OptIn(ExperimentalReadiumApi::class) +internal class CompositeInputListener : InputListener { + private val listeners = mutableListOf<InputListener>() + + fun add(listener: InputListener) { + listeners.add(listener) + } + + fun remove(listener: InputListener) { + listeners.remove(listener) + } + + override fun onTap(event: TapEvent): Boolean = + listeners.any { it.onTap(event) } + + override fun onDrag(event: DragEvent): Boolean = + listeners.any { it.onDrag(event) } + + override fun onKey(event: KeyEvent): Boolean = + listeners.any { it.onKey(event) } +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/input/InputModifier.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/input/InputModifier.kt new file mode 100644 index 0000000000..dd13e6530a --- /dev/null +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/input/InputModifier.kt @@ -0,0 +1,8 @@ +package org.readium.r2.navigator.input + +/** + * Represents a key modifier for an input event. + */ +public enum class InputModifier { + Alt, Control, Meta, Shift +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/input/KeyEvent.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/input/KeyEvent.kt new file mode 100644 index 0000000000..0aa4021c72 --- /dev/null +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/input/KeyEvent.kt @@ -0,0 +1,366 @@ +package org.readium.r2.navigator.input + +import android.view.KeyEvent as AndroidKeyEvent + +/** + * Represents a keyboard event emitted by a navigator. + * + * @param type Nature of the event. + * @param key Key the user pressed or released. + * @param modifiers Additional input modifiers for keyboard shortcuts. + * @param characters Characters generated by the keypress, if any. + */ +public data class KeyEvent( + val type: Type, + val key: Key, + val modifiers: Set<InputModifier>, + val characters: String? +) { + + public enum class Type { + Down, Up + } + + public companion object { + public operator fun invoke(type: Type, event: AndroidKeyEvent): KeyEvent? { + return KeyEvent( + type = type, + key = Key(event) ?: return null, + modifiers = inputModifiers(event), + characters = event.unicodeChar.toChar().toString() + ) + } + } +} + +/** + * Represents a physical key on a keyboard. + * + * See https://w3c.github.io/uievents-code/ + */ +@JvmInline +public value class Key(public val code: String) { + + public companion object { + + // Alphanumeric section + + public val Backquote: Key = Key("Backquote") + public val Backslash: Key = Key("Backslash") + public val BracketLeft: Key = Key("BracketLeft") + public val BracketRight: Key = Key("BracketRight") + public val Comma: Key = Key("Comma") + public val Digit0: Key = Key("Digit0") + public val Digit1: Key = Key("Digit1") + public val Digit2: Key = Key("Digit2") + public val Digit3: Key = Key("Digit3") + public val Digit4: Key = Key("Digit4") + public val Digit5: Key = Key("Digit5") + public val Digit6: Key = Key("Digit6") + public val Digit7: Key = Key("Digit7") + public val Digit8: Key = Key("Digit8") + public val Digit9: Key = Key("Digit9") + public val Equal: Key = Key("Equal") + public val IntlBackslash: Key = Key("IntlBackslash") + public val IntlRo: Key = Key("IntlRo") + public val IntlYen: Key = Key("IntlYen") + public val KeyA: Key = Key("KeyA") + public val KeyB: Key = Key("KeyB") + public val KeyC: Key = Key("KeyC") + public val KeyD: Key = Key("KeyD") + public val KeyE: Key = Key("KeyE") + public val KeyF: Key = Key("KeyF") + public val KeyG: Key = Key("KeyG") + public val KeyH: Key = Key("KeyH") + public val KeyI: Key = Key("KeyI") + public val KeyJ: Key = Key("KeyJ") + public val KeyK: Key = Key("KeyK") + public val KeyL: Key = Key("KeyL") + public val KeyM: Key = Key("KeyM") + public val KeyN: Key = Key("KeyN") + public val KeyO: Key = Key("KeyO") + public val KeyP: Key = Key("KeyP") + public val KeyQ: Key = Key("KeyQ") + public val KeyR: Key = Key("KeyR") + public val KeyS: Key = Key("KeyS") + public val KeyT: Key = Key("KeyT") + public val KeyU: Key = Key("KeyU") + public val KeyV: Key = Key("KeyV") + public val KeyW: Key = Key("KeyW") + public val KeyX: Key = Key("KeyX") + public val KeyY: Key = Key("KeyY") + public val KeyZ: Key = Key("KeyZ") + public val Minus: Key = Key("Minus") + public val Period: Key = Key("Period") + public val Quote: Key = Key("Quote") + public val Semicolon: Key = Key("Semicolon") + public val Slash: Key = Key("Slash") + + // Function keys + + public val AltLeft: Key = Key("AltLeft") + public val AltRight: Key = Key("AltRight") + public val Backspace: Key = Key("Backspace") + public val CapsLock: Key = Key("CapsLock") + public val ContextMenu: Key = Key("ContextMenu") + public val ControlLeft: Key = Key("ControlLeft") + public val ControlRight: Key = Key("ControlRight") + public val Enter: Key = Key("Enter") + public val MetaLeft: Key = Key("MetaLeft") + public val MetaRight: Key = Key("MetaRight") + public val ShiftLeft: Key = Key("ShiftLeft") + public val ShiftRight: Key = Key("ShiftRight") + public val Space: Key = Key("Space") + public val Tab: Key = Key("Tab") + + // Function keys (Japanese and Korean keyboards) + + public val Convert: Key = Key("Convert") + public val KanaMode: Key = Key("KanaMode") + public val Lang1: Key = Key("Lang1") + public val Lang2: Key = Key("Lang2") + public val Lang3: Key = Key("Lang3") + public val Lang4: Key = Key("Lang4") + public val Lang5: Key = Key("Lang5") + public val NonConvert: Key = Key("NonConvert") + + // Control pad section + + public val Delete: Key = Key("Delete") + public val End: Key = Key("End") + public val Help: Key = Key("Help") + public val Home: Key = Key("Home") + public val Insert: Key = Key("Insert") + public val PageDown: Key = Key("PageDown") + public val PageUp: Key = Key("PageUp") + + // Arrow pad section + + public val ArrowDown: Key = Key("ArrowDown") + public val ArrowLeft: Key = Key("ArrowLeft") + public val ArrowRight: Key = Key("ArrowRight") + public val ArrowUp: Key = Key("ArrowUp") + + // Numpad section + + public val NumLock: Key = Key("NumLock") + public val Numpad0: Key = Key("Numpad0") + public val Numpad1: Key = Key("Numpad1") + public val Numpad2: Key = Key("Numpad2") + public val Numpad3: Key = Key("Numpad3") + public val Numpad4: Key = Key("Numpad4") + public val Numpad5: Key = Key("Numpad5") + public val Numpad6: Key = Key("Numpad6") + public val Numpad7: Key = Key("Numpad7") + public val Numpad8: Key = Key("Numpad8") + public val Numpad9: Key = Key("Numpad9") + public val NumpadAdd: Key = Key("NumpadAdd") + public val NumpadBackspace: Key = Key("NumpadBackspace") + public val NumpadClear: Key = Key("NumpadClear") + public val NumpadClearEntry: Key = Key("NumpadClearEntry") + public val NumpadComma: Key = Key("NumpadComma") + public val NumpadDecimal: Key = Key("NumpadDecimal") + public val NumpadDivide: Key = Key("NumpadDivide") + public val NumpadEnter: Key = Key("NumpadEnter") + public val NumpadEqual: Key = Key("NumpadEqual") + public val NumpadHash: Key = Key("NumpadHash") + public val NumpadMemoryAdd: Key = Key("NumpadMemoryAdd") + public val NumpadMemoryClear: Key = Key("NumpadMemoryClear") + public val NumpadMemoryRecall: Key = Key("NumpadMemoryRecall") + public val NumpadMemoryStore: Key = Key("NumpadMemoryStore") + public val NumpadMemorySubtract: Key = Key("NumpadMemorySubtract") + public val NumpadMultiply: Key = Key("NumpadMultiply") + public val NumpadParenLeft: Key = Key("NumpadParenLeft") + public val NumpadParenRight: Key = Key("NumpadParenRight") + public val NumpadStar: Key = Key("NumpadStar") + public val NumpadSubtract: Key = Key("NumpadSubtract") + + // Function section + + public val Escape: Key = Key("Escape") + public val F1: Key = Key("F1") + public val F2: Key = Key("F2") + public val F3: Key = Key("F3") + public val F4: Key = Key("F4") + public val F5: Key = Key("F5") + public val F6: Key = Key("F6") + public val F7: Key = Key("F7") + public val F8: Key = Key("F8") + public val F9: Key = Key("F9") + public val F10: Key = Key("F10") + public val F11: Key = Key("F11") + public val F12: Key = Key("F12") + public val Fn: Key = Key("Fn") + public val FnLock: Key = Key("FnLock") + public val PrintScreen: Key = Key("PrintScreen") + public val ScrollLock: Key = Key("ScrollLock") + public val Pause: Key = Key("Pause") + + // Media keys + + public val BrowserBack: Key = Key("BrowserBack") + public val BrowserFavorites: Key = Key("BrowserFavorites") + public val BrowserForward: Key = Key("BrowserForward") + public val BrowserHome: Key = Key("BrowserHome") + public val BrowserRefresh: Key = Key("BrowserRefresh") + public val BrowserSearch: Key = Key("BrowserSearch") + public val BrowserStop: Key = Key("BrowserStop") + public val Eject: Key = Key("Eject") + public val LaunchApp1: Key = Key("LaunchApp1") + public val LaunchApp2: Key = Key("LaunchApp2") + public val LaunchMail: Key = Key("LaunchMail") + public val MediaPlayPause: Key = Key("MediaPlayPause") + public val MediaSelect: Key = Key("MediaSelect") + public val MediaStop: Key = Key("MediaStop") + public val MediaTrackNext: Key = Key("MediaTrackNext") + public val MediaTrackPrevious: Key = Key("MediaTrackPrevious") + public val Power: Key = Key("Power") + public val Sleep: Key = Key("Sleep") + public val AudioVolumeDown: Key = Key("AudioVolumeDown") + public val AudioVolumeMute: Key = Key("AudioVolumeMute") + public val AudioVolumeUp: Key = Key("AudioVolumeUp") + public val WakeUp: Key = Key("WakeUp") + + public operator fun invoke(event: AndroidKeyEvent): Key? = + when (event.keyCode) { + AndroidKeyEvent.KEYCODE_DEL -> Backspace + AndroidKeyEvent.KEYCODE_ENTER -> Enter + AndroidKeyEvent.KEYCODE_FORWARD_DEL -> Delete + AndroidKeyEvent.KEYCODE_SPACE -> Space + AndroidKeyEvent.KEYCODE_TAB -> Tab + + AndroidKeyEvent.KEYCODE_0 -> Digit0 + AndroidKeyEvent.KEYCODE_1 -> Digit1 + AndroidKeyEvent.KEYCODE_2 -> Digit2 + AndroidKeyEvent.KEYCODE_3 -> Digit3 + AndroidKeyEvent.KEYCODE_4 -> Digit4 + AndroidKeyEvent.KEYCODE_5 -> Digit5 + AndroidKeyEvent.KEYCODE_6 -> Digit6 + AndroidKeyEvent.KEYCODE_7 -> Digit7 + AndroidKeyEvent.KEYCODE_8 -> Digit8 + AndroidKeyEvent.KEYCODE_9 -> Digit9 + + AndroidKeyEvent.KEYCODE_A -> KeyA + AndroidKeyEvent.KEYCODE_B -> KeyB + AndroidKeyEvent.KEYCODE_C -> KeyC + AndroidKeyEvent.KEYCODE_D -> KeyD + AndroidKeyEvent.KEYCODE_E -> KeyE + AndroidKeyEvent.KEYCODE_F -> KeyF + AndroidKeyEvent.KEYCODE_G -> KeyG + AndroidKeyEvent.KEYCODE_H -> KeyH + AndroidKeyEvent.KEYCODE_I -> KeyI + AndroidKeyEvent.KEYCODE_J -> KeyJ + AndroidKeyEvent.KEYCODE_K -> KeyK + AndroidKeyEvent.KEYCODE_L -> KeyL + AndroidKeyEvent.KEYCODE_M -> KeyM + AndroidKeyEvent.KEYCODE_N -> KeyN + AndroidKeyEvent.KEYCODE_O -> KeyO + AndroidKeyEvent.KEYCODE_P -> KeyP + AndroidKeyEvent.KEYCODE_Q -> KeyQ + AndroidKeyEvent.KEYCODE_R -> KeyR + AndroidKeyEvent.KEYCODE_S -> KeyS + AndroidKeyEvent.KEYCODE_T -> KeyT + AndroidKeyEvent.KEYCODE_U -> KeyU + AndroidKeyEvent.KEYCODE_V -> KeyV + AndroidKeyEvent.KEYCODE_W -> KeyW + AndroidKeyEvent.KEYCODE_X -> KeyX + AndroidKeyEvent.KEYCODE_Y -> KeyY + AndroidKeyEvent.KEYCODE_Z -> KeyZ + + AndroidKeyEvent.KEYCODE_APOSTROPHE -> Quote + AndroidKeyEvent.KEYCODE_BACKSLASH -> Backslash + AndroidKeyEvent.KEYCODE_GRAVE -> Backquote + AndroidKeyEvent.KEYCODE_COMMA -> Comma + AndroidKeyEvent.KEYCODE_EQUALS -> Equal + AndroidKeyEvent.KEYCODE_LEFT_BRACKET -> BracketLeft + AndroidKeyEvent.KEYCODE_MINUS -> Minus + AndroidKeyEvent.KEYCODE_PERIOD -> Period + AndroidKeyEvent.KEYCODE_RIGHT_BRACKET -> BracketRight + AndroidKeyEvent.KEYCODE_SEMICOLON -> Semicolon + AndroidKeyEvent.KEYCODE_SLASH -> Slash + + AndroidKeyEvent.KEYCODE_NUM_LOCK -> NumLock + AndroidKeyEvent.KEYCODE_NUMPAD_0 -> Numpad0 + AndroidKeyEvent.KEYCODE_NUMPAD_1 -> Numpad1 + AndroidKeyEvent.KEYCODE_NUMPAD_2 -> Numpad2 + AndroidKeyEvent.KEYCODE_NUMPAD_3 -> Numpad3 + AndroidKeyEvent.KEYCODE_NUMPAD_4 -> Numpad4 + AndroidKeyEvent.KEYCODE_NUMPAD_5 -> Numpad5 + AndroidKeyEvent.KEYCODE_NUMPAD_6 -> Numpad6 + AndroidKeyEvent.KEYCODE_NUMPAD_7 -> Numpad7 + AndroidKeyEvent.KEYCODE_NUMPAD_8 -> Numpad8 + AndroidKeyEvent.KEYCODE_NUMPAD_9 -> Numpad9 + AndroidKeyEvent.KEYCODE_NUMPAD_ADD -> NumpadAdd + AndroidKeyEvent.KEYCODE_NUMPAD_COMMA -> NumpadComma + AndroidKeyEvent.KEYCODE_NUMPAD_DIVIDE -> NumpadDivide + AndroidKeyEvent.KEYCODE_NUMPAD_DOT -> NumpadDecimal + AndroidKeyEvent.KEYCODE_NUMPAD_ENTER -> NumpadEnter + AndroidKeyEvent.KEYCODE_NUMPAD_EQUALS -> NumpadEqual + AndroidKeyEvent.KEYCODE_NUMPAD_LEFT_PAREN -> NumpadParenLeft + AndroidKeyEvent.KEYCODE_NUMPAD_MULTIPLY -> NumpadMultiply + AndroidKeyEvent.KEYCODE_NUMPAD_RIGHT_PAREN -> NumpadParenRight + AndroidKeyEvent.KEYCODE_NUMPAD_SUBTRACT -> NumpadSubtract + + AndroidKeyEvent.KEYCODE_CAPS_LOCK -> CapsLock + AndroidKeyEvent.KEYCODE_ESCAPE -> Escape + AndroidKeyEvent.KEYCODE_FUNCTION -> Fn + AndroidKeyEvent.KEYCODE_INSERT -> Insert + + AndroidKeyEvent.KEYCODE_DPAD_DOWN -> ArrowDown + AndroidKeyEvent.KEYCODE_DPAD_LEFT -> ArrowLeft + AndroidKeyEvent.KEYCODE_DPAD_RIGHT -> ArrowRight + AndroidKeyEvent.KEYCODE_DPAD_UP -> ArrowUp + AndroidKeyEvent.KEYCODE_HOME -> Home + AndroidKeyEvent.KEYCODE_PAGE_DOWN -> PageDown + AndroidKeyEvent.KEYCODE_PAGE_UP -> PageUp + + AndroidKeyEvent.KEYCODE_F1 -> F1 + AndroidKeyEvent.KEYCODE_F2 -> F2 + AndroidKeyEvent.KEYCODE_F3 -> F3 + AndroidKeyEvent.KEYCODE_F4 -> F4 + AndroidKeyEvent.KEYCODE_F5 -> F5 + AndroidKeyEvent.KEYCODE_F6 -> F6 + AndroidKeyEvent.KEYCODE_F7 -> F7 + AndroidKeyEvent.KEYCODE_F8 -> F8 + AndroidKeyEvent.KEYCODE_F9 -> F9 + AndroidKeyEvent.KEYCODE_F10 -> F10 + AndroidKeyEvent.KEYCODE_F11 -> F11 + AndroidKeyEvent.KEYCODE_F12 -> F12 + + AndroidKeyEvent.KEYCODE_MEDIA_NEXT -> MediaTrackNext + AndroidKeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> MediaPlayPause + AndroidKeyEvent.KEYCODE_MEDIA_PREVIOUS -> MediaTrackPrevious + AndroidKeyEvent.KEYCODE_MEDIA_STOP -> MediaStop + AndroidKeyEvent.KEYCODE_VOLUME_DOWN -> AudioVolumeDown + AndroidKeyEvent.KEYCODE_VOLUME_MUTE -> AudioVolumeMute + AndroidKeyEvent.KEYCODE_VOLUME_UP -> AudioVolumeUp + + AndroidKeyEvent.KEYCODE_ALT_LEFT -> AltLeft + AndroidKeyEvent.KEYCODE_ALT_RIGHT -> AltRight + AndroidKeyEvent.KEYCODE_CTRL_LEFT -> ControlLeft + AndroidKeyEvent.KEYCODE_CTRL_RIGHT -> ControlRight + AndroidKeyEvent.KEYCODE_META_LEFT -> MetaLeft + AndroidKeyEvent.KEYCODE_META_RIGHT -> MetaRight + AndroidKeyEvent.KEYCODE_SHIFT_LEFT -> ShiftLeft + AndroidKeyEvent.KEYCODE_SHIFT_RIGHT -> ShiftRight + + else -> null + } + } +} + +private fun inputModifiers(event: android.view.KeyEvent): Set<InputModifier> = + buildSet { + if (event.isAltPressed) { + add(InputModifier.Alt) + } + if (event.isCtrlPressed) { + add(InputModifier.Control) + } + if (event.isMetaPressed) { + add(InputModifier.Meta) + } + if (event.isShiftPressed) { + add(InputModifier.Shift) + } + } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/input/KeyInterceptorView.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/input/KeyInterceptorView.kt new file mode 100644 index 0000000000..f32688e282 --- /dev/null +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/input/KeyInterceptorView.kt @@ -0,0 +1,42 @@ +package org.readium.r2.navigator.input + +import android.annotation.SuppressLint +import android.os.Build +import android.view.View +import android.widget.FrameLayout +import org.readium.r2.navigator.VisualNavigator +import org.readium.r2.shared.ExperimentalReadiumApi + +/** + * Utility view to intercept keyboard events and forward them to a [VisualNavigator.Listener]. + */ +@SuppressLint("ViewConstructor") +@OptIn(ExperimentalReadiumApi::class) +internal class KeyInterceptorView( + view: View, + private val listener: InputListener? +) : FrameLayout(view.context) { + + init { + addView(view) + + isFocusable = true + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + defaultFocusHighlightEnabled = false + } + } + + override fun onKeyUp(keyCode: Int, event: android.view.KeyEvent?): Boolean { + event + ?.let { KeyEvent(KeyEvent.Type.Up, it) } + ?.let { listener?.onKey(it) } + return super.onKeyUp(keyCode, event) + } + + override fun onKeyDown(keyCode: Int, event: android.view.KeyEvent?): Boolean { + event + ?.let { KeyEvent(KeyEvent.Type.Down, it) } + ?.let { listener?.onKey(it) } + return super.onKeyDown(keyCode, event) + } +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/input/TapEvent.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/input/TapEvent.kt new file mode 100644 index 0000000000..8a3adf1928 --- /dev/null +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/input/TapEvent.kt @@ -0,0 +1,12 @@ +package org.readium.r2.navigator.input + +import android.graphics.PointF + +/** + * Represents a tap event emitted by a navigator at the given [point]. + * + * All the points are relative to the navigator view. + */ +public data class TapEvent( + val point: PointF +) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt index 2b0f617a5e..9ca55d6450 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt @@ -4,6 +4,9 @@ * available in the top-level LICENSE file of the project. */ +// Everything in this file will be deprecated +@file:Suppress("DEPRECATION") + package org.readium.r2.navigator.media import android.app.Notification @@ -18,35 +21,46 @@ import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.session.MediaControllerCompat import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat -import com.google.android.exoplayer2.* +import com.google.android.exoplayer2.C +import com.google.android.exoplayer2.ExoPlayer +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.PlaybackException +import com.google.android.exoplayer2.PlaybackParameters +import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator import com.google.android.exoplayer2.source.DefaultMediaSourceFactory import com.google.android.exoplayer2.ui.PlayerNotificationManager import com.google.android.exoplayer2.upstream.DataSource -import com.google.android.exoplayer2.upstream.HttpDataSource import com.google.android.exoplayer2.upstream.cache.Cache import com.google.android.exoplayer2.upstream.cache.CacheDataSource -import java.net.UnknownHostException import kotlin.time.Duration.Companion.seconds -import kotlin.time.ExperimentalTime -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import org.readium.r2.navigator.ExperimentalAudiobook import org.readium.r2.navigator.R -import org.readium.r2.navigator.audio.PublicationDataSource import org.readium.r2.navigator.extensions.timeWithDuration -import org.readium.r2.shared.extensions.asInstance -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.publication.* +import org.readium.r2.shared.extensions.findInstance +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.PublicationId +import org.readium.r2.shared.publication.indexOfFirstWithHref +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.toUri import timber.log.Timber /** * An implementation of [MediaPlayer] using ExoPlayer. */ @ExperimentalAudiobook -@OptIn(ExperimentalTime::class) -class ExoMediaPlayer( +public class ExoMediaPlayer( context: Context, mediaSession: MediaSessionCompat, media: PendingMedia, @@ -105,17 +119,21 @@ class ExoMediaPlayer( MEDIA_NOTIFICATION_ID, MEDIA_CHANNEL_ID ) - .setChannelNameResourceId(R.string.r2_media_notification_channel_name) - .setChannelDescriptionResourceId(R.string.r2_media_notification_channel_description) + .setChannelNameResourceId(R.string.readium_media_notification_channel_name) + .setChannelDescriptionResourceId( + R.string.readium_media_notification_channel_description + ) .setMediaDescriptionAdapter(DescriptionAdapter(mediaSession.controller, media)) .setNotificationListener(NotificationListener()) - .setRewindActionIconResourceId(R.drawable.r2_media_notification_rewind) - .setFastForwardActionIconResourceId(R.drawable.r2_media_notification_fastforward) + .setRewindActionIconResourceId(R.drawable.readium_media_notification_rewind) + .setFastForwardActionIconResourceId(R.drawable.readium_media_notification_fastforward) .build() .apply { setMediaSessionToken(mediaSession.sessionToken) setPlayer(player) - setSmallIcon(com.google.android.exoplayer2.ui.R.drawable.exo_notification_small_icon) + setSmallIcon( + com.google.android.exoplayer2.ui.R.drawable.exo_notification_small_icon + ) setUsePlayPauseActions(true) setUseStopAction(false) setUseChronometer(false) @@ -155,7 +173,7 @@ class ExoMediaPlayer( private fun prepareTracklist() { player.setMediaItems( publication.readingOrder.map { link -> - MediaItem.fromUri(link.href) + MediaItem.fromUri(link.url().toUri()) } ) player.prepare() @@ -179,16 +197,14 @@ class ExoMediaPlayer( } override fun onPlayerError(error: PlaybackException) { - var resourceException: Resource.Exception? = error.asInstance<Resource.Exception>() - if (resourceException == null && (error.cause as? HttpDataSource.HttpDataSourceException)?.cause is UnknownHostException) { - resourceException = Resource.Exception.Offline - } + val readError = error.findInstance<ReadException>()?.error - if (resourceException != null) { + if (readError != null) { player.currentMediaItem?.mediaId + ?.let { Url(it) } ?.let { href -> publication.linkWithHref(href) } ?.let { link -> - listener?.onResourceLoadFailed(link, resourceException) + listener?.onResourceLoadFailed(link, readError) } } else { Timber.e(error) @@ -223,7 +239,9 @@ class ExoMediaPlayer( override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {} } - private inner class QueueNavigator(mediaSession: MediaSessionCompat) : TimelineQueueNavigator(mediaSession) { + private inner class QueueNavigator(mediaSession: MediaSessionCompat) : TimelineQueueNavigator( + mediaSession + ) { override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat = createMediaMetadata(publication.readingOrder[windowIndex]).description @@ -257,10 +275,10 @@ class ExoMediaPlayer( controller.sessionActivity override fun getCurrentContentText(player: Player): CharSequence = - publication.metadata.title + publication.metadata.title ?: "" override fun getCurrentContentTitle(player: Player): CharSequence = - controller.metadata.description.title ?: publication.metadata.title + controller.metadata.description.title ?: publication.metadata.title ?: "" override fun getCurrentLargeIcon( player: Player, diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayback.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayback.kt index c02d7e08df..d242310e91 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayback.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayback.kt @@ -7,7 +7,6 @@ package org.readium.r2.navigator.media import kotlin.time.Duration -import kotlin.time.ExperimentalTime import org.readium.r2.navigator.ExperimentalAudiobook /** @@ -17,18 +16,17 @@ import org.readium.r2.navigator.ExperimentalAudiobook * @param rate Speed of the playback, defaults to 1.0. * @param timeline Position and duration of the current resource. */ -@OptIn(ExperimentalTime::class) @ExperimentalAudiobook -data class MediaPlayback(val state: State, val rate: Double, val timeline: Timeline) { +public data class MediaPlayback(val state: State, val rate: Double, val timeline: Timeline) { - enum class State { + public enum class State { Idle, Loading, Playing, Paused; - val isPlaying: Boolean get() = + public val isPlaying: Boolean get() = (this == Playing || this == Loading) } - data class Timeline( + public data class Timeline( val position: Duration, val duration: Duration?, val buffered: Duration? diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt index dba53f0d02..320fa79e6c 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt @@ -11,11 +11,11 @@ import android.graphics.Bitmap import android.os.Bundle import android.os.ResultReceiver import org.readium.r2.navigator.ExperimentalAudiobook -import org.readium.r2.shared.fetcher.Resource import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationId +import org.readium.r2.shared.util.data.ReadError /** * Media player compatible with Android's MediaSession and handling the playback for @@ -26,44 +26,47 @@ import org.readium.r2.shared.publication.PublicationId * `publicationId#resourceHref` with a [Locator] as a `locator` extra field. */ @ExperimentalAudiobook -interface MediaPlayer { +public interface MediaPlayer { - data class NotificationMetadata( + public data class NotificationMetadata( val publicationTitle: String?, val trackTitle: String?, val authors: String? ) { - constructor(publication: Publication, link: Link) : this( + public constructor(publication: Publication, link: Link) : this( publicationTitle = publication.metadata.title, trackTitle = link.title, authors = publication.metadata.authors.joinToString(", ") { it.name }.takeIf { it.isNotBlank() } ) } - interface Listener { + public interface Listener { - fun locatorFromMediaId(mediaId: String, extras: Bundle?): Locator? + public fun locatorFromMediaId(mediaId: String, extras: Bundle?): Locator? - suspend fun coverOfPublication(publication: Publication, publicationId: PublicationId): Bitmap? + public suspend fun coverOfPublication( + publication: Publication, + publicationId: PublicationId + ): Bitmap? - fun onNotificationPosted(notificationId: Int, notification: Notification) - fun onNotificationCancelled(notificationId: Int) - fun onCommand(command: String, args: Bundle?, cb: ResultReceiver?): Boolean + public fun onNotificationPosted(notificationId: Int, notification: Notification) + public fun onNotificationCancelled(notificationId: Int) + public fun onCommand(command: String, args: Bundle?, cb: ResultReceiver?): Boolean - fun onPlayerStopped() + public fun onPlayerStopped() /** * Called when a resource failed to be loaded, for example because the Internet connection * is offline and the resource is streamed. */ - fun onResourceLoadFailed(link: Link, error: Resource.Exception) + public fun onResourceLoadFailed(link: Link, error: ReadError) /** * Creates the [NotificationMetadata] for the given resource [link]. * * The metadata will be used for the media-style notification. */ - fun onCreateNotificationMetadata( + public fun onCreateNotificationMetadata( publication: Publication, publicationId: PublicationId, link: Link @@ -71,9 +74,9 @@ interface MediaPlayer { } // FIXME: ExoPlayer's media session connector doesn't handle the playback speed yet, so I used a custom solution until we create our own connector - var playbackRate: Double + public var playbackRate: Double - var listener: Listener? + public var listener: Listener? - fun onDestroy() + public fun onDestroy() } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt index 2b7ed5e74b..10647f8b42 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt @@ -17,7 +17,8 @@ import android.os.Process import android.os.ResultReceiver import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.session.MediaSessionCompat -import android.widget.Toast +import androidx.core.app.ServiceCompat +import androidx.core.os.BundleCompat import androidx.media.MediaBrowserServiceCompat import kotlin.reflect.KMutableProperty0 import kotlinx.coroutines.* @@ -27,12 +28,13 @@ import org.readium.r2.navigator.ExperimentalAudiobook import org.readium.r2.navigator.extensions.let import org.readium.r2.navigator.extensions.splitAt import org.readium.r2.navigator.media.extensions.publicationId -import org.readium.r2.shared.fetcher.Resource import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationId import org.readium.r2.shared.publication.services.cover +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError import timber.log.Timber /** @@ -45,26 +47,26 @@ import timber.log.Timber */ @ExperimentalAudiobook @OptIn(ExperimentalCoroutinesApi::class) -open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainScope() { +public open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainScope() { /** * Creates the instance of [MediaPlayer] which will be used for playing the given [media]. * * The default implementation uses ExoPlayer. */ - open fun onCreatePlayer(mediaSession: MediaSessionCompat, media: PendingMedia): MediaPlayer = + public open fun onCreatePlayer(mediaSession: MediaSessionCompat, media: PendingMedia): MediaPlayer = ExoMediaPlayer(this, mediaSession, media) /** * Called when the underlying [MediaPlayer] was stopped. */ - open fun onPlayerStopped() {} + public open fun onPlayerStopped() {} /** * Creates the [PendingIntent] which will be used to start the media activity when the user * activates the media notification. */ - open suspend fun onCreateNotificationIntent( + public open suspend fun onCreateNotificationIntent( publicationId: PublicationId, publication: Publication ): PendingIntent? = null @@ -74,7 +76,7 @@ open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainSco * * The metadata will be used for the media-style notification. */ - open fun onCreateNotificationMetadata( + public open fun onCreateNotificationMetadata( publicationId: PublicationId, publication: Publication, link: Link @@ -84,7 +86,10 @@ open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainSco /** * Returns the cover for the given [publication] which should be used in media notifications. */ - open suspend fun coverOfPublication(publicationId: PublicationId, publication: Publication): Bitmap? = + public open suspend fun coverOfPublication( + publicationId: PublicationId, + publication: Publication + ): Bitmap? = publication.cover() /** @@ -92,7 +97,7 @@ open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainSco * * @return Whether the custom command was handled. */ - open fun onCommand(command: String, args: Bundle?, cb: ResultReceiver?): Boolean = false + public open fun onCommand(command: String, args: Bundle?, cb: ResultReceiver?): Boolean = false /** * Called when a resource failed to be loaded, for example because the Internet connection @@ -100,9 +105,7 @@ open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainSco * * You should present the exception to the user. */ - open fun onResourceLoadFailed(link: Link, error: Resource.Exception) { - Toast.makeText(this, error.getUserMessage(this), Toast.LENGTH_LONG).show() - } + public open fun onResourceLoadFailed(link: Link, error: ReadError) {} /** * Override to control which app can access the MediaSession through the MediaBrowserService. @@ -111,7 +114,7 @@ open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainSco * @param packageName The package name of the application which is requesting access. * @param uid The UID of the application which is requesting access. */ - open fun isClientAuthorized(packageName: String, uid: Int): Boolean = + public open fun isClientAuthorized(packageName: String, uid: Int): Boolean = (uid == Process.myUid()) protected val mediaSession: MediaSessionCompat get() = getMediaSession(this, javaClass) @@ -142,19 +145,28 @@ open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainSco */ override fun locatorFromMediaId(mediaId: String, extras: Bundle?): Locator? { val navigator = currentNavigator.value ?: return null - val (publicationId, href) = mediaId.splitAt("#") + val (publicationId, rawHref) = mediaId.splitAt("#") + val href = rawHref?.let { Url(it) } if (navigator.publicationId != publicationId) { return null } - val locator = (extras?.getParcelable(EXTRA_LOCATOR) as? Locator) + val locator = extras?.let { + BundleCompat.getParcelable( + it, + EXTRA_LOCATOR, + Locator::class.java + ) + } ?: href ?.let { navigator.publication.linkWithHref(it) } ?.let { navigator.publication.locatorFromLink(it) } if (locator != null && href != null && locator.href != href) { - Timber.e("Ambiguous playback location provided. HREF `$href` doesn't match locator $locator.") + Timber.e( + "Ambiguous playback location provided. HREF `$href` doesn't match locator $locator." + ) } return locator @@ -175,7 +187,7 @@ open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainSco override fun onNotificationCancelled(notificationId: Int) { this@MediaService.notificationId = null this@MediaService.notification = null - stopForeground(true) + ServiceCompat.stopForeground(this@MediaService, ServiceCompat.STOP_FOREGROUND_REMOVE) if (currentNavigator.value?.isPlaying == false) { onPlayerStopped() @@ -199,7 +211,7 @@ open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainSco this@MediaService.onPlayerStopped() } - override fun onResourceLoadFailed(link: Link, error: Resource.Exception) { + override fun onResourceLoadFailed(link: Link, error: ReadError) { this@MediaService.onResourceLoadFailed(link, error) } } @@ -246,7 +258,10 @@ open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainSco startForeground(id, note) } } else { - stopForeground(false) + ServiceCompat.stopForeground( + this@MediaService, + ServiceCompat.STOP_FOREGROUND_DETACH + ) } } } @@ -278,20 +293,21 @@ open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainSco result.sendResult(mutableListOf()) } - companion object { + public companion object { internal const val EVENT_PUBLICATION_CHANGED = "org.readium.r2.navigator.EVENT_PUBLICATION_CHANGED" internal const val EXTRA_PUBLICATION_ID = "org.readium.r2.navigator.EXTRA_PUBLICATION_ID" @Volatile private var connection: Connection? = null + @Volatile private var mediaSession: MediaSessionCompat? = null private val currentNavigator = MutableStateFlow<MediaSessionNavigator?>(null) private val pendingNavigator = Channel<PendingNavigator>(Channel.CONFLATED) - val navigator = currentNavigator.asStateFlow() + public val navigator: StateFlow<MediaSessionNavigator?> = currentNavigator.asStateFlow() - fun connect(serviceClass: Class<*> = MediaService::class.java): Connection = + public fun connect(serviceClass: Class<*> = MediaService::class.java): Connection = createIfNull(this::connection, this) { Connection(serviceClass) } @@ -316,11 +332,11 @@ open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainSco * Use a [Connection] to get a [MediaSessionNavigator] from a [Publication]. * It will start the service if needed. */ - class Connection internal constructor(private val serviceClass: Class<*>) { + public class Connection internal constructor(private val serviceClass: Class<*>) { - val currentNavigator: StateFlow<MediaSessionNavigator?> get() = navigator + public val currentNavigator: StateFlow<MediaSessionNavigator?> get() = navigator - fun getNavigator( + public fun getNavigator( context: Context, publication: Publication, publicationId: PublicationId, @@ -332,7 +348,11 @@ open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainSco ?.takeIf { it.publicationId == publicationId } ?.let { return it } - val navigator = MediaSessionNavigator(publication, publicationId, getMediaSession(context, serviceClass).controller) + val navigator = MediaSessionNavigator( + publication, + publicationId, + getMediaSession(context, serviceClass).controller + ) pendingNavigator.trySend( PendingNavigator( navigator = navigator, @@ -340,7 +360,9 @@ open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by MainSco publication = publication, publicationId = publicationId, locator = initialLocator - ?: requireNotNull(publication.locatorFromLink(publication.readingOrder.first())) + ?: requireNotNull( + publication.locatorFromLink(publication.readingOrder.first()) + ) ) ) ) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaSessionNavigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaSessionNavigator.kt index 03d371d734..11cd4d0b5d 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaSessionNavigator.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaSessionNavigator.kt @@ -18,8 +18,8 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.ExperimentalTime import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import org.readium.r2.navigator.ExperimentalAudiobook import org.readium.r2.navigator.MediaNavigator +import org.readium.r2.navigator.extensions.normalizeLocator import org.readium.r2.navigator.extensions.sum import org.readium.r2.navigator.media.extensions.elapsedPosition import org.readium.r2.navigator.media.extensions.id @@ -27,7 +27,10 @@ import org.readium.r2.navigator.media.extensions.isPlaying import org.readium.r2.navigator.media.extensions.publicationId import org.readium.r2.navigator.media.extensions.resourceHref import org.readium.r2.navigator.media.extensions.toPlaybackState +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.publication.* +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType import timber.log.Timber /** @@ -35,24 +38,22 @@ import timber.log.Timber */ private const val playbackPositionRefreshRate: Double = 2.0 // Hz -@OptIn(ExperimentalTime::class) private val skipForwardInterval: Duration = 30.seconds -@OptIn(ExperimentalTime::class) private val skipBackwardInterval: Duration = 30.seconds /** * An implementation of [MediaNavigator] using an Android's MediaSession compatible media player. */ -@ExperimentalAudiobook -@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class) -class MediaSessionNavigator( - override val publication: Publication, - val publicationId: PublicationId, - val controller: MediaControllerCompat, - var listener: Listener? = null +@Deprecated("Use the new AudioNavigator from the readium-navigator-media-audio module.") +@OptIn(ExperimentalTime::class) +public class MediaSessionNavigator( + public val publication: Publication, + public val publicationId: PublicationId, + public val controller: MediaControllerCompat, + public var listener: Listener? = null ) : MediaNavigator, CoroutineScope by MainScope() { - interface Listener : MediaNavigator.Listener + public interface Listener : MediaNavigator.Listener /** * Indicates whether the media session is loaded with a resource from this [publication]. This @@ -144,7 +145,10 @@ class MediaSessionNavigator( override fun onSessionEvent(event: String?, extras: Bundle?) { super.onSessionEvent(event, extras) - if (event == MediaService.EVENT_PUBLICATION_CHANGED && extras?.getString(MediaService.EXTRA_PUBLICATION_ID) == publicationId && playWhenReady && needsPlaying) { + if (event == MediaService.EVENT_PUBLICATION_CHANGED && extras?.getString( + MediaService.EXTRA_PUBLICATION_ID + ) == publicationId && playWhenReady && needsPlaying + ) { play() } } @@ -152,7 +156,9 @@ class MediaSessionNavigator( // Navigator - private val _currentLocator = MutableStateFlow(Locator(href = "#", type = "")) + private val _currentLocator = MutableStateFlow( + Locator(href = Url("#")!!, mediaType = MediaType.BINARY) + ) override val currentLocator: StateFlow<Locator> get() = _currentLocator.asStateFlow() /** @@ -178,9 +184,13 @@ class MediaSessionNavigator( return locator } + @OptIn(DelicateReadiumApi::class) override fun go(locator: Locator, animated: Boolean, completion: () -> Unit): Boolean { if (!isActive) return false + @Suppress("NAME_SHADOWING") + val locator = publication.normalizeLocator(locator) + listener?.onJumpToLocator(locator) transportControls.playFromMediaId( @@ -198,7 +208,7 @@ class MediaSessionNavigator( return go(locator, animated, completion) } - override fun goForward(animated: Boolean, completion: () -> Unit): Boolean { + public fun goForward(animated: Boolean = true, completion: () -> Unit = {}): Boolean { if (!isActive) return false seekRelative(skipForwardInterval) @@ -206,7 +216,7 @@ class MediaSessionNavigator( return true } - override fun goBackward(animated: Boolean, completion: () -> Unit): Boolean { + public fun goBackward(animated: Boolean = true, completion: () -> Unit = {}): Boolean { if (!isActive) return false seekRelative(-skipBackwardInterval) @@ -226,7 +236,11 @@ class MediaSessionNavigator( // See https://github.com/Kotlin/kotlinx.coroutines/issues/2353 val position = positionMs.milliseconds - val index = metadata.resourceHref?.let { publication.readingOrder.indexOfFirstWithHref(it) } + val index = metadata.resourceHref?.let { + publication.readingOrder.indexOfFirstWithHref( + it + ) + } if (index == null) { Timber.e("Can't find resource index in publication for media ID `${metadata.id}`.") } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/PendingMedia.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/PendingMedia.kt index 52987ab2fd..da036017d2 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/PendingMedia.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/PendingMedia.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.publication.PublicationId * Holds information about a media-based [publication] waiting to be rendered by a [MediaPlayer]. */ @ExperimentalAudiobook -data class PendingMedia( +public data class PendingMedia( val publication: Publication, val publicationId: PublicationId, val locator: Locator diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/PublicationDataSource.kt similarity index 58% rename from readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt rename to readium/navigator/src/main/java/org/readium/r2/navigator/media/PublicationDataSource.kt index 5ac5dad6e0..582b38cca6 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/PublicationDataSource.kt @@ -4,7 +4,10 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.audio +// Everything in this file will be deprecated +@file:Suppress("DEPRECATION") + +package org.readium.r2.navigator.media import android.net.Uri import com.google.android.exoplayer2.C.LENGTH_UNSET @@ -13,22 +16,20 @@ import com.google.android.exoplayer2.upstream.BaseDataSource import com.google.android.exoplayer2.upstream.DataSource import com.google.android.exoplayer2.upstream.DataSpec import com.google.android.exoplayer2.upstream.TransferListener -import java.io.IOException import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.buffered import org.readium.r2.shared.publication.Publication - -internal sealed class PublicationDataSourceException(message: String, cause: Throwable?) : IOException(message, cause) { - class NotOpened(message: String) : PublicationDataSourceException(message, null) - class NotFound(message: String) : PublicationDataSourceException(message, null) - class ReadFailed(uri: Uri, offset: Int, readLength: Int, cause: Throwable) : PublicationDataSourceException("Failed to read $readLength bytes of URI $uri at offset $offset.", cause) -} +import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.buffered +import org.readium.r2.shared.util.toUrl /** * An ExoPlayer's [DataSource] which retrieves resources from a [Publication]. */ -internal class PublicationDataSource(private val publication: Publication) : BaseDataSource(/* isNetwork = */ true) { +internal class PublicationDataSource(private val publication: Publication) : BaseDataSource(/* isNetwork = */ + true +) { class Factory( private val publication: Publication, @@ -46,23 +47,25 @@ internal class PublicationDataSource(private val publication: Publication) : Bas private data class OpenedResource( val resource: Resource, val uri: Uri, - var position: Long, + var position: Long ) private var openedResource: OpenedResource? = null override fun open(dataSpec: DataSpec): Long { - val link = publication.linkWithHref(dataSpec.uri.toString()) - ?: throw PublicationDataSourceException.NotFound("Can't find a [Link] for URI: ${dataSpec.uri}. Make sure you only request resources declared in the manifest.") - - val resource = publication.get(link) + val resource = dataSpec.uri.toUrl() + ?.let { publication.linkWithHref(it) } + ?.let { publication.get(it) } // Significantly improves performances, in particular with deflated ZIP entries. - .buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) + ?.buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) + ?: throw IllegalStateException( + "Can't find a [Link] for URI: ${dataSpec.uri}. Make sure you only request resources declared in the manifest." + ) openedResource = OpenedResource( resource = resource, uri = dataSpec.uri, - position = dataSpec.position, + position = dataSpec.position ) val bytesToRead = @@ -95,34 +98,29 @@ internal class PublicationDataSource(private val publication: Publication) : Bas return 0 } - val openedResource = openedResource ?: throw PublicationDataSourceException.NotOpened("No opened resource to read from. Did you call open()?") + val openedResource = openedResource + ?: throw IllegalStateException("No opened resource to read from. Did you call open()?") - try { - val data = runBlocking { - openedResource.resource - .read(range = openedResource.position until (openedResource.position + length)) - .getOrThrow() - } + val data = runBlocking { + openedResource.resource + .read(range = openedResource.position until (openedResource.position + length)) + .mapFailure { ReadException(it) } + .getOrThrow() + } - if (data.isEmpty()) { - return RESULT_END_OF_INPUT - } + if (data.isEmpty()) { + return RESULT_END_OF_INPUT + } - data.copyInto( - destination = target, - destinationOffset = offset, - startIndex = 0, - endIndex = data.size - ) + data.copyInto( + destination = target, + destinationOffset = offset, + startIndex = 0, + endIndex = data.size + ) - openedResource.position += data.count() - return data.count() - } catch (e: Exception) { - if (e is InterruptedException) { - return 0 - } - throw PublicationDataSourceException.ReadFailed(uri = openedResource.uri, offset = offset, readLength = length, cause = e) - } + openedResource.position += data.count() + return data.count() } override fun getUri(): Uri? = openedResource?.uri diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/extensions/MediaMetadataCompat.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/extensions/MediaMetadataCompat.kt index 232bc60f6c..73d932fb89 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/extensions/MediaMetadataCompat.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/extensions/MediaMetadataCompat.kt @@ -8,6 +8,7 @@ package org.readium.r2.navigator.media.extensions import android.support.v4.media.MediaMetadataCompat import org.readium.r2.navigator.extensions.splitAt +import org.readium.r2.shared.util.Url internal val MediaMetadataCompat.id: String? get() = getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID) @@ -15,5 +16,5 @@ internal val MediaMetadataCompat.id: String? get() = internal val MediaMetadataCompat.publicationId: String? get() = id?.splitAt("#")?.first -internal val MediaMetadataCompat.resourceHref: String? get() = - id?.splitAt("#")?.second +internal val MediaMetadataCompat.resourceHref: Url? get() = + id?.splitAt("#")?.second?.let { Url(it) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/MediaNavigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/MediaNavigator.kt deleted file mode 100644 index 0947ede120..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/MediaNavigator.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.api - -import androidx.media3.common.Player -import kotlinx.coroutines.flow.StateFlow -import org.readium.r2.navigator.Navigator -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.util.Closeable - -@ExperimentalReadiumApi -interface MediaNavigator<P : MediaNavigator.Position> : Navigator, Closeable { - - /** - * Marker interface for the [position] flow. - */ - interface Position - - /** - * State of the player. - */ - sealed interface State { - - /** - * The navigator is ready to play. - */ - interface Ready : State - - /** - * The end of the media has been reached. - */ - interface Ended : State - - /** - * The navigator cannot play because the buffer is starved. - */ - interface Buffering : State - - /** - * The navigator cannot play because an error occurred. - */ - interface Error : State - } - - /** - * State of the playback. - * - * @param state The current state. - * @param playWhenReady If the navigator should play as soon as the state is Ready. - */ - data class Playback( - val state: State, - val playWhenReady: Boolean - ) - - /** - * Indicates the current state of the playback. - */ - val playback: StateFlow<Playback> - - val position: StateFlow<P> - - /** - * Resumes the playback at the current location. - */ - fun play() - - /** - * Pauses the playback. - */ - fun pause() - - /** - * Adapts this navigator to the media3 [Player] interface. - */ - fun asPlayer(): Player -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/SynchronizedMediaNavigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/SynchronizedMediaNavigator.kt deleted file mode 100644 index 6b5bbcb8c8..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/SynchronizedMediaNavigator.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.api - -import kotlinx.coroutines.flow.StateFlow -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.publication.Locator - -/** - * A [MediaNavigator] aware of the utterances that are being read aloud. - */ -@ExperimentalReadiumApi -interface SynchronizedMediaNavigator<P : MediaNavigator.Position> : - MediaNavigator<P> { - - interface Utterance<P : MediaNavigator.Position> { - val text: String - - val position: P - - val range: IntRange? - - val utteranceLocator: Locator - - val tokenLocator: Locator? - } - - val utterance: StateFlow<Utterance<P>> -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/audio/AudioEngine.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/audio/AudioEngine.kt deleted file mode 100644 index 9c6465b1bc..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/audio/AudioEngine.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.audio - -import androidx.media3.common.Player -import kotlin.time.Duration -import kotlinx.coroutines.flow.StateFlow -import org.readium.r2.navigator.media3.api.MediaNavigator -import org.readium.r2.navigator.preferences.Configurable -import org.readium.r2.shared.ExperimentalReadiumApi - -@ExperimentalReadiumApi -interface AudioEngine<S : Configurable.Settings, P : Configurable.Preferences<P>, E : AudioEngine.Error> : - Configurable<S, P> { - - interface Error - - data class Playback<E : Error>( - val state: MediaNavigator.State, - val playWhenReady: Boolean, - val error: E? - ) - - data class Position( - val index: Int, - val duration: Duration - ) - - val playback: StateFlow<Playback<E>> - - val position: StateFlow<Position> - - fun play() - - fun pause() - - fun seek(index: Long, position: Duration) - - fun close() - - fun asPlayer(): Player -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/audio/AudioEngineProvider.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/audio/AudioEngineProvider.kt deleted file mode 100644 index 4413448bea..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/audio/AudioEngineProvider.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.audio - -import org.readium.r2.navigator.preferences.Configurable -import org.readium.r2.navigator.preferences.PreferencesEditor -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.publication.Metadata -import org.readium.r2.shared.publication.Publication - -@ExperimentalReadiumApi -interface AudioEngineProvider<S : Configurable.Settings, P : Configurable.Preferences<P>, - E : PreferencesEditor<P>, F : AudioEngine.Error> { - - suspend fun createEngine(publication: Publication): AudioEngine<S, P, F> - - /** - * Creates settings for [metadata] and [preferences]. - */ - fun computeSettings(metadata: Metadata, preferences: P): S - - /** - * Creates a preferences editor for [publication] and [initialPreferences]. - */ - fun createPreferenceEditor(publication: Publication, initialPreferences: P): E - - /** - * Creates an empty set of preferences of this TTS engine provider. - */ - fun createEmptyPreferences(): P -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/audio/AudioNavigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/audio/AudioNavigator.kt deleted file mode 100644 index 69fda4d58d..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/audio/AudioNavigator.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.audio - -import androidx.media3.common.Player -import kotlinx.coroutines.flow.StateFlow -import org.readium.r2.navigator.media3.api.MediaNavigator -import org.readium.r2.navigator.preferences.Configurable -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.Publication - -@ExperimentalReadiumApi -class AudioNavigator<S : Configurable.Settings, P : Configurable.Preferences<P>, E : AudioEngine.Error>( - private val mediaEngine: AudioEngine<S, P, E> -) : MediaNavigator<AudioNavigator.Position>, Configurable<S, P> by mediaEngine { - - class Position : MediaNavigator.Position - - class Error : MediaNavigator.State.Error - - override val publication: Publication - get() = TODO("Not yet implemented") - - override val currentLocator: StateFlow<Locator> - get() = TODO("Not yet implemented") - - override fun go(locator: Locator, animated: Boolean, completion: () -> Unit): Boolean { - TODO("Not yet implemented") - } - - override fun go(link: Link, animated: Boolean, completion: () -> Unit): Boolean { - TODO("Not yet implemented") - } - - override fun goForward(animated: Boolean, completion: () -> Unit): Boolean { - TODO("Not yet implemented") - } - - override fun goBackward(animated: Boolean, completion: () -> Unit): Boolean { - TODO("Not yet implemented") - } - - override fun close() { - TODO("Not yet implemented") - } - - override val playback: StateFlow<MediaNavigator.Playback> - get() = TODO("Not yet implemented") - - override val position: StateFlow<Position> - get() = TODO("Not yet implemented") - - override fun play() { - TODO("Not yet implemented") - } - - override fun pause() { - TODO("Not yet implemented") - } - - override fun asPlayer(): Player { - TODO("Not yet implemented") - } -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/audio/DurationSerializer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/audio/DurationSerializer.kt deleted file mode 100644 index ea9836096b..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/audio/DurationSerializer.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.audio - -import kotlin.time.Duration -import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder - -object DurationSerializer : KSerializer<Duration> { - - private val serializer = Duration.serializer() - - override val descriptor: SerialDescriptor = serializer.descriptor - - override fun deserialize(decoder: Decoder): Duration = - decoder.decodeSerializableValue(serializer) - - override fun serialize(encoder: Encoder, value: Duration) { - encoder.encodeSerializableValue(serializer, value) - } -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerEngine.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerEngine.kt deleted file mode 100644 index 3022884ce5..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerEngine.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.exoplayer - -import android.app.Application -import androidx.media3.common.AudioAttributes -import androidx.media3.common.C -import androidx.media3.common.Player -import androidx.media3.datasource.DataSource -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.source.DefaultMediaSourceFactory -import kotlin.time.Duration -import kotlinx.coroutines.flow.StateFlow -import org.readium.r2.navigator.media3.audio.AudioEngine -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.publication.Publication - -@ExperimentalReadiumApi -@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) -class ExoPlayerEngine( - private val application: Application, - private val publication: Publication, - private val exoPlayer: ExoPlayer, -) : AudioEngine<ExoPlayerSettings, ExoPlayerPreferences, ExoPlayerEngine.Error> { - - companion object { - - private fun createExoPlayer( - application: Application, - publication: Publication, - ): ExoPlayer { - val dataSourceFactory: DataSource.Factory = ExoPlayerDataSource.Factory(publication) - return ExoPlayer.Builder(application) - .setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory)) - .setAudioAttributes( - AudioAttributes.Builder() - .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) - .setUsage(C.USAGE_MEDIA) - .build(), - true - ) - .setHandleAudioBecomingNoisy(true) - .build() - } - } - - class Error : AudioEngine.Error - - override val playback: StateFlow<AudioEngine.Playback<Error>> - get() = TODO("Not yet implemented") - - override val position: StateFlow<AudioEngine.Position> - get() = TODO("Not yet implemented") - - override val settings: StateFlow<ExoPlayerSettings> - get() = TODO("Not yet implemented") - - override fun play() { - TODO("Not yet implemented") - } - - override fun pause() { - TODO("Not yet implemented") - } - - override fun seek(index: Long, position: Duration) { - TODO("Not yet implemented") - } - - override fun close() { - TODO("Not yet implemented") - } - - override fun asPlayer(): Player { - return exoPlayer - } - - override fun submitPreferences(preferences: ExoPlayerPreferences) { - TODO("Not yet implemented") - } -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerEngineProvider.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerEngineProvider.kt deleted file mode 100644 index bbfa3d9211..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerEngineProvider.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.exoplayer - -import org.readium.r2.navigator.media3.audio.AudioEngineProvider -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.publication.Metadata -import org.readium.r2.shared.publication.Publication - -@ExperimentalReadiumApi -@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) -class ExoPlayerEngineProvider() : AudioEngineProvider<ExoPlayerSettings, ExoPlayerPreferences, ExoPlayerPreferencesEditor, ExoPlayerEngine.Error> { - - override suspend fun createEngine(publication: Publication): ExoPlayerEngine { - TODO("Not yet implemented") - } - - override fun computeSettings( - metadata: Metadata, - preferences: ExoPlayerPreferences - ): ExoPlayerSettings = - ExoPlayerSettingsResolver(metadata).settings(preferences) - - override fun createPreferenceEditor( - publication: Publication, - initialPreferences: ExoPlayerPreferences - ): ExoPlayerPreferencesEditor = - ExoPlayerPreferencesEditor() - - override fun createEmptyPreferences(): ExoPlayerPreferences = - ExoPlayerPreferences() -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerPreferencesEditor.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerPreferencesEditor.kt deleted file mode 100644 index 6cc4efe150..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerPreferencesEditor.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.exoplayer - -import org.readium.r2.navigator.preferences.PreferencesEditor -import org.readium.r2.shared.ExperimentalReadiumApi - -@ExperimentalReadiumApi -class ExoPlayerPreferencesEditor : PreferencesEditor<ExoPlayerPreferences> { - - override val preferences: ExoPlayerPreferences - get() = TODO("Not yet implemented") - - override fun clear() { - TODO("Not yet implemented") - } -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerPreferencesFilters.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerPreferencesFilters.kt deleted file mode 100644 index db3f3b7aad..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/exoplayer/ExoPlayerPreferencesFilters.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.exoplayer - -import org.readium.r2.navigator.preferences.PreferencesFilter -import org.readium.r2.shared.ExperimentalReadiumApi - -/** - * Suggested filter to keep only shared [ExoPlayerPreferences]. - */ -@ExperimentalReadiumApi -object ExoPlayerSharedPreferencesFilter : PreferencesFilter<ExoPlayerPreferences> { - - override fun filter(preferences: ExoPlayerPreferences): ExoPlayerPreferences = - preferences.copy() -} - -/** - * Suggested filter to keep only publication-specific [ExoPlayerPreferences]. - */ -@ExperimentalReadiumApi -object ExoPlayerPublicationPreferencesFilter : PreferencesFilter<ExoPlayerPreferences> { - - override fun filter(preferences: ExoPlayerPreferences): ExoPlayerPreferences = - ExoPlayerPreferences() -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsAliases.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsAliases.kt deleted file mode 100644 index 717a2e74c0..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsAliases.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.tts - -import org.readium.r2.navigator.media3.tts.android.AndroidTtsEngine -import org.readium.r2.navigator.media3.tts.android.AndroidTtsPreferences -import org.readium.r2.navigator.media3.tts.android.AndroidTtsPreferencesEditor -import org.readium.r2.navigator.media3.tts.android.AndroidTtsSettings -import org.readium.r2.shared.ExperimentalReadiumApi - -@OptIn(ExperimentalReadiumApi::class) -typealias AndroidTtsNavigatorFactory = TtsNavigatorFactory<AndroidTtsSettings, AndroidTtsPreferences, AndroidTtsPreferencesEditor, AndroidTtsEngine.Error, AndroidTtsEngine.Voice> - -@OptIn(ExperimentalReadiumApi::class) -typealias AndroidTtsNavigator = TtsNavigator<AndroidTtsSettings, AndroidTtsPreferences, AndroidTtsEngine.Error, AndroidTtsEngine.Voice> diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsNavigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsNavigator.kt deleted file mode 100644 index 8240d0573e..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsNavigator.kt +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.tts - -import android.app.Application -import androidx.media3.common.MediaItem -import androidx.media3.common.PlaybackParameters -import androidx.media3.common.Player -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.flow.StateFlow -import org.readium.r2.navigator.media3.api.MediaMetadataProvider -import org.readium.r2.navigator.media3.api.MediaNavigator -import org.readium.r2.navigator.media3.api.SynchronizedMediaNavigator -import org.readium.r2.navigator.media3.tts.session.TtsSessionAdapter -import org.readium.r2.navigator.preferences.Configurable -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.extensions.mapStateIn -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.services.content.ContentService -import org.readium.r2.shared.util.Language -import org.readium.r2.shared.util.tokenizer.TextTokenizer - -/** - * A navigator to read aloud a [Publication] with a TTS engine. - */ -@ExperimentalReadiumApi -class TtsNavigator<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, - E : TtsEngine.Error, V : TtsEngine.Voice> private constructor( - coroutineScope: CoroutineScope, - override val publication: Publication, - private val player: TtsPlayer<S, P, E, V>, - private val sessionAdapter: TtsSessionAdapter<E>, -) : SynchronizedMediaNavigator<TtsNavigator.Position>, Configurable<S, P> by player { - - companion object { - - suspend operator fun <S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, - E : TtsEngine.Error, V : TtsEngine.Voice> invoke( - application: Application, - publication: Publication, - ttsEngineProvider: TtsEngineProvider<S, P, *, E, V>, - tokenizerFactory: (language: Language?) -> TextTokenizer, - metadataProvider: MediaMetadataProvider, - listener: Listener, - initialPreferences: P? = null, - initialLocator: Locator? = null, - ): TtsNavigator<S, P, E, V>? { - - if (publication.findService(ContentService::class) == null) { - return null - } - - val actualInitialPreferences = - initialPreferences - ?: ttsEngineProvider.createEmptyPreferences() - - val contentIterator = - TtsContentIterator(publication, tokenizerFactory, initialLocator) - - val ttsEngine = - ttsEngineProvider.createEngine(publication, actualInitialPreferences) - ?: return null - - val metadataFactory = - metadataProvider.createMetadataFactory(publication) - - val playlistMetadata = - metadataFactory.publicationMetadata() - - val mediaItems = - publication.readingOrder.indices.map { index -> - val metadata = metadataFactory.resourceMetadata(index) - MediaItem.Builder() - .setMediaMetadata(metadata) - .build() - } - - val ttsPlayer = - TtsPlayer(ttsEngine, contentIterator, actualInitialPreferences) - ?: return null - - val coroutineScope = - MainScope() - - val playbackParameters = - ttsPlayer.settings.mapStateIn(coroutineScope) { - ttsEngineProvider.getPlaybackParameters(it) - } - - val onSetPlaybackParameters = { parameters: PlaybackParameters -> - val newPreferences = ttsEngineProvider.updatePlaybackParameters( - ttsPlayer.lastPreferences, - parameters - ) - ttsPlayer.submitPreferences(newPreferences) - } - - val sessionAdapter = - TtsSessionAdapter( - application, - ttsPlayer, - playlistMetadata, - mediaItems, - listener::onStopRequested, - playbackParameters, - onSetPlaybackParameters, - ttsEngineProvider::mapEngineError - ) - - return TtsNavigator(coroutineScope, publication, ttsPlayer, sessionAdapter) - } - } - - interface Listener { - - fun onStopRequested() - } - - data class Position( - val resourceIndex: Int, - val cssSelector: String, - val textBefore: String?, - val textAfter: String?, - ) : MediaNavigator.Position - - data class Utterance( - override val text: String, - override val position: Position, - override val range: IntRange?, - override val utteranceLocator: Locator, - override val tokenLocator: Locator? - ) : SynchronizedMediaNavigator.Utterance<Position> - - sealed class State { - - object Ready : MediaNavigator.State.Ready - - object Ended : MediaNavigator.State.Ended - - sealed class Error : MediaNavigator.State.Error { - - data class EngineError<E : TtsEngine.Error> (val error: E) : Error() - - data class ContentError(val exception: Exception) : Error() - } - } - - val voices: Set<V> get() = - player.voices - - override val playback: StateFlow<MediaNavigator.Playback> = - player.playback.mapStateIn(coroutineScope) { it.toPlayback() } - - override val utterance: StateFlow<Utterance> = - player.utterance.mapStateIn(coroutineScope) { it.toUtterance() } - - override val position: StateFlow<Position> = - utterance.mapStateIn(coroutineScope) { utterance -> - utterance.position.copy(textAfter = utterance.text + utterance.position.textAfter) - } - - override fun play() { - player.play() - } - - override fun pause() { - player.pause() - } - - fun go(locator: Locator) { - player.go(locator) - } - - fun previousUtterance() { - player.previousUtterance() - } - - fun nextUtterance() { - player.nextUtterance() - } - - override fun asPlayer(): Player = - sessionAdapter - - override fun close() { - player.close() - } - - override val currentLocator: StateFlow<Locator> = - utterance.mapStateIn(coroutineScope) { it.tokenLocator ?: it.utteranceLocator } - - override fun go(locator: Locator, animated: Boolean, completion: () -> Unit): Boolean { - player.go(locator) - return true - } - - override fun go(link: Link, animated: Boolean, completion: () -> Unit): Boolean { - val locator = publication.locatorFromLink(link) ?: return false - return go(locator, animated, completion) - } - - override fun goForward(animated: Boolean, completion: () -> Unit): Boolean { - player.nextUtterance() - return true - } - - override fun goBackward(animated: Boolean, completion: () -> Unit): Boolean { - player.previousUtterance() - return true - } - - private fun TtsPlayer.Playback.toPlayback() = - MediaNavigator.Playback( - state = state.toState(), - playWhenReady = playWhenReady, - ) - - private fun TtsPlayer.State.toState() = - when (this) { - TtsPlayer.State.Ready -> State.Ready - TtsPlayer.State.Ended -> State.Ended - is TtsPlayer.State.Error -> this.toError() - } - - private fun TtsPlayer.State.Error.toError(): State.Error = - when (this) { - is TtsPlayer.State.Error.ContentError -> State.Error.ContentError(exception) - is TtsPlayer.State.Error.EngineError<*> -> State.Error.EngineError(error) - } - - private fun TtsPlayer.Utterance.Position.toPosition(): Position = - Position( - resourceIndex = resourceIndex, - cssSelector = cssSelector, - textBefore = textBefore, - textAfter = textAfter - ) - - private fun TtsPlayer.Utterance.toUtterance(): Utterance { - val utteranceHighlight = publication - .locatorFromLink(publication.readingOrder[position.resourceIndex])!! - .copyWithLocations( - progression = null, - otherLocations = buildMap { - put("cssSelector", position.cssSelector) - } - ).copy( - text = - Locator.Text( - highlight = text, - before = position.textBefore, - after = position.textAfter - ) - ) - - val tokenHighlight = range - ?.let { utteranceHighlight.copy(text = utteranceHighlight.text.substring(it)) } - - return Utterance( - text = text, - position = position.toPosition(), - range = range, - utteranceLocator = utteranceHighlight, - tokenLocator = tokenHighlight, - ) - } -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsNavigatorFactory.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsNavigatorFactory.kt deleted file mode 100644 index 48201ee251..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsNavigatorFactory.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.tts - -import android.app.Application -import org.readium.r2.navigator.media3.api.DefaultMediaMetadataProvider -import org.readium.r2.navigator.media3.api.MediaMetadataProvider -import org.readium.r2.navigator.media3.tts.android.AndroidTtsDefaults -import org.readium.r2.navigator.media3.tts.android.AndroidTtsEngine -import org.readium.r2.navigator.media3.tts.android.AndroidTtsEngineProvider -import org.readium.r2.navigator.preferences.PreferencesEditor -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.services.content.Content -import org.readium.r2.shared.publication.services.content.content -import org.readium.r2.shared.util.Language -import org.readium.r2.shared.util.tokenizer.DefaultTextContentTokenizer -import org.readium.r2.shared.util.tokenizer.TextTokenizer -import org.readium.r2.shared.util.tokenizer.TextUnit - -@ExperimentalReadiumApi -class TtsNavigatorFactory<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, E : PreferencesEditor<P>, - F : TtsEngine.Error, V : TtsEngine.Voice> private constructor( - private val application: Application, - private val publication: Publication, - private val ttsEngineProvider: TtsEngineProvider<S, P, E, F, V>, - private val tokenizerFactory: (language: Language?) -> TextTokenizer, - private val metadataProvider: MediaMetadataProvider -) { - companion object { - - suspend operator fun invoke( - application: Application, - publication: Publication, - tokenizerFactory: (language: Language?) -> TextTokenizer = defaultTokenizerFactory, - metadataProvider: MediaMetadataProvider = defaultMediaMetadataProvider, - defaults: AndroidTtsDefaults = AndroidTtsDefaults(), - voiceSelector: (Language?, Set<AndroidTtsEngine.Voice>) -> AndroidTtsEngine.Voice? = defaultVoiceSelector, - listener: AndroidTtsEngine.Listener? = null - ): AndroidTtsNavigatorFactory? { - - val engineProvider = AndroidTtsEngineProvider( - context = application, - defaults = defaults, - voiceSelector = voiceSelector, - listener = listener - ) - - return createNavigatorFactory( - application, - publication, - engineProvider, - tokenizerFactory, - metadataProvider - ) - } - - suspend operator fun <S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, E : PreferencesEditor<P>, - F : TtsEngine.Error, V : TtsEngine.Voice> invoke( - application: Application, - publication: Publication, - ttsEngineProvider: TtsEngineProvider<S, P, E, F, V>, - tokenizerFactory: (language: Language?) -> TextTokenizer = defaultTokenizerFactory, - metadataProvider: MediaMetadataProvider = defaultMediaMetadataProvider - ): TtsNavigatorFactory<S, P, E, F, V>? { - - return createNavigatorFactory( - application, - publication, - ttsEngineProvider, - tokenizerFactory, - metadataProvider - ) - } - - private suspend fun <S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, E : PreferencesEditor<P>, - F : TtsEngine.Error, V : TtsEngine.Voice> createNavigatorFactory( - application: Application, - publication: Publication, - ttsEngineProvider: TtsEngineProvider<S, P, E, F, V>, - tokenizerFactory: (language: Language?) -> TextTokenizer, - metadataProvider: MediaMetadataProvider - ): TtsNavigatorFactory<S, P, E, F, V>? { - - publication.content() - ?.iterator() - ?.takeIf { it.hasNext() } - ?: return null - - return TtsNavigatorFactory( - application, - publication, - ttsEngineProvider, - tokenizerFactory, - metadataProvider - ) - } - - /** - * The default content tokenizer will split the [Content.Element] items into individual sentences. - */ - private val defaultTokenizerFactory: (Language?) -> TextTokenizer = { language -> - DefaultTextContentTokenizer(TextUnit.Sentence, language) - } - - private val defaultMediaMetadataProvider: MediaMetadataProvider = - DefaultMediaMetadataProvider() - - private val defaultVoiceSelector: (Language?, Set<AndroidTtsEngine.Voice>) -> AndroidTtsEngine.Voice? = - { _, _ -> null } - } - - suspend fun createNavigator( - listener: TtsNavigator.Listener, - initialPreferences: P? = null, - initialLocator: Locator? = null - ): TtsNavigator<S, P, F, V>? { - return TtsNavigator( - application, - publication, - ttsEngineProvider, - tokenizerFactory, - metadataProvider, - listener, - initialPreferences, - initialLocator - ) - } - - fun createTtsPreferencesEditor( - currentPreferences: P, - ): E = ttsEngineProvider.createPreferencesEditor(publication, currentPreferences) -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsEngine.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsEngine.kt deleted file mode 100644 index 289f46c9ae..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsEngine.kt +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.media3.tts.android - -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Build -import android.speech.tts.TextToSpeech -import android.speech.tts.TextToSpeech.* -import android.speech.tts.UtteranceProgressListener -import android.speech.tts.Voice as AndroidVoice -import android.speech.tts.Voice.* -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import org.readium.r2.navigator.media3.tts.TtsEngine -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.util.Language - -/** - * Default [TtsEngine] implementation using Android's native text to speech engine. - */ -@ExperimentalReadiumApi -class AndroidTtsEngine private constructor( - private val engine: TextToSpeech, - private val settingsResolver: SettingsResolver, - private val voiceSelector: VoiceSelector, - private val listener: Listener?, - initialPreferences: AndroidTtsPreferences -) : TtsEngine<AndroidTtsSettings, AndroidTtsPreferences, - AndroidTtsEngine.Error, AndroidTtsEngine.Voice> { - - companion object { - - suspend operator fun invoke( - context: Context, - settingsResolver: SettingsResolver, - voiceSelector: VoiceSelector, - listener: Listener?, - initialPreferences: AndroidTtsPreferences - ): AndroidTtsEngine? { - - val init = CompletableDeferred<Boolean>() - - val initListener = OnInitListener { status -> - init.complete(status == SUCCESS) - } - val engine = TextToSpeech(context, initListener) - - return if (init.await()) - AndroidTtsEngine(engine, settingsResolver, voiceSelector, listener, initialPreferences) - else - null - } - - /** - * Starts the activity to install additional voice data. - */ - fun requestInstallVoice(context: Context) { - val intent = Intent() - .setAction(Engine.ACTION_INSTALL_TTS_DATA) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - val availableActivities = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.packageManager.queryIntentActivities( - intent, - PackageManager.ResolveInfoFlags.of(0) - ) - } else { - @Suppress("Deprecation") - context.packageManager.queryIntentActivities(intent, 0) - } - - if (availableActivities.isNotEmpty()) { - context.startActivity(intent) - } - } - } - - fun interface SettingsResolver { - - /** - * Computes a set of engine settings from the engine preferences. - */ - fun settings(preferences: AndroidTtsPreferences): AndroidTtsSettings - } - - fun interface VoiceSelector { - - /** - * Selects a voice for the given [language]. - */ - fun voice(language: Language?, availableVoices: Set<Voice>): Voice? - } - - class Error(code: Int) : TtsEngine.Error { - - val kind: Kind = - Kind.getOrDefault(code) - - /** - * Android's TTS error code. - * See https://developer.android.com/reference/android/speech/tts/TextToSpeech#ERROR - */ - enum class Kind(val code: Int) { - /** Denotes a generic operation failure. */ - Unknown(-1), - /** Denotes a failure caused by an invalid request. */ - InvalidRequest(-8), - /** Denotes a failure caused by a network connectivity problems. */ - Network(-6), - /** Denotes a failure caused by network timeout. */ - NetworkTimeout(-7), - /** Denotes a failure caused by an unfinished download of the voice data. */ - NotInstalledYet(-9), - /** Denotes a failure related to the output (audio device or a file). */ - Output(-5), - /** Denotes a failure of a TTS service. */ - Service(-4), - /** Denotes a failure of a TTS engine to synthesize the given input. */ - Synthesis(-3); - - companion object { - - fun getOrDefault(key: Int): Kind = - values() - .firstOrNull { it.code == key } - ?: Unknown - } - } - } - - /** - * Represents a voice provided by the TTS engine which can speak an utterance. - * - * @param id Unique and stable identifier for this voice - * @param language Language (and region) this voice belongs to. - * @param quality Voice quality. - * @param requiresNetwork Indicates whether using this voice requires an Internet connection. - */ - data class Voice( - val id: Id, - override val language: Language, - val quality: Quality = Quality.Normal, - val requiresNetwork: Boolean = false, - ) : TtsEngine.Voice { - - @kotlinx.serialization.Serializable - @JvmInline - value class Id(val value: String) - - enum class Quality { - Lowest, Low, Normal, High, Highest - } - } - - interface Listener { - - fun onMissingData(language: Language) - - fun onLanguageNotSupported(language: Language) - } - - private val _settings: MutableStateFlow<AndroidTtsSettings> = - MutableStateFlow(settingsResolver.settings(initialPreferences)) - - private var utteranceListener: TtsEngine.Listener<Error>? = - null - - override val voices: Set<Voice> get() = - engine.voices - ?.map { it.toTtsEngineVoice() } - ?.toSet() - .orEmpty() - - override fun setListener( - listener: TtsEngine.Listener<Error>? - ) { - if (listener == null) { - engine.setOnUtteranceProgressListener(null) - this@AndroidTtsEngine.utteranceListener = null - } else { - this@AndroidTtsEngine.utteranceListener = listener - engine.setOnUtteranceProgressListener(UtteranceListener(listener)) - } - } - - override fun speak( - requestId: TtsEngine.RequestId, - text: String, - language: Language? - ) { - engine.setupVoice(settings.value, language, voices) - val queued = engine.speak(text, QUEUE_ADD, null, requestId.id) - if (queued == ERROR) { - utteranceListener?.onError(requestId, Error(Error.Kind.Unknown.code)) - } - } - - override fun stop() { - engine.stop() - } - - override fun close() { - engine.shutdown() - } - - override val settings: StateFlow<AndroidTtsSettings> = - _settings.asStateFlow() - - override fun submitPreferences(preferences: AndroidTtsPreferences) { - val newSettings = settingsResolver.settings(preferences) - engine.setupPitchAndSpeed(newSettings) - _settings.value = newSettings - } - - private fun TextToSpeech.setupPitchAndSpeed(settings: AndroidTtsSettings) { - setSpeechRate(settings.speed.toFloat()) - setPitch(settings.pitch.toFloat()) - } - - private fun TextToSpeech.setupVoice( - settings: AndroidTtsSettings, - utteranceLanguage: Language?, - voices: Set<Voice> - ) { - val language = utteranceLanguage - .takeUnless { settings.overrideContentLanguage } - ?: settings.language - - when (engine.isLanguageAvailable(language.locale)) { - LANG_MISSING_DATA -> listener?.onMissingData(language) - LANG_NOT_SUPPORTED -> listener?.onLanguageNotSupported(language) - } - - val preferredVoiceWithRegion = - settings.voices[language] - ?.let { voiceForName(it.value) } - - val preferredVoiceWithoutRegion = - settings.voices[language.removeRegion()] - ?.let { voiceForName(it.value) } - - val voice = preferredVoiceWithRegion - ?: preferredVoiceWithoutRegion - ?: defaultVoice(language, voices) - - voice - ?.let { engine.voice = it } - ?: run { engine.language = language.locale } - } - - private fun defaultVoice(language: Language?, voices: Set<Voice>): AndroidVoice? = - voiceSelector - .voice(language, voices) - ?.let { voiceForName(it.id.value) } - - private fun voiceForName(name: String) = - engine.voices - .firstOrNull { it.name == name } - - private fun AndroidVoice.toTtsEngineVoice() = - Voice( - id = Voice.Id(name), - language = Language(locale), - quality = when (quality) { - QUALITY_VERY_HIGH -> Voice.Quality.Highest - QUALITY_HIGH -> Voice.Quality.High - QUALITY_NORMAL -> Voice.Quality.Normal - QUALITY_LOW -> Voice.Quality.Low - QUALITY_VERY_LOW -> Voice.Quality.Lowest - else -> throw IllegalStateException("Unexpected voice quality.") - }, - requiresNetwork = isNetworkConnectionRequired - ) - - class UtteranceListener( - private val listener: TtsEngine.Listener<Error>? - ) : UtteranceProgressListener() { - override fun onStart(utteranceId: String) { - listener?.onStart(TtsEngine.RequestId(utteranceId)) - } - - override fun onStop(utteranceId: String, interrupted: Boolean) { - listener?.let { - val requestId = TtsEngine.RequestId(utteranceId) - if (interrupted) { - it.onInterrupted(requestId) - } else { - it.onFlushed(requestId) - } - } - } - - override fun onDone(utteranceId: String) { - listener?.onDone(TtsEngine.RequestId(utteranceId)) - } - - @Deprecated("Deprecated in the interface", ReplaceWith("onError(utteranceId, -1)")) - override fun onError(utteranceId: String) { - onError(utteranceId, -1) - } - - override fun onError(utteranceId: String, errorCode: Int) { - listener?.onError( - TtsEngine.RequestId(utteranceId), - Error(errorCode) - ) - } - - override fun onRangeStart(utteranceId: String, start: Int, end: Int, frame: Int) { - listener?.onRange(TtsEngine.RequestId(utteranceId), start until end) - } - } -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/DelegatingPagerAdapter.java b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/DelegatingPagerAdapter.java index ef34b16c21..32f0d17e35 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/DelegatingPagerAdapter.java +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/DelegatingPagerAdapter.java @@ -24,7 +24,7 @@ import androidx.annotation.NonNull; import androidx.viewpager.widget.PagerAdapter; -public class DelegatingPagerAdapter extends PagerAdapter { +class DelegatingPagerAdapter extends PagerAdapter { private final PagerAdapter mDelegate; diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2CbzPageFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2CbzPageFragment.kt index 631dab30ce..218c352289 100755 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2CbzPageFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2CbzPageFragment.kt @@ -15,18 +15,21 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager +import androidx.core.os.BundleCompat import androidx.core.view.ViewCompat +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.github.chrisbanes.photoview.PhotoView import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.readium.r2.navigator.databinding.ViewpagerFragmentCbzBinding +import org.readium.r2.navigator.databinding.ReadiumNavigatorViewpagerFragmentCbzBinding import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication -class R2CbzPageFragment( +internal class R2CbzPageFragment( private val publication: Publication, private val onTapListener: (Float, Float) -> Unit ) : @@ -36,12 +39,12 @@ class R2CbzPageFragment( get() = Dispatchers.Main private val link: Link - get() = requireArguments().getParcelable("link")!! + get() = BundleCompat.getParcelable(requireArguments(), "link", Link::class.java)!! private lateinit var containerView: View private lateinit var photoView: PhotoView - private var _binding: ViewpagerFragmentCbzBinding? = null + private var _binding: ReadiumNavigatorViewpagerFragmentCbzBinding? = null private val binding get() = _binding!! override fun onCreateView( @@ -49,8 +52,7 @@ class R2CbzPageFragment( container: ViewGroup?, savedInstanceState: Bundle? ): View? { - - _binding = ViewpagerFragmentCbzBinding.inflate(inflater, container, false) + _binding = ReadiumNavigatorViewpagerFragmentCbzBinding.inflate(inflater, container, false) containerView = binding.root photoView = binding.imageView photoView.setOnViewTapListener { _, x, y -> onTapListener(x, y) } @@ -59,8 +61,8 @@ class R2CbzPageFragment( launch { publication.get(link) - .read() - .getOrNull() + ?.read() + ?.getOrNull() ?.let { BitmapFactory.decodeByteArray(it, 0, it.size) } ?.let { photoView.setImageBitmap(it) } } @@ -85,25 +87,27 @@ class R2CbzPageFragment( } private fun updatePadding() { - viewLifecycleOwner.lifecycleScope.launchWhenResumed { - val window = activity?.window ?: return@launchWhenResumed - var top = 0 - var bottom = 0 - - // Add additional padding to take into account the display cutout, if needed. - if ( - android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P && - window.attributes.layoutInDisplayCutoutMode != WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - ) { - // Request the display cutout insets from the decor view because the ones given by - // setOnApplyWindowInsetsListener are not always correct for preloaded views. - window.decorView.rootWindowInsets?.displayCutout?.let { displayCutoutInsets -> - top += displayCutoutInsets.safeInsetTop - bottom += displayCutoutInsets.safeInsetBottom + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + val window = activity?.window ?: return@repeatOnLifecycle + var top = 0 + var bottom = 0 + + // Add additional padding to take into account the display cutout, if needed. + if ( + android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P && + window.attributes.layoutInDisplayCutoutMode != WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER + ) { + // Request the display cutout insets from the decor view because the ones given by + // setOnApplyWindowInsetsListener are not always correct for preloaded views. + window.decorView.rootWindowInsets?.displayCutout?.let { displayCutoutInsets -> + top += displayCutoutInsets.safeInsetTop + bottom += displayCutoutInsets.safeInsetBottom + } } - } - photoView.setPadding(0, top, 0, bottom) + photoView.setPadding(0, top, 0, bottom) + } } } } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2EpubPageFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2EpubPageFragment.kt index ab9166b00a..55f0ea03c2 100755 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2EpubPageFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2EpubPageFragment.kt @@ -7,8 +7,6 @@ * LICENSE file present in the project repository where this source code is maintained. */ -@file:OptIn(ExperimentalReadiumApi::class) - package org.readium.r2.navigator.pager import android.annotation.SuppressLint @@ -21,11 +19,14 @@ import android.view.* import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView +import androidx.core.os.BundleCompat import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.webkit.WebViewClientCompat import kotlin.math.roundToInt import kotlinx.coroutines.flow.* @@ -33,24 +34,25 @@ import kotlinx.coroutines.launch import org.readium.r2.navigator.R import org.readium.r2.navigator.R2BasicWebView import org.readium.r2.navigator.R2WebView -import org.readium.r2.navigator.databinding.ViewpagerFragmentEpubBinding +import org.readium.r2.navigator.databinding.ReadiumNavigatorViewpagerFragmentEpubBinding import org.readium.r2.navigator.epub.EpubNavigatorFragment import org.readium.r2.navigator.epub.EpubNavigatorViewModel import org.readium.r2.navigator.extensions.htmlId +import org.readium.r2.navigator.preferences.ReadingProgression import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.InternalReadiumApi -import org.readium.r2.shared.SCROLL_REF import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.ReadingProgression +import org.readium.r2.shared.util.Url -class R2EpubPageFragment : Fragment() { +@OptIn(ExperimentalReadiumApi::class) +internal class R2EpubPageFragment : Fragment() { - private val resourceUrl: String? - get() = requireArguments().getString("url") + private val resourceUrl: Url? + get() = BundleCompat.getParcelable(requireArguments(), "url", Url::class.java) internal val link: Link? - get() = requireArguments().getParcelable("link") + get() = BundleCompat.getParcelable(requireArguments(), "link", Link::class.java) private var pendingLocator: Locator? = null @@ -62,9 +64,11 @@ class R2EpubPageFragment : Fragment() { private lateinit var containerView: View private lateinit var preferences: SharedPreferences - private val viewModel: EpubNavigatorViewModel by viewModels(ownerProducer = { requireParentFragment() }) + private val viewModel: EpubNavigatorViewModel by viewModels( + ownerProducer = { requireParentFragment() } + ) - private var _binding: ViewpagerFragmentEpubBinding? = null + private var _binding: ReadiumNavigatorViewpagerFragmentEpubBinding? = null private val binding get() = _binding!! private var isLoading: Boolean = false @@ -118,7 +122,11 @@ class R2EpubPageFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - pendingLocator = requireArguments().getParcelable("initialLocator") + pendingLocator = BundleCompat.getParcelable( + requireArguments(), + "initialLocator", + Locator::class.java + ) } @SuppressLint("SetJavaScriptEnabled", "JavascriptInterface") @@ -126,10 +134,13 @@ class R2EpubPageFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - _binding = ViewpagerFragmentEpubBinding.inflate(inflater, container, false) + ): View { + _binding = ReadiumNavigatorViewpagerFragmentEpubBinding.inflate(inflater, container, false) containerView = binding.root - preferences = activity?.getSharedPreferences("org.readium.r2.settings", Context.MODE_PRIVATE)!! + preferences = activity?.getSharedPreferences( + "org.readium.r2.settings", + Context.MODE_PRIVATE + )!! val webView = binding.webView this.webView = webView @@ -149,11 +160,6 @@ class R2EpubPageFragment : Fragment() { } webView.preferences = preferences - if (viewModel.useLegacySettings) { - @Suppress("DEPRECATION") - webView.setScrollMode(preferences.getBoolean(SCROLL_REF, false)) - } - webView.useLegacySettings = viewModel.useLegacySettings webView.settings.javaScriptEnabled = true webView.isVerticalScrollBarEnabled = false webView.isHorizontalScrollBarEnabled = false @@ -175,9 +181,8 @@ class R2EpubPageFragment : Fragment() { clampedX: Boolean, clampedY: Boolean ) { - val activity = activity ?: return + activity ?: return val metrics = DisplayMetrics() - activity.windowManager.defaultDisplay.getMetrics(metrics) val topDecile = webView.contentHeight - 1.15 * metrics.heightPixels val bottomDecile = (webView.contentHeight - metrics.heightPixels).toDouble() @@ -234,7 +239,7 @@ class R2EpubPageFragment : Fragment() { resourceUrl?.let { isLoading = true _isLoaded.value = false - webView.loadUrl(it) + webView.loadUrl(it.toString()) } setupPadding() @@ -251,13 +256,11 @@ class R2EpubPageFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - if (!viewModel.useLegacySettings) { - val lifecycleOwner = viewLifecycleOwner - lifecycleOwner.lifecycleScope.launch { - viewModel.isScrollEnabled - .flowWithLifecycle(lifecycleOwner.lifecycle) - .collectLatest { webView?.scrollModeFlow?.value = it } - } + val lifecycleOwner = viewLifecycleOwner + lifecycleOwner.lifecycleScope.launch { + viewModel.isScrollEnabled + .flowWithLifecycle(lifecycleOwner.lifecycle) + .collectLatest { webView?.scrollModeFlow?.value = it } } } @@ -303,32 +306,36 @@ class R2EpubPageFragment : Fragment() { private fun updatePadding() { if (view == null) return - viewLifecycleOwner.lifecycleScope.launchWhenResumed { - val window = activity?.window ?: return@launchWhenResumed - var top = 0 - var bottom = 0 + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + val window = activity?.window ?: return@repeatOnLifecycle + var top = 0 + var bottom = 0 + + // Add additional padding to take into account the display cutout, if needed. + if ( + shouldApplyInsetsPadding && + android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P && + window.attributes.layoutInDisplayCutoutMode != WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER + ) { + // Request the display cutout insets from the decor view because the ones given by + // setOnApplyWindowInsetsListener are not always correct for preloaded views. + window.decorView.rootWindowInsets?.displayCutout?.let { displayCutoutInsets -> + top += displayCutoutInsets.safeInsetTop + bottom += displayCutoutInsets.safeInsetBottom + } + } - // Add additional padding to take into account the display cutout, if needed. - if ( - shouldApplyInsetsPadding && - android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P && - window.attributes.layoutInDisplayCutoutMode != WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - ) { - // Request the display cutout insets from the decor view because the ones given by - // setOnApplyWindowInsetsListener are not always correct for preloaded views. - window.decorView.rootWindowInsets?.displayCutout?.let { displayCutoutInsets -> - top += displayCutoutInsets.safeInsetTop - bottom += displayCutoutInsets.safeInsetBottom + if (!viewModel.isScrollEnabled.value) { + val margin = + resources.getDimension(R.dimen.readium_navigator_epub_vertical_padding) + .toInt() + top += margin + bottom += margin } - } - if (!viewModel.isScrollEnabled.value) { - val margin = resources.getDimension(R.dimen.r2_navigator_epub_vertical_padding).toInt() - top += margin - bottom += margin + containerView.setPadding(0, top, 0, bottom) } - - containerView.setPadding(0, top, 0, bottom) } } @@ -348,17 +355,23 @@ class R2EpubPageFragment : Fragment() { if (view == null) return - viewLifecycleOwner.lifecycleScope.launchWhenCreated { - val webView = requireNotNull(webView) - webView.visibility = View.VISIBLE - - pendingLocator - ?.let { locator -> - loadLocator(webView, requireNotNull(navigator).readingProgression, locator) - } - .also { pendingLocator = null } + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { + val webView = requireNotNull(webView) + webView.visibility = View.VISIBLE + + pendingLocator + ?.let { locator -> + loadLocator( + webView, + requireNotNull(navigator).overflow.value.readingProgression, + locator + ) + } + .also { pendingLocator = null } - webView.listener?.onPageLoaded() + webView.listener?.onPageLoaded() + } } } @@ -368,11 +381,13 @@ class R2EpubPageFragment : Fragment() { return } - viewLifecycleOwner.lifecycleScope.launchWhenCreated { - val webView = requireNotNull(webView) - val epubNavigator = requireNotNull(navigator) - loadLocator(webView, epubNavigator.readingProgression, locator) - webView.listener?.onProgressionChanged() + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { + val webView = requireNotNull(webView) + val epubNavigator = requireNotNull(navigator) + loadLocator(webView, epubNavigator.overflow.value.readingProgression, locator) + webView.listener?.onProgressionChanged() + } } } @@ -398,8 +413,11 @@ class R2EpubPageFragment : Fragment() { // We need to reverse the progression with RTL because the Web View // always scrolls from left to right, no matter the reading direction. progression = - if (webView.scrollMode || readingProgression == ReadingProgression.LTR) progression - else 1 - progression + if (webView.scrollMode || readingProgression == ReadingProgression.LTR) { + progression + } else { + 1 - progression + } if (webView.scrollMode) { webView.scrollToPosition(progression) @@ -418,14 +436,14 @@ class R2EpubPageFragment : Fragment() { private const val textZoomBundleKey = "org.readium.textZoom" fun newInstance( - url: String, + url: Url, link: Link? = null, initialLocator: Locator? = null, positionCount: Int = 0 ): R2EpubPageFragment = R2EpubPageFragment().apply { arguments = Bundle().apply { - putString("url", url) + putParcelable("url", url) putParcelable("link", link) putParcelable("initialLocator", initialLocator) putLong("positionCount", positionCount.toLong()) @@ -440,7 +458,7 @@ class R2EpubPageFragment : Fragment() { private fun View.setOnClickListenerWithPoint(action: (View, PointF) -> Unit) { var point = PointF() - setOnTouchListener { v, event -> + setOnTouchListener { _, event -> if (event.action == MotionEvent.ACTION_DOWN) { point = PointF(event.x, event.y) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2FXLPageFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2FXLPageFragment.kt index 8bd6da3d12..ad502cd321 100755 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2FXLPageFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2FXLPageFragment.kt @@ -18,47 +18,54 @@ import android.view.ViewGroup import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView +import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.webkit.WebViewClientCompat import org.readium.r2.navigator.R2BasicWebView -import org.readium.r2.navigator.databinding.FragmentFxllayoutDoubleBinding -import org.readium.r2.navigator.databinding.FragmentFxllayoutSingleBinding +import org.readium.r2.navigator.databinding.ReadiumNavigatorFragmentFxllayoutDoubleBinding +import org.readium.r2.navigator.databinding.ReadiumNavigatorFragmentFxllayoutSingleBinding import org.readium.r2.navigator.epub.EpubNavigatorFragment import org.readium.r2.navigator.epub.EpubNavigatorViewModel import org.readium.r2.navigator.epub.fxl.R2FXLLayout import org.readium.r2.navigator.epub.fxl.R2FXLOnDoubleTapListener +import org.readium.r2.shared.util.Url -class R2FXLPageFragment : Fragment() { +internal class R2FXLPageFragment : Fragment() { - private val firstResourceUrl: String? - get() = requireArguments().getString("firstUrl") + private val firstResourceUrl: Url? + get() = BundleCompat.getParcelable(requireArguments(), "firstUrl", Url::class.java) - private val secondResourceUrl: String? - get() = requireArguments().getString("secondUrl") + private val secondResourceUrl: Url? + get() = BundleCompat.getParcelable(requireArguments(), "secondUrl", Url::class.java) private var webViews = mutableListOf<R2BasicWebView>() - private var _doubleBinding: FragmentFxllayoutDoubleBinding? = null + private var _doubleBinding: ReadiumNavigatorFragmentFxllayoutDoubleBinding? = null private val doubleBinding get() = _doubleBinding!! - private var _singleBinding: FragmentFxllayoutSingleBinding? = null + private var _singleBinding: ReadiumNavigatorFragmentFxllayoutSingleBinding? = null private val singleBinding get() = _singleBinding!! private val navigator: EpubNavigatorFragment? get() = parentFragment as? EpubNavigatorFragment - private val viewModel: EpubNavigatorViewModel by viewModels(ownerProducer = { requireParentFragment() }) + private val viewModel: EpubNavigatorViewModel by viewModels( + ownerProducer = { requireParentFragment() } + ) @SuppressLint("SetJavaScriptEnabled") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - + ): View { secondResourceUrl?.let { - _doubleBinding = FragmentFxllayoutDoubleBinding.inflate(inflater, container, false) + _doubleBinding = ReadiumNavigatorFragmentFxllayoutDoubleBinding.inflate( + inflater, + container, + false + ) val view: View = doubleBinding.root view.setPadding(0, 0, 0, 0) @@ -80,7 +87,11 @@ class R2FXLPageFragment : Fragment() { return view } ?: run { - _singleBinding = FragmentFxllayoutSingleBinding.inflate(inflater, container, false) + _singleBinding = ReadiumNavigatorFragmentFxllayoutSingleBinding.inflate( + inflater, + container, + false + ) val view: View = singleBinding.root view.setPadding(0, 0, 0, 0) @@ -125,13 +136,12 @@ class R2FXLPageFragment : Fragment() { } @SuppressLint("SetJavaScriptEnabled") - private fun setupWebView(webView: R2BasicWebView, resourceUrl: String?) { + private fun setupWebView(webView: R2BasicWebView, resourceUrl: Url?) { webViews.add(webView) navigator?.let { webView.listener = it.webViewListener } - webView.useLegacySettings = viewModel.useLegacySettings webView.settings.javaScriptEnabled = true webView.isVerticalScrollBarEnabled = false webView.isHorizontalScrollBarEnabled = false @@ -164,16 +174,16 @@ class R2FXLPageFragment : Fragment() { true } - resourceUrl?.let { webView.loadUrl(it) } + resourceUrl?.let { webView.loadUrl(it.toString()) } } companion object { - fun newInstance(url: String?, url2: String? = null): R2FXLPageFragment = + fun newInstance(url: Url?, url2: Url? = null): R2FXLPageFragment = R2FXLPageFragment().apply { arguments = Bundle().apply { - putString("firstUrl", url) - putString("secondUrl", url2) + putParcelable("firstUrl", url) + putParcelable("secondUrl", url2) } } } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2FragmentPagerAdapter.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2FragmentPagerAdapter.kt index e213b8a055..2c1c3df999 100755 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2FragmentPagerAdapter.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2FragmentPagerAdapter.kt @@ -15,12 +15,18 @@ import android.os.Parcelable import android.view.View import android.view.ViewGroup import androidx.collection.LongSparseArray +import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import androidx.viewpager.widget.PagerAdapter -abstract class R2FragmentPagerAdapter(private val mFragmentManager: FragmentManager) : androidx.fragment.app.FragmentStatePagerAdapter(mFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { +// This class will be going away when the navigator is rewritten +@Suppress("DEPRECATION") +internal abstract class R2FragmentPagerAdapter(private val mFragmentManager: FragmentManager) : androidx.fragment.app.FragmentStatePagerAdapter( + mFragmentManager, + BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT +) { val mFragments = LongSparseArray<Fragment>() private val mSavedStates = LongSparseArray<Fragment.SavedState>() @@ -116,7 +122,6 @@ abstract class R2FragmentPagerAdapter(private val mFragmentManager: FragmentMana override fun saveState(): Parcelable? { var state: Bundle? = null if (mSavedStates.size() > 0) { - state = Bundle() val stateIds = LongArray(mSavedStates.size()) for (i in 0 until mSavedStates.size()) { @@ -148,7 +153,14 @@ abstract class R2FragmentPagerAdapter(private val mFragmentManager: FragmentMana mFragments.clear() if (fss != null) { for (fs in fss) { - mSavedStates.put(fs, bundle.getParcelable<Parcelable>(fs.toString()) as Fragment.SavedState) + mSavedStates.put( + fs, + BundleCompat.getParcelable( + bundle, + fs.toString(), + Fragment.SavedState::class.java + ) + ) } } val keys = bundle.keySet() diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2PagerAdapter.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2PagerAdapter.kt index 0e5b7934a4..aa6e3b344a 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2PagerAdapter.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2PagerAdapter.kt @@ -18,8 +18,9 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.util.Url -class R2PagerAdapter internal constructor( +internal class R2PagerAdapter internal constructor( val fm: FragmentManager, private val resources: List<PageResource> ) : R2FragmentPagerAdapter(fm) { @@ -31,12 +32,12 @@ class R2PagerAdapter internal constructor( internal var listener: Listener? = null internal sealed class PageResource { - data class EpubReflowable(val link: Link, val url: String, val positionCount: Int) : PageResource() + data class EpubReflowable(val link: Link, val url: Url, val positionCount: Int) : PageResource() data class EpubFxl( val leftLink: Link? = null, - val leftUrl: String? = null, + val leftUrl: Url? = null, val rightLink: Link? = null, - val rightUrl: String? = null + val rightUrl: Url? = null ) : PageResource() data class Cbz(val link: Link) : PageResource() } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2RTLViewPager.java b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2RTLViewPager.java index 77deed1d49..6c2b00e143 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2RTLViewPager.java +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2RTLViewPager.java @@ -30,7 +30,8 @@ import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; -import org.readium.r2.shared.publication.ReadingProgression; +import org.readium.r2.navigator.preferences.ReadingProgression; +import org.readium.r2.shared.ExperimentalReadiumApi; import java.util.HashMap; @@ -44,7 +45,7 @@ * <code>OnPageChangeListener</code>s so that clients can be agnostic to layout direction and * modifications are kept internal to <code>RtlViewPager</code>. */ -public class R2RTLViewPager extends ViewPager { +class R2RTLViewPager extends ViewPager { public ReadingProgression direction = ReadingProgression.LTR; private int mLayoutDirection = ViewCompat.LAYOUT_DIRECTION_LTR; private HashMap<OnPageChangeListener, ReversingOnPageChangeListener> mPageChangeListeners = new HashMap<>(); diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2ViewPager.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2ViewPager.kt index 17ab0f6079..571160e495 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2ViewPager.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2ViewPager.kt @@ -13,12 +13,18 @@ import android.content.Context import android.util.AttributeSet import android.view.MotionEvent import org.readium.r2.navigator.BuildConfig.DEBUG -import org.readium.r2.shared.publication.Publication import timber.log.Timber -class R2ViewPager : R2RTLViewPager { +internal class R2ViewPager : R2RTLViewPager { - lateinit var type: Publication.TYPE + internal enum class PublicationType { + EPUB, CBZ, FXL, WEBPUB, AUDIO, DiViNa + } + + internal lateinit var publicationType: PublicationType + + @Deprecated(message = "You shouldn't be using these internals.", level = DeprecationLevel.ERROR) + val type = Unit constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) @@ -29,7 +35,7 @@ class R2ViewPager : R2RTLViewPager { override fun onTouchEvent(ev: MotionEvent): Boolean { if (DEBUG) Timber.d("ev.action ${ev.action}") - if (type == Publication.TYPE.EPUB) { + if (publicationType == PublicationType.EPUB) { when (ev.action and MotionEvent.ACTION_MASK) { MotionEvent.ACTION_DOWN -> { // prevent swipe from view pager directly @@ -51,7 +57,7 @@ class R2ViewPager : R2RTLViewPager { } override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { - if (type == Publication.TYPE.EPUB) { + if (publicationType == PublicationType.EPUB) { when (ev.action and MotionEvent.ACTION_MASK) { MotionEvent.ACTION_DOWN -> { // prevent swipe from view pager directly diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/RtlViewPager.java b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/RtlViewPager.java index d18dc86228..30fd3b6f85 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/RtlViewPager.java +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/RtlViewPager.java @@ -40,7 +40,7 @@ * <code>OnPageChangeListener</code>s so that clients can be agnostic to layout direction and * modifications are kept internal to <code>RtlViewPager</code>. */ -public class RtlViewPager extends ViewPager { +class RtlViewPager extends ViewPager { private final HashMap<OnPageChangeListener, ReversingOnPageChangeListener> mPageChangeListeners = new HashMap<>(); private int mLayoutDirection = ViewCompat.LAYOUT_DIRECTION_LTR; diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt index a517691302..cfb12ce30a 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt @@ -6,79 +6,71 @@ package org.readium.r2.navigator.pdf -import android.graphics.PointF import androidx.fragment.app.Fragment -import org.readium.r2.navigator.VisualNavigator +import kotlinx.coroutines.flow.StateFlow +import org.readium.r2.navigator.Navigator +import org.readium.r2.navigator.OverflowableNavigator +import org.readium.r2.navigator.input.InputListener import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.navigator.preferences.PreferencesEditor +import org.readium.r2.navigator.util.SingleFragmentFactory import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Metadata import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Url /** * To be implemented by adapters for third-party PDF engines which can be used with [PdfNavigatorFragment]. */ @ExperimentalReadiumApi -interface PdfEngineProvider<S : Configurable.Settings, P : Configurable.Preferences<P>, E : PreferencesEditor<P>> { +public interface PdfEngineProvider<S : Configurable.Settings, P : Configurable.Preferences<P>, E : PreferencesEditor<P>> { + + public interface Listener /** - * Creates a [PdfDocumentFragment] for [input]. + * Creates a [PdfDocumentFragment] factory for [input]. */ - suspend fun createDocumentFragment(input: PdfDocumentFragmentInput<S>): PdfDocumentFragment<S> + public fun createDocumentFragmentFactory(input: PdfDocumentFragmentInput<S>): SingleFragmentFactory<*> /** * Creates settings for [metadata] and [preferences]. */ - fun computeSettings(metadata: Metadata, preferences: P): S + public fun computeSettings(metadata: Metadata, preferences: P): S + + @Deprecated( + "Renamed to computeOverflow", + replaceWith = ReplaceWith("computeOverflow"), + level = DeprecationLevel.ERROR + ) + public fun computePresentation(settings: S): Any = + throw NotImplementedError() /** - * Infers a [VisualNavigator.Presentation] from settings. + * Infers a [OverflowableNavigator.Overflow] from [settings]. */ - fun computePresentation(settings: S): VisualNavigator.Presentation + public fun computeOverflow(settings: S): OverflowableNavigator.Overflow /** * Creates a preferences editor for [publication] and [initialPreferences]. */ - fun createPreferenceEditor(publication: Publication, initialPreferences: P): E + public fun createPreferenceEditor(publication: Publication, initialPreferences: P): E /** * Creates an empty set of preferences of this PDF engine provider. */ - fun createEmptyPreferences(): P + public fun createEmptyPreferences(): P } -@ExperimentalReadiumApi -typealias PdfDocumentFragmentFactory<S> = suspend (PdfDocumentFragmentInput<S>) -> PdfDocumentFragment<S> - /** - * A [PdfDocumentFragment] renders a single PDF resource. + * A [PdfDocumentFragment] renders a single PDF document. */ @ExperimentalReadiumApi -abstract class PdfDocumentFragment<S : Configurable.Settings> : Fragment() { - - interface Listener { - /** - * Called when the fragment navigates to a different page. - */ - fun onPageChanged(pageIndex: Int) - - /** - * Called when the user triggers a tap on the document. - */ - fun onTap(point: PointF): Boolean - - /** - * Called when the PDF engine fails to load the PDF document. - */ - fun onResourceLoadFailed(link: Link, error: Resource.Exception) - } +public abstract class PdfDocumentFragment<S : Configurable.Settings> : Fragment() { /** - * Returns the current page index in the document, from 0. + * Current page index displayed in the PDF document. */ - abstract val pageIndex: Int + public abstract val pageIndex: StateFlow<Int> /** * Jumps to the given page [index]. @@ -86,19 +78,20 @@ abstract class PdfDocumentFragment<S : Configurable.Settings> : Fragment() { * @param animated Indicates if the transition should be animated. * @return Whether the jump is valid. */ - abstract fun goToPageIndex(index: Int, animated: Boolean): Boolean + public abstract fun goToPageIndex(index: Int, animated: Boolean): Boolean /** - * Current settings for the PDF document. + * Submits a new set of settings. */ - abstract var settings: S + public abstract fun applySettings(settings: S) } @ExperimentalReadiumApi -data class PdfDocumentFragmentInput<S : Configurable.Settings>( +public data class PdfDocumentFragmentInput<S : Configurable.Settings>( val publication: Publication, - val link: Link, - val initialPageIndex: Int, + val href: Url, + val pageIndex: Int, val settings: S, - val listener: PdfDocumentFragment.Listener? + val navigatorListener: Navigator.Listener?, + val inputListener: InputListener? ) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorFactory.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorFactory.kt index 6522ff8825..827d8693d7 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorFactory.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorFactory.kt @@ -21,7 +21,7 @@ import org.readium.r2.shared.publication.Publication * @param pdfEngineProvider provider for third-party PDF engine adapter. */ @ExperimentalReadiumApi -class PdfNavigatorFactory<S : Configurable.Settings, P : Configurable.Preferences<P>, E : PreferencesEditor<P>>( +public class PdfNavigatorFactory<S : Configurable.Settings, P : Configurable.Preferences<P>, E : PreferencesEditor<P>>( private val publication: Publication, private val pdfEngineProvider: PdfEngineProvider<S, P, E> ) { @@ -35,10 +35,10 @@ class PdfNavigatorFactory<S : Configurable.Settings, P : Configurable.Preference * @param listener Optional listener to implement to observe events, such as user taps. */ @ExperimentalReadiumApi - fun createFragmentFactory( + public fun createFragmentFactory( initialLocator: Locator? = null, initialPreferences: P? = null, - listener: PdfNavigatorFragment.Listener? = null, + listener: PdfNavigatorFragment.Listener? = null ): FragmentFactory = createFragmentFactory { PdfNavigatorFragment( publication = publication, @@ -54,7 +54,7 @@ class PdfNavigatorFactory<S : Configurable.Settings, P : Configurable.Preference * * @param initialPreferences Initial set of preferences for the editor. */ - fun createPreferencesEditor( + public fun createPreferencesEditor( initialPreferences: P ): E = pdfEngineProvider.createPreferenceEditor( diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorFragment.kt index ec69069945..7f6e8ec089 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorFragment.kt @@ -6,64 +6,68 @@ package org.readium.r2.navigator.pdf -import android.graphics.PointF import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.* +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentContainerView +import androidx.fragment.app.FragmentFactory +import androidx.fragment.app.commitNow +import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import org.readium.r2.navigator.NavigatorFragment +import org.readium.r2.navigator.OverflowableNavigator import org.readium.r2.navigator.R import org.readium.r2.navigator.RestorationNotSupportedException import org.readium.r2.navigator.VisualNavigator import org.readium.r2.navigator.dummyPublication +import org.readium.r2.navigator.extensions.normalizeLocator import org.readium.r2.navigator.extensions.page +import org.readium.r2.navigator.input.CompositeInputListener +import org.readium.r2.navigator.input.InputListener +import org.readium.r2.navigator.input.KeyInterceptorView import org.readium.r2.navigator.preferences.Configurable -import org.readium.r2.navigator.preferences.PreferencesEditor -import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.navigator.util.SingleFragmentFactory import org.readium.r2.navigator.util.createFragmentFactory +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.extensions.mapStateIn -import org.readium.r2.shared.fetcher.Resource import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.ReadingProgression as PublicationReadingProgression import org.readium.r2.shared.publication.services.isRestricted +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType -import timber.log.Timber /** * Navigator for PDF publications. * * The PDF navigator delegates the actual PDF rendering to third-party engines like PDFium or - * PSPDFKit. You must use an implementation of [PdfDocumentFragmentFactory] provided by the PDF - * engine of your choice. + * PSPDFKit. * * To use this [Fragment], create a factory with [PdfNavigatorFactory.createFragmentFactory]. */ @ExperimentalReadiumApi -class PdfNavigatorFragment<S : Configurable.Settings, P : Configurable.Preferences<P>> internal constructor( - override val publication: Publication, - initialLocator: Locator? = null, - initialPreferences: P, +@OptIn(DelicateReadiumApi::class) +public class PdfNavigatorFragment<S : Configurable.Settings, P : Configurable.Preferences<P>> internal constructor( + publication: Publication, + private val initialLocator: Locator? = null, + private val initialPreferences: P, private val listener: Listener?, private val pdfEngineProvider: PdfEngineProvider<S, P, *> -) : Fragment(), VisualNavigator, Configurable<S, P> { +) : NavigatorFragment(publication), VisualNavigator, OverflowableNavigator, Configurable<S, P> { - interface Listener : VisualNavigator.Listener { + public interface Listener : VisualNavigator.Listener - /** - * Called when a PDF resource failed to be loaded, for example because of an [OutOfMemoryError]. - */ - fun onResourceLoadFailed(link: Link, error: Resource.Exception) {} - } - - companion object { + public companion object { /** * Creates a factory for [PdfNavigatorFragment]. @@ -76,17 +80,19 @@ class PdfNavigatorFragment<S : Configurable.Settings, P : Configurable.Preferenc * @param pdfEngineProvider provider for third-party PDF engine adapter. */ @ExperimentalReadiumApi - fun <S : Configurable.Settings, P : Configurable.Preferences<P>, E : PreferencesEditor<P>> createFactory( + public fun <P : Configurable.Preferences<P>> createFactory( publication: Publication, initialLocator: Locator? = null, preferences: P? = null, listener: Listener? = null, - pdfEngineProvider: PdfEngineProvider<S, P, E> + pdfEngineProvider: PdfEngineProvider<*, P, *> ): FragmentFactory = createFragmentFactory { PdfNavigatorFragment( - publication, initialLocator, + publication, + initialLocator, preferences ?: pdfEngineProvider.createEmptyPreferences(), - listener, pdfEngineProvider + listener, + pdfEngineProvider ) } @@ -96,12 +102,12 @@ class PdfNavigatorFragment<S : Configurable.Settings, P : Configurable.Preferenc * Used when Android restore the [PdfNavigatorFragment] after the process was killed. You need * to make sure the fragment is removed from the screen before `onResume` is called. */ - fun <P : Configurable.Preferences<P>> createDummyFactory( + public fun <P : Configurable.Preferences<P>> createDummyFactory( pdfEngineProvider: PdfEngineProvider<*, P, *> ): FragmentFactory = createFragmentFactory { PdfNavigatorFragment( publication = dummyPublication, - initialLocator = Locator(href = "#", type = "application/pdf"), + initialLocator = Locator(href = Url("#")!!, mediaType = MediaType.PDF), initialPreferences = pdfEngineProvider.createEmptyPreferences(), listener = null, pdfEngineProvider = pdfEngineProvider @@ -115,36 +121,42 @@ class PdfNavigatorFragment<S : Configurable.Settings, P : Configurable.Preferenc if (publication != dummyPublication) { require( publication.readingOrder.count() == 1 && - publication.readingOrder.first().mediaType.matches(MediaType.PDF) + publication.readingOrder.first().mediaType?.matches(MediaType.PDF) == true ) { "[PdfNavigatorFragment] currently supports only publications with a single PDF for reading order" } } } - // Configurable - - @Suppress("Unchecked_cast") - override val settings: StateFlow<S> get() = viewModel.settings as StateFlow<S> - - override fun submitPreferences(preferences: P) { - viewModel.submitPreferences(preferences) - } + private val inputListener = CompositeInputListener() private val viewModel: PdfNavigatorViewModel<S, P> by viewModels { PdfNavigatorViewModel.createFactory( requireActivity().application, publication, - initialLocator, - initialPreferences = initialPreferences, - pdfEngineProvider = pdfEngineProvider, + initialLocator?.locations, + initialPreferences, + pdfEngineProvider ) } - private lateinit var documentFragment: StateFlow<PdfDocumentFragment<S>?> + private lateinit var documentFragment: PdfDocumentFragment<S> + + private val documentFragmentFactory: SingleFragmentFactory<*> by lazy { + val locator = viewModel.currentLocator.value + pdfEngineProvider.createDocumentFragmentFactory( + PdfDocumentFragmentInput( + publication = publication, + href = locator.href, + pageIndex = locator.locations.pageIndex, + settings = viewModel.settings.value, + navigatorListener = listener, + inputListener = inputListener + ) + ) + } override fun onCreate(savedInstanceState: Bundle?) { - // Clears the savedInstanceState to prevent the child fragment manager from restoring the - // pdfFragment, as the [ResourceDataProvider] is not [Parcelable]. - super.onCreate(null) + childFragmentManager.fragmentFactory = documentFragmentFactory + super.onCreate(savedInstanceState) } override fun onCreateView( @@ -154,36 +166,34 @@ class PdfNavigatorFragment<S : Configurable.Settings, P : Configurable.Preferenc ): View { val view = FragmentContainerView(inflater.context) view.id = R.id.readium_pdf_container - return view + return KeyInterceptorView(view, inputListener) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - documentFragment = viewModel.currentLocator - .distinctUntilChanged { old, new -> - old.href == new.href - } - .map { locator -> - createPdfDocumentFragment(locator, settings.value) + val tag = "documentFragment" + if (savedInstanceState == null) { + childFragmentManager.commitNow { + replace( + R.id.readium_pdf_container, + documentFragmentFactory(), + tag + ) } - .stateIn(viewLifecycleOwner.lifecycleScope, started = SharingStarted.Eagerly, null) + } + + @Suppress("UNCHECKED_CAST") + documentFragment = childFragmentManager.findFragmentByTag(tag) as PdfDocumentFragment<S> viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - documentFragment - .filterNotNull() - .onEach { fragment: PdfDocumentFragment<S> -> - childFragmentManager.commitNow { - replace(R.id.readium_pdf_container, fragment, "readium_pdf_fragment") - } - } + repeatOnLifecycle(Lifecycle.State.STARTED) { + documentFragment.pageIndex + .onEach { viewModel.onPageChanged(it) } .launchIn(this) - settings - .onEach { settings -> - documentFragment.value?.settings = settings - } + viewModel.settings + .onEach { documentFragment.applySettings(it) } .launchIn(this) } } @@ -197,60 +207,23 @@ class PdfNavigatorFragment<S : Configurable.Settings, P : Configurable.Preferenc } } - private suspend fun createPdfDocumentFragment(locator: Locator, settings: S): PdfDocumentFragment<S>? { - val link = publication.linkWithHref(locator.href) ?: return null - - return try { - val pageIndex = (locator.locations.page ?: 1) - 1 - pdfEngineProvider.createDocumentFragment( - PdfDocumentFragmentInput( - publication = publication, - link = link, - initialPageIndex = pageIndex, - settings = settings, - listener = DocumentFragmentListener() - ) - ) - } catch (e: Exception) { - Timber.e(e, "Failed to load PDF resource ${link.href}") - listener?.onResourceLoadFailed(link, Resource.Exception.wrap(e)) - null - } - } - - private inner class DocumentFragmentListener : PdfDocumentFragment.Listener { - override fun onPageChanged(pageIndex: Int) { - viewModel.onPageChanged(pageIndex) - } + // Configurable - override fun onTap(point: PointF): Boolean { - return listener?.onTap(point) ?: false - } + override val settings: StateFlow<S> get() = viewModel.settings - override fun onResourceLoadFailed(link: Link, error: Resource.Exception) { - listener?.onResourceLoadFailed(link, error) - } + override fun submitPreferences(preferences: P) { + viewModel.submitPreferences(preferences) } - @ExperimentalReadiumApi - override val presentation: StateFlow<VisualNavigator.Presentation> - get() = settings.mapStateIn(lifecycleScope) { settings -> - pdfEngineProvider.computePresentation(settings) - } - - override val readingProgression: PublicationReadingProgression - get() = when (presentation.value.readingProgression) { - ReadingProgression.LTR -> PublicationReadingProgression.LTR - ReadingProgression.RTL -> PublicationReadingProgression.RTL - } + // Navigator - override val currentLocator: StateFlow<Locator> - get() = viewModel.currentLocator + override val currentLocator: StateFlow<Locator> get() = viewModel.currentLocator override fun go(locator: Locator, animated: Boolean, completion: () -> Unit): Boolean { + @Suppress("NAME_SHADOWING") + val locator = publication.normalizeLocator(locator) listener?.onJumpToLocator(locator) - val pageNumber = locator.locations.page ?: locator.locations.position ?: 1 - return goToPageIndex(pageNumber - 1, animated, completion) + return goToPageIndex(locator.locations.pageIndex, animated, completion) } override fun go(link: Link, animated: Boolean, completion: () -> Unit): Boolean { @@ -259,19 +232,48 @@ class PdfNavigatorFragment<S : Configurable.Settings, P : Configurable.Preferenc } override fun goForward(animated: Boolean, completion: () -> Unit): Boolean { - val fragment = documentFragment.value ?: return false - return goToPageIndex(fragment.pageIndex + 1, animated, completion) + val pageIndex = currentLocator.value.locations.pageIndex + 1 + return goToPageIndex(pageIndex, animated, completion) } override fun goBackward(animated: Boolean, completion: () -> Unit): Boolean { - val fragment = documentFragment.value ?: return false - return goToPageIndex(fragment.pageIndex - 1, animated, completion) + val pageIndex = currentLocator.value.locations.pageIndex - 1 + return goToPageIndex(pageIndex, animated, completion) } private fun goToPageIndex(pageIndex: Int, animated: Boolean, completion: () -> Unit): Boolean { - val fragment = documentFragment.value ?: return false - val success = fragment.goToPageIndex(pageIndex, animated = animated) + val success = documentFragment.goToPageIndex(pageIndex, animated = animated) if (success) { completion() } return success } + + // VisualNavigator + + override val publicationView: View + get() = requireView() + + @ExperimentalReadiumApi + override val overflow: StateFlow<OverflowableNavigator.Overflow> + get() = settings.mapStateIn(lifecycleScope) { settings -> + pdfEngineProvider.computeOverflow(settings) + } + + @Deprecated( + "Use `presentation.value.readingProgression` instead", + replaceWith = ReplaceWith("presentation.value.readingProgression"), + level = DeprecationLevel.ERROR + ) + override val readingProgression: PublicationReadingProgression + get() = throw NotImplementedError() + + override fun addInputListener(listener: InputListener) { + inputListener.add(listener) + } + + override fun removeInputListener(listener: InputListener) { + inputListener.remove(listener) + } } + +private val Locator.Locations.pageIndex: Int get() = + (page ?: position ?: 1) - 1 diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorViewModel.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorViewModel.kt index e67c11608e..04047bc21a 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorViewModel.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorViewModel.kt @@ -14,7 +14,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.readium.r2.navigator.preferences.Configurable -import org.readium.r2.navigator.preferences.PreferencesEditor import org.readium.r2.navigator.util.createViewModelFactory import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Locator @@ -25,26 +24,31 @@ import org.readium.r2.shared.publication.services.positions internal class PdfNavigatorViewModel<S : Configurable.Settings, P : Configurable.Preferences<P>>( application: Application, private val publication: Publication, - initialLocator: Locator, + initialLocations: Locator.Locations?, initialPreferences: P, private val pdfEngineProvider: PdfEngineProvider<S, P, *> ) : AndroidViewModel(application) { private val _currentLocator: MutableStateFlow<Locator> = - MutableStateFlow(initialLocator) + MutableStateFlow( + requireNotNull(publication.locatorFromLink(publication.readingOrder.first())) + .copy(locations = initialLocations ?: Locator.Locations()) + ) val currentLocator: StateFlow<Locator> = _currentLocator.asStateFlow() - private val _settings: MutableStateFlow<Configurable.Settings> = MutableStateFlow( - pdfEngineProvider.computeSettings(publication.metadata, initialPreferences) - ) + private val _settings: MutableStateFlow<S> = + MutableStateFlow(computeSettings(initialPreferences)) - val settings: StateFlow<Configurable.Settings> = _settings.asStateFlow() + val settings: StateFlow<S> = _settings.asStateFlow() fun submitPreferences(preferences: P) = viewModelScope.launch { - _settings.value = pdfEngineProvider.computeSettings(publication.metadata, preferences) + _settings.value = computeSettings(preferences) } + private fun computeSettings(preferences: P): S = + pdfEngineProvider.computeSettings(publication.metadata, preferences) + fun onPageChanged(pageIndex: Int) = viewModelScope.launch { publication.positions().getOrNull(pageIndex)?.let { locator -> _currentLocator.value = locator @@ -52,18 +56,17 @@ internal class PdfNavigatorViewModel<S : Configurable.Settings, P : Configurable } companion object { - fun <S : Configurable.Settings, P : Configurable.Preferences<P>, E : PreferencesEditor<P>> createFactory( + fun <S : Configurable.Settings, P : Configurable.Preferences<P>> createFactory( application: Application, publication: Publication, - initialLocator: Locator?, + initialLocations: Locator.Locations?, initialPreferences: P, - pdfEngineProvider: PdfEngineProvider<S, P, E> + pdfEngineProvider: PdfEngineProvider<S, P, *> ) = createViewModelFactory { PdfNavigatorViewModel( application = application, publication = publication, - initialLocator = initialLocator - ?: requireNotNull(publication.locatorFromLink(publication.readingOrder.first())), + initialLocations = initialLocations, initialPreferences = initialPreferences, pdfEngineProvider = pdfEngineProvider ) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/R2PdfActivity.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/R2PdfActivity.kt index c7ad30d3a4..e81ede51af 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/R2PdfActivity.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/R2PdfActivity.kt @@ -11,5 +11,8 @@ package org.readium.r2.navigator.pdf import androidx.appcompat.app.AppCompatActivity -@Deprecated("Use `PdfNavigatorFragment` in your own activity instead", level = DeprecationLevel.ERROR) -abstract class R2PdfActivity : AppCompatActivity() +@Deprecated( + "Use `PdfNavigatorFragment` in your own activity instead", + level = DeprecationLevel.ERROR +) +public abstract class R2PdfActivity : AppCompatActivity() diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/Configurable.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/Configurable.kt index 62f1ffb176..6006b1a372 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/Configurable.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/Configurable.kt @@ -15,30 +15,30 @@ import org.readium.r2.shared.ExperimentalReadiumApi * A [Configurable] is a component with a set of configurable [Settings]. */ @ExperimentalReadiumApi -interface Configurable<S : Settings, P : Preferences<P>> { +public interface Configurable<S : Settings, P : Preferences<P>> { /** * Marker interface for the [Settings] properties holder. */ - interface Settings + public interface Settings /** * Marker interface for the [Preferences] properties holder. */ - interface Preferences<P : Preferences<P>> { + public interface Preferences<P : Preferences<P>> { /** * Creates a new instance of [P] after merging the values of [other]. * * In case of conflict, [other] takes precedence. */ - operator fun plus(other: P): P + public operator fun plus(other: P): P } /** * Current [Settings] values. */ - val settings: StateFlow<S> + public val settings: StateFlow<S> /** * Submits a new set of [Preferences] to update the current [Settings]. @@ -46,24 +46,24 @@ interface Configurable<S : Settings, P : Preferences<P>> { * Note that the [Configurable] might not update its [settings] right away, or might even ignore * some of the provided preferences. They are only used as hints to compute the new settings. */ - fun submitPreferences(preferences: P) + public fun submitPreferences(preferences: P) } /** * JSON serializer of [P]. */ @ExperimentalReadiumApi -interface PreferencesSerializer<P : Preferences<P>> { +public interface PreferencesSerializer<P : Preferences<P>> { /** * Serialize [P] into a JSON string. */ - fun serialize(preferences: P): String + public fun serialize(preferences: P): String /** * Deserialize [P] from a JSON string. */ - fun deserialize(preferences: String): P + public fun deserialize(preferences: String): P } /** @@ -72,30 +72,30 @@ interface PreferencesSerializer<P : Preferences<P>> { * This can be used as a helper for a user preferences screen. */ @ExperimentalReadiumApi -interface PreferencesEditor<P : Preferences<P>> { +public interface PreferencesEditor<P : Preferences<P>> { /** * The current preferences. */ - val preferences: P + public val preferences: P /** * Unset all preferences. */ - fun clear() + public fun clear() } /** * A filter to keep only some preferences and filter out some others. */ @ExperimentalReadiumApi -fun interface PreferencesFilter<P : Preferences<P>> { +public fun interface PreferencesFilter<P : Preferences<P>> { - fun filter(preferences: P): P + public fun filter(preferences: P): P } @ExperimentalReadiumApi -operator fun <P : Preferences<P>> PreferencesFilter<P>.plus(other: PreferencesFilter<P>): PreferencesFilter<P> = +public operator fun <P : Preferences<P>> PreferencesFilter<P>.plus(other: PreferencesFilter<P>): PreferencesFilter<P> = CombinedPreferencesFilter(this, other) @ExperimentalReadiumApi diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/MappedPreference.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/MappedPreference.kt index 163855a102..412e57bdc9 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/MappedPreference.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/MappedPreference.kt @@ -7,21 +7,21 @@ import org.readium.r2.shared.ExperimentalReadiumApi * the target type [V]. */ @ExperimentalReadiumApi -fun <T, V> Preference<T>.map(from: (T) -> V, to: (V) -> T): Preference<V> = +public fun <T, V> Preference<T>.map(from: (T) -> V, to: (V) -> T): Preference<V> = MappedPreference(this, from, to) /** * Creates a new [EnumPreference] object wrapping the receiver with the provided [supportedValues]. */ @ExperimentalReadiumApi -fun <T> Preference<T>.withSupportedValues(vararg supportedValues: T): EnumPreference<T> = +public fun <T> Preference<T>.withSupportedValues(vararg supportedValues: T): EnumPreference<T> = withSupportedValues(supportedValues.toList()) /** * Creates a new [EnumPreference] object wrapping the receiver with the provided [supportedValues]. */ @ExperimentalReadiumApi -fun <T> Preference<T>.withSupportedValues(supportedValues: List<T>): EnumPreference<T> = +public fun <T> Preference<T>.withSupportedValues(supportedValues: List<T>): EnumPreference<T> = PreferenceWithSupportedValues(this, supportedValues) /** @@ -29,7 +29,7 @@ fun <T> Preference<T>.withSupportedValues(supportedValues: List<T>): EnumPrefere * [supportedValues], [from] and [to] the target type [V]. */ @ExperimentalReadiumApi -fun <T, V> EnumPreference<T>.map( +public fun <T, V> EnumPreference<T>.map( from: (T) -> V, to: (V) -> T, supportedValues: (List<T>) -> List<V> = { it.map(from) } @@ -40,14 +40,14 @@ fun <T, V> EnumPreference<T>.map( * Creates a new [EnumPreference] object wrapping the receiver with the provided [supportedValues]. */ @ExperimentalReadiumApi -fun <T> EnumPreference<T>.withSupportedValues(vararg supportedValues: T): EnumPreference<T> = +public fun <T> EnumPreference<T>.withSupportedValues(vararg supportedValues: T): EnumPreference<T> = withSupportedValues(supportedValues.toList()) /** * Creates a new [EnumPreference] object wrapping the receiver with the provided [supportedValues]. */ @ExperimentalReadiumApi -fun <T> EnumPreference<T>.withSupportedValues(supportedValues: List<T>): EnumPreference<T> = +public fun <T> EnumPreference<T>.withSupportedValues(supportedValues: List<T>): EnumPreference<T> = map(from = { it }, to = { it }, supportedValues = { supportedValues }) /** @@ -55,7 +55,7 @@ fun <T> EnumPreference<T>.withSupportedValues(supportedValues: List<T>): EnumPre * values with [transform]. */ @ExperimentalReadiumApi -fun <T> EnumPreference<T>.mapSupportedValues(transform: (List<T>) -> List<T>): EnumPreference<T> = +public fun <T> EnumPreference<T>.mapSupportedValues(transform: (List<T>) -> List<T>): EnumPreference<T> = map(from = { it }, to = { it }, supportedValues = transform) /** @@ -65,16 +65,18 @@ fun <T> EnumPreference<T>.mapSupportedValues(transform: (List<T>) -> List<T>): E * The value formatter, or [increment] and [decrement] strategy of the receiver can be overwritten. */ @ExperimentalReadiumApi -fun <T : Comparable<T>, V : Comparable<V>> RangePreference<T>.map( +public fun <T : Comparable<T>, V : Comparable<V>> RangePreference<T>.map( from: (T) -> V, to: (V) -> T, supportedRange: (ClosedRange<T>) -> ClosedRange<V> = { from(it.start)..from(it.endInclusive) }, formatValue: ((V) -> String)? = null, increment: (RangePreference<V>.() -> Unit)? = null, - decrement: (RangePreference<V>.() -> Unit)? = null, + decrement: (RangePreference<V>.() -> Unit)? = null ): RangePreference<V> = MappedRangePreference( - this, from, to, + this, + from, + to, transformSupportedRange = supportedRange, valueFormatter = formatValue, incrementer = increment, @@ -86,14 +88,16 @@ fun <T : Comparable<T>, V : Comparable<V>> RangePreference<T>.map( * [supportedRange], or overwriting its [formatValue] or [increment] and [decrement] strategy. */ @ExperimentalReadiumApi -fun <T : Comparable<T>> RangePreference<T>.map( +public fun <T : Comparable<T>> RangePreference<T>.map( supportedRange: (ClosedRange<T>) -> ClosedRange<T> = { it }, formatValue: ((T) -> String)? = null, increment: (RangePreference<T>.() -> Unit)? = null, - decrement: (RangePreference<T>.() -> Unit)? = null, + decrement: (RangePreference<T>.() -> Unit)? = null ): RangePreference<T> = MappedRangePreference( - this, { it }, { it }, + this, + { it }, + { it }, transformSupportedRange = supportedRange, valueFormatter = formatValue, incrementer = increment, @@ -106,7 +110,7 @@ fun <T : Comparable<T>> RangePreference<T>.map( * and decrement. */ @ExperimentalReadiumApi -fun <T : Comparable<T>> RangePreference<T>.withSupportedRange( +public fun <T : Comparable<T>> RangePreference<T>.withSupportedRange( range: ClosedRange<T> = supportedRange, progressionStrategy: ProgressionStrategy<T> ): RangePreference<T> = @@ -143,7 +147,7 @@ private open class MappedPreference<T, V>( @ExperimentalReadiumApi private class PreferenceWithSupportedValues<T>( override val original: Preference<T>, - override val supportedValues: List<T>, + override val supportedValues: List<T> ) : MappedPreference<T, T>(original, from = { it }, to = { it }), EnumPreference<T> @ExperimentalReadiumApi diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/Preference.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/Preference.kt index 0788520078..fd74422f10 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/Preference.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/Preference.kt @@ -13,44 +13,45 @@ import org.readium.r2.shared.ExperimentalReadiumApi * which value the [Configurable] will effectively use. */ @ExperimentalReadiumApi -interface Preference<T> { +public interface Preference<T> { /** * The current value of the preference. */ - val value: T? + public val value: T? /** * The value that will be effectively used by the navigator * if preferences are submitted as they are. */ - val effectiveValue: T + public val effectiveValue: T /** * If this preference will be effectively used by the navigator * if preferences are submitted as they are. */ - val isEffective: Boolean + public val isEffective: Boolean /** * Set the preference to [value]. A null value means unsetting the preference. */ - fun set(value: T?) + public fun set(value: T?) } /** * Unset the preference. */ @ExperimentalReadiumApi -fun <T> Preference<T>.clear() = +public fun <T> Preference<T>.clear() { set(null) +} /** * Toggle the preference value. A default value is taken as the initial one if * the preference is currently unset. */ @OptIn(ExperimentalReadiumApi::class) -fun Preference<Boolean>.toggle() { +public fun Preference<Boolean>.toggle() { set(!(value ?: effectiveValue)) } @@ -58,40 +59,40 @@ fun Preference<Boolean>.toggle() { * Returns a new preference with its boolean value flipped. */ @OptIn(ExperimentalReadiumApi::class) -fun Preference<Boolean>.flipped(): Preference<Boolean> = +public fun Preference<Boolean>.flipped(): Preference<Boolean> = map(from = { !it }, to = { !it }) /** * A [Preference] which accepts a closed set of values. */ @ExperimentalReadiumApi -interface EnumPreference<T> : Preference<T> { +public interface EnumPreference<T> : Preference<T> { /** * List of valid values for this preference. */ - val supportedValues: List<T> + public val supportedValues: List<T> } /** * A [Preference] whose values must be in a [ClosedRange] of [T]. */ @ExperimentalReadiumApi -interface RangePreference<T : Comparable<T>> : Preference<T> { +public interface RangePreference<T : Comparable<T>> : Preference<T> { - val supportedRange: ClosedRange<T> + public val supportedRange: ClosedRange<T> /** * Increment the preference value from its current value or a default value. */ - fun increment() + public fun increment() /** * Decrement the preference value from its current value or a default value. */ - fun decrement() + public fun decrement() /** * Format [value] in a way suitable for display, including unit if relevant. */ - fun formatValue(value: T): String + public fun formatValue(value: T): String } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/PreferenceDelegate.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/PreferenceDelegate.kt index 66c45882b2..15543e2a00 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/PreferenceDelegate.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/PreferenceDelegate.kt @@ -12,11 +12,11 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.InternalReadiumApi @InternalReadiumApi -open class PreferenceDelegate<T>( +public open class PreferenceDelegate<T>( private val getValue: () -> T?, private val getEffectiveValue: () -> T, private val getIsEffective: () -> Boolean, - private val updateValue: (T?) -> Unit, + private val updateValue: (T?) -> Unit ) : Preference<T> { override val value: T? @@ -28,17 +28,18 @@ open class PreferenceDelegate<T>( override val isEffective: Boolean get() = getIsEffective() - override fun set(value: T?) = + public override fun set(value: T?) { updateValue(value) + } } @InternalReadiumApi -class EnumPreferenceDelegate<T>( +public class EnumPreferenceDelegate<T>( getValue: () -> T?, getEffectiveValue: () -> T, getIsEffective: () -> Boolean, updateValue: (T?) -> Unit, - override val supportedValues: List<T>, + override val supportedValues: List<T> ) : PreferenceDelegate<T>(getValue, getEffectiveValue, getIsEffective, updateValue), EnumPreference<T> { @@ -49,14 +50,14 @@ class EnumPreferenceDelegate<T>( } @InternalReadiumApi -class RangePreferenceDelegate<T : Comparable<T>>( +public class RangePreferenceDelegate<T : Comparable<T>>( getValue: () -> T?, getEffectiveValue: () -> T, getIsEffective: () -> Boolean, updateValue: (T?) -> Unit, private val valueFormatter: (T) -> String, override val supportedRange: ClosedRange<T>, - private val progressionStrategy: ProgressionStrategy<T>, + private val progressionStrategy: ProgressionStrategy<T> ) : PreferenceDelegate<T>(getValue, getEffectiveValue, getIsEffective, updateValue), RangePreference<T> { diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/ProgressionStrategy.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/ProgressionStrategy.kt index 04bcc633e0..b8eca01445 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/ProgressionStrategy.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/ProgressionStrategy.kt @@ -11,11 +11,11 @@ import org.readium.r2.shared.extensions.equalsDelta /** * A strategy to increment or decrement a setting. */ -interface ProgressionStrategy<V> { +public interface ProgressionStrategy<V> { - fun increment(value: V): V + public fun increment(value: V): V - fun decrement(value: V): V + public fun decrement(value: V): V } /** @@ -25,19 +25,19 @@ interface ProgressionStrategy<V> { * * @param equalsDelta Provide an equality algorithm to compare floating point numbers. */ -class StepsProgression<T : Comparable<T>>( +public class StepsProgression<T : Comparable<T>>( private val steps: List<T>, private val equalsDelta: (T, T) -> Boolean ) : ProgressionStrategy<T> { - companion object { - operator fun invoke(vararg steps: Int): StepsProgression<Int> = + public companion object { + public operator fun invoke(vararg steps: Int): StepsProgression<Int> = StepsProgression(steps = steps.toList(), equalsDelta = Int::equals) - operator fun invoke(vararg steps: Float): StepsProgression<Float> = + public operator fun invoke(vararg steps: Float): StepsProgression<Float> = StepsProgression(steps = steps.toList(), equalsDelta = { a, b -> a.equalsDelta(b) }) - operator fun invoke(vararg steps: Double): StepsProgression<Double> = + public operator fun invoke(vararg steps: Double): StepsProgression<Double> = StepsProgression(steps = steps.toList(), equalsDelta = { a, b -> a.equalsDelta(b) }) } @@ -56,7 +56,7 @@ class StepsProgression<T : Comparable<T>>( * Simple progression strategy which increments or decrements the setting * by a fixed number. */ -class IntIncrement(private val increment: Int) : ProgressionStrategy<Int> { +public class IntIncrement(private val increment: Int) : ProgressionStrategy<Int> { override fun increment(value: Int): Int = value + increment @@ -67,7 +67,7 @@ class IntIncrement(private val increment: Int) : ProgressionStrategy<Int> { * Simple progression strategy which increments or decrements the setting * by a fixed number. */ -class DoubleIncrement(private val increment: Double) : ProgressionStrategy<Double> { +public class DoubleIncrement(private val increment: Double) : ProgressionStrategy<Double> { override fun increment(value: Double): Double = value + increment diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/Types.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/Types.kt index 627bbe1ed6..f2f4856c67 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/Types.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/preferences/Types.kt @@ -14,79 +14,109 @@ import org.readium.r2.shared.ExperimentalReadiumApi // https://github.com/readium/readium-css/blob/master/css/src/modules/ReadiumCSS-day_mode.css @ColorInt private val dayContentColor: Int = AndroidColor.parseColor("#121212") + @ColorInt private val dayBackgroundColor: Int = AndroidColor.parseColor("#FFFFFF") + // https://github.com/readium/readium-css/blob/master/css/src/modules/ReadiumCSS-night_mode.css @ColorInt private val nightContentColor: Int = AndroidColor.parseColor("#FEFEFE") + @ColorInt private val nightBackgroundColor: Int = AndroidColor.parseColor("#000000") + // https://github.com/readium/readium-css/blob/master/css/src/modules/ReadiumCSS-sepia_mode.css @ColorInt private val sepiaContentColor: Int = AndroidColor.parseColor("#121212") + @ColorInt private val sepiaBackgroundColor: Int = AndroidColor.parseColor("#faf4e8") @ExperimentalReadiumApi @Serializable -enum class Theme(@ColorInt val contentColor: Int, @ColorInt val backgroundColor: Int) { - @SerialName("light") LIGHT(contentColor = dayContentColor, backgroundColor = dayBackgroundColor), - @SerialName("dark") DARK(contentColor = nightContentColor, backgroundColor = nightBackgroundColor), - @SerialName("sepia") SEPIA(contentColor = sepiaContentColor, backgroundColor = sepiaBackgroundColor); +public enum class Theme( + @ColorInt public val contentColor: Int, + @ColorInt public val backgroundColor: Int +) { + @SerialName("light") + LIGHT(contentColor = dayContentColor, backgroundColor = dayBackgroundColor), + + @SerialName("dark") + DARK(contentColor = nightContentColor, backgroundColor = nightBackgroundColor), + + @SerialName("sepia") + SEPIA(contentColor = sepiaContentColor, backgroundColor = sepiaBackgroundColor); } @ExperimentalReadiumApi @Serializable -enum class TextAlign { +public enum class TextAlign { /** Align the text in the center of the page. */ - @SerialName("center") CENTER, + @SerialName("center") + CENTER, + /** Stretch lines of text that end with a soft line break to fill the width of the page. */ - @SerialName("justify") JUSTIFY, + @SerialName("justify") + JUSTIFY, + /** Align the text on the leading edge of the page. */ - @SerialName("start") START, + @SerialName("start") + START, + /** Align the text on the trailing edge of the page. */ - @SerialName("end") END, + @SerialName("end") + END, + /** Align the text on the left edge of the page. */ - @SerialName("left") LEFT, + @SerialName("left") + LEFT, + /** Align the text on the right edge of the page. */ - @SerialName("right") RIGHT; + @SerialName("right") + RIGHT; } @ExperimentalReadiumApi @Serializable -enum class ColumnCount { - @SerialName("auto") AUTO, - @SerialName("1") ONE, - @SerialName("2") TWO; +public enum class ColumnCount { + @SerialName("auto") + AUTO, + + @SerialName("1") + ONE, + + @SerialName("2") + TWO; } @ExperimentalReadiumApi @Serializable -enum class ImageFilter { - @SerialName("darken") DARKEN, - @SerialName("invert") INVERT; +public enum class ImageFilter { + @SerialName("darken") + DARKEN, + + @SerialName("invert") + INVERT; } /** * Typeface for a publication's text. * - * When not available, the Navigator should use [alternate] as a fallback. - * * For a list of vetted font families, see https://readium.org/readium-css/docs/CSS10-libre_fonts. */ @JvmInline @ExperimentalReadiumApi @Serializable -value class FontFamily(val name: String) { +public value class FontFamily(public val name: String) { - companion object { + public companion object { // Generic font families // See https://www.w3.org/TR/css-fonts-4/#generic-font-families - val SERIF = FontFamily("serif") - val SANS_SERIF = FontFamily("sans-serif") - val CURSIVE = FontFamily("cursive") - val FANTASY = FontFamily("fantasy") - val MONOSPACE = FontFamily("monospace") + public val SERIF: FontFamily = FontFamily("serif") + public val SANS_SERIF: FontFamily = FontFamily("sans-serif") + public val CURSIVE: FontFamily = FontFamily("cursive") + public val FANTASY: FontFamily = FontFamily("fantasy") + public val MONOSPACE: FontFamily = FontFamily("monospace") // Accessibility fonts embedded with Readium - val ACCESSIBLE_DFA = FontFamily("AccessibleDfA") - val IA_WRITER_DUOSPACE = FontFamily("IA Writer Duospace") - val OPEN_DYSLEXIC = FontFamily("OpenDyslexic") + public val ACCESSIBLE_DFA: FontFamily = FontFamily("AccessibleDfA") + public val IA_WRITER_DUOSPACE: FontFamily = FontFamily("IA Writer Duospace") + public val OPEN_DYSLEXIC: FontFamily = FontFamily("OpenDyslexic") } } @@ -96,16 +126,19 @@ value class FontFamily(val name: String) { @ExperimentalReadiumApi @Serializable @JvmInline -value class Color(@ColorInt val int: Int) +public value class Color(@ColorInt public val int: Int) /** * Layout axis. */ @ExperimentalReadiumApi @Serializable -enum class Axis(val value: String) { - @SerialName("horizontal") HORIZONTAL("horizontal"), - @SerialName("vertical") VERTICAL("vertical"); +public enum class Axis(public val value: String) { + @SerialName("horizontal") + HORIZONTAL("horizontal"), + + @SerialName("vertical") + VERTICAL("vertical"); } /** @@ -113,10 +146,15 @@ enum class Axis(val value: String) { */ @ExperimentalReadiumApi @Serializable -enum class Spread(val value: String) { - @SerialName("auto") AUTO("auto"), - @SerialName("never") NEVER("never"), - @SerialName("always") ALWAYS("always"); +public enum class Spread(public val value: String) { + @SerialName("auto") + AUTO("auto"), + + @SerialName("never") + NEVER("never"), + + @SerialName("always") + ALWAYS("always"); } /** @@ -124,9 +162,12 @@ enum class Spread(val value: String) { */ @ExperimentalReadiumApi @Serializable -enum class ReadingProgression(val value: String) { - @SerialName("ltr") LTR("ltr"), - @SerialName("rtl") RTL("rtl"); +public enum class ReadingProgression(public val value: String) { + @SerialName("ltr") + LTR("ltr"), + + @SerialName("rtl") + RTL("rtl"); } /** @@ -134,9 +175,16 @@ enum class ReadingProgression(val value: String) { */ @ExperimentalReadiumApi @Serializable -enum class Fit(val value: String) { - @SerialName("cover") COVER("cover"), - @SerialName("contain") CONTAIN("contain"), - @SerialName("width") WIDTH("width"), - @SerialName("height") HEIGHT("height"); +public enum class Fit(public val value: String) { + @SerialName("cover") + COVER("cover"), + + @SerialName("contain") + CONTAIN("contain"), + + @SerialName("width") + WIDTH("width"), + + @SerialName("height") + HEIGHT("height"); } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/tts/AndroidTtsEngine.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/tts/AndroidTtsEngine.kt deleted file mode 100644 index cfe696fb59..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/tts/AndroidTtsEngine.kt +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.tts - -import android.content.Context -import android.content.Intent -import android.speech.tts.TextToSpeech -import android.speech.tts.UtteranceProgressListener -import android.speech.tts.Voice as AndroidVoice -import java.util.* -import kotlin.Exception -import kotlin.coroutines.resume -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.onFailure -import org.readium.r2.navigator.tts.TtsEngine.Voice -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.extensions.tryOrLog -import org.readium.r2.shared.util.Language -import org.readium.r2.shared.util.Try - -/** - * Default [TtsEngine] implementation using Android's native text to speech engine. - */ -@ExperimentalReadiumApi -class AndroidTtsEngine( - context: Context, - private val listener: TtsEngine.Listener -) : TtsEngine { - - /** - * Android's TTS error code. - * See https://developer.android.com/reference/android/speech/tts/TextToSpeech#ERROR - */ - enum class EngineError(val code: Int) { - /** Denotes a generic operation failure. */ - Unknown(-1), - /** Denotes a failure caused by an invalid request. */ - InvalidRequest(-8), - /** Denotes a failure caused by a network connectivity problems. */ - Network(-6), - /** Denotes a failure caused by network timeout. */ - NetworkTimeout(-7), - /** Denotes a failure caused by an unfinished download of the voice data. */ - NotInstalledYet(-9), - /** Denotes a failure related to the output (audio device or a file). */ - Output(-5), - /** Denotes a failure of a TTS service. */ - Service(-4), - /** Denotes a failure of a TTS engine to synthesize the given input. */ - Synthesis(-3); - - companion object { - fun getOrDefault(key: Int): EngineError = - values() - .firstOrNull { it.code == key } - ?: Unknown - } - } - - class EngineException(code: Int) : Exception("Android TTS engine error: $code") { - val error: EngineError = - EngineError.getOrDefault(code) - } - - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - - /** - * Utterances to be synthesized, in order of [speak] calls. - */ - private val tasks = Channel<UtteranceTask>(Channel.BUFFERED) - - /** Future completed when the [engine] is fully initialized. */ - private val init = CompletableDeferred<Unit>() - - init { - scope.launch { - init.await() - - for (task in tasks) { - ensureActive() - task.run() - } - } - } - - override val rateMultiplierRange: ClosedRange<Double> = 0.1..4.0 - - override var availableVoices: List<Voice> = emptyList() - private set(value) { - field = value - listener.onAvailableVoicesChange(value) - } - - override suspend fun close() { - scope.cancel() - tasks.cancel() - engine.shutdown() - } - - override suspend fun speak( - utterance: TtsEngine.Utterance, - onSpeakRange: (IntRange) -> Unit - ): TtsTry<Unit> = - suspendCancellableCoroutine { cont -> - val result = tasks.trySend( - UtteranceTask( - utterance = utterance, - continuation = cont, - onSpeakRange = onSpeakRange - ) - ) - - result.onFailure { - listener.onEngineError( - TtsEngine.Exception.Other(IllegalStateException("Failed to schedule a new utterance task")) - ) - } - } - - /** - * Start the activity to install additional language data. - * To be called if you receive a [TtsEngine.Exception.LanguageSupportIncomplete] error. - * - * Returns whether the request was successful. - * - * See https://android-developers.googleblog.com/2009/09/introduction-to-text-to-speech-in.html - */ - fun requestInstallMissingVoice( - context: Context, - intentFlags: Int = Intent.FLAG_ACTIVITY_NEW_TASK - ): Boolean { - val intent = Intent() - .setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA) - .setFlags(intentFlags) - - if (context.packageManager.queryIntentActivities(intent, 0).isEmpty()) { - return false - } - - context.startActivity(intent) - return true - } - - // Engine - - /** Underlying Android [TextToSpeech] engine. */ - private val engine = TextToSpeech(context, EngineInitListener()) - - private inner class EngineInitListener : TextToSpeech.OnInitListener { - override fun onInit(status: Int) { - if (status == TextToSpeech.SUCCESS) { - scope.launch { - tryOrLog { - availableVoices = engine.voices.map { it.toVoice() } - } - init.complete(Unit) - } - } else { - listener.onEngineError(TtsEngine.Exception.InitializationFailed()) - } - } - } - - /** - * Holds a single utterance to be synthesized and the continuation for the [speak] call. - */ - private inner class UtteranceTask( - val utterance: TtsEngine.Utterance, - val continuation: CancellableContinuation<TtsTry<Unit>>, - val onSpeakRange: (IntRange) -> Unit, - ) { - fun run() { - if (!continuation.isActive) return - - // Interrupt the engine when the task is cancelled. - continuation.invokeOnCancellation { - tryOrLog { - engine.stop() - engine.setOnUtteranceProgressListener(null) - } - } - - try { - val id = UUID.randomUUID().toString() - engine.setup() - engine.setOnUtteranceProgressListener(Listener(id)) - engine.speak(utterance.text, TextToSpeech.QUEUE_FLUSH, null, id) - } catch (e: Exception) { - finish(TtsEngine.Exception.wrap(e)) - } - } - - /** - * Terminates this task. - */ - private fun finish(error: TtsEngine.Exception? = null) { - continuation.resume( - error?.let { Try.failure(error) } - ?: Try.success(Unit) - ) - } - - /** - * Setups the [engine] using the [utterance]'s configuration. - */ - private fun TextToSpeech.setup() { - setSpeechRate(utterance.rateMultiplier.toFloat()) - - utterance.voiceOrLanguage - .onLeft { voice -> - // Setup the user selected voice. - engine.voice = engine.voices - .firstOrNull { it.name == voice.id } - ?: throw IllegalStateException("Unknown Android voice: ${voice.id}") - } - .onRight { language -> - // Or fallback on the language. - val localeResult = engine.setLanguage(language.locale) - if (localeResult < TextToSpeech.LANG_AVAILABLE) { - if (localeResult == TextToSpeech.LANG_MISSING_DATA) - throw TtsEngine.Exception.LanguageSupportIncomplete(language) - else - throw TtsEngine.Exception.LanguageNotSupported(language) - } - } - } - - inner class Listener(val id: String) : UtteranceProgressListener() { - override fun onStart(utteranceId: String?) {} - - override fun onStop(utteranceId: String?, interrupted: Boolean) { - require(utteranceId == id) - finish() - } - - override fun onDone(utteranceId: String?) { - require(utteranceId == id) - finish() - } - - @Deprecated("Deprecated in the interface", ReplaceWith("onError(utteranceId, -1)")) - override fun onError(utteranceId: String?) { - onError(utteranceId, -1) - } - - override fun onError(utteranceId: String?, errorCode: Int) { - require(utteranceId == id) - - val error = EngineException(errorCode) - finish( - when (error.error) { - EngineError.Network, EngineError.NetworkTimeout -> - TtsEngine.Exception.Network(error) - EngineError.NotInstalledYet -> - TtsEngine.Exception.LanguageSupportIncomplete(utterance.language, cause = error) - else -> TtsEngine.Exception.Other(error) - } - ) - } - - override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) { - require(utteranceId == id) - onSpeakRange(start until end) - } - } - } -} - -@OptIn(ExperimentalReadiumApi::class) -private fun AndroidVoice.toVoice(): Voice = - Voice( - id = name, - name = null, - language = Language(locale), - quality = when (quality) { - AndroidVoice.QUALITY_VERY_HIGH -> Voice.Quality.Highest - AndroidVoice.QUALITY_HIGH -> Voice.Quality.High - AndroidVoice.QUALITY_LOW -> Voice.Quality.Low - AndroidVoice.QUALITY_VERY_LOW -> Voice.Quality.Lowest - else -> Voice.Quality.Normal - }, - requiresNetwork = isNetworkConnectionRequired - ) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/tts/PublicationSpeechSynthesizer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/tts/PublicationSpeechSynthesizer.kt deleted file mode 100644 index d96c6c5ae5..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/tts/PublicationSpeechSynthesizer.kt +++ /dev/null @@ -1,556 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.tts - -import android.content.Context -import java.util.* -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import org.readium.r2.shared.DelicateReadiumApi -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.extensions.tryOrLog -import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.services.content.Content -import org.readium.r2.shared.publication.services.content.ContentTokenizer -import org.readium.r2.shared.publication.services.content.TextContentTokenizer -import org.readium.r2.shared.publication.services.content.content -import org.readium.r2.shared.util.* -import org.readium.r2.shared.util.tokenizer.TextUnit - -/** - * [PublicationSpeechSynthesizer] orchestrates the rendition of a [publication] by iterating through - * its content, splitting it into individual utterances using a [ContentTokenizer], then using a - * [TtsEngine] to read them aloud. - * - * Don't forget to call [close] when you are done using the [PublicationSpeechSynthesizer]. - */ -@OptIn(DelicateReadiumApi::class) -@ExperimentalReadiumApi -@Deprecated("The API described in this guide will be changed in the next version of the Kotlin toolkit to support background TTS playback and media notifications. It is recommended that you wait before integrating it in your app.") -class PublicationSpeechSynthesizer<E : TtsEngine> private constructor( - private val publication: Publication, - config: Configuration, - engineFactory: (listener: TtsEngine.Listener) -> E, - private val tokenizerFactory: (defaultLanguage: Language?) -> ContentTokenizer, - var listener: Listener? = null, -) : SuspendingCloseable { - - companion object { - - /** - * Creates a [PublicationSpeechSynthesizer] using the default native [AndroidTtsEngine]. - * - * @param publication Publication which will be iterated through and synthesized. - * @param config Initial TTS configuration. - * @param tokenizerFactory Factory to create a [ContentTokenizer] which will be used to - * split each [Content.Element] item into smaller chunks. Splits by sentences by default. - * @param listener Optional callbacks listener. - */ - operator fun invoke( - context: Context, - publication: Publication, - config: Configuration = Configuration(), - tokenizerFactory: (defaultLanguage: Language?) -> ContentTokenizer = defaultTokenizerFactory, - listener: Listener? = null, - ): PublicationSpeechSynthesizer<AndroidTtsEngine>? = invoke( - publication, - config = config, - engineFactory = { AndroidTtsEngine(context, listener = it) }, - tokenizerFactory = tokenizerFactory, - listener = listener - ) - - /** - * Creates a [PublicationSpeechSynthesizer] using a custom [TtsEngine]. - * - * @param publication Publication which will be iterated through and synthesized. - * @param config Initial TTS configuration. - * @param engineFactory Factory to create an instance of [TtsEngine]. - * @param tokenizerFactory Factory to create a [ContentTokenizer] which will be used to - * split each [Content.Element] item into smaller chunks. Splits by sentences by default. - * @param listener Optional callbacks listener. - */ - operator fun <E : TtsEngine> invoke( - publication: Publication, - config: Configuration = Configuration(), - engineFactory: (TtsEngine.Listener) -> E, - tokenizerFactory: (defaultLanguage: Language?) -> ContentTokenizer = defaultTokenizerFactory, - listener: Listener? = null, - ): PublicationSpeechSynthesizer<E>? { - if (!canSpeak(publication)) return null - - return PublicationSpeechSynthesizer(publication, config, engineFactory, tokenizerFactory, listener) - } - - /** - * The default content tokenizer will split the [Content.Element] items into individual sentences. - */ - val defaultTokenizerFactory: (Language?) -> ContentTokenizer = { language -> - TextContentTokenizer( - language = language, - unit = TextUnit.Sentence - ) - } - - /** - * Returns whether the [publication] can be played with a [PublicationSpeechSynthesizer]. - */ - fun canSpeak(publication: Publication): Boolean = - publication.content() != null - } - - @ExperimentalReadiumApi - interface Listener { - /** Called when an [error] occurs while speaking [utterance]. */ - fun onUtteranceError(utterance: Utterance, error: Exception) - - /** Called when a global [error] occurs. */ - fun onError(error: Exception) - } - - @ExperimentalReadiumApi - sealed class Exception private constructor( - override val message: String, - cause: Throwable? = null - ) : kotlin.Exception(message, cause) { - - /** Underlying [TtsEngine] error. */ - class Engine(val error: TtsEngine.Exception) : - Exception(error.message, error) - } - - /** - * An utterance is an arbitrary text (e.g. sentence) extracted from the [publication], that can - * be synthesized by the TTS [engine]. - * - * @param text Text to be spoken. - * @param locator Locator to the utterance in the [publication]. - * @param language Language of this utterance, if it differs from the default publication - * language. - */ - @ExperimentalReadiumApi - data class Utterance( - val text: String, - val locator: Locator, - val language: Language?, - ) - - /** - * Represents a state of the [PublicationSpeechSynthesizer]. - */ - sealed class State { - /** The [PublicationSpeechSynthesizer] is completely stopped and must be (re)started from a given locator. */ - object Stopped : State() - - /** The [PublicationSpeechSynthesizer] is paused at the given utterance. */ - data class Paused(val utterance: Utterance) : State() - - /** - * The TTS engine is synthesizing [utterance]. - * - * [range] will be regularly updated while the [utterance] is being played. - */ - data class Playing(val utterance: Utterance, val range: Locator? = null) : State() - } - - private val _state = MutableStateFlow<State>(State.Stopped) - - /** - * Current state of the [PublicationSpeechSynthesizer]. - */ - val state: StateFlow<State> = _state.asStateFlow() - - private val scope = MainScope() - - init { - require(canSpeak(publication)) { - "The content of the publication cannot be synthesized, as it is not iterable" - } - } - - /** - * Underlying [TtsEngine] instance. - * - * WARNING: Don't control the playback or set the config directly with the engine. Use the - * [PublicationSpeechSynthesizer] APIs instead. This property is used to access engine-specific APIs such as - * [AndroidTtsEngine.requestInstallMissingVoice]. - */ - @DelicateReadiumApi - val engine: E by lazy { - engineFactory(object : TtsEngine.Listener { - override fun onEngineError(error: TtsEngine.Exception) { - listener?.onError(Exception.Engine(error)) - stop() - } - - override fun onAvailableVoicesChange(voices: List<TtsEngine.Voice>) { - _availableVoices.value = voices - } - }) - } - - /** - * Interrupts the [TtsEngine] and closes this [PublicationSpeechSynthesizer]. - */ - override suspend fun close() { - tryOrLog { - scope.cancel() - if (::engine.isLazyInitialized) { - engine.close() - } - } - } - - /** - * User configuration for the text-to-speech engine. - * - * @param defaultLanguage Language overriding the publication one. - * @param voiceId Identifier for the voice used to speak the utterances. - * @param rateMultiplier Multiplier for the voice speech rate. Normal is 1.0. See [rateMultiplierRange] - * for the range of values supported by the [TtsEngine]. - * @param extras Extensibility for custom TTS engines. - */ - @ExperimentalReadiumApi - data class Configuration( - val defaultLanguage: Language? = null, - val voiceId: String? = null, - val rateMultiplier: Double = 1.0, - val extras: Any? = null - ) - - private val _config = MutableStateFlow(config) - - /** - * Current user configuration. - */ - val config: StateFlow<Configuration> = _config.asStateFlow() - - /** - * Updates the user configuration. - * - * The change is not immediate, it will be applied for the next utterance. - */ - fun setConfig(config: Configuration) { - _config.value = config.copy( - rateMultiplier = config.rateMultiplier.coerceIn(engine.rateMultiplierRange), - ) - } - - /** - * Range for the speech rate multiplier. Normal is 1.0. - */ - val rateMultiplierRange: ClosedRange<Double> - get() = engine.rateMultiplierRange - - private val _availableVoices = MutableStateFlow<List<TtsEngine.Voice>>(emptyList()) - - /** - * List of synthesizer voices supported by the TTS [engine]. - */ - val availableVoices: StateFlow<List<TtsEngine.Voice>> = _availableVoices.asStateFlow() - - /** - * Returns the first voice with the given [id] supported by the TTS [engine]. - * - * This can be used to restore the user selected voice after storing it in the shared - * preferences. - */ - fun voiceWithId(id: String): TtsEngine.Voice? { - val voice = lastUsedVoice?.takeIf { it.id == id } - ?: engine.voiceWithId(id) - ?: return null - - lastUsedVoice = voice - return voice - } - - /** - * Cache for the last requested voice, for performance. - */ - private var lastUsedVoice: TtsEngine.Voice? = null - - /** - * (Re)starts the TTS from the given locator or the beginning of the publication. - */ - fun start(fromLocator: Locator? = null) { - replacePlaybackJob { - publicationIterator = publication.content(fromLocator)?.iterator() - playNextUtterance(Direction.Forward) - } - } - - /** - * Stops the synthesizer. - * - * Use [start] to restart it. - */ - fun stop() { - replacePlaybackJob { - _state.value = State.Stopped - publicationIterator = null - } - } - - /** - * Interrupts a played utterance. - * - * Use [resume] to restart the playback from the same utterance. - */ - fun pause() { - replacePlaybackJob { - _state.update { state -> - when (state) { - is State.Playing -> State.Paused(state.utterance) - else -> state - } - } - } - } - - /** - * Resumes an utterance interrupted with [pause]. - */ - fun resume() { - replacePlaybackJob { - (state.value as? State.Paused)?.let { paused -> - play(paused.utterance) - } - } - } - - /** - * Pauses or resumes the playback of the current utterance. - */ - fun pauseOrResume() { - when (state.value) { - is State.Stopped -> return - is State.Playing -> pause() - is State.Paused -> resume() - } - } - - /** - * Skips to the previous utterance. - */ - fun previous() { - replacePlaybackJob { - playNextUtterance(Direction.Backward) - } - } - - /** - * Skips to the next utterance. - */ - fun next() { - replacePlaybackJob { - playNextUtterance(Direction.Forward) - } - } - - /** - * [Content.Iterator] used to iterate through the [publication]. - */ - private var publicationIterator: Content.Iterator? = null - set(value) { - field = value - utterances = CursorList() - } - - /** - * Utterances for the current publication [Content.Element] item. - */ - private var utterances: CursorList<Utterance> = CursorList() - - /** - * Plays the next utterance in the given [direction]. - */ - private suspend fun playNextUtterance(direction: Direction) { - val utterance = nextUtterance(direction) - if (utterance == null) { - _state.value = State.Stopped - return - } - play(utterance) - } - - /** - * Plays the given [utterance] with the TTS [engine]. - */ - private suspend fun play(utterance: Utterance) { - _state.value = State.Playing(utterance) - - engine - .speak( - utterance = TtsEngine.Utterance( - text = utterance.text, - rateMultiplier = config.value.rateMultiplier, - voiceOrLanguage = utterance.voiceOrLanguage() - ), - onSpeakRange = { range -> - _state.value = State.Playing( - utterance = utterance, - range = utterance.locator.copy( - text = utterance.locator.text.substring(range) - ) - ) - } - ) - .onSuccess { - playNextUtterance(Direction.Forward) - } - .onFailure { - _state.value = State.Paused(utterance) - listener?.onUtteranceError(utterance, Exception.Engine(it)) - } - } - - /** - * Returns the user selected voice if it's compatible with the utterance language. Otherwise, - * falls back on the languages. - */ - private fun Utterance.voiceOrLanguage(): Either<TtsEngine.Voice, Language> { - // User selected voice, if it's compatible with the utterance language. - // Or fallback on the languages. - val voice = config.value.voiceId - ?.let { voiceWithId(it) } - ?.takeIf { language == null || it.language.removeRegion() == language.removeRegion() } - - return Either( - voice - ?: language - ?: config.value.defaultLanguage - ?: publication.metadata.language - ?: Language(Locale.getDefault()) - ) - } - - /** - * Gets the next utterance in the given [direction], or null when reaching the beginning or the - * end. - */ - private suspend fun nextUtterance(direction: Direction): Utterance? { - val utterance = utterances.nextIn(direction) - if (utterance == null && loadNextUtterances(direction)) { - return nextUtterance(direction) - } - return utterance - } - - /** - * Loads the utterances for the next publication [Content.Element] item in the given [direction]. - */ - private suspend fun loadNextUtterances(direction: Direction): Boolean { - val content = publicationIterator?.nextIn(direction) - ?: return false - - val nextUtterances = content - .tokenize() - .flatMap { it.utterances() } - - if (nextUtterances.isEmpty()) { - return loadNextUtterances(direction) - } - - utterances = CursorList( - list = nextUtterances, - index = when (direction) { - Direction.Forward -> -1 - Direction.Backward -> nextUtterances.size - } - ) - - return true - } - - /** - * Splits a publication [Content.Element] item into smaller chunks using the provided tokenizer. - * - * This is used to split a paragraph into sentences, for example. - */ - private fun Content.Element.tokenize(): List<Content.Element> = - tokenizerFactory(config.value.defaultLanguage ?: publication.metadata.language) - .tokenize(this) - - /** - * Splits a publication [Content.Element] item into the utterances to be spoken. - */ - private fun Content.Element.utterances(): List<Utterance> { - fun utterance(text: String, locator: Locator, language: Language? = null): Utterance? { - if (!text.any { it.isLetterOrDigit() }) - return null - - return Utterance( - text = text, - locator = locator, - language = language - // If the language is the same as the one declared globally in the publication, - // we omit it. This way, the app can customize the default language used in the - // configuration. - ?.takeIf { it != publication.metadata.language } - ) - } - - return when (this) { - is Content.TextElement -> { - segments.mapNotNull { segment -> - utterance( - text = segment.text, - locator = segment.locator, - language = segment.language - ) - } - } - - is Content.TextualElement -> { - listOfNotNull( - text - ?.takeIf { it.isNotBlank() } - ?.let { utterance(text = it, locator = locator) } - ) - } - - else -> emptyList() - } - } - - /** - * Cancels the previous playback-related job and starts a new one with the given suspending - * [block]. - * - * This is used to interrupt on-going commands. - */ - private fun replacePlaybackJob(block: suspend CoroutineScope.() -> Unit) { - scope.launch { - playbackJob?.cancelAndJoin() - playbackJob = launch { - block() - } - } - } - - private var playbackJob: Job? = null - - private enum class Direction { - Forward, Backward; - } - - private fun <E> CursorList<E>.nextIn(direction: Direction): E? = - when (direction) { - Direction.Forward -> next() - Direction.Backward -> previous() - } - - private suspend fun Content.Iterator.nextIn(direction: Direction): Content.Element? = - when (direction) { - Direction.Forward -> nextOrNull() - Direction.Backward -> previousOrNull() - } -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/tts/TtsEngine.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/tts/TtsEngine.kt deleted file mode 100644 index ae3a454f97..0000000000 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/tts/TtsEngine.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.navigator.tts - -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.util.Either -import org.readium.r2.shared.util.Language -import org.readium.r2.shared.util.SuspendingCloseable -import org.readium.r2.shared.util.Try - -@ExperimentalReadiumApi -typealias TtsTry<SuccessT> = Try<SuccessT, TtsEngine.Exception> - -/** - * A text-to-speech engine synthesizes text utterances (e.g. sentence). - * - * Implement this interface to support third-party engines with [PublicationSpeechSynthesizer]. - */ -@ExperimentalReadiumApi -interface TtsEngine : SuspendingCloseable { - - @ExperimentalReadiumApi - sealed class Exception private constructor( - override val message: String, - cause: Throwable? = null - ) : kotlin.Exception(message, cause) { - /** Failed to initialize the TTS engine. */ - class InitializationFailed(cause: Throwable? = null) : - Exception("The TTS engine failed to initialize", cause) - - /** Tried to synthesize an utterance with an unsupported language. */ - class LanguageNotSupported(val language: Language, cause: Throwable? = null) : - Exception("The language ${language.code} is not supported by the TTS engine", cause) - - /** The selected language is missing downloadable data. */ - class LanguageSupportIncomplete(val language: Language, cause: Throwable? = null) : - Exception("The language ${language.code} requires additional files by the TTS engine", cause) - - /** Error during network calls. */ - class Network(cause: Throwable? = null) : - Exception("A network error occurred", cause) - - /** Other engine-specific errors. */ - class Other(override val cause: Throwable) : - Exception(cause.message ?: "An unknown error occurred", cause) - - companion object { - fun wrap(e: Throwable): Exception = when (e) { - is Exception -> e - else -> Other(e) - } - } - } - - /** - * TTS engine callbacks. - */ - @ExperimentalReadiumApi - interface Listener { - /** - * Called when a general engine error occurred. - */ - fun onEngineError(error: Exception) - - /** - * Called when the list of available voices is updated. - */ - fun onAvailableVoicesChange(voices: List<Voice>) - } - - /** - * An utterance is an arbitrary text (e.g. sentence) that can be synthesized by the TTS engine. - * - * @param text Text to be spoken. - * @param rateMultiplier Multiplier for the speech rate. - * @param voiceOrLanguage Either an explicit voice or the language of the text. If a language - * is provided, the default voice for this language will be used. - */ - @ExperimentalReadiumApi - data class Utterance( - val text: String, - val rateMultiplier: Double, - val voiceOrLanguage: Either<Voice, Language> - ) { - val language: Language = - when (val vl = voiceOrLanguage) { - is Either.Left -> vl.value.language - is Either.Right -> vl.value - } - } - - /** - * Represents a voice provided by the TTS engine which can speak an utterance. - * - * @param id Unique and stable identifier for this voice. Can be used to store and retrieve the - * voice from the user preferences. - * @param name Human-friendly name for this voice, when available. - * @param language Language (and region) this voice belongs to. - * @param quality Voice quality. - * @param requiresNetwork Indicates whether using this voice requires an Internet connection. - */ - @ExperimentalReadiumApi - data class Voice( - val id: String, - val name: String? = null, - val language: Language, - val quality: Quality = Quality.Normal, - val requiresNetwork: Boolean = false, - ) { - enum class Quality { - Lowest, Low, Normal, High, Highest - } - } - - /** - * Synthesizes the given [utterance] and returns its status. - * - * [onSpeakRange] is called repeatedly while the engine plays portions (e.g. words) of the - * utterance. - * - * To interrupt the utterance, cancel the parent coroutine job. - */ - suspend fun speak( - utterance: Utterance, - onSpeakRange: (IntRange) -> Unit = { _ -> } - ): TtsTry<Unit> - - /** - * Supported range for the speech rate multiplier. - */ - val rateMultiplierRange: ClosedRange<Double> - - /** - * List of available synthesizer voices. - * - * Implement [Listener.onAvailableVoicesChange] to be aware of changes in the available voices. - */ - val availableVoices: List<Voice> - - /** - * Returns the voice with given identifier, if it exists. - */ - fun voiceWithId(id: String): Voice? = - availableVoices.firstOrNull { it.id == id } -} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/util/BaseActionModeCallback.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/util/BaseActionModeCallback.kt index 5c71d29144..ce75e7124c 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/util/BaseActionModeCallback.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/util/BaseActionModeCallback.kt @@ -14,7 +14,7 @@ import android.view.MenuItem * A convenient base implementation of [ActionMode.Callback], when you don't need to override all * methods. */ -abstract class BaseActionModeCallback : ActionMode.Callback { +public abstract class BaseActionModeCallback : ActionMode.Callback { override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = false override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean = false override fun onDestroyActionMode(mode: ActionMode) {} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/util/DirectionalNavigationAdapter.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/util/DirectionalNavigationAdapter.kt new file mode 100644 index 0000000000..1089cc7b4b --- /dev/null +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/util/DirectionalNavigationAdapter.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2021 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.navigator.util + +import org.readium.r2.navigator.OverflowableNavigator +import org.readium.r2.navigator.input.InputListener +import org.readium.r2.navigator.input.Key +import org.readium.r2.navigator.input.KeyEvent +import org.readium.r2.navigator.input.TapEvent +import org.readium.r2.navigator.preferences.ReadingProgression +import org.readium.r2.shared.ExperimentalReadiumApi + +/** + * Helper handling directional UI events (e.g. edge taps or arrow keys) to turn the pages of a + * VisualNavigator. + * + * This takes into account the reading progression of the navigator to turn pages in the right + * direction. + * + * Add it to a navigator with `addInputListener(DirectionalNavigationAdapter())`. + * + * @param tapEdges: Indicates which viewport edges handle taps. + * @param handleTapsWhileScrolling: Indicates whether the page turns should be handled when the + * publication is scrollable. + * @param minimumHorizontalEdgeSize: The minimum horizontal edge dimension triggering page turns, in + * pixels. + * @param horizontalEdgeThresholdPercent: The percentage of the viewport dimension used to compute + * the horizontal edge size. When null, minimumHorizontalEdgeSize will be used instead. + * @param minimumVerticalEdgeSize: The minimum vertical edge dimension triggering page turns, in + * pixels. + * @param verticalEdgeThresholdPercent: The percentage of the viewport dimension used to compute the + * vertical edge size. When null, minimumVerticalEdgeSize will be used instead. + * @param animatedTransition: Indicates whether the page turns should be animated. + */ +@ExperimentalReadiumApi +public class DirectionalNavigationAdapter( + private val navigator: OverflowableNavigator, + private val tapEdges: Set<TapEdge> = setOf(TapEdge.Horizontal), + private val handleTapsWhileScrolling: Boolean = false, + private val minimumHorizontalEdgeSize: Double = 80.0, + private val horizontalEdgeThresholdPercent: Double? = 0.3, + private val minimumVerticalEdgeSize: Double = 80.0, + private val verticalEdgeThresholdPercent: Double? = 0.3, + private val animatedTransition: Boolean = false +) : InputListener { + + /** + * Indicates which viewport edges trigger page turns on tap. + */ + public enum class TapEdge { + Horizontal, Vertical; + } + + override fun onTap(event: TapEvent): Boolean { + if (navigator.overflow.value.scroll && !handleTapsWhileScrolling) { + return false + } + + if (tapEdges.contains(TapEdge.Horizontal)) { + val width = navigator.publicationView.width.toDouble() + + val horizontalEdgeSize = horizontalEdgeThresholdPercent?.let { + maxOf(minimumHorizontalEdgeSize, it * width) + } ?: minimumHorizontalEdgeSize + val leftRange = 0.0..horizontalEdgeSize + val rightRange = (width - horizontalEdgeSize)..width + + if (event.point.x in rightRange) { + return navigator.goRight(animated = animatedTransition) + } else if (event.point.x in leftRange) { + return navigator.goLeft(animated = animatedTransition) + } + } + + if (tapEdges.contains(TapEdge.Vertical)) { + val height = navigator.publicationView.height.toDouble() + + val verticalEdgeSize = verticalEdgeThresholdPercent?.let { + maxOf(minimumVerticalEdgeSize, it * height) + } ?: minimumVerticalEdgeSize + val topRange = 0.0..verticalEdgeSize + val bottomRange = (height - verticalEdgeSize)..height + + if (event.point.y in bottomRange) { + return navigator.goForward(animated = animatedTransition) + } else if (event.point.y in topRange) { + return navigator.goBackward(animated = animatedTransition) + } + } + + return false + } + + override fun onKey(event: KeyEvent): Boolean { + if (event.type != KeyEvent.Type.Down || event.modifiers.isNotEmpty()) { + return false + } + + return when (event.key) { + Key.ArrowUp -> navigator.goBackward(animated = animatedTransition) + Key.ArrowDown, Key.Space -> navigator.goForward(animated = animatedTransition) + Key.ArrowLeft -> navigator.goLeft(animated = animatedTransition) + Key.ArrowRight -> navigator.goRight(animated = animatedTransition) + else -> false + } + } + + /** + * Moves to the left content portion (eg. page) relative to the reading progression direction. + */ + private fun OverflowableNavigator.goLeft(animated: Boolean = false): Boolean { + return when (overflow.value.readingProgression) { + ReadingProgression.LTR -> + goBackward(animated = animated) + + ReadingProgression.RTL -> + goForward(animated = animated) + } + } + + /** + * Moves to the right content portion (eg. page) relative to the reading progression direction. + */ + private fun OverflowableNavigator.goRight(animated: Boolean = false): Boolean { + return when (overflow.value.readingProgression) { + ReadingProgression.LTR -> + goForward(animated = animated) + + ReadingProgression.RTL -> + goBackward(animated = animated) + } + } +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/util/EdgeTapNavigation.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/util/EdgeTapNavigation.kt index 12ceec7569..63514b88a0 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/util/EdgeTapNavigation.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/util/EdgeTapNavigation.kt @@ -1,93 +1,8 @@ package org.readium.r2.navigator.util -import android.graphics.PointF -import android.view.View -import org.readium.r2.navigator.VisualNavigator -import org.readium.r2.navigator.preferences.Axis -import org.readium.r2.navigator.preferences.ReadingProgression -import org.readium.r2.shared.ExperimentalReadiumApi - -/** - * Convenience utility to handle page turns when tapping the edge of the screen. - * - * Call [EdgeTapNavigation.onTap] from the [VisualNavigator.Listener.onTap] callback to turn pages - * automatically. - * - * @param navigator Navigator used to turn pages. - * @param minimumEdgeSize The minimum edge dimension triggering page turns, in pixels. - * @param edgeThresholdPercent The percentage of the viewport dimension used to compute the edge - * dimension. When null, minimumEdgeSize will be used instead. - * @param animatedTransition Indicates whether the page turns should be animated. - */ -@OptIn(ExperimentalReadiumApi::class) -class EdgeTapNavigation( - private val navigator: VisualNavigator, - private val minimumEdgeSize: Double = 200.0, - private val edgeThresholdPercent: Double? = 0.3, - private val animatedTransition: Boolean = false, - private val handleTapsWhileScrolling: Boolean = false -) { - private enum class Transition { - FORWARD, BACKWARD, NONE; - - fun reverse() = when (this) { - FORWARD -> BACKWARD - BACKWARD -> FORWARD - NONE -> NONE - } - } - - /** - * Handles a tap in the navigator viewport and returns whether it was successful. - * - * To be called from [VisualNavigator.Listener.onTap]. - * - * @param view Navigator view from which the point is relative. - */ - fun onTap(point: PointF, view: View): Boolean { - val horizontalEdgeSize by lazy { - if (edgeThresholdPercent != null) - (edgeThresholdPercent * view.width).coerceAtLeast(minimumEdgeSize) - else minimumEdgeSize - } - val leftRange by lazy { 0.0..horizontalEdgeSize } - val rightRange by lazy { (view.width - horizontalEdgeSize)..view.width.toDouble() } - - val verticalEdgeSize by lazy { - if (edgeThresholdPercent != null) - (edgeThresholdPercent * view.height).coerceAtLeast(minimumEdgeSize) - else minimumEdgeSize - } - val topRange by lazy { 0.0..verticalEdgeSize } - val bottomRange by lazy { (view.height - verticalEdgeSize)..view.height.toDouble() } - - val presentation = navigator.presentation.value - - var transition: Transition = - when { - presentation.scroll && !handleTapsWhileScrolling -> - Transition.NONE - presentation.axis == Axis.HORIZONTAL -> - when { - rightRange.contains(point.x) -> Transition.FORWARD - leftRange.contains(point.x) -> Transition.BACKWARD - else -> Transition.NONE - } - else -> when { - bottomRange.contains(point.y) -> Transition.FORWARD - topRange.contains(point.y) -> Transition.BACKWARD - else -> Transition.NONE - } - } - - if (presentation.readingProgression == ReadingProgression.RTL) { - transition = transition.reverse() - } - - return when (transition) { - Transition.FORWARD -> navigator.goForward(animated = animatedTransition) - Transition.BACKWARD -> navigator.goBackward(animated = animatedTransition) - Transition.NONE -> false - } - } -} +@Deprecated( + "Replaced by `DirectionalNavigationAdapter`. See the migration guide.", + replaceWith = ReplaceWith("DirectionalNavigationAdapter"), + level = DeprecationLevel.ERROR +) +public class EdgeTapNavigation diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/util/FragmentFactory.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/util/FragmentFactory.kt index bb16b84fd5..ef8f34a1b1 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/util/FragmentFactory.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/util/FragmentFactory.kt @@ -12,20 +12,33 @@ import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.extensions.tryOrNull /** - * Creates a [FragmentFactory] for a single type of [Fragment] using the result of the given - * [factory] closure. + * A [FragmentFactory] which will instantiate a single type of [Fragment] using the result of the + * given [factory] closure. */ -@InternalReadiumApi -inline fun <reified T : Fragment> createFragmentFactory(crossinline factory: () -> T): FragmentFactory = object : FragmentFactory() { +public class SingleFragmentFactory<T : Fragment>( + public val fragmentClass: Class<T>, + private val factory: () -> T +) : FragmentFactory() { + + public operator fun invoke(): T = + factory() override fun instantiate(classLoader: ClassLoader, className: String): Fragment { return when (className) { - T::class.java.name -> factory() + fragmentClass.name -> factory() else -> super.instantiate(classLoader, className) } } } +/** + * Creates a [FragmentFactory] for a single type of [Fragment] using the result of the given + * [factory] closure. + */ +@InternalReadiumApi +public inline fun <reified T : Fragment> createFragmentFactory(noinline factory: () -> T): SingleFragmentFactory<T> = + SingleFragmentFactory(T::class.java, factory) + /** * A [FragmentFactory] which will iterate over a provided list of [factories] until finding one * instantiating successfully the requested [Fragment]. @@ -38,9 +51,9 @@ inline fun <reified T : Fragment> createFragmentFactory(crossinline factory: () * ``` */ @InternalReadiumApi -class CompositeFragmentFactory(private val factories: List<FragmentFactory>) : FragmentFactory() { +public class CompositeFragmentFactory(private val factories: List<FragmentFactory>) : FragmentFactory() { - constructor(vararg factories: FragmentFactory) : this(factories.toList()) + public constructor(vararg factories: FragmentFactory) : this(factories.toList()) override fun instantiate(classLoader: ClassLoader, className: String): Fragment { for (factory in factories) { diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/util/ViewModelFactory.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/util/ViewModelFactory.kt index d9369a0202..449ca59807 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/util/ViewModelFactory.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/util/ViewModelFactory.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.InternalReadiumApi * given [factory] closure. */ @InternalReadiumApi -inline fun <reified T : ViewModel> createViewModelFactory(crossinline factory: () -> T): ViewModelProvider.Factory = +public inline fun <reified T : ViewModel> createViewModelFactory(crossinline factory: () -> T): ViewModelProvider.Factory = object : ViewModelProvider.Factory { override fun <V : ViewModel> create(modelClass: Class<V>): V { diff --git a/readium/navigator/src/main/res/drawable/baseline_forward_10_white_24.png b/readium/navigator/src/main/res/drawable/baseline_forward_10_white_24.png deleted file mode 100755 index 4028f34ff9..0000000000 Binary files a/readium/navigator/src/main/res/drawable/baseline_forward_10_white_24.png and /dev/null differ diff --git a/readium/navigator/src/main/res/drawable/baseline_replay_10_white_24.png b/readium/navigator/src/main/res/drawable/baseline_replay_10_white_24.png deleted file mode 100755 index 653f545270..0000000000 Binary files a/readium/navigator/src/main/res/drawable/baseline_replay_10_white_24.png and /dev/null differ diff --git a/readium/navigator/src/main/res/drawable/ic_pause_white_24dp.png b/readium/navigator/src/main/res/drawable/ic_pause_white_24dp.png deleted file mode 100644 index 660ac65858..0000000000 Binary files a/readium/navigator/src/main/res/drawable/ic_pause_white_24dp.png and /dev/null differ diff --git a/readium/navigator/src/main/res/drawable/ic_play_arrow_white_24dp.png b/readium/navigator/src/main/res/drawable/ic_play_arrow_white_24dp.png deleted file mode 100644 index be5c062b5f..0000000000 Binary files a/readium/navigator/src/main/res/drawable/ic_play_arrow_white_24dp.png and /dev/null differ diff --git a/readium/navigator/src/main/res/drawable/ic_skip_next_white_24dp.png b/readium/navigator/src/main/res/drawable/ic_skip_next_white_24dp.png deleted file mode 100644 index 19c4929cca..0000000000 Binary files a/readium/navigator/src/main/res/drawable/ic_skip_next_white_24dp.png and /dev/null differ diff --git a/readium/navigator/src/main/res/drawable/ic_skip_previous_white_24dp.png b/readium/navigator/src/main/res/drawable/ic_skip_previous_white_24dp.png deleted file mode 100644 index f9186c0b63..0000000000 Binary files a/readium/navigator/src/main/res/drawable/ic_skip_previous_white_24dp.png and /dev/null differ diff --git a/readium/navigator/src/main/res/drawable/r2_media_notification_fastforward.xml b/readium/navigator/src/main/res/drawable/readium_media_notification_fastforward.xml similarity index 100% rename from readium/navigator/src/main/res/drawable/r2_media_notification_fastforward.xml rename to readium/navigator/src/main/res/drawable/readium_media_notification_fastforward.xml diff --git a/readium/navigator/src/main/res/drawable/r2_media_notification_rewind.xml b/readium/navigator/src/main/res/drawable/readium_media_notification_rewind.xml similarity index 100% rename from readium/navigator/src/main/res/drawable/r2_media_notification_rewind.xml rename to readium/navigator/src/main/res/drawable/readium_media_notification_rewind.xml diff --git a/readium/navigator/src/main/res/layout/activity_r2_audiobook.xml b/readium/navigator/src/main/res/layout/activity_r2_audiobook.xml deleted file mode 100644 index 78aef62609..0000000000 --- a/readium/navigator/src/main/res/layout/activity_r2_audiobook.xml +++ /dev/null @@ -1,166 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:id="@+id/constraint_view"> - - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/linearLayout" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="@color/colorPrimaryDark" - android:orientation="horizontal" - android:paddingBottom="8dp" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> - - <TextView - android:id="@+id/progressTime" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:text="@string/zero" - android:minWidth="40dp" - android:textColor="@android:color/white" - app:layout_constraintBottom_toBottomOf="@+id/chapterView" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@+id/chapterView" /> - - <TextView - android:id="@+id/chapterView" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginEnd="8dp" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:ellipsize="end" - android:gravity="center" - android:maxLines="1" - android:scrollHorizontally="true" - android:textColor="@android:color/white" - android:textSize="18sp" - app:layout_constraintEnd_toStartOf="@+id/chapterTime" - app:layout_constraintStart_toEndOf="@+id/progressTime" - app:layout_constraintTop_toTopOf="parent" - tools:text="Chapter" /> - - <TextView - android:id="@+id/chapterTime" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginEnd="16dp" - android:text="@string/zero" - android:minWidth="40dp" - android:textColor="@android:color/white" - app:layout_constraintBottom_toBottomOf="@+id/chapterView" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/chapterView" /> - - <SeekBar - android:id="@+id/seekBar" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginEnd="8dp" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:foregroundTint="@android:color/white" - android:progressTint="@android:color/white" - android:thumbTint="@android:color/white" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/chapterView" /> - - </androidx.constraintlayout.widget.ConstraintLayout> - - - <ImageView - android:id="@+id/imageView" - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_centerHorizontal="true" - android:adjustViewBounds="true" - android:cropToPadding="true" - android:scaleType="fitCenter" - app:layout_constraintBottom_toTopOf="@+id/controls" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/linearLayout" - android:background="@color/colorPrimaryDark" - android:contentDescription="TODO" /> - - - <LinearLayout - android:id="@+id/controls" - android:layout_width="0dp" - android:layout_height="70dp" - android:background="@color/colorPrimaryDark" - android:gravity="center_vertical|center_horizontal" - android:orientation="horizontal" - android:paddingBottom="10dp" - android:paddingTop="10dp" - android:weightSum="5" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent"> - - - - <ImageButton - android:id="@+id/prev_chapter" - android:layout_width="30dp" - android:layout_height="30dp" - android:layout_weight="1" - android:background="@android:color/transparent" - android:scaleType="fitCenter" - android:src="@drawable/ic_skip_previous_white_24dp" - android:contentDescription="TODO" /> - - <ImageButton - android:id="@+id/fast_back" - android:layout_width="30dp" - android:layout_height="30dp" - android:layout_weight="1" - android:background="@android:color/transparent" - android:scaleType="fitCenter" - android:src="@drawable/baseline_replay_10_white_24" - android:contentDescription="TODO" /> - - - <ImageButton - android:id="@+id/play_pause" - android:layout_width="50dp" - android:layout_height="50dp" - android:layout_weight="1" - android:background="@android:color/transparent" - android:scaleType="fitCenter" - android:src="@drawable/ic_play_arrow_white_24dp" - android:contentDescription="TODO" /> - - <ImageButton - android:id="@+id/fast_forward" - android:layout_width="30dp" - android:layout_height="30dp" - android:layout_weight="1" - android:background="@android:color/transparent" - android:scaleType="fitCenter" - android:src="@drawable/baseline_forward_10_white_24" - android:contentDescription="TODO" /> - - <ImageButton - android:id="@+id/next_chapter" - android:layout_width="30dp" - android:layout_height="30dp" - android:layout_weight="1" - android:background="@android:color/transparent" - android:scaleType="fitCenter" - android:src="@drawable/ic_skip_next_white_24dp" - android:contentDescription="TODO" /> - - - </LinearLayout> - -</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/readium/navigator/src/main/res/layout/activity_r2_divina.xml b/readium/navigator/src/main/res/layout/activity_r2_divina.xml deleted file mode 100644 index b4c2533157..0000000000 --- a/readium/navigator/src/main/res/layout/activity_r2_divina.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <org.readium.r2.navigator.R2BasicWebView - android:id="@+id/divinaWebView" - android:layerType="hardware" - android:layout_width="match_parent" - android:layout_height="match_parent"/> - -</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/readium/navigator/src/main/res/layout/activity_r2_epub.xml b/readium/navigator/src/main/res/layout/activity_r2_epub.xml deleted file mode 100644 index dbb9666632..0000000000 --- a/readium/navigator/src/main/res/layout/activity_r2_epub.xml +++ /dev/null @@ -1,31 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <TextView - android:id="@+id/book_title" - android:layout_width="0dp" - android:layout_height="100dp" - android:gravity="center" - android:visibility="gone" - app:layout_constraintBottom_toTopOf="@+id/epub_navigator" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <androidx.fragment.app.FragmentContainerView - android:id="@+id/epub_navigator" - android:tag="@string/epub_navigator_tag" - android:name="org.readium.r2.navigator.epub.EpubNavigatorFragment" - android:layout_width="0dp" - android:layout_height="0dp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/book_title" - /> - -</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/readium/navigator/src/main/res/layout/activity_r2_image.xml b/readium/navigator/src/main/res/layout/activity_r2_image.xml deleted file mode 100644 index dd2fd71bee..0000000000 --- a/readium/navigator/src/main/res/layout/activity_r2_image.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout - xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="match_parent"> - - <androidx.fragment.app.FragmentContainerView - android:id="@+id/image_navigator" - android:tag="@string/image_navigator_tag" - android:name="org.readium.r2.navigator.image.ImageNavigatorFragment" - android:layout_width="match_parent" - android:layout_height="match_parent" - /> - -</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/readium/navigator/src/main/res/layout/item_spinner_font.xml b/readium/navigator/src/main/res/layout/item_spinner_font.xml deleted file mode 100644 index 33ab3f29ae..0000000000 --- a/readium/navigator/src/main/res/layout/item_spinner_font.xml +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - -<TextView xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="end" - android:textSize="12sp" /> \ No newline at end of file diff --git a/readium/navigator/src/main/res/layout/fragment_fxllayout_double.xml b/readium/navigator/src/main/res/layout/readium_navigator_fragment_fxllayout_double.xml similarity index 79% rename from readium/navigator/src/main/res/layout/fragment_fxllayout_double.xml rename to readium/navigator/src/main/res/layout/readium_navigator_fragment_fxllayout_double.xml index b26c22522d..88209a3e56 100644 --- a/readium/navigator/src/main/res/layout/fragment_fxllayout_double.xml +++ b/readium/navigator/src/main/res/layout/readium_navigator_fragment_fxllayout_double.xml @@ -1,15 +1,14 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <org.readium.r2.navigator.epub.fxl.R2FXLLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/r2FXLLayout" + android:focusable="false" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -17,11 +16,13 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" + android:focusable="false" android:scrollbars="none"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" + android:focusable="false" android:layout_gravity="center"> <LinearLayout @@ -30,6 +31,7 @@ android:layout_centerInParent="true" android:gravity="center_horizontal" android:orientation="horizontal" + android:focusable="false" tools:ignore="UselessParent"> <org.readium.r2.navigator.R2BasicWebView @@ -46,7 +48,8 @@ android:layout_height="wrap_content" android:layout_gravity="start" android:layout_weight="1" - android:layerType="hardware" /> + android:layerType="hardware" + /> </LinearLayout> </RelativeLayout> diff --git a/readium/navigator/src/main/res/layout/fragment_fxllayout_single.xml b/readium/navigator/src/main/res/layout/readium_navigator_fragment_fxllayout_single.xml similarity index 78% rename from readium/navigator/src/main/res/layout/fragment_fxllayout_single.xml rename to readium/navigator/src/main/res/layout/readium_navigator_fragment_fxllayout_single.xml index 1ffae6ce46..ee6c96a986 100644 --- a/readium/navigator/src/main/res/layout/fragment_fxllayout_single.xml +++ b/readium/navigator/src/main/res/layout/readium_navigator_fragment_fxllayout_single.xml @@ -1,15 +1,14 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <org.readium.r2.navigator.epub.fxl.R2FXLLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/r2FXLLayout" + android:focusable="false" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -17,11 +16,13 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" + android:focusable="false" android:scrollbars="none"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" + android:focusable="false" android:layout_gravity="center"> <LinearLayout @@ -30,6 +31,7 @@ android:layout_centerInParent="true" android:gravity="center_horizontal" android:orientation="horizontal" + android:focusable="false" tools:ignore="UselessParent"> <org.readium.r2.navigator.R2BasicWebView diff --git a/readium/navigator/src/main/res/layout/popup_footnote.xml b/readium/navigator/src/main/res/layout/readium_navigator_popup_footnote.xml similarity index 100% rename from readium/navigator/src/main/res/layout/popup_footnote.xml rename to readium/navigator/src/main/res/layout/readium_navigator_popup_footnote.xml diff --git a/readium/navigator/src/main/res/layout/activity_r2_viewpager.xml b/readium/navigator/src/main/res/layout/readium_navigator_viewpager.xml similarity index 69% rename from readium/navigator/src/main/res/layout/activity_r2_viewpager.xml rename to readium/navigator/src/main/res/layout/readium_navigator_viewpager.xml index 056126cda9..b038e98b22 100644 --- a/readium/navigator/src/main/res/layout/activity_r2_viewpager.xml +++ b/readium/navigator/src/main/res/layout/readium_navigator_viewpager.xml @@ -1,12 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" diff --git a/readium/navigator/src/main/res/layout/viewpager_fragment_cbz.xml b/readium/navigator/src/main/res/layout/readium_navigator_viewpager_fragment_cbz.xml similarity index 71% rename from readium/navigator/src/main/res/layout/viewpager_fragment_cbz.xml rename to readium/navigator/src/main/res/layout/readium_navigator_viewpager_fragment_cbz.xml index e2b4d4acc5..bd38fe8182 100755 --- a/readium/navigator/src/main/res/layout/viewpager_fragment_cbz.xml +++ b/readium/navigator/src/main/res/layout/readium_navigator_viewpager_fragment_cbz.xml @@ -1,12 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" diff --git a/readium/navigator/src/main/res/layout/viewpager_fragment_epub.xml b/readium/navigator/src/main/res/layout/readium_navigator_viewpager_fragment_epub.xml similarity index 59% rename from readium/navigator/src/main/res/layout/viewpager_fragment_epub.xml rename to readium/navigator/src/main/res/layout/readium_navigator_viewpager_fragment_epub.xml index 8c37e6fcfa..ed0f97fc90 100755 --- a/readium/navigator/src/main/res/layout/viewpager_fragment_epub.xml +++ b/readium/navigator/src/main/res/layout/readium_navigator_viewpager_fragment_epub.xml @@ -1,16 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:focusable="false" android:orientation="vertical"> <org.readium.r2.navigator.R2WebView diff --git a/readium/navigator/src/main/res/values-land/dimens.xml b/readium/navigator/src/main/res/values-land/dimens.xml index d59660e077..50aaba3af5 100644 --- a/readium/navigator/src/main/res/values-land/dimens.xml +++ b/readium/navigator/src/main/res/values-land/dimens.xml @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <dimen name="r2.navigator.epub.vertical_padding">20dp</dimen> + <dimen name="readium_navigator_epub_vertical_padding">20dp</dimen> </resources> \ No newline at end of file diff --git a/readium/navigator/src/main/res/values/colors.xml b/readium/navigator/src/main/res/values/colors.xml deleted file mode 100644 index 7e1964e6bb..0000000000 --- a/readium/navigator/src/main/res/values/colors.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - -<resources> - <color name="colorPrimary">#1f21b5</color> - <color name="colorPrimaryDark">#17188c</color> - <color name="colorAccent">#eceaeb</color> - <color name="colorAccentPrefs">#17188c</color> - - - <color name="snackbar_background_color">@color/colorPrimaryDark</color> - <color name="snackbar_text_color">@color/colorAccent</color> - -</resources> diff --git a/readium/navigator/src/main/res/values/dimens.xml b/readium/navigator/src/main/res/values/dimens.xml index 05b9b31048..00fcb80b64 100644 --- a/readium/navigator/src/main/res/values/dimens.xml +++ b/readium/navigator/src/main/res/values/dimens.xml @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <dimen name="r2.navigator.epub.vertical_padding">40dp</dimen> + <dimen name="readium_navigator_epub_vertical_padding">40dp</dimen> </resources> \ No newline at end of file diff --git a/readium/navigator/src/main/res/values/strings.xml b/readium/navigator/src/main/res/values/strings.xml index 7c349f7021..f41448b3c8 100644 --- a/readium/navigator/src/main/res/values/strings.xml +++ b/readium/navigator/src/main/res/values/strings.xml @@ -1,20 +1,12 @@ <!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <resources> - <string name="end_of_chapter">end of chapter</string> - <string name="end_of_chapter_indicator">~ ~ ~</string> - <string name="zero">00:00</string> - <string name="epub_navigator_tag">epub_navigator</string> - <string name="image_navigator_tag">image_navigator</string> - <string name="r2_media_notification_channel_name">Audiobook</string> - <string name="r2_media_notification_channel_description">Audiobook currently being played</string> + <string name="readium_media_notification_channel_name">Audiobook</string> + <string name="readium_media_notification_channel_description">Audiobook currently being played</string> </resources> diff --git a/readium/navigator/src/main/res/values/styles.xml b/readium/navigator/src/main/res/values/styles.xml deleted file mode 100644 index 987aabf2d7..0000000000 --- a/readium/navigator/src/main/res/values/styles.xml +++ /dev/null @@ -1,31 +0,0 @@ -<!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - -<resources> - - <style name="AppTheme" parent="Theme.AppCompat.Light"> - <item name="android:popupAnimationStyle">@null</item> - - </style> - - <style name="AppTheme.NoActionBar"> - <item name="windowActionBar">false</item> - <item name="windowNoTitle">true</item> - <item name="android:popupAnimationStyle">@null</item> - </style> - - <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"> - <item name="android:popupAnimationStyle">@null</item> - </style> - - <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"> - <item name="android:popupAnimationStyle">@null</item> - </style> - -</resources> diff --git a/readium/navigator/src/test/java/org/readium/r2/navigator/DecorationTest.kt b/readium/navigator/src/test/java/org/readium/r2/navigator/DecorationTest.kt index 2d3cc9f5df..ee32a5595e 100644 --- a/readium/navigator/src/test/java/org/readium/r2/navigator/DecorationTest.kt +++ b/readium/navigator/src/test/java/org/readium/r2/navigator/DecorationTest.kt @@ -6,6 +6,8 @@ import kotlin.test.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @OptIn(ExperimentalDecorator::class) @@ -16,21 +18,21 @@ class DecorationTest { fun `Decorations can be compared`() { val d1a = Decoration( id = "1", - locator = Locator("chapter.html", "text/html"), + locator = Locator(Url("chapter.html")!!, mediaType = MediaType.HTML), style = Decoration.Style.Highlight(tint = Color.RED), - extras = mapOf("param" to "value"), + extras = mapOf("param" to "value") ) val d1b = Decoration( id = "1", - locator = Locator("chapter.html", "text/html"), + locator = Locator(Url("chapter.html")!!, mediaType = MediaType.HTML), style = Decoration.Style.Highlight(tint = Color.RED), - extras = mapOf("param" to "value"), + extras = mapOf("param" to "value") ) val d2 = Decoration( id = "2", - locator = Locator("chapter2.html", "text/html"), + locator = Locator(Url("chapter2.html")!!, mediaType = MediaType.HTML), style = Decoration.Style.Highlight(tint = Color.RED), - extras = mapOf("param" to "value"), + extras = mapOf("param" to "value") ) assertTrue { d1a == d1a } diff --git a/readium/navigator/src/test/java/org/readium/r2/navigator/epub/EpubSettingsResolverTest.kt b/readium/navigator/src/test/java/org/readium/r2/navigator/epub/EpubSettingsResolverTest.kt index 580eb69ae6..94d83c2071 100644 --- a/readium/navigator/src/test/java/org/readium/r2/navigator/epub/EpubSettingsResolverTest.kt +++ b/readium/navigator/src/test/java/org/readium/r2/navigator/epub/EpubSettingsResolverTest.kt @@ -29,7 +29,7 @@ class EpubSettingsResolverTest { private fun resolveLayout( languages: List<String> = emptyList(), - readingProgression: PublicationReadingProgression = PublicationReadingProgression.AUTO, + readingProgression: PublicationReadingProgression? = null, defaults: EpubDefaults = EpubDefaults(), preferences: EpubPreferences = EpubPreferences() ): Layout { @@ -53,47 +53,91 @@ class EpubSettingsResolverTest { resolveLayout() ) assertEquals( - Layout(language = Language("en"), stylesheets = Stylesheets.Default, readingProgression = LTR), + Layout( + language = Language("en"), + stylesheets = Stylesheets.Default, + readingProgression = LTR + ), resolveLayout(languages = listOf("en")) ) assertEquals( - Layout(language = Language("ar"), stylesheets = Stylesheets.Rtl, readingProgression = RTL), + Layout( + language = Language("ar"), + stylesheets = Stylesheets.Rtl, + readingProgression = RTL + ), resolveLayout(languages = listOf("ar")) ) assertEquals( - Layout(language = Language("fa"), stylesheets = Stylesheets.Rtl, readingProgression = RTL), + Layout( + language = Language("fa"), + stylesheets = Stylesheets.Rtl, + readingProgression = RTL + ), resolveLayout(languages = listOf("fa")) ) assertEquals( - Layout(language = Language("he"), stylesheets = Stylesheets.Rtl, readingProgression = RTL), + Layout( + language = Language("he"), + stylesheets = Stylesheets.Rtl, + readingProgression = RTL + ), resolveLayout(languages = listOf("he")) ) assertEquals( - Layout(language = Language("ja"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), + Layout( + language = Language("ja"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), resolveLayout(languages = listOf("ja")) ) assertEquals( - Layout(language = Language("ko"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), + Layout( + language = Language("ko"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), resolveLayout(languages = listOf("ko")) ) assertEquals( - Layout(language = Language("zh"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), + Layout( + language = Language("zh"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), resolveLayout(languages = listOf("zh")) ) assertEquals( - Layout(language = Language("zh-HK"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), + Layout( + language = Language("zh-HK"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), resolveLayout(languages = listOf("zh-HK")) ) assertEquals( - Layout(language = Language("zh-Hans"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), + Layout( + language = Language("zh-Hans"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), resolveLayout(languages = listOf("zh-Hans")) ) assertEquals( - Layout(language = Language("zh-Hant"), stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), + Layout( + language = Language("zh-Hant"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = RTL + ), resolveLayout(languages = listOf("zh-Hant")) ) assertEquals( - Layout(language = Language("zh-TW"), stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), + Layout( + language = Language("zh-TW"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = RTL + ), resolveLayout(languages = listOf("zh-TW")) ) } @@ -105,47 +149,91 @@ class EpubSettingsResolverTest { resolveLayout(readingProgression = PublicationLTR) ) assertEquals( - Layout(language = Language("en"), stylesheets = Stylesheets.Default, readingProgression = LTR), + Layout( + language = Language("en"), + stylesheets = Stylesheets.Default, + readingProgression = LTR + ), resolveLayout(readingProgression = PublicationLTR, languages = listOf("en")) ) assertEquals( - Layout(language = Language("ar"), stylesheets = Stylesheets.Default, readingProgression = LTR), + Layout( + language = Language("ar"), + stylesheets = Stylesheets.Default, + readingProgression = LTR + ), resolveLayout(readingProgression = PublicationLTR, languages = listOf("ar")) ) assertEquals( - Layout(language = Language("fa"), stylesheets = Stylesheets.Default, readingProgression = LTR), + Layout( + language = Language("fa"), + stylesheets = Stylesheets.Default, + readingProgression = LTR + ), resolveLayout(readingProgression = PublicationLTR, languages = listOf("fa")) ) assertEquals( - Layout(language = Language("he"), stylesheets = Stylesheets.Default, readingProgression = LTR), + Layout( + language = Language("he"), + stylesheets = Stylesheets.Default, + readingProgression = LTR + ), resolveLayout(readingProgression = PublicationLTR, languages = listOf("he")) ) assertEquals( - Layout(language = Language("ja"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), + Layout( + language = Language("ja"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), resolveLayout(readingProgression = PublicationLTR, languages = listOf("ja")) ) assertEquals( - Layout(language = Language("ko"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), + Layout( + language = Language("ko"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), resolveLayout(readingProgression = PublicationLTR, languages = listOf("ko")) ) assertEquals( - Layout(language = Language("zh"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), + Layout( + language = Language("zh"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), resolveLayout(readingProgression = PublicationLTR, languages = listOf("zh")) ) assertEquals( - Layout(language = Language("zh-HK"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), + Layout( + language = Language("zh-HK"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), resolveLayout(readingProgression = PublicationLTR, languages = listOf("zh-HK")) ) assertEquals( - Layout(language = Language("zh-Hans"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), + Layout( + language = Language("zh-Hans"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), resolveLayout(readingProgression = PublicationLTR, languages = listOf("zh-Hans")) ) assertEquals( - Layout(language = Language("zh-Hant"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), + Layout( + language = Language("zh-Hant"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), resolveLayout(readingProgression = PublicationLTR, languages = listOf("zh-Hant")) ) assertEquals( - Layout(language = Language("zh-TW"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), + Layout( + language = Language("zh-TW"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), resolveLayout(readingProgression = PublicationLTR, languages = listOf("zh-TW")) ) } @@ -157,47 +245,91 @@ class EpubSettingsResolverTest { resolveLayout(readingProgression = PublicationRTL) ) assertEquals( - Layout(language = Language("en"), stylesheets = Stylesheets.Rtl, readingProgression = RTL), + Layout( + language = Language("en"), + stylesheets = Stylesheets.Rtl, + readingProgression = RTL + ), resolveLayout(readingProgression = PublicationRTL, languages = listOf("en")) ) assertEquals( - Layout(language = Language("ar"), stylesheets = Stylesheets.Rtl, readingProgression = RTL), + Layout( + language = Language("ar"), + stylesheets = Stylesheets.Rtl, + readingProgression = RTL + ), resolveLayout(readingProgression = PublicationRTL, languages = listOf("ar")) ) assertEquals( - Layout(language = Language("fa"), stylesheets = Stylesheets.Rtl, readingProgression = RTL), + Layout( + language = Language("fa"), + stylesheets = Stylesheets.Rtl, + readingProgression = RTL + ), resolveLayout(readingProgression = PublicationRTL, languages = listOf("fa")) ) assertEquals( - Layout(language = Language("he"), stylesheets = Stylesheets.Rtl, readingProgression = RTL), + Layout( + language = Language("he"), + stylesheets = Stylesheets.Rtl, + readingProgression = RTL + ), resolveLayout(readingProgression = PublicationRTL, languages = listOf("he")) ) assertEquals( - Layout(language = Language("ja"), stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), + Layout( + language = Language("ja"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = RTL + ), resolveLayout(readingProgression = PublicationRTL, languages = listOf("ja")) ) assertEquals( - Layout(language = Language("ko"), stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), + Layout( + language = Language("ko"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = RTL + ), resolveLayout(readingProgression = PublicationRTL, languages = listOf("ko")) ) assertEquals( - Layout(language = Language("zh"), stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), + Layout( + language = Language("zh"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = RTL + ), resolveLayout(readingProgression = PublicationRTL, languages = listOf("zh")) ) assertEquals( - Layout(language = Language("zh-HK"), stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), + Layout( + language = Language("zh-HK"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = RTL + ), resolveLayout(readingProgression = PublicationRTL, languages = listOf("zh-HK")) ) assertEquals( - Layout(language = Language("zh-Hans"), stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), + Layout( + language = Language("zh-Hans"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = RTL + ), resolveLayout(readingProgression = PublicationRTL, languages = listOf("zh-Hans")) ) assertEquals( - Layout(language = Language("zh-Hant"), stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), + Layout( + language = Language("zh-Hant"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = RTL + ), resolveLayout(readingProgression = PublicationRTL, languages = listOf("zh-Hant")) ) assertEquals( - Layout(language = Language("zh-TW"), stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), + Layout( + language = Language("zh-TW"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = RTL + ), resolveLayout(readingProgression = PublicationRTL, languages = listOf("zh-TW")) ) } @@ -210,15 +342,29 @@ class EpubSettingsResolverTest { ) assertEquals( Layout(language = null, stylesheets = Stylesheets.CjkVertical, readingProgression = LTR), - resolveLayout(readingProgression = PublicationLTR, preferences = EpubPreferences(verticalText = true)) + resolveLayout( + readingProgression = PublicationLTR, + preferences = EpubPreferences(verticalText = true) + ) ) assertEquals( Layout(language = null, stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), - resolveLayout(readingProgression = PublicationRTL, preferences = EpubPreferences(verticalText = true)) - ) - assertEquals( - Layout(language = Language("en"), stylesheets = Stylesheets.CjkVertical, readingProgression = LTR), - resolveLayout(readingProgression = PublicationLTR, languages = listOf("en"), preferences = EpubPreferences(verticalText = true)) + resolveLayout( + readingProgression = PublicationRTL, + preferences = EpubPreferences(verticalText = true) + ) + ) + assertEquals( + Layout( + language = Language("en"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = LTR + ), + resolveLayout( + readingProgression = PublicationLTR, + languages = listOf("en"), + preferences = EpubPreferences(verticalText = true) + ) ) } @@ -230,31 +376,77 @@ class EpubSettingsResolverTest { ) assertEquals( Layout(language = null, stylesheets = Stylesheets.Default, readingProgression = LTR), - resolveLayout(readingProgression = PublicationLTR, preferences = EpubPreferences(verticalText = false)) + resolveLayout( + readingProgression = PublicationLTR, + preferences = EpubPreferences(verticalText = false) + ) ) assertEquals( Layout(language = null, stylesheets = Stylesheets.Rtl, readingProgression = RTL), - resolveLayout(readingProgression = PublicationRTL, preferences = EpubPreferences(verticalText = false)) - ) - assertEquals( - Layout(language = Language("en"), stylesheets = Stylesheets.Default, readingProgression = LTR), - resolveLayout(readingProgression = PublicationLTR, languages = listOf("en"), preferences = EpubPreferences(verticalText = false)) - ) - assertEquals( - Layout(language = Language("ar"), stylesheets = Stylesheets.Rtl, readingProgression = RTL), - resolveLayout(readingProgression = PublicationRTL, languages = listOf("ar"), preferences = EpubPreferences(verticalText = false)) - ) - assertEquals( - Layout(language = Language("ja"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), - resolveLayout(readingProgression = PublicationLTR, languages = listOf("ja"), preferences = EpubPreferences(verticalText = false)) - ) - assertEquals( - Layout(language = Language("ja"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), - resolveLayout(readingProgression = PublicationLTR, languages = listOf("ja"), preferences = EpubPreferences(verticalText = false)) - ) - assertEquals( - Layout(language = Language("ja"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = RTL), - resolveLayout(readingProgression = PublicationRTL, languages = listOf("ja"), preferences = EpubPreferences(verticalText = false)) + resolveLayout( + readingProgression = PublicationRTL, + preferences = EpubPreferences(verticalText = false) + ) + ) + assertEquals( + Layout( + language = Language("en"), + stylesheets = Stylesheets.Default, + readingProgression = LTR + ), + resolveLayout( + readingProgression = PublicationLTR, + languages = listOf("en"), + preferences = EpubPreferences(verticalText = false) + ) + ) + assertEquals( + Layout( + language = Language("ar"), + stylesheets = Stylesheets.Rtl, + readingProgression = RTL + ), + resolveLayout( + readingProgression = PublicationRTL, + languages = listOf("ar"), + preferences = EpubPreferences(verticalText = false) + ) + ) + assertEquals( + Layout( + language = Language("ja"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), + resolveLayout( + readingProgression = PublicationLTR, + languages = listOf("ja"), + preferences = EpubPreferences(verticalText = false) + ) + ) + assertEquals( + Layout( + language = Language("ja"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), + resolveLayout( + readingProgression = PublicationLTR, + languages = listOf("ja"), + preferences = EpubPreferences(verticalText = false) + ) + ) + assertEquals( + Layout( + language = Language("ja"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = RTL + ), + resolveLayout( + readingProgression = PublicationRTL, + languages = listOf("ja"), + preferences = EpubPreferences(verticalText = false) + ) ) } @@ -262,11 +454,22 @@ class EpubSettingsResolverTest { fun `RTL readingProgression preference takes precedence over LTR readingProgression hint`() { assertEquals( Layout(language = null, stylesheets = Stylesheets.Rtl, readingProgression = RTL), - resolveLayout(readingProgression = PublicationLTR, preferences = EpubPreferences(readingProgression = RTL)) - ) - assertEquals( - Layout(language = Language("zh-tw"), stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), - resolveLayout(readingProgression = PublicationLTR, languages = listOf("zh-tw"), preferences = EpubPreferences(readingProgression = RTL)) + resolveLayout( + readingProgression = PublicationLTR, + preferences = EpubPreferences(readingProgression = RTL) + ) + ) + assertEquals( + Layout( + language = Language("zh-tw"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = RTL + ), + resolveLayout( + readingProgression = PublicationLTR, + languages = listOf("zh-tw"), + preferences = EpubPreferences(readingProgression = RTL) + ) ) } @@ -274,11 +477,22 @@ class EpubSettingsResolverTest { fun `LTR readingProgression setting takes precedence over RTL readingProgression hint`() { assertEquals( Layout(language = null, stylesheets = Stylesheets.Default, readingProgression = LTR), - resolveLayout(readingProgression = PublicationRTL, preferences = EpubPreferences(readingProgression = LTR)) - ) - assertEquals( - Layout(language = Language("zh-tw"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), - resolveLayout(readingProgression = PublicationRTL, languages = listOf("zh-tw"), preferences = EpubPreferences(readingProgression = LTR)) + resolveLayout( + readingProgression = PublicationRTL, + preferences = EpubPreferences(readingProgression = LTR) + ) + ) + assertEquals( + Layout( + language = Language("zh-tw"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), + resolveLayout( + readingProgression = PublicationRTL, + languages = listOf("zh-tw"), + preferences = EpubPreferences(readingProgression = LTR) + ) ) } @@ -286,11 +500,22 @@ class EpubSettingsResolverTest { fun `LTR readingProgression hint takes precedence over default RTL readingProgression`() { assertEquals( Layout(language = null, stylesheets = Stylesheets.Default, readingProgression = LTR), - resolveLayout(readingProgression = PublicationLTR, defaults = EpubDefaults(readingProgression = RTL)) - ) - assertEquals( - Layout(language = Language("ja"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), - resolveLayout(readingProgression = PublicationLTR, languages = listOf("ja"), defaults = EpubDefaults(readingProgression = RTL)) + resolveLayout( + readingProgression = PublicationLTR, + defaults = EpubDefaults(readingProgression = RTL) + ) + ) + assertEquals( + Layout( + language = Language("ja"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), + resolveLayout( + readingProgression = PublicationLTR, + languages = listOf("ja"), + defaults = EpubDefaults(readingProgression = RTL) + ) ) } @@ -298,11 +523,22 @@ class EpubSettingsResolverTest { fun `RTL readingProgression hint takes precedence over default LTR readingProgression`() { assertEquals( Layout(language = null, stylesheets = Stylesheets.Rtl, readingProgression = RTL), - resolveLayout(readingProgression = PublicationRTL, defaults = EpubDefaults(readingProgression = LTR)) - ) - assertEquals( - Layout(language = Language("zh-tw"), stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), - resolveLayout(readingProgression = PublicationRTL, languages = listOf("zh-tw"), defaults = EpubDefaults(readingProgression = LTR)) + resolveLayout( + readingProgression = PublicationRTL, + defaults = EpubDefaults(readingProgression = LTR) + ) + ) + assertEquals( + Layout( + language = Language("zh-tw"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = RTL + ), + resolveLayout( + readingProgression = PublicationRTL, + languages = listOf("zh-tw"), + defaults = EpubDefaults(readingProgression = LTR) + ) ) } @@ -325,69 +561,139 @@ class EpubSettingsResolverTest { @Test fun `metadata language takes precedence over default readingProgression`() { assertEquals( - Layout(language = Language("en"), stylesheets = Stylesheets.Default, readingProgression = LTR), - resolveLayout(languages = listOf("en"), defaults = EpubDefaults(readingProgression = RTL)) + Layout( + language = Language("en"), + stylesheets = Stylesheets.Default, + readingProgression = LTR + ), + resolveLayout( + languages = listOf("en"), + defaults = EpubDefaults(readingProgression = RTL) + ) ) assertEquals( - Layout(language = Language("zh"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), - resolveLayout(languages = listOf("zh"), defaults = EpubDefaults(readingProgression = RTL)) + Layout( + language = Language("zh"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), + resolveLayout( + languages = listOf("zh"), + defaults = EpubDefaults(readingProgression = RTL) + ) ) } @Test fun `RTL readingProgression preference takes precedence over metadata language`() { assertEquals( - Layout(language = Language("en"), stylesheets = Stylesheets.Rtl, readingProgression = RTL), - resolveLayout(languages = listOf("en"), preferences = EpubPreferences(readingProgression = RTL)) - ) - assertEquals( - Layout(language = Language("zh"), stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), - resolveLayout(languages = listOf("zh"), preferences = EpubPreferences(readingProgression = RTL)) + Layout( + language = Language("en"), + stylesheets = Stylesheets.Rtl, + readingProgression = RTL + ), + resolveLayout( + languages = listOf("en"), + preferences = EpubPreferences(readingProgression = RTL) + ) + ) + assertEquals( + Layout( + language = Language("zh"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = RTL + ), + resolveLayout( + languages = listOf("zh"), + preferences = EpubPreferences(readingProgression = RTL) + ) ) } @Test fun `LTR readingProgression preference takes precedence over metadata language`() { assertEquals( - Layout(language = Language("he"), stylesheets = Stylesheets.Default, readingProgression = LTR), - resolveLayout(languages = listOf("he"), preferences = EpubPreferences(readingProgression = LTR)) - ) - assertEquals( - Layout(language = Language("zh-tw"), stylesheets = Stylesheets.CjkHorizontal, readingProgression = LTR), - resolveLayout(languages = listOf("zh-tw"), preferences = EpubPreferences(readingProgression = LTR)) + Layout( + language = Language("he"), + stylesheets = Stylesheets.Default, + readingProgression = LTR + ), + resolveLayout( + languages = listOf("he"), + preferences = EpubPreferences(readingProgression = LTR) + ) + ) + assertEquals( + Layout( + language = Language("zh-tw"), + stylesheets = Stylesheets.CjkHorizontal, + readingProgression = LTR + ), + resolveLayout( + languages = listOf("zh-tw"), + preferences = EpubPreferences(readingProgression = LTR) + ) ) } @Test fun `RTL readingProgression preference takes precedence over language preference`() { assertEquals( - Layout(language = Language("en"), stylesheets = Stylesheets.Rtl, readingProgression = RTL), - resolveLayout(preferences = EpubPreferences(readingProgression = RTL, language = Language("en"))) + Layout( + language = Language("en"), + stylesheets = Stylesheets.Rtl, + readingProgression = RTL + ), + resolveLayout( + preferences = EpubPreferences(readingProgression = RTL, language = Language("en")) + ) ) } @Test fun `LTR readingProgression preference takes precedence over language preference`() { assertEquals( - Layout(language = Language("he"), stylesheets = Stylesheets.Default, readingProgression = LTR), - resolveLayout(preferences = EpubPreferences(readingProgression = LTR, language = Language("he"))) + Layout( + language = Language("he"), + stylesheets = Stylesheets.Default, + readingProgression = LTR + ), + resolveLayout( + preferences = EpubPreferences(readingProgression = LTR, language = Language("he")) + ) ) } @Test fun `he language preference takes precedence over language metadata`() { assertEquals( - Layout(language = Language("he"), stylesheets = Stylesheets.Rtl, readingProgression = RTL), - resolveLayout(readingProgression = PublicationLTR, languages = listOf("en"), preferences = EpubPreferences(language = Language("he"))) + Layout( + language = Language("he"), + stylesheets = Stylesheets.Rtl, + readingProgression = RTL + ), + resolveLayout( + readingProgression = PublicationLTR, + languages = listOf("en"), + preferences = EpubPreferences(language = Language("he")) + ) ) } @Test fun `zh-tw language preference takes precedence over language metadata`() { assertEquals( - Layout(language = Language("zh-tw"), stylesheets = Stylesheets.CjkVertical, readingProgression = RTL), - resolveLayout(readingProgression = PublicationLTR, languages = listOf("en"), preferences = EpubPreferences(language = Language("zh-tw"))) + Layout( + language = Language("zh-tw"), + stylesheets = Stylesheets.CjkVertical, + readingProgression = RTL + ), + resolveLayout( + readingProgression = PublicationLTR, + languages = listOf("en"), + preferences = EpubPreferences(language = Language("zh-tw")) + ) ) } } diff --git a/readium/navigator/src/test/java/org/readium/r2/navigator/epub/css/PropertiesTest.kt b/readium/navigator/src/test/java/org/readium/r2/navigator/epub/css/PropertiesTest.kt index 0da3b2f539..df35418f64 100644 --- a/readium/navigator/src/test/java/org/readium/r2/navigator/epub/css/PropertiesTest.kt +++ b/readium/navigator/src/test/java/org/readium/r2/navigator/epub/css/PropertiesTest.kt @@ -52,7 +52,7 @@ class PropertiesTest { "--RS__serif-ja-v" to null, "--RS__sans-serif-ja-v" to null, "--RS__compFontFamily" to null, - "--RS__codeFontFamily" to null, + "--RS__codeFontFamily" to null ), RsProperties().toCssProperties() ) @@ -109,7 +109,7 @@ class PropertiesTest { "--RS__serif-ja-v" to """"Serif", "JaV"""", "--RS__sans-serif-ja-v" to """"Sans serif", "JaV"""", "--RS__compFontFamily" to """"Arial"""", - "--RS__codeFontFamily" to """"Monaco", "Console Sans"""", + "--RS__codeFontFamily" to """"Monaco", "Console Sans"""" ), RsProperties( colWidth = Length.Cm(1.2), @@ -175,7 +175,7 @@ class PropertiesTest { "--USER__letterSpacing" to null, "--USER__bodyHyphens" to null, "--USER__ligatures" to null, - "--USER__a11yNormalize" to null, + "--USER__a11yNormalize" to null ), UserProperties().toCssProperties() ) @@ -206,7 +206,7 @@ class PropertiesTest { "--USER__letterSpacing" to "8.9rem", "--USER__bodyHyphens" to "auto", "--USER__ligatures" to "common-ligatures", - "--USER__a11yNormalize" to "readium-a11y-on", + "--USER__a11yNormalize" to "readium-a11y-on" ), UserProperties( view = View.SCROLL, @@ -230,7 +230,7 @@ class PropertiesTest { letterSpacing = Length.Rem(8.9), bodyHyphens = Hyphens.AUTO, ligatures = Ligatures.COMMON, - a11yNormalize = true, + a11yNormalize = true ).toCssProperties() ) } @@ -267,7 +267,7 @@ class PropertiesTest { """.trimIndent(), UserProperties( view = View.SCROLL, - colCount = ColCount.AUTO, + colCount = ColCount.AUTO ).toCss() ) } @@ -322,7 +322,7 @@ class PropertiesTest { letterSpacing = Length.Rem(8.9), bodyHyphens = Hyphens.AUTO, ligatures = Ligatures.COMMON, - a11yNormalize = true, + a11yNormalize = true ).toCss() ) } diff --git a/readium/navigator/src/test/java/org/readium/r2/navigator/epub/css/ReadiumCssTest.kt b/readium/navigator/src/test/java/org/readium/r2/navigator/epub/css/ReadiumCssTest.kt index 093572d53a..265aea617e 100644 --- a/readium/navigator/src/test/java/org/readium/r2/navigator/epub/css/ReadiumCssTest.kt +++ b/readium/navigator/src/test/java/org/readium/r2/navigator/epub/css/ReadiumCssTest.kt @@ -14,6 +14,7 @@ import org.readium.r2.navigator.preferences.FontFamily import org.readium.r2.navigator.preferences.ReadingProgression import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.Language +import org.readium.r2.shared.util.Url import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -28,7 +29,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.Default, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ @@ -73,7 +74,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.Default, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ @@ -118,7 +119,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.Default, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ @@ -149,7 +150,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.Default, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ @@ -180,7 +181,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.Default, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ @@ -224,7 +225,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.Default, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) // A <link> tag is considered styled. assertFalse( @@ -282,7 +283,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.Rtl, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ @@ -327,7 +328,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.CjkHorizontal, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ @@ -373,7 +374,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.CjkVertical, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ @@ -418,7 +419,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.Default, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ @@ -463,7 +464,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.Default, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ @@ -508,7 +509,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.Default, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ @@ -553,7 +554,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.Default, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ @@ -608,9 +609,9 @@ class ReadiumCssTest { googleFonts = listOf( FontFamily.OPEN_DYSLEXIC, FontFamily.SANS_SERIF, - FontFamily.SERIF, + FontFamily.SERIF ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ @@ -660,7 +661,7 @@ class ReadiumCssTest { stylesheets = Layout.Stylesheets.Default, readingProgression = ReadingProgression.LTR ), - assetsBaseHref = "/assets/" + assetsBaseHref = Url("/assets/")!! ) assertEquals( """ diff --git a/readium/navigators/media/audio/build.gradle.kts b/readium/navigators/media/audio/build.gradle.kts new file mode 100644 index 0000000000..fcd4d4b97e --- /dev/null +++ b/readium/navigators/media/audio/build.gradle.kts @@ -0,0 +1,62 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +plugins { + id("com.android.library") + kotlin("android") + kotlin("plugin.parcelize") +} + +android { + resourcePrefix = "readium_" + + compileSdk = 34 + + defaultConfig { + minSdk = 21 + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + freeCompilerArgs = freeCompilerArgs + listOf( + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=org.readium.r2.shared.InternalReadiumApi" + ) + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android.txt")) + } + } + buildFeatures { + viewBinding = true + } + namespace = "org.readium.navigators.media.audio" +} + +kotlin { + explicitApi() +} + +rootProject.ext["publish.artifactId"] = "readium-navigator-media-audio" +apply(from = "$rootDir/scripts/publish-module.gradle") + +dependencies { + api(project(":readium:navigators:media:readium-navigator-media-common")) + + implementation(libs.androidx.media3.common) + implementation(libs.androidx.media3.session) + + implementation(libs.androidx.core) + implementation(libs.timber) + implementation(libs.bundles.coroutines) +} diff --git a/readium/navigators/media/audio/src/main/AndroidManifest.xml b/readium/navigators/media/audio/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2d10029868 --- /dev/null +++ b/readium/navigators/media/audio/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest /> + diff --git a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioEngine.kt b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioEngine.kt new file mode 100644 index 0000000000..67be3ebec8 --- /dev/null +++ b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioEngine.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.navigator.media.audio + +import androidx.media3.common.Player +import kotlin.time.Duration +import kotlinx.coroutines.flow.StateFlow +import org.readium.r2.navigator.preferences.Configurable +import org.readium.r2.shared.ExperimentalReadiumApi + +/** + * An audio engine playing a list of items. + */ +@ExperimentalReadiumApi +public interface AudioEngine<S : Configurable.Settings, P : Configurable.Preferences<P>> : + Configurable<S, P> { + + /** + * Marker interface for the errors that the [AudioEngine] returns. + */ + public interface Error + + /** + * State of the player. + */ + public sealed class State { + + /** + * The player is ready to play. + */ + public object Ready : State() + + /** + * The end of the content has been reached. + */ + public object Ended : State() + + /** + * The engine cannot play because the buffer is starved. + */ + public object Buffering : State() + + /** + * The engine cannot play because an error occurred. + */ + public data class Failure(val error: AudioEngine.Error) : State() + } + + /** + * State of the playback. + * + * @param state The current state. + * @param playWhenReady Indicates if the navigator should play as soon as the state is Ready. + * @param index Index of the reading order item currently being played. + * @param offset Position of the playback in the current item. + * @param buffered Position in the current item until which the content is buffered. + */ + public data class Playback( + val state: State, + val playWhenReady: Boolean, + val index: Int, + val offset: Duration, + val buffered: Duration? + ) + + /** + * Current state of the playback. + */ + public val playback: StateFlow<Playback> + + /** + * Resumes the playback at the current location. + */ + public fun play() + + /** + * Pauses the playback. + */ + public fun pause() + + /** + * Skips to [offset] in the item at [index]. + */ + public fun skipTo(index: Int, offset: Duration) + + /** + * Skips [duration] either forward or backward if [duration] is negative. + */ + public fun skip(duration: Duration) + + /** + * Skips forward a small increment. + */ + public fun skipForward() + + /** + * Skips backward a small increment. + */ + public fun skipBackward() + + /** + * Closes the player. + */ + public fun close() + + /** + * Adapts this engine to the media3 [Player] interface. + */ + public fun asPlayer(): Player +} diff --git a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioEngineProvider.kt b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioEngineProvider.kt new file mode 100644 index 0000000000..057d35314c --- /dev/null +++ b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioEngineProvider.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.navigator.media.audio + +import org.readium.r2.navigator.preferences.Configurable +import org.readium.r2.navigator.preferences.PreferencesEditor +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Try + +/** + * To be implemented by adapters for third-party audio engines which can be used with [AudioNavigator]. + */ +@ExperimentalReadiumApi +public interface AudioEngineProvider<S : Configurable.Settings, P : Configurable.Preferences<P>, + E : PreferencesEditor<P>> { + + public suspend fun createEngine( + publication: Publication, + initialLocator: Locator, + initialPreferences: P + ): Try<AudioEngine<S, P>, Error> + + /** + * Creates a preferences editor for [publication] and [initialPreferences]. + */ + public fun createPreferenceEditor(publication: Publication, initialPreferences: P): E + + /** + * Creates an empty set of preferences of this TTS engine provider. + */ + public fun createEmptyPreferences(): P +} diff --git a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigator.kt b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigator.kt new file mode 100644 index 0000000000..69f2876524 --- /dev/null +++ b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigator.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.navigator.media.audio + +import androidx.media3.common.Player +import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.StateFlow +import org.readium.navigator.media.common.Media3Adapter +import org.readium.navigator.media.common.MediaNavigator +import org.readium.navigator.media.common.TimeBasedMediaNavigator +import org.readium.r2.navigator.extensions.normalizeLocator +import org.readium.r2.navigator.extensions.sum +import org.readium.r2.navigator.extensions.time +import org.readium.r2.navigator.preferences.Configurable +import org.readium.r2.shared.DelicateReadiumApi +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.extensions.mapStateIn +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Url +import timber.log.Timber + +@ExperimentalReadiumApi +@OptIn(ExperimentalTime::class, DelicateReadiumApi::class) +public class AudioNavigator<S : Configurable.Settings, P : Configurable.Preferences<P>> internal constructor( + private val publication: Publication, + private val audioEngine: AudioEngine<S, P>, + override val readingOrder: ReadingOrder +) : + MediaNavigator<AudioNavigator.Location, AudioNavigator.Playback, AudioNavigator.ReadingOrder>, + TimeBasedMediaNavigator<AudioNavigator.Location, AudioNavigator.Playback, AudioNavigator.ReadingOrder>, + Media3Adapter, + Configurable<S, P> by audioEngine { + + public data class Location( + override val href: Url, + override val offset: Duration + ) : TimeBasedMediaNavigator.Location + + public data class ReadingOrder( + override val duration: Duration?, + override val items: List<Item> + ) : TimeBasedMediaNavigator.ReadingOrder { + + public data class Item( + val href: Url, + override val duration: Duration? + ) : TimeBasedMediaNavigator.ReadingOrder.Item + } + + public data class Playback( + override val state: MediaNavigator.State, + override val playWhenReady: Boolean, + override val index: Int, + override val offset: Duration, + override val buffered: Duration? + ) : TimeBasedMediaNavigator.Playback + + public sealed class State { + + public object Ready : MediaNavigator.State.Ready + + public object Ended : MediaNavigator.State.Ended + + public object Buffering : MediaNavigator.State.Buffering + + public data class Failure<E : AudioEngine.Error> (val error: E) : MediaNavigator.State.Failure + } + + private val coroutineScope: CoroutineScope = + MainScope() + + override val currentLocator: StateFlow<Locator> = + audioEngine.playback.mapStateIn(coroutineScope) { playback -> + val currentItem = readingOrder.items[playback.index] + val link = requireNotNull(publication.linkWithHref(currentItem.href)) + val item = readingOrder.items[playback.index] + val itemStartPosition = readingOrder.items + .slice(0 until playback.index) + .mapNotNull { it.duration } + .takeIf { it.size == readingOrder.items.size } + ?.sum() + val totalProgression = + if (itemStartPosition == null) { + null + } else { + readingOrder.duration?.let { (itemStartPosition + playback.offset) / it } + } + + val locator = requireNotNull(publication.locatorFromLink(link)) + locator.copyWithLocations( + fragments = listOf("t=${playback.offset.inWholeSeconds}"), + progression = item.duration?.let { playback.offset / it }, + totalProgression = totalProgression + ) + } + + override val playback: StateFlow<Playback> = + audioEngine.playback.mapStateIn(coroutineScope) { playback -> + Playback( + playback.state.toState(), + playback.playWhenReady, + playback.index, + playback.offset, + playback.buffered + ) + } + + override val location: StateFlow<Location> = + audioEngine.playback.mapStateIn(coroutineScope) { + val currentItem = readingOrder.items[it.index] + Location(currentItem.href, it.offset) + } + + override fun play() { + audioEngine.play() + } + + override fun pause() { + audioEngine.pause() + } + + override fun skipTo(index: Int, offset: Duration) { + audioEngine.skipTo(index, offset) + } + + public override fun skipForward() { + audioEngine.skipForward() + } + + public override fun skipBackward() { + audioEngine.skipBackward() + } + + public override fun skip(duration: Duration) { + audioEngine.skip(duration) + } + + override fun close() { + audioEngine.close() + } + + override fun asMedia3Player(): Player = + audioEngine.asPlayer() + + override fun go(locator: Locator, animated: Boolean, completion: () -> Unit): Boolean { + @Suppress("NAME_SHADOWING") + val locator = publication.normalizeLocator(locator) + val itemIndex = readingOrder.items.indexOfFirst { it.href == locator.href } + .takeUnless { it == -1 } + ?: return false + val position = locator.locations.time ?: Duration.ZERO + Timber.v("Go to locator $locator") + audioEngine.skipTo(itemIndex, position) + return true + } + + override fun go(link: Link, animated: Boolean, completion: () -> Unit): Boolean { + val locator = publication.locatorFromLink(link) ?: return false + return go(locator, animated, completion) + } + + private fun AudioEngine.State.toState(): MediaNavigator.State = + when (this) { + is AudioEngine.State.Ready -> State.Ready + is AudioEngine.State.Ended -> State.Ended + is AudioEngine.State.Buffering -> State.Buffering + is AudioEngine.State.Failure -> State.Failure(error) + } +} diff --git a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigatorFactory.kt b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigatorFactory.kt new file mode 100644 index 0000000000..52356d206b --- /dev/null +++ b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigatorFactory.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.navigator.media.audio + +import android.os.Build +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.ExperimentalTime +import org.readium.r2.navigator.extensions.normalizeLocator +import org.readium.r2.navigator.extensions.sum +import org.readium.r2.navigator.preferences.Configurable +import org.readium.r2.navigator.preferences.PreferencesEditor +import org.readium.r2.shared.DelicateReadiumApi +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse + +@ExperimentalReadiumApi +@OptIn(ExperimentalTime::class, DelicateReadiumApi::class) +public class AudioNavigatorFactory<S : Configurable.Settings, P : Configurable.Preferences<P>, + E : PreferencesEditor<P>> private constructor( + private val publication: Publication, + private val audioEngineProvider: AudioEngineProvider<S, P, E> +) { + + public companion object { + + public operator fun <S : Configurable.Settings, P : Configurable.Preferences<P>, + E : PreferencesEditor<P>> invoke( + publication: Publication, + audioEngineProvider: AudioEngineProvider<S, P, E> + ): AudioNavigatorFactory<S, P, E>? { + if (!publication.conformsTo(Publication.Profile.AUDIOBOOK)) { + return null + } + + if (publication.readingOrder.isEmpty()) { + return null + } + + if (publication.readingOrder.any { it.duration == 0.0 }) { + return null + } + + return AudioNavigatorFactory( + publication, + audioEngineProvider + ) + } + } + + public sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { + + public class UnsupportedPublication( + cause: org.readium.r2.shared.util.Error? = null + ) : Error("Publication is not supported.", cause) + + public class EngineInitialization( + cause: org.readium.r2.shared.util.Error? = null + ) : Error("Failed to initialize audio engine.", cause) + } + + public suspend fun createNavigator( + initialLocator: Locator? = null, + initialPreferences: P? = null, + readingOrder: List<Link> = publication.readingOrder + ): Try<AudioNavigator<S, P>, Error> { + fun duration(link: Link, publication: Publication): Duration? { + var duration: Duration? = link.duration?.seconds + .takeUnless { it == Duration.ZERO } + + if (duration == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val resource = requireNotNull(publication.get(link)) + val metadataRetriever = MetadataRetriever(resource) + duration = metadataRetriever.duration() + metadataRetriever.close() + } + + return duration + } + + val items = readingOrder.map { + AudioNavigator.ReadingOrder.Item( + href = it.url(), + duration = duration(it, publication) + ) + } + val totalDuration = publication.metadata.duration?.seconds + ?: items.mapNotNull { it.duration } + .takeIf { it.size == items.size } + ?.sum() + + val actualReadingOrder = AudioNavigator.ReadingOrder(totalDuration, items) + + val actualInitialLocator = + initialLocator?.let { publication.normalizeLocator(it) } + ?: publication.locatorFromLink(publication.readingOrder[0])!! + + val audioEngine = + audioEngineProvider.createEngine( + publication, + actualInitialLocator, + initialPreferences ?: audioEngineProvider.createEmptyPreferences() + ).getOrElse { + return Try.failure(Error.EngineInitialization(it)) + } + + val audioNavigator = AudioNavigator( + publication = publication, + audioEngine = audioEngine, + readingOrder = actualReadingOrder + ) + + return Try.success(audioNavigator) + } + + public fun createAudioPreferencesEditor( + currentPreferences: P + ): E = audioEngineProvider.createPreferenceEditor(publication, currentPreferences) +} diff --git a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/MetadataRetriever.kt b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/MetadataRetriever.kt new file mode 100644 index 0000000000..7c49ccb475 --- /dev/null +++ b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/MetadataRetriever.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.navigator.media.audio + +import android.media.MediaDataSource +import android.media.MediaMetadataRetriever +import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION +import android.os.Build +import androidx.annotation.RequiresApi +import java.io.IOException +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.runBlocking +import org.readium.r2.shared.extensions.tryOrLog +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.resource.Resource + +@RequiresApi(Build.VERSION_CODES.M) +internal class MetadataRetriever( + resource: Resource +) { + + private val retriever: MediaMetadataRetriever = + MediaMetadataRetriever() + .apply { + setDataSource(ResourceMediaDataSource(resource)) + } + + fun duration(): Duration? = + retriever.extractMetadata(METADATA_KEY_DURATION) + ?.toIntOrNull() + ?.takeUnless { it == 0 } + ?.milliseconds + + fun close() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + tryOrLog { retriever.close() } + } + } + + private class ResourceMediaDataSource( + private val resource: Resource + ) : MediaDataSource() { + + override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int { + if (size == 0) { + return 0 + } + + val data = runBlocking { + resource.read(position until position + size) + .mapFailure { IOException("Resource error", ErrorException(it)) } + .getOrThrow() + } + + if (data.isEmpty()) { + return -1 + } + + data.copyInto(buffer, offset) + return data.size + } + + override fun getSize(): Long { + return runBlocking { + resource.length() + .mapFailure { IOException("Resource error", ErrorException(it)) } + .getOrThrow() + } + } + + override fun close() { + runBlocking { resource.close() } + } + } +} diff --git a/readium/navigators/media/build.gradle.kts b/readium/navigators/media/build.gradle.kts new file mode 100644 index 0000000000..290194c510 --- /dev/null +++ b/readium/navigators/media/build.gradle.kts @@ -0,0 +1,54 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +plugins { + id("com.android.library") + kotlin("android") + kotlin("plugin.parcelize") +} + +android { + resourcePrefix = "readium_" + + compileSdk = 34 + + defaultConfig { + minSdk = 21 + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + freeCompilerArgs = freeCompilerArgs + listOf( + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=org.readium.r2.shared.InternalReadiumApi" + ) + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android.txt")) + } + } + namespace = "org.readium.navigator.media" +} + +kotlin { + explicitApi() +} + +rootProject.ext["publish.artifactId"] = "readium-navigator-media" +apply(from = "$rootDir/scripts/publish-module.gradle") + +dependencies { + api(project(":readium:navigators:media:readium-navigator-media-common")) + api(project(":readium:navigators:media:readium-navigator-media-audio")) + api(project(":readium:navigators:media:readium-navigator-media-tts")) +} diff --git a/readium/navigators/media/common/build.gradle.kts b/readium/navigators/media/common/build.gradle.kts new file mode 100644 index 0000000000..de55505f49 --- /dev/null +++ b/readium/navigators/media/common/build.gradle.kts @@ -0,0 +1,60 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +plugins { + id("com.android.library") + kotlin("android") + kotlin("plugin.parcelize") +} + +android { + resourcePrefix = "readium_" + + compileSdk = 34 + + defaultConfig { + minSdk = 21 + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + freeCompilerArgs = freeCompilerArgs + listOf( + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=org.readium.r2.shared.InternalReadiumApi" + ) + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android.txt")) + } + } + buildFeatures { + viewBinding = true + } + namespace = "org.readium.navigators.media.common" +} + +kotlin { + explicitApi() +} + +rootProject.ext["publish.artifactId"] = "readium-navigator-media-common" +apply(from = "$rootDir/scripts/publish-module.gradle") + +dependencies { + api(project(":readium:readium-shared")) + api(project(":readium:readium-navigator")) + + implementation(libs.androidx.media3.common) + implementation(libs.timber) + implementation(libs.bundles.coroutines) +} diff --git a/readium/navigators/media/common/src/main/AndroidManifest.xml b/readium/navigators/media/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2d10029868 --- /dev/null +++ b/readium/navigators/media/common/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest /> + diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/DefaultMediaMetadataFactory.kt b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/DefaultMediaMetadataFactory.kt similarity index 55% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/DefaultMediaMetadataFactory.kt rename to readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/DefaultMediaMetadataFactory.kt index 292e585273..2a09b9445a 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/DefaultMediaMetadataFactory.kt +++ b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/DefaultMediaMetadataFactory.kt @@ -4,15 +4,18 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.api +package org.readium.navigator.media.common +import android.graphics.Bitmap +import android.net.Uri +import android.util.Size import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata.PICTURE_TYPE_FRONT_COVER -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async +import java.io.ByteArrayOutputStream +import kotlinx.coroutines.* +import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.services.coverFitting /** * Builds media metadata using the given title, author and cover, @@ -23,24 +26,27 @@ internal class DefaultMediaMetadataFactory( private val publication: Publication, title: String? = null, author: String? = null, - cover: ByteArray? = null + private val cover: Uri? = null ) : MediaMetadataFactory { private val coroutineScope = CoroutineScope(Dispatchers.Default) - private val title: String = + private val title: String? = title ?: publication.metadata.title - private val authors: String? = + private val author: String? = author ?: publication.metadata.authors .firstOrNull { it.name.isNotBlank() }?.name - private val cover: Deferred<ByteArray?> = coroutineScope.async { - cover ?: publication.linkWithRel("cover") - ?.let { publication.get(it) } - ?.read() - ?.getOrNull() + private val coverBytes: Deferred<ByteArray?> = coroutineScope.async(start = CoroutineStart.LAZY) { + tryOrNull { + val byteStream = ByteArrayOutputStream(4096) + // byte array will go cross processes and should be kept small + publication.coverFitting(Size(400, 400)) + ?.compress(Bitmap.CompressFormat.PNG, 80, byteStream) + ?.let { byteStream.toByteArray() } + } } override suspend fun publicationMetadata(): MediaMetadata { @@ -48,11 +54,10 @@ internal class DefaultMediaMetadataFactory( .setTitle(title) .setTotalTrackCount(publication.readingOrder.size) - authors + author ?.let { builder.setArtist(it) } - cover.await() - ?.let { builder.maybeSetArtworkData(it, PICTURE_TYPE_FRONT_COVER) } + putCover(builder) return builder.build() } @@ -62,12 +67,20 @@ internal class DefaultMediaMetadataFactory( .setTrackNumber(index) .setTitle(title) - authors + author ?.let { builder.setArtist(it) } - cover.await() - ?.let { builder.maybeSetArtworkData(it, PICTURE_TYPE_FRONT_COVER) } + putCover(builder) return builder.build() } + + private suspend fun putCover(builder: MediaMetadata.Builder) { + cover + ?.let { builder.setArtworkUri(it) } + ?: run { + coverBytes.await() + ?.let { builder.setArtworkData(it, PICTURE_TYPE_FRONT_COVER) } + } + } } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/DefaultMediaMetadataProvider.kt b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/DefaultMediaMetadataProvider.kt similarity index 82% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/DefaultMediaMetadataProvider.kt rename to readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/DefaultMediaMetadataProvider.kt index 573bbc2725..18e5e24dd9 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/DefaultMediaMetadataProvider.kt +++ b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/DefaultMediaMetadataProvider.kt @@ -4,18 +4,19 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.api +package org.readium.navigator.media.common +import android.net.Uri import org.readium.r2.shared.publication.Publication /** * Builds a [MediaMetadataFactory] which will use the given title, author and cover, * and fall back on what is in the publication. */ -class DefaultMediaMetadataProvider( +public class DefaultMediaMetadataProvider( private val title: String? = null, private val author: String? = null, - private val cover: ByteArray? = null + private val cover: Uri? = null ) : MediaMetadataProvider { override fun createMetadataFactory(publication: Publication): MediaMetadataFactory { diff --git a/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/Media3Adapter.kt b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/Media3Adapter.kt new file mode 100644 index 0000000000..089d48ca61 --- /dev/null +++ b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/Media3Adapter.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.navigator.media.common + +import androidx.media3.common.Player +import org.readium.r2.shared.ExperimentalReadiumApi + +/** + * An object able to pass as a Jetpack media3 [Player]. + */ +@ExperimentalReadiumApi +public interface Media3Adapter { + public fun asMedia3Player(): Player +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/MediaMetadataFactory.kt b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/MediaMetadataFactory.kt similarity index 73% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/MediaMetadataFactory.kt rename to readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/MediaMetadataFactory.kt index 0e705f354b..ab8a139c52 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/MediaMetadataFactory.kt +++ b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/MediaMetadataFactory.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.api +package org.readium.navigator.media.common import androidx.media3.common.MediaMetadata @@ -13,15 +13,15 @@ import androidx.media3.common.MediaMetadata * * The metadata are used for example in the media-style Android notification. */ -interface MediaMetadataFactory { +public interface MediaMetadataFactory { /** * Creates the [MediaMetadata] for the whole publication. */ - suspend fun publicationMetadata(): MediaMetadata + public suspend fun publicationMetadata(): MediaMetadata /** * Creates the [MediaMetadata] for the reading order resource at the given [index]. */ - suspend fun resourceMetadata(index: Int): MediaMetadata + public suspend fun resourceMetadata(index: Int): MediaMetadata } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/MediaMetadataProvider.kt b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/MediaMetadataProvider.kt similarity index 64% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/MediaMetadataProvider.kt rename to readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/MediaMetadataProvider.kt index fb1a45d8d1..54ca323ab5 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/api/MediaMetadataProvider.kt +++ b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/MediaMetadataProvider.kt @@ -4,14 +4,14 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.api +package org.readium.navigator.media.common import org.readium.r2.shared.publication.Publication /** * To be implemented to use a custom [MediaMetadataFactory]. */ -fun interface MediaMetadataProvider { +public fun interface MediaMetadataProvider { - fun createMetadataFactory(publication: Publication): MediaMetadataFactory + public fun createMetadataFactory(publication: Publication): MediaMetadataFactory } diff --git a/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/MediaNavigator.kt b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/MediaNavigator.kt new file mode 100644 index 0000000000..aa198291cf --- /dev/null +++ b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/MediaNavigator.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.navigator.media.common + +import kotlinx.coroutines.flow.StateFlow +import org.readium.r2.navigator.Navigator +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.Closeable +import org.readium.r2.shared.util.Url + +/** + * A [Navigator] which can play multimedia content. + */ +@ExperimentalReadiumApi +public interface MediaNavigator< + L : MediaNavigator.Location, + P : MediaNavigator.Playback, + R : MediaNavigator.ReadingOrder + > : Navigator, Closeable { + + /** + * Location of the navigator. + */ + public interface Location { + + public val href: Url + } + + /** + * State of the player. + */ + public sealed interface State { + + /** + * The navigator is ready to play. + */ + public interface Ready : State + + /** + * The end of the media has been reached. + */ + public interface Ended : State + + /** + * The navigator cannot play because the buffer is starved. + */ + public interface Buffering : State + + /** + * The navigator cannot play because an error occurred. + */ + public interface Failure : State + } + + /** + * State of the playback. + */ + public interface Playback { + + /** + * The current state. + */ + public val state: State + + /** + * Indicates if the navigator should play as soon as the state is Ready. + */ + public val playWhenReady: Boolean + + /** + * Index of the reading order item currently being played. + */ + public val index: Int + } + + /** + * Data about the content to play. + */ + public interface ReadingOrder { + + /** + * List of items to play. + */ + public val items: List<Item> + + /** + * A piece of the content to play. + */ + public interface Item + } + + /** + * Current state of the playback. + */ + public val playback: StateFlow<P> + + /** + * Current location of the navigator. + */ + public val location: StateFlow<L> + + /** + * Reading order being read by this navigator. + */ + public val readingOrder: R + + /** + * Resumes the playback at the current location. + */ + public fun play() + + /** + * Pauses the playback. + */ + public fun pause() +} diff --git a/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/TextAwareMediaNavigator.kt b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/TextAwareMediaNavigator.kt new file mode 100644 index 0000000000..79075771fc --- /dev/null +++ b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/TextAwareMediaNavigator.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.navigator.media.common + +import kotlinx.coroutines.flow.StateFlow +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Locator + +/** + * A [MediaNavigator] aware of the utterances being read aloud. + */ +@ExperimentalReadiumApi +public interface TextAwareMediaNavigator< + L : TextAwareMediaNavigator.Location, + P : TextAwareMediaNavigator.Playback, + R : TextAwareMediaNavigator.ReadingOrder + > : MediaNavigator<L, P, R> { + + /** + * Location of the navigator. + */ + public interface Location : MediaNavigator.Location { + + /** + * The utterance being played. + */ + public val utterance: String + + /** + * The text right before the utterance being played, if any in the current item. + */ + public val textBefore: String? + + /** + * The text right after the utterance being played, if any in the current item. + */ + public val textAfter: String? + + /** + * The range of [utterance] being played, if known. + */ + public val range: IntRange? + + /** + * A locator pointing to the current utterance. + */ + public val utteranceLocator: Locator + + /** + * A locator pointing to the current token, if known. + */ + public val tokenLocator: Locator? + } + + /** + * State of the playback. + */ + public interface Playback : MediaNavigator.Playback { + + /** + * The utterance being played. + */ + public val utterance: String + + /** + * The range of [utterance] being played. + */ + public val range: IntRange? + } + + /** + * Data about the content to play. + */ + public interface ReadingOrder : MediaNavigator.ReadingOrder { + + /** + * List of items to play. + */ + override val items: List<Item> + + /** + * A piece of the content to play.. + */ + public interface Item : MediaNavigator.ReadingOrder.Item + } + + /** + * Current state of the playback. + */ + override val playback: StateFlow<P> + + /** + * Current location of the navigator. + */ + override val location: StateFlow<L> + + /** + * Reading order being read by this navigator. + */ + override val readingOrder: R + + /** + * Jumps to the previous. + * + * Does nothing if the current utterance is the first one. + */ + public fun skipToPreviousUtterance() + + /** + * Jumps to the next utterance. + * + * Does nothing if the current utterance is the last one. + */ + public fun skipToNextUtterance() + + /** + * Whether the current utterance has a previous one or is the first one. + */ + public fun hasPreviousUtterance(): Boolean + + /** + * Whether the current utterance has a next utterance or is the last one. + */ + public fun hasNextUtterance(): Boolean +} diff --git a/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/TimeBasedMediaNavigator.kt b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/TimeBasedMediaNavigator.kt new file mode 100644 index 0000000000..bb3e0ce1ce --- /dev/null +++ b/readium/navigators/media/common/src/main/java/org/readium/navigator/media/common/TimeBasedMediaNavigator.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.navigator.media.common + +import kotlin.time.Duration +import kotlinx.coroutines.flow.StateFlow +import org.readium.r2.shared.ExperimentalReadiumApi + +/** + * A [MediaNavigator] whose locations provide time offsets. + */ +@ExperimentalReadiumApi +public interface TimeBasedMediaNavigator<L : TimeBasedMediaNavigator.Location, P : TimeBasedMediaNavigator.Playback, + R : TimeBasedMediaNavigator.ReadingOrder> : MediaNavigator<L, P, R> { + + /** + * Location of the navigator. + */ + public interface Location : MediaNavigator.Location { + + /** + * The duration offset in the resource. + */ + public val offset: Duration + } + + /** + * State of the playback. + */ + public interface Playback : MediaNavigator.Playback { + + /** + * Position of the playback in the current item. + */ + public val offset: Duration + + /** + * Position in the current item until which the content is buffered. + */ + public val buffered: Duration? + } + + /** + * Data about the content to play. + */ + public interface ReadingOrder : MediaNavigator.ReadingOrder { + + /** + * Total duration of the content to play. + */ + public val duration: Duration? + + /** + * List of items to play. + */ + override val items: List<Item> + + /** + * A piece of the content to play. + */ + public interface Item : MediaNavigator.ReadingOrder.Item { + + /** + * Duration of the item. + */ + public val duration: Duration? + } + } + + /** + * Current state of the playback. + */ + override val playback: StateFlow<P> + + /** + * Current location of the navigator. + */ + override val location: StateFlow<L> + + /** + * Reading order being read by this navigator. + */ + override val readingOrder: R + + /** + * Skips to [offset] in the item at [index]. + */ + public fun skipTo(index: Int, offset: Duration) + + /** + * Skips [duration] either forward or backward if [duration] is negative. + */ + public fun skip(duration: Duration) + + /** + * Skips forward a small increment. + */ + public fun skipForward() + + /** + * Skips backward a small increment. + */ + public fun skipBackward() +} diff --git a/readium/navigators/media/src/main/AndroidManifest.xml b/readium/navigators/media/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2d10029868 --- /dev/null +++ b/readium/navigators/media/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest /> + diff --git a/readium/navigators/media/tts/build.gradle.kts b/readium/navigators/media/tts/build.gradle.kts new file mode 100644 index 0000000000..9e234ea5fc --- /dev/null +++ b/readium/navigators/media/tts/build.gradle.kts @@ -0,0 +1,63 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +plugins { + id("com.android.library") + kotlin("android") + kotlin("plugin.parcelize") + kotlin("plugin.serialization") +} + +android { + resourcePrefix = "readium_" + + compileSdk = 34 + + defaultConfig { + minSdk = 21 + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + freeCompilerArgs = freeCompilerArgs + listOf( + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=org.readium.r2.shared.InternalReadiumApi" + ) + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android.txt")) + } + } + buildFeatures { + viewBinding = true + } + namespace = "org.readium.navigators.media.tts" +} + +kotlin { + explicitApi() +} + +rootProject.ext["publish.artifactId"] = "readium-navigator-media-tts" +apply(from = "$rootDir/scripts/publish-module.gradle") + +dependencies { + api(project(":readium:navigators:media:readium-navigator-media-common")) + + implementation(libs.androidx.media3.common) + implementation(libs.androidx.media3.session) + + implementation(libs.timber) + implementation(libs.bundles.coroutines) + implementation(libs.kotlinx.serialization.json) +} diff --git a/readium/navigators/media/tts/src/main/AndroidManifest.xml b/readium/navigators/media/tts/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2d10029868 --- /dev/null +++ b/readium/navigators/media/tts/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest /> + diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsAliases.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsAliases.kt new file mode 100644 index 0000000000..007c9df020 --- /dev/null +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsAliases.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.navigator.media.tts + +import org.readium.navigator.media.tts.android.AndroidTtsEngine +import org.readium.navigator.media.tts.android.AndroidTtsPreferences +import org.readium.navigator.media.tts.android.AndroidTtsPreferencesEditor +import org.readium.navigator.media.tts.android.AndroidTtsSettings +import org.readium.r2.shared.ExperimentalReadiumApi + +@OptIn(ExperimentalReadiumApi::class) +public typealias AndroidTtsNavigatorFactory = TtsNavigatorFactory<AndroidTtsSettings, AndroidTtsPreferences, AndroidTtsPreferencesEditor, AndroidTtsEngine.Error, AndroidTtsEngine.Voice> + +@OptIn(ExperimentalReadiumApi::class) +public typealias AndroidTtsNavigator = TtsNavigator<AndroidTtsSettings, AndroidTtsPreferences, AndroidTtsEngine.Error, AndroidTtsEngine.Voice> diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsEngine.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngine.kt similarity index 66% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsEngine.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngine.kt index 36ec182284..25c6ecb281 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsEngine.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngine.kt @@ -4,69 +4,70 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts +package org.readium.navigator.media.tts import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.Closeable +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Language /** * A text-to-speech engine synthesizes text utterances (e.g. sentence). */ @ExperimentalReadiumApi -interface TtsEngine<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, +public interface TtsEngine<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, E : TtsEngine.Error, V : TtsEngine.Voice> : Configurable<S, P>, Closeable { - interface Preferences<P : Configurable.Preferences<P>> : Configurable.Preferences<P> { + public interface Preferences<P : Configurable.Preferences<P>> : Configurable.Preferences<P> { /** * The default language to use when no language information is passed to [speak]. */ - val language: Language? + public val language: Language? } - interface Settings : Configurable.Settings { + public interface Settings : Configurable.Settings { /** * The default language to use when no language information is passed to [speak]. */ - val language: Language? + public val language: Language? /** * Whether language information in content should be superseded by [language]. */ - val overrideContentLanguage: Boolean + public val overrideContentLanguage: Boolean } - interface Voice { + public interface Voice { /** * The voice's language. */ - val language: Language + public val language: Language } /** * Marker interface for the errors that the [TtsEngine] returns. */ - interface Error + public interface Error : org.readium.r2.shared.util.Error /** * An id to identify a request to speak. */ @JvmInline - value class RequestId(val id: String) + public value class RequestId(public val value: String) /** * TTS engine callbacks. */ - interface Listener<E : Error> { + public interface Listener<E : Error> { /** * Called when the utterance with the given id starts as perceived by the caller. */ - fun onStart(requestId: RequestId) + public fun onStart(requestId: RequestId) /** * Called when the [TtsEngine] is about to speak the specified [range] of the utterance with @@ -74,48 +75,48 @@ interface TtsEngine<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, * * This callback may not be called if the [TtsEngine] does not provide range information. */ - fun onRange(requestId: RequestId, range: IntRange) + public fun onRange(requestId: RequestId, range: IntRange) /** * Called if the utterance with the given id has been stopped while in progress * by a call to [stop]. */ - fun onInterrupted(requestId: RequestId) + public fun onInterrupted(requestId: RequestId) /** * Called when the utterance with the given id has been flushed from the synthesis queue * by a call to [stop]. */ - fun onFlushed(requestId: RequestId) + public fun onFlushed(requestId: RequestId) /** * Called when the utterance with the given id has successfully completed processing. */ - fun onDone(requestId: RequestId) + public fun onDone(requestId: RequestId) /** * Called when an error has occurred during processing of the utterance with the given id. */ - fun onError(requestId: RequestId, error: E) + public fun onError(requestId: RequestId, error: E) } /** * Sets of voices available with this [TtsEngine]. */ - val voices: Set<V> + public val voices: Set<V> /** * Enqueues a new speak request. */ - fun speak(requestId: RequestId, text: String, language: Language?) + public fun speak(requestId: RequestId, text: String, language: Language?) /** * Stops the [TtsEngine]. */ - fun stop() + public fun stop() /** * Sets a new listener or removes the current one. */ - fun setListener(listener: Listener<E>?) + public fun setListener(listener: Listener<E>?) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsEngineFacade.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngineFacade.kt similarity index 72% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsEngineFacade.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngineFacade.kt index 5f5151ca28..e96eaadada 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsEngineFacade.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngineFacade.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts +package org.readium.navigator.media.tts import java.util.* import kotlinx.coroutines.CancellableContinuation @@ -26,8 +26,6 @@ internal class TtsEngineFacade<S : TtsEngine.Settings, P : TtsEngine.Preferences engine.setListener(listener) } - private var currentTask: UtteranceTask<E>? = null - val voices: Set<V> get() = engine.voices @@ -45,54 +43,44 @@ internal class TtsEngineFacade<S : TtsEngine.Settings, P : TtsEngine.Preferences engine.close() } + private var currentTask: UtteranceTask<E>? = null + private data class UtteranceTask<E : TtsEngine.Error>( val requestId: TtsEngine.RequestId, val continuation: CancellableContinuation<E?>, val onRange: (IntRange) -> Unit ) + private fun getTask(id: TtsEngine.RequestId) = + currentTask?.takeIf { it.requestId == id } + + private fun popTask(id: TtsEngine.RequestId) = + getTask(id) + ?.also { currentTask = null } + private inner class EngineListener : TtsEngine.Listener<E> { override fun onStart(requestId: TtsEngine.RequestId) { } override fun onRange(requestId: TtsEngine.RequestId, range: IntRange) { - currentTask - ?.takeIf { it.requestId == requestId } - ?.onRange - ?.invoke(range) + getTask(requestId)?.onRange?.invoke(range) } override fun onInterrupted(requestId: TtsEngine.RequestId) { - currentTask - ?.takeIf { it.requestId == requestId } - ?.continuation - ?.cancel() - currentTask = null + popTask(requestId)?.continuation?.cancel() } override fun onFlushed(requestId: TtsEngine.RequestId) { - currentTask - ?.takeIf { it.requestId == requestId } - ?.continuation - ?.cancel() - currentTask = null + popTask(requestId)?.continuation?.cancel() } override fun onDone(requestId: TtsEngine.RequestId) { - currentTask - ?.takeIf { it.requestId == requestId } - ?.continuation - ?.resume(null) {} - currentTask = null + popTask(requestId)?.continuation?.resume(null) {} } override fun onError(requestId: TtsEngine.RequestId, error: E) { - currentTask - ?.takeIf { it.requestId == requestId } - ?.continuation - ?.resume(error) {} - currentTask = null + popTask(requestId)?.continuation?.resume(error) {} } } } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsEngineProvider.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngineProvider.kt similarity index 61% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsEngineProvider.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngineProvider.kt index 5e1d26e580..a8870eed98 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsEngineProvider.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngineProvider.kt @@ -4,48 +4,53 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts +package org.readium.navigator.media.tts import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackParameters import org.readium.r2.navigator.preferences.PreferencesEditor import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Try /** * To be implemented by adapters for third-party TTS engines which can be used with [TtsNavigator]. */ @ExperimentalReadiumApi -interface TtsEngineProvider<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, E : PreferencesEditor<P>, +public interface TtsEngineProvider<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, E : PreferencesEditor<P>, F : TtsEngine.Error, V : TtsEngine.Voice> { /** * Creates a [TtsEngine] for [publication] and [initialPreferences]. */ - suspend fun createEngine(publication: Publication, initialPreferences: P): TtsEngine<S, P, F, V>? + public suspend fun createEngine(publication: Publication, initialPreferences: P): Try<TtsEngine<S, P, F, V>, Error> /** * Creates a preferences editor for [publication] and [initialPreferences]. */ - fun createPreferencesEditor(publication: Publication, initialPreferences: P): E + public fun createPreferencesEditor(publication: Publication, initialPreferences: P): E /** * Creates an empty set of preferences of this TTS engine provider. */ - fun createEmptyPreferences(): P + public fun createEmptyPreferences(): P /** * Computes Media3 [PlaybackParameters] from the given [settings]. */ - fun getPlaybackParameters(settings: S): PlaybackParameters + public fun getPlaybackParameters(settings: S): PlaybackParameters /** * Updates [previousPreferences] to honor the given Media3 [playbackParameters]. */ - fun updatePlaybackParameters(previousPreferences: P, playbackParameters: PlaybackParameters): P + public fun updatePlaybackParameters( + previousPreferences: P, + playbackParameters: PlaybackParameters + ): P /** * Maps an engine-specific error to Media3 [PlaybackException]. */ - fun mapEngineError(error: F): PlaybackException + public fun mapEngineError(error: F): PlaybackException } diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt new file mode 100644 index 0000000000..f0a21bdb09 --- /dev/null +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.navigator.media.tts + +import androidx.media3.common.Player +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import org.readium.navigator.media.common.Media3Adapter +import org.readium.navigator.media.common.MediaNavigator +import org.readium.navigator.media.common.TextAwareMediaNavigator +import org.readium.navigator.media.tts.session.TtsSessionAdapter +import org.readium.r2.navigator.extensions.normalizeLocator +import org.readium.r2.navigator.preferences.Configurable +import org.readium.r2.shared.DelicateReadiumApi +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.extensions.combineStateIn +import org.readium.r2.shared.extensions.mapStateIn +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Url + +/** + * A navigator to read aloud a [Publication] with a TTS engine. + */ +@ExperimentalReadiumApi +@OptIn(DelicateReadiumApi::class) +public class TtsNavigator<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, + E : TtsEngine.Error, V : TtsEngine.Voice> internal constructor( + coroutineScope: CoroutineScope, + private val publication: Publication, + private val player: TtsPlayer<S, P, E, V>, + private val sessionAdapter: TtsSessionAdapter<E> +) : + MediaNavigator<TtsNavigator.Location, TtsNavigator.Playback, TtsNavigator.ReadingOrder>, + TextAwareMediaNavigator<TtsNavigator.Location, TtsNavigator.Playback, TtsNavigator.ReadingOrder>, + Media3Adapter, + Configurable<S, P> { + + public interface Listener { + + public fun onStopRequested() + } + + public data class Location( + override val href: Url, + override val utterance: String, + override val range: IntRange?, + override val textBefore: String?, + override val textAfter: String?, + override val utteranceLocator: Locator, + override val tokenLocator: Locator? + ) : TextAwareMediaNavigator.Location + + public data class Playback( + override val state: MediaNavigator.State, + override val playWhenReady: Boolean, + override val index: Int, + override val utterance: String, + override val range: IntRange? + ) : TextAwareMediaNavigator.Playback + + public data class ReadingOrder( + override val items: List<Item> + ) : TextAwareMediaNavigator.ReadingOrder { + + public data class Item( + val href: Url + ) : TextAwareMediaNavigator.ReadingOrder.Item + } + + public sealed class State { + + public object Ready : MediaNavigator.State.Ready + + public object Ended : MediaNavigator.State.Ended + + public data class Failure(val error: Error) : MediaNavigator.State.Failure + } + + public sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { + + public class EngineError<E : TtsEngine.Error> (override val cause: E) : + Error("An error occurred in the TTS engine.", cause) + + public class ContentError(cause: org.readium.r2.shared.util.Error) : + Error("An error occurred while trying to read publication content.", cause) + } + + public val voices: Set<V> get() = + player.voices + + override val readingOrder: ReadingOrder = + ReadingOrder( + items = publication.readingOrder + .map { ReadingOrder.Item(it.url()) } + ) + + override val playback: StateFlow<Playback> = + player.playback.combineStateIn(coroutineScope, player.utterance) { playback, utterance -> + navigatorPlayback(playback, utterance) + } + + override val location: StateFlow<Location> = + player.utterance.mapStateIn(coroutineScope) { playerUtterance -> + playerUtterance.toPosition() + } + + override fun play() { + player.play() + } + + override fun pause() { + player.pause() + } + + public fun go(locator: Locator) { + player.go(publication.normalizeLocator(locator)) + } + + override fun skipToPreviousUtterance() { + player.previousUtterance() + } + + override fun skipToNextUtterance() { + player.nextUtterance() + } + + override fun hasPreviousUtterance(): Boolean { + return player.hasPreviousUtterance() + } + + override fun hasNextUtterance(): Boolean { + return player.hasNextUtterance() + } + + override fun asMedia3Player(): Player = + sessionAdapter + + override fun close() { + player.close() + sessionAdapter.release() + } + + override val currentLocator: StateFlow<Locator> = + location.mapStateIn(coroutineScope) { it.tokenLocator ?: it.utteranceLocator } + + override fun go(locator: Locator, animated: Boolean, completion: () -> Unit): Boolean { + player.go(publication.normalizeLocator(locator)) + return true + } + + override fun go(link: Link, animated: Boolean, completion: () -> Unit): Boolean { + val locator = publication.locatorFromLink(link) ?: return false + return go(locator, animated, completion) + } + + override val settings: StateFlow<S> = + player.settings + + override fun submitPreferences(preferences: P) { + player.submitPreferences(preferences) + player.restartUtterance() + } + + private fun navigatorPlayback(playback: TtsPlayer.Playback, utterance: TtsPlayer.Utterance) = + Playback( + state = playback.state.toState(), + playWhenReady = playback.playWhenReady, + index = utterance.position.resourceIndex, + utterance = utterance.text, + range = utterance.range + ) + + private fun TtsPlayer.State.toState() = + when (this) { + TtsPlayer.State.Ready -> State.Ready + TtsPlayer.State.Ended -> State.Ended + is TtsPlayer.State.Failure -> this.toError() + } + + private fun TtsPlayer.State.Failure.toError(): State.Failure = + when (this) { + is TtsPlayer.State.Failure.Content -> State.Failure(Error.ContentError(error)) + is TtsPlayer.State.Failure.Engine<*> -> State.Failure(Error.EngineError(error)) + } + + private fun TtsPlayer.Utterance.toPosition(): Location { + val currentLink = publication.readingOrder[position.resourceIndex] + val url = currentLink.url() + + val utteranceLocator = publication + .locatorFromLink(currentLink)!! + .copy( + locations = position.locations, + text = position.text + ) + + val tokenLocator = range + ?.let { utteranceLocator.copy(text = utteranceLocator.text.substring(it)) } + + return Location( + href = url, + textBefore = position.text.before, + textAfter = position.text.after, + utterance = text, + range = range, + utteranceLocator = utteranceLocator, + tokenLocator = tokenLocator + ) + } +} diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt new file mode 100644 index 0000000000..27deebac6a --- /dev/null +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt @@ -0,0 +1,230 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.navigator.media.tts + +import android.app.Application +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackParameters +import kotlinx.coroutines.MainScope +import org.readium.navigator.media.common.DefaultMediaMetadataProvider +import org.readium.navigator.media.common.MediaMetadataProvider +import org.readium.navigator.media.tts.android.AndroidTtsDefaults +import org.readium.navigator.media.tts.android.AndroidTtsEngine +import org.readium.navigator.media.tts.android.AndroidTtsEngineProvider +import org.readium.navigator.media.tts.session.TtsSessionAdapter +import org.readium.r2.navigator.extensions.normalizeLocator +import org.readium.r2.navigator.preferences.PreferencesEditor +import org.readium.r2.shared.DelicateReadiumApi +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.extensions.mapStateIn +import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.services.content.Content +import org.readium.r2.shared.publication.services.content.ContentService +import org.readium.r2.shared.publication.services.content.content +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Language +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.tokenizer.DefaultTextContentTokenizer +import org.readium.r2.shared.util.tokenizer.TextTokenizer +import org.readium.r2.shared.util.tokenizer.TextUnit + +@ExperimentalReadiumApi +@OptIn(DelicateReadiumApi::class) +public class TtsNavigatorFactory<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, E : PreferencesEditor<P>, + F : TtsEngine.Error, V : TtsEngine.Voice> private constructor( + private val application: Application, + private val publication: Publication, + private val ttsEngineProvider: TtsEngineProvider<S, P, E, F, V>, + private val tokenizerFactory: (language: Language?) -> TextTokenizer, + private val metadataProvider: MediaMetadataProvider +) { + public companion object { + + public operator fun invoke( + application: Application, + publication: Publication, + tokenizerFactory: (language: Language?) -> TextTokenizer = defaultTokenizerFactory, + metadataProvider: MediaMetadataProvider = defaultMediaMetadataProvider, + defaults: AndroidTtsDefaults = AndroidTtsDefaults(), + voiceSelector: (Language?, Set<AndroidTtsEngine.Voice>) -> AndroidTtsEngine.Voice? = defaultVoiceSelector + ): AndroidTtsNavigatorFactory? { + val engineProvider = AndroidTtsEngineProvider( + context = application, + defaults = defaults, + voiceSelector = voiceSelector + ) + + return createNavigatorFactory( + application, + publication, + engineProvider, + tokenizerFactory, + metadataProvider + ) + } + + public operator fun <S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, E : PreferencesEditor<P>, + F : TtsEngine.Error, V : TtsEngine.Voice> invoke( + application: Application, + publication: Publication, + ttsEngineProvider: TtsEngineProvider<S, P, E, F, V>, + tokenizerFactory: (language: Language?) -> TextTokenizer = defaultTokenizerFactory, + metadataProvider: MediaMetadataProvider = defaultMediaMetadataProvider + ): TtsNavigatorFactory<S, P, E, F, V>? = + createNavigatorFactory( + application, + publication, + ttsEngineProvider, + tokenizerFactory, + metadataProvider + ) + + private fun <S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, E : PreferencesEditor<P>, + F : TtsEngine.Error, V : TtsEngine.Voice> createNavigatorFactory( + application: Application, + publication: Publication, + ttsEngineProvider: TtsEngineProvider<S, P, E, F, V>, + tokenizerFactory: (language: Language?) -> TextTokenizer, + metadataProvider: MediaMetadataProvider + ): TtsNavigatorFactory<S, P, E, F, V>? { + publication.content() ?: return null + + return TtsNavigatorFactory( + application, + publication, + ttsEngineProvider, + tokenizerFactory, + metadataProvider + ) + } + + /** + * The default content tokenizer will split the [Content.Element] items into individual sentences. + */ + private val defaultTokenizerFactory: (Language?) -> TextTokenizer = { language -> + DefaultTextContentTokenizer(TextUnit.Sentence, language) + } + + private val defaultMediaMetadataProvider: MediaMetadataProvider = + DefaultMediaMetadataProvider() + + private val defaultVoiceSelector: (Language?, Set<AndroidTtsEngine.Voice>) -> AndroidTtsEngine.Voice? = + { _, _ -> null } + } + + public sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { + + public class UnsupportedPublication( + cause: org.readium.r2.shared.util.Error? = null + ) : Error("Publication is not supported.", cause) + + public class EngineInitialization( + cause: org.readium.r2.shared.util.Error? = null + ) : Error("Failed to initialize TTS engine.", cause) + } + + public suspend fun createNavigator( + listener: TtsNavigator.Listener, + initialLocator: Locator? = null, + initialPreferences: P? = null + ): Try<TtsNavigator<S, P, F, V>, Error> { + if (publication.findService(ContentService::class) == null) { + return Try.failure( + Error.UnsupportedPublication( + DebugError("No content service found in publication.") + ) + ) + } + + @Suppress("NAME_SHADOWING") + val initialLocator = + initialLocator?.let { publication.normalizeLocator(it) } + + val actualInitialPreferences = + initialPreferences + ?: ttsEngineProvider.createEmptyPreferences() + + val contentIterator = + TtsUtteranceIterator(publication, tokenizerFactory, initialLocator) + if (!contentIterator.hasNext()) { + return Try.failure( + Error.UnsupportedPublication( + DebugError("Content iterator is empty.") + ) + ) + } + + val ttsEngine = + ttsEngineProvider.createEngine(publication, actualInitialPreferences) + .getOrElse { + return Try.failure( + Error.EngineInitialization() + ) + } + + val metadataFactory = + metadataProvider.createMetadataFactory(publication) + + val playlistMetadata = + metadataFactory.publicationMetadata() + + val mediaItems = + publication.readingOrder.indices.map { index -> + val metadata = metadataFactory.resourceMetadata(index) + MediaItem.Builder() + .setMediaMetadata(metadata) + .build() + } + + val ttsPlayer = + TtsPlayer(ttsEngine, contentIterator, actualInitialPreferences) + ?: return Try.failure( + Error.UnsupportedPublication(DebugError("Empty content.")) + ) + + val coroutineScope = + MainScope() + + val playbackParameters = + ttsPlayer.settings.mapStateIn(coroutineScope) { + ttsEngineProvider.getPlaybackParameters(it) + } + + val onSetPlaybackParameters = { parameters: PlaybackParameters -> + val newPreferences = ttsEngineProvider.updatePlaybackParameters( + ttsPlayer.lastPreferences, + parameters + ) + ttsPlayer.submitPreferences(newPreferences) + } + + val sessionAdapter = + TtsSessionAdapter( + application, + ttsPlayer, + playlistMetadata, + mediaItems, + listener::onStopRequested, + playbackParameters, + onSetPlaybackParameters, + ttsEngineProvider::mapEngineError + ) + + val ttsNavigator = + TtsNavigator(coroutineScope, publication, ttsPlayer, sessionAdapter) + + return Try.success(ttsNavigator) + } + + public fun createPreferencesEditor(preferences: P): E = + ttsEngineProvider.createPreferencesEditor(publication, preferences) +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsPlayer.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsPlayer.kt similarity index 69% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsPlayer.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsPlayer.kt index d98a450d62..b934be68bb 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsPlayer.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsPlayer.kt @@ -4,28 +4,43 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts +package org.readium.navigator.media.tts +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack +import android.os.Build import kotlin.coroutines.coroutineContext -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.ThrowableError +import timber.log.Timber /** - * Plays the content from a [TtsContentIterator] with a [TtsEngine]. + * Plays the content from a [TtsUtteranceIterator] with a [TtsEngine]. */ @ExperimentalReadiumApi internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, E : TtsEngine.Error, V : TtsEngine.Voice> private constructor( private val engineFacade: TtsEngineFacade<S, P, E, V>, - private val contentIterator: TtsContentIterator, + private val contentIterator: TtsUtteranceIterator, initialWindow: UtteranceWindow, initialPreferences: P ) : Configurable<S, P> { @@ -35,10 +50,9 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, suspend operator fun <S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, E : TtsEngine.Error, V : TtsEngine.Voice> invoke( engine: TtsEngine<S, P, E, V>, - contentIterator: TtsContentIterator, - initialPreferences: P, + contentIterator: TtsUtteranceIterator, + initialPreferences: P ): TtsPlayer<S, P, E, V>? { - val initialContext = tryOrNull { contentIterator.startContext() } ?: return null @@ -55,23 +69,23 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, ) } - private suspend fun TtsContentIterator.startContext(): UtteranceWindow? { - val previousUtterance = previousUtterance() - val currentUtterance = nextUtterance() + private suspend fun TtsUtteranceIterator.startContext(): UtteranceWindow? { + val previousUtterance = previous() + val currentUtterance = next() val startWindow = if (currentUtterance != null) { UtteranceWindow( previousUtterance = previousUtterance, currentUtterance = currentUtterance, - nextUtterance = nextUtterance(), + nextUtterance = next(), ended = false ) } else { val actualCurrentUtterance = previousUtterance ?: return null - val actualPreviousUtterance = previousUtterance() + val actualPreviousUtterance = previous() // Go back to the end of the iterator. - nextUtterance() + next() UtteranceWindow( previousUtterance = actualPreviousUtterance, @@ -93,27 +107,27 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, /** * The player is ready to play. */ - object Ready : State + data object Ready : State /** * The end of the media has been reached. */ - object Ended : State + data object Ended : State /** * The player cannot play because an error occurred. */ - sealed class Error : State { + sealed class Failure : State { - data class EngineError<E : TtsEngine.Error> (val error: E) : Error() + data class Engine<E : TtsEngine.Error> (val error: E) : Failure() - data class ContentError(val exception: Exception) : Error() + data class Content(val error: Error) : Failure() } } data class Playback( val state: State, - val playWhenReady: Boolean, + val playWhenReady: Boolean ) data class Utterance( @@ -124,16 +138,15 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, data class Position( val resourceIndex: Int, - val cssSelector: String, - val textBefore: String?, - val textAfter: String?, + val locations: Locator.Locations, + val text: Locator.Text ) } private data class UtteranceWindow( - val previousUtterance: TtsContentIterator.Utterance?, - val currentUtterance: TtsContentIterator.Utterance, - val nextUtterance: TtsContentIterator.Utterance?, + val previousUtterance: TtsUtteranceIterator.Utterance?, + val currentUtterance: TtsUtteranceIterator.Utterance, + val nextUtterance: TtsUtteranceIterator.Utterance?, val ended: Boolean = false ) @@ -184,35 +197,81 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, } fun play() { - coroutineScope.launch { - playAsync() - } - } + // This can be called by the session adapter with a pending intent for a foreground service + // if playWhenReady is false or the state is Ended. + // We must keep or transition to a state which will be translated by media3 to a + // foreground state. + // If the state was State.Ended, it will get back to its initial value later. - private suspend fun playAsync() = mutex.withLock { - if (isPlaying()) { + if (playbackMutable.value.playWhenReady && playback.value.state == State.Ready) { return } - playbackMutable.value = playbackMutable.value.copy(playWhenReady = true) - playIfReadyAndNotPaused() - } + playbackMutable.value = + playbackMutable.value.copy(state = State.Ready, playWhenReady = true) - fun pause() { coroutineScope.launch { - pauseAsync() + mutex.withLock { + // WORKAROUND to get the media buttons correctly working when an audio player was + // running before. + fakePlayingAudio() + playIfReadyAndNotPaused() + } } } - private suspend fun pauseAsync() = mutex.withLock { + private fun fakePlayingAudio() { + val audioAttributes = + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .build() + + val audioFormat = + AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) + .build() + + val bufferSize = 8092 + + val audioTrack = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + tryOrNull { + AudioTrack.Builder() + .setAudioAttributes(audioAttributes) + .setAudioFormat(audioFormat) + .setBufferSizeInBytes(bufferSize) + .build() + } + } else { + AudioTrack( + audioAttributes, + audioFormat, + bufferSize, + AudioTrack.MODE_STREAM, + AudioManager.AUDIO_SESSION_ID_GENERATE + ).takeIf { it.state == AudioTrack.STATE_INITIALIZED } + } + + audioTrack + ?.play() + ?: run { Timber.e("Couldn't fake playing audio.") } + } + + fun pause() { if (!playbackMutable.value.playWhenReady) { return } playbackMutable.value = playbackMutable.value.copy(playWhenReady = false) utteranceMutable.value = utteranceMutable.value.copy(range = null) - playbackJob?.cancelAndJoin() - Unit + + coroutineScope.launch { + mutex.withLock { + playbackJob?.cancelAndJoin() + } + } } fun tryRecover() { @@ -257,19 +316,16 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, } fun restartUtterance() { - coroutineScope.launch { - restartUtteranceAsync() - } - } - - private suspend fun restartUtteranceAsync() = mutex.withLock { - playbackJob?.cancel() if (playbackMutable.value.state == State.Ended) { playbackMutable.value = playbackMutable.value.copy(state = State.Ready) } - utteranceMutable.value = utteranceMutable.value.copy(range = null) - playbackJob?.join() - playIfReadyAndNotPaused() + + coroutineScope.launch { + playbackJob?.cancel() + playbackJob?.join() + utteranceMutable.value = utteranceMutable.value.copy(range = null) + playIfReadyAndNotPaused() + } } fun hasNextUtterance() = @@ -311,9 +367,11 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, playIfReadyAndNotPaused() } + @Suppress("unused") fun hasNextResource(): Boolean = utteranceMutable.value.position.resourceIndex + 1 < contentIterator.resourceCount + @Suppress("unused") fun nextResource() { coroutineScope.launch { nextResourceAsync() @@ -333,9 +391,11 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, playIfReadyAndNotPaused() } + @Suppress("MemberVisibilityCanBePrivate") fun hasPreviousResource(): Boolean = utteranceMutable.value.position.resourceIndex > 0 + @Suppress("unused") fun previousResource() { coroutineScope.launch { previousResourceAsync() @@ -369,23 +429,23 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, val previousUtterance = try { // Get previously currentUtterance once more - contentIterator.previousUtterance() + contentIterator.previous() // Get previously previousUtterance once more - contentIterator.previousUtterance() + contentIterator.previous() // Get new previous utterance - val previousUtterance = contentIterator.previousUtterance() + val previousUtterance = contentIterator.previous() // Go to currentUtterance position - contentIterator.nextUtterance() + contentIterator.next() // Go to nextUtterance position - contentIterator.nextUtterance() + contentIterator.next() previousUtterance } catch (e: Exception) { - onContentError(e) + onContentException(e) return } @@ -406,9 +466,9 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, } val nextUtterance = try { - contentIterator.nextUtterance() + contentIterator.next() } catch (e: Exception) { - onContentError(e) + onContentException(e) return } @@ -427,7 +487,7 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, val startContext = try { contentIterator.startContext() } catch (e: Exception) { - onContentError(e) + onContentException(e) return } utteranceWindow = checkNotNull(startContext) @@ -438,7 +498,7 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, private fun onEndReached() { playbackMutable.value = playbackMutable.value.copy( - state = State.Ended, + state = State.Ended ) } @@ -456,19 +516,29 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, playContinuous() } - private suspend fun speakUtterance(utterance: TtsContentIterator.Utterance): E? = - engineFacade.speak(utterance.text, utterance.language, ::onRangeChanged) + private suspend fun speakUtterance(utterance: TtsUtteranceIterator.Utterance): E? = + engineFacade.speak(utterance.utterance, utterance.language, ::onRangeChanged) private fun onEngineError(error: E) { playbackMutable.value = playbackMutable.value.copy( - state = State.Error.EngineError(error) + state = State.Failure.Engine(error) ) playbackJob?.cancel() } - private fun onContentError(exception: Exception) { + private fun onContentException(exception: Exception) { + val error = + if (exception is ErrorException) { + exception.error + } else { + ThrowableError(exception) + } + onContentError(error) + } + + private fun onContentError(error: Error) { playbackMutable.value = playbackMutable.value.copy( - state = State.Error.ContentError(exception) + state = State.Failure.Content(error) ) playbackJob?.cancel() } @@ -490,18 +560,14 @@ internal class TtsPlayer<S : TtsEngine.Settings, P : TtsEngine.Preferences<P>, contentIterator.overrideContentLanguage = engineFacade.settings.value.overrideContentLanguage } - private fun isPlaying() = - playbackMutable.value.playWhenReady && playback.value.state == State.Ready - - private fun TtsContentIterator.Utterance.ttsPlayerUtterance(): Utterance = + private fun TtsUtteranceIterator.Utterance.ttsPlayerUtterance(): Utterance = Utterance( - text = text, + text = utterance, range = null, position = Utterance.Position( resourceIndex = resourceIndex, - cssSelector = cssSelector, - textAfter = textAfter, - textBefore = textBefore + locations = locations, + text = text ) ) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsContentIterator.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsUtteranceIterator.kt similarity index 83% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsContentIterator.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsUtteranceIterator.kt index 04e738242b..f3179ab5a7 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/TtsContentIterator.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsUtteranceIterator.kt @@ -4,12 +4,11 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts +package org.readium.navigator.media.tts import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.html.cssSelector import org.readium.r2.shared.publication.indexOfFirstWithHref import org.readium.r2.shared.publication.services.content.Content import org.readium.r2.shared.publication.services.content.ContentService @@ -25,17 +24,16 @@ import org.readium.r2.shared.util.tokenizer.TextTokenizer * * Not thread-safe. */ -internal class TtsContentIterator( +internal class TtsUtteranceIterator( private val publication: Publication, private val tokenizerFactory: (language: Language?) -> TextTokenizer, initialLocator: Locator? ) { data class Utterance( + val utterance: String, val resourceIndex: Int, - val cssSelector: String, - val text: String, - val textBefore: String?, - val textAfter: String?, + val locations: Locator.Locations, + val text: Locator.Text, val language: Language? ) @@ -107,17 +105,30 @@ internal class TtsContentIterator( private fun createIterator(locator: Locator?): Content.Iterator = contentService.content(locator).iterator() + suspend fun hasPrevious(): Boolean = + hasNextIn(Direction.Backward) + + suspend fun hasNext(): Boolean = + hasNextIn(Direction.Forward) + + private suspend fun hasNextIn(direction: Direction): Boolean { + if (utterances.isEmpty()) { + loadNextUtterances(direction) + } + return utterances.hasNextIn(direction) + } + /** * Advances to the previous item and returns it, or null if we reached the beginning. */ - suspend fun previousUtterance(): Utterance? = - nextUtterance(Direction.Backward) + suspend fun previous(): Utterance? = + next(Direction.Backward) /** * Advances to the next item and returns it, or null if we reached the end. */ - suspend fun nextUtterance(): Utterance? = - nextUtterance(Direction.Forward) + suspend fun next(): Utterance? = + next(Direction.Forward) private enum class Direction { Forward, Backward; @@ -127,10 +138,10 @@ internal class TtsContentIterator( * Gets the next utterance in the given [direction], or null when reaching the beginning or the * end. */ - private suspend fun nextUtterance(direction: Direction): Utterance? { + private suspend fun next(direction: Direction): Utterance? { val utterance = utterances.nextIn(direction) if (utterance == null && loadNextUtterances(direction)) { - return nextUtterance(direction) + return next(direction) } return utterance } @@ -168,7 +179,7 @@ internal class TtsContentIterator( */ private fun Content.Element.tokenize(): List<Content.Element> { val contentTokenizer = TextContentTokenizer( - language = this@TtsContentIterator.language, + language = this@TtsUtteranceIterator.language, textTokenizerFactory = tokenizerFactory, overrideContentLanguage = overrideContentLanguage ) @@ -180,22 +191,19 @@ internal class TtsContentIterator( */ private fun Content.Element.utterances(): List<Utterance> { fun utterance(text: String, locator: Locator, language: Language? = null): Utterance? { - if (!text.any { it.isLetterOrDigit() }) + if (!text.any { it.isLetterOrDigit() }) { return null + } val resourceIndex = publication.readingOrder.indexOfFirstWithHref(locator.href) ?: throw IllegalStateException("Content Element cannot be found in readingOrder.") - val cssSelector = locator.locations.cssSelector - ?: throw IllegalStateException("Css selectors are expected in iterator locators.") - return Utterance( - text = text, - language = language, + utterance = text, resourceIndex = resourceIndex, - textBefore = locator.text.before, - textAfter = locator.text.after, - cssSelector = cssSelector, + locations = locator.locations, + text = locator.text, + language = language ) } @@ -222,6 +230,12 @@ internal class TtsContentIterator( } } + private fun <E> CursorList<E>.hasNextIn(direction: Direction): Boolean = + when (direction) { + Direction.Forward -> hasNext() + Direction.Backward -> hasPrevious() + } + private fun <E> CursorList<E>.nextIn(direction: Direction): E? = when (direction) { Direction.Forward -> if (hasNext()) next() else null diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsDefaults.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsDefaults.kt similarity index 89% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsDefaults.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsDefaults.kt index a46e75eb7b..79d9b9afd4 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsDefaults.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsDefaults.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts.android +package org.readium.navigator.media.tts.android import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.Language @@ -18,7 +18,7 @@ import org.readium.r2.shared.util.Language * @see AndroidTtsPreferences */ @ExperimentalReadiumApi -data class AndroidTtsDefaults( +public data class AndroidTtsDefaults( val language: Language? = null, val pitch: Double? = null, val speed: Double? = null diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngine.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngine.kt new file mode 100644 index 0000000000..0639f30306 --- /dev/null +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngine.kt @@ -0,0 +1,494 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.navigator.media.tts.android + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.speech.tts.TextToSpeech +import android.speech.tts.TextToSpeech.* +import android.speech.tts.UtteranceProgressListener +import android.speech.tts.Voice as AndroidVoice +import android.speech.tts.Voice.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.readium.navigator.media.tts.TtsEngine +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.extensions.tryOrNull +import org.readium.r2.shared.util.Language + +/* + * On some Android implementations (i.e. on Oppo A9 2020 running Android 11), + * the TextToSpeech instance is often disconnected from the underlying service when the playback + * is paused and the app moves to the background. So we try to reset the TextToSpeech before + * actually returning an error. In the meantime, requests to the engine are queued + * into [pendingRequests]. + */ + +/** + * Default [TtsEngine] implementation using Android's native text to speech engine. + */ +@ExperimentalReadiumApi +public class AndroidTtsEngine private constructor( + private val context: Context, + engine: TextToSpeech, + private val settingsResolver: SettingsResolver, + private val voiceSelector: VoiceSelector, + override val voices: Set<Voice>, + initialPreferences: AndroidTtsPreferences +) : TtsEngine<AndroidTtsSettings, AndroidTtsPreferences, + AndroidTtsEngine.Error, AndroidTtsEngine.Voice> { + + public companion object { + + public suspend operator fun invoke( + context: Context, + settingsResolver: SettingsResolver, + voiceSelector: VoiceSelector, + initialPreferences: AndroidTtsPreferences + ): AndroidTtsEngine? { + val textToSpeech = initializeTextToSpeech(context) + ?: return null + + val voices = tryOrNull { textToSpeech.voices } // throws on Nexus 4 + ?.map { it.toTtsEngineVoice() } + ?.toSet() + .orEmpty() + + return AndroidTtsEngine( + context, + textToSpeech, + settingsResolver, + voiceSelector, + voices, + initialPreferences + ) + } + + private suspend fun initializeTextToSpeech( + context: Context + ): TextToSpeech? { + val init = CompletableDeferred<Boolean>() + + val initListener = OnInitListener { status -> + init.complete(status == SUCCESS) + } + val engine = TextToSpeech(context, initListener) + return if (init.await()) engine else null + } + + /** + * Starts the activity to install additional voice data. + */ + @SuppressLint("QueryPermissionsNeeded") + public fun requestInstallVoice(context: Context) { + val intent = Intent() + .setAction(Engine.ACTION_INSTALL_TTS_DATA) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + val availableActivities = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.queryIntentActivities( + intent, + PackageManager.ResolveInfoFlags.of(0) + ) + } else { + context.packageManager.queryIntentActivities(intent, 0) + } + + if (availableActivities.isNotEmpty()) { + context.startActivity(intent) + } + } + + private fun AndroidVoice.toTtsEngineVoice() = + Voice( + id = Voice.Id(name), + language = Language(locale), + quality = when (quality) { + QUALITY_VERY_HIGH -> Voice.Quality.Highest + QUALITY_HIGH -> Voice.Quality.High + QUALITY_NORMAL -> Voice.Quality.Normal + QUALITY_LOW -> Voice.Quality.Low + QUALITY_VERY_LOW -> Voice.Quality.Lowest + else -> throw IllegalStateException("Unexpected voice quality.") + }, + requiresNetwork = isNetworkConnectionRequired + ) + } + + public fun interface SettingsResolver { + + /** + * Computes a set of engine settings from the engine preferences. + */ + public fun settings(preferences: AndroidTtsPreferences): AndroidTtsSettings + } + + public fun interface VoiceSelector { + + /** + * Selects a voice for the given [language]. + */ + public fun voice(language: Language?, availableVoices: Set<Voice>): Voice? + } + + public sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? = null + ) : TtsEngine.Error { + + /** Denotes a generic operation failure. */ + public object Unknown : Error("An unknown error occurred.") + + /** Denotes a failure caused by an invalid request. */ + public object InvalidRequest : Error("Invalid request") + + /** Denotes a failure caused by a network connectivity problems. */ + public object Network : Error("A network error occurred.") + + /** Denotes a failure caused by network timeout. */ + public object NetworkTimeout : Error("Network timeout") + + /** Denotes a failure caused by an unfinished download of the voice data. */ + public object NotInstalledYet : Error("Voice not installed yet.") + + /** Denotes a failure related to the output (audio device or a file). */ + public object Output : Error("An error related to the output occurred.") + + /** Denotes a failure of a TTS service. */ + public object Service : Error("An error occurred with the TTS service.") + + /** Denotes a failure of a TTS engine to synthesize the given input. */ + public object Synthesis : Error("Synthesis failed.") + + /** + * Denotes the language data is missing. + * + * You can open the Android settings to install the missing data with: + * AndroidTtsEngine.requestInstallVoice(context) + */ + public data class LanguageMissingData(val language: Language) : + Error("Language data is missing.") + + /** + * Android's TTS error code. + * See https://developer.android.com/reference/android/speech/tts/TextToSpeech#ERROR + */ + public companion object { + internal fun fromNativeError(code: Int): Error = + when (code) { + ERROR_INVALID_REQUEST -> InvalidRequest + ERROR_NETWORK -> Network + ERROR_NETWORK_TIMEOUT -> NetworkTimeout + ERROR_NOT_INSTALLED_YET -> NotInstalledYet + ERROR_OUTPUT -> Output + ERROR_SERVICE -> Service + ERROR_SYNTHESIS -> Synthesis + else -> Unknown + } + } + } + + /** + * Represents a voice provided by the TTS engine which can speak an utterance. + * + * @param id Unique and stable identifier for this voice + * @param language Language (and region) this voice belongs to. + * @param quality Voice quality. + * @param requiresNetwork Indicates whether using this voice requires an Internet connection. + */ + public data class Voice( + val id: Id, + override val language: Language, + val quality: Quality = Quality.Normal, + val requiresNetwork: Boolean = false + ) : TtsEngine.Voice { + + @kotlinx.serialization.Serializable + @JvmInline + public value class Id(public val value: String) + + public enum class Quality { + Lowest, Low, Normal, High, Highest + } + } + + private data class Request( + val id: TtsEngine.RequestId, + val text: String, + val language: Language? + ) + + private sealed class State { + + data class EngineAvailable( + val engine: TextToSpeech + ) : State() + + data class WaitingForService( + val pendingRequests: MutableList<Request> = mutableListOf() + ) : State() + + data class Failure( + val error: AndroidTtsEngine.Error + ) : State() + } + + private val coroutineScope: CoroutineScope = + MainScope() + + private val _settings: MutableStateFlow<AndroidTtsSettings> = + MutableStateFlow(settingsResolver.settings(initialPreferences)) + .apply { engine.setupPitchAndSpeed(value) } + + private var utteranceListener: TtsEngine.Listener<Error>? = + null + + private var state: State = + State.EngineAvailable(engine) + + private var isClosed: Boolean = + false + + override val settings: StateFlow<AndroidTtsSettings> = + _settings.asStateFlow() + + override fun submitPreferences(preferences: AndroidTtsPreferences) { + val newSettings = settingsResolver.settings(preferences) + _settings.value = newSettings + (state as? State.EngineAvailable) + ?.engine?.setupPitchAndSpeed(newSettings) + } + + override fun setListener( + listener: TtsEngine.Listener<Error>? + ) { + utteranceListener = listener + (state as? State.EngineAvailable) + ?.let { setupListener(it.engine) } + } + + override fun speak( + requestId: TtsEngine.RequestId, + text: String, + language: Language? + ) { + check(!isClosed) { "Engine is closed." } + val request = Request(requestId, text, language) + + when (val stateNow = state) { + is State.WaitingForService -> { + stateNow.pendingRequests.add(request) + } + is State.Failure -> { + tryReconnect(request) + } + is State.EngineAvailable -> { + if (!doSpeak(stateNow.engine, request)) { + cleanEngine(stateNow.engine) + tryReconnect(request) + } + } + } + } + + override fun stop() { + when (val stateNow = state) { + is State.EngineAvailable -> { + stateNow.engine.stop() + } + is State.Failure -> { + // Do nothing + } + is State.WaitingForService -> { + for (request in stateNow.pendingRequests) { + utteranceListener?.onFlushed(request.id) + } + stateNow.pendingRequests.clear() + } + } + } + + override fun close() { + if (isClosed) { + return + } + + isClosed = true + coroutineScope.cancel() + + when (val stateNow = state) { + is State.EngineAvailable -> { + cleanEngine(stateNow.engine) + } + is State.Failure -> { + // Do nothing + } + is State.WaitingForService -> { + // Do nothing + } + } + } + + private fun doSpeak( + engine: TextToSpeech, + request: Request + ): Boolean { + return engine.setupVoice(settings.value, request.id, request.language, voices) && + (engine.speak(request.text, QUEUE_ADD, null, request.id.value) == SUCCESS) + } + + private fun setupListener(engine: TextToSpeech) { + if (utteranceListener == null) { + engine.setOnUtteranceProgressListener(null) + } else { + engine.setOnUtteranceProgressListener(UtteranceListener(utteranceListener)) + } + } + + private fun onReconnectionSucceeded(engine: TextToSpeech) { + val previousState = state as State.WaitingForService + setupListener(engine) + engine.setupPitchAndSpeed(_settings.value) + state = State.EngineAvailable(engine) + if (isClosed) { + engine.shutdown() + } else { + for (request in previousState.pendingRequests) { + doSpeak(engine, request) + } + } + } + + private fun onReconnectionFailed() { + val previousState = state as State.WaitingForService + val error = Error.Service + state = State.Failure(error) + + for (request in previousState.pendingRequests) { + utteranceListener?.onError(request.id, error) + } + } + + private fun tryReconnect(request: Request) { + state = State.WaitingForService(mutableListOf(request)) + coroutineScope.launch { + initializeTextToSpeech(context) + ?.let { onReconnectionSucceeded(it) } + ?: onReconnectionFailed() + } + } + + private fun cleanEngine(engine: TextToSpeech) { + engine.setOnUtteranceProgressListener(null) + engine.shutdown() + } + + private fun TextToSpeech.setupPitchAndSpeed(settings: AndroidTtsSettings) { + setSpeechRate(settings.speed.toFloat()) + setPitch(settings.pitch.toFloat()) + } + + private fun TextToSpeech.setupVoice( + settings: AndroidTtsSettings, + id: TtsEngine.RequestId, + utteranceLanguage: Language?, + voices: Set<Voice> + ): Boolean { + val language = utteranceLanguage + .takeUnless { settings.overrideContentLanguage } + // We take utterance language if data are missing but not if the language is not supported + ?.takeIf { isLanguageAvailable(it.locale) != LANG_NOT_SUPPORTED } + ?: settings.language + .takeIf { isLanguageAvailable(it.locale) != LANG_NOT_SUPPORTED } + ?: defaultVoice?.locale?.let { Language(it) } + + if (language == null) { + // We don't know what to do. + utteranceListener?.onError(id, Error.Unknown) + return false + } + + if (isLanguageAvailable(language.locale) < LANG_AVAILABLE) { + utteranceListener?.onError(id, Error.LanguageMissingData(language)) + return false + } + + val preferredVoiceWithRegion = + settings.voices[language] + ?.let { voiceForName(it.value) } + + val preferredVoiceWithoutRegion = + settings.voices[language.removeRegion()] + ?.let { voiceForName(it.value) } + + val voice = preferredVoiceWithRegion + ?: preferredVoiceWithoutRegion + ?: run { + voiceSelector + .voice(language, voices) + ?.let { voiceForName(it.id.value) } + } + + voice + ?.let { this.voice = it } + ?: run { this.language = language.locale } + + return true + } + + private fun TextToSpeech.voiceForName(name: String) = + voices.firstOrNull { it.name == name } + + private class UtteranceListener( + private val listener: TtsEngine.Listener<Error>? + ) : UtteranceProgressListener() { + override fun onStart(utteranceId: String) { + listener?.onStart(TtsEngine.RequestId(utteranceId)) + } + + override fun onStop(utteranceId: String, interrupted: Boolean) { + listener?.let { + val requestId = TtsEngine.RequestId(utteranceId) + if (interrupted) { + it.onInterrupted(requestId) + } else { + it.onFlushed(requestId) + } + } + } + + override fun onDone(utteranceId: String) { + listener?.onDone(TtsEngine.RequestId(utteranceId)) + } + + @Deprecated( + "Deprecated in the interface", + ReplaceWith("onError(utteranceId, -1)"), + level = DeprecationLevel.ERROR + ) + override fun onError(utteranceId: String) { + onError(utteranceId, -1) + } + + override fun onError(utteranceId: String, errorCode: Int) { + listener?.onError( + TtsEngine.RequestId(utteranceId), + Error.fromNativeError(errorCode) + ) + } + + override fun onRangeStart(utteranceId: String, start: Int, end: Int, frame: Int) { + listener?.onRange(TtsEngine.RequestId(utteranceId), start until end) + } + } +} diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsEngineProvider.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngineProvider.kt similarity index 68% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsEngineProvider.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngineProvider.kt index 6b27277e1a..d497f46be3 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsEngineProvider.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngineProvider.kt @@ -4,23 +4,24 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts.android +package org.readium.navigator.media.tts.android import android.content.Context import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException.* import androidx.media3.common.PlaybackParameters -import org.readium.r2.navigator.media3.tts.TtsEngineProvider +import org.readium.navigator.media.tts.TtsEngineProvider import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.publication.Metadata import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Try @ExperimentalReadiumApi @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) -class AndroidTtsEngineProvider( +public class AndroidTtsEngineProvider( private val context: Context, private val defaults: AndroidTtsDefaults = AndroidTtsDefaults(), - private val listener: AndroidTtsEngine.Listener? = null, private val voiceSelector: AndroidTtsEngine.VoiceSelector = AndroidTtsEngine.VoiceSelector { _, _ -> null } ) : TtsEngineProvider<AndroidTtsSettings, AndroidTtsPreferences, AndroidTtsPreferencesEditor, AndroidTtsEngine.Error, AndroidTtsEngine.Voice> { @@ -28,24 +29,21 @@ class AndroidTtsEngineProvider( override suspend fun createEngine( publication: Publication, initialPreferences: AndroidTtsPreferences - ): AndroidTtsEngine? { + ): Try<AndroidTtsEngine, Error> { val settingsResolver = AndroidTtsSettingsResolver(publication.metadata, defaults) - return AndroidTtsEngine( + val engine = AndroidTtsEngine( context, settingsResolver, voiceSelector, - listener, initialPreferences + ) ?: return Try.failure( + DebugError("Initialization of Android Tts service failed.") ) - } - fun computeSettings( - metadata: Metadata, - preferences: AndroidTtsPreferences - ): AndroidTtsSettings = - AndroidTtsSettingsResolver(metadata, defaults).settings(preferences) + return Try.success(engine) + } override fun createPreferencesEditor( publication: Publication, @@ -73,26 +71,24 @@ class AndroidTtsEngineProvider( } override fun mapEngineError(error: AndroidTtsEngine.Error): PlaybackException { - val errorCode = when (error.kind) { - AndroidTtsEngine.Error.Kind.Unknown -> + val errorCode = when (error) { + AndroidTtsEngine.Error.Unknown -> ERROR_CODE_UNSPECIFIED - AndroidTtsEngine.Error.Kind.InvalidRequest -> + AndroidTtsEngine.Error.InvalidRequest -> ERROR_CODE_IO_BAD_HTTP_STATUS - AndroidTtsEngine.Error.Kind.Network -> + AndroidTtsEngine.Error.Network -> ERROR_CODE_IO_NETWORK_CONNECTION_FAILED - AndroidTtsEngine.Error.Kind.NetworkTimeout -> + AndroidTtsEngine.Error.NetworkTimeout -> ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT - AndroidTtsEngine.Error.Kind.NotInstalledYet -> - ERROR_CODE_UNSPECIFIED - AndroidTtsEngine.Error.Kind.Output -> - ERROR_CODE_UNSPECIFIED - AndroidTtsEngine.Error.Kind.Service -> - ERROR_CODE_UNSPECIFIED - AndroidTtsEngine.Error.Kind.Synthesis -> + AndroidTtsEngine.Error.Output, + AndroidTtsEngine.Error.Service, + AndroidTtsEngine.Error.Synthesis, + is AndroidTtsEngine.Error.LanguageMissingData, + AndroidTtsEngine.Error.NotInstalledYet -> ERROR_CODE_UNSPECIFIED } - val message = "Android TTS engine error: ${error.kind.code}" + val message = "Android TTS engine error: ${error.javaClass.simpleName}" return PlaybackException(message, null, errorCode) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsPreferences.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsPreferences.kt similarity index 86% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsPreferences.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsPreferences.kt index b397d3712c..4cfa0dc0c8 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsPreferences.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsPreferences.kt @@ -4,10 +4,10 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts.android +package org.readium.navigator.media.tts.android import kotlinx.serialization.Serializable -import org.readium.r2.navigator.media3.tts.TtsEngine +import org.readium.navigator.media.tts.TtsEngine import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.Language @@ -21,11 +21,11 @@ import org.readium.r2.shared.util.Language */ @ExperimentalReadiumApi @Serializable -data class AndroidTtsPreferences( +public data class AndroidTtsPreferences( override val language: Language? = null, val pitch: Double? = null, val speed: Double? = null, - val voices: Map<Language, AndroidTtsEngine.Voice.Id>? = null, + val voices: Map<Language, AndroidTtsEngine.Voice.Id>? = null ) : TtsEngine.Preferences<AndroidTtsPreferences> { init { @@ -38,6 +38,6 @@ data class AndroidTtsPreferences( language = other.language ?: language, pitch = other.pitch ?: pitch, speed = other.speed ?: speed, - voices = other.voices ?: voices, + voices = other.voices ?: voices ) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsPreferencesEditor.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsPreferencesEditor.kt similarity index 85% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsPreferencesEditor.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsPreferencesEditor.kt index b8a47e159e..55138bf29f 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsPreferencesEditor.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsPreferencesEditor.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts.android +package org.readium.navigator.media.tts.android import org.readium.r2.navigator.extensions.format import org.readium.r2.navigator.preferences.* @@ -20,10 +20,10 @@ import org.readium.r2.shared.util.Language * or ranges. */ @ExperimentalReadiumApi -class AndroidTtsPreferencesEditor( +public class AndroidTtsPreferencesEditor( initialPreferences: AndroidTtsPreferences, publicationMetadata: Metadata, - defaults: AndroidTtsDefaults, + defaults: AndroidTtsDefaults ) : PreferencesEditor<AndroidTtsPreferences> { private data class State( @@ -44,15 +44,15 @@ class AndroidTtsPreferencesEditor( updateValues { AndroidTtsPreferences() } } - val language: Preference<Language?> = + public val language: Preference<Language?> = PreferenceDelegate( getValue = { preferences.language }, getEffectiveValue = { state.settings.language }, getIsEffective = { true }, - updateValue = { value -> updateValues { it.copy(language = value) } }, + updateValue = { value -> updateValues { it.copy(language = value) } } ) - val pitch: RangePreference<Double> = + public val pitch: RangePreference<Double> = RangePreferenceDelegate( getValue = { preferences.pitch }, getEffectiveValue = { state.settings.pitch }, @@ -60,10 +60,10 @@ class AndroidTtsPreferencesEditor( updateValue = { value -> updateValues { it.copy(pitch = value) } }, supportedRange = 0.1..Double.MAX_VALUE, progressionStrategy = DoubleIncrement(0.1), - valueFormatter = { "${it.format(2)}x" }, + valueFormatter = { "${it.format(2)}x" } ) - val speed: RangePreference<Double> = + public val speed: RangePreference<Double> = RangePreferenceDelegate( getValue = { preferences.speed }, getEffectiveValue = { state.settings.speed }, @@ -71,15 +71,15 @@ class AndroidTtsPreferencesEditor( updateValue = { value -> updateValues { it.copy(speed = value) } }, supportedRange = 0.1..Double.MAX_VALUE, progressionStrategy = DoubleIncrement(0.1), - valueFormatter = { "${it.format(2)}x" }, + valueFormatter = { "${it.format(2)}x" } ) - val voices: Preference<Map<Language, AndroidTtsEngine.Voice.Id>> = + public val voices: Preference<Map<Language, AndroidTtsEngine.Voice.Id>> = PreferenceDelegate( getValue = { preferences.voices }, getEffectiveValue = { state.settings.voices }, getIsEffective = { true }, - updateValue = { value -> updateValues { it.copy(voices = value) } }, + updateValue = { value -> updateValues { it.copy(voices = value) } } ) private fun updateValues(updater: (AndroidTtsPreferences) -> AndroidTtsPreferences) { diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsPreferencesFilters.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsPreferencesFilters.kt similarity index 77% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsPreferencesFilters.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsPreferencesFilters.kt index 971bcd6f52..4c5523d1c7 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsPreferencesFilters.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsPreferencesFilters.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts.android +package org.readium.navigator.media.tts.android import org.readium.r2.navigator.preferences.PreferencesFilter import org.readium.r2.shared.ExperimentalReadiumApi @@ -13,7 +13,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi * Suggested filter to keep only shared [AndroidTtsPreferences]. */ @ExperimentalReadiumApi -object AndroidTtsSharedPreferencesFilter : PreferencesFilter<AndroidTtsPreferences> { +public object AndroidTtsSharedPreferencesFilter : PreferencesFilter<AndroidTtsPreferences> { override fun filter(preferences: AndroidTtsPreferences): AndroidTtsPreferences = preferences.copy( @@ -25,7 +25,7 @@ object AndroidTtsSharedPreferencesFilter : PreferencesFilter<AndroidTtsPreferenc * Suggested filter to keep only publication-specific [AndroidTtsPreferences]. */ @ExperimentalReadiumApi -object AndroidTtsPublicationPreferencesFilter : PreferencesFilter<AndroidTtsPreferences> { +public object AndroidTtsPublicationPreferencesFilter : PreferencesFilter<AndroidTtsPreferences> { override fun filter(preferences: AndroidTtsPreferences): AndroidTtsPreferences = AndroidTtsPreferences( diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsPreferencesSerializer.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsPreferencesSerializer.kt similarity index 83% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsPreferencesSerializer.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsPreferencesSerializer.kt index e6e0b948b2..65f6f3676d 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsPreferencesSerializer.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsPreferencesSerializer.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts.android +package org.readium.navigator.media.tts.android import kotlinx.serialization.json.Json import org.readium.r2.navigator.preferences.PreferencesSerializer @@ -14,7 +14,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi * JSON serializer of [AndroidTtsPreferences]. */ @ExperimentalReadiumApi -class AndroidTtsPreferencesSerializer : PreferencesSerializer<AndroidTtsPreferences> { +public class AndroidTtsPreferencesSerializer : PreferencesSerializer<AndroidTtsPreferences> { override fun serialize(preferences: AndroidTtsPreferences): String = Json.encodeToString(AndroidTtsPreferences.serializer(), preferences) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsSettings.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsSettings.kt similarity index 74% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsSettings.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsSettings.kt index 4f0542fd04..b302f63120 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsSettings.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsSettings.kt @@ -4,9 +4,9 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts.android +package org.readium.navigator.media.tts.android -import org.readium.r2.navigator.media3.tts.TtsEngine +import org.readium.navigator.media.tts.TtsEngine import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.Language @@ -16,10 +16,10 @@ import org.readium.r2.shared.util.Language * @see AndroidTtsPreferences */ @ExperimentalReadiumApi -data class AndroidTtsSettings( +public data class AndroidTtsSettings( override val language: Language, override val overrideContentLanguage: Boolean, val pitch: Double, val speed: Double, - val voices: Map<Language, AndroidTtsEngine.Voice.Id>, + val voices: Map<Language, AndroidTtsEngine.Voice.Id> ) : TtsEngine.Settings diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsSettingsResolver.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsSettingsResolver.kt similarity index 87% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsSettingsResolver.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsSettingsResolver.kt index 5a17cefd8f..a51476b61e 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/android/AndroidTtsSettingsResolver.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsSettingsResolver.kt @@ -4,9 +4,9 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts.android +package org.readium.navigator.media.tts.android -import androidx.compose.ui.text.intl.Locale +import java.util.* import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Metadata import org.readium.r2.shared.util.Language @@ -21,7 +21,7 @@ internal class AndroidTtsSettingsResolver( val language = preferences.language ?: metadata.language ?: defaults.language - ?: Language(Locale.current.toLanguageTag()) + ?: Language(Locale.getDefault()) return AndroidTtsSettings( language = language, diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/AudioBecomingNoisyManager.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/AudioBecomingNoisyManager.kt similarity index 94% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/AudioBecomingNoisyManager.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/AudioBecomingNoisyManager.kt index 57df1b096d..641589fa66 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/AudioBecomingNoisyManager.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/AudioBecomingNoisyManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.readium.r2.navigator.media3.tts.session +package org.readium.navigator.media.tts.session import android.content.BroadcastReceiver import android.content.Context @@ -50,7 +50,8 @@ internal class AudioBecomingNoisyManager( fun setEnabled(enabled: Boolean) { if (enabled && !receiverRegistered) { context.registerReceiver( - receiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY) + receiver, + IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY) ) receiverRegistered = true } else if (!enabled && receiverRegistered) { diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/AudioFocusManager.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/AudioFocusManager.kt similarity index 85% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/AudioFocusManager.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/AudioFocusManager.kt index 1532391955..a25755c44b 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/AudioFocusManager.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/AudioFocusManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.readium.r2.navigator.media3.tts.session +package org.readium.navigator.media.tts.session import android.content.Context import android.media.AudioFocusRequest @@ -29,7 +29,7 @@ import androidx.media3.common.C import androidx.media3.common.Player import androidx.media3.common.util.Log import androidx.media3.common.util.Util -import org.readium.r2.navigator.media3.tts.session.AudioFocusManager.PlayerControl +import org.readium.navigator.media.tts.session.AudioFocusManager.PlayerControl @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) /** Manages requesting and responding to changes in audio focus. @@ -68,7 +68,9 @@ internal class AudioFocusManager( @MustBeDocumented @Retention(AnnotationRetention.SOURCE) @IntDef( - PLAYER_COMMAND_DO_NOT_PLAY, PLAYER_COMMAND_WAIT_FOR_CALLBACK, PLAYER_COMMAND_PLAY_WHEN_READY + PLAYER_COMMAND_DO_NOT_PLAY, + PLAYER_COMMAND_WAIT_FOR_CALLBACK, + PLAYER_COMMAND_PLAY_WHEN_READY ) annotation class PlayerCommand @@ -218,12 +220,13 @@ internal class AudioFocusManager( private fun requestAudioFocusV26(): Int { if (!::audioFocusRequest.isInitialized || rebuildAudioFocusRequest) { val builder = - if (!::audioFocusRequest.isInitialized) + if (!::audioFocusRequest.isInitialized) { AudioFocusRequest.Builder(focusGainToRequest) - else + } else { AudioFocusRequest.Builder( audioFocusRequest ) + } val willPauseWhenDucked = willPauseWhenDucked() audioFocusRequest = builder .setAudioAttributes( @@ -259,10 +262,11 @@ internal class AudioFocusManager( } this.audioFocusState = audioFocusState val volumeMultiplier = - if (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK) + if (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK) { VOLUME_MULTIPLIER_DUCK - else + } else { VOLUME_MULTIPLIER_DEFAULT + } if (this.volumeMultiplier == volumeMultiplier) { return } @@ -382,38 +386,42 @@ internal class AudioFocusManager( // Don't handle audio focus. It may be either video only contents or developers // want to have more finer grained control. (e.g. adding audio focus listener) AUDIOFOCUS_NONE - } else when (audioAttributes.usage) { - C.USAGE_VOICE_COMMUNICATION_SIGNALLING -> AUDIOFOCUS_NONE - C.USAGE_GAME, C.USAGE_MEDIA -> AUDIOFOCUS_GAIN - C.USAGE_UNKNOWN -> { - Log.w( - TAG, - "Specify a proper usage in the audio attributes for audio focus" + - " handling. Using AUDIOFOCUS_GAIN by default." - ) - AUDIOFOCUS_GAIN - } - C.USAGE_ALARM, C.USAGE_VOICE_COMMUNICATION -> AUDIOFOCUS_GAIN_TRANSIENT - C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, C.USAGE_ASSISTANCE_SONIFICATION, - C.USAGE_NOTIFICATION, C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED, - C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT, C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST, - C.USAGE_NOTIFICATION_EVENT, C.USAGE_NOTIFICATION_RINGTONE -> - AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK - C.USAGE_ASSISTANT -> - if (Util.SDK_INT >= 19) { - AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE - } else { - AUDIOFOCUS_GAIN_TRANSIENT + } else { + when (audioAttributes.usage) { + C.USAGE_VOICE_COMMUNICATION_SIGNALLING -> AUDIOFOCUS_NONE + C.USAGE_GAME, C.USAGE_MEDIA -> AUDIOFOCUS_GAIN + C.USAGE_UNKNOWN -> { + Log.w( + TAG, + "Specify a proper usage in the audio attributes for audio focus" + + " handling. Using AUDIOFOCUS_GAIN by default." + ) + AUDIOFOCUS_GAIN + } + C.USAGE_ALARM, C.USAGE_VOICE_COMMUNICATION -> AUDIOFOCUS_GAIN_TRANSIENT + C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, C.USAGE_ASSISTANCE_SONIFICATION, + C.USAGE_NOTIFICATION, C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED, + C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT, C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST, + C.USAGE_NOTIFICATION_EVENT, C.USAGE_NOTIFICATION_RINGTONE -> + AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + C.USAGE_ASSISTANT -> + if (Util.SDK_INT >= 19) { + AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE + } else { + AUDIOFOCUS_GAIN_TRANSIENT + } + C.USAGE_ASSISTANCE_ACCESSIBILITY -> { + if (audioAttributes.contentType == C.AUDIO_CONTENT_TYPE_SPEECH) { + // Voice shouldn't be interrupted by other playback. + AUDIOFOCUS_GAIN_TRANSIENT + } else { + AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + } + } + else -> { + Log.w(TAG, "Unidentified audio usage: " + audioAttributes.usage) + AUDIOFOCUS_NONE } - C.USAGE_ASSISTANCE_ACCESSIBILITY -> { - if (audioAttributes.contentType == C.AUDIO_CONTENT_TYPE_SPEECH) { - // Voice shouldn't be interrupted by other playback. - AUDIOFOCUS_GAIN_TRANSIENT - } else AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK - } - else -> { - Log.w(TAG, "Unidentified audio usage: " + audioAttributes.usage) - AUDIOFOCUS_NONE } } } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/StreamVolumeManager.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/StreamVolumeManager.kt similarity index 98% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/StreamVolumeManager.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/StreamVolumeManager.kt index 1190643074..7c2ff0b53e 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/StreamVolumeManager.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/StreamVolumeManager.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.readium.r2.navigator.media3.tts.session +package org.readium.navigator.media.tts.session import android.content.BroadcastReceiver import android.content.Context @@ -199,7 +199,8 @@ internal class StreamVolumeManager(context: Context, eventHandler: Handler, list } catch (e: RuntimeException) { Log.w( TAG, - "Could not retrieve stream volume for stream type $streamType", e + "Could not retrieve stream volume for stream type $streamType", + e ) audioManager.getStreamMaxVolume(streamType) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/TtsSessionAdapter.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt similarity index 82% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/TtsSessionAdapter.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt index a50c7ac9f2..836fc6cb36 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/TtsSessionAdapter.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts.session +package org.readium.navigator.media.tts.session import android.app.Application import android.os.Handler @@ -13,24 +13,39 @@ import android.view.Surface import android.view.SurfaceHolder import android.view.SurfaceView import android.view.TextureView -import androidx.media3.common.* +import androidx.media3.common.AudioAttributes import androidx.media3.common.C.* +import androidx.media3.common.DeviceInfo +import androidx.media3.common.FlagSet +import androidx.media3.common.IllegalSeekPositionException +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException.* +import androidx.media3.common.PlaybackParameters +import androidx.media3.common.Player import androidx.media3.common.Player.* +import androidx.media3.common.Timeline +import androidx.media3.common.TrackSelectionParameters +import androidx.media3.common.Tracks +import androidx.media3.common.VideoSize import androidx.media3.common.text.CueGroup import androidx.media3.common.util.Clock import androidx.media3.common.util.ListenerSet import androidx.media3.common.util.Size import androidx.media3.common.util.Util +import java.lang.Error import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import org.readium.r2.navigator.media3.tts.TtsEngine -import org.readium.r2.navigator.media3.tts.TtsPlayer +import org.readium.navigator.media.tts.TtsEngine +import org.readium.navigator.media.tts.TtsPlayer import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.fetcher.Resource +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.http.HttpError /** * Adapts the [TtsPlayer] to media3 [Player] interface. @@ -65,12 +80,13 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( private val streamVolumeManager = StreamVolumeManager( application, - Handler(applicationLooper), + eventHandler, StreamVolumeManagerListener() ) init { - streamVolumeManager.setStreamType(Util.getStreamTypeForAudioUsage(audioAttributes.usage)) + val streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage) + streamVolumeManager.setStreamType(streamType) } private val audioFocusManager = AudioFocusManager( @@ -101,7 +117,10 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( .onEach { playback -> notifyListenersPlaybackChanged(lastPlayback, playback) lastPlayback = playback - audioFocusManager.updateAudioFocus(playback.playWhenReady, playback.state.playerCode) + audioFocusManager.updateAudioFocus( + playback.playWhenReady, + playback.state.playerCode + ) }.launchIn(coroutineScope) playbackParametersState @@ -114,9 +133,9 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( private var listeners: ListenerSet<Listener> = ListenerSet( applicationLooper, - Clock.DEFAULT, - ) { listener: Listener, flags: FlagSet? -> - listener.onEvents(this, Events(flags!!)) + Clock.DEFAULT + ) { listener: Listener, flags: FlagSet -> + listener.onEvents(this, Events(flags)) } private val permanentAvailableCommands = @@ -133,11 +152,11 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( COMMAND_GET_AUDIO_ATTRIBUTES, COMMAND_GET_DEVICE_VOLUME, - COMMAND_SET_DEVICE_VOLUME, + COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS, COMMAND_SET_SPEED_AND_PITCH, COMMAND_GET_CURRENT_MEDIA_ITEM, - COMMAND_GET_MEDIA_ITEMS_METADATA, + COMMAND_GET_METADATA, COMMAND_GET_TEXT ).build() @@ -193,6 +212,16 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( override fun moveMediaItems(fromIndex: Int, toIndex: Int, newIndex: Int) { } + override fun replaceMediaItem(index: Int, mediaItem: MediaItem) { + } + + override fun replaceMediaItems( + fromIndex: Int, + toIndex: Int, + mediaItems: MutableList<MediaItem> + ) { + } + override fun removeMediaItem(index: Int) { } @@ -236,7 +265,7 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( } override fun getPlayerError(): PlaybackException? { - return (lastPlayback.state as? TtsPlayer.State.Error)?.toPlaybackException() + return (lastPlayback.state as? TtsPlayer.State.Failure)?.toPlaybackException() } override fun play() { @@ -425,9 +454,6 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( onStop() } - @Deprecated("Deprecated in Java") - override fun stop(reset: Boolean) {} - override fun release() { streamVolumeManager.release() audioFocusManager.release() @@ -458,15 +484,15 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( } override fun setPlaylistMetadata(mediaMetadata: MediaMetadata) { - throw NotImplementedError() } override fun getCurrentManifest(): Any? { val timeline = currentTimeline - return if (timeline.isEmpty) + return if (timeline.isEmpty) { null - else + } else { timeline.getWindow(currentMediaItemIndex, window).manifest + } } override fun getCurrentTimeline(): Timeline { @@ -494,14 +520,15 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( override fun getNextMediaItemIndex(): Int { val timeline = currentTimeline - return if (timeline.isEmpty) + return if (timeline.isEmpty) { INDEX_UNSET - else + } else { timeline.getNextWindowIndex( currentMediaItemIndex, getRepeatModeForNavigation(), shuffleModeEnabled ) + } } @Deprecated("Deprecated in Java", ReplaceWith("previousMediaItemIndex")) @@ -511,20 +538,24 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( override fun getPreviousMediaItemIndex(): Int { val timeline = currentTimeline - return if (timeline.isEmpty) + return if (timeline.isEmpty) { INDEX_UNSET - else + } else { timeline.getPreviousWindowIndex( - currentMediaItemIndex, getRepeatModeForNavigation(), shuffleModeEnabled + currentMediaItemIndex, + getRepeatModeForNavigation(), + shuffleModeEnabled ) + } } override fun getCurrentMediaItem(): MediaItem? { val timeline = currentTimeline - return if (timeline.isEmpty) + return if (timeline.isEmpty) { null - else + } else { timeline.getWindow(currentMediaItemIndex, window).mediaItem + } } override fun getMediaItemCount(): Int { @@ -550,15 +581,17 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( override fun getBufferedPercentage(): Int { val position = bufferedPosition val duration = duration - return if (position == TIME_UNSET || duration == TIME_UNSET) + return if (position == TIME_UNSET || duration == TIME_UNSET) { 0 - else if (duration == 0L) + } else if (duration == 0L) { 100 - else Util.constrainValue( - (position * 100 / duration).toInt(), - 0, - 100 - ) + } else { + Util.constrainValue( + (position * 100 / duration).toInt(), + 0, + 100 + ) + } } override fun getTotalBufferedDuration(): Long { @@ -593,7 +626,9 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( val windowStartTimeMs = timeline.getWindow(currentMediaItemIndex, window).windowStartTimeMs return if (windowStartTimeMs == TIME_UNSET) { TIME_UNSET - } else window.currentUnixTimeMs - window.windowStartTimeMs - contentPosition + } else { + window.currentUnixTimeMs - window.windowStartTimeMs - contentPosition + } } @Deprecated("Deprecated in Java", ReplaceWith("isCurrentMediaItemSeekable")) @@ -620,10 +655,11 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( override fun getContentDuration(): Long { val timeline = currentTimeline - return if (timeline.isEmpty) + return if (timeline.isEmpty) { TIME_UNSET - else + } else { timeline.getWindow(currentMediaItemIndex, window).durationMs + } } override fun getContentPosition(): Long { @@ -700,36 +736,60 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( return streamVolumeManager.isMuted() } + @Deprecated("Deprecated in Java") override fun setDeviceVolume(volume: Int) { streamVolumeManager.setVolume(volume) } + override fun setDeviceVolume(volume: Int, flags: Int) { + streamVolumeManager.setVolume(volume) + } + + @Deprecated("Deprecated in Java") override fun increaseDeviceVolume() { streamVolumeManager.increaseVolume() } + override fun increaseDeviceVolume(flags: Int) { + streamVolumeManager.increaseVolume() + } + + @Deprecated("Deprecated in Java") override fun decreaseDeviceVolume() { streamVolumeManager.decreaseVolume() } + override fun decreaseDeviceVolume(flags: Int) { + streamVolumeManager.decreaseVolume() + } + + @Deprecated("Deprecated in Java") override fun setDeviceMuted(muted: Boolean) { streamVolumeManager.setMuted(muted) } + override fun setDeviceMuted(muted: Boolean, flags: Int) { + streamVolumeManager.setMuted(muted) + } + + override fun setAudioAttributes(audioAttributes: AudioAttributes, handleAudioFocus: Boolean) { + audioFocusManager.setAudioAttributes(audioAttributes) + } + private fun notifyListenersPlaybackChanged( previousPlaybackInfo: TtsPlayer.Playback, - playbackInfo: TtsPlayer.Playback, + playbackInfo: TtsPlayer.Playback // playWhenReadyChangeReason: @Player.PlayWhenReadyChangeReason Int, ) { - if (previousPlaybackInfo.state as? TtsPlayer.State.Error != playbackInfo.state as? Error) { + if (previousPlaybackInfo.state as? TtsPlayer.State.Failure != playbackInfo.state as? Error) { listeners.queueEvent( EVENT_PLAYER_ERROR ) { listener: Listener -> listener.onPlayerErrorChanged( - (playbackInfo.state as? TtsPlayer.State.Error)?.toPlaybackException() + (playbackInfo.state as? TtsPlayer.State.Failure)?.toPlaybackException() ) } - if (playbackInfo.state is TtsPlayer.State.Error) { + if (playbackInfo.state is TtsPlayer.State.Failure) { listeners.queueEvent( EVENT_PLAYER_ERROR ) { listener: Listener -> @@ -756,10 +816,7 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( ) { listener: Listener -> listener.onPlayWhenReadyChanged( playbackInfo.playWhenReady, - if (playbackInfo.state == TtsPlayer.State.Ended) - PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM - else - PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST + PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST // PLAYBACK_SUPPRESSION_REASON_NONE // playWhenReadyChangeReason ) @@ -793,11 +850,12 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( } private fun createDeviceInfo(streamVolumeManager: StreamVolumeManager): DeviceInfo { - val newDeviceInfo = DeviceInfo( - DeviceInfo.PLAYBACK_TYPE_LOCAL, - streamVolumeManager.minVolume, - streamVolumeManager.maxVolume + val newDeviceInfo = DeviceInfo.Builder( + DeviceInfo.PLAYBACK_TYPE_LOCAL ) + .setMinVolume(streamVolumeManager.minVolume) + .setMaxVolume(streamVolumeManager.maxVolume) + .build() deviceInfo = newDeviceInfo return newDeviceInfo } @@ -850,7 +908,8 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( } } - private inner class AudioBecomingNoisyManagerListener : AudioBecomingNoisyManager.EventListener { + private inner class AudioBecomingNoisyManagerListener : + AudioBecomingNoisyManager.EventListener { override fun onAudioBecomingNoisy() { playWhenReady = false @@ -860,31 +919,35 @@ internal class TtsSessionAdapter<E : TtsEngine.Error>( private val TtsPlayer.State.playerCode get() = when (this) { TtsPlayer.State.Ready -> STATE_READY TtsPlayer.State.Ended -> STATE_ENDED - is TtsPlayer.State.Error -> STATE_IDLE + is TtsPlayer.State.Failure -> STATE_IDLE } @Suppress("Unchecked_cast") - private fun TtsPlayer.State.Error.toPlaybackException(): PlaybackException = when (this) { - is TtsPlayer.State.Error.EngineError<*> -> mapEngineError(error as E) - is TtsPlayer.State.Error.ContentError -> when (exception) { - is Resource.Exception.BadRequest -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_IO_BAD_HTTP_STATUS) - is Resource.Exception.NotFound -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_IO_BAD_HTTP_STATUS) - is Resource.Exception.Forbidden -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_DRM_DISALLOWED_OPERATION) - is Resource.Exception.Unavailable -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_IO_NETWORK_CONNECTION_FAILED) - is Resource.Exception.Offline -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_IO_NETWORK_CONNECTION_FAILED) - is Resource.Exception.OutOfMemory -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_UNSPECIFIED) - is Resource.Exception.Cancelled -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_IO_UNSPECIFIED) - is Resource.Exception.Other -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_UNSPECIFIED) - else -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_UNSPECIFIED) + private fun TtsPlayer.State.Failure.toPlaybackException(): PlaybackException = when (this) { + is TtsPlayer.State.Failure.Engine<*> -> { + mapEngineError(error as E) + } + is TtsPlayer.State.Failure.Content -> { + val errorCode = when (error) { + is ReadError.Access -> + when (error.cause) { + is HttpError.ErrorResponse -> + ERROR_CODE_IO_BAD_HTTP_STATUS + is HttpError.Timeout -> + ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT + is HttpError.Unreachable -> + ERROR_CODE_IO_NETWORK_CONNECTION_FAILED + else -> ERROR_CODE_UNSPECIFIED + } + + else -> + ERROR_CODE_UNSPECIFIED + } + PlaybackException( + error.message, + error.cause?.let { ErrorException(it) }, + errorCode + ) } } } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/TtsTimeline.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsTimeline.kt similarity index 85% rename from readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/TtsTimeline.kt rename to readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsTimeline.kt index 8cfdacad23..271c5b9ab0 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media3/tts/session/TtsTimeline.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsTimeline.kt @@ -4,18 +4,19 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.navigator.media3.tts.session +package org.readium.navigator.media.tts.session +import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Timeline import java.util.* @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) internal class TtsTimeline( - private val mediaItems: List<MediaItem>, + private val mediaItems: List<MediaItem> ) : Timeline() { - private val uuids = (0 until windowCount) + private val uuids = mediaItems.indices .map { UUID.randomUUID() } override fun getWindowCount(): Int { @@ -49,6 +50,8 @@ internal class TtsTimeline( override fun getIndexOfPeriod(uid: Any): Int { return uuids.indexOfFirst { it == uid } + .takeUnless { it == -1 } + ?: C.INDEX_UNSET } override fun getUidOfPeriod(periodIndex: Int): Any { diff --git a/readium/opds/build.gradle.kts b/readium/opds/build.gradle.kts index 749f173f10..cdaf0d7949 100644 --- a/readium/opds/build.gradle.kts +++ b/readium/opds/build.gradle.kts @@ -11,18 +11,20 @@ plugins { } android { - compileSdk = 33 + resourcePrefix = "readium_" + + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs = freeCompilerArgs + listOf( "-opt-in=kotlin.RequiresOptIn", "-opt-in=org.readium.r2.shared.InternalReadiumApi" @@ -40,6 +42,10 @@ android { namespace = "org.readium.r2.opds" } +kotlin { + explicitApi() +} + rootProject.ext["publish.artifactId"] = "readium-opds" apply(from = "$rootDir/scripts/publish-module.gradle") @@ -49,7 +55,6 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.timber) implementation(libs.joda.time) - implementation("nl.komponents.kovenant:kovenant:3.3.0") implementation(libs.kotlinx.coroutines.core) // Tests diff --git a/readium/opds/src/main/AndroidManifest.xml b/readium/opds/src/main/AndroidManifest.xml index b37569aae9..eb475b4918 100644 --- a/readium/opds/src/main/AndroidManifest.xml +++ b/readium/opds/src/main/AndroidManifest.xml @@ -1,10 +1,7 @@ <!-- - ~ Module: r2-opds-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <manifest /> diff --git a/readium/opds/src/main/java/org/readium/r2/opds/Extensions.kt b/readium/opds/src/main/java/org/readium/r2/opds/Extensions.kt deleted file mode 100644 index b46b1582af..0000000000 --- a/readium/opds/src/main/java/org/readium/r2/opds/Extensions.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2021 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.opds - -import kotlinx.coroutines.runBlocking -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.task -import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpFetchResponse -import org.readium.r2.shared.util.http.HttpRequest - -internal fun HttpClient.fetchPromise(request: HttpRequest): Promise<HttpFetchResponse, Exception> { - return task { runBlocking { fetch(request).getOrThrow() } } -} diff --git a/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt b/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt index e3258ee607..d7eb47cd2f 100644 --- a/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt +++ b/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt @@ -10,91 +10,84 @@ package org.readium.r2.opds import java.net.URL -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.then import org.joda.time.DateTime import org.readium.r2.shared.extensions.toList import org.readium.r2.shared.extensions.toMap import org.readium.r2.shared.opds.* -import org.readium.r2.shared.parser.xml.ElementNode -import org.readium.r2.shared.parser.xml.XmlParser import org.readium.r2.shared.publication.* import org.readium.r2.shared.toJSON -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.fetchWithDecoder +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.shared.util.xml.XmlParser -enum class OPDSParserError { +public enum class OPDSParserError { MissingTitle } -data class MimeTypeParameters( +public data class MimeTypeParameters( var type: String, var parameters: MutableMap<String, String> = mutableMapOf() ) -object Namespaces { - const val Opds = "http://opds-spec.org/2010/catalog" - const val Dc = "http://purl.org/dc/elements/1.1/" - const val Dcterms = "http://purl.org/dc/terms/" - const val Atom = "http://www.w3.org/2005/Atom" - const val Search = "http://a9.com/-/spec/opensearch/1.1/" - const val Thread = "http://purl.org/syndication/thread/1.0" +public object Namespaces { + public const val Opds: String = "http://opds-spec.org/2010/catalog" + public const val Dc: String = "http://purl.org/dc/elements/1.1/" + public const val Dcterms: String = "http://purl.org/dc/terms/" + public const val Atom: String = "http://www.w3.org/2005/Atom" + public const val Search: String = "http://a9.com/-/spec/opensearch/1.1/" + public const val Thread: String = "http://purl.org/syndication/thread/1.0" } -class OPDS1Parser { - companion object { - - suspend fun parseUrlString(url: String, client: HttpClient = DefaultHttpClient()): Try<ParseData, Exception> { - return client.fetchWithDecoder(HttpRequest(url)) { - this.parse(it.body, URL(url)) - } - } - - suspend fun parseRequest(request: HttpRequest, client: HttpClient = DefaultHttpClient()): Try<ParseData, Exception> { +public class OPDS1Parser { + public companion object { + + public suspend fun parseUrlString( + url: String, + client: HttpClient = DefaultHttpClient() + ): Try<ParseData, Exception> = + AbsoluteUrl(url) + ?.let { parseRequest(HttpRequest(it), client) } + ?: run { Try.failure(Exception("Not an absolute URL.")) } + + public suspend fun parseRequest( + request: HttpRequest, + client: HttpClient = DefaultHttpClient() + ): Try<ParseData, Exception> { return client.fetchWithDecoder(request) { - this.parse(it.body, URL(request.url)) - } + this.parse(it.body, request.url) + }.mapFailure { ErrorException(it) } } - @Deprecated( - "Use `parseRequest` or `parseUrlString` with coroutines instead", - ReplaceWith("OPDS1Parser.parseUrlString(url)"), - DeprecationLevel.WARNING - ) - fun parseURL(url: URL): Promise<ParseData, Exception> { - return DefaultHttpClient().fetchPromise(HttpRequest(url.toString())) then { - this.parse(xmlData = it.body, url = url) + public fun parse(xmlData: ByteArray, url: Url): ParseData { + val root = XmlParser().parse(xmlData.inputStream()) + return if (root.name == "feed") { + ParseData(parseFeed(root, url), null, 1) + } else { + ParseData(null, parseEntry(root, url), 1) } } + @Suppress("UNUSED_PARAMETER") @Deprecated( - "Use `parseRequest` or `parseUrlString` with coroutines instead", - ReplaceWith("OPDS1Parser.parseUrlString(url)"), - DeprecationLevel.WARNING + "Provide an instance of `Url` instead", + ReplaceWith("parse(jsonData, url.toUrl()!!)"), + DeprecationLevel.ERROR ) - @Suppress("unused") - fun parseURL(headers: MutableMap<String, String>, url: URL): Promise<ParseData, Exception> { - return DefaultHttpClient().fetchPromise(HttpRequest(url = url.toString(), headers = headers)) then { - this.parse(xmlData = it.body, url = url) - } - } + public fun parse(jsonData: ByteArray, url: URL): ParseData = + throw NotImplementedError() - fun parse(xmlData: ByteArray, url: URL): ParseData { - val root = XmlParser().parse(xmlData.inputStream()) - return if (root.name == "feed") - ParseData(parseFeed(root, url), null, 1) - else - ParseData(null, parseEntry(root, url), 1) - } - - private fun parseFeed(root: ElementNode, url: URL): Feed { + private fun parseFeed(root: ElementNode, url: Url): Feed { val feedTitle = root.getFirst("title", Namespaces.Atom)?.text ?: throw Exception(OPDSParserError.MissingTitle.name) - val feed = Feed(feedTitle, 1, url) + val feed = Feed.Builder(feedTitle, 1, url) val tmpDate = root.getFirst("updated", Namespaces.Atom)?.text feed.metadata.modified = tmpDate?.let { DateTime(it).toDate() } @@ -113,7 +106,7 @@ class OPDS1Parser { var collectionLink: Link? = null val links = entry.get("link", Namespaces.Atom) for (link in links) { - val href = link.getAttr("href") + val href = link.getAttr("href")?.let { Url(it) } val rel = link.getAttr("rel") if (rel != null) { if (rel.startsWith("http://opds-spec.org/acquisition")) { @@ -121,7 +114,7 @@ class OPDS1Parser { } if (href != null && (rel == "collection" || rel == "http://opds-spec.org/group")) { collectionLink = Link( - href = Href(href, baseHref = feed.href.toString()).percentEncodedString, + href = feed.href.resolve(href), title = link.getAttr("title"), rels = setOf("collection") ) @@ -139,7 +132,7 @@ class OPDS1Parser { } } else { val link = entry.getFirst("link", Namespaces.Atom) - val href = link?.getAttr("href") + val href = link?.getAttr("href")?.let { Url(it) } if (href != null) { val otherProperties = mutableMapOf<String, Any>() val facetElementCount = link.getAttrNs("count", Namespaces.Thread)?.toInt() @@ -148,8 +141,8 @@ class OPDS1Parser { } val newLink = Link( - href = Href(href, baseHref = feed.href.toString()).percentEncodedString, - type = link.getAttr("type"), + href = feed.href.resolve(href), + mediaType = link.getAttr("type")?.let { MediaType(it) }, title = entry.getFirst("title", Namespaces.Atom)?.text, rels = listOfNotNull(link.getAttr("rel")).toSet(), properties = Properties(otherProperties = otherProperties) @@ -165,10 +158,10 @@ class OPDS1Parser { } // Parse links for (link in root.get("link", Namespaces.Atom)) { - val hrefAttr = link.getAttr("href") ?: continue - val href = Href(hrefAttr, baseHref = feed.href.toString()).percentEncodedString + val hrefAttr = link.getAttr("href")?.let { Url(it) } ?: continue + val href = feed.href.resolve(hrefAttr) val title = link.getAttr("title") - val type = link.getAttr("type") + val type = link.getAttr("type")?.let { MediaType(it) } val rels = listOfNotNull(link.getAttr("rel")).toSet() val facetGroupName = link.getAttrNs("facetGroup", Namespaces.Opds) @@ -178,13 +171,21 @@ class OPDS1Parser { if (facetElementCount != null) { otherProperties["numberOfItems"] = facetElementCount } - val newLink = Link(href = href, type = type, title = title, rels = rels, properties = Properties(otherProperties = otherProperties)) + val newLink = Link( + href = href, + mediaType = type, + title = title, + rels = rels, + properties = Properties(otherProperties = otherProperties) + ) addFacet(feed, newLink, facetGroupName) } else { - feed.links.add(Link(href = href, type = type, title = title, rels = rels)) + feed.links.add( + Link(href = href, mediaType = type, title = title, rels = rels) + ) } } - return feed + return feed.build() } private fun parseMimeType(mimeTypeString: String): MimeTypeParameters { @@ -201,86 +202,28 @@ class OPDS1Parser { } @Suppress("unused") - suspend fun retrieveOpenSearchTemplate(feed: Feed): Try<String?, Exception> { - - var openSearchURL: URL? = null - var selfMimeType: String? = null - - for (link in feed.links) { - if (link.rels.contains("self")) { - if (link.type != null) { - selfMimeType = link.type - } - } else if (link.rels.contains("search")) { - openSearchURL = URL(link.href) - } - } - - val unwrappedURL = openSearchURL?.let { - return@let it - } - - return DefaultHttpClient().fetchWithDecoder(HttpRequest(unwrappedURL.toString())) { - - val document = XmlParser().parse(it.body.inputStream()) - - val urls = document.get("Url", Namespaces.Search) - - var typeAndProfileMatch: ElementNode? = null - var typeMatch: ElementNode? = null - - selfMimeType?.let { s -> - - val selfMimeParams = parseMimeType(mimeTypeString = s) - for (url in urls) { - val urlMimeType = url.getAttr("type") ?: continue - val otherMimeParams = parseMimeType(mimeTypeString = urlMimeType) - if (selfMimeParams.type == otherMimeParams.type) { - if (typeMatch == null) { - typeMatch = url - } - if (selfMimeParams.parameters["profile"] == otherMimeParams.parameters["profile"]) { - typeAndProfileMatch = url - break - } - } - } - val match = typeAndProfileMatch ?: (typeMatch ?: urls[0]) - val template = match.getAttr("template") - - template - } - null - } - } - - @Deprecated( - "Use `retrieveOpenSearchTemplate` with coroutines instead", - ReplaceWith("OPDS1Parser.retrieveOpenSearchTemplate(feed)"), - DeprecationLevel.WARNING - ) - @Suppress("unused") - fun fetchOpenSearchTemplate(feed: Feed): Promise<String?, Exception> { - - var openSearchURL: URL? = null - var selfMimeType: String? = null + public suspend fun retrieveOpenSearchTemplate( + feed: Feed, + client: HttpClient = DefaultHttpClient() + ): Try<String?, Exception> { + var openSearchURL: Href? = null + var selfMimeType: MediaType? = null for (link in feed.links) { if (link.rels.contains("self")) { - if (link.type != null) { - selfMimeType = link.type + if (link.mediaType != null) { + selfMimeType = link.mediaType } } else if (link.rels.contains("search")) { - openSearchURL = URL(link.href) + openSearchURL = link.href } } - val unwrappedURL = openSearchURL?.let { - return@let it - } - - return DefaultHttpClient().fetchPromise(HttpRequest(unwrappedURL.toString())) then { + val unwrappedURL = openSearchURL + ?.let { it.resolve() as? AbsoluteUrl } + ?: return Try.success(null) + return client.fetchWithDecoder(HttpRequest(unwrappedURL)) { val document = XmlParser().parse(it.body.inputStream()) val urls = document.get("Url", Namespaces.Search) @@ -290,7 +233,7 @@ class OPDS1Parser { selfMimeType?.let { s -> - val selfMimeParams = parseMimeType(mimeTypeString = s) + val selfMimeParams = parseMimeType(mimeTypeString = s.toString()) for (url in urls) { val urlMimeType = url.getAttr("type") ?: continue val otherMimeParams = parseMimeType(mimeTypeString = urlMimeType) @@ -310,17 +253,17 @@ class OPDS1Parser { template } null - } + }.mapFailure { ErrorException(it) } } - private fun parseEntry(entry: ElementNode, baseUrl: URL): Publication? { + private fun parseEntry(entry: ElementNode, baseUrl: Url): Publication? { // A title is mandatory val title = entry.getFirst("title", Namespaces.Atom)?.text ?: return null var links = entry.get("link", Namespaces.Atom) .mapNotNull { element -> - val href = element.getAttr("href") + val href = element.getAttr("href")?.let { Url(it) } val rel = element.getAttr("rel") if (href == null || rel == "collection" || rel == "http://opds-spec.org/group") { return@mapNotNull null @@ -343,8 +286,8 @@ class OPDS1Parser { } Link( - href = Href(href, baseHref = baseUrl.toString()).percentEncodedString, - type = element.getAttr("type"), + href = baseUrl.resolve(href), + mediaType = element.getAttr("type")?.let { MediaType(it) }, title = element.getAttr("title"), rels = listOfNotNull(rel).toSet(), properties = Properties(otherProperties = properties) @@ -352,7 +295,9 @@ class OPDS1Parser { } val images = links.filter { - it.rels.contains("http://opds-spec.org/image") || it.rels.contains("http://opds-spec.org/image-thumbnail") + it.rels.contains("http://opds-spec.org/image") || it.rels.contains( + "http://opds-spec.org/image-thumbnail" + ) } links = links - images @@ -375,7 +320,11 @@ class OPDS1Parser { publishers = entry.get("publisher", Namespaces.Dcterms) .mapNotNull { - it.text?.let { name -> Contributor(localizedName = LocalizedString(name)) } + it.text?.let { name -> + Contributor( + localizedName = LocalizedString(name) + ) + } }, subjects = entry.get("category", Namespaces.Atom) @@ -396,6 +345,7 @@ class OPDS1Parser { localizedName = LocalizedString(name), links = listOfNotNull( element.getFirst("uri", Namespaces.Atom)?.text + ?.let { Url(it) } ?.let { Link(href = it) } ) ) @@ -417,20 +367,20 @@ class OPDS1Parser { return Publication(manifest) } - private fun addFacet(feed: Feed, link: Link, title: String) { + private fun addFacet(feed: Feed.Builder, link: Link, title: String) { for (facet in feed.facets) { if (facet.metadata.title == title) { facet.links.add(link) return } } - val newFacet = Facet(title = title) + val newFacet = Facet.Builder(title = title) newFacet.links.add(link) feed.facets.add(newFacet) } private fun addPublicationInGroup( - feed: Feed, + feed: Feed.Builder, publication: Publication, collectionLink: Link ) { @@ -447,14 +397,14 @@ class OPDS1Parser { val selfLink = collectionLink.copy( rels = collectionLink.rels + "self" ) - val newGroup = Group(title = title) + val newGroup = Group.Builder(title = title) newGroup.links.add(selfLink) newGroup.publications.add(publication) feed.groups.add(newGroup) } } - private fun addNavigationInGroup(feed: Feed, link: Link, collectionLink: Link) { + private fun addNavigationInGroup(feed: Feed.Builder, link: Link, collectionLink: Link) { for (group in feed.groups) { for (l in group.links) { if (l.href == collectionLink.href) { @@ -468,7 +418,7 @@ class OPDS1Parser { val selfLink = collectionLink.copy( rels = collectionLink.rels + "self" ) - val newGroup = Group(title = title) + val newGroup = Group.Builder(title = title) newGroup.links.add(selfLink) newGroup.navigation.add(link) feed.groups.add(newGroup) diff --git a/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt b/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt index 9d98ea884b..d287a2c19e 100644 --- a/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt +++ b/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt @@ -7,83 +7,86 @@ * LICENSE file present in the project repository where this source code is maintained. */ +@file:OptIn(ExperimentalReadiumApi::class) + package org.readium.r2.opds import java.net.URL -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.then import org.joda.time.DateTime import org.json.JSONArray import org.json.JSONObject -import org.readium.r2.shared.extensions.removeLastComponent -import org.readium.r2.shared.opds.* +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.opds.Facet +import org.readium.r2.shared.opds.Feed +import org.readium.r2.shared.opds.Group +import org.readium.r2.shared.opds.OpdsMetadata +import org.readium.r2.shared.opds.ParseData import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.publication.normalizeHrefsToBase +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.fetchWithDecoder -enum class OPDS2ParserError { +public enum class OPDS2ParserError { MetadataNotFound, InvalidLink, MissingTitle, InvalidFacet, - InvalidGroup, + InvalidGroup } -class OPDS2Parser { +public class OPDS2Parser { - companion object { + public companion object { - private lateinit var feed: Feed + public suspend fun parseUrlString( + url: String, + client: HttpClient = DefaultHttpClient() + ): Try<ParseData, Exception> = + AbsoluteUrl(url) + ?.let { parseRequest(HttpRequest(it), client) } + ?: run { Try.failure(Exception("Not an absolute URL.")) } - suspend fun parseUrlString(url: String, client: HttpClient = DefaultHttpClient()): Try<ParseData, Exception> { - return client.fetchWithDecoder(HttpRequest(url)) { - this.parse(it.body, URL(url)) - } - } - - suspend fun parseRequest(request: HttpRequest, client: HttpClient = DefaultHttpClient()): Try<ParseData, Exception> { + public suspend fun parseRequest( + request: HttpRequest, + client: HttpClient = DefaultHttpClient() + ): Try<ParseData, Exception> { return client.fetchWithDecoder(request) { - this.parse(it.body, URL(request.url)) - } - } - - @Deprecated( - "Use `parseRequest` or `parseUrlString` with coroutines instead", - ReplaceWith("OPDS2Parser.parseUrlString(url)"), - DeprecationLevel.WARNING - ) - fun parseURL(url: URL): Promise<ParseData, Exception> { - return DefaultHttpClient().fetchPromise(HttpRequest(url.toString())) then { - this.parse(it.body, url) - } + this.parse(it.body, request.url) + }.mapFailure { ErrorException(it) } } - @Deprecated( - "Use `parseRequest` or `parseUrlString` with coroutines instead", - ReplaceWith("OPDS2Parser.parseUrlString(url)"), - DeprecationLevel.WARNING - ) - @Suppress("unused") - fun parseURL(headers: MutableMap<String, String>, url: URL): Promise<ParseData, Exception> { - return DefaultHttpClient().fetchPromise(HttpRequest(url = url.toString(), headers = headers)) then { - this.parse(it.body, url) - } - } - - fun parse(jsonData: ByteArray, url: URL): ParseData { + public fun parse(jsonData: ByteArray, url: Url): ParseData { return if (isFeed(jsonData)) { ParseData(parseFeed(jsonData, url), null, 2) } else { - ParseData(null, Manifest.fromJSON(JSONObject(String(jsonData)))?.let { Publication(it) }, 2) + ParseData( + null, + parsePublication( + JSONObject(String(jsonData)), + url + ), + 2 + ) } } + @Deprecated( + "Provide an instance of `Url` instead", + ReplaceWith("parse(jsonData, url.toUrl()!!)"), + DeprecationLevel.ERROR + ) + @Suppress("UNUSED_PARAMETER") + public fun parse(jsonData: ByteArray, url: URL): ParseData = + throw NotImplementedError() + private fun isFeed(jsonData: ByteArray) = JSONObject(String(jsonData)).let { ( @@ -94,13 +97,13 @@ class OPDS2Parser { ) } - private fun parseFeed(jsonData: ByteArray, url: URL): Feed { + private fun parseFeed(jsonData: ByteArray, url: Url): Feed { val topLevelDict = JSONObject(String(jsonData)) val metadataDict: JSONObject = topLevelDict.getJSONObject("metadata") ?: throw Exception(OPDS2ParserError.MetadataNotFound.name) val title = metadataDict.getString("title") ?: throw Exception(OPDS2ParserError.MissingTitle.name) - feed = Feed(title, 2, url) + val feed = Feed.Builder(title, 2, url) parseFeedMetadata(opdsMetadata = feed.metadata, metadataDict = metadataDict) if (topLevelDict.has("@context")) { if (topLevelDict.get("@context") is JSONObject) { @@ -150,10 +153,10 @@ class OPDS2Parser { parseGroups(feed, groups) } } - return feed + return feed.build() } - private fun parseFeedMetadata(opdsMetadata: OpdsMetadata, metadataDict: JSONObject) { + private fun parseFeedMetadata(opdsMetadata: OpdsMetadata.Builder, metadataDict: JSONObject) { if (metadataDict.has("title")) { metadataDict.get("title").let { opdsMetadata.title = it.toString() @@ -186,21 +189,21 @@ class OPDS2Parser { } } - private fun parseFacets(feed: Feed, facets: JSONArray) { + private fun parseFacets(feed: Feed.Builder, facets: JSONArray) { for (i in 0 until facets.length()) { val facetDict = facets.getJSONObject(i) val metadata = facetDict.getJSONObject("metadata") ?: throw Exception(OPDS2ParserError.InvalidFacet.name) val title = metadata["title"] as? String ?: throw Exception(OPDS2ParserError.InvalidFacet.name) - val facet = Facet(title = title) + val facet = Facet.Builder(title = title) parseFeedMetadata(opdsMetadata = facet.metadata, metadataDict = metadata) if (facetDict.has("links")) { val links = facetDict.getJSONArray("links") ?: throw Exception(OPDS2ParserError.InvalidFacet.name) for (k in 0 until links.length()) { val linkDict = links.getJSONObject(k) - parseLink(feed, linkDict)?.let { + parseLink(linkDict, feed.href)?.let { facet.links.add(it) } } @@ -209,41 +212,41 @@ class OPDS2Parser { } } - private fun parseLinks(feed: Feed, links: JSONArray) { + private fun parseLinks(feed: Feed.Builder, links: JSONArray) { for (i in 0 until links.length()) { val linkDict = links.getJSONObject(i) - parseLink(feed, linkDict)?.let { + parseLink(linkDict, feed.href)?.let { feed.links.add(it) } } } - private fun parsePublications(feed: Feed, publications: JSONArray) { + private fun parsePublications(feed: Feed.Builder, publications: JSONArray) { for (i in 0 until publications.length()) { val pubDict = publications.getJSONObject(i) - Manifest.fromJSON(pubDict)?.let { manifest -> - feed.publications.add(Publication(manifest)) + parsePublication(pubDict, feed.href)?.let { + feed.publications.add(it) } } } - private fun parseNavigation(feed: Feed, navLinks: JSONArray) { + private fun parseNavigation(feed: Feed.Builder, navLinks: JSONArray) { for (i in 0 until navLinks.length()) { val navDict = navLinks.getJSONObject(i) - parseLink(feed, navDict)?.let { link -> + parseLink(navDict, feed.href)?.let { link -> feed.navigation.add(link) } } } - private fun parseGroups(feed: Feed, groups: JSONArray) { + private fun parseGroups(feed: Feed.Builder, groups: JSONArray) { for (i in 0 until groups.length()) { val groupDict = groups.getJSONObject(i) val metadata = groupDict.getJSONObject("metadata") ?: throw Exception(OPDS2ParserError.InvalidGroup.name) val title = metadata.getString("title") ?: throw Exception(OPDS2ParserError.InvalidGroup.name) - val group = Group(title = title) + val group = Group.Builder(title = title) parseFeedMetadata(opdsMetadata = group.metadata, metadataDict = metadata) if (groupDict.has("links")) { @@ -251,7 +254,7 @@ class OPDS2Parser { ?: throw Exception(OPDS2ParserError.InvalidGroup.name) for (j in 0 until links.length()) { val linkDict = links.getJSONObject(j) - parseLink(feed, linkDict)?.let { link -> + parseLink(linkDict, feed.href)?.let { link -> group.links.add(link) } } @@ -261,7 +264,7 @@ class OPDS2Parser { ?: throw Exception(OPDS2ParserError.InvalidGroup.name) for (j in 0 until links.length()) { val linkDict = links.getJSONObject(j) - parseLink(feed, linkDict)?.let { link -> + parseLink(linkDict, feed.href)?.let { link -> group.navigation.add(link) } } @@ -271,8 +274,8 @@ class OPDS2Parser { ?: throw Exception(OPDS2ParserError.InvalidGroup.name) for (j in 0 until publications.length()) { val pubDict = publications.getJSONObject(j) - Manifest.fromJSON(pubDict)?.let { manifest -> - group.publications.add(Publication(manifest)) + parsePublication(pubDict, feed.href)?.let { + group.publications.add(it) } } } @@ -280,9 +283,14 @@ class OPDS2Parser { } } - private fun parseLink(feed: Feed, json: JSONObject): Link? { - val baseUrl = feed.href.removeLastComponent() - return Link.fromJSON(json, normalizeHref = { Href(it, baseUrl.toString()).string }) - } + private fun parsePublication(json: JSONObject, baseUrl: Url): Publication? = + Manifest.fromJSON(json) + // Self link takes precedence over the given `baseUrl`. + ?.let { it.normalizeHrefsToBase(it.linkWithRel("self")?.href?.resolve() ?: baseUrl) } + ?.let { Publication(it) } + + private fun parseLink(json: JSONObject, baseUrl: Url): Link? = + Link.fromJSON(json) + ?.normalizeHrefsToBase(baseUrl) } } diff --git a/readium/opds/src/test/java/org/readium/r2/opds/OPDS1ParserTest.kt b/readium/opds/src/test/java/org/readium/r2/opds/OPDS1ParserTest.kt index c8f91d6a1c..c748060409 100644 --- a/readium/opds/src/test/java/org/readium/r2/opds/OPDS1ParserTest.kt +++ b/readium/opds/src/test/java/org/readium/r2/opds/OPDS1ParserTest.kt @@ -1,9 +1,7 @@ package org.readium.r2.opds -import java.net.URL import java.util.* import org.joda.time.DateTime -import org.json.JSONObject import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith @@ -13,6 +11,8 @@ import org.readium.r2.shared.opds.OpdsMetadata import org.readium.r2.shared.opds.ParseData import org.readium.r2.shared.publication.* import org.readium.r2.shared.publication.Properties +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -27,40 +27,50 @@ class OPDS1ParserTest { feed = Feed( title = "OPDS Catalog Root Example", type = 1, - href = URL("https://example.com"), + href = Url("https://example.com")!!, metadata = OpdsMetadata( title = "OPDS Catalog Root Example", modified = parseDate("2010-01-10T10:03:10Z") ), links = mutableListOf( Link( - href = "https://example.com/opds-catalogs/root.xml", - type = "application/atom+xml;profile=opds-catalog;kind=navigation", + href = Href("https://example.com/opds-catalogs/root.xml")!!, + mediaType = MediaType( + "application/atom+xml;profile=opds-catalog;kind=navigation" + )!!, rels = setOf("self"), properties = Properties() ), Link( - href = "https://example.com/opds-catalogs/root.xml", - type = "application/atom+xml;profile=opds-catalog;kind=navigation", + href = Href("https://example.com/opds-catalogs/root.xml")!!, + mediaType = MediaType( + "application/atom+xml;profile=opds-catalog;kind=navigation" + )!!, rels = setOf("start") ) ), navigation = mutableListOf( Link( - href = "https://example.com/opds-catalogs/popular.xml", - type = "application/atom+xml;profile=opds-catalog;kind=acquisition", + href = Href("https://example.com/opds-catalogs/popular.xml")!!, + mediaType = MediaType( + "application/atom+xml;profile=opds-catalog;kind=acquisition" + )!!, title = "Popular Publications", rels = setOf("http://opds-spec.org/sort/popular") ), Link( - href = "https://example.com/opds-catalogs/new.xml", - type = "application/atom+xml;profile=opds-catalog;kind=acquisition", + href = Href("https://example.com/opds-catalogs/new.xml")!!, + mediaType = MediaType( + "application/atom+xml;profile=opds-catalog;kind=acquisition" + )!!, title = "New Publications", rels = setOf("http://opds-spec.org/sort/new") ), Link( - href = "https://example.com/opds-catalogs/unpopular.xml", - type = "application/atom+xml;profile=opds-catalog;kind=acquisition", + href = Href("https://example.com/opds-catalogs/unpopular.xml")!!, + mediaType = MediaType( + "application/atom+xml;profile=opds-catalog;kind=acquisition" + )!!, title = "Unpopular Publications", rels = setOf("subsection") ) @@ -81,7 +91,7 @@ class OPDS1ParserTest { val feed = parseData.feed!! assertEquals("Unpopular Publications", feed.title) assertEquals(1, feed.type) - assertEquals(URL("https://example.com"), feed.href) + assertEquals(Url("https://example.com"), feed.href) assertEquals( OpdsMetadata( title = "Unpopular Publications", @@ -92,23 +102,31 @@ class OPDS1ParserTest { assertEquals( mutableListOf( Link( - href = "https://example.com/opds-catalogs/vampire.farming.xml", - type = "application/atom+xml;profile=opds-catalog;kind=acquisition", + href = Href("https://example.com/opds-catalogs/vampire.farming.xml")!!, + mediaType = MediaType( + "application/atom+xml;profile=opds-catalog;kind=acquisition" + )!!, rels = setOf("related") ), Link( - href = "https://example.com/opds-catalogs/unpopular.xml", - type = "application/atom+xml;profile=opds-catalog;kind=acquisition", + href = Href("https://example.com/opds-catalogs/unpopular.xml")!!, + mediaType = MediaType( + "application/atom+xml;profile=opds-catalog;kind=acquisition" + )!!, rels = setOf("self") ), Link( - href = "https://example.com/opds-catalogs/root.xml", - type = "application/atom+xml;profile=opds-catalog;kind=navigation", + href = Href("https://example.com/opds-catalogs/root.xml")!!, + mediaType = MediaType( + "application/atom+xml;profile=opds-catalog;kind=navigation" + )!!, rels = setOf("start") ), Link( - href = "https://example.com/opds-catalogs/root.xml", - type = "application/atom+xml;profile=opds-catalog;kind=navigation", + href = Href("https://example.com/opds-catalogs/root.xml")!!, + mediaType = MediaType( + "application/atom+xml;profile=opds-catalog;kind=navigation" + )!!, rels = setOf("up") ) ), @@ -121,12 +139,12 @@ class OPDS1ParserTest { metadata = OpdsMetadata(title = "Categories"), links = mutableListOf( Link( - href = "https://example.com/sci-fi", + href = Href("https://example.com/sci-fi")!!, title = "Science-Fiction", rels = setOf("http://opds-spec.org/facet") ), Link( - href = "https://example.com/romance", + href = Href("https://example.com/romance")!!, title = "Romance", rels = setOf("http://opds-spec.org/facet"), properties = Properties(mapOf("numberOfItems" to 600)) @@ -156,31 +174,37 @@ class OPDS1ParserTest { authors = listOf( Contributor( localizedName = LocalizedString("Bob the Recursive"), - links = listOf(Link(href = "http://opds-spec.org/authors/1285")) + links = listOf( + Link(href = Href("http://opds-spec.org/authors/1285")!!) + ) ) ), description = "The story of the son of the Bob and the gallant part he played in the lives of a man and a woman." ), links = listOf( Link( - href = "https://example.com/covers/4561.thmb.gif", - type = "image/gif", + href = Href("https://example.com/covers/4561.thmb.gif")!!, + mediaType = MediaType("image/gif")!!, rels = setOf("http://opds-spec.org/image/thumbnail") ), Link( - href = "https://example.com/opds-catalogs/entries/4571.complete.xml", - type = "application/atom+xml;type=entry;profile=opds-catalog", + href = Href( + "https://example.com/opds-catalogs/entries/4571.complete.xml" + )!!, + mediaType = MediaType( + "application/atom+xml;type=entry;profile=opds-catalog" + )!!, title = "Complete Catalog Entry for Bob, Son of Bob", rels = setOf("alternate") ), Link( - href = "https://example.com/content/free/4561.epub", - type = "application/epub+zip", + href = Href("https://example.com/content/free/4561.epub")!!, + mediaType = MediaType("application/epub+zip")!!, rels = setOf("http://opds-spec.org/acquisition") ), Link( - href = "https://example.com/content/free/4561.mobi", - type = "application/x-mobipocket-ebook", + href = Href("https://example.com/content/free/4561.mobi")!!, + mediaType = MediaType("application/x-mobipocket-ebook")!!, rels = setOf("http://opds-spec.org/acquisition") ) ), @@ -189,16 +213,16 @@ class OPDS1ParserTest { PublicationCollection( links = listOf( Link( - href = "https://example.com/covers/4561.lrg.png", - type = "image/png", + href = Href("https://example.com/covers/4561.lrg.png")!!, + mediaType = MediaType("image/png")!!, rels = setOf("http://opds-spec.org/image") ) ) ) ) - ), + ) ).toJSON(), - JSONObject(feed.publications[0].jsonManifest) + feed.publications[0].manifest.toJSON() ) assertJSONEquals( @@ -211,15 +235,21 @@ class OPDS1ParserTest { authors = listOf( Contributor( localizedName = LocalizedString("Stampy McGee"), - links = listOf(Link(href = "http://opds-spec.org/authors/21285")) + links = listOf( + Link(href = Href("http://opds-spec.org/authors/21285")!!) + ) ), Contributor( localizedName = LocalizedString("Alice McGee"), - links = listOf(Link(href = "http://opds-spec.org/authors/21284")) + links = listOf( + Link(href = Href("http://opds-spec.org/authors/21284")!!) + ) ), Contributor( localizedName = LocalizedString("Harold McGee"), - links = listOf(Link(href = "http://opds-spec.org/authors/21283")) + links = listOf( + Link(href = Href("http://opds-spec.org/authors/21283")!!) + ) ) ), publishers = listOf( @@ -229,10 +259,12 @@ class OPDS1ParserTest { ), links = listOf( Link( - href = "https://example.com/content/buy/11241.epub", - type = "application/epub+zip", + href = Href("https://example.com/content/buy/11241.epub")!!, + mediaType = MediaType("application/epub+zip")!!, rels = setOf("http://opds-spec.org/acquisition/buy"), - properties = Properties(mapOf("price" to mapOf("currency" to "USD", "value" to 18.99))) + properties = Properties( + mapOf("price" to mapOf("currency" to "USD", "value" to 18.99)) + ) ) ), subcollections = mapOf( @@ -240,16 +272,16 @@ class OPDS1ParserTest { PublicationCollection( links = listOf( Link( - href = "https://example.com/covers/11241.lrg.jpg", - type = "image/jpeg", + href = Href("https://example.com/covers/11241.lrg.jpg")!!, + mediaType = MediaType("image/jpeg")!!, rels = setOf("http://opds-spec.org/image") ) ) ) ) - ), + ) ).toJSON(), - JSONObject(feed.publications[1].jsonManifest) + feed.publications[1].manifest.toJSON() ) } @@ -276,30 +308,36 @@ class OPDS1ParserTest { authors = listOf( Contributor( localizedName = LocalizedString("Bob the Recursive"), - links = listOf(Link(href = "http://opds-spec.org/authors/1285")) + links = listOf( + Link(href = Href("http://opds-spec.org/authors/1285")!!) + ) ) ), description = "The story of the son of the Bob and the gallant part he played in the lives of a man and a woman. Bob begins his humble life under the wandering eye of his senile mother, but quickly learns how to escape into the wilder world. Follow Bob as he uncovers his father's past and uses those lessons to improve the lives of others." ), links = listOf( Link( - href = "https://example.com/covers/4561.thmb.gif", - type = "image/gif", + href = Href("https://example.com/covers/4561.thmb.gif")!!, + mediaType = MediaType("image/gif")!!, rels = setOf("http://opds-spec.org/image/thumbnail") ), Link( - href = "https://example.com/opds-catalogs/entries/4571.complete.xml", - type = "application/atom+xml;type=entry;profile=opds-catalog", + href = Href( + "https://example.com/opds-catalogs/entries/4571.complete.xml" + )!!, + mediaType = MediaType( + "application/atom+xml;type=entry;profile=opds-catalog" + )!!, rels = setOf("self") ), Link( - href = "https://example.com/content/free/4561.epub", - type = "application/epub+zip", + href = Href("https://example.com/content/free/4561.epub")!!, + mediaType = MediaType("application/epub+zip")!!, rels = setOf("http://opds-spec.org/acquisition") ), Link( - href = "https://example.com/content/free/4561.mobi", - type = "application/x-mobipocket-ebook", + href = Href("https://example.com/content/free/4561.mobi")!!, + mediaType = MediaType("application/x-mobipocket-ebook")!!, rels = setOf("http://opds-spec.org/acquisition") ) ), @@ -308,20 +346,20 @@ class OPDS1ParserTest { PublicationCollection( links = listOf( Link( - href = "https://example.com/covers/4561.lrg.png", - type = "image/png", + href = Href("https://example.com/covers/4561.lrg.png")!!, + mediaType = MediaType("image/png")!!, rels = setOf("http://opds-spec.org/image") ) ) ) ) - ), + ) ).toJSON(), - JSONObject(publication!!.jsonManifest) + publication!!.manifest.toJSON() ) } - private fun parse(filename: String, url: URL = URL("https://example.com")): ParseData = + private fun parse(filename: String, url: Url = Url("https://example.com")!!): ParseData = OPDS1Parser.parse(fixtures.bytesAt(filename), url) private fun parseDate(string: String): Date = diff --git a/readium/shared/build.gradle.kts b/readium/shared/build.gradle.kts index 219216d124..8282274403 100644 --- a/readium/shared/build.gradle.kts +++ b/readium/shared/build.gradle.kts @@ -12,15 +12,17 @@ plugins { } android { - compileSdk = 33 + resourcePrefix = "readium_" + + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { allWarningsAsErrors = true @@ -38,25 +40,24 @@ android { proguardFiles(getDefaultProguardFile("proguard-android.txt")) } } + buildFeatures { + buildConfig = true + } namespace = "org.readium.r2.shared" } +kotlin { + explicitApi() +} + rootProject.ext["publish.artifactId"] = "readium-shared" apply(from = "$rootDir/scripts/publish-module.gradle") dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.browser) - implementation("com.github.kittinunf.fuel:fuel-android:2.3.1") - implementation("com.github.kittinunf.fuel:fuel:2.3.1") implementation(libs.timber) implementation(libs.joda.time) - implementation("nl.komponents.kovenant:kovenant-android:3.3.0") - implementation("nl.komponents.kovenant:kovenant-combine:3.3.0") - implementation("nl.komponents.kovenant:kovenant-core:3.3.0") - implementation("nl.komponents.kovenant:kovenant-functional:3.3.0") - implementation("nl.komponents.kovenant:kovenant-jvm:3.3.0") - implementation("nl.komponents.kovenant:kovenant:3.3.0") implementation(libs.kotlin.reflect) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) diff --git a/readium/shared/src/androidTest/java/org/readium/r2/shared/util/HrefTest.kt b/readium/shared/src/androidTest/java/org/readium/r2/shared/util/HrefTest.kt deleted file mode 100644 index 34d201fbd5..0000000000 --- a/readium/shared/src/androidTest/java/org/readium/r2/shared/util/HrefTest.kt +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright 2020 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Test -import org.readium.r2.shared.util.Href.QueryParameter - -class HrefTest { - - @Test - fun normalizeToBaseHref() { - assertEquals("/folder/", Href("", "/folder/").string) - assertEquals("/", Href("/", "/folder/").string) - - assertEquals("/foo/bar.txt", Href("foo/bar.txt", "").string) - assertEquals("/foo/bar.txt", Href("foo/bar.txt", "/").string) - assertEquals("/foo/bar.txt", Href("foo/bar.txt", "/file.txt").string) - assertEquals("/foo/bar.txt", Href("foo/bar.txt", "/folder").string) - assertEquals("/folder/foo/bar.txt", Href("foo/bar.txt", "/folder/").string) - assertEquals("http://example.com/folder/foo/bar.txt", Href("foo/bar.txt", "http://example.com/folder/file.txt").string) - assertEquals("http://example.com/foo/bar.txt", Href("foo/bar.txt", "http://example.com/folder").string) - assertEquals("http://example.com/folder/foo/bar.txt", Href("foo/bar.txt", "http://example.com/folder/").string) - - assertEquals("/foo/bar.txt", Href("/foo/bar.txt", "").string) - assertEquals("/foo/bar.txt", Href("/foo/bar.txt", "/").string) - assertEquals("/foo/bar.txt", Href("/foo/bar.txt", "/file.txt").string) - assertEquals("/foo/bar.txt", Href("/foo/bar.txt", "/folder").string) - assertEquals("/foo/bar.txt", Href("/foo/bar.txt", "/folder/").string) - assertEquals("http://example.com/foo/bar.txt", Href("/foo/bar.txt", "http://example.com/folder/file.txt").string) - assertEquals("http://example.com/foo/bar.txt", Href("/foo/bar.txt", "http://example.com/folder").string) - assertEquals("http://example.com/foo/bar.txt", Href("/foo/bar.txt", "http://example.com/folder/").string) - - assertEquals("/foo/bar.txt", Href("../foo/bar.txt", "").string) - assertEquals("/foo/bar.txt", Href("../foo/bar.txt", "/").string) - assertEquals("/foo/bar.txt", Href("../foo/bar.txt", "/file.txt").string) - assertEquals("/foo/bar.txt", Href("../foo/bar.txt", "/folder").string) - assertEquals("/foo/bar.txt", Href("../foo/bar.txt", "/folder/").string) - assertEquals("http://example.com/foo/bar.txt", Href("../foo/bar.txt", "http://example.com/folder/file.txt").string) - assertEquals("http://example.com/foo/bar.txt", Href("../foo/bar.txt", "http://example.com/folder").string) - assertEquals("http://example.com/foo/bar.txt", Href("../foo/bar.txt", "http://example.com/folder/").string) - - assertEquals("/bar.txt", Href("foo/../bar.txt", "").string) - assertEquals("/bar.txt", Href("foo/../bar.txt", "/").string) - assertEquals("/bar.txt", Href("foo/../bar.txt", "/file.txt").string) - assertEquals("/bar.txt", Href("foo/../bar.txt", "/folder").string) - assertEquals("/folder/bar.txt", Href("foo/../bar.txt", "/folder/").string) - assertEquals("http://example.com/folder/bar.txt", Href("foo/../bar.txt", "http://example.com/folder/file.txt").string) - assertEquals("http://example.com/bar.txt", Href("foo/../bar.txt", "http://example.com/folder").string) - assertEquals("http://example.com/folder/bar.txt", Href("foo/../bar.txt", "http://example.com/folder/").string) - - assertEquals("http://absolute.com/foo/bar.txt", Href("http://absolute.com/foo/bar.txt", "/").string) - assertEquals("http://absolute.com/foo/bar.txt", Href("http://absolute.com/foo/bar.txt", "https://example.com/").string) - - // Anchor and query parameters are preserved - assertEquals("/foo/bar.txt#anchor", Href("foo/bar.txt#anchor", "/").string) - assertEquals("/foo/bar.txt?query=param#anchor", Href("foo/bar.txt?query=param#anchor", "/").string) - assertEquals("/foo/bar.txt?query=param#anchor", Href("/foo/bar.txt?query=param#anchor", "/").string) - assertEquals("http://absolute.com/foo/bar.txt?query=param#anchor", Href("http://absolute.com/foo/bar.txt?query=param#anchor", "/").string) - - assertEquals("/foo/bar.txt#anchor", Href("foo/bar.txt#anchor", "/").string) - assertEquals("/foo/bar.txt?query=param#anchor", Href("foo/bar.txt?query=param#anchor", "/").string) - assertEquals("/foo/bar.txt?query=param#anchor", Href("/foo/bar.txt?query=param#anchor", "/").string) - assertEquals("http://absolute.com/foo/bar.txt?query=param#anchor", Href("http://absolute.com/foo/bar.txt?query=param#anchor", "/").string) - - // HREF that is just an anchor - assertEquals("/#anchor", Href("#anchor", "").string) - assertEquals("/#anchor", Href("#anchor", "/").string) - assertEquals("/file.txt#anchor", Href("#anchor", "/file.txt").string) - assertEquals("/folder#anchor", Href("#anchor", "/folder").string) - assertEquals("/folder/#anchor", Href("#anchor", "/folder/").string) - assertEquals("http://example.com/folder/file.txt#anchor", Href("#anchor", "http://example.com/folder/file.txt").string) - assertEquals("http://example.com/folder#anchor", Href("#anchor", "http://example.com/folder").string) - assertEquals("http://example.com/folder/#anchor", Href("#anchor", "http://example.com/folder/").string) - - // HREF containing spaces. - assertEquals("/foo bar.txt", Href("foo bar.txt", "").string) - assertEquals("/foo bar.txt", Href("foo bar.txt", "/").string) - assertEquals("/foo bar.txt", Href("foo bar.txt", "/file.txt").string) - assertEquals("/foo bar.txt", Href("foo bar.txt", "/base folder").string) - assertEquals("/base folder/foo bar.txt", Href("foo bar.txt", "/base folder/").string) - assertEquals("/base folder/foo bar.txt", Href("foo bar.txt", "/base folder/file.txt").string) - assertEquals("/base folder/foo bar.txt", Href("foo bar.txt", "base folder/file.txt").string) - - // HREF containing special characters - assertEquals("/base%folder/foo bar/baz%qux.txt", Href("foo bar/baz%qux.txt", "/base%folder/").string) - assertEquals("/base folder/foo bar/baz%qux.txt", Href("foo%20bar/baz%25qux.txt", "/base%20folder/").string) - assertEquals("http://example.com/foo bar/baz qux.txt", Href("foo bar/baz qux.txt", "http://example.com/base%20folder").string) - assertEquals("http://example.com/base folder/foo bar/baz qux.txt", Href("foo bar/baz qux.txt", "http://example.com/base%20folder/").string) - assertEquals("http://example.com/base folder/foo bar/baz%qux.txt", Href("foo bar/baz%qux.txt", "http://example.com/base%20folder/").string) - assertEquals("/foo bar.txt?query=param#anchor", Href("/foo bar.txt?query=param#anchor", "/").string) - assertEquals("http://example.com/foo bar.txt?query=param#anchor", Href("/foo bar.txt?query=param#anchor", "http://example.com/").string) - assertEquals("http://example.com/foo bar.txt?query=param#anchor", Href("/foo%20bar.txt?query=param#anchor", "http://example.com/").string) - assertEquals("http://absolute.com/foo bar.txt?query=param#Hello world £500", Href("http://absolute.com/foo%20bar.txt?query=param#Hello%20world%20%C2%A3500", "/").string) - assertEquals("http://absolute.com/foo bar.txt?query=param#Hello world £500", Href("http://absolute.com/foo bar.txt?query=param#Hello world £500", "/").string) - } - - @Test - fun getPercentEncodedString() { - assertEquals("/folder/", Href("", "/folder/").percentEncodedString) - assertEquals("/", Href("/", "/folder/").percentEncodedString) - - assertEquals("/foo/bar.txt", Href("foo/bar.txt", "").percentEncodedString) - assertEquals("/foo/bar.txt", Href("foo/bar.txt", "/").percentEncodedString) - assertEquals("/foo/bar.txt", Href("foo/bar.txt", "/file.txt").percentEncodedString) - assertEquals("/foo/bar.txt", Href("foo/bar.txt", "/folder").percentEncodedString) - assertEquals("/folder/foo/bar.txt", Href("foo/bar.txt", "/folder/").percentEncodedString) - assertEquals("http://example.com/folder/foo/bar.txt", Href("foo/bar.txt", "http://example.com/folder/file.txt").percentEncodedString) - assertEquals("http://example.com/foo/bar.txt", Href("foo/bar.txt", "http://example.com/folder").percentEncodedString) - assertEquals("http://example.com/folder/foo/bar.txt", Href("foo/bar.txt", "http://example.com/folder/").percentEncodedString) - - assertEquals("/foo/bar.txt", Href("/foo/bar.txt", "").percentEncodedString) - assertEquals("/foo/bar.txt", Href("/foo/bar.txt", "/").percentEncodedString) - assertEquals("/foo/bar.txt", Href("/foo/bar.txt", "/file.txt").percentEncodedString) - assertEquals("/foo/bar.txt", Href("/foo/bar.txt", "/folder").percentEncodedString) - assertEquals("/foo/bar.txt", Href("/foo/bar.txt", "/folder/").percentEncodedString) - assertEquals("http://example.com/foo/bar.txt", Href("/foo/bar.txt", "http://example.com/folder/file.txt").percentEncodedString) - assertEquals("http://example.com/foo/bar.txt", Href("/foo/bar.txt", "http://example.com/folder").percentEncodedString) - assertEquals("http://example.com/foo/bar.txt", Href("/foo/bar.txt", "http://example.com/folder/").percentEncodedString) - - assertEquals("/foo/bar.txt", Href("../foo/bar.txt", "").percentEncodedString) - assertEquals("/foo/bar.txt", Href("../foo/bar.txt", "/").percentEncodedString) - assertEquals("/foo/bar.txt", Href("../foo/bar.txt", "/file.txt").percentEncodedString) - assertEquals("/foo/bar.txt", Href("../foo/bar.txt", "/folder").percentEncodedString) - assertEquals("/foo/bar.txt", Href("../foo/bar.txt", "/folder/").percentEncodedString) - assertEquals("http://example.com/foo/bar.txt", Href("../foo/bar.txt", "http://example.com/folder/file.txt").percentEncodedString) - assertEquals("http://example.com/foo/bar.txt", Href("../foo/bar.txt", "http://example.com/folder").percentEncodedString) - assertEquals("http://example.com/foo/bar.txt", Href("../foo/bar.txt", "http://example.com/folder/").percentEncodedString) - - assertEquals("/bar.txt", Href("foo/../bar.txt", "").percentEncodedString) - assertEquals("/bar.txt", Href("foo/../bar.txt", "/").percentEncodedString) - assertEquals("/bar.txt", Href("foo/../bar.txt", "/file.txt").percentEncodedString) - assertEquals("/bar.txt", Href("foo/../bar.txt", "/folder").percentEncodedString) - assertEquals("/folder/bar.txt", Href("foo/../bar.txt", "/folder/").percentEncodedString) - assertEquals("http://example.com/folder/bar.txt", Href("foo/../bar.txt", "http://example.com/folder/file.txt").percentEncodedString) - assertEquals("http://example.com/bar.txt", Href("foo/../bar.txt", "http://example.com/folder").percentEncodedString) - assertEquals("http://example.com/folder/bar.txt", Href("foo/../bar.txt", "http://example.com/folder/").percentEncodedString) - - assertEquals("http://absolute.com/foo/bar.txt", Href("http://absolute.com/foo/bar.txt", "/").percentEncodedString) - assertEquals("http://absolute.com/foo/bar.txt", Href("http://absolute.com/foo/bar.txt", "https://example.com/").percentEncodedString) - - // Anchor and query parameters are preserved - assertEquals("/foo/bar.txt#anchor", Href("foo/bar.txt#anchor", "/").percentEncodedString) - assertEquals("/foo/bar.txt?query=param#anchor", Href("foo/bar.txt?query=param#anchor", "/").percentEncodedString) - assertEquals("/foo/bar.txt?query=param#anchor", Href("/foo/bar.txt?query=param#anchor", "/").percentEncodedString) - assertEquals("http://absolute.com/foo/bar.txt?query=param#anchor", Href("http://absolute.com/foo/bar.txt?query=param#anchor", "/").percentEncodedString) - - assertEquals("/foo/bar.txt#anchor", Href("foo/bar.txt#anchor", "/").percentEncodedString) - assertEquals("/foo/bar.txt?query=param#anchor", Href("foo/bar.txt?query=param#anchor", "/").percentEncodedString) - assertEquals("/foo/bar.txt?query=param#anchor", Href("/foo/bar.txt?query=param#anchor", "/").percentEncodedString) - assertEquals("http://absolute.com/foo/bar.txt?query=param#anchor", Href("http://absolute.com/foo/bar.txt?query=param#anchor", "/").percentEncodedString) - - // HREF that is just an anchor - assertEquals("/#anchor", Href("#anchor", "").percentEncodedString) - assertEquals("/#anchor", Href("#anchor", "/").percentEncodedString) - assertEquals("/file.txt#anchor", Href("#anchor", "/file.txt").percentEncodedString) - assertEquals("/folder#anchor", Href("#anchor", "/folder").percentEncodedString) - assertEquals("/folder/#anchor", Href("#anchor", "/folder/").percentEncodedString) - assertEquals("http://example.com/folder/file.txt#anchor", Href("#anchor", "http://example.com/folder/file.txt").percentEncodedString) - assertEquals("http://example.com/folder#anchor", Href("#anchor", "http://example.com/folder").percentEncodedString) - assertEquals("http://example.com/folder/#anchor", Href("#anchor", "http://example.com/folder/").percentEncodedString) - - // HREF containing spaces. - assertEquals("/foo%20bar.txt", Href("foo bar.txt", "").percentEncodedString) - assertEquals("/foo%20bar.txt", Href("foo bar.txt", "/").percentEncodedString) - assertEquals("/foo%20bar.txt", Href("foo bar.txt", "/file.txt").percentEncodedString) - assertEquals("/foo%20bar.txt", Href("foo bar.txt", "/base folder").percentEncodedString) - assertEquals("/base%20folder/foo%20bar.txt", Href("foo bar.txt", "/base folder/").percentEncodedString) - assertEquals("/base%20folder/foo%20bar.txt", Href("foo bar.txt", "/base folder/file.txt").percentEncodedString) - assertEquals("/base%20folder/foo%20bar.txt", Href("foo bar.txt", "base folder/file.txt").percentEncodedString) - - // HREF containing special characters - assertEquals("/base%25folder/foo%20bar/baz%25qux.txt", Href("foo bar/baz%qux.txt", "/base%folder/").percentEncodedString) - assertEquals("/base%20folder/foo%20bar/baz%25qux.txt", Href("foo%20bar/baz%25qux.txt", "/base%20folder/").percentEncodedString) - assertEquals("http://example.com/foo%20bar/baz%20qux.txt", Href("foo bar/baz qux.txt", "http://example.com/base%20folder").percentEncodedString) - assertEquals("http://example.com/base%20folder/foo%20bar/baz%20qux.txt", Href("foo bar/baz qux.txt", "http://example.com/base%20folder/").percentEncodedString) - assertEquals("http://example.com/base%20folder/foo%20bar/baz%25qux.txt", Href("foo bar/baz%qux.txt", "http://example.com/base%20folder/").percentEncodedString) - assertEquals("/foo%20bar.txt?query=param#anchor", Href("/foo bar.txt?query=param#anchor", "/").percentEncodedString) - assertEquals("http://example.com/foo%20bar.txt?query=param#anchor", Href("/foo bar.txt?query=param#anchor", "http://example.com/").percentEncodedString) - assertEquals("http://example.com/foo%20bar.txt?query=param#anchor", Href("/foo%20bar.txt?query=param#anchor", "http://example.com/").percentEncodedString) - assertEquals("http://absolute.com/foo%20bar.txt?query=param#Hello%20world%20%C2%A3500", Href("http://absolute.com/foo%20bar.txt?query=param#Hello%20world%20%C2%A3500", "/").percentEncodedString) - assertEquals("http://absolute.com/foo%20bar.txt?query=param#Hello%20world%20%C2%A3500", Href("http://absolute.com/foo bar.txt?query=param#Hello world £500", "/").percentEncodedString) - } - - @Test - fun getQueryParameters() { - assertEquals(emptyList<QueryParameter>(), Href("http://domain.com/path").queryParameters) - assertEquals(listOf(QueryParameter(name = "query", value = "param")), Href("http://domain.com/path?query=param#anchor").queryParameters) - assertEquals( - listOf( - QueryParameter(name = "query", value = "param"), - QueryParameter(name = "fruit", value = "banana"), - QueryParameter(name = "query", value = "other"), - QueryParameter(name = "empty", value = null) - ), - Href("http://domain.com/path?query=param&fruit=banana&query=other&empty").queryParameters - ) - } - - @Test - fun getFirstParameterNamedX() { - val params = listOf( - QueryParameter(name = "query", value = "param"), - QueryParameter(name = "fruit", value = "banana"), - QueryParameter(name = "query", value = "other"), - QueryParameter(name = "empty", value = null) - ) - - assertEquals(params.firstNamedOrNull("query"), "param") - assertEquals(params.firstNamedOrNull("fruit"), "banana") - assertNull(params.firstNamedOrNull("empty")) - assertNull(params.firstNamedOrNull("not-found")) - } - - @Test - fun getAllParametersNamedX() { - val params = listOf( - QueryParameter(name = "query", value = "param"), - QueryParameter(name = "fruit", value = "banana"), - QueryParameter(name = "query", value = "other"), - QueryParameter(name = "empty", value = null) - ) - - assertEquals(params.allNamed("query"), listOf("param", "other")) - assertEquals(params.allNamed("fruit"), listOf("banana")) - assertEquals(params.allNamed("empty"), emptyList<QueryParameter>()) - assertEquals(params.allNamed("not-found"), emptyList<QueryParameter>()) - } -} diff --git a/readium/shared/src/main/AndroidManifest.xml b/readium/shared/src/main/AndroidManifest.xml index cd35c46d46..eb475b4918 100644 --- a/readium/shared/src/main/AndroidManifest.xml +++ b/readium/shared/src/main/AndroidManifest.xml @@ -1,10 +1,7 @@ <!-- - ~ Module: r2-shared-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <manifest /> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/Deprecated.kt b/readium/shared/src/main/java/org/readium/r2/shared/Deprecated.kt index b3d793f758..eaafca90a1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/Deprecated.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/Deprecated.kt @@ -11,6 +11,7 @@ package org.readium.r2.shared +import android.content.Context import java.net.URL import org.json.JSONObject import org.readium.r2.shared.extensions.removeLastComponent @@ -23,87 +24,192 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.Subject import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.publication.presentation.Presentation -import org.readium.r2.shared.util.Href -@Deprecated("Moved to another package", ReplaceWith("org.readium.r2.shared.publication.Locator")) -typealias Locator = org.readium.r2.shared.publication.Locator - -@Deprecated("Renamed into [Locator.Locations]", ReplaceWith("Locator.Locations", "org.readium.r2.shared.publication.Locator")) -typealias Locations = org.readium.r2.shared.publication.Locator.Locations - -@Deprecated("Renamed into [Locator.Text]", ReplaceWith("Locator.Text", "org.readium.r2.shared.publication.Locator")) -typealias LocatorText = org.readium.r2.shared.publication.Locator.Text - -@Deprecated("Moved to another package", ReplaceWith("Locator.Text", "org.readium.r2.shared.publication.html.DomRange")) -typealias DomRange = org.readium.r2.shared.publication.html.DomRange - -@Deprecated("Renamed into [DomRange.Point]", ReplaceWith("DomRange.Point", "org.readium.r2.shared.publication.html.DomRange")) -typealias Range = org.readium.r2.shared.publication.html.DomRange.Point - -@Deprecated("Refactored into [LocalizedString]", ReplaceWith("org.readium.r2.shared.publication.LocalizedString")) -typealias MultilanguageString = org.readium.r2.shared.publication.LocalizedString - -@Deprecated("Renamed into [ReadingProgression]", ReplaceWith("org.readium.r2.shared.publication.ReadingProgression")) -typealias PageProgressionDirection = org.readium.r2.shared.publication.ReadingProgression - -@Deprecated("Moved to another package", ReplaceWith("org.readium.r2.shared.publication.Publication")) -typealias Publication = org.readium.r2.shared.publication.Publication - -@Deprecated("Moved to another package", ReplaceWith("org.readium.r2.shared.publication.Link")) -typealias Link = Link - -@Deprecated("Moved to another package", ReplaceWith("org.readium.r2.shared.publication.Properties")) -typealias Properties = Properties - -@Deprecated("Moved to another package", ReplaceWith("org.readium.r2.shared.publication.Metadata")) -typealias Metadata = Metadata - -@Deprecated("Moved to another package", ReplaceWith("org.readium.r2.shared.publication.Contributor")) -typealias Contributor = Contributor - -@Deprecated("Moved to another package", ReplaceWith("org.readium.r2.shared.publication.Collection")) -typealias Collection = Collection - -@Deprecated("Moved to another package", ReplaceWith("org.readium.r2.shared.publication.Subject")) -typealias Subject = Subject - -@Deprecated("Moved to another package", ReplaceWith("org.readium.r2.shared.publication.encryption.Encryption")) -typealias Encryption = Encryption - -@Deprecated("Refactored into [Presentation]", ReplaceWith("org.readium.r2.shared.publication.presentation.Presentation")) -typealias Rendition = Presentation - -@Deprecated("Refactored into [EpubLayout]", ReplaceWith("org.readium.r2.shared.publication.epub.EpubLayout")) -typealias RenditionLayout = org.readium.r2.shared.publication.epub.EpubLayout - -@Deprecated("Refactored into [Presentation.Overflow]", ReplaceWith("org.readium.r2.shared.publication.presentation.Presentation.Overflow")) -typealias RenditionFlow = Presentation.Overflow - -@Deprecated("Refactored into [Presentation.Orientation]", ReplaceWith("org.readium.r2.shared.publication.presentation.Presentation.Orientation")) -typealias RenditionOrientation = Presentation.Orientation - -@Deprecated("Refactored into [Presentation.Spread]", ReplaceWith("org.readium.r2.shared.publication.presentation.Presentation.Spread")) -typealias RenditionSpread = Presentation.Spread +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.r2.shared.publication.Locator"), + level = DeprecationLevel.ERROR +) +public typealias Locator = org.readium.r2.shared.publication.Locator + +@Deprecated( + "Renamed into [Locator.Locations]", + ReplaceWith("Locator.Locations", "org.readium.r2.shared.publication.Locator"), + level = DeprecationLevel.ERROR +) +public typealias Locations = org.readium.r2.shared.publication.Locator.Locations + +@Deprecated( + "Renamed into [Locator.Text]", + ReplaceWith("Locator.Text", "org.readium.r2.shared.publication.Locator"), + level = DeprecationLevel.ERROR +) +public typealias LocatorText = org.readium.r2.shared.publication.Locator.Text + +@Deprecated( + "Moved to another package", + ReplaceWith("Locator.Text", "org.readium.r2.shared.publication.html.DomRange"), + level = DeprecationLevel.ERROR +) +public typealias DomRange = org.readium.r2.shared.publication.html.DomRange + +@Deprecated( + "Renamed into [DomRange.Point]", + ReplaceWith("DomRange.Point", "org.readium.r2.shared.publication.html.DomRange"), + level = DeprecationLevel.ERROR +) +public typealias Range = org.readium.r2.shared.publication.html.DomRange.Point + +@Deprecated( + "Refactored into [LocalizedString]", + ReplaceWith("org.readium.r2.shared.publication.LocalizedString"), + level = DeprecationLevel.ERROR +) +public typealias MultilanguageString = org.readium.r2.shared.publication.LocalizedString + +@Deprecated( + "Renamed into [ReadingProgression]", + ReplaceWith("org.readium.r2.shared.publication.ReadingProgression"), + level = DeprecationLevel.ERROR +) +public typealias PageProgressionDirection = org.readium.r2.shared.publication.ReadingProgression + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.r2.shared.publication.Publication"), + level = DeprecationLevel.ERROR +) +public typealias Publication = org.readium.r2.shared.publication.Publication + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.r2.shared.publication.Link"), + level = DeprecationLevel.ERROR +) +public typealias Link = Link + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.r2.shared.publication.Properties"), + level = DeprecationLevel.ERROR +) +public typealias Properties = Properties + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.r2.shared.publication.Metadata"), + level = DeprecationLevel.ERROR +) +public typealias Metadata = Metadata + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.r2.shared.publication.Contributor"), + level = DeprecationLevel.ERROR +) +public typealias Contributor = Contributor + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.r2.shared.publication.Collection"), + level = DeprecationLevel.ERROR +) +public typealias Collection = Collection + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.r2.shared.publication.Subject"), + level = DeprecationLevel.ERROR +) +public typealias Subject = Subject + +@Deprecated( + "Moved to another package", + ReplaceWith("org.readium.r2.shared.publication.encryption.Encryption"), + level = DeprecationLevel.ERROR +) +public typealias Encryption = Encryption + +@Deprecated( + "Refactored into [Presentation]", + ReplaceWith("org.readium.r2.shared.publication.presentation.Presentation"), + level = DeprecationLevel.ERROR +) +public typealias Rendition = Presentation + +@Deprecated( + "Refactored into [EpubLayout]", + ReplaceWith("org.readium.r2.shared.publication.epub.EpubLayout"), + level = DeprecationLevel.ERROR +) +public typealias RenditionLayout = org.readium.r2.shared.publication.epub.EpubLayout + +@Deprecated( + "Refactored into [Presentation.Overflow]", + ReplaceWith("org.readium.r2.shared.publication.presentation.Presentation.Overflow"), + level = DeprecationLevel.ERROR +) +public typealias RenditionFlow = Presentation.Overflow + +@Deprecated( + "Refactored into [Presentation.Orientation]", + ReplaceWith("org.readium.r2.shared.publication.presentation.Presentation.Orientation"), + level = DeprecationLevel.ERROR +) +public typealias RenditionOrientation = Presentation.Orientation + +@Deprecated( + "Refactored into [Presentation.Spread]", + ReplaceWith("org.readium.r2.shared.publication.presentation.Presentation.Spread"), + level = DeprecationLevel.ERROR +) +public typealias RenditionSpread = Presentation.Spread + +@Deprecated( + "Use [Manifest::fromJSON] instead", + ReplaceWith("Manifest.fromJSON(pubDict)", "org.readium.r2.shared.publication.Manifest"), + level = DeprecationLevel.ERROR +) +public fun parsePublication(): org.readium.r2.shared.publication.Publication = + throw NotImplementedError() + +@Suppress("Unused_parameter") +@Deprecated( + "Use [Link::fromJSON] instead", + ReplaceWith("Link.fromJSON(linkDict)", "org.readium.r2.shared.publication.Link"), + level = DeprecationLevel.ERROR +) +public fun parseLink(linkDict: JSONObject, feedUrl: URL? = null): Link { + throw NotImplementedError() +} -@Deprecated("Use [Manifest::fromJSON] instead", ReplaceWith("Manifest.fromJSON(pubDict)", "org.readium.r2.shared.publication.Manifest")) -fun parsePublication(pubDict: JSONObject): org.readium.r2.shared.publication.Publication { - return org.readium.r2.shared.publication.Manifest.fromJSON(pubDict)?.let { Publication(it) } - ?: throw Exception("Invalid publication") +@Deprecated( + "Moved to another package", + ReplaceWith("removeLastComponent()", "org.readium.r2.shared.extensions.removeLastComponent"), + level = DeprecationLevel.ERROR +) +public fun URL.removeLastComponent(): URL = removeLastComponent() + +@Suppress("Unused_parameter") +@Deprecated( + "Use `Href().string` instead", + replaceWith = ReplaceWith("Href(href, base).string"), + level = DeprecationLevel.ERROR +) +public fun normalize(base: String, href: String?): String { + throw NotImplementedError() } -@Deprecated("Use [Link::fromJSON] instead", ReplaceWith("Link.fromJSON(linkDict)", "org.readium.r2.shared.publication.Link")) -fun parseLink(linkDict: JSONObject, feedUrl: URL? = null): Link = - Link.fromJSON(linkDict, normalizeHref = { - if (feedUrl == null) { - it - } else { - Href(it, baseHref = feedUrl.toString()).string - } - }) ?: Link(href = "#") - -@Deprecated("Moved to another package", ReplaceWith("removeLastComponent()", "org.readium.r2.shared.extensions.removeLastComponent")) -fun URL.removeLastComponent(): URL = removeLastComponent() - -@Deprecated("Use `Href().string` instead", replaceWith = ReplaceWith("Href(href, base).string")) -fun normalize(base: String, href: String?): String = - Href(href ?: "", baseHref = base).string +@Deprecated( + "Readium does not provide user error messages anymore. You need to map error cases to your own custom messages.", + level = DeprecationLevel.ERROR +) +public class UserException : Exception() { + + @Deprecated( + "Readium does not provide user error messages anymore. You need to map error cases to your own custom messages.", + level = DeprecationLevel.ERROR + ) + @Suppress("UNUSED_PARAMETER") + public fun getUserMessage(context: Context): String = throw NotImplementedError() +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/FuelPromiseExtension.kt b/readium/shared/src/main/java/org/readium/r2/shared/FuelPromiseExtension.kt deleted file mode 100644 index 555f827b18..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/FuelPromiseExtension.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Aferdita Muriqi, Clément Baumann - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared - -import com.github.kittinunf.fuel.core.Request -import com.github.kittinunf.fuel.core.Response -import com.github.kittinunf.result.Result -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.deferred -import nl.komponents.kovenant.task - -@Deprecated("Dependency to Fuel and kovenant will eventually be removed from the Readium Toolkit") -fun Request.promise(): Promise<Triple<Request, Response, ByteArray>, Exception> { - val deferred = deferred<Triple<Request, Response, ByteArray>, Exception>() - task { response() } success { - val (request, response, result) = it - when (result) { - is Result.Success -> deferred.resolve(Triple(request, response, result.value)) - is Result.Failure -> deferred.reject(result.error) - } - } fail { - deferred.reject(it) - } - return deferred.promise -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/Injectable.kt b/readium/shared/src/main/java/org/readium/r2/shared/Injectable.kt index 7a176488c7..118b83f9df 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/Injectable.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/Injectable.kt @@ -10,14 +10,10 @@ package org.readium.r2.shared import java.io.Serializable -import org.readium.r2.shared.util.MapCompanion -enum class Injectable(val rawValue: String) : Serializable { +@Deprecated("Migrate the HTTP server, see the migration guide", level = DeprecationLevel.ERROR) +public enum class Injectable(public val value: String) : Serializable { Script("scripts"), Font("fonts"), Style("styles"); - - companion object : MapCompanion<String, Injectable>(values(), Injectable::rawValue) - - override fun toString(): String = rawValue } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/JSONable.kt b/readium/shared/src/main/java/org/readium/r2/shared/JSONable.kt index dc2e8cf77f..0564ce5c30 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/JSONable.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/JSONable.kt @@ -12,16 +12,16 @@ package org.readium.r2.shared import org.json.JSONArray import org.json.JSONObject -interface JSONable { +public interface JSONable { /** * Serializes the object to its JSON representation. */ - fun toJSON(): JSONObject + public fun toJSON(): JSONObject } /** * Serializes a list of [JSONable] into a [JSONArray]. */ -fun List<JSONable>.toJSON(): JSONArray = +public fun List<JSONable>.toJSON(): JSONArray = JSONArray(map(JSONable::toJSON)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/MediaOverlayNode.kt b/readium/shared/src/main/java/org/readium/r2/shared/MediaOverlayNode.kt index a0ec7b58dd..49a55022f3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/MediaOverlayNode.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/MediaOverlayNode.kt @@ -10,36 +10,39 @@ package org.readium.r2.shared import java.io.Serializable +import org.readium.r2.shared.util.Url -data class Clip( +@InternalReadiumApi +public data class Clip( val audioResource: String? = null, val fragmentId: String? = null, val start: Double? = null, val end: Double? = null ) -data class MediaOverlayNode( - val text: String, // an URI possibly finishing by a fragment (textFile#id) - val audio: String?, // an URI possibly finishing by a simple timer (audioFile#t=start,end) +@InternalReadiumApi +public data class MediaOverlayNode( + val text: Url, // an URI possibly finishing by a fragment (textFile#id) + val audio: Url?, // an URI possibly finishing by a simple timer (audioFile#t=start,end) val children: List<MediaOverlayNode> = listOf(), val role: List<String> = listOf() ) : Serializable { val audioFile: String? - get() = audio?.split("#")?.first() + get() = audio?.removeFragment()?.path!! val audioTime: String? - get() = if (audio != null && '#' in audio) audio.split("#", limit = 2).last() else null + get() = audio?.fragment val textFile: String - get() = text.split("#").first() + get() = text.removeFragment().path!! val fragmentId: String? - get() = if ('#' in text) text.split('#', limit = 2).last() else null + get() = text.fragment val clip: Clip get() { - val audioString = this.audio ?: throw Exception("audio") - val audioFileString = audioString.split('#').first() - val times = audioString.split('#').last() + val audio = audio ?: throw Exception("audio") + val audioFile = audio.removeFragment().path + val times = audio.fragment ?: "" val (start, end) = parseTimer(times) - return Clip(audioFileString, fragmentId, start, end) + return Clip(audioFile, fragmentId, start, end) } private fun parseTimer(times: String): Pair<Double?, Double?> { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/MediaOverlays.kt b/readium/shared/src/main/java/org/readium/r2/shared/MediaOverlays.kt index 185573e64c..1cd4af6df9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/MediaOverlays.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/MediaOverlays.kt @@ -10,39 +10,43 @@ package org.readium.r2.shared import java.io.Serializable +import org.readium.r2.shared.util.Url -data class MediaOverlays(private val nodes: List<MediaOverlayNode> = listOf()) : Serializable { - fun clip(ref: String): Clip? { +@InternalReadiumApi +public data class MediaOverlays(private val nodes: List<MediaOverlayNode> = listOf()) : Serializable { + public fun clip(ref: Url): Clip? { val fragmentNode = nodeForFragment(ref) return fragmentNode?.clip } - fun nodeForFragment(ref: String?): MediaOverlayNode? = findNode(ref, this.nodes) + private fun nodeForFragment(ref: Url?): MediaOverlayNode? = findNode(ref, this.nodes) - private fun findNode(ref: String?, inNodes: List<MediaOverlayNode>): MediaOverlayNode? { + private fun findNode(ref: Url?, inNodes: List<MediaOverlayNode>): MediaOverlayNode? { for (node in inNodes) { - if (node.role.contains("section")) + if (node.role.contains("section")) { return findNode(ref, node.children) - else if (ref == null || node.text == ref) + } else if (ref == null || node.text == ref) { return node + } } return null } - data class NextNodeResult(val found: MediaOverlayNode?, val prevFound: Boolean) + public data class NextNodeResult(val found: MediaOverlayNode?, val prevFound: Boolean) - private fun nodeAfterFragment(ref: String?): MediaOverlayNode? = findNextNode(ref, this.nodes).found + private fun nodeAfterFragment(ref: Url?): MediaOverlayNode? = findNextNode(ref, this.nodes).found - private fun findNextNode(fragment: String?, inNodes: List<MediaOverlayNode>): NextNodeResult { + private fun findNextNode(fragment: Url?, inNodes: List<MediaOverlayNode>): NextNodeResult { var prevNodeFoundFlag = false // For each node of the current scope... for (node in inNodes) { if (prevNodeFoundFlag) { // If the node is a section, we get the first non section child. - if (node.role.contains("section")) + if (node.role.contains("section")) { getFirstNonSectionChild(node)?.let { return NextNodeResult(it, false) } - else + } else { return NextNodeResult(node, false) + } } else { // If the node is a "section" (<seq> sequence element) if (node.role.contains("section")) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/OptIn.kt b/readium/shared/src/main/java/org/readium/r2/shared/OptIn.kt index 882d18d465..ae95dc85cb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/OptIn.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/OptIn.kt @@ -14,37 +14,50 @@ package org.readium.r2.shared message = "This is an internal API that should not be used outside of Readium modules. No compatibility guarantees are provided." ) @Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY, AnnotationTarget.CONSTRUCTOR) -annotation class InternalReadiumApi +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS, + AnnotationTarget.PROPERTY, + AnnotationTarget.CONSTRUCTOR +) +public annotation class InternalReadiumApi @RequiresOptIn( level = RequiresOptIn.Level.WARNING, message = "This API is still experimental. It might change in the future without notice." ) @Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY) -annotation class ExperimentalReadiumApi +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS, + AnnotationTarget.PROPERTY +) +public annotation class ExperimentalReadiumApi @RequiresOptIn( level = RequiresOptIn.Level.WARNING, message = "This is a delicate API and its use requires care. Make sure you fully read and understand documentation of the declaration that is marked as a delicate API." ) @Retention(value = AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY) -annotation class DelicateReadiumApi - -@RequiresOptIn( - level = RequiresOptIn.Level.WARNING, - message = "Support for PDF is still experimental. The API may be changed in the future without notice." +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS, + AnnotationTarget.PROPERTY ) -@Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY) -annotation class PdfSupport +public annotation class DelicateReadiumApi @RequiresOptIn( level = RequiresOptIn.Level.WARNING, message = "Support for SearchService is still experimental. The API may be changed in the future without notice." ) @Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY) -annotation class Search +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS, + AnnotationTarget.PROPERTY +) +public annotation class Search diff --git a/readium/shared/src/main/java/org/readium/r2/shared/ReadiumCSS.kt b/readium/shared/src/main/java/org/readium/r2/shared/ReadiumCSS.kt index 498d338ac0..40257e49e7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/ReadiumCSS.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/ReadiumCSS.kt @@ -1,78 +1,81 @@ -@file:Suppress("DEPRECATION") - package org.readium.r2.shared -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val FONT_SIZE_REF = "fontSize" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val FONT_FAMILY_REF = "fontFamily" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val FONT_OVERRIDE_REF = "fontOverride" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val APPEARANCE_REF = "appearance" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val SCROLL_REF = "scroll" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val PUBLISHER_DEFAULT_REF = "advancedSettings" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val TEXT_ALIGNMENT_REF = "textAlign" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val COLUMN_COUNT_REF = "colCount" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val WORD_SPACING_REF = "wordSpacing" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val LETTER_SPACING_REF = "letterSpacing" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val PAGE_MARGINS_REF = "pageMargins" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val LINE_HEIGHT_REF = "lineHeight" - -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val FONT_SIZE_NAME = "--USER__$FONT_SIZE_REF" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val FONT_FAMILY_NAME = "--USER__$FONT_FAMILY_REF" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val FONT_OVERRIDE_NAME = "--USER__$FONT_OVERRIDE_REF" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val APPEARANCE_NAME = "--USER__$APPEARANCE_REF" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val SCROLL_NAME = "--USER__$SCROLL_REF" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val PUBLISHER_DEFAULT_NAME = "--USER__$PUBLISHER_DEFAULT_REF" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val TEXT_ALIGNMENT_NAME = "--USER__$TEXT_ALIGNMENT_REF" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val COLUMN_COUNT_NAME = "--USER__$COLUMN_COUNT_REF" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val WORD_SPACING_NAME = "--USER__$WORD_SPACING_REF" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val LETTER_SPACING_NAME = "--USER__$LETTER_SPACING_REF" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val PAGE_MARGINS_NAME = "--USER__$PAGE_MARGINS_REF" -@Deprecated("Migrate to the new Settings API (see migration guide)") -const val LINE_HEIGHT_NAME = "--USER__$LINE_HEIGHT_REF" +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val FONT_SIZE_REF: String = "fontSize" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val FONT_FAMILY_REF: String = "fontFamily" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val FONT_OVERRIDE_REF: String = "fontOverride" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val APPEARANCE_REF: String = "appearance" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val SCROLL_REF: String = "scroll" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val PUBLISHER_DEFAULT_REF: String = "advancedSettings" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val TEXT_ALIGNMENT_REF: String = "textAlign" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val COLUMN_COUNT_REF: String = "colCount" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val WORD_SPACING_REF: String = "wordSpacing" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val LETTER_SPACING_REF: String = "letterSpacing" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val PAGE_MARGINS_REF: String = "pageMargins" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val LINE_HEIGHT_REF: String = "lineHeight" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val FONT_SIZE_NAME: String = "" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val FONT_FAMILY_NAME: String = "" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val FONT_OVERRIDE_NAME: String = "" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val APPEARANCE_NAME: String = "" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val SCROLL_NAME: String = "" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val PUBLISHER_DEFAULT_NAME: String = "" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val TEXT_ALIGNMENT_NAME: String = "" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val COLUMN_COUNT_NAME: String = "" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val WORD_SPACING_NAME: String = "" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val LETTER_SPACING_NAME: String = "" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val PAGE_MARGINS_NAME: String = "" + +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public const val LINE_HEIGHT_NAME: String = "" // List of strings that can identify the name of a CSS custom property // Also used for storing UserSettings in UserDefaults -@Deprecated("Migrate to the new Settings API (see migration guide)") -enum class ReadiumCSSName(val ref: String) { - fontSize("--USER__fontSize"), - fontFamily("--USER__fontFamily"), - fontOverride("--USER__fontOverride"), - appearance("--USER__appearance"), - scroll("--USER__scroll"), - publisherDefault("--USER__advancedSettings"), - textAlignment("--USER__textAlign"), - columnCount("--USER__colCount"), - wordSpacing("--USER__wordSpacing"), - letterSpacing("--USER__letterSpacing"), - pageMargins("--USER__pageMargins"), - lineHeight("--USER__lineHeight"), - paraIndent("--USER__paraIndent"), - hyphens("--USER__bodyHyphens"), - ligatures("--USER__ligatures"); - - companion object { - fun ref(name: String): ReadiumCSSName = valueOf(name) - } -} +@Deprecated( + "Migrate to the new Settings API (see migration guide)", + level = DeprecationLevel.WARNING +) +public enum class ReadiumCSSName(public val ref: String) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/RootFile.kt b/readium/shared/src/main/java/org/readium/r2/shared/RootFile.kt index b3865c0d17..4feb2055d6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/RootFile.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/RootFile.kt @@ -9,23 +9,12 @@ package org.readium.r2.shared -class RootFile() { +@Deprecated("Not used anymore", level = DeprecationLevel.ERROR) +public class RootFile { + public var rootPath: String = "" - constructor( - rootPath: String = "", - rootFilePath: String = "", - mimetype: String = "", - version: Double? = null - ) : this() { - this.rootPath = rootPath - this.rootFilePath = rootFilePath - this.mimetype = mimetype - this.version = version - } - - var rootPath: String = "" // Path to OPF - var rootFilePath: String = "" - var mimetype: String = "" - var version: Double? = null + public var rootFilePath: String = "" + public var mimetype: String = "" + public var version: Double? = null } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/URLHelper.kt b/readium/shared/src/main/java/org/readium/r2/shared/URLHelper.kt deleted file mode 100644 index f3b429512f..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/URLHelper.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Aferdita Muriqi, Clément Baumann - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared diff --git a/readium/shared/src/main/java/org/readium/r2/shared/UserException.kt b/readium/shared/src/main/java/org/readium/r2/shared/UserException.kt deleted file mode 100644 index 4629b285b6..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/UserException.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2020 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared - -import android.content.Context -import androidx.annotation.PluralsRes -import androidx.annotation.StringRes -import java.text.DateFormat -import java.util.* -import org.joda.time.DateTime -import org.readium.r2.shared.extensions.asInstance - -/** - * An exception that can be presented to the user using a localized message. - */ -open class UserException protected constructor( - protected val content: Content, - cause: Throwable? -) : Exception(cause) { - - constructor(@StringRes userMessageId: Int, vararg args: Any?, cause: Throwable? = null) : - this(Content(userMessageId, *args), cause) - - constructor( - @PluralsRes userMessageId: Int, - quantity: Int?, - vararg args: Any?, - cause: Throwable? = null - ) : - this(Content(userMessageId, quantity, *args), cause) - - constructor(message: String, cause: Throwable? = null) : - this(Content(message), cause) - - constructor(cause: UserException) : - this(Content(cause), cause) - - /** - * Gets the localized user-facing message for this exception. - * - * @param includesCauses Includes nested [UserException] causes in the user message when true. - */ - open fun getUserMessage(context: Context, includesCauses: Boolean = true): String = - content.getUserMessage(context, cause, includesCauses) - - /** - * Provides a way to generate a localized user message. - */ - protected sealed class Content { - - abstract fun getUserMessage( - context: Context, - cause: Throwable? = null, - includesCauses: Boolean = true - ): String - - /** - * Holds a nested [UserException]. - */ - class Exception(val exception: UserException) : Content() { - override fun getUserMessage( - context: Context, - cause: Throwable?, - includesCauses: Boolean - ): String = - exception.getUserMessage(context, includesCauses) - } - - /** - * Holds the parts of a localized string message. - * - * @param userMessageId String resource id of the localized user message. - * @param args Optional arguments to expand in the message. - * @param quantity Quantity to use if the user message is a quantity strings. - */ - class LocalizedString( - private val userMessageId: Int, - private val args: Array<out Any?>, - private val quantity: Int? - ) : Content() { - override fun getUserMessage( - context: Context, - cause: Throwable?, - includesCauses: Boolean - ): String { - // Convert complex objects to strings, such as Date, to be interpolated. - val args = args.map { arg -> - when (arg) { - is Date -> DateFormat.getDateInstance().format(arg) - is DateTime -> DateFormat.getDateInstance().format(arg.toDate()) - else -> arg - } - } - - var message = - if (quantity != null) context.resources.getQuantityString(userMessageId, quantity, *(args.toTypedArray())) - else context.getString(userMessageId, *(args.toTypedArray())) - - // Includes nested causes if they are also [UserException]. - val userException = cause?.asInstance<UserException>() - if (userException != null && includesCauses) { - message += ": ${userException.getUserMessage(context, includesCauses)}" - } - - return message - } - } - - /** - * Holds an already localized string message. For example, received from an HTTP - * Problem Details object. - */ - class Message(private val message: String) : Content() { - override fun getUserMessage( - context: Context, - cause: Throwable?, - includesCauses: Boolean - ): String = message - } - - companion object { - operator fun invoke(@StringRes userMessageId: Int, vararg args: Any?) = - LocalizedString(userMessageId, args, null) - operator fun invoke(@PluralsRes userMessageId: Int, quantity: Int?, vararg args: Any?) = - LocalizedString(userMessageId, args, quantity) - operator fun invoke(cause: UserException) = - Exception(cause) - operator fun invoke(message: String) = - Message(message) - } - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/UserProperties.kt b/readium/shared/src/main/java/org/readium/r2/shared/UserProperties.kt index 3399243241..583a38fc7d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/UserProperties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/UserProperties.kt @@ -7,91 +7,42 @@ * LICENSE file present in the project repository where this source code is maintained. */ -@file:Suppress("DEPRECATION") +@file:Suppress("UNUSED_PARAMETER") package org.readium.r2.shared import java.io.Serializable -@Deprecated("Migrate to the new Settings API (see migration guide)") -sealed class UserProperty(var ref: String, var name: String) { +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public sealed class UserProperty(public var ref: String, public var name: String) - private val value: String - get() = this.toString() - - abstract override fun toString(): String - fun getJson(): String { - return """{name:"$name",value:"$this"}""" - } -} - -// TODO add here your new Subclasses of UserPreference. It has to be an abstract class inheriting from UserSetting. - -@Deprecated("Migrate to the new Settings API (see migration guide)") -class Enumerable(var index: Int, private val values: List<String>, ref: String, name: String) : UserProperty(ref, name) { - override fun toString() = values[index] -} +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public class Enumerable( + public var index: Int, + private val values: List<String>, + ref: String, + name: String +) -@Deprecated("Migrate to the new Settings API (see migration guide)") -class Incremental( - var value: Float, - val min: Float, - val max: Float, +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public class Incremental( + public var value: Float, + public val min: Float, + public val max: Float, private val step: Float, private val suffix: String, ref: String, name: String -) : UserProperty(ref, name) { - - fun increment() { - value += (if (value + step <= max) step else 0.0f) - } - - fun decrement() { - value -= (if (value - step >= min) step else 0.0f) - } - - override fun toString() = value.toString() + suffix -} - -@Deprecated("Migrate to the new Settings API (see migration guide)") -class Switchable(onValue: String, offValue: String, var on: Boolean, ref: String, name: String) : UserProperty(ref, name) { +) - private val values = mapOf(true to onValue, false to offValue) - - fun switch() { - on = !on - } - - override fun toString() = values[on] ?: error("") -} - -@Deprecated("Migrate to the new Settings API (see migration guide)") -class UserProperties : Serializable { - - val properties: MutableList<UserProperty> = mutableListOf() - - fun addIncremental( - nValue: Float, - min: Float, - max: Float, - step: Float, - suffix: String, - ref: String, - name: String - ) { - properties.add(Incremental(nValue, min, max, step, suffix, ref, name)) - } - - fun addSwitchable(onValue: String, offValue: String, on: Boolean, ref: String, name: String) { - properties.add(Switchable(onValue, offValue, on, ref, name)) - } - - fun addEnumerable(index: Int, values: List<String>, ref: String, name: String) { - properties.add(Enumerable(index, values, ref, name)) - } +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public class Switchable( + onValue: String, + offValue: String, + public var on: Boolean, + ref: String, + name: String +) - inline fun <reified T : UserProperty> getByRef(ref: String) = properties.firstOrNull { - it.ref == ref - }!! as T -} +@Deprecated("Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR) +public class UserProperties : Serializable diff --git a/readium/shared/src/main/java/org/readium/r2/shared/drm/DRM.kt b/readium/shared/src/main/java/org/readium/r2/shared/drm/DRM.kt index 03599f9c9f..4f8cd7178f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/drm/DRM.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/drm/DRM.kt @@ -10,34 +10,21 @@ package org.readium.r2.shared.drm import java.io.Serializable -import org.readium.r2.shared.util.MapCompanion -data class DRM(val brand: Brand) : Serializable { - val scheme: Scheme - var license: DRMLicense? = null +@Deprecated("Not used anymore", level = DeprecationLevel.ERROR) +public class DRM { - enum class Brand(val rawValue: String) : Serializable { - lcp("lcp"); + @Deprecated("Not used anymore", level = DeprecationLevel.ERROR) + public enum class Brand(public val rawValue: String) : Serializable - companion object : MapCompanion<String, Brand>(values(), Brand::rawValue) - } - - enum class Scheme(val rawValue: String) : Serializable { - lcp("http://readium.org/2014/01/lcp"); - - companion object : MapCompanion<String, Scheme>(values(), Scheme::rawValue) - } - - init { - when (brand) { - Brand.lcp -> scheme = Scheme.lcp - } - } + @Deprecated("Not used anymore", level = DeprecationLevel.ERROR) + public enum class Scheme(public val rawValue: String) : Serializable } -interface DRMLicense : Serializable { - val encryptionProfile: String? - fun decipher(data: ByteArray): ByteArray? - val canCopy: Boolean - fun copy(text: String): String? +@Deprecated("Not used anymore", level = DeprecationLevel.ERROR) +public interface DRMLicense : Serializable { + public val encryptionProfile: String? + public fun decipher(data: ByteArray): ByteArray? + public val canCopy: Boolean + public fun copy(text: String): String? } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Bitmap.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Bitmap.kt index 65b2e35043..8a6b9e3915 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Bitmap.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Bitmap.kt @@ -15,13 +15,15 @@ import java.io.ByteArrayOutputStream import kotlin.math.min import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.readium.r2.shared.InternalReadiumApi /** * Resizes a bitmap to fit [maxSize] with bilinear filtering. */ internal fun Bitmap.scaleToFit(maxSize: Size): Bitmap { - if (width <= maxSize.width && height <= maxSize.height) + if (width <= maxSize.width && height <= maxSize.height) { return this + } val ratio = min( maxSize.width / width.toFloat(), @@ -36,7 +38,8 @@ internal fun Bitmap.scaleToFit(maxSize: Size): Bitmap { internal val Bitmap.size get() = Size(width, height) -suspend fun Bitmap.toPng(quality: Int = 100): ByteArray? = withContext(Dispatchers.Default) { +@InternalReadiumApi +public suspend fun Bitmap.toPng(quality: Int = 100): ByteArray? = withContext(Dispatchers.Default) { val stream = ByteArrayOutputStream() compress(Bitmap.CompressFormat.PNG, quality, stream).let { if (it) stream.toByteArray() else null diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/ByteArray.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/ByteArray.kt index c6e890703c..9606e762e2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/ByteArray.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/ByteArray.kt @@ -12,6 +12,7 @@ package org.readium.r2.shared.extensions import java.io.ByteArrayOutputStream import java.security.MessageDigest import java.util.zip.Inflater +import org.readium.r2.shared.InternalReadiumApi import timber.log.Timber /** @@ -19,7 +20,8 @@ import timber.log.Timber * * @param nowrap If true then support GZIP compatible compression, see the documentation of [Inflater] */ -fun ByteArray.inflate(nowrap: Boolean = false, bufferSize: Int = 32 * 1024 /* 32 KB */): ByteArray = +@InternalReadiumApi +public fun ByteArray.inflate(nowrap: Boolean = false, bufferSize: Int = 32 * 1024 /* 32 KB */): ByteArray = ByteArrayOutputStream().use { output -> val inflater = Inflater(nowrap) inflater.setInput(this) @@ -34,7 +36,8 @@ fun ByteArray.inflate(nowrap: Boolean = false, bufferSize: Int = 32 * 1024 /* 32 } /** Computes the MD5 hash of the byte array. */ -fun ByteArray.md5(): String? = +@InternalReadiumApi +public fun ByteArray.md5(): String? = try { MessageDigest .getInstance("MD5") @@ -44,3 +47,14 @@ fun ByteArray.md5(): String? = Timber.e(e) null } + +internal fun ByteArray.read(range: LongRange?): ByteArray { + range ?: return this + + @Suppress("NAME_SHADOWING") + val range = range + .coerceIn(0L until size) + .requireLengthFitInt() + + return sliceArray(range.map(Long::toInt)) +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Date.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Date.kt index 595336f9e7..67d2e040b9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Date.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Date.kt @@ -12,6 +12,8 @@ package org.readium.r2.shared.extensions import java.util.* import org.joda.time.DateTime import org.joda.time.DateTimeZone +import org.readium.r2.shared.InternalReadiumApi -fun Date.toIso8601String(timeZone: TimeZone = TimeZone.getTimeZone("UTC")): String = +@InternalReadiumApi +public fun Date.toIso8601String(timeZone: TimeZone = TimeZone.getTimeZone("UTC")): String = DateTime(this, DateTimeZone.forTimeZone(timeZone)).toString() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Exception.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Exception.kt index 2a52d7dcee..b37ea12bfd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Exception.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Exception.kt @@ -9,25 +9,29 @@ package org.readium.r2.shared.extensions +import org.readium.r2.shared.InternalReadiumApi import timber.log.Timber /** * Returns the result of the given [closure], or null if an [Exception] was raised. */ -inline fun <T> tryOrNull(closure: () -> T): T? = +@InternalReadiumApi +public inline fun <T> tryOrNull(closure: () -> T): T? = tryOr(null, closure) /** * Returns the result of the given [closure], or [default] if an [Exception] was raised. */ -inline fun <T> tryOr(default: T, closure: () -> T): T = +@InternalReadiumApi +public inline fun <T> tryOr(default: T, closure: () -> T): T = try { closure() } catch (e: Exception) { default } /** * Returns the result of the given [closure], or null if an [Exception] was raised. * The [Exception] will be logged. */ -inline fun <T> tryOrLog(closure: () -> T): T? = +@InternalReadiumApi +public inline fun <T> tryOrLog(closure: () -> T): T? = try { closure() } catch (e: Exception) { Timber.e(e) null @@ -36,15 +40,17 @@ inline fun <T> tryOrLog(closure: () -> T): T? = /** * Finds the first cause instance of the given type. */ -inline fun <reified T> Throwable.asInstance(): T? = - asInstance(T::class.java) +@InternalReadiumApi +public inline fun <reified T : Throwable> Throwable.findInstance(): T? = + findInstance(T::class.java) /** * Finds the first cause instance of the given type. */ -fun <R> Throwable.asInstance(klass: Class<R>): R? = +@InternalReadiumApi +public fun <R : Throwable> Throwable.findInstance(klass: Class<R>): R? = @Suppress("UNCHECKED_CAST") when { klass.isInstance(this) -> this as R - else -> cause?.asInstance(klass) + else -> cause?.findInstance(klass) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/File.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/File.kt index 674043168f..112f71e0d3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/File.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/File.kt @@ -12,6 +12,7 @@ package org.readium.r2.shared.extensions import java.io.File import java.io.FileInputStream import java.security.MessageDigest +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.util.mediatype.MediaType import timber.log.Timber @@ -20,7 +21,8 @@ import timber.log.Timber * * Returns null if [File] is a directory or a file that failed to be read. */ -fun File.md5(): String? = +@InternalReadiumApi +public fun File.md5(): String? = try { val md = MessageDigest.getInstance("MD5") // https://stackoverflow.com/questions/10143731/android-optimal-buffer-size @@ -47,7 +49,8 @@ fun File.md5(): String? = /** * Returns whether the `other` is a descendant of this file. */ -fun File.isParentOf(other: File): Boolean { +@InternalReadiumApi +public fun File.isParentOf(other: File): Boolean { val canonicalThis = canonicalFile var parent = other.canonicalFile.parentFile while (parent != null) { @@ -64,5 +67,10 @@ fun File.isParentOf(other: File): Boolean { * * If unknown, fallback on `MediaType.BINARY`. */ -suspend fun File.mediaType(mediaTypeHint: String? = null): MediaType = - MediaType.ofFile(this, mediaType = mediaTypeHint) ?: MediaType.BINARY +@Suppress("UnusedReceiverParameter", "RedundantSuspendModifier", "UNUSED_PARAMETER") +@Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR +) +public suspend fun File.mediaType(mediaTypeHint: String? = null): MediaType = + throw NotImplementedError() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Float.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Float.kt index 45e7ebbfcd..81a4893225 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Float.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Float.kt @@ -10,9 +10,9 @@ import kotlin.math.abs import org.readium.r2.shared.InternalReadiumApi @InternalReadiumApi -fun Float.equalsDelta(other: Float, delta: Float = 0.001f) = +public fun Float.equalsDelta(other: Float, delta: Float = 0.001f): Boolean = this == other || abs(this - other) < delta @InternalReadiumApi -fun Double.equalsDelta(other: Double, delta: Double = 0.001) = +public fun Double.equalsDelta(other: Double, delta: Double = 0.001): Boolean = this == other || abs(this - other) < delta diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Flow.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Flow.kt index 7d97ff3fc9..46685c96f8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Flow.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Flow.kt @@ -7,10 +7,7 @@ package org.readium.r2.shared.extensions import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.* import org.readium.r2.shared.InternalReadiumApi /** @@ -18,7 +15,7 @@ import org.readium.r2.shared.InternalReadiumApi * [coroutineScope]. */ @InternalReadiumApi -fun <T, M> StateFlow<T>.mapStateIn( +public fun <T, M> StateFlow<T>.mapStateIn( coroutineScope: CoroutineScope, transform: (value: T) -> M ): StateFlow<M> = @@ -28,3 +25,20 @@ fun <T, M> StateFlow<T>.mapStateIn( SharingStarted.Eagerly, transform(value) ) + +/** + * Transforms the values of two [StateFlow]s and stores the result in a new [StateFlow] using the + * given [coroutineScope]. + */ +@InternalReadiumApi +public fun <T1, T2, R> StateFlow<T1>.combineStateIn( + coroutineScope: CoroutineScope, + flow: StateFlow<T2>, + transform: (a: T1, b: T2) -> R +): StateFlow<R> = + this.combine(flow, transform) + .stateIn( + coroutineScope, + SharingStarted.Eagerly, + transform(value, flow.value) + ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/InputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/InputStream.kt index e25f5a091e..2d1e1441be 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/InputStream.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/InputStream.kt @@ -16,6 +16,7 @@ package org.readium.r2.shared.extensions import java.io.ByteArrayOutputStream import java.io.InputStream import java.io.OutputStream +import java.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -61,8 +62,9 @@ internal suspend fun InputStream.readRange(range: LongRange): ByteArray { .coerceFirstNonNegative() .requireLengthFitInt() - if (range.isEmpty()) + if (range.isEmpty()) { return ByteArray(0) + } return withContext(Dispatchers.IO) { val skipped = skip(range.first) @@ -79,3 +81,17 @@ internal suspend fun InputStream.readFully(): ByteArray = withContext(Dispatchers.IO) { readBytes() } + +internal fun InputStream.readSafe(b: ByteArray): Int = + readSafe(b, 0, b.size) + +internal fun InputStream.readSafe(b: ByteArray, off: Int, len: Int): Int { + Objects.checkFromIndexSize(off, len, b.size) + var n = 0 + while (n < len) { + val count = read(b, off + n, len - n) + if (count < 0) break + n += count + } + return n +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Intent.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Intent.kt index 8bebc7d340..c0041d989c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Intent.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Intent.kt @@ -14,25 +14,38 @@ import android.content.Intent import android.os.Bundle import java.util.* import org.readium.r2.shared.BuildConfig -import org.readium.r2.shared.publication.LocalizedString import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Metadata import org.readium.r2.shared.publication.Publication import timber.log.Timber -private val extraKey = "publicationId" -private val deprecationException = IllegalArgumentException("The [publication] intent extra is not supported anymore. Use the shared [PublicationRepository] instead.") +private const val extraKey = "publicationId" +private val deprecationException = IllegalArgumentException( + "The [publication] intent extra is not supported anymore. Use the shared [PublicationRepository] instead." +) -fun Intent.putPublication(publication: Publication) { +@Deprecated( + "Use your own repository to share publications between activities. See the migration guide.", + level = DeprecationLevel.ERROR +) +public fun Intent.putPublication(publication: Publication) { val id = PublicationRepository.add(publication) putExtra(extraKey, id) } -fun Intent.putPublicationFrom(activity: Activity) { +@Deprecated( + "Use your own repository to share publications between activities. See the migration guide.", + level = DeprecationLevel.ERROR +) +public fun Intent.putPublicationFrom(activity: Activity) { putExtra(extraKey, activity.intent.getStringExtra(extraKey)) } -fun Intent.getPublication(activity: Activity?): Publication { +@Deprecated( + "Use your own repository to share publications between activities. See the migration guide.", + level = DeprecationLevel.ERROR +) +public fun Intent.getPublication(activity: Activity?): Publication { if (hasExtra("publication")) { if (BuildConfig.DEBUG) { throw deprecationException @@ -46,14 +59,18 @@ fun Intent.getPublication(activity: Activity?): Publication { if (publication == null) { activity?.finish() // Fallbacks on a dummy Publication to avoid crashing the app until the Activity finishes. - val metadata = Metadata(identifier = "dummy", localizedTitle = LocalizedString("")) + val metadata = Metadata(identifier = "dummy") return Publication(Manifest(metadata = metadata)) } return publication } -fun Intent.getPublicationOrNull(): Publication? { +@Deprecated( + "Use your own repository to share publications between activities. See the migration guide.", + level = DeprecationLevel.ERROR +) +public fun Intent.getPublicationOrNull(): Publication? { if (hasExtra("publication")) { if (BuildConfig.DEBUG) { throw deprecationException @@ -66,11 +83,16 @@ fun Intent.getPublicationOrNull(): Publication? { } @Suppress("UNUSED_PARAMETER") -@Deprecated("The `activity` parameter is not necessary", ReplaceWith("getPublicationOrNull()"), level = DeprecationLevel.WARNING) -fun Intent.getPublicationOrNull(activity: Activity): Publication? = - getPublicationOrNull() +@Deprecated("The `activity` parameter is not necessary", level = DeprecationLevel.ERROR) +public fun Intent.getPublicationOrNull(activity: Activity): Publication? { + throw NotImplementedError() +} -fun Intent.destroyPublication(activity: Activity?) { +@Deprecated( + "Use your own repository to share publications between activities. See the migration guide.", + level = DeprecationLevel.ERROR +) +public fun Intent.destroyPublication(activity: Activity?) { if (activity == null || activity.isFinishing) { getStringExtra(extraKey)?.let { PublicationRepository.remove(it) @@ -78,16 +100,28 @@ fun Intent.destroyPublication(activity: Activity?) { } } -fun Bundle.putPublication(publication: Publication) { +@Deprecated( + "Use your own repository to share publications between activities. See the migration guide.", + level = DeprecationLevel.ERROR +) +public fun Bundle.putPublication(publication: Publication) { val id = PublicationRepository.add(publication) putString(extraKey, id) } -fun Bundle.putPublicationFrom(activity: Activity) { +@Deprecated( + "Use your own repository to share publications between activities. See the migration guide.", + level = DeprecationLevel.ERROR +) +public fun Bundle.putPublicationFrom(activity: Activity) { putString(extraKey, activity.intent.getStringExtra(extraKey)) } -fun Bundle.getPublicationOrNull(): Publication? { +@Deprecated( + "Use your own repository to share publications between activities. See the migration guide.", + level = DeprecationLevel.ERROR +) +public fun Bundle.getPublicationOrNull(): Publication? { return getString(extraKey)?.let { PublicationRepository.get(it) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/JSON.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/JSON.kt index 88c092fe2a..9d811c538e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/JSON.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/JSON.kt @@ -13,13 +13,15 @@ import android.os.Parcel import kotlinx.parcelize.Parceler import org.json.JSONArray import org.json.JSONObject +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.JSONable import timber.log.Timber /** * Unwraps recursively the [JSONObject] to a [Map<String, Any>]. */ -fun JSONObject.toMap(): Map<String, Any> { +@InternalReadiumApi +public fun JSONObject.toMap(): Map<String, Any> { val map = mutableMapOf<String, Any>() for (key in keys()) { if (!isNull(key)) { @@ -32,7 +34,8 @@ fun JSONObject.toMap(): Map<String, Any> { /** * Unwraps recursively the [JSONArray] to a [List<Any>]. */ -fun JSONArray.toList(): List<Any> { +@InternalReadiumApi +public fun JSONArray.toList(): List<Any> { val list = mutableListOf<Any>() for (i in 0 until length()) { list.add(unwrapJSON(get(i))) @@ -69,7 +72,8 @@ private fun wrapJSON(value: Any?): Any? = when (value) { * Maps [name] to [jsonObject], clobbering any existing name/value mapping with the same name. If * the [JSONObject] is empty, any existing mapping for [name] is removed. */ -fun JSONObject.putIfNotEmpty(name: String, jsonObject: JSONObject?) { +@InternalReadiumApi +public fun JSONObject.putIfNotEmpty(name: String, jsonObject: JSONObject?) { if (jsonObject == null || jsonObject.length() == 0) { remove(name) return @@ -82,7 +86,8 @@ fun JSONObject.putIfNotEmpty(name: String, jsonObject: JSONObject?) { * Maps [name] to [jsonArray], clobbering any existing name/value mapping with the same name. If * the [JSONArray] is empty, any existing mapping for [name] is removed. */ -fun JSONObject.putIfNotEmpty(name: String, jsonArray: JSONArray?) { +@InternalReadiumApi +public fun JSONObject.putIfNotEmpty(name: String, jsonArray: JSONArray?) { if (jsonArray == null || jsonArray.length() == 0) { remove(name) return @@ -96,7 +101,8 @@ fun JSONObject.putIfNotEmpty(name: String, jsonArray: JSONArray?) { * name/value mapping with the same name. If the [JSONObject] argument is empty, any existing mapping * for [name] is removed. */ -fun JSONObject.putIfNotEmpty(name: String, jsonable: JSONable?) { +@InternalReadiumApi +public fun JSONObject.putIfNotEmpty(name: String, jsonable: JSONable?) { val json = jsonable?.toJSON() if (json == null || json.length() == 0) { remove(name) @@ -146,7 +152,8 @@ internal fun JSONObject.putIfNotEmpty(name: String, map: Map<String, *>) { * positive integer, or [fallback] otherwise. * If [remove] is true, then the mapping will be removed from the [JSONObject]. */ -fun JSONObject.optPositiveInt(name: String, fallback: Int = -1, remove: Boolean = false): Int? { +@InternalReadiumApi +public fun JSONObject.optPositiveInt(name: String, fallback: Int = -1, remove: Boolean = false): Int? { val int = optInt(name, fallback) val value = if (int >= 0) int else null if (remove) { @@ -160,7 +167,12 @@ fun JSONObject.optPositiveInt(name: String, fallback: Int = -1, remove: Boolean * positive double, or [fallback] otherwise. * If [remove] is true, then the mapping will be removed from the [JSONObject]. */ -fun JSONObject.optPositiveDouble(name: String, fallback: Double = -1.0, remove: Boolean = false): Double? { +@InternalReadiumApi +public fun JSONObject.optPositiveDouble( + name: String, + fallback: Double = -1.0, + remove: Boolean = false +): Double? { val double = optDouble(name, fallback) val value = if (double >= 0) double else null if (remove) { @@ -174,7 +186,8 @@ fun JSONObject.optPositiveDouble(name: String, fallback: Double = -1.0, remove: * mapping exists, it is not a string or it is empty. * If [remove] is true, then the mapping will be removed from the [JSONObject]. */ -fun JSONObject.optNullableString(name: String, remove: Boolean = false): String? { +@InternalReadiumApi +public fun JSONObject.optNullableString(name: String, remove: Boolean = false): String? { val value = if (remove) this.remove(name) else this.opt(name) val string = value as? String return string?.takeUnless(String::isEmpty) @@ -185,7 +198,8 @@ fun JSONObject.optNullableString(name: String, remove: Boolean = false): String? * mapping exists. * If [remove] is true, then the mapping will be removed from the [JSONObject]. */ -fun JSONObject.optNullableBoolean(name: String, remove: Boolean = false): Boolean? { +@InternalReadiumApi +public fun JSONObject.optNullableBoolean(name: String, remove: Boolean = false): Boolean? { if (!has(name)) { return null } @@ -201,7 +215,8 @@ fun JSONObject.optNullableBoolean(name: String, remove: Boolean = false): Boolea * mapping exists. * If [remove] is true, then the mapping will be removed from the [JSONObject]. */ -fun JSONObject.optNullableInt(name: String, remove: Boolean = false): Int? { +@InternalReadiumApi +public fun JSONObject.optNullableInt(name: String, remove: Boolean = false): Int? { if (!has(name)) { return null } @@ -217,7 +232,8 @@ fun JSONObject.optNullableInt(name: String, remove: Boolean = false): Int? { * mapping exists. * If [remove] is true, then the mapping will be removed from the [JSONObject]. */ -fun JSONObject.optNullableLong(name: String, remove: Boolean = false): Long? { +@InternalReadiumApi +public fun JSONObject.optNullableLong(name: String, remove: Boolean = false): Long? { if (!has(name)) { return null } @@ -233,7 +249,8 @@ fun JSONObject.optNullableLong(name: String, remove: Boolean = false): Long? { * mapping exists. * If [remove] is true, then the mapping will be removed from the [JSONObject]. */ -fun JSONObject.optNullableDouble(name: String, remove: Boolean = false): Double? { +@InternalReadiumApi +public fun JSONObject.optNullableDouble(name: String, remove: Boolean = false): Double? { if (!has(name)) { return null } @@ -251,7 +268,8 @@ fun JSONObject.optNullableDouble(name: String, remove: Boolean = false): Double? * * E.g. ["a", "b"] or "a" */ -fun JSONObject.optStringsFromArrayOrSingle(name: String, remove: Boolean = false): List<String> { +@InternalReadiumApi +public fun JSONObject.optStringsFromArrayOrSingle(name: String, remove: Boolean = false): List<String> { val value = if (remove) this.remove(name) else opt(name) return when (value) { @@ -266,7 +284,8 @@ fun JSONObject.optStringsFromArrayOrSingle(name: String, remove: Boolean = false * in the original [JSONObject]. * If the tranform returns `null`, it is not included in the output list. */ -fun <T> JSONObject.mapNotNull(transform: (Pair<String, Any>) -> T?): List<T> { +@InternalReadiumApi +public fun <T> JSONObject.mapNotNull(transform: (Pair<String, Any>) -> T?): List<T> { val result = mutableListOf<T>() for (key in keys()) { val transformedValue = transform(Pair(key, get(key))) @@ -282,7 +301,8 @@ fun <T> JSONObject.mapNotNull(transform: (Pair<String, Any>) -> T?): List<T> { * in the original [JSONArray]. * If the tranform returns `null`, it is not included in the output list. */ -fun <T> JSONArray.mapNotNull(transform: (Any) -> T?): List<T> { +@InternalReadiumApi +public inline fun <T> JSONArray.mapNotNull(transform: (Any) -> T?): List<T> { val result = mutableListOf<T>() for (i in 0 until length()) { val transformedValue = transform(get(i)) @@ -311,7 +331,7 @@ internal fun <T> JSONArray.filterIsInstance(klass: Class<T>): List<T> { /** * Parses a [JSONArray] of [JSONObject] into a [List] of models using the given [factory]. */ -internal fun <T> JSONArray?.parseObjects(factory: (Any) -> T?): List<T> { +internal inline fun <T> JSONArray?.parseObjects(factory: (Any) -> T?): List<T> { this ?: return emptyList() val models = mutableListOf<T>() @@ -327,7 +347,8 @@ internal fun <T> JSONArray?.parseObjects(factory: (Any) -> T?): List<T> { /** * Implementation of a [Parceler] to be used with [@Parcelize] to serialize JSON objects. */ -object JSONParceler : Parceler<Map<String, Any>> { +@InternalReadiumApi +public object JSONParceler : Parceler<Map<String, Any>> { override fun create(parcel: Parcel): Map<String, Any> = try { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/LongRange.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/LongRange.kt index 6317ebcf20..5e43eecd5b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/LongRange.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/LongRange.kt @@ -9,11 +9,23 @@ package org.readium.r2.shared.extensions -fun LongRange.coerceFirstNonNegative() = LongRange(first.coerceAtLeast(0), last) +import org.readium.r2.shared.InternalReadiumApi -fun LongRange.coerceIn(range: LongRange) = LongRange(first.coerceAtLeast(range.first), last.coerceAtMost(range.last)) +@InternalReadiumApi +public fun LongRange.coerceFirstNonNegative(): LongRange = LongRange(first.coerceAtLeast(0), last) -fun LongRange.requireLengthFitInt() = this.apply { require(last - first + 1 <= Int.MAX_VALUE) } +@InternalReadiumApi +public fun LongRange.coerceIn(range: LongRange): LongRange = LongRange( + first.coerceAtLeast(range.first), + last.coerceAtMost(range.last) +) + +@InternalReadiumApi +public fun LongRange.requireLengthFitInt(): LongRange = this.apply { + require( + last - first + 1 <= Int.MAX_VALUE + ) +} internal fun LongRange.contains(range: LongRange) = contains(range.first) && contains(range.last) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Map.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Map.kt new file mode 100644 index 0000000000..9c028edc24 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Map.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.extensions + +internal fun Map<String, List<String>>.lowerCaseKeys(): Map<String, List<String>> { + val normalizedMap = mutableMapOf<String, MutableList<String>>() + for ((k, v) in this) { + normalizedMap.getOrPut( + k.lowercase(), + mutableListOf() + ).addAll(v) + } + return normalizedMap +} + +internal fun <K, V> MutableMap<K, V>.getOrPut(key: K, fallbackValue: V): V = + get(key) ?: run { + put(key, fallbackValue) + fallbackValue + } + +internal fun Map<String, List<String>>.joinValues(separator: CharSequence): Map<String, String> = + mapValues { it.value.joinToString(separator) } + +internal fun <K, V> Map<K, List<V>>.toMutable() = + mapValues { it.value.toMutableList() }.toMutableMap() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/String.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/String.kt index b8a0bde976..baa7f5ab14 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/String.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/String.kt @@ -9,16 +9,18 @@ package org.readium.r2.shared.extensions +import android.net.Uri import java.net.URL -import java.net.URLDecoder import java.security.MessageDigest import java.util.* import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.json.JSONException import org.json.JSONObject +import org.readium.r2.shared.InternalReadiumApi -fun String.iso8601ToDate(): Date? = +@InternalReadiumApi +public fun String.iso8601ToDate(): Date? = try { // We assume that a date without a time zone component is in UTC. To handle this properly, // we need to set the default time zone of Joda to UTC, since by default it uses the local @@ -39,11 +41,24 @@ fun String.iso8601ToDate(): Date? = * If this string starts with the given [prefix], returns this string. * Otherwise, returns a copy of this string after adding the [prefix]. */ -fun String.addPrefix(prefix: CharSequence): String { +@InternalReadiumApi +public fun String.addPrefix(prefix: CharSequence): String { if (startsWith(prefix)) { return this } - return "$prefix$this" + return prefix.toString() + this +} + +/** + * If this string ends with the given [suffix], returns this string. + * Otherwise, returns a copy of this string after adding the [suffix]. + */ +@InternalReadiumApi +public fun String.addSuffix(suffix: CharSequence): String { + if (endsWith(suffix)) { + return this + } + return this + suffix } internal enum class HashAlgorithm(val key: String) { @@ -71,9 +86,18 @@ internal fun String.toJsonOrNull(): JSONObject? = null } -internal fun String.queryParameters(): Map<String, String> = URLDecoder.decode(this, "UTF-8") - .substringAfter("?") // query start - .takeWhile { it != '#' } // anchor start - .split("&") - .mapNotNull { it.split("=").takeIf { it.size == 2 } } - .associate { Pair(it[0], it[1]) } +/** + * Percent-encodes an URL path section. + * + * Equivalent to Swift's `string.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)` + */ +internal fun String.percentEncodedPath(): String = + Uri.encode(this, "$&+,/:=@") + +/** + * Percent-encodes an URL query key or value. + * + * Equivalent to Swift's `string.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)` + */ +internal fun String.percentEncodedQuery(): String = + Uri.encode(this, "$+,/?:=@") diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/URL.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/URL.kt index 7daf557937..dade60fa16 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/URL.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/URL.kt @@ -11,8 +11,10 @@ package org.readium.r2.shared.extensions import java.io.File import java.net.URL +import org.readium.r2.shared.InternalReadiumApi -fun URL.removeLastComponent(): URL { +@InternalReadiumApi +public fun URL.removeLastComponent(): URL { val lastPathComponent = path.split("/") .lastOrNull { it.isNotEmpty() } ?: return this @@ -26,5 +28,6 @@ fun URL.removeLastComponent(): URL { } /** Returns the file extension of the URL. */ -val URL.extension: String? get() = +@InternalReadiumApi +public val URL.extension: String? get() = File(path).extension.ifBlank { null } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/ArchiveFetcher.kt b/readium/shared/src/main/java/org/readium/r2/shared/fetcher/ArchiveFetcher.kt deleted file mode 100644 index 86aa9e1d11..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/ArchiveFetcher.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared.fetcher - -import java.io.File -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.readium.r2.shared.extensions.addPrefix -import org.readium.r2.shared.extensions.tryOr -import org.readium.r2.shared.extensions.tryOrNull -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.Properties -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.archive.Archive -import org.readium.r2.shared.util.archive.ArchiveFactory -import org.readium.r2.shared.util.archive.DefaultArchiveFactory -import org.readium.r2.shared.util.mediatype.MediaType -import timber.log.Timber - -/** Provides access to entries of an archive. */ -class ArchiveFetcher private constructor(private val archive: Archive) : Fetcher { - - override suspend fun links(): List<Link> = - tryOr(emptyList()) { archive.entries() } - .map { it.toLink() } - - override fun get(link: Link): Resource = - EntryResource(link, archive) - - override suspend fun close() = withContext(Dispatchers.IO) { - try { - archive.close() - } catch (e: Exception) { - Timber.e(e) - } - } - - companion object { - - suspend fun fromPath(path: String, archiveFactory: ArchiveFactory = DefaultArchiveFactory()): ArchiveFetcher? = - withContext(Dispatchers.IO) { - tryOrNull { ArchiveFetcher(archiveFactory.open(File(path), password = null)) } - } - } - - private class EntryResource(val originalLink: Link, val archive: Archive) : Resource { - - private lateinit var _entry: ResourceTry<Archive.Entry> - - suspend fun entry(): ResourceTry<Archive.Entry> { - if (!::_entry.isInitialized) { - _entry = try { - Try.success(findEntry(originalLink)) - } catch (e: Exception) { - Try.failure(Resource.Exception.NotFound(e)) - } - } - - return _entry - } - - suspend fun findEntry(link: Link): Archive.Entry { - val href = link.href.removePrefix("/") - return try { - archive.entry(href) - } catch (e: Exception) { - // Try again after removing query parameters and anchors from the href. - archive.entry(href.takeWhile { it !in "#?" }) - } - } - - override suspend fun link(): Link { - val entry = entry().getOrNull() ?: return originalLink - return originalLink.addProperties(entry.toLinkProperties()) - } - - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> = - entry().mapCatching { - it.read(range) - } - - override suspend fun length(): ResourceTry<Long> = - metadataLength()?.let { Try.success(it) } - ?: read().map { it.size.toLong() } - - override suspend fun close() { - if (::_entry.isInitialized) { - _entry.onSuccess { it.close() } - } - } - - private suspend fun metadataLength(): Long? = - entry().getOrNull()?.length - - override fun toString(): String = - "${javaClass.simpleName}(${archive::class.java.simpleName}, ${originalLink.href})" - } -} - -private suspend fun Archive.Entry.toLink(): Link { - return Link( - href = path.addPrefix("/"), - type = MediaType.of(fileExtension = File(path).extension)?.toString(), - properties = Properties(toLinkProperties()) - ) -} - -private fun Archive.Entry.toLinkProperties(): Map<String, Any> { - val properties = mutableMapOf<String, Any>( - "archive" to mapOf( - "entryLength" to (compressedLength ?: length ?: 0), - "isEntryCompressed" to (compressedLength != null) - ) - ) - - compressedLength?.let { - // FIXME: Legacy property, should be removed in 3.0.0 - properties["compressedLength"] = it - } - - return properties -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/BytesResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/fetcher/BytesResource.kt deleted file mode 100644 index c0998838af..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/BytesResource.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared.fetcher - -import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.extensions.coerceIn -import org.readium.r2.shared.extensions.requireLengthFitInt -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.util.Try - -sealed class BaseBytesResource(val link: Link, val bytes: suspend () -> ByteArray) : Resource { - - private lateinit var _bytes: ByteArray - - override suspend fun link(): Link = link - - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> { - try { - if (!::_bytes.isInitialized) - _bytes = bytes() - - if (range == null) - return Try.success(_bytes) - - @Suppress("NAME_SHADOWING") - val range = range - .coerceIn(0L until _bytes.size) - .requireLengthFitInt() - - return Try.success(_bytes.sliceArray(range.map(Long::toInt))) - } catch (e: Exception) { - return Try.failure(Resource.Exception.wrap(e)) - } - } - - override suspend fun length(): ResourceTry<Long> = - read().map { it.size.toLong() } - - override suspend fun close() {} -} - -/** Creates a Resource serving [ByteArray]. */ -class BytesResource(link: Link, bytes: suspend () -> ByteArray) : BaseBytesResource(link, bytes) { - - constructor(link: Link, bytes: ByteArray) : this(link, { bytes }) - - override fun toString(): String = - "${javaClass.simpleName}(${runBlocking { bytes().size }} bytes)" -} - -/** Creates a Resource serving a [String]. */ -class StringResource(link: Link, string: suspend () -> String) : BaseBytesResource(link, { string().toByteArray() }) { - - constructor(link: Link, string: String) : this(link, { string }) - - override fun toString(): String = - "${javaClass.simpleName}(${runBlocking { bytes().toString() }})" -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/Fetcher.kt b/readium/shared/src/main/java/org/readium/r2/shared/fetcher/Fetcher.kt deleted file mode 100644 index 1c22fad46c..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/Fetcher.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared.fetcher - -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.util.SuspendingCloseable - -/** Provides access to a [Resource] from a [Link]. */ -interface Fetcher : SuspendingCloseable { - - /** - * Known resources available in the medium, such as file paths on the file system - * or entries in a ZIP archive. This list is not exhaustive, and additional - * unknown resources might be reachable. - * - * If the medium has an inherent resource order, it should be followed. - * Otherwise, HREFs are sorted alphabetically. - */ - suspend fun links(): List<Link> - - /** - * Returns the [Resource] at the given [link]'s HREF. - * - * A [Resource] is always returned, since for some cases we can't know if it exists before - * actually fetching it, such as HTTP. Therefore, errors are handled at the Resource level. - */ - fun get(link: Link): Resource - - /** Returns the [Resource] at the given [href]. */ - fun get(href: String): Resource = - get(Link(href = href)) - - // To be able to add extensions on Fetcher.Companion in other components... - companion object -} - -/** A [Fetcher] providing no resources at all. */ -class EmptyFetcher : Fetcher { - - override suspend fun links(): List<Link> = emptyList() - - override fun get(link: Link): Resource = FailureResource(link, Resource.Exception.NotFound()) - - override suspend fun close() {} -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/FileFetcher.kt b/readium/shared/src/main/java/org/readium/r2/shared/fetcher/FileFetcher.kt deleted file mode 100644 index d2c2f50651..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/FileFetcher.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared.fetcher - -import java.io.File -import java.io.FileNotFoundException -import java.io.RandomAccessFile -import java.lang.ref.WeakReference -import java.nio.channels.Channels -import java.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.readium.r2.shared.extensions.* -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.isLazyInitialized -import org.readium.r2.shared.util.mediatype.MediaType -import timber.log.Timber - -/** - * Provides access to resources on the local file system. - * - * [paths] contains the reachable local paths, indexed by the exposed HREF. Sub-paths are reachable - * as well, to be able to access a whole directory. - */ -class FileFetcher(private val paths: Map<String, File>) : Fetcher { - - /** Provides access to the given local [file] at [href]. */ - constructor(href: String, file: File) : this(mapOf(href to file)) - - private val openedResources: MutableList<WeakReference<Resource>> = LinkedList() - - override suspend fun links(): List<Link> = - paths.toSortedMap().flatMap { (href, file) -> - file.walk().toList().mapNotNull { - tryOrNull { - if (it.isDirectory) { - null - } else { - Link( - href = File(href, it.canonicalPath.removePrefix(file.canonicalPath)).canonicalPath, - type = MediaType.ofFile(file, fileExtension = it.extension)?.toString() - ) - } - } - } - } - - override fun get(link: Link): Resource { - val linkHref = link.href.addPrefix("/") - for ((itemHref, itemFile) in paths) { - @Suppress("NAME_SHADOWING") - val itemHref = itemHref.addPrefix("/") - if (linkHref.startsWith(itemHref)) { - val resourceFile = File(itemFile, linkHref.removePrefix(itemHref)) - // Make sure that the requested resource is [path] or one of its descendant. - if (resourceFile.canonicalPath.startsWith(itemFile.canonicalPath)) { - val resource = FileResource(link, resourceFile) - openedResources.add(WeakReference(resource)) - return resource - } - } - } - return FailureResource(link, Resource.Exception.NotFound()) - } - - override suspend fun close() { - openedResources.mapNotNull(WeakReference<Resource>::get).forEach { it.close() } - openedResources.clear() - } - - class FileResource(val link: Link, override val file: File) : Resource { - - private val randomAccessFile by lazy { - ResourceTry.catching { - RandomAccessFile(file, "r") - } - } - - override suspend fun link(): Link = link - - override suspend fun close() = withContext(Dispatchers.IO) { - if (::randomAccessFile.isLazyInitialized) { - randomAccessFile.onSuccess { - try { - it.close() - } catch (e: java.lang.Exception) { - Timber.e(e) - } - } - } - } - - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> = - withContext(Dispatchers.IO) { - ResourceTry.catching { - readSync(range) - } - } - - private fun readSync(range: LongRange?): ByteArray { - if (range == null) { - return file.readBytes() - } - - @Suppress("NAME_SHADOWING") - val range = range - .coerceFirstNonNegative() - .requireLengthFitInt() - - if (range.isEmpty()) { - return ByteArray(0) - } - - return randomAccessFile.getOrThrow().run { - channel.position(range.first) - - // The stream must not be closed here because it would close the underlying - // [FileChannel] too. Instead, [close] is responsible for that. - Channels.newInputStream(channel).run { - val length = range.last - range.first + 1 - read(length) - } - } - } - - override suspend fun length(): ResourceTry<Long> = - metadataLength?.let { Try.success(it) } - ?: read().map { it.size.toLong() } - - private val metadataLength: Long? = - try { - if (file.isFile) - file.length() - else - null - } catch (e: Exception) { - null - } - - private inline fun <T> Try.Companion.catching(closure: () -> T): ResourceTry<T> = - try { - success(closure()) - } catch (e: FileNotFoundException) { - failure(Resource.Exception.NotFound(e)) - } catch (e: SecurityException) { - failure(Resource.Exception.Forbidden(e)) - } catch (e: Exception) { - failure(Resource.Exception.wrap(e)) - } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - failure(Resource.Exception.wrap(e)) - } - - override fun toString(): String = - "${javaClass.simpleName}(${file.path})" - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/HttpFetcher.kt b/readium/shared/src/main/java/org/readium/r2/shared/fetcher/HttpFetcher.kt deleted file mode 100644 index 69afb00589..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/HttpFetcher.kt +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2021 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.fetcher - -import android.webkit.URLUtil -import java.io.InputStream -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.readium.r2.shared.extensions.read -import org.readium.r2.shared.extensions.tryOrLog -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.flatMap -import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpException -import org.readium.r2.shared.util.http.HttpException.Kind -import org.readium.r2.shared.util.http.HttpRequest -import org.readium.r2.shared.util.http.HttpRequest.Method -import org.readium.r2.shared.util.http.HttpResponse -import org.readium.r2.shared.util.io.CountingInputStream -import timber.log.Timber - -/** - * Fetches remote resources through HTTP. - * - * Since this fetcher is used when doing progressive download streaming (e.g. audiobook), the HTTP - * byte range requests are open-ended and reused. This helps to avoid issuing too many requests. - * - * @param client HTTP client used to perform HTTP requests. - * @param baseUrl Base URL from which relative HREF are served. - */ -class HttpFetcher( - private val client: HttpClient, - private val baseUrl: String? = null, -) : Fetcher { - - override suspend fun links(): List<Link> = emptyList() - - override fun get(link: Link): Resource { - val url = link.toUrl(baseUrl) - - return if (url == null || !URLUtil.isNetworkUrl(url)) { - val cause = IllegalArgumentException("Invalid HREF: ${link.href}, produced URL: $url") - Timber.e(cause) - FailureResource(link, error = Resource.Exception.BadRequest(cause = cause)) - } else { - HttpResource(client, link, url) - } - } - - override suspend fun close() {} - - /** Provides access to an external URL. */ - private class HttpResource( - private val client: HttpClient, - private val link: Link, - private val url: String, - ) : Resource { - - override suspend fun link(): Link = - headResponse() - .map { link.copy(type = it.mediaType.toString()) } - .getOrNull() ?: link - - override suspend fun length(): ResourceTry<Long> = - headResponse().flatMap { - val contentLength = it.contentLength - return if (contentLength != null) { - Try.success(contentLength) - } else { - Try.failure(Resource.Exception.Unavailable()) - } - } - - override suspend fun close() {} - - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> = withContext(Dispatchers.IO) { - try { - stream(range?.first).map { stream -> - if (range != null) { - stream.read(range.count().toLong()) - } else { - stream.readBytes() - } - } - } catch (e: HttpException) { - Try.failure(Resource.Exception.wrapHttp(e)) - } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(e)) - } - } - - /** Cached HEAD response to get the expected content length and other metadata. */ - private lateinit var _headResponse: ResourceTry<HttpResponse> - - private suspend fun headResponse(): ResourceTry<HttpResponse> { - if (::_headResponse.isInitialized) - return _headResponse - - _headResponse = client.fetch(HttpRequest(url, method = Method.HEAD)) - .map { it.response } - .mapFailure { Resource.Exception.wrapHttp(it) } - - return _headResponse - } - - /** - * Returns an HTTP stream for the resource, starting at the [from] byte offset. - * - * The stream is cached and reused for next calls, if the next [from] offset is in a forward - * direction. - */ - private suspend fun stream(from: Long? = null): ResourceTry<InputStream> { - val stream = inputStream - if (from != null && stream != null) { - // TODO Figure out a better way to handle this Kotlin warning - tryOrLog<Nothing> { - val bytesToSkip = from - (inputStreamStart + stream.count) - if (bytesToSkip >= 0) { - stream.skip(bytesToSkip) - } - return Try.success(stream) - } - } - tryOrLog { inputStream?.close() } - - val request = HttpRequest(url) { - from?.let { setRange(from..-1) } - } - - return client.stream(request) - .map { CountingInputStream(it.body) } - .mapFailure { Resource.Exception.wrapHttp(it) } - .onSuccess { - inputStream = it - inputStreamStart = from ?: 0 - } - } - - private var inputStream: CountingInputStream? = null - private var inputStreamStart = 0L - - private fun Resource.Exception.Companion.wrapHttp(e: HttpException): Resource.Exception = - when (e.kind) { - Kind.MalformedRequest, Kind.BadRequest -> - Resource.Exception.BadRequest(cause = e) - Kind.Timeout, Kind.Offline -> - Resource.Exception.Unavailable(e) - Kind.Unauthorized, Kind.Forbidden -> - Resource.Exception.Forbidden(e) - Kind.NotFound -> - Resource.Exception.NotFound(e) - Kind.Cancelled -> - Resource.Exception.Cancelled - Kind.MalformedResponse, Kind.ClientError, Kind.ServerError, Kind.Other -> - Resource.Exception.Other(e) - } - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/fetcher/Resource.kt index 0f95ec36f2..9de181d6e9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/fetcher/Resource.kt @@ -1,563 +1,16 @@ -/* - * Module: r2-shared-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - package org.readium.r2.shared.fetcher -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import androidx.annotation.StringRes -import java.io.ByteArrayInputStream -import java.io.File -import java.nio.charset.Charset -import kotlinx.coroutines.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import org.json.JSONObject -import org.readium.r2.shared.R -import org.readium.r2.shared.UserException -import org.readium.r2.shared.extensions.coerceIn -import org.readium.r2.shared.extensions.contains -import org.readium.r2.shared.extensions.requireLengthFitInt -import org.readium.r2.shared.parser.xml.ElementNode -import org.readium.r2.shared.parser.xml.XmlParser -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.util.SuspendingCloseable -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.flatMap - -typealias ResourceTry<SuccessT> = Try<SuccessT, Resource.Exception> - -/** - * Implements the transformation of a Resource. It can be used, for example, to decrypt, - * deobfuscate, inject CSS or JavaScript, correct content – e.g. adding a missing dir="rtl" in an - * HTML document, pre-process – e.g. before indexing a publication's content, etc. - * - * If the transformation doesn't apply, simply return resource unchanged. - */ -typealias ResourceTransformer = (Resource) -> Resource - -/** - * Acts as a proxy to an actual resource by handling read access. - */ -interface Resource : SuspendingCloseable { - - /** - * Direct file to this resource, when available. - * - * This is meant to be used as an optimization for consumers which can't work efficiently - * with streams. However, [file] is not guaranteed to be set, for example if the resource - * underwent transformations or is being read from an archive. Therefore, consumers should - * always fallback on regular stream reading, using [read] or [ResourceInputStream]. - */ - val file: File? get() = null - - /** - * Returns the link from which the resource was retrieved. - * - * It might be modified by the [Resource] to include additional metadata, e.g. the - * `Content-Type` HTTP header in [Link.type]. - */ - suspend fun link(): Link - - /** - * Returns data length from metadata if available, or calculated from reading the bytes otherwise. - * - * This value must be treated as a hint, as it might not reflect the actual bytes length. To get - * the real length, you need to read the whole resource. - */ - suspend fun length(): ResourceTry<Long> - - /** - * Reads the bytes at the given range. - * - * When [range] is null, the whole content is returned. Out-of-range indexes are clamped to the - * available length automatically. - */ - suspend fun read(range: LongRange? = null): ResourceTry<ByteArray> - - /** - * Reads the full content as a [String]. - * - * If [charset] is null, then it is parsed from the `charset` parameter of link().type, - * or falls back on UTF-8. - */ - suspend fun readAsString(charset: Charset? = null): ResourceTry<String> = - read().mapCatching { - String(it, charset = charset ?: link().mediaType.charset ?: Charsets.UTF_8) - } - - /** - * Reads the full content as a JSON object. - */ - suspend fun readAsJson(): ResourceTry<JSONObject> = - readAsString(charset = Charsets.UTF_8).mapCatching { JSONObject(it) } - - /** - * Reads the full content as an XML document. - */ - suspend fun readAsXml(): ResourceTry<ElementNode> = - read().mapCatching { XmlParser().parse(ByteArrayInputStream(it)) } - - /** - * Reads the full content as a [Bitmap]. - */ - suspend fun readAsBitmap(): ResourceTry<Bitmap> = - read().mapCatching { - BitmapFactory.decodeByteArray(it, 0, it.size) - ?: throw kotlin.Exception("Could not decode resource ${link().href} as a bitmap") - } - - companion object { - /** - * Creates a cached resource wrapping this resource. - */ - @Deprecated( - "If you were caching a TransformingResource, build it with cacheBytes set to true." + - "Otherwise, please report your use case.", - level = DeprecationLevel.ERROR - ) - fun Resource.cached(): Resource = this - } - - /** - * Errors occurring while accessing a resource. - */ - sealed class Exception(@StringRes userMessageId: Int, cause: Throwable? = null) : UserException(userMessageId, cause = cause) { - - /** Equivalent to a 400 HTTP error. */ - class BadRequest(val parameters: Map<String, String> = emptyMap(), cause: Throwable? = null) : - Exception(R.string.r2_shared_resource_exception_bad_request, cause) - - /** Equivalent to a 404 HTTP error. */ - class NotFound(cause: Throwable? = null) : - Exception(R.string.r2_shared_resource_exception_not_found, cause) - - /** - * Equivalent to a 403 HTTP error. - * - * This can be returned when trying to read a resource protected with a DRM that is not - * unlocked. - */ - class Forbidden(cause: Throwable? = null) : - Exception(R.string.r2_shared_resource_exception_forbidden, cause) - - /** - * Equivalent to a 503 HTTP error. - * - * Used when the source can't be reached, e.g. no Internet connection, or an issue with the - * file system. Usually this is a temporary error. - */ - class Unavailable(cause: Throwable? = null) : - Exception(R.string.r2_shared_resource_exception_unavailable, cause) - - /** - * The Internet connection appears to be offline. - */ - object Offline : Exception(R.string.r2_shared_resource_exception_offline) - - /** - * Equivalent to a 507 HTTP error. - * - * Used when the requested range is too large to be read in memory. - */ - class OutOfMemory(cause: OutOfMemoryError) : - Exception(R.string.r2_shared_resource_exception_out_of_memory, cause) - - /** - * The request was cancelled by the caller. - * - * For example, when a coroutine is cancelled. - */ - object Cancelled : Exception(R.string.r2_shared_resource_exception_cancelled) - - /** For any other error, such as HTTP 500. */ - class Other(cause: Throwable) : Exception(R.string.r2_shared_resource_exception_other, cause) - - companion object { - - fun wrap(e: Throwable): Exception = - when (e) { - is Resource.Exception -> e - is CancellationException -> Cancelled - is OutOfMemoryError -> OutOfMemory(e) - else -> Other(e) - } - } - } -} - -/** Creates a Resource that will always return the given [error]. */ -class FailureResource(private val link: Link, private val error: Resource.Exception) : Resource { +@Deprecated( + "Moved to a different package.", + ReplaceWith("org.readium.r2.shared.util.resource.Resource"), + DeprecationLevel.ERROR +) +public class Resource { - internal constructor(link: Link, cause: Throwable) : this(link, Resource.Exception.Other(cause)) - - override suspend fun link(): Link = link - - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> = Try.failure(error) - - override suspend fun length(): ResourceTry<Long> = Try.failure(error) - - override suspend fun close() {} - - override fun toString(): String = - "${javaClass.simpleName}($error)" -} - -/** - * Resource that will act as a proxy to a fallback resource if the [originalResource] errors out. - */ -class FallbackResource( - private val originalResource: Resource, - private val fallbackResourceFactory: (Resource.Exception) -> Resource -) : Resource { - private val coroutineScope = - CoroutineScope(Dispatchers.Default) - - private val resource: Deferred<Resource> = coroutineScope.async { - when (val result = originalResource.length()) { - is Try.Success -> originalResource - is Try.Failure -> fallbackResourceFactory(result.exception) - } - } - - override suspend fun link(): Link = - resource.await().link() - - override suspend fun length(): ResourceTry<Long> = - resource.await().length() - - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> = - resource.await().read(range) - - @OptIn(ExperimentalCoroutinesApi::class) - override suspend fun close() { - coroutineScope.cancel() - if (resource.isCompleted) { - resource.getCompleted().close() - } - } -} - -/** - * Falls back to alternative resources when the receiver fails. - */ -fun Resource.fallback(fallbackResourceFactory: (Resource.Exception) -> Resource): Resource = - FallbackResource(this, fallbackResourceFactory) - -/** - * Falls back to the given alternative [resource] when the receiver fails. - */ -fun Resource.fallback(fallbackResource: Resource): Resource = - FallbackResource(this) { fallbackResource } - -/** - * A base class for a [Resource] which acts as a proxy to another one. - * - * Every function is delegating to the proxied resource, and subclasses should override some of them. - */ -abstract class ProxyResource(protected val resource: Resource) : Resource { - - override suspend fun link(): Link = resource.link() - - override suspend fun length(): ResourceTry<Long> = resource.length() - - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> = resource.read(range) - - override suspend fun close() = resource.close() - - override val file: File? get() = resource.file - - override fun toString(): String = - "${javaClass.simpleName}($resource)" + @Deprecated( + "`Resource.Exception` was split into several `Error` classes. You probably need `ReadError`.", + ReplaceWith("org.readium.r2.shared.util.data.ReadError"), + DeprecationLevel.ERROR + ) + public class Exception } - -/** - * Transforms the bytes of [resource] on-the-fly. - * - * If you set [cacheBytes] to false, consider providing your own implementation of [length] to avoid - * unnecessary transformations. - * - * Warning: The transformation runs on the full content of [resource], so it's not appropriate for - * large resources which can't be held in memory. - */ -abstract class TransformingResource( - resource: Resource, - private val cacheBytes: Boolean = true -) : ProxyResource(resource) { - - companion object { - /** - * Creates a [TransformingResource] using the given [transform] function. - */ - operator fun invoke(resource: Resource, transform: suspend (ByteArray) -> ByteArray): TransformingResource = - object : TransformingResource(resource) { - override suspend fun transform(data: ResourceTry<ByteArray>): ResourceTry<ByteArray> = - data.mapCatching { transform(it) } - } - } - - private lateinit var _bytes: ResourceTry<ByteArray> - - abstract suspend fun transform(data: ResourceTry<ByteArray>): ResourceTry<ByteArray> - - private suspend fun bytes(): ResourceTry<ByteArray> { - if (::_bytes.isInitialized) - return _bytes - - val bytes = transform(resource.read()) - if (cacheBytes) - _bytes = bytes - - return bytes - } - - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> = - bytes().map { - if (range == null) - return bytes() - - @Suppress("NAME_SHADOWING") - val range = range - .coerceIn(0L until it.size) - .requireLengthFitInt() - - it.sliceArray(range.map(Long::toInt)) - } - - override suspend fun length(): ResourceTry<Long> = bytes().map { it.size.toLong() } -} - -/** - * Wraps a [Resource] which will be created only when first accessing one of its members. - */ -class LazyResource(private val factory: suspend () -> Resource) : Resource { - - private lateinit var _resource: Resource - - private suspend fun resource(): Resource { - if (!::_resource.isInitialized) - _resource = factory() - - return _resource - } - - override suspend fun link(): Link = resource().link() - - override suspend fun length(): ResourceTry<Long> = resource().length() - - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> = resource().read(range) - - override suspend fun close() { - if (::_resource.isInitialized) - _resource.close() - } - - override fun toString(): String = - if (::_resource.isInitialized) { - "${javaClass.simpleName}($_resource)" - } else { - "${javaClass.simpleName}(...)" - } -} - -/** - * Protects the access to a wrapped resource with a mutex to make it thread-safe. - * - * This doesn't implement [ProxyResource] to avoid forgetting the synchronization for a future API. - */ -class SynchronizedResource( - private val resource: Resource -) : Resource { - - private val mutex = Mutex() - - override suspend fun link(): Link = - mutex.withLock { resource.link() } - - override suspend fun length(): ResourceTry<Long> = - mutex.withLock { resource.length() } - - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> = - mutex.withLock { resource.read(range) } - - override suspend fun close() = - mutex.withLock { resource.close() } - - override val file: File? get() = - resource.file - - override fun toString(): String = - "${javaClass.simpleName}($resource)" -} - -/** - * Wraps this resource in a [SynchronizedResource] to protect the access from multiple threads. - */ -fun Resource.synchronized(): SynchronizedResource = - SynchronizedResource(this) - -/** - * Wraps a [Resource] and buffers its content. - * - * Expensive interaction with the underlying resource is minimized, since most (smaller) requests - * can be satisfied by accessing the buffer alone. The drawback is that some extra space is required - * to hold the buffer and that copying takes place when filling that buffer, but this is usually - * outweighed by the performance benefits. - * - * Note that this implementation is pretty limited and the benefits are only apparent when reading - * forward and consecutively – e.g. when downloading the resource by chunks. The buffer is ignored - * when reading backward or far ahead. - * - * @param resource Underlying resource which will be buffered. - * @param resourceLength The total length of the resource, when known. This can improve performance - * by avoiding requesting the length from the underlying resource. - * @param bufferSize Size of the buffer chunks to read. - */ -class BufferingResource( - resource: Resource, - resourceLength: Long? = null, - private val bufferSize: Long = DEFAULT_BUFFER_SIZE, -) : ProxyResource(resource) { - - companion object { - const val DEFAULT_BUFFER_SIZE: Long = 8192 - } - - /** - * The buffer containing the current bytes read from the wrapped [Resource], with the range it - * covers. - */ - private var buffer: Pair<ByteArray, LongRange>? = null - - private lateinit var _cachedLength: ResourceTry<Long> - private suspend fun cachedLength(): ResourceTry<Long> { - if (!::_cachedLength.isInitialized) - _cachedLength = resource.length() - return _cachedLength - } - - init { - if (resourceLength != null) { - _cachedLength = Try.success(resourceLength) - } - } - - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> { - val length = cachedLength().getOrNull() - // Reading the whole resource bypasses buffering to keep things simple. - if (range == null || length == null) { - return super.read(range) - } - - val requestedRange = range - .coerceIn(0L until length) - .requireLengthFitInt() - if (requestedRange.isEmpty()) { - return Try.success(ByteArray(0)) - } - - // Round up the range to be read to the next `bufferSize`, because we will buffer the - // excess. - val readLast = (requestedRange.last + 1).ceilMultipleOf(bufferSize).coerceAtMost(length) - var readRange = requestedRange.first until readLast - - // Attempt to serve parts or all of the request using the buffer. - buffer?.let { pair -> - var (buffer, bufferedRange) = pair - - // Everything already buffered? - if (bufferedRange.contains(requestedRange)) { - val data = extractRange(requestedRange, buffer, start = bufferedRange.first) - return Try.success(data) - - // Beginning of requested data is buffered? - } else if (bufferedRange.contains(requestedRange.first)) { - readRange = (bufferedRange.last + 1)..readRange.last - - return super.read(readRange).map { readData -> - buffer += readData - // Shift the current buffer to the tail of the read data. - saveBuffer(buffer, readRange) - - val bytes = extractRange(requestedRange, buffer, start = bufferedRange.first) - bytes - } - } - } - - // Fallback on reading the requested range from the original resource. - return super.read(readRange).map { data -> - saveBuffer(data, readRange) - - val res = if (data.count() > requestedRange.count()) - data.copyOfRange(0, requestedRange.count()) - else - data - - res - } - } - - /** - * Keeps the last chunk of the given data as the buffer for next reads. - * - * @param data Data read from the original resource. - * @param range Range of the read data in the resource. - */ - private fun saveBuffer(data: ByteArray, range: LongRange) { - val lastChunk = data.takeLast(bufferSize.toInt()).toByteArray() - val chunkRange = (range.last + 1 - lastChunk.count())..range.last - buffer = Pair(lastChunk, chunkRange) - } - - /** - * Reads a sub-range of the given [data] after shifting the given absolute (to the resource) - * ranges to be relative to [data]. - */ - private fun extractRange(requestedRange: LongRange, data: ByteArray, start: Long): ByteArray { - val first = requestedRange.first - start - val lastExclusive = first + requestedRange.count() - require(first >= 0) - require(lastExclusive <= data.count()) { "$lastExclusive > ${data.count()}" } - return data.copyOfRange(first.toInt(), lastExclusive.toInt()) - } - - private fun Long.ceilMultipleOf(divisor: Long) = - divisor * (this / divisor + if (this % divisor == 0L) 0 else 1) -} - -/** - * Wraps this resource in a [BufferingResource] to improve reading performances. - * - * @param resourceLength The total length of the resource, when known. This can improve performance - * by avoiding requesting the length from the underlying resource. - * @param bufferSize Size of the buffer chunks to read. - */ -fun Resource.buffered( - resourceLength: Long? = null, - size: Long = BufferingResource.DEFAULT_BUFFER_SIZE -) = - BufferingResource(resource = this, resourceLength = resourceLength, bufferSize = size) - -/** - * Maps the result with the given [transform] - * - * If the [transform] throws an [Exception], it is wrapped in a failure with Resource.Exception.Other. - */ -inline fun <R, S> ResourceTry<S>.mapCatching(transform: (value: S) -> R): ResourceTry<R> = - try { - Try.success((transform(getOrThrow()))) - } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(e)) - } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - Try.failure(Resource.Exception.wrap(e)) - } - -inline fun <R, S> ResourceTry<S>.flatMapCatching(transform: (value: S) -> ResourceTry<R>): ResourceTry<R> = - mapCatching(transform).flatMap { it } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/ResourceContentExtractor.kt b/readium/shared/src/main/java/org/readium/r2/shared/fetcher/ResourceContentExtractor.kt deleted file mode 100644 index 3efae6aeaa..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/ResourceContentExtractor.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2021 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.fetcher - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.jsoup.Jsoup -import org.jsoup.parser.Parser -import org.readium.r2.shared.Search -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.mediatype.MediaType - -/** - * Extracts pure content from a marked-up (e.g. HTML) or binary (e.g. PDF) resource. - */ -@Search -interface ResourceContentExtractor { - - /** - * Extracts the text content of the given [resource]. - */ - suspend fun extractText(resource: Resource): ResourceTry<String> = Try.success("") - - interface Factory { - /** - * Creates a [ResourceContentExtractor] instance for the given [resource]. - * - * Return null if the resource format is not supported. - */ - suspend fun createExtractor(resource: Resource): ResourceContentExtractor? - } -} - -@Search -class DefaultResourceContentExtractorFactory : ResourceContentExtractor.Factory { - - override suspend fun createExtractor(resource: Resource): ResourceContentExtractor? = - when (resource.link().mediaType) { - MediaType.HTML, MediaType.XHTML -> HtmlResourceContentExtractor() - else -> null - } -} - -/** - * [ResourceContentExtractor] implementation for HTML resources. - */ -@Search -class HtmlResourceContentExtractor : ResourceContentExtractor { - - override suspend fun extractText(resource: Resource): ResourceTry<String> = withContext(Dispatchers.IO) { - resource.readAsString().mapCatching { html -> - val body = Jsoup.parse(html).body().text() - // Transform HTML entities into their actual characters. - Parser.unescapeEntities(body, false) - } - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/ResourceInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/fetcher/ResourceInputStream.kt deleted file mode 100644 index 67d2d92baa..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/ResourceInputStream.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Mickaël Menu - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared.fetcher - -import java.io.IOException -import java.io.InputStream -import kotlinx.coroutines.runBlocking - -/** - * Input stream reading a [Resource]'s content. - * - * If you experience bad performances, consider wrapping the stream in a BufferedInputStream. This - * is particularly useful when streaming deflated ZIP entries. - */ -class ResourceInputStream( - private val resource: Resource, - val range: LongRange? = null -) : InputStream() { - - private var isClosed = false - - private val end: Long by lazy { - val resourceLength = try { - runBlocking { resource.length().getOrThrow() } - } catch (e: Exception) { - throw IOException("Can't get resource length", e) - } - - if (range == null) - resourceLength - else { - kotlin.math.min(resourceLength, range.last + 1) - } - } - - /** Current position in the resource. */ - private var position: Long = range?.start ?: 0 - - /** - * The currently marked position in the stream. Defaults to 0. - */ - private var mark: Long = range?.start ?: 0 - - @Throws(IOException::class) - override fun available(): Int { - checkNotClosed() - return (end - position).toInt() - } - - @Throws(IOException::class) - override fun skip(n: Long): Long = synchronized(this) { - checkNotClosed() - - val newPosition = (position + n).coerceAtMost(end) - val skipped = newPosition - position - position = newPosition - skipped - } - - @Throws(IOException::class) - override fun read(): Int = synchronized(this) { - checkNotClosed() - - if (available() <= 0) { - return -1 - } - - try { - val bytes = runBlocking { resource.read(position until (position + 1)).getOrThrow() } - position += 1 - return bytes.first().toInt() - } catch (e: Exception) { - throw IOException("Can't read ResourceInputStream", e) - } - } - - @Throws(IOException::class) - override fun read(b: ByteArray, off: Int, len: Int): Int = synchronized(this) { - checkNotClosed() - - if (available() <= 0) { - return -1 - } - - try { - val bytesToRead = len.coerceAtMost(available()) - val bytes = runBlocking { resource.read(position until (position + bytesToRead)).getOrThrow() } - check(bytes.size <= bytesToRead) - bytes.copyInto( - destination = b, - destinationOffset = off, - startIndex = 0, - endIndex = bytes.size - ) - position += bytes.size - return bytes.size - } catch (e: Exception) { - throw IOException("Can't read ResourceInputStream", e) - } - } - - override fun markSupported(): Boolean = true - - @Throws(IOException::class) - override fun mark(readlimit: Int) = synchronized(this) { - checkNotClosed() - mark = position - } - - @Throws(IOException::class) - override fun reset() = synchronized(this) { - checkNotClosed() - position = mark - } - - /** - * Closes the underlying resource. - */ - override fun close() = synchronized(this) { - if (isClosed) - return - - isClosed = true - runBlocking { resource.close() } - } - - private fun checkNotClosed() { - if (isClosed) - throw IOException("InputStream is closed.") - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/RoutingFetcher.kt b/readium/shared/src/main/java/org/readium/r2/shared/fetcher/RoutingFetcher.kt deleted file mode 100644 index ca70619e83..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/RoutingFetcher.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared.fetcher - -import org.readium.r2.shared.publication.Link - -/** - * Routes requests to child fetchers, depending on a provided predicate. - * - * This can be used for example to serve a publication containing both local and remote resources, - * and more generally to concatenate different content sources. - * - * The [routes] will be tested in the given order. - */ -class RoutingFetcher(private val routes: List<Route>) : Fetcher { - - /** - * Holds a child fetcher and the predicate used to determine if it can answer a request. - * - * The default value for [accepts] means that the fetcher will accept any link. - */ - class Route(val fetcher: Fetcher, val accepts: (Link) -> Boolean = { true }) - - constructor(local: Fetcher, remote: Fetcher) : - this(listOf(Route(local, Link::isLocal), Route(remote))) - - override suspend fun links(): List<Link> = routes.flatMap { it.fetcher.links() } - - override fun get(link: Link): Resource = - routes.firstOrNull { it.accepts(link) }?.fetcher?.get(link) ?: FailureResource(link, Resource.Exception.NotFound()) - - override suspend fun close() { - routes.forEach { it.fetcher.close() } - } -} - -private val Link.isLocal: Boolean get() = href.startsWith("/") diff --git a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/TransformingFetcher.kt b/readium/shared/src/main/java/org/readium/r2/shared/fetcher/TransformingFetcher.kt deleted file mode 100644 index 2cecc76398..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/fetcher/TransformingFetcher.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared.fetcher - -import org.readium.r2.shared.publication.Link - -/** - * Transforms the resources' content of a child fetcher using a list of [ResourceTransformer] - * functions. - */ -class TransformingFetcher( - private val fetcher: Fetcher, - private val transformers: List<ResourceTransformer> -) : Fetcher { - - constructor(fetcher: Fetcher, transformer: ResourceTransformer) : - this(fetcher, listOf(transformer)) - - override suspend fun links(): List<Link> = fetcher.links() - - override fun get(link: Link): Resource { - val resource = fetcher.get(link) - return transformers.fold(resource) { acc, transformer -> transformer(acc) } - } - - override suspend fun close() { - fetcher.close() - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/format/Deprecated.kt b/readium/shared/src/main/java/org/readium/r2/shared/format/Deprecated.kt deleted file mode 100644 index 0695cf8f5d..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/format/Deprecated.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.format - -import com.github.kittinunf.fuel.core.Response -import java.net.HttpURLConnection -import org.readium.r2.shared.util.mediatype.MediaType as NewMediaType -import org.readium.r2.shared.util.mediatype.Sniffer -import org.readium.r2.shared.util.mediatype.SnifferContext -import org.readium.r2.shared.util.mediatype.Sniffers -import org.readium.r2.shared.util.mediatype.sniffMediaType - -@Deprecated("Moved to another package", replaceWith = ReplaceWith("org.readium.r2.shared.util.mediatype.MediaType"), level = DeprecationLevel.ERROR) -typealias MediaType = NewMediaType -@Deprecated("Format and MediaType got merged together", replaceWith = ReplaceWith("org.readium.r2.shared.util.mediatype.MediaType"), level = DeprecationLevel.ERROR) -typealias Format = NewMediaType -@Deprecated("Renamed Sniffer", replaceWith = ReplaceWith("org.readium.r2.shared.util.mediatype.Sniffer"), level = DeprecationLevel.ERROR) -typealias FormatSniffer = Sniffer -@Deprecated("Renamed Sniffers", replaceWith = ReplaceWith("org.readium.r2.shared.util.mediatype.Sniffers"), level = DeprecationLevel.ERROR) -typealias FormatSniffers = Sniffers -@Deprecated("Renamed SnifferContext", replaceWith = ReplaceWith("org.readium.r2.shared.util.mediatype.SnifferContext"), level = DeprecationLevel.ERROR) -typealias FormatSnifferContext = SnifferContext - -@Deprecated("Renamed to another package", ReplaceWith("org.readium.r2.shared.util.mediatype.sniffMediaType"), level = DeprecationLevel.ERROR) -suspend fun Response.sniffFormat( - mediaTypes: List<String> = emptyList(), - fileExtensions: List<String> = emptyList(), - sniffers: List<Sniffer> = NewMediaType.sniffers -): NewMediaType? = - sniffMediaType(mediaTypes, fileExtensions, sniffers) - -@Deprecated("Renamed to another package", ReplaceWith("org.readium.r2.shared.util.mediatype.sniffMediaType"), level = DeprecationLevel.ERROR) -suspend fun HttpURLConnection.sniffFormat( - bytes: (() -> ByteArray)? = null, - mediaTypes: List<String> = emptyList(), - fileExtensions: List<String> = emptyList(), - sniffers: List<Sniffer> = NewMediaType.sniffers -): NewMediaType? = - sniffMediaType(bytes, mediaTypes, fileExtensions, sniffers) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/opds/Acquisition.kt b/readium/shared/src/main/java/org/readium/r2/shared/opds/Acquisition.kt index a064034015..0ced3d2727 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/opds/Acquisition.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/opds/Acquisition.kt @@ -27,30 +27,30 @@ import org.readium.r2.shared.util.mediatype.MediaType * https://drafts.opds.io/schema/acquisition-object.schema.json */ @Parcelize -data class Acquisition( +public data class Acquisition( val type: String, val children: List<Acquisition> = emptyList() ) : JSONable, Parcelable { /** Media type of the resource to acquire. */ val mediaType: MediaType get() = - MediaType.parse(type) ?: MediaType.BINARY + MediaType(type) ?: MediaType.BINARY /** * Serializes an [Acquisition] to its JSON representation. */ - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { put("type", type) putIfNotEmpty("child", children) } - companion object { + public companion object { /** * Creates an [Acquisition] from its JSON representation. * If the acquisition can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Acquisition? { + public fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Acquisition? { val type = json?.optNullableString("type") if (type == null) { warnings?.log(Acquisition::class.java, "[type] is required", json) @@ -67,7 +67,7 @@ data class Acquisition( * Creates a list of [Acquisition] from its JSON representation. * If an acquisition can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSONArray( + public fun fromJSONArray( json: JSONArray?, warnings: WarningLogger? = null ): List<Acquisition> { @@ -75,19 +75,27 @@ data class Acquisition( } } - @Deprecated("Use [type] instead", ReplaceWith("type")) + @Deprecated("Use [type] instead", ReplaceWith("type"), level = DeprecationLevel.ERROR) val typeAcquisition: String? get() = type - @Deprecated("Use [children] instead", ReplaceWith("children")) + @Deprecated("Use [children] instead", ReplaceWith("children"), level = DeprecationLevel.ERROR) val child: List<Acquisition> get() = children } -@Deprecated("Renamed into [Acquisition]", ReplaceWith("Acquisition")) -typealias IndirectAcquisition = Acquisition +@Deprecated( + "Renamed into [Acquisition]", + ReplaceWith("Acquisition"), + level = DeprecationLevel.ERROR +) +public typealias IndirectAcquisition = Acquisition -@Deprecated("Use [Acquisition::fromJSON] instead", ReplaceWith("Acquisition.fromJSON")) -fun parseIndirectAcquisition(indirectAcquisitionDict: JSONObject): Acquisition = +@Deprecated( + "Use [Acquisition::fromJSON] instead", + ReplaceWith("Acquisition.fromJSON"), + level = DeprecationLevel.ERROR +) +public fun parseIndirectAcquisition(indirectAcquisitionDict: JSONObject): Acquisition = Acquisition.fromJSON(indirectAcquisitionDict) ?: throw Exception("Invalid indirect acquisition") diff --git a/readium/shared/src/main/java/org/readium/r2/shared/opds/Availability.kt b/readium/shared/src/main/java/org/readium/r2/shared/opds/Availability.kt index ffc0382de6..f7269900ac 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/opds/Availability.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/opds/Availability.kt @@ -30,37 +30,37 @@ import org.readium.r2.shared.util.logging.log * @param until Timestamp for the next state change. */ @Parcelize -data class Availability( +public data class Availability( val state: State, val since: Date? = null, val until: Date? = null ) : JSONable, Parcelable { - enum class State(val value: String) { + public enum class State(public val value: String) { AVAILABLE("available"), UNAVAILABLE("unavailable"), RESERVED("reserved"), READY("ready"); - companion object : MapCompanion<String, State>(values(), State::value) + public companion object : MapCompanion<String, State>(values(), State::value) } /** * Serializes an [Availability] to its JSON representation. */ - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { put("state", state.value) put("since", since?.toIso8601String()) put("until", until?.toIso8601String()) } - companion object { + public companion object { /** * Creates an [Availability] from its JSON representation. * If the availability can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Availability? { + public fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Availability? { val state = State(json?.optNullableString("state")) if (state == null) { warnings?.log(Availability::class.java, "[state] is required", json) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/opds/Copies.kt b/readium/shared/src/main/java/org/readium/r2/shared/opds/Copies.kt index 9ca56025fe..c1a318d0d8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/opds/Copies.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/opds/Copies.kt @@ -21,7 +21,7 @@ import org.readium.r2.shared.extensions.optPositiveInt * https://drafts.opds.io/schema/properties.schema.json */ @Parcelize -data class Copies( +public data class Copies( val total: Int? = null, val available: Int? = null ) : JSONable, Parcelable { @@ -29,17 +29,17 @@ data class Copies( /** * Serializes an [Copies] to its JSON representation. */ - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { put("total", total) put("available", available) } - companion object { + public companion object { /** * Creates an [Copies] from its JSON representation. */ - fun fromJSON(json: JSONObject?): Copies? { + public fun fromJSON(json: JSONObject?): Copies? { json ?: return null return Copies( total = json.optPositiveInt("total"), diff --git a/readium/shared/src/main/java/org/readium/r2/shared/opds/Facet.kt b/readium/shared/src/main/java/org/readium/r2/shared/opds/Facet.kt index 29947eb3b1..d33dba6866 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/opds/Facet.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/opds/Facet.kt @@ -9,10 +9,25 @@ package org.readium.r2.shared.opds +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.Link -data class Facet( +public data class Facet( val title: String, - var metadata: OpdsMetadata = OpdsMetadata(title = title), - var links: MutableList<Link> = mutableListOf() -) + val metadata: OpdsMetadata = OpdsMetadata(title = title), + val links: List<Link> = emptyList() +) { + @InternalReadiumApi + public data class Builder( + val title: String, + val metadata: OpdsMetadata.Builder = OpdsMetadata.Builder(title = title), + val links: MutableList<Link> = mutableListOf() + ) { + public fun build(): Facet = + Facet( + title = title, + metadata = metadata.build(), + links = links.toList() + ) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/opds/Feed.kt b/readium/shared/src/main/java/org/readium/r2/shared/opds/Feed.kt index 2badb74a5c..36395bbffb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/opds/Feed.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/opds/Feed.kt @@ -9,21 +9,50 @@ package org.readium.r2.shared.opds -import java.net.URL +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Url -data class Feed( +public data class Feed( val title: String, val type: Int, - val href: URL, - var metadata: OpdsMetadata = OpdsMetadata(title = title), - var links: MutableList<Link> = mutableListOf(), - var facets: MutableList<Facet> = mutableListOf(), - var groups: MutableList<Group> = mutableListOf(), - var publications: MutableList<Publication> = mutableListOf(), - var navigation: MutableList<Link> = mutableListOf(), - var context: MutableList<String> = mutableListOf() -) + val href: Url, + val metadata: OpdsMetadata = OpdsMetadata(title = title), + val links: List<Link> = emptyList(), + val facets: List<Facet> = emptyList(), + val groups: List<Group> = emptyList(), + val publications: List<Publication> = emptyList(), + val navigation: List<Link> = emptyList(), + val context: List<String> = emptyList() +) { + @InternalReadiumApi + public data class Builder( + val title: String, + val type: Int, + val href: Url, + val metadata: OpdsMetadata.Builder = OpdsMetadata.Builder(title = title), + val links: MutableList<Link> = mutableListOf(), + val facets: MutableList<Facet.Builder> = mutableListOf(), + val groups: MutableList<Group.Builder> = mutableListOf(), + val publications: MutableList<Publication> = mutableListOf(), + val navigation: MutableList<Link> = mutableListOf(), + val context: MutableList<String> = mutableListOf() + ) { + public fun build(): Feed = + Feed( + title = title, + type = type, + href = href, + metadata = metadata.build(), + links = links.toList(), + facets = facets.map { it.build() }, + groups = groups.map { it.build() }, + publications = publications.toList(), + navigation = navigation.toList(), + context = context.toList() + ) + } +} -data class ParseData(val feed: Feed?, val publication: Publication?, val type: Int) +public data class ParseData(val feed: Feed?, val publication: Publication?, val type: Int) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/opds/Group.kt b/readium/shared/src/main/java/org/readium/r2/shared/opds/Group.kt index d2a4fdf828..ba253bc811 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/opds/Group.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/opds/Group.kt @@ -9,12 +9,32 @@ package org.readium.r2.shared.opds +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication -data class Group(val title: String) { - var metadata: OpdsMetadata = OpdsMetadata(title = title) - var links = mutableListOf<Link>() - var publications = mutableListOf<Publication>() - var navigation = mutableListOf<Link>() +public data class Group( + val title: String, + val metadata: OpdsMetadata = OpdsMetadata(title = title), + val links: List<Link> = emptyList(), + val publications: List<Publication> = emptyList(), + val navigation: List<Link> = emptyList() +) { + @InternalReadiumApi + public data class Builder( + val title: String, + val metadata: OpdsMetadata.Builder = OpdsMetadata.Builder(title = title), + val links: MutableList<Link> = mutableListOf(), + val publications: MutableList<Publication> = mutableListOf(), + val navigation: MutableList<Link> = mutableListOf() + ) { + public fun build(): Group = + Group( + title = title, + metadata = metadata.build(), + links = links.toList(), + publications = publications.toList(), + navigation = navigation.toList() + ) + } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/opds/Holds.kt b/readium/shared/src/main/java/org/readium/r2/shared/opds/Holds.kt index 27fce88f84..4bf60ce4a9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/opds/Holds.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/opds/Holds.kt @@ -21,7 +21,7 @@ import org.readium.r2.shared.extensions.optPositiveInt * https://drafts.opds.io/schema/properties.schema.json */ @Parcelize -data class Holds( +public data class Holds( val total: Int? = null, val position: Int? = null ) : JSONable, Parcelable { @@ -29,17 +29,17 @@ data class Holds( /** * Serializes an [Holds] to its JSON representation. */ - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { put("total", total) put("position", position) } - companion object { + public companion object { /** * Creates an [Holds] from its JSON representation. */ - fun fromJSON(json: JSONObject?): Holds? { + public fun fromJSON(json: JSONObject?): Holds? { json ?: return null return Holds( total = json.optPositiveInt("total"), diff --git a/readium/shared/src/main/java/org/readium/r2/shared/opds/OpdsMetadata.kt b/readium/shared/src/main/java/org/readium/r2/shared/opds/OpdsMetadata.kt index 1990ab9aa0..2fe6a0f723 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/opds/OpdsMetadata.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/opds/OpdsMetadata.kt @@ -10,13 +10,36 @@ package org.readium.r2.shared.opds import java.util.* +import org.readium.r2.shared.InternalReadiumApi -data class OpdsMetadata( - var title: String, - var numberOfItems: Int? = null, - var itemsPerPage: Int? = null, - var currentPage: Int? = null, - var modified: Date? = null, - var position: Int? = null, - var rdfType: String? = null -) +public data class OpdsMetadata( + val title: String, + val numberOfItems: Int? = null, + val itemsPerPage: Int? = null, + val currentPage: Int? = null, + val modified: Date? = null, + val position: Int? = null, + val rdfType: String? = null +) { + @InternalReadiumApi + public data class Builder( + var title: String, + var numberOfItems: Int? = null, + var itemsPerPage: Int? = null, + var currentPage: Int? = null, + var modified: Date? = null, + var position: Int? = null, + var rdfType: String? = null + ) { + public fun build(): OpdsMetadata = + OpdsMetadata( + title = title, + numberOfItems = numberOfItems, + itemsPerPage = itemsPerPage, + currentPage = currentPage, + modified = modified, + position = position, + rdfType = rdfType + ) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/opds/Price.kt b/readium/shared/src/main/java/org/readium/r2/shared/opds/Price.kt index 0e332c3674..1d873a1cea 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/opds/Price.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/opds/Price.kt @@ -28,7 +28,7 @@ import org.readium.r2.shared.util.logging.log * inherent with Double and the JSON parsing. */ @Parcelize -data class Price( +public data class Price( val currency: String, val value: Double ) : JSONable, Parcelable { @@ -36,18 +36,18 @@ data class Price( /** * Serializes an [Price] to its JSON representation. */ - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { put("currency", currency) put("value", value) } - companion object { + public companion object { /** * Creates an [Price] from its JSON representation. * If the price can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Price? { + public fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Price? { val currency = json?.optNullableString("currency") val value = json?.optPositiveDouble("value") if (currency == null || value == null) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Accessibility.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Accessibility.kt index c4be22b88f..2c4af0ea5f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Accessibility.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Accessibility.kt @@ -45,7 +45,7 @@ import org.readium.r2.shared.util.logging.log * dangerous to some users. */ @Parcelize -data class Accessibility( +public data class Accessibility( val conformsTo: Set<Profile>, val certification: Certification? = null, val summary: String? = null, @@ -60,17 +60,23 @@ data class Accessibility( */ @Parcelize @JvmInline - value class Profile(val uri: String) : Parcelable { + public value class Profile(public val uri: String) : Parcelable { - companion object { + public companion object { - val EPUB_A11Y_10_WCAG_20_A = Profile("http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a") + public val EPUB_A11Y_10_WCAG_20_A: Profile = Profile( + "http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a" + ) - val EPUB_A11Y_10_WCAG_20_AA = Profile("http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa") + public val EPUB_A11Y_10_WCAG_20_AA: Profile = Profile( + "http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa" + ) - val EPUB_A11Y_10_WCAG_20_AAA = Profile("http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa") + public val EPUB_A11Y_10_WCAG_20_AAA: Profile = Profile( + "http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa" + ) - fun Set<Profile>.toJSONArray(): JSONArray = + public fun Set<Profile>.toJSONArray(): JSONArray = JSONArray(this.map(Profile::uri)) } } @@ -86,7 +92,7 @@ data class Accessibility( * in the associated [certifiedBy] property. */ @Parcelize - data class Certification( + public data class Certification( val certifiedBy: String?, val credential: String?, val report: String? @@ -98,14 +104,14 @@ data class Accessibility( put("report", report) } - companion object { + public companion object { /** * Parses a [Certification] from its RWPM JSON representation. * * If the certification can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSON( + public fun fromJSON( json: JSONObject?, warnings: WarningLogger? = null ): Certification? { @@ -115,7 +121,11 @@ data class Accessibility( val report = json.optNullableString("report") if (listOfNotNull(certifiedBy, credential, report).isEmpty()) { - warnings?.log(Certification::class.java, "no valid property in certification object", json) + warnings?.log( + Certification::class.java, + "no valid property in certification object", + json + ) return null } @@ -134,43 +144,43 @@ data class Accessibility( */ @Parcelize @JvmInline - value class AccessMode(val value: String) : Parcelable { + public value class AccessMode(public val value: String) : Parcelable { - companion object { + public companion object { /** * Indicates that the resource contains information encoded in auditory form. */ - val AUDITORY = AccessMode("auditory") + public val AUDITORY: AccessMode = AccessMode("auditory") /** * Indicates that the resource contains charts encoded in visual form. */ - val CHART_ON_VISUAL = AccessMode("chartOnVisual") + public val CHART_ON_VISUAL: AccessMode = AccessMode("chartOnVisual") /** * Indicates that the resource contains chemical equations encoded in visual form. */ - val CHEM_ON_VISUAL = AccessMode("chemOnVisual") + public val CHEM_ON_VISUAL: AccessMode = AccessMode("chemOnVisual") /** * Indicates that the resource contains information encoded such that color perception is necessary. */ - val COLOR_DEPENDENT = AccessMode("colorDependent") + public val COLOR_DEPENDENT: AccessMode = AccessMode("colorDependent") /** * Indicates that the resource contains diagrams encoded in visual form. */ - val DIAGRAM_ON_VISUAL = AccessMode("diagramOnVisual") + public val DIAGRAM_ON_VISUAL: AccessMode = AccessMode("diagramOnVisual") /** * Indicates that the resource contains mathematical notations encoded in visual form. */ - val MATH_ON_VISUAL = AccessMode("mathOnVisual") + public val MATH_ON_VISUAL: AccessMode = AccessMode("mathOnVisual") /** * Indicates that the resource contains musical notation encoded in visual form. */ - val MUSIC_ON_VISUAL = AccessMode("musicOnVisual") + public val MUSIC_ON_VISUAL: AccessMode = AccessMode("musicOnVisual") /** * Indicates that the resource contains information encoded in tactile form. @@ -179,32 +189,32 @@ data class Accessibility( * using a braille system, this is not always the case. Tactile perception may also indicate, * for example, the use of tactile graphics to convey information. */ - val TACTILE = AccessMode("tactile") + public val TACTILE: AccessMode = AccessMode("tactile") /** * Indicates that the resource contains text encoded in visual form. */ - val TEXT_ON_VISUAL = AccessMode("textOnVisual") + public val TEXT_ON_VISUAL: AccessMode = AccessMode("textOnVisual") /** * Indicates that the resource contains information encoded in textual form. */ - val TEXTUAL = AccessMode("textual") + public val TEXTUAL: AccessMode = AccessMode("textual") /** * Indicates that the resource contains information encoded in visual form. */ - val VISUAL = AccessMode("visual") + public val VISUAL: AccessMode = AccessMode("visual") /** * Creates a list of [AccessMode] from its RWPM JSON representation. */ - fun fromJSONArray(json: JSONArray?): List<AccessMode> = + public fun fromJSONArray(json: JSONArray?): List<AccessMode> = json?.filterIsInstance(String::class.java) ?.map { AccessMode(it) } .orEmpty() - fun Set<AccessMode>.toJSONArray(): JSONArray = + public fun Set<AccessMode>.toJSONArray(): JSONArray = JSONArray(this.map(AccessMode::value)) } } @@ -215,17 +225,19 @@ data class Accessibility( */ @Parcelize @Serializable - enum class PrimaryAccessMode(val value: String) : Parcelable { + public enum class PrimaryAccessMode(public val value: String) : Parcelable { /** * Indicates that auditory perception is necessary to consume the information. */ - @SerialName("auditory") AUDITORY("auditory"), + @SerialName("auditory") + AUDITORY("auditory"), /** * Indicates that tactile perception is necessary to consume the information. */ - @SerialName("tactile") TACTILE("tactile"), + @SerialName("tactile") + TACTILE("tactile"), /** * Indicates that the ability to read textual content is necessary to consume the information. @@ -233,24 +245,29 @@ data class Accessibility( * Note that reading textual content does not require visual perception, as textual content * can be rendered as audio using a text-to-speech capable device or assistive technology. */ - @SerialName("textual") TEXTUAL("textual"), + @SerialName("textual") + TEXTUAL("textual"), /** * Indicates that visual perception is necessary to consume the information. */ - @SerialName("visual") VISUAL("visual"); + @SerialName("visual") + VISUAL("visual"); - companion object : MapCompanion<String, PrimaryAccessMode>(values(), PrimaryAccessMode::value) { + public companion object : MapCompanion<String, PrimaryAccessMode>( + values(), + PrimaryAccessMode::value + ) { /** * Creates a list of [PrimaryAccessMode] from its RWPM JSON representation. */ - fun fromJSONArray(json: JSONArray?): List<PrimaryAccessMode> = + public fun fromJSONArray(json: JSONArray?): List<PrimaryAccessMode> = json?.filterIsInstance(String::class.java) ?.mapNotNull { get(it) } .orEmpty() - fun Set<PrimaryAccessMode>.toJSONArray(): JSONArray = + public fun Set<PrimaryAccessMode>.toJSONArray(): JSONArray = JSONArray(this.map(PrimaryAccessMode::value)) } } @@ -261,13 +278,13 @@ data class Accessibility( */ @Parcelize @JvmInline - value class Feature(val value: String) : Parcelable { + public value class Feature(public val value: String) : Parcelable { - companion object { + public companion object { /** * The work includes annotations from the author, instructor and/or others. */ - val ANNOTATIONS = Feature("annotations") + public val ANNOTATIONS: Feature = Feature("annotations") /** * Indicates the resource includes ARIA roles to organize and improve the structure and navigation. @@ -275,74 +292,74 @@ data class Accessibility( * The use of this value corresponds to the inclusion of Document Structure, Landmark, * Live Region, and Window roles [WAI-ARIA]. */ - val ARIA = Feature("ARIA") + public val ARIA: Feature = Feature("ARIA") /** * The work includes bookmarks to facilitate navigation to key points. */ - val BOOKMARKS = Feature("bookmark") + public val BOOKMARKS: Feature = Feature("bookmark") /** * The work includes an index to the content. */ - val INDEX = Feature("index") + public val INDEX: Feature = Feature("index") /** * The work includes equivalent print page numbers. This setting is most commonly used * with ebooks for which there is a print equivalent. */ - val PRINT_PAGE_NUMBERS = Feature("printPageNumbers") + public val PRINT_PAGE_NUMBERS: Feature = Feature("printPageNumbers") /** * The reading order of the content is clearly defined in the markup * (e.g., figures, sidebars and other secondary content has been marked up to allow it * to be skipped automatically and/or manually escaped from). */ - val READING_ORDER = Feature("readingOrder") + public val READING_ORDER: Feature = Feature("readingOrder") /** * The use of headings in the work fully and accurately reflects the document hierarchy, * allowing navigation by assistive technologies. */ - val STRUCTURAL_NAVIGATION = Feature("structuralNavigation") + public val STRUCTURAL_NAVIGATION: Feature = Feature("structuralNavigation") /** * The work includes a table of contents that provides links to the major sections of the content. */ - val TABLE_OF_CONTENTS = Feature("tableOfContents") + public val TABLE_OF_CONTENTS: Feature = Feature("tableOfContents") /** * The contents of the PDF have been tagged to permit access by assistive technologies. */ - val TAGGED_PDF = Feature("taggedPDF") + public val TAGGED_PDF: Feature = Feature("taggedPDF") /** * Alternative text is provided for visual content (e.g., via the HTML `alt` attribute). */ - val ALTERNATIVE_TEXT = Feature("alternativeText") + public val ALTERNATIVE_TEXT: Feature = Feature("alternativeText") /** * Audio descriptions are available (e.g., via an HTML `track` element with its `kind` * attribute set to "descriptions"). */ - val AUDIO_DESCRIPTION = Feature("audioDescription") + public val AUDIO_DESCRIPTION: Feature = Feature("audioDescription") /** * Indicates that synchronized captions are available for audio and video content. */ - val CAPTIONS = Feature("captions") + public val CAPTIONS: Feature = Feature("captions") /** * Textual descriptions of math equations are included, whether in the alt attribute * for image-based equations, */ - val DESCRIBED_MATH = Feature("describeMath") + public val DESCRIBED_MATH: Feature = Feature("describeMath") /** * Descriptions are provided for image-based visual content and/or complex structures * such as tables, mathematics, diagrams, and charts. */ - val LONG_DESCRIPTION = Feature("longDescription") + public val LONG_DESCRIPTION: Feature = Feature("longDescription") /** * Indicates that `ruby` annotations HTML are provided in the content. Ruby annotations @@ -351,17 +368,17 @@ data class Accessibility( * * The absence of rubyAnnotations implies that no CJK ideographic characters have ruby. */ - val RUBY_ANNOTATIONS = Feature("rubyAnnotations") + public val RUBY_ANNOTATIONS: Feature = Feature("rubyAnnotations") /** * Sign language interpretation is available for audio and video content. */ - val SIGN_LANGUAGE = Feature("signLanguage") + public val SIGN_LANGUAGE: Feature = Feature("signLanguage") /** * Indicates that a transcript of the audio content is available. */ - val TRANSCRIPT = Feature("transcript") + public val TRANSCRIPT: Feature = Feature("transcript") /** * Display properties are controllable by the user. This property can be set, for example, @@ -369,7 +386,7 @@ data class Accessibility( * It can also be used to indicate that styling in document formats like Word and PDF * can be modified. */ - val DISPLAY_TRANSFORMABILITY = Feature("displayTransformability") + public val DISPLAY_TRANSFORMABILITY: Feature = Feature("displayTransformability") /** * Describes a resource that offers both audio and text, with information that allows them @@ -377,93 +394,93 @@ data class Accessibility( * This term is not recommended when the only material that is synchronized is * the document headings. */ - val SYNCHRONIZED_AUDIO_TEXT = Feature("synchronizedAudioText") + public val SYNCHRONIZED_AUDIO_TEXT: Feature = Feature("synchronizedAudioText") /** * For content with timed interaction, this value indicates that the user can control * the timing to meet their needs (e.g., pause and reset) */ - val TIMING_CONTROL = Feature("timingControl") + public val TIMING_CONTROL: Feature = Feature("timingControl") /** * No digital rights management or other content restriction protocols have been applied * to the resource. */ - val UNLOCKED = Feature("unlocked") + public val UNLOCKED: Feature = Feature("unlocked") /** * Identifies that chemical information is encoded using the ChemML markup language. */ - val CHEM_ML = Feature("ChemML") + public val CHEM_ML: Feature = Feature("ChemML") /** * Identifies that mathematical equations and formulas are encoded in the LaTeX * typesetting system. */ - val LATEX = Feature("latex") + public val LATEX: Feature = Feature("latex") /** * Identifies that mathematical equations and formulas are encoded in MathML. */ - val MATH_ML = Feature("MathML") + public val MATH_ML: Feature = Feature("MathML") /** * One or more of SSML, Pronunciation-Lexicon, and CSS3-Speech properties has been used * to enhance text-to-speech playback quality. */ - val TTS_MARKUP = Feature("ttsMarkup") + public val TTS_MARKUP: Feature = Feature("ttsMarkup") /** * Audio content with speech in the foreground meets the contrast thresholds set out * in WCAG Success Criteria 1.4.7. */ - val HIGH_CONTRAST_AUDIO = Feature("highContrastAudio") + public val HIGH_CONTRAST_AUDIO: Feature = Feature("highContrastAudio") /** * Content meets the visual contrast threshold set out in WCAG Success Criteria 1.4.6. */ - val HIGH_CONTRAST_DISPLAY = Feature("highContrastDisplay") + public val HIGH_CONTRAST_DISPLAY: Feature = Feature("highContrastDisplay") /** * The content has been formatted to meet large print guidelines. * * The property is not set if the font size can be increased. See DISPLAY_TRANSFORMABILITY. */ - val LARGE_PRINT = Feature("largePrint") + public val LARGE_PRINT: Feature = Feature("largePrint") /** * The content is in braille format, or alternatives are available in braille. */ - val BRAILLE = Feature("braille") + public val BRAILLE: Feature = Feature("braille") /** * When used with creative works such as books, indicates that the resource includes * tactile graphics. When used to describe an image resource or physical object, * indicates that the resource is a tactile graphic. */ - val TACTILE_GRAPHIC = Feature("tactileGraphic") + public val TACTILE_GRAPHIC: Feature = Feature("tactileGraphic") /** * When used with creative works such as books, indicates that the resource includes models * to generate tactile 3D objects. When used to describe a physical object, * indicates that the resource is a tactile 3D object. */ - val TACTILE_OBJECT = Feature("tactileObject") + public val TACTILE_OBJECT: Feature = Feature("tactileObject") /** * Indicates that the resource does not contain any accessibility features. */ - val NONE = Feature("none") + public val NONE: Feature = Feature("none") /** * Creates a list of [Feature] from its RWPM JSON representation. */ - fun fromJSONArray(json: JSONArray?): List<Feature> = + public fun fromJSONArray(json: JSONArray?): List<Feature> = json?.filterIsInstance(String::class.java) ?.map { Feature(it) } .orEmpty() - fun Set<Feature>.toJSONArray(): JSONArray = + public fun Set<Feature>.toJSONArray(): JSONArray = JSONArray(this.map(Feature::value)) } } @@ -473,19 +490,19 @@ data class Accessibility( */ @Parcelize @JvmInline - value class Hazard(val value: String) : Parcelable { + public value class Hazard(public val value: String) : Parcelable { - companion object { + public companion object { /** * Indicates that the resource presents a flashing hazard for photosensitive persons. */ - val FLASHING = Hazard("flashing") + public val FLASHING: Hazard = Hazard("flashing") /** * Indicates that the resource does not present a flashing hazard. */ - val NO_FLASHING_HAZARD = Hazard("noFlashingHazard") + public val NO_FLASHING_HAZARD: Hazard = Hazard("noFlashingHazard") /** * Indicates that the resource contains instances of motion simulation that @@ -494,44 +511,44 @@ data class Accessibility( * Some examples of motion simulation include video games with a first-person perspective * and CSS-controlled backgrounds that move when a user scrolls a page. */ - val MOTION_SIMULATION = Hazard("motionSimulation") + public val MOTION_SIMULATION: Hazard = Hazard("motionSimulation") /** * Indicates that the resource does not contain instances of motion simulation. * * See MOTION_SIMULATION. */ - val NO_MOTION_SIMULATION_HAZARD = Hazard("noMotionSimulationHazard") + public val NO_MOTION_SIMULATION_HAZARD: Hazard = Hazard("noMotionSimulationHazard") /** * Indicates that the resource contains auditory sounds that may affect some individuals. */ - val SOUND = Hazard("sound") + public val SOUND: Hazard = Hazard("sound") /** * Indicates that the resource does not contain auditory hazards. */ - val NO_SOUND_HAZARD = Hazard("noSoundHazard") + public val NO_SOUND_HAZARD: Hazard = Hazard("noSoundHazard") /** * Indicates that the author is not able to determine if the resource presents any hazards. */ - val UNKNOWN = Hazard("unknown") + public val UNKNOWN: Hazard = Hazard("unknown") /** * Indicates that the resource does not contain any hazards. */ - val NONE = Hazard("none") + public val NONE: Hazard = Hazard("none") /** * Creates a list of [Hazard] from its RWPM JSON representation. */ - fun fromJSONArray(json: JSONArray?): List<Hazard> = + public fun fromJSONArray(json: JSONArray?): List<Hazard> = json?.filterIsInstance(String::class.java) ?.map { Hazard(it) } .orEmpty() - fun Set<Hazard>.toJSONArray(): JSONArray = + public fun Set<Hazard>.toJSONArray(): JSONArray = JSONArray(this.map(Hazard::value)) } } @@ -546,14 +563,14 @@ data class Accessibility( putIfNotEmpty("feature", features.toJSONArray()) } - companion object { + public companion object { /** * Parses a [Accessibility] from its RWPM JSON representation. * * If the accessibility metadata can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSON(json: Any?): Accessibility? { + public fun fromJSON(json: Any?): Accessibility? { if (json !is JSONObject) { return null } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Collection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Collection.kt index 2424c17af5..0e84b501c0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Collection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Collection.kt @@ -13,4 +13,4 @@ package org.readium.r2.shared.publication * Collection type used for collection/series metadata. * For convenience, the JSON schema reuse the Contributor's definition. */ -typealias Collection = Contributor +public typealias Collection = Contributor diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/ContentLayout.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/ContentLayout.kt index 0677ba3cbe..ad53826299 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/ContentLayout.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/ContentLayout.kt @@ -13,7 +13,6 @@ package org.readium.r2.shared.publication import android.os.Parcelable import kotlinx.parcelize.Parcelize -import org.readium.r2.shared.util.MapWithDefaultCompanion /** * The [ContentLayout] defines how a [Publication] should be laid out, based on the declared @@ -25,26 +24,17 @@ import org.readium.r2.shared.util.MapWithDefaultCompanion * @param cssId Identifier for this layout style, shared with ReadiumCSS. */ @Parcelize -@Deprecated("Use `Metadata.effectiveReadingProgression` instead", level = DeprecationLevel.WARNING) -enum class ContentLayout(val cssId: String) : Parcelable { +@Deprecated("Use `Metadata.effectiveReadingProgression` instead", level = DeprecationLevel.ERROR) +public enum class ContentLayout(private val cssId: String) : Parcelable { // Right to left RTL("rtl"), + // Left to right LTR("ltr"), + // Asian language, vertically laid out CJK_VERTICAL("cjk-vertical"), + // Asian language, horizontally laid out CJK_HORIZONTAL("cjk-horizontal"); - - companion object : MapWithDefaultCompanion<String, ContentLayout>(values(), ContentLayout::cssId, LTR) { - - @Deprecated("Renamed to [RTL]", ReplaceWith("ContentLayout.RTL")) - val rtl: ContentLayout = RTL - @Deprecated("Renamed to [LTR]", ReplaceWith("ContentLayout.LTR")) - val ltr: ContentLayout = LTR - @Deprecated("Renamed to [CJK_VERTICAL]", ReplaceWith("ContentLayout.CJK_VERTICAL")) - val cjkv: ContentLayout = CJK_VERTICAL - @Deprecated("Renamed to [CJK_HORIZONTAL]", ReplaceWith("ContentLayout.CJK_HORIZONTAL")) - val cjkh: ContentLayout = CJK_HORIZONTAL - } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/ContentProtection.kt deleted file mode 100644 index 7218d32842..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/ContentProtection.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared.publication - -import androidx.annotation.StringRes -import org.readium.r2.shared.R -import org.readium.r2.shared.UserException -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.publication.asset.PublicationAsset -import org.readium.r2.shared.publication.services.ContentProtectionService -import org.readium.r2.shared.util.Try - -/** - * Bridge between a Content Protection technology and the Readium toolkit. - * - * Its responsibilities are to: - * - Unlock a publication by returning a customized [Fetcher]. - * - Create a [ContentProtectionService] publication service. - */ -interface ContentProtection { - - /** - * Attempts to unlock a potentially protected publication asset. - * - * The Streamer will create a leaf [fetcher] for the low-level [asset] access (e.g. - * ArchiveFetcher for a ZIP archive), to avoid having each Content Protection open the asset to - * check if it's protected or not. - * - * A publication might be protected in such a way that the package format can't be recognized, - * in which case the Content Protection will have the responsibility of creating a new leaf - * [Fetcher]. - * - * @return A [ProtectedAsset] in case of success, null if the asset is not protected by this - * technology or a [Publication.OpeningException] if the asset can't be successfully opened, - * even in restricted mode. - */ - suspend fun open( - asset: PublicationAsset, - fetcher: Fetcher, - credentials: String?, - allowUserInteraction: Boolean, - sender: Any? - ): Try<ProtectedAsset, Publication.OpeningException>? - - /** - * Holds the result of opening a [PublicationAsset] with a [ContentProtection]. - * - * @property asset Protected asset which will be provided to the parsers. - * In most cases, this will be the asset provided to ContentProtection::open(), - * but a Content Protection might modify it in some cases: - * - If the original asset has a media type that can't be recognized by parsers, - * the Content Protection must return an asset with the matching unprotected media type. - * - If the Content Protection technology needs to redirect the Streamer to a different file. - * For example, this could be used to decrypt a publication to a temporary secure location. - * - * @property fetcher Primary leaf fetcher to be used by parsers. - * The Content Protection can unlock resources by modifying the Fetcher provided to - * ContentProtection::open(), for example by: - * - Wrapping the given fetcher in a TransformingFetcher with a decryption Resource.Transformer - * function. - * - Discarding the provided fetcher altogether and creating a new one to handle access - * restrictions. For example, by creating an HTTPFetcher which will inject a Bearer Token in - * requests. - * - * @property onCreatePublication Called on every parsed Publication.Builder. - * It can be used to modify the `Manifest`, the root [Fetcher] or the list of service factories - * of a [Publication]. - */ - data class ProtectedAsset( - val asset: PublicationAsset, - val fetcher: Fetcher, - val onCreatePublication: Publication.Builder.() -> Unit = {} - ) - - /** - * Represents a specific Content Protection technology, uniquely identified with an [uri]. - */ - class Scheme( - val uri: String, - val name: LocalizedString? - ) { - override fun hashCode(): Int = uri.hashCode() - override fun equals(other: Any?): Boolean = (other as? Scheme)?.uri == uri - - companion object { - /** Readium LCP DRM scheme. */ - val Lcp = Scheme(uri = "http://readium.org/2014/01/lcp", name = LocalizedString("Readium LCP")) - /** Adobe ADEPT DRM scheme. */ - val Adept = Scheme(uri = "http://ns.adobe.com/adept", name = LocalizedString("Adobe ADEPT")) - } - } - - sealed class Exception( - userMessageId: Int, - vararg args: Any?, - quantity: Int? = null, - cause: Throwable? = null - ) : UserException(userMessageId, quantity, *args, cause = cause) { - constructor(@StringRes userMessageId: Int, vararg args: Any?, cause: Throwable? = null) : this(userMessageId, *args, quantity = null, cause = cause) - - /** - * Exception returned when the given Content Protection [scheme] is not supported by the - * app. - */ - class SchemeNotSupported(val scheme: Scheme? = null) : Exception( - if (scheme?.name == null) R.string.r2_shared_publication_content_protection_exception_not_supported_unknown - else R.string.r2_shared_publication_content_protection_exception_not_supported, - scheme?.name?.string - ) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Contributor.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Contributor.kt index cdd476d199..dfa3d7c02b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Contributor.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Contributor.kt @@ -14,7 +14,11 @@ import kotlinx.parcelize.Parcelize import org.json.JSONArray import org.json.JSONObject import org.readium.r2.shared.JSONable -import org.readium.r2.shared.extensions.* +import org.readium.r2.shared.extensions.optNullableDouble +import org.readium.r2.shared.extensions.optNullableString +import org.readium.r2.shared.extensions.optStringsFromArrayOrSingle +import org.readium.r2.shared.extensions.parseObjects +import org.readium.r2.shared.extensions.putIfNotEmpty import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.logging.log @@ -23,15 +27,15 @@ import org.readium.r2.shared.util.logging.log * https://readium.org/webpub-manifest/schema/contributor-object.schema.json * * @param localizedName The name of the contributor. + * @param localizedSortAs The string used to sort the name of the contributor. * @param identifier An unambiguous reference to this contributor. - * @param sortAs The string used to sort the name of the contributor. * @param roles The roles of the contributor in the publication making. * @param position The position of the publication in this collection/series, * when the contributor represents a collection. * @param links Used to retrieve similar publications for the given contributor. */ @Parcelize -data class Contributor( +public data class Contributor( val localizedName: LocalizedString, val localizedSortAs: LocalizedString? = null, val identifier: String? = null, @@ -43,7 +47,7 @@ data class Contributor( /** * Shortcut to create a [Contributor] using a string as [name]. */ - constructor(name: String) : this( + public constructor(name: String) : this( localizedName = LocalizedString(name) ) @@ -60,7 +64,7 @@ data class Contributor( /** * Serializes a [Subject] to its RWPM JSON representation. */ - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { putIfNotEmpty("name", localizedName) put("identifier", identifier) putIfNotEmpty("sortAs", localizedSortAs) @@ -69,19 +73,16 @@ data class Contributor( putIfNotEmpty("links", links) } - companion object { + public companion object { /** * Parses a [Contributor] from its RWPM JSON representation. * * A contributor can be parsed from a single string, or a full-fledged object. - * The [links]' href and their children's will be normalized recursively using the - * provided [normalizeHref] closure. * If the contributor can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSON( + public fun fromJSON( json: Any?, - normalizeHref: LinkHrefNormalizer = LinkHrefNormalizerIdentity, warnings: WarningLogger? = null ): Contributor? { json ?: return null @@ -103,35 +104,44 @@ data class Contributor( localizedSortAs = LocalizedString.fromJSON(jsonObject.remove("sortAs"), warnings), roles = jsonObject.optStringsFromArrayOrSingle("role").toSet(), position = jsonObject.optNullableDouble("position"), - links = Link.fromJSONArray(jsonObject.optJSONArray("links"), normalizeHref) + links = Link.fromJSONArray( + jsonObject.optJSONArray("links"), + warnings + ) ) } /** * Creates a list of [Contributor] from its RWPM JSON representation. * - * The [links]' href and their children's will be normalized recursively using the - * provided [normalizeHref] closure. * If a contributor can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSONArray( + public fun fromJSONArray( json: Any?, - normalizeHref: LinkHrefNormalizer = LinkHrefNormalizerIdentity, warnings: WarningLogger? = null ): List<Contributor> { return when (json) { is String, is JSONObject -> - listOf(json).mapNotNull { fromJSON(it, normalizeHref, warnings) } + listOf(json).mapNotNull { + fromJSON( + it, + warnings + ) + } is JSONArray -> - json.parseObjects { fromJSON(it, normalizeHref, warnings) } + json.parseObjects { fromJSON(it, warnings) } else -> emptyList() } } } - @Deprecated("Use [localizedName] instead.", ReplaceWith("localizedName")) + @Deprecated( + "Use [localizedName] instead.", + ReplaceWith("localizedName"), + level = DeprecationLevel.ERROR + ) val multilanguageName: LocalizedString get() = localizedName } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Href.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Href.kt new file mode 100644 index 0000000000..1287ac2b50 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Href.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2020 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.publication + +import android.os.Parcelable +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import org.readium.r2.shared.util.URITemplate +import org.readium.r2.shared.util.Url as SharedUrl +import timber.log.Timber + +/** + * An hypertext reference points to a resource in a [Publication]. + * + * It is potentially templated, use [resolve] to get the actual URL. + */ +@Parcelize +public class Href private constructor(private val href: Url) : Parcelable { + + /** + * Creates an [Href] from a valid URL. + */ + public constructor(href: SharedUrl) : this(StaticUrl(href)) + + public companion object { + /** + * Creates an [Href] from a valid URL or URL template (RFC 6570). + * + * @param templated Indicates whether [href] is a URL template. + */ + public operator fun invoke(href: String, templated: Boolean = false): Href? = + if (templated) { + Href(TemplatedUrl(href)) + } else { + SharedUrl(href)?.let { Href(StaticUrl(it)) } + } + } + + /** + * Returns the URL represented by this HREF, resolved to the given [base] URL. + * + * If the HREF is a template, the [parameters] are used to expand it according to RFC 6570. + */ + public fun resolve( + base: SharedUrl? = null, + parameters: Map<String, String> = emptyMap() + ): SharedUrl = href.resolve(base, parameters) + + /** + * Indicates whether this HREF is templated. + */ + public val isTemplated: Boolean get() = + href is TemplatedUrl + + /** + * List of URI template parameter keys, if the HREF is templated. + */ + public val parameters: List<String>? get() = + href.parameters + + /** + * Resolves the receiver HREF to the given [baseUrl]. + * + * For example: + * href = "bar/baz" + * baseUrl = "http://example.com/foo/" + * result = "http://example.com/foo/bar/baz" + */ + internal fun resolveTo(baseUrl: SharedUrl): Href = + when (href) { + is StaticUrl -> Href(StaticUrl(baseUrl.resolve(href.url))) + is TemplatedUrl -> { + Timber.w("Cannot safely resolve a URI template to a base URL before expanding it") + this + } + } + + override fun toString(): String = href.toString() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Href + + if (href != other.href) return false + + return true + } + + override fun hashCode(): Int = + href.hashCode() + + private sealed class Url : Parcelable { + abstract fun resolve(base: SharedUrl?, parameters: Map<String, String>): SharedUrl + abstract val parameters: List<String>? + } + + /** + * A static hypertext reference to a publication resource. + */ + @Parcelize + private data class StaticUrl(val url: SharedUrl) : Url() { + @IgnoredOnParcel + override val parameters: List<String>? = null + + override fun resolve(base: SharedUrl?, parameters: Map<String, String>): SharedUrl = + base?.resolve(url) ?: url + + override fun toString(): String = url.toString() + } + + /** + * A templated hypertext reference to a publication resource. + * + * @param template The URL template, as defined in RFC 6570. + */ + @Parcelize + private data class TemplatedUrl(val template: String) : Url() { + + companion object { + operator fun invoke(template: String): TemplatedUrl? { + // Check that the produced URL is valid. + SharedUrl(URITemplate(template).expand(emptyMap())) ?: return null + return TemplatedUrl(template) + } + } + + @IgnoredOnParcel + override val parameters: List<String> = + URITemplate(template).parameters + + override fun resolve(base: SharedUrl?, parameters: Map<String, String>): SharedUrl { + val url = checkNotNull(SharedUrl(URITemplate(template).expand(parameters))) + return base?.resolve(url) ?: url + } + + override fun toString(): String = template + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/HrefNormalizer.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/HrefNormalizer.kt new file mode 100644 index 0000000000..d1df65d838 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/HrefNormalizer.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.publication + +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.Url + +/** + * Returns a copy of the receiver after normalizing its HREFs to the link with `rel="self"`. + */ +@ExperimentalReadiumApi +public fun Manifest.normalizeHrefsToSelf(): Manifest { + val base = linkWithRel("self")?.href?.resolve() + ?: return this + + return normalizeHrefsToBase(base) +} + +/** + * Returns a copy of the receiver after normalizing its HREFs to the given [baseUrl]. + */ +@ExperimentalReadiumApi +public fun Manifest.normalizeHrefsToBase(baseUrl: Url): Manifest { + return copy(HrefNormalizer(baseUrl)) +} + +/** + * Returns a copy of the receiver after normalizing its HREFs to the given [baseUrl]. + */ +@ExperimentalReadiumApi +public fun Link.normalizeHrefsToBase(baseUrl: Url?): Link { + baseUrl ?: return this + return copy(HrefNormalizer(baseUrl)) +} + +@OptIn(ExperimentalReadiumApi::class) +private class HrefNormalizer(private val baseUrl: Url) : ManifestTransformer { + override fun transform(href: Href): Href = + href.resolveTo(baseUrl) +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Link.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Link.kt index faddd21ea4..4460536f04 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Link.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Link.kt @@ -15,31 +15,23 @@ import kotlinx.parcelize.Parcelize import org.json.JSONArray import org.json.JSONObject import org.readium.r2.shared.JSONable -import org.readium.r2.shared.extensions.* -import org.readium.r2.shared.util.Href -import org.readium.r2.shared.util.URITemplate +import org.readium.r2.shared.extensions.optNullableString +import org.readium.r2.shared.extensions.optPositiveDouble +import org.readium.r2.shared.extensions.optPositiveInt +import org.readium.r2.shared.extensions.optStringsFromArrayOrSingle +import org.readium.r2.shared.extensions.parseObjects +import org.readium.r2.shared.extensions.putIfNotEmpty +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.logging.log import org.readium.r2.shared.util.mediatype.MediaType -/** - * Function used to recursively transform the href of a [Link] when parsing its JSON - * representation. - */ -typealias LinkHrefNormalizer = (String) -> String - -/** - * Default href normalizer for [Link], doing nothing. - */ -val LinkHrefNormalizerIdentity: LinkHrefNormalizer = { it } - /** * Link Object for the Readium Web Publication Manifest. * https://readium.org/webpub-manifest/schema/link.schema.json * * @param href URI or URI template of the linked resource. - * @param type MIME type of the linked resource. - * @param templated Indicates that a URI template is used in href. + * @param mediaType Media type of the linked resource. * @param title Title of the linked resource. * @param rels Relation between the linked resource and its containing collection. * @param properties Properties associated to the linked resource. @@ -53,10 +45,9 @@ val LinkHrefNormalizerIdentity: LinkHrefNormalizer = { it } * collection role. */ @Parcelize -data class Link( - val href: String, - val type: String? = null, - val templated: Boolean = false, +public data class Link( + val href: Href, + val mediaType: MediaType? = null, val title: String? = null, val rels: Set<String> = setOf(), val properties: Properties = Properties(), @@ -69,50 +60,80 @@ data class Link( val children: List<Link> = listOf() ) : JSONable, Parcelable { - /** Media type of the linked resource. */ - val mediaType: MediaType get() = - type?.let { MediaType.parse(it) } ?: MediaType.BINARY + /** + * Convenience constructor for a [Link] with a [Url] as [href]. + */ + public constructor( + href: Url, + mediaType: MediaType? = null, + title: String? = null, + rels: Set<String> = setOf(), + properties: Properties = Properties(), + alternates: List<Link> = listOf(), + children: List<Link> = listOf() + ) : this( + href = Href(href), + mediaType = mediaType, + title = title, + rels = rels, + properties = properties, + alternates = alternates, + children = children + ) + + /** + * Returns the URL represented by this link's HREF, resolved to the given [base] URL. + * + * If the HREF is a template, the [parameters] are used to expand it according to RFC 6570. + */ + public fun url( + base: Url? = null, + parameters: Map<String, String> = emptyMap() + ): Url = href.resolve(base, parameters) /** * List of URI template parameter keys, if the [Link] is templated. */ @IgnoredOnParcel - val templateParameters: List<String> by lazy { - if (!templated) - emptyList() - else - URITemplate(href).parameters - } + @Deprecated("Open a GitHub issue if you were using this", level = DeprecationLevel.ERROR) + val templateParameters: List<String> get() = + throw NotImplementedError() /** * Expands the HREF by replacing URI template variables by the given parameters. * * See RFC 6570 on URI template. */ - fun expandTemplate(parameters: Map<String, String>): Link = - copy(href = URITemplate(href).expand(parameters), templated = false) + @Deprecated( + "Use `url(parameters)` instead", + ReplaceWith("this.url(parameters = parameters)"), + level = DeprecationLevel.ERROR + ) + @Suppress("UNUSED_PARAMETER") + public fun expandTemplate(parameters: Map<String, String>): Link? = + throw NotImplementedError() /** * Computes an absolute URL to the link, relative to the given [baseUrl]. * * If the link's [href] is already absolute, the [baseUrl] is ignored. */ - fun toUrl(baseUrl: String?): String? { - val href = href.removePrefix("/") - if (href.isBlank()) { - return null - } - - return Href(href, baseHref = baseUrl ?: "/").percentEncodedString - } + @Deprecated( + "Use `url(baseUrl)` instead", + ReplaceWith("this.url(baseUrl)"), + level = DeprecationLevel.ERROR + ) + @Suppress("UNUSED_PARAMETER") + public fun toUrl(baseUrl: Url?): String? = + throw NotImplementedError() /** * Serializes a [Link] to its RWPM JSON representation. */ override fun toJSON(): JSONObject = JSONObject().apply { - put("href", href) - put("type", type) - put("templated", templated) + put("href", href.toString()) + put("type", mediaType?.toString()) + put("templated", href.isTemplated) put("title", title) putIfNotEmpty("rel", rels) putIfNotEmpty("properties", properties) @@ -128,32 +149,26 @@ data class Link( /** * Makes a copy of this [Link] after merging in the given additional other [properties]. */ - fun addProperties(properties: Map<String, Any>): Link = + public fun addProperties(properties: Map<String, Any>): Link = copy(properties = this.properties.add(properties)) - companion object { + public companion object { /** * Creates an [Link] from its RWPM JSON representation. - * It's [href] and its children's recursively will be normalized using the provided - * [normalizeHref] closure. + * * If the link can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSON( + public fun fromJSON( json: JSONObject?, - normalizeHref: LinkHrefNormalizer = LinkHrefNormalizerIdentity, warnings: WarningLogger? = null ): Link? { - val href = json?.optNullableString("href") - if (href == null) { - warnings?.log(Link::class.java, "[href] is required", json) - return null - } + json ?: return null return Link( - href = normalizeHref(href), - type = json.optNullableString("type"), - templated = json.optBoolean("templated", false), + href = parseHref(json, warnings) ?: return null, + mediaType = json.optNullableString("type") + ?.let { MediaType(it) }, title = json.optNullableString("title"), rels = json.optStringsFromArrayOrSingle("rel").toSet(), properties = Properties.fromJSON(json.optJSONObject("properties")), @@ -162,31 +177,79 @@ data class Link( bitrate = json.optPositiveDouble("bitrate"), duration = json.optPositiveDouble("duration"), languages = json.optStringsFromArrayOrSingle("language"), - alternates = fromJSONArray(json.optJSONArray("alternate"), normalizeHref), - children = fromJSONArray(json.optJSONArray("children"), normalizeHref) + alternates = fromJSONArray( + json.optJSONArray("alternate") + ), + children = fromJSONArray( + json.optJSONArray("children") + ) ) } + private fun parseHref( + json: JSONObject, + warnings: WarningLogger? = null + ): Href? { + val hrefString = json.optNullableString("href") + if (hrefString == null) { + warnings?.log(Link::class.java, "[href] is required", json) + return null + } + + val templated = json.optBoolean("templated", false) + val href = if (templated) { + Href(hrefString, templated = true) + } else { + // We support existing publications with incorrect HREFs (not valid percent-encoded + // URIs). We try to parse them first as valid, but fall back on a percent-decoded + // path if it fails. + val url = Url(hrefString) ?: run { + warnings?.log( + Link::class.java, + "[href] is not a valid percent-encoded URL", + json + ) + Url.fromDecodedPath(hrefString) + } + url?.let { Href(it) } + } + + if (href == null) { + warnings?.log(Link::class.java, "[href] is not a valid URL or URL template", json) + } + + return href + } + /** * Creates a list of [Link] from its RWPM JSON representation. - * It's [href] and its children's recursively will be normalized using the provided - * [normalizeHref] closure. + * * If a link can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSONArray( + public fun fromJSONArray( json: JSONArray?, - normalizeHref: LinkHrefNormalizer = LinkHrefNormalizerIdentity, warnings: WarningLogger? = null ): List<Link> { - return json.parseObjects { fromJSON(it as? JSONObject, normalizeHref, warnings) } + return json.parseObjects { + fromJSON( + it as? JSONObject, + warnings + ) + } } } - @Deprecated("Use [type] instead", ReplaceWith("type")) - val typeLink: String? - get() = type + @Deprecated( + "Use [mediaType.toString()] instead", + ReplaceWith("mediaType.toString()"), + level = DeprecationLevel.ERROR + ) + val type: String? get() = throw NotImplementedError() - @Deprecated("Use [rels] instead.", ReplaceWith("rels")) + @Deprecated("Use [type] instead", ReplaceWith("type"), level = DeprecationLevel.ERROR) + val typeLink: String? get() = throw NotImplementedError() + + @Deprecated("Use [rels] instead.", ReplaceWith("rels"), level = DeprecationLevel.ERROR) val rel: List<String> get() = rels.toList() } @@ -194,84 +257,97 @@ data class Link( /** * Returns the first [Link] with the given [href], or null if not found. */ -fun List<Link>.indexOfFirstWithHref(href: String): Int? = - indexOfFirst { it.href == href } +public fun List<Link>.indexOfFirstWithHref(href: Url): Int? = + indexOfFirst { it.url() == href } .takeUnless { it == -1 } /** * Finds the first link matching the given HREF. */ -fun List<Link>.firstWithHref(href: String): Link? = firstOrNull { it.href == href } +public fun List<Link>.firstWithHref(href: Url): Link? = firstOrNull { it.url() == href } /** * Finds the first link with the given relation. */ -fun List<Link>.firstWithRel(rel: String): Link? = firstOrNull { it.rels.contains(rel) } +public fun List<Link>.firstWithRel(rel: String): Link? = firstOrNull { it.rels.contains(rel) } /** * Finds all the links with the given relation. */ -fun List<Link>.filterByRel(rel: String): List<Link> = filter { it.rels.contains(rel) } +public fun List<Link>.filterByRel(rel: String): List<Link> = filter { it.rels.contains(rel) } /** * Finds the first link matching the given media type. */ -fun List<Link>.firstWithMediaType(mediaType: MediaType): Link? = firstOrNull { - it.mediaType.matches(mediaType) +public fun List<Link>.firstWithMediaType(mediaType: MediaType): Link? = firstOrNull { + mediaType.matches(it.mediaType) } /** * Finds all the links matching the given media type. */ -fun List<Link>.filterByMediaType(mediaType: MediaType): List<Link> = filter { - it.mediaType.matches(mediaType) +public fun List<Link>.filterByMediaType(mediaType: MediaType): List<Link> = filter { + mediaType.matches(it.mediaType) } /** * Finds all the links matching any of the given media types. */ -fun List<Link>.filterByMediaTypes(mediaTypes: List<MediaType>): List<Link> = filter { - mediaTypes.any { mediaType -> mediaType.matches(it.type) } +public fun List<Link>.filterByMediaTypes(mediaTypes: List<MediaType>): List<Link> = filter { + mediaTypes.any { mediaType -> mediaType.matches(it.mediaType) } } /** * Returns whether all the resources in the collection are bitmaps. */ -val List<Link>.allAreBitmap: Boolean get() = isNotEmpty() && all { - it.mediaType.isBitmap +public val List<Link>.allAreBitmap: Boolean get() = isNotEmpty() && all { + it.mediaType?.isBitmap ?: false } /** * Returns whether all the resources in the collection are audio clips. */ -val List<Link>.allAreAudio: Boolean get() = isNotEmpty() && all { - it.mediaType.isAudio +public val List<Link>.allAreAudio: Boolean get() = isNotEmpty() && all { + it.mediaType?.isAudio ?: false } /** * Returns whether all the resources in the collection are video clips. */ -val List<Link>.allAreVideo: Boolean get() = isNotEmpty() && all { - it.mediaType.isVideo +public val List<Link>.allAreVideo: Boolean get() = isNotEmpty() && all { + it.mediaType?.isVideo ?: false } /** * Returns whether all the resources in the collection are HTML documents. */ -val List<Link>.allAreHtml: Boolean get() = isNotEmpty() && all { - it.mediaType.isHtml +public val List<Link>.allAreHtml: Boolean get() = isNotEmpty() && all { + it.mediaType?.isHtml ?: false } /** * Returns whether all the resources in the collection are matching the given media type. */ -fun List<Link>.allMatchMediaType(mediaType: MediaType): Boolean = isNotEmpty() && all { +public fun List<Link>.allMatchMediaType(mediaType: MediaType): Boolean = isNotEmpty() && all { mediaType.matches(it.mediaType) } /** * Returns whether all the resources in the collection are matching any of the given media types. */ -fun List<Link>.allMatchMediaTypes(mediaTypes: List<MediaType>): Boolean = isNotEmpty() && all { +public fun List<Link>.allMatchMediaTypes(mediaTypes: List<MediaType>): Boolean = isNotEmpty() && all { mediaTypes.any { mediaType -> mediaType.matches(it.mediaType) } } + +/** + * Returns a list of `Link` after flattening the `children` and `alternates` links of the receiver. + */ +public fun List<Link>.flatten(): List<Link> { + fun Link.flatten(): List<Link> { + val children = children.flatten() + val alternates = alternates.flatten() + return listOf(this) + children.flatten() + alternates.flatten() + } + + return flatMap { it.flatten() } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/LocalizedString.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/LocalizedString.kt index 8169ebd555..bee448bab2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/LocalizedString.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/LocalizedString.kt @@ -22,17 +22,17 @@ import org.readium.r2.shared.util.logging.log * Represents a string with multiple [translations] indexed by a BCP 47 language tag. */ @Parcelize -data class LocalizedString(val translations: Map<String?, Translation> = emptyMap()) : JSONable, Parcelable { +public data class LocalizedString(val translations: Map<String?, Translation> = emptyMap()) : JSONable, Parcelable { @Parcelize - data class Translation( + public data class Translation( val string: String ) : Parcelable /** * Shortcut to create a [LocalizedString] using a single string for a given language. */ - constructor(value: String, lang: String? = null) : this( + public constructor(value: String, lang: String? = null) : this( translations = mapOf(lang to Translation(string = value)) ) @@ -58,7 +58,7 @@ data class LocalizedString(val translations: Map<String?, Translation> = emptyMa * 3. on the English language * 4. the first translation found */ - fun getOrFallback(language: String?): Translation? { + public fun getOrFallback(language: String?): Translation? { return translations[language] ?: translations[Locale.getDefault().toLanguageTag()] ?: translations[null] @@ -71,42 +71,42 @@ data class LocalizedString(val translations: Map<String?, Translation> = emptyMa * Returns a new [LocalizedString] after adding (or replacing) the translation with the given * [language]. */ - fun copyWithString(language: String?, string: String): LocalizedString = + public fun copyWithString(language: String?, string: String): LocalizedString = copy(translations = translations + Pair(language, Translation(string = string))) /** * Returns a new [LocalizedString] after applying the [transform] function to each language. */ - fun mapLanguages(transform: (Map.Entry<String?, Translation>) -> String?): LocalizedString = + public fun mapLanguages(transform: (Map.Entry<String?, Translation>) -> String?): LocalizedString = copy(translations = translations.mapKeys(transform)) /** * Returns a new [LocalizedString] after applying the [transform] function to each translation. */ - fun mapTranslations(transform: (Map.Entry<String?, Translation>) -> Translation): LocalizedString = + public fun mapTranslations(transform: (Map.Entry<String?, Translation>) -> Translation): LocalizedString = copy(translations = translations.mapValues(transform)) /** * Serializes a [LocalizedString] to its RWPM JSON representation. */ - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { for ((language, translation) in translations) { put(language ?: UNDEFINED_LANGUAGE, translation.string) } } - companion object { + public companion object { /** * BCP-47 tag for an undefined language. */ - const val UNDEFINED_LANGUAGE = "und" + public const val UNDEFINED_LANGUAGE: String = "und" /** * Shortcut to create a [LocalizedString] using a map of translations indexed by the BCP 47 * language tag. */ - fun fromStrings(strings: Map<String?, String>): LocalizedString = LocalizedString( + public fun fromStrings(strings: Map<String?, String>): LocalizedString = LocalizedString( translations = strings .mapValues { (_, string) -> Translation(string = string) } ) @@ -132,7 +132,7 @@ data class LocalizedString(val translations: Map<String?, Translation> = emptyMa * } * ] */ - fun fromJSON(json: Any?, warnings: WarningLogger? = null): LocalizedString? { + public fun fromJSON(json: Any?, warnings: WarningLogger? = null): LocalizedString? { json ?: return null return when (json) { @@ -145,12 +145,16 @@ data class LocalizedString(val translations: Map<String?, Translation> = emptyMa } } - private fun fromJSONObject(json: JSONObject, warnings: WarningLogger?): LocalizedString? { + private fun fromJSONObject(json: JSONObject, warnings: WarningLogger?): LocalizedString { val translations = mutableMapOf<String?, Translation>() for (key in json.keys()) { val string = json.optNullableString(key) if (string == null) { - warnings?.log(LocalizedString::class.java, "invalid localized string object", json) + warnings?.log( + LocalizedString::class.java, + "invalid localized string object", + json + ) } else { translations[key] = Translation(string = string) } @@ -160,11 +164,11 @@ data class LocalizedString(val translations: Map<String?, Translation> = emptyMa } } - @Deprecated("Use [string] instead.", ReplaceWith("string")) + @Deprecated("Use [string] instead.", ReplaceWith("string"), level = DeprecationLevel.ERROR) val singleString: String? get() = string.ifEmpty { null } - @Deprecated("Use [get] instead.", ReplaceWith("()")) + @Deprecated("Use [get] instead.", ReplaceWith("()"), level = DeprecationLevel.ERROR) val multiString: Map<String?, String> get() = translations.mapValues { (_, translation) -> translation.string } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt index 9875a5877f..b32bb5dbee 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt @@ -14,11 +14,14 @@ import kotlinx.parcelize.Parcelize import kotlinx.parcelize.WriteWith import org.json.JSONArray import org.json.JSONObject +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.JSONable import org.readium.r2.shared.extensions.* import org.readium.r2.shared.toJSON +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.logging.log +import org.readium.r2.shared.util.mediatype.MediaType /** * Represents a precise location in a publication in a format that can be stored and shared. @@ -33,9 +36,9 @@ import org.readium.r2.shared.util.logging.log * https://readium.org/architecture/models/locators/ */ @Parcelize -data class Locator( - val href: String, - val type: String, +public data class Locator( + val href: Url, + val mediaType: MediaType, val title: String? = null, val locations: Locations = Locations(), val text: Text = Text() @@ -53,7 +56,7 @@ data class Locator( * @param otherLocations Additional locations for extensions. */ @Parcelize - data class Locations( + public data class Locations( val fragments: List<String> = emptyList(), val progression: Double? = null, val position: Int? = null, @@ -61,7 +64,7 @@ data class Locator( val otherLocations: @WriteWith<JSONParceler> Map<String, Any> = emptyMap() ) : JSONable, Parcelable { - override fun toJSON() = JSONObject(otherLocations).apply { + override fun toJSON(): JSONObject = JSONObject(otherLocations).apply { putIfNotEmpty("fragments", fragments) put("progression", progression) put("position", position) @@ -72,11 +75,13 @@ data class Locator( * Syntactic sugar to access the [otherLocations] values by subscripting [Locations] directly. * `locations["cssSelector"] == locations.otherLocations["cssSelector"]` */ - operator fun get(key: String): Any? = otherLocations[key] + public operator fun get(key: String): Any? = otherLocations[key] - companion object { + public companion object { - fun fromJSON(json: JSONObject?): Locations { + public fun fromJSON( + json: JSONObject? + ): Locations { val fragments = json?.optStringsFromArrayOrSingle("fragments", remove = true)?.takeIf { it.isNotEmpty() } ?: json?.optStringsFromArrayOrSingle("fragment", remove = true) ?: emptyList() @@ -100,7 +105,11 @@ data class Locator( } } - @Deprecated("Renamed to [fragments]", ReplaceWith("fragments")) + @Deprecated( + "Renamed to [fragments]", + ReplaceWith("fragments"), + level = DeprecationLevel.ERROR + ) val fragment: String? get() = fragments.firstOrNull() } @@ -116,30 +125,35 @@ data class Locator( * @param after The text after the locator. */ @Parcelize - data class Text( + public data class Text( val before: String? = null, val highlight: String? = null, val after: String? = null ) : JSONable, Parcelable { - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { put("before", before) put("highlight", highlight) put("after", after) } - fun substring(range: IntRange): Text { - highlight ?: return this + public fun substring(range: IntRange): Text { + if (highlight.isNullOrBlank()) return this + + val fixedRange = range.first.coerceIn(0, highlight.length)..range.last.coerceIn( + 0, + highlight.length - 1 + ) return copy( - before = (before ?: "") + highlight.substring(0, range.first), - highlight = highlight.substring(range), - after = highlight.substring(range.last) + (after ?: "") + before = (before ?: "") + highlight.substring(0, fixedRange.first), + highlight = highlight.substring(fixedRange), + after = highlight.substring((fixedRange.last + 1).coerceAtMost(highlight.length)) + (after ?: "") ) } - companion object { + public companion object { - fun fromJSON(json: JSONObject?) = Text( + public fun fromJSON(json: JSONObject?): Text = Text( before = json?.optNullableString("before"), highlight = json?.optNullableString("highlight"), after = json?.optNullableString("after") @@ -150,13 +164,13 @@ data class Locator( /** * Shortcut to get a copy of the [Locator] with different [Locations] sub-properties. */ - fun copyWithLocations( + public fun copyWithLocations( fragments: List<String> = locations.fragments, progression: Double? = locations.progression, position: Int? = locations.position, totalProgression: Double? = locations.totalProgression, otherLocations: Map<String, Any> = locations.otherLocations - ) = copy( + ): Locator = copy( locations = locations.copy( fragments = fragments, progression = progression, @@ -166,17 +180,65 @@ data class Locator( ) ) - override fun toJSON() = JSONObject().apply { - put("href", href) - put("type", type) + override fun toJSON(): JSONObject = JSONObject().apply { + put("href", href.toString()) + put("type", mediaType.toString()) put("title", title) putIfNotEmpty("locations", locations) putIfNotEmpty("text", text) } - companion object { + @Suppress("UNUSED_PARAMETER") + @Deprecated( + "Provide a `Url` and `MediaType` instances.", + ReplaceWith("Locator(href = Url(href)!!, mediaType = MediaType(type)!!"), + level = DeprecationLevel.ERROR + ) + public constructor( + href: String, + type: String, + title: String? = null, + locations: Locations = Locations(), + text: Text = Text() + ) : this(Url("#")!!, MediaType.BINARY) + + @Deprecated( + "Use [mediaType.toString()] instead", + ReplaceWith("mediaType.toString()"), + level = DeprecationLevel.ERROR + ) + public val type: String get() = throw NotImplementedError() + + public companion object { - fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Locator? { + /** + * Creates a [Locator] from its JSON representation. + */ + public fun fromJSON( + json: JSONObject?, + warnings: WarningLogger? = null + ): Locator? = + fromJSON(json, warnings, withLegacyHref = false) + + /** + * Creates a [Locator] from its legacy JSON representation. + * + * Only use this API when you are upgrading to Readium 3.x and migrating the [Locator] + * objects stored in your database. See the migration guide for more information. + */ + @DelicateReadiumApi + public fun fromLegacyJSON( + json: JSONObject?, + warnings: WarningLogger? = null + ): Locator? = + fromJSON(json, warnings, withLegacyHref = true) + + @OptIn(DelicateReadiumApi::class) + private fun fromJSON( + json: JSONObject?, + warnings: WarningLogger? = null, + withLegacyHref: Boolean = false + ): Locator? { val href = json?.optNullableString("href") val type = json?.optNullableString("type") if (href == null || type == null) { @@ -184,20 +246,36 @@ data class Locator( return null } + val url = ( + if (withLegacyHref) { + Url.fromLegacyHref(href) + } else { + Url(href) + } + ) ?: run { + warnings?.log(Locator::class.java, "[href] is not a valid URL", json) + return null + } + + val mediaType = MediaType(type) ?: run { + warnings?.log(Locator::class.java, "[type] is not a valid media type", json) + return null + } + return Locator( - href = href, - type = type, + href = url, + mediaType = mediaType, title = json.optNullableString("title"), locations = Locations.fromJSON(json.optJSONObject("locations")), text = Text.fromJSON(json.optJSONObject("text")) ) } - fun fromJSONArray( + public fun fromJSONArray( json: JSONArray?, warnings: WarningLogger? = null ): List<Locator> { - return json.parseObjects { Locator.fromJSON(it as? JSONObject, warnings) } + return json.parseObjects { fromJSON(it as? JSONObject, warnings) } } } } @@ -205,18 +283,11 @@ data class Locator( /** * Creates a [Locator] from a reading order [Link]. */ -@Deprecated("This may create an incorrect `Locator` if the link `type` is missing. Use `publication.locatorFromLink()` instead.") -fun Link.toLocator(): Locator { - val components = href.split("#", limit = 2) - return Locator( - href = components.firstOrNull() ?: href, - type = type ?: "", - title = title, - locations = Locator.Locations( - fragments = listOfNotNull(components.getOrNull(1)) - ) - ) -} +@Deprecated( + "This may create an incorrect `Locator` if the link `type` is missing. Use `publication.locatorFromLink()` instead.", + level = DeprecationLevel.ERROR +) +public fun Link.toLocator(): Locator = throw NotImplementedError() /** * Represents a sequential list of `Locator` objects. @@ -224,10 +295,10 @@ fun Link.toLocator(): Locator { * For example, a search result or a list of positions. */ @Parcelize -data class LocatorCollection( +public data class LocatorCollection( val metadata: Metadata = Metadata(), val links: List<Link> = emptyList(), - val locators: List<Locator> = emptyList(), + val locators: List<Locator> = emptyList() ) : JSONable, Parcelable { /** @@ -236,10 +307,10 @@ data class LocatorCollection( * @param numberOfItems Indicates the total number of locators in the collection. */ @Parcelize - data class Metadata( + public data class Metadata( val localizedTitle: LocalizedString? = null, val numberOfItems: Int? = null, - val otherMetadata: @WriteWith<JSONParceler> Map<String, Any> = mapOf(), + val otherMetadata: @WriteWith<JSONParceler> Map<String, Any> = mapOf() ) : JSONable, Parcelable { /** @@ -247,14 +318,14 @@ data class LocatorCollection( */ val title: String? get() = localizedTitle?.string - override fun toJSON() = JSONObject(otherMetadata).apply { + override fun toJSON(): JSONObject = JSONObject(otherMetadata).apply { putIfNotEmpty("title", localizedTitle) putOpt("numberOfItems", numberOfItems) } - companion object { + public companion object { - fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Metadata { + public fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Metadata { json ?: return Metadata() val localizedTitle = LocalizedString.fromJSON(json.remove("title"), warnings) @@ -269,19 +340,28 @@ data class LocatorCollection( } } - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { putIfNotEmpty("metadata", metadata.toJSON()) putIfNotEmpty("links", links.toJSON()) put("locators", locators.toJSON()) } - companion object { + public companion object { - fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): LocatorCollection { + public fun fromJSON( + json: JSONObject?, + warnings: WarningLogger? = null + ): LocatorCollection { return LocatorCollection( metadata = Metadata.fromJSON(json?.optJSONObject("metadata"), warnings), - links = Link.fromJSONArray(json?.optJSONArray("links"), warnings = warnings), - locators = Locator.fromJSONArray(json?.optJSONArray("locators"), warnings), + links = Link.fromJSONArray( + json?.optJSONArray("links"), + warnings = warnings + ), + locators = Locator.fromJSONArray( + json?.optJSONArray("locators"), + warnings + ) ) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Manifest.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Manifest.kt index b872994de2..ccadeeb927 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Manifest.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Manifest.kt @@ -9,17 +9,14 @@ package org.readium.r2.shared.publication -import android.os.Parcelable -import kotlinx.parcelize.Parcelize import org.json.JSONArray import org.json.JSONObject import org.readium.r2.shared.JSONable import org.readium.r2.shared.extensions.optStringsFromArrayOrSingle import org.readium.r2.shared.extensions.putIfNotEmpty -import org.readium.r2.shared.extensions.removeLastComponent -import org.readium.r2.shared.extensions.toUrlOrNull import org.readium.r2.shared.toJSON -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.logging.ConsoleWarningLogger import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.logging.log import org.readium.r2.shared.util.mediatype.MediaType @@ -27,24 +24,20 @@ import org.readium.r2.shared.util.mediatype.MediaType /** * Holds the metadata of a Readium publication, as described in the Readium Web Publication Manifest. */ -@Parcelize -data class Manifest( +public data class Manifest( val context: List<String> = emptyList(), val metadata: Metadata, - // FIXME: Currently Readium requires to set the [Link] with [rel] "self" when adding it to the - // server. So we need to keep [links] as a mutable property. - var links: List<Link> = emptyList(), + val links: List<Link> = emptyList(), val readingOrder: List<Link> = emptyList(), val resources: List<Link> = emptyList(), val tableOfContents: List<Link> = emptyList(), val subcollections: Map<String, List<PublicationCollection>> = emptyMap() - -) : JSONable, Parcelable { +) : JSONable { /** * Returns whether this manifest conforms to the given Readium Web Publication Profile. */ - fun conformsTo(profile: Publication.Profile): Boolean { + public fun conformsTo(profile: Publication.Profile): Boolean { if (readingOrder.isEmpty()) { return false } @@ -70,12 +63,12 @@ data class Manifest( * If there's no match, tries again after removing any query parameter and anchor from the * given [href]. */ - fun linkWithHref(href: String): Link? { - fun List<Link>.deepLinkWithHref(href: String): Link? { + public fun linkWithHref(href: Url): Link? { + fun List<Link>.deepLinkWithHref(href: Url): Link? { for (l in this) { - if (l.href == href) + if (l.url() == href) { return l - else { + } else { l.alternates.deepLinkWithHref(href)?.let { return it } l.children.deepLinkWithHref(href)?.let { return it } } @@ -83,20 +76,20 @@ data class Manifest( return null } - fun find(href: String): Link? { + fun find(href: Url): Link? { return readingOrder.deepLinkWithHref(href) ?: resources.deepLinkWithHref(href) ?: links.deepLinkWithHref(href) } return find(href) - ?: find(href.takeWhile { it !in "#?" }) + ?: find(href.removeFragment().removeQuery()) } /** * Finds the first [Link] with the given relation in the manifest's links. */ - fun linkWithRel(rel: String): Link? = + public fun linkWithRel(rel: String): Link? = readingOrder.firstWithRel(rel) ?: resources.firstWithRel(rel) ?: links.firstWithRel(rel) @@ -104,7 +97,7 @@ data class Manifest( /** * Finds all [Link]s having the given [rel] in the manifest's links. */ - fun linksWithRel(rel: String): List<Link> = + public fun linksWithRel(rel: String): List<Link> = (readingOrder + resources + links).filterByRel(rel) /** @@ -112,16 +105,17 @@ data class Manifest( * * Returns null if the resource is not found in this manifest. */ - fun locatorFromLink(link: Link): Locator? { - val components = link.href.split("#", limit = 2) - val href = components.firstOrNull() ?: link.href - val resourceLink = linkWithHref(href) ?: return null - val type = resourceLink.type ?: return null - val fragment = components.getOrNull(1) + public fun locatorFromLink(link: Link): Locator? { + var url = link.url() + val fragment = url.fragment + url = url.removeFragment() + + val resourceLink = linkWithHref(url) ?: return null + val mediaType = resourceLink.mediaType ?: return null return Locator( - href = href, - type = type, + href = url, + mediaType = mediaType, title = resourceLink.title ?: link.title, locations = Locator.Locations( fragments = listOfNotNull(fragment), @@ -133,7 +127,7 @@ data class Manifest( /** * Serializes a [Publication] to its RWPM JSON representation. */ - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { putIfNotEmpty("@context", context) put("metadata", metadata.toJSON()) put("links", links.toJSON()) @@ -148,7 +142,7 @@ data class Manifest( */ override fun toString(): String = toJSON().toString().replace("\\/", "/") - companion object { + public companion object { /** * Parses a [Manifest] from its RWPM JSON representation. @@ -157,50 +151,52 @@ data class Manifest( * https://readium.org/webpub-manifest/ * https://readium.org/webpub-manifest/schema/publication.schema.json */ - fun fromJSON( + public fun fromJSON( json: JSONObject?, - packaged: Boolean = false, - warnings: WarningLogger? = null + warnings: WarningLogger? = ConsoleWarningLogger() ): Manifest? { json ?: return null - val baseUrl = - if (packaged) - "/" - else - Link.fromJSONArray(json.optJSONArray("links"), warnings = warnings) - .firstWithRel("self") - ?.href - ?.toUrlOrNull() - ?.removeLastComponent() - ?.toString() - ?: "/" - - val normalizeHref = { href: String -> Href(href, baseUrl).string } - val context = json.optStringsFromArrayOrSingle("@context", remove = true) - val metadata = Metadata.fromJSON(json.remove("metadata") as? JSONObject, normalizeHref, warnings) + val metadata = Metadata.fromJSON( + json.remove("metadata") as? JSONObject, + warnings + ) if (metadata == null) { warnings?.log(Manifest::class.java, "[metadata] is required", json) return null } - val links = Link.fromJSONArray(json.remove("links") as? JSONArray, normalizeHref, warnings) - .map { if (!packaged || "self" !in it.rels) it else it.copy(rels = it.rels - "self" + "alternate") } + val links = Link.fromJSONArray( + json.remove("links") as? JSONArray, + warnings + ) // [readingOrder] used to be [spine], so we parse [spine] as a fallback. val readingOrderJSON = (json.remove("readingOrder") ?: json.remove("spine")) as? JSONArray - val readingOrder = Link.fromJSONArray(readingOrderJSON, normalizeHref, warnings) - .filter { it.type != null } + val readingOrder = Link.fromJSONArray( + readingOrderJSON, + warnings + ) + .filter { it.mediaType != null } - val resources = Link.fromJSONArray(json.remove("resources") as? JSONArray, normalizeHref, warnings) - .filter { it.type != null } + val resources = Link.fromJSONArray( + json.remove("resources") as? JSONArray, + warnings + ) + .filter { it.mediaType != null } - val tableOfContents = Link.fromJSONArray(json.remove("toc") as? JSONArray, normalizeHref, warnings) + val tableOfContents = Link.fromJSONArray( + json.remove("toc") as? JSONArray, + warnings + ) // Parses subcollections from the remaining JSON properties. - val subcollections = PublicationCollection.collectionsFromJSON(json, normalizeHref, warnings) + val subcollections = PublicationCollection.collectionsFromJSON( + json, + warnings + ) return Manifest( context = context, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/ManifestTransformer.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/ManifestTransformer.kt new file mode 100644 index 0000000000..4709d67501 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/ManifestTransformer.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.publication + +import org.readium.r2.shared.ExperimentalReadiumApi + +/** + * Transforms a manifest's components. + */ +@ExperimentalReadiumApi +public interface ManifestTransformer { + public fun transform(manifest: Manifest): Manifest = manifest + public fun transform(metadata: Metadata): Metadata = metadata + public fun transform(link: Link): Link = link + public fun transform(href: Href): Href = href +} + +/** + * Creates a copy of the receiver [Manifest], applying the given [transformer] to each component. + */ +@ExperimentalReadiumApi +public fun Manifest.copy(transformer: ManifestTransformer): Manifest = + transformer.transform( + copy( + metadata = metadata.copy(transformer), + links = links.copy(transformer), + readingOrder = readingOrder.copy(transformer), + resources = resources.copy(transformer), + tableOfContents = tableOfContents.copy(transformer), + subcollections = subcollections.copy(transformer) + ) + ) + +@ExperimentalReadiumApi +public fun Metadata.copy(transformer: ManifestTransformer): Metadata = + transformer.transform( + copy( + subjects = subjects.copy(transformer), + authors = authors.copy(transformer), + translators = translators.copy(transformer), + editors = editors.copy(transformer), + artists = artists.copy(transformer), + illustrators = illustrators.copy(transformer), + letterers = letterers.copy(transformer), + pencilers = pencilers.copy(transformer), + colorists = colorists.copy(transformer), + inkers = inkers.copy(transformer), + narrators = narrators.copy(transformer), + contributors = contributors.copy(transformer), + publishers = publishers.copy(transformer), + imprints = imprints.copy(transformer), + belongsTo = belongsTo.copy(transformer) + ) + ) + +@ExperimentalReadiumApi +public fun PublicationCollection.copy(transformer: ManifestTransformer): PublicationCollection = + copy( + links = links.copy(transformer), + subcollections = subcollections.copy(transformer) + ) + +@ExperimentalReadiumApi +@JvmName("copyPublicationCollections") +public fun Map<String, List<PublicationCollection>>.copy(transformer: ManifestTransformer): Map<String, List<PublicationCollection>> = + mapValues { (_, value) -> + value.map { it.copy(transformer) } + } + +@ExperimentalReadiumApi +@JvmName("copyContributorsMap") +public fun Map<String, List<Contributor>>.copy(transformer: ManifestTransformer): Map<String, List<Contributor>> = + mapValues { (_, value) -> + value.map { it.copy(transformer) } + } + +@ExperimentalReadiumApi +@JvmName("copyContributors") +public fun List<Contributor>.copy(transformer: ManifestTransformer): List<Contributor> = + map { it.copy(transformer) } + +@ExperimentalReadiumApi +public fun Contributor.copy(transformer: ManifestTransformer): Contributor = + copy( + links = links.copy(transformer) + ) + +@ExperimentalReadiumApi +@JvmName("copySubjects") +public fun List<Subject>.copy(transformer: ManifestTransformer): List<Subject> = + map { it.copy(transformer) } + +@ExperimentalReadiumApi +public fun Subject.copy(transformer: ManifestTransformer): Subject = + copy( + links = links.copy(transformer) + ) + +@ExperimentalReadiumApi +@JvmName("copyLinks") +public fun List<Link>.copy(transformer: ManifestTransformer): List<Link> = + map { it.copy(transformer) } + +@ExperimentalReadiumApi +public fun Link.copy(transformer: ManifestTransformer): Link = + transformer.transform( + copy( + href = href.copy(transformer), + alternates = alternates.copy(transformer), + children = children.copy(transformer) + ) + ) + +@ExperimentalReadiumApi +public fun Href.copy(transformer: ManifestTransformer): Href = + transformer.transform(this) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt index 6a54f8d3c6..bdb09fcaa1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt @@ -7,13 +7,21 @@ package org.readium.r2.shared.publication import android.os.Parcelable -import java.util.* +import java.util.Date +import java.util.Locale import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import kotlinx.parcelize.WriteWith import org.json.JSONObject import org.readium.r2.shared.JSONable -import org.readium.r2.shared.extensions.* +import org.readium.r2.shared.extensions.JSONParceler +import org.readium.r2.shared.extensions.iso8601ToDate +import org.readium.r2.shared.extensions.optPositiveDouble +import org.readium.r2.shared.extensions.optPositiveInt +import org.readium.r2.shared.extensions.optStringsFromArrayOrSingle +import org.readium.r2.shared.extensions.putIfNotEmpty +import org.readium.r2.shared.extensions.toIso8601String +import org.readium.r2.shared.extensions.toMap import org.readium.r2.shared.publication.presentation.Presentation import org.readium.r2.shared.publication.presentation.presentation import org.readium.r2.shared.util.Language @@ -29,11 +37,11 @@ import org.readium.r2.shared.util.logging.log * @param otherMetadata Additional metadata for extensions, as a JSON dictionary. */ @Parcelize -data class Metadata( +public data class Metadata( val identifier: String? = null, // URI val type: String? = null, // URI (@type) val conformsTo: Set<Publication.Profile> = emptySet(), - val localizedTitle: LocalizedString, + val localizedTitle: LocalizedString? = null, val localizedSubtitle: LocalizedString? = null, val localizedSortAs: LocalizedString? = null, val modified: Date? = null, @@ -54,7 +62,7 @@ data class Metadata( val contributors: List<Contributor> = emptyList(), val publishers: List<Contributor> = emptyList(), val imprints: List<Contributor> = emptyList(), - val readingProgression: ReadingProgression = ReadingProgression.AUTO, + val readingProgression: ReadingProgression? = null, val description: String? = null, val duration: Double? = null, val numberOfPages: Int? = null, @@ -62,11 +70,11 @@ data class Metadata( val otherMetadata: @WriteWith<JSONParceler> Map<String, Any> = mapOf() ) : JSONable, Parcelable { - constructor( + public constructor( identifier: String? = null, // URI type: String? = null, // URI (@type) conformsTo: Set<Publication.Profile> = emptySet(), - localizedTitle: LocalizedString, + localizedTitle: LocalizedString? = null, localizedSubtitle: LocalizedString? = null, localizedSortAs: LocalizedString? = null, modified: Date? = null, @@ -87,7 +95,7 @@ data class Metadata( contributors: List<Contributor> = emptyList(), publishers: List<Contributor> = emptyList(), imprints: List<Contributor> = emptyList(), - readingProgression: ReadingProgression = ReadingProgression.AUTO, + readingProgression: ReadingProgression? = null, description: String? = null, duration: Double? = null, numberOfPages: Int? = null, @@ -141,7 +149,7 @@ data class Metadata( /** * Returns the default translation string for the [localizedTitle]. */ - val title: String get() = localizedTitle.string + val title: String? get() = localizedTitle?.string /** * Returns the default translation string for the [localizedSortAs]. @@ -168,10 +176,13 @@ data class Metadata( * * See this issue for more details: https://github.com/readium/architecture/issues/113 */ - @Deprecated("You should resolve [ReadingProgression.AUTO] by yourself.", level = DeprecationLevel.WARNING) + @Deprecated( + "You should resolve [ReadingProgression.AUTO] by yourself.", + level = DeprecationLevel.WARNING + ) @IgnoredOnParcel val effectiveReadingProgression: ReadingProgression get() { - if (readingProgression != ReadingProgression.AUTO) { + if (readingProgression != null) { return readingProgression } @@ -197,7 +208,7 @@ data class Metadata( /** * Serializes a [Metadata] to its RWPM JSON representation. */ - override fun toJSON() = JSONObject(otherMetadata).apply { + override fun toJSON(): JSONObject = JSONObject(otherMetadata).apply { put("identifier", identifier) put("@type", type) putIfNotEmpty("conformsTo", conformsTo.map { it.uri }) @@ -222,7 +233,7 @@ data class Metadata( putIfNotEmpty("contributor", contributors) putIfNotEmpty("publisher", publishers) putIfNotEmpty("imprint", imprints) - put("readingProgression", readingProgression.value) + put("readingProgression", readingProgression?.value ?: "auto") put("description", description) put("duration", duration) put("numberOfPages", numberOfPages) @@ -233,18 +244,17 @@ data class Metadata( * Syntactic sugar to access the [otherMetadata] values by subscripting [Metadata] directly. * `metadata["layout"] == metadata.otherMetadata["layout"]` */ - operator fun get(key: String): Any? = otherMetadata[key] + public operator fun get(key: String): Any? = otherMetadata[key] - companion object { + public companion object { /** * Parses a [Metadata] from its RWPM JSON representation. * * If the metadata can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSON( + public fun fromJSON( json: JSONObject?, - normalizeHref: LinkHrefNormalizer = LinkHrefNormalizerIdentity, warnings: WarningLogger? = null ): Metadata? { json ?: return null @@ -265,21 +275,65 @@ data class Metadata( val accessibility = Accessibility.fromJSON(json.remove("accessibility")) val languages = json.optStringsFromArrayOrSingle("language", remove = true) val localizedSortAs = LocalizedString.fromJSON(json.remove("sortAs"), warnings) - val subjects = Subject.fromJSONArray(json.remove("subject"), normalizeHref, warnings) - val authors = Contributor.fromJSONArray(json.remove("author"), normalizeHref, warnings) - val translators = Contributor.fromJSONArray(json.remove("translator"), normalizeHref, warnings) - val editors = Contributor.fromJSONArray(json.remove("editor"), normalizeHref, warnings) - val artists = Contributor.fromJSONArray(json.remove("artist"), normalizeHref, warnings) - val illustrators = Contributor.fromJSONArray(json.remove("illustrator"), normalizeHref, warnings) - val letterers = Contributor.fromJSONArray(json.remove("letterer"), normalizeHref, warnings) - val pencilers = Contributor.fromJSONArray(json.remove("penciler"), normalizeHref, warnings) - val colorists = Contributor.fromJSONArray(json.remove("colorist"), normalizeHref, warnings) - val inkers = Contributor.fromJSONArray(json.remove("inker"), normalizeHref, warnings) - val narrators = Contributor.fromJSONArray(json.remove("narrator"), normalizeHref, warnings) - val contributors = Contributor.fromJSONArray(json.remove("contributor"), normalizeHref, warnings) - val publishers = Contributor.fromJSONArray(json.remove("publisher"), normalizeHref, warnings) - val imprints = Contributor.fromJSONArray(json.remove("imprint"), normalizeHref, warnings) - val readingProgression = ReadingProgression(json.remove("readingProgression") as? String) + val subjects = Subject.fromJSONArray( + json.remove("subject"), + warnings + ) + val authors = Contributor.fromJSONArray( + json.remove("author"), + warnings + ) + val translators = Contributor.fromJSONArray( + json.remove("translator"), + warnings + ) + val editors = Contributor.fromJSONArray( + json.remove("editor"), + warnings + ) + val artists = Contributor.fromJSONArray( + json.remove("artist"), + warnings + ) + val illustrators = Contributor.fromJSONArray( + json.remove("illustrator"), + warnings + ) + val letterers = Contributor.fromJSONArray( + json.remove("letterer"), + warnings + ) + val pencilers = Contributor.fromJSONArray( + json.remove("penciler"), + warnings + ) + val colorists = Contributor.fromJSONArray( + json.remove("colorist"), + warnings + ) + val inkers = Contributor.fromJSONArray( + json.remove("inker"), + warnings + ) + val narrators = Contributor.fromJSONArray( + json.remove("narrator"), + warnings + ) + val contributors = Contributor.fromJSONArray( + json.remove("contributor"), + warnings + ) + val publishers = Contributor.fromJSONArray( + json.remove("publisher"), + warnings + ) + val imprints = Contributor.fromJSONArray( + json.remove("imprint"), + warnings + ) + val readingProgression = ReadingProgression( + json.remove("readingProgression") as? String + ) val description = json.remove("description") as? String val duration = json.optPositiveDouble("duration", remove = true) val numberOfPages = json.optPositiveInt("numberOfPages", remove = true) @@ -294,7 +348,10 @@ data class Metadata( for (key in belongsToJson.keys()) { if (!belongsToJson.isNull(key)) { val value = belongsToJson.get(key) - belongsTo[key] = Collection.fromJSONArray(value, normalizeHref, warnings) + belongsTo[key] = Collection.fromJSONArray( + value, + warnings + ) } } @@ -333,36 +390,62 @@ data class Metadata( } } - @Deprecated("Use [type] instead", ReplaceWith("type")) + @Deprecated("Use [type] instead", ReplaceWith("type"), level = DeprecationLevel.ERROR) val rdfType: String? get() = type - @Deprecated("Use [localizeTitle] instead.", ReplaceWith("localizedTitle")) + @Deprecated( + "Use [localizeTitle] instead.", + ReplaceWith("localizedTitle"), + level = DeprecationLevel.ERROR + ) val multilanguageTitle: LocalizedString? get() = localizedTitle - @Deprecated("Use [localizedTitle.get] instead", ReplaceWith("localizedTitle.translationForLanguage(key)?.string")) - fun titleForLang(key: String): String? = - localizedTitle.getOrFallback(key)?.string + @Deprecated( + "Use [localizedTitle.get] instead", + ReplaceWith("localizedTitle.translationForLanguage(key)?.string"), + level = DeprecationLevel.ERROR + ) + public fun titleForLang(key: String): String? = + localizedTitle?.getOrFallback(key)?.string - @Deprecated("Use [readingProgression] instead.", ReplaceWith("readingProgression")) + @Deprecated( + "Use [readingProgression] instead.", + ReplaceWith("readingProgression"), + level = DeprecationLevel.ERROR + ) val direction: String - get() = readingProgression.value + get() { + throw NotImplementedError() + } - @Deprecated("Use [published] instead", ReplaceWith("published?.toIso8601String()")) + @Deprecated( + "Use [published] instead", + ReplaceWith("published?.toIso8601String()"), + level = DeprecationLevel.ERROR + ) val publicationDate: String? get() = published?.toIso8601String() - @Deprecated("Use [presentation] instead", ReplaceWith("presentation", "org.readium.r2.shared.publication.presentation.presentation")) + @Deprecated( + "Use [presentation] instead", + ReplaceWith("presentation", "org.readium.r2.shared.publication.presentation.presentation"), + level = DeprecationLevel.ERROR + ) val rendition: Presentation get() = presentation - @Deprecated("Access from [otherMetadata] instead", ReplaceWith("otherMetadata[\"source\"] as? String")) + @Deprecated( + "Access from [otherMetadata] instead", + ReplaceWith("otherMetadata[\"source\"] as? String"), + level = DeprecationLevel.ERROR + ) val source: String? get() = otherMetadata["source"] as? String - @Deprecated("Not used anymore", ReplaceWith("null")) + @Deprecated("Not used anymore", ReplaceWith("null"), level = DeprecationLevel.ERROR) val rights: String? get() = null - @Deprecated("Renamed into [toJSON]", ReplaceWith("toJSON()")) - fun writeJSON(): JSONObject = toJSON() + @Deprecated("Renamed into [toJSON]", ReplaceWith("toJSON()"), level = DeprecationLevel.ERROR) + public fun writeJSON(): JSONObject = toJSON() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt index 3e97a2e932..8386db5121 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt @@ -24,36 +24,42 @@ import org.readium.r2.shared.extensions.toMap * https://readium.org/webpub-manifest/schema/link.schema.json */ @Parcelize -data class Properties( +public data class Properties( val otherProperties: @WriteWith<JSONParceler> Map<String, Any> = emptyMap() ) : JSONable, Parcelable { /** * Serializes a [Properties] to its RWPM JSON representation. */ - override fun toJSON() = JSONObject(otherProperties) + override fun toJSON(): JSONObject = JSONObject(otherProperties) /** * Makes a copy of this [Properties] after merging in the given additional other [properties]. */ - fun add(properties: Map<String, Any>): Properties { + public fun add(properties: Map<String, Any>): Properties { val otherProperties = otherProperties.toMutableMap() otherProperties.putAll(properties) return copy(otherProperties = otherProperties) } + /** + * Makes a copy of this [Properties] after merging in the given additional other [properties]. + */ + public fun add(properties: Properties): Properties = + add(properties.otherProperties) + /** * Syntactic sugar to access the [otherProperties] values by subscripting [Properties] directly. * `properties["price"] == properties.otherProperties["price"]` */ - operator fun get(key: String): Any? = otherProperties[key] + public operator fun get(key: String): Any? = otherProperties[key] - companion object { + public companion object { /** * Creates a [Properties] from its RWPM JSON representation. */ - fun fromJSON(json: JSONObject?) = Properties( + public fun fromJSON(json: JSONObject?): Properties = Properties( otherProperties = json?.toMap() ?: emptyMap() ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index c3ece07f08..39a143c47a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -9,24 +9,13 @@ package org.readium.r2.shared.publication -import android.net.Uri import android.os.Parcelable -import androidx.annotation.StringRes import java.net.URL -import java.net.URLEncoder import kotlin.reflect.KClass -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.json.JSONObject import org.readium.r2.shared.* -import org.readium.r2.shared.BuildConfig.DEBUG import org.readium.r2.shared.extensions.* -import org.readium.r2.shared.extensions.removeLastComponent -import org.readium.r2.shared.fetcher.EmptyFetcher -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.fetcher.Resource import org.readium.r2.shared.publication.epub.listOfAudioClips import org.readium.r2.shared.publication.epub.listOfVideoClips import org.readium.r2.shared.publication.services.CacheService @@ -35,11 +24,14 @@ import org.readium.r2.shared.publication.services.CoverService import org.readium.r2.shared.publication.services.DefaultLocatorService import org.readium.r2.shared.publication.services.LocatorService import org.readium.r2.shared.publication.services.PositionsService +import org.readium.r2.shared.publication.services.ResourceCoverService import org.readium.r2.shared.publication.services.content.ContentService import org.readium.r2.shared.publication.services.search.SearchService import org.readium.r2.shared.util.Closeable -import org.readium.r2.shared.util.Ref -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.EmptyContainer +import org.readium.r2.shared.util.resource.Resource internal typealias ServiceFactory = (Publication.Service.Context) -> Publication.Service? @@ -51,141 +43,166 @@ internal typealias ServiceFactory = (Publication.Service.Context) -> Publication * We can't use publication.metadata.identifier directly because it might be null or not really * unique in the reading app. That's why sometimes we require an ID provided by the app. */ -typealias PublicationId = String +public typealias PublicationId = String /** * The Publication shared model is the entry-point for all the metadata and services * related to a Readium publication. * * @param manifest The manifest holding the publication metadata extracted from the publication file. - * @param fetcher The underlying fetcher used to read publication resources. + * @param container The underlying container used to read publication resources. * The default implementation returns Resource.Exception.NotFound for all HREFs. * @param servicesBuilder Holds the list of service factories used to create the instances of * Publication.Service attached to this Publication. */ -class Publication( - manifest: Manifest, - private val fetcher: Fetcher = EmptyFetcher(), +public class Publication( + public val manifest: Manifest, + private val container: Container<Resource> = EmptyContainer(), private val servicesBuilder: ServicesBuilder = ServicesBuilder(), - // FIXME: To refactor after specifying the User and Rendition Settings API - @Deprecated("Migrate to the new Settings API (see migration guide)") - var userSettingsUIPreset: MutableMap<ReadiumCSSName, Boolean> = mutableMapOf(), - @Deprecated("Migrate to the new Settings API (see migration guide)") - var cssStyle: String? = null, + @Deprecated( + "Migrate to the new Settings API (see migration guide)", + level = DeprecationLevel.ERROR + ) + public var userSettingsUIPreset: MutableMap<ReadiumCSSName, Boolean> = mutableMapOf(), + @Deprecated( + "Migrate to the new Settings API (see migration guide)", + level = DeprecationLevel.ERROR + ) + public var cssStyle: String? = null ) : PublicationServicesHolder { - private val _manifest: Manifest private val services = ListPublicationServicesHolder() init { - // We use a Ref<Publication> instead of passing directly `this` to the services to prevent - // them from using the Publication before it is fully initialized. - val pubRef = Ref<Publication>() - - services.services = servicesBuilder.build(Service.Context(pubRef, manifest, fetcher, services)) - _manifest = manifest.copy(links = manifest.links + services.services.map(Service::links).flatten()) - - pubRef.ref = this + services.services = servicesBuilder.build( + context = Service.Context(manifest, container, services) + ) } // Shortcuts to manifest properties - val context: List<String> get() = _manifest.context - val metadata: Metadata get() = _manifest.metadata - val links: List<Link> get() = _manifest.links + public val context: List<String> get() = manifest.context + public val metadata: Metadata get() = manifest.metadata + public val links: List<Link> get() = manifest.links /** Identifies a list of resources in reading order for the publication. */ - val readingOrder: List<Link> get() = _manifest.readingOrder + public val readingOrder: List<Link> get() = manifest.readingOrder /** Identifies resources that are necessary for rendering the publication. */ - val resources: List<Link> get() = _manifest.resources + public val resources: List<Link> get() = manifest.resources /** Identifies the collection that contains a table of contents. */ - val tableOfContents: List<Link> get() = _manifest.tableOfContents + public val tableOfContents: List<Link> get() = manifest.tableOfContents - val subcollections: Map<String, List<PublicationCollection>> get() = _manifest.subcollections + public val subcollections: Map<String, List<PublicationCollection>> get() = manifest.subcollections - @Deprecated("Use conformsTo() to check the kind of a publication.", level = DeprecationLevel.WARNING) - var type: TYPE = when { - metadata.type == "http://schema.org/Audiobook" || readingOrder.allAreAudio -> TYPE.AUDIO - readingOrder.allAreBitmap -> TYPE.DiViNa - else -> TYPE.WEBPUB - } + @Deprecated( + "Use conformsTo() to check the kind of a publication.", + level = DeprecationLevel.ERROR + ) + public var type: TYPE = TYPE.EPUB @Deprecated("Version is not available any more.", level = DeprecationLevel.ERROR) - var version: Double = 0.0 + public var version: Double = 0.0 /** * Returns the RWPM JSON representation for this [Publication]'s manifest, as a string. */ - val jsonManifest: String - get() = _manifest.toJSON().toString().replace("\\/", "/") + @Deprecated( + "Jsonify the manifest by yourself.", + replaceWith = ReplaceWith("""manifest.toJSON().toString().replace("\\/", "/")"""), + DeprecationLevel.ERROR + ) + public val jsonManifest: String + get() = manifest.toJSON().toString().replace("\\/", "/") /** - * The URL where this publication is served, computed from the [Link] with `self` relation. + * The URL from which the publication resources are relative to, computed from the [Link] with + * `self` relation. */ + public val baseUrl: Url? + get() = links.firstWithRel("self")?.href + ?.takeUnless { it.isTemplated } + ?.resolve() - val baseUrl: URL? - get() = links.firstWithRel("self") - ?.let { it.href.toUrlOrNull()?.removeLastComponent() } + /** + * Returns the URL to the resource represented by the given [locator], relative to the + * publication's link with `self` relation. + */ + public fun url(locator: Locator): Url = + baseUrl?.let { locator.href.resolve(it) } ?: locator.href + + /** + * Returns the URL to the resource represented by the given [link], relative to the + * publication's link with `self` relation. + * + * If the link HREF is a template, the [parameters] are used to expand it according to RFC 6570. + */ + public fun url(link: Link, parameters: Map<String, String> = emptyMap()): Url = + url(link.href, parameters) + + /** + * Returns the URL to the resource represented by the given [href], relative to the + * publication's link with `self` relation. + * + * If the HREF is a template, the [parameters] are used to expand it according to RFC 6570. + */ + public fun url(href: Href, parameters: Map<String, String> = emptyMap()): Url = + href.resolve(baseUrl, parameters = parameters) /** * Returns whether this publication conforms to the given Readium Web Publication Profile. */ - fun conformsTo(profile: Profile): Boolean = - _manifest.conformsTo(profile) + public fun conformsTo(profile: Profile): Boolean = + manifest.conformsTo(profile) /** * Finds the first [Link] with the given HREF in the publication's links. * * Searches through (in order) [readingOrder], [resources] and [links] recursively following - * [alternate] and [children] links. + * `alternate` and `children` links. * * If there's no match, tries again after removing any query parameter and anchor from the * given [href]. */ - fun linkWithHref(href: String): Link? = _manifest.linkWithHref(href) + public fun linkWithHref(href: Url): Link? = manifest.linkWithHref(href) /** * Finds the first [Link] having the given [rel] in the publications's links. */ - fun linkWithRel(rel: String): Link? = _manifest.linkWithRel(rel) + public fun linkWithRel(rel: String): Link? = manifest.linkWithRel(rel) /** * Finds all [Link]s having the given [rel] in the publications's links. */ - fun linksWithRel(rel: String): List<Link> = _manifest.linksWithRel(rel) + public fun linksWithRel(rel: String): List<Link> = manifest.linksWithRel(rel) /** * Creates a new [Locator] object from a [Link] to a resource of this publication. * * Returns null if the resource is not found in this publication. */ - fun locatorFromLink(link: Link): Locator? = _manifest.locatorFromLink(link) + public fun locatorFromLink(link: Link): Locator? = manifest.locatorFromLink(link) /** * Returns the resource targeted by the given non-templated [link]. */ - fun get(link: Link): Resource { - if (DEBUG) { require(!link.templated) { "You must expand templated links before calling [Publication.get]" } } + public fun get(link: Link): Resource? = + get(link.url()) - services.services.forEach { service -> service.get(link)?.let { return it } } - return fetcher.get(link) - } + /** + * Returns the resource targeted by the given [href]. + */ + public fun get(href: Url): Resource? = + // Try first the original href and falls back to href without query and fragment. + container[href] ?: container[href.removeQuery().removeFragment()] /** * Closes any opened resource associated with the [Publication], including services. */ - @OptIn(DelicateCoroutinesApi::class) - // TODO Change this to be a suspend function - override fun close() { - GlobalScope.launch { - tryOrLog { - fetcher.close() - } - - services.close() - } + override suspend fun close() { + container.close() + services.close() } // PublicationServicesHolder @@ -196,11 +213,21 @@ class Publication( override fun <T : Service> findServices(serviceType: KClass<T>): List<T> = services.findServices(serviceType) - enum class TYPE { - EPUB, CBZ, FXL, WEBPUB, AUDIO, DiViNa + @Deprecated( + "Use Publication.Profile ", + replaceWith = ReplaceWith("Publication.Profile"), + level = DeprecationLevel.WARNING + ) + public enum class TYPE { + EPUB } - enum class EXTENSION(var value: String) { + @Deprecated( + "Use Publication.Profile ", + replaceWith = ReplaceWith("Publication.Profile"), + level = DeprecationLevel.ERROR + ) + public enum class EXTENSION(public var value: String) { EPUB(".epub"), CBZ(".cbz"), JSON(".json"), @@ -208,21 +235,15 @@ class Publication( AUDIO(".audiobook"), LCPL(".lcpl"), UNKNOWN(""); - - companion object { - fun fromString(type: String): EXTENSION? = - values().firstOrNull { it.value == type } - } } /** * Sets the URL where this [Publication]'s RWPM manifest is served. */ - fun setSelfLink(href: String) { - _manifest.links = _manifest.links.toMutableList().apply { - removeAll { it.rels.contains("self") } - add(Link(href = href, type = MediaType.READIUM_WEBPUB_MANIFEST.toString(), rels = setOf("self"))) - } + @Deprecated(message = "Not used anymore.", level = DeprecationLevel.ERROR) + @Suppress("UNUSED_PARAMETER") + public fun setSelfLink(href: String) { + throw NotImplementedError() } /** @@ -232,7 +253,7 @@ class Publication( internal fun linksWithRole(role: String): List<Link> = subcollections[role]?.firstOrNull()?.links ?: emptyList() - companion object { + public companion object { /** * Creates the base URL for a [Publication] locally served through HTTP, from the @@ -242,36 +263,38 @@ class Publication( * Server, and set in the self [Link]. Unfortunately, the self [Link] is not available * in the navigator at the moment without changing the code in reading apps. */ - @Deprecated("The HTTP server is not needed anymore (see migration guide)") - fun localBaseUrlOf(filename: String, port: Int): String { - val sanitizedFilename = filename - .removePrefix("/") - .hash(HashAlgorithm.MD5) - .let { URLEncoder.encode(it, "UTF-8") } - - return "http://127.0.0.1:$port/$sanitizedFilename" + @Suppress("UNUSED_PARAMETER") + @Deprecated( + "The HTTP server is not needed anymore (see migration guide)", + level = DeprecationLevel.ERROR + ) + public fun localBaseUrlOf(filename: String, port: Int): String { + throw NotImplementedError() } /** * Gets the absolute URL of a resource locally served through HTTP. */ - @Deprecated("The HTTP server is not needed anymore (see migration guide)") - fun localUrlOf(filename: String, port: Int, href: String): String = - localBaseUrlOf(filename, port) + href + @Suppress("UNUSED_PARAMETER") + @Deprecated( + "The HTTP server is not needed anymore (see migration guide)", + level = DeprecationLevel.ERROR + ) + public fun localUrlOf(filename: String, port: Int, href: String): String { + throw NotImplementedError() + } @Suppress("UNUSED_PARAMETER") @Deprecated( "Parse a RWPM with [Manifest::fromJSON] and then instantiate a Publication", ReplaceWith( "Manifest.fromJSON(json)", - "org.readium.r2.shared.publication.Publication", "org.readium.r2.shared.publication.Manifest" + "org.readium.r2.shared.publication.Publication", + "org.readium.r2.shared.publication.Manifest" ), level = DeprecationLevel.ERROR ) - fun fromJSON( - json: JSONObject?, - normalizeHref: LinkHrefNormalizer = LinkHrefNormalizerIdentity - ): Publication? { + public fun fromJSON(json: JSONObject?): Publication? { throw NotImplementedError() } } @@ -283,77 +306,40 @@ class Publication( * https://readium.org/webpub-manifest/profiles/ */ @Parcelize - data class Profile(val uri: String) : Parcelable { - companion object { + public data class Profile(val uri: String) : Parcelable { + public companion object { /** Profile for EPUB publications. */ - val EPUB = Profile("https://readium.org/webpub-manifest/profiles/epub") + public val EPUB: Profile = Profile("https://readium.org/webpub-manifest/profiles/epub") + /** Profile for audiobooks. */ - val AUDIOBOOK = Profile("https://readium.org/webpub-manifest/profiles/audiobook") + public val AUDIOBOOK: Profile = Profile( + "https://readium.org/webpub-manifest/profiles/audiobook" + ) + /** Profile for visual narratives (comics, manga and bandes dessinées). */ - val DIVINA = Profile("https://readium.org/webpub-manifest/profiles/divina") + public val DIVINA: Profile = Profile( + "https://readium.org/webpub-manifest/profiles/divina" + ) + /** Profile for PDF documents. */ - val PDF = Profile("https://readium.org/webpub-manifest/profiles/pdf") + public val PDF: Profile = Profile("https://readium.org/webpub-manifest/profiles/pdf") } } /** * Base interface to be implemented by all publication services. */ - interface Service : Closeable { + public interface Service : Closeable { /** * Container for the context from which a service is created. - * - * @param publication Reference to the parent publication. - * Don't store directly the referenced publication, always access it through the - * [Ref] property. The publication won't be set when the service is created or when - * calling [Service.links], but you can use it during regular service operations. If - * you need to initialize your service differently depending on the publication, use - * `manifest`. */ - class Context( - val publication: Ref<Publication>, - val manifest: Manifest, - val fetcher: Fetcher, - val services: PublicationServicesHolder + public class Context( + public val manifest: Manifest, + public val container: Container<Resource>, + public val services: PublicationServicesHolder ) - /** - * Links which will be added to [Publication.links]. - * It can be used to expose a web API for the service, through [Publication.get]. - * - * To disambiguate the href with a publication's local resources, you should use the prefix - * `/~readium/`. A custom media type or rel should be used to identify the service. - * - * You can use a templated URI to accept query parameters, e.g.: - * - * ``` - * Link( - * href = "/~readium/search{?text}", - * type = "application/vnd.readium.search+json", - * templated = true - * ) - * ``` - */ - val links: List<Link> get() = emptyList() - - /** - * A service can return a Resource to: - * - respond to a request to its web API declared in links, - * - serve additional resources on behalf of the publication, - * - replace a publication resource by its own version. - * - * Called by [Publication.get] for each request. - * - * Warning: If you need to request one of the publication resources to answer the request, - * use the [Fetcher] provided by the [Publication.Service.Context] instead of - * [Publication.get], otherwise it will trigger an infinite loop. - * - * @return The [Resource] containing the response, or null if the service doesn't recognize - * this request. - */ - fun get(link: Link): Resource? = null - /** * Closes any opened file handles, removes temporary files, etc. */ @@ -365,20 +351,20 @@ class Publication( * * Provides helpers to manipulate the list of services of a [Publication]. */ - class ServicesBuilder private constructor( + public class ServicesBuilder private constructor( private val serviceFactories: MutableMap<String, ServiceFactory> ) { @OptIn(Search::class, ExperimentalReadiumApi::class) @Suppress("UNCHECKED_CAST") - constructor( + public constructor( cache: ServiceFactory? = null, content: ServiceFactory? = null, contentProtection: ServiceFactory? = null, cover: ServiceFactory? = null, - locator: ServiceFactory? = { DefaultLocatorService(it.manifest.readingOrder, it.services) }, + locator: ServiceFactory? = null, positions: ServiceFactory? = null, - search: ServiceFactory? = null, + search: ServiceFactory? = null ) : this( mapOf( CacheService::class.java.simpleName to cache, @@ -387,21 +373,41 @@ class Publication( CoverService::class.java.simpleName to cover, LocatorService::class.java.simpleName to locator, PositionsService::class.java.simpleName to positions, - SearchService::class.java.simpleName to search, + SearchService::class.java.simpleName to search ).filterValues { it != null }.toMutableMap() as MutableMap<String, ServiceFactory> ) /** Builds the actual list of publication services to use in a Publication. */ - fun build(context: Service.Context): List<Service> = serviceFactories.values.mapNotNull { it(context) } + public fun build(context: Service.Context): List<Service> { + val serviceFactories = + buildMap<String, ServiceFactory> { + putAll(this@ServicesBuilder.serviceFactories) + + if (!containsKey(LocatorService::class.java.simpleName)) { + val factory: ServiceFactory = { + DefaultLocatorService(it.manifest.readingOrder, it.services) + } + put(LocatorService::class.java.simpleName, factory) + } + + if (!containsKey(CoverService::class.java.simpleName)) { + val factory = ResourceCoverService.createFactory() + put(CoverService::class.java.simpleName, factory) + } + } + + return serviceFactories.values + .mapNotNull { it(context) } + } /** Gets the publication service factory for the given service type. */ - operator fun <T : Service> get(serviceType: KClass<T>): ServiceFactory? { + public operator fun <T : Service> get(serviceType: KClass<T>): ServiceFactory? { val key = requireNotNull(serviceType.simpleName) return serviceFactories[key] } /** Sets the publication service factory for the given service type. */ - operator fun <T : Service> set(serviceType: KClass<T>, factory: ServiceFactory?) { + public operator fun <T : Service> set(serviceType: KClass<T>, factory: ServiceFactory?) { val key = requireNotNull(serviceType.simpleName) if (factory != null) { serviceFactories[key] = factory @@ -411,7 +417,7 @@ class Publication( } /** Removes the service factory producing the given kind of service, if any. */ - fun <T : Service> remove(serviceType: KClass<T>) { + public fun <T : Service> remove(serviceType: KClass<T>) { val key = requireNotNull(serviceType.simpleName) serviceFactories.remove(key) } @@ -420,7 +426,7 @@ class Publication( * Replaces the service factory associated with the given service type with the result of * [transform]. */ - fun <T : Service> decorate( + public fun <T : Service> decorate( serviceType: KClass<T>, transform: ((ServiceFactory)?) -> ServiceFactory ) { @@ -429,59 +435,21 @@ class Publication( } } - /** - * Errors occurring while opening a Publication. - */ - sealed class OpeningException(@StringRes userMessageId: Int, cause: Throwable? = null) : UserException(userMessageId, cause = cause) { - - /** - * The file format could not be recognized by any parser. - */ - class UnsupportedFormat(cause: Throwable? = null) : OpeningException(R.string.r2_shared_publication_opening_exception_unsupported_format, cause) - - /** - * The publication file was not found on the file system. - */ - class NotFound(cause: Throwable? = null) : OpeningException(R.string.r2_shared_publication_opening_exception_not_found, cause) - - /** - * The publication parser failed with the given underlying exception. - */ - class ParsingFailed(cause: Throwable? = null) : OpeningException(R.string.r2_shared_publication_opening_exception_parsing_failed, cause) - - /** - * We're not allowed to open the publication at all, for example because it expired. - */ - class Forbidden(cause: Throwable? = null) : OpeningException(R.string.r2_shared_publication_opening_exception_forbidden, cause) - - /** - * The publication can't be opened at the moment, for example because of a networking error. - * This error is generally temporary, so the operation may be retried or postponed. - */ - class Unavailable(cause: Throwable? = null) : OpeningException(R.string.r2_shared_publication_opening_exception_unavailable, cause) - - /** - * The provided credentials are incorrect and we can't open the publication in a - * `restricted` state (e.g. for a password-protected ZIP). - */ - object IncorrectCredentials : OpeningException(R.string.r2_shared_publication_opening_exception_incorrect_credentials) - } - /** * Builds a Publication from its components. * * A Publication's construction is distributed over the Streamer and its parsers, * so a builder is useful to pass the parts around. */ - class Builder( - var manifest: Manifest, - var fetcher: Fetcher, - var servicesBuilder: ServicesBuilder = ServicesBuilder() + public class Builder( + public var manifest: Manifest, + public var container: Container<Resource>, + public var servicesBuilder: ServicesBuilder = ServicesBuilder() ) { - fun build(): Publication = Publication( + public fun build(): Publication = Publication( manifest = manifest, - fetcher = fetcher, + container = container, servicesBuilder = servicesBuilder ) } @@ -489,119 +457,119 @@ class Publication( /** * Finds the first [Link] to the publication's cover (rel = cover). */ - @Deprecated("Use [Publication.cover] to get the cover as a [Bitmap]", ReplaceWith("cover")) - val coverLink: Link? get() = linkWithRel("cover") + @Deprecated( + "Use [Publication.cover] to get the cover as a [Bitmap]", + ReplaceWith("cover"), + level = DeprecationLevel.ERROR + ) + public val coverLink: Link? get() = linkWithRel("cover") /** * Copy the [Publication] with a different [PositionListFactory]. * The provided closure will be used to build the [PositionListFactory], with this being the * [Publication]. */ - @Suppress("DEPRECATION", "UNUSED_PARAMETER") - @Deprecated("Use [Publication.copy(serviceFactories)] instead", ReplaceWith("Publication.copy(serviceFactories = listOf(positionsServiceFactory)"), level = DeprecationLevel.ERROR) - fun copyWithPositionsFactory(createFactory: Publication.() -> PositionListFactory): Publication { + @Deprecated( + "Use [Publication.copy(serviceFactories)] instead", + ReplaceWith("Publication.copy(serviceFactories = listOf(positionsServiceFactory)"), + level = DeprecationLevel.ERROR + ) + public fun copyWithPositionsFactory(): Publication { throw NotImplementedError() } - @Deprecated("Renamed to [listOfAudioClips]", ReplaceWith("listOfAudioClips")) - val listOfAudioFiles: List<Link> = listOfAudioClips - - @Deprecated("Renamed to [listOfVideoClips]", ReplaceWith("listOfVideoClips")) - val listOfVideos: List<Link> = listOfVideoClips - - @Deprecated("Renamed to [linkWithHref]", ReplaceWith("linkWithHref(href)")) - fun resource(href: String): Link? = linkWithHref(href) - - @Deprecated("Refactored as a property", ReplaceWith("baseUrl")) - fun baseUrl(): URL? = baseUrl - - @Deprecated("Renamed [subcollections]", ReplaceWith("subcollections")) - val otherCollections: Map<String, List<PublicationCollection>> get() = subcollections - - @Deprecated("Use [setSelfLink] instead", ReplaceWith("setSelfLink")) - fun addSelfLink(endPoint: String, baseURL: URL) { - setSelfLink( - Uri.parse(baseURL.toString()) - .buildUpon() - .appendEncodedPath("$endPoint/manifest.json") - .build() - .toString() - ) + @Deprecated( + "Renamed to [listOfAudioClips]", + ReplaceWith("listOfAudioClips"), + level = DeprecationLevel.ERROR + ) + public val listOfAudioFiles: List<Link> = listOfAudioClips + + @Deprecated( + "Renamed to [listOfVideoClips]", + ReplaceWith("listOfVideoClips"), + level = DeprecationLevel.ERROR + ) + public val listOfVideos: List<Link> = listOfVideoClips + + @Deprecated( + "Renamed to [linkWithHref]", + ReplaceWith("linkWithHref(href)"), + level = DeprecationLevel.ERROR + ) + @Suppress("UNUSED_PARAMETER") + public fun resource(href: String): Link = throw NotImplementedError() + + @Deprecated("Refactored as a property", ReplaceWith("baseUrl"), level = DeprecationLevel.ERROR) + public fun baseUrl(): URL = throw NotImplementedError() + + @Deprecated( + "Renamed [subcollections]", + ReplaceWith("subcollections"), + level = DeprecationLevel.ERROR + ) + public val otherCollections: Map<String, List<PublicationCollection>> get() = subcollections + + @Deprecated( + "Use [setSelfLink] instead", + ReplaceWith("setSelfLink"), + level = DeprecationLevel.ERROR + ) + @Suppress("UNUSED_PARAMETER") + public fun addSelfLink(endPoint: String, baseURL: URL) { + throw NotImplementedError() } - /** - * Finds the first resource [Link] (asset or [readingOrder] item) at the given relative path. - */ - @Deprecated("Use [linkWithHref] instead.", ReplaceWith("linkWithHref(href)")) - fun resourceWithHref(href: String): Link? = linkWithHref(href) - - /** - * Creates a [Publication]'s [positions]. - * - * The parsers provide an implementation of this interface for each format, but a host app - * might want to use a custom factory to implement, for example, a caching mechanism or use a - * different calculation method. - */ - @Deprecated("Use a [ServiceFactory] for a [PositionsService] instead.") - interface PositionListFactory { - fun create(): List<Locator> + @Deprecated( + "Use [linkWithHref] instead.", + ReplaceWith("linkWithHref(href)"), + level = DeprecationLevel.ERROR + ) + @Suppress("UNUSED_PARAMETER") + public fun resourceWithHref(href: String): Link = throw NotImplementedError() + + @Deprecated( + "Use a [ServiceFactory] for a [PositionsService] instead.", + level = DeprecationLevel.ERROR + ) + public interface PositionListFactory { + public fun create(): List<Locator> } - /** - * Finds the first [Link] matching the given [predicate] in the publications's [Link] - * properties: [resources], [readingOrder] and [links]. - * - * Searches through (in order) [readingOrder], [resources] and [links] - * recursively following [alternate] and [children] links. - * The search order is unspecified. - */ - @Deprecated("Use [linkWithHref()] to find a link with the given HREF", replaceWith = ReplaceWith("linkWithHref"), level = DeprecationLevel.ERROR) + @Deprecated( + "Use [linkWithHref()] to find a link with the given HREF", + replaceWith = ReplaceWith("linkWithHref"), + level = DeprecationLevel.ERROR + ) @Suppress("UNUSED_PARAMETER") - fun link(predicate: (Link) -> Boolean): Link? = null - - @Deprecated("Use [jsonManifest] instead", ReplaceWith("jsonManifest")) - fun toJSON() = JSONObject(jsonManifest) - - @Deprecated("Use `metadata.effectiveReadingProgression` instead", ReplaceWith("metadata.effectiveReadingProgression"), level = DeprecationLevel.ERROR) - val contentLayout: ReadingProgression get() = metadata.effectiveReadingProgression - - @Deprecated("Use `metadata.effectiveReadingProgression` instead", ReplaceWith("metadata.effectiveReadingProgression"), level = DeprecationLevel.ERROR) + public fun link(predicate: (Link) -> Boolean): Link? = null + + @Deprecated( + "Jsonify the manifest by yourself", + ReplaceWith("manifest.toJSON()"), + level = DeprecationLevel.ERROR + ) + public fun toJSON(): JSONObject = throw NotImplementedError() + + @Deprecated( + "Use `metadata.effectiveReadingProgression` instead", + ReplaceWith("metadata.effectiveReadingProgression"), + level = DeprecationLevel.ERROR + ) + public val contentLayout: ReadingProgression get() = metadata.effectiveReadingProgression + + @Deprecated( + "Use `metadata.effectiveReadingProgression` instead", + ReplaceWith("metadata.effectiveReadingProgression"), + level = DeprecationLevel.ERROR + ) @Suppress("UNUSED_PARAMETER") - fun contentLayoutForLanguage(language: String?) = metadata.effectiveReadingProgression -} - -/** - * Holds [Publication.Service] instances for a [Publication]. - */ -interface PublicationServicesHolder { - /** - * Returns the first publication service that is an instance of [serviceType]. - */ - fun <T : Publication.Service> findService(serviceType: KClass<T>): T? - - /** - * Returns all the publication services that are instances of [serviceType]. - */ - fun <T : Publication.Service> findServices(serviceType: KClass<T>): List<T> - - /** - * Closes the publication services. - */ - fun close() -} - -internal class ListPublicationServicesHolder( - var services: List<Publication.Service> = emptyList() -) : PublicationServicesHolder { - override fun <T : Publication.Service> findService(serviceType: KClass<T>): T? = - findServices(serviceType).firstOrNull() - - override fun <T : Publication.Service> findServices(serviceType: KClass<T>): List<T> = - services.filterIsInstance(serviceType.java) - - override fun close() { - for (service in services) { - tryOrLog { service.close() } - } - } + public fun contentLayoutForLanguage(language: String?): ReadingProgression = metadata.effectiveReadingProgression + + @Deprecated( + "Renamed to `OpenError`", + replaceWith = ReplaceWith("Publication.OpenError"), + level = DeprecationLevel.ERROR + ) + public sealed class OpeningException } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/PublicationCollection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/PublicationCollection.kt index 86ade0f660..201e5ee6ee 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/PublicationCollection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/PublicationCollection.kt @@ -29,7 +29,7 @@ import org.readium.r2.shared.util.logging.log * Can be used as extension point in the Readium Web Publication Manifest. */ @Parcelize -data class PublicationCollection( +public data class PublicationCollection( val metadata: @WriteWith<JSONParceler> Map<String, Any> = emptyMap(), val links: List<Link> = emptyList(), val subcollections: Map<String, List<PublicationCollection>> = emptyMap() @@ -38,24 +38,21 @@ data class PublicationCollection( /** * Serializes a [PublicationCollection] to its RWPM JSON representation. */ - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { put("metadata", metadata) putIfNotEmpty("links", links) subcollections.appendToJSONObject(this) } - companion object { + public companion object { /** * Parses a [PublicationCollection] from its RWPM JSON representation. * * If the collection can't be parsed, a warning will be logged with [warnings]. - * The [links]' href and their children's will be normalized recursively using the - * provided [normalizeHref] closure. */ - fun fromJSON( + public fun fromJSON( json: Any?, - normalizeHref: LinkHrefNormalizer = LinkHrefNormalizerIdentity, warnings: WarningLogger? = null ): PublicationCollection? { json ?: return null @@ -67,14 +64,20 @@ data class PublicationCollection( when (json) { // Parses a sub-collection object. is JSONObject -> { - links = Link.fromJSONArray(json.remove("links") as? JSONArray, normalizeHref, warnings) + links = Link.fromJSONArray( + json.remove("links") as? JSONArray, + warnings + ) metadata = (json.remove("metadata") as? JSONObject)?.toMap() - subcollections = collectionsFromJSON(json, normalizeHref, warnings) + subcollections = collectionsFromJSON( + json, + warnings + ) } // Parses an array of links. is JSONArray -> { - links = Link.fromJSONArray(json, normalizeHref, warnings) + links = Link.fromJSONArray(json, warnings) } else -> { @@ -84,7 +87,10 @@ data class PublicationCollection( } if (links.isEmpty()) { - warnings?.log(PublicationCollection::class.java, "core collection's [links] must not be empty") + warnings?.log( + PublicationCollection::class.java, + "core collection's [links] must not be empty" + ) return null } @@ -99,12 +105,9 @@ data class PublicationCollection( * Parses a map of [PublicationCollection] indexed by their roles from its RWPM JSON representation. * * If the collection can't be parsed, a warning will be logged with [warnings]. - * The [links]' href and their children's will be normalized recursively using the - * provided [normalizeHref] closure. */ - fun collectionsFromJSON( + public fun collectionsFromJSON( json: JSONObject, - normalizeHref: LinkHrefNormalizer = LinkHrefNormalizerIdentity, warnings: WarningLogger? = null ): Map<String, List<PublicationCollection>> { val collections = mutableMapOf<String, MutableList<PublicationCollection>>() @@ -112,14 +115,19 @@ data class PublicationCollection( val subJSON = json.get(role) // Parses a list of links or a single collection object. - val collection = fromJSON(subJSON, normalizeHref, warnings) + val collection = fromJSON(subJSON, warnings) if (collection != null) { collections.getOrPut(role) { mutableListOf() }.add(collection) // Parses a list of collection objects. } else if (subJSON is JSONArray) { collections.getOrPut(role) { mutableListOf() }.addAll( - subJSON.mapNotNull { fromJSON(it, normalizeHref, warnings) } + subJSON.mapNotNull { + fromJSON( + it, + warnings + ) + } ) } } @@ -149,10 +157,18 @@ internal fun Map<String, List<PublicationCollection>>.appendToJSONObject(jsonObj } } -@Deprecated("Use [subcollections[role].firstOrNull()] instead", ReplaceWith("subcollections[role].firstOrNull()")) -fun Map<String, List<PublicationCollection>>.firstWithRole(role: String): PublicationCollection? = +@Deprecated( + "Use [subcollections[role].firstOrNull()] instead", + ReplaceWith("subcollections[role].firstOrNull()"), + level = DeprecationLevel.ERROR +) +public fun Map<String, List<PublicationCollection>>.firstWithRole(role: String): PublicationCollection? = get(role)?.firstOrNull() -@Deprecated("Use [subcollections[role]] instead", ReplaceWith("subcollections[role]")) -fun Map<String, List<PublicationCollection>>.findAllWithRole(role: String): List<PublicationCollection> = +@Deprecated( + "Use [subcollections[role]] instead", + ReplaceWith("subcollections[role]"), + level = DeprecationLevel.ERROR +) +public fun Map<String, List<PublicationCollection>>.findAllWithRole(role: String): List<PublicationCollection> = get(role) ?: emptyList() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/PublicationServicesHolder.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/PublicationServicesHolder.kt new file mode 100644 index 0000000000..b412694df5 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/PublicationServicesHolder.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.publication + +import kotlin.reflect.KClass +import org.readium.r2.shared.extensions.tryOrLog +import org.readium.r2.shared.util.SuspendingCloseable + +/** + * Holds [Publication.Service] instances for a [Publication]. + */ +public interface PublicationServicesHolder : SuspendingCloseable { + /** + * Returns the first publication service that is an instance of [serviceType]. + */ + public fun <T : Publication.Service> findService(serviceType: KClass<T>): T? + + /** + * Returns all the publication services that are instances of [serviceType]. + */ + public fun <T : Publication.Service> findServices(serviceType: KClass<T>): List<T> +} + +internal class ListPublicationServicesHolder( + var services: List<Publication.Service> = emptyList() +) : PublicationServicesHolder { + override fun <T : Publication.Service> findService(serviceType: KClass<T>): T? = + findServices(serviceType).firstOrNull() + + override fun <T : Publication.Service> findServices(serviceType: KClass<T>): List<T> = + services.filterIsInstance(serviceType.java) + + override suspend fun close() { + for (service in services) { + tryOrLog { service.close() } + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/ReadingProgression.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/ReadingProgression.kt index 0cd5ab954c..4faf049042 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/ReadingProgression.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/ReadingProgression.kt @@ -14,31 +14,23 @@ import java.util.* import kotlinx.parcelize.Parcelize import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import org.readium.r2.shared.util.MapWithDefaultCompanion +import org.readium.r2.shared.util.MapCompanion @Serializable @Parcelize -enum class ReadingProgression(val value: String) : Parcelable { +public enum class ReadingProgression(public val value: String) : Parcelable { /** Right to left */ - @SerialName("rtl") RTL("rtl"), - /** Left to right */ - @SerialName("ltr") LTR("ltr"), - /** Top to bottom */ - @SerialName("ttb") TTB("ttb"), - /** Bottom to top */ - @SerialName("btt") BTT("btt"), - @SerialName("auto") AUTO("auto"); + @SerialName("rtl") + RTL("rtl"), - /** - * Indicates whether this reading progression is on the horizontal axis, or null if unknown. - */ - val isHorizontal: Boolean? get() = when (this) { - RTL, LTR -> true - TTB, BTT -> false - AUTO -> null - } + /** Left to right */ + @SerialName("ltr") + LTR("ltr"); - companion object : MapWithDefaultCompanion<String, ReadingProgression>(values(), ReadingProgression::value, AUTO) { + public companion object : MapCompanion<String, ReadingProgression>( + values(), + ReadingProgression::value + ) { override fun get(key: String?): ReadingProgression? = // For backward compatibility, we allow uppercase keys. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Subject.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Subject.kt index 749c1a506b..7159e09662 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Subject.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Subject.kt @@ -23,13 +23,13 @@ import org.readium.r2.shared.util.logging.log /** * https://github.com/readium/webpub-manifest/tree/master/contexts/default#subjects * - * @param sortAs Provides a string that a machine can sort. + * @param localizedSortAs Provides a string that a machine can sort. * @param scheme EPUB 3.1 opf:authority. * @param code EPUB 3.1 opf:term. * @param links Used to retrieve similar publications for the given subjects. */ @Parcelize -data class Subject( +public data class Subject( val localizedName: LocalizedString, val localizedSortAs: LocalizedString? = null, val scheme: String? = null, @@ -40,7 +40,7 @@ data class Subject( /** * Shortcut to create a [Subject] using a string as [name]. */ - constructor(name: String) : this( + public constructor(name: String) : this( localizedName = LocalizedString(name) ) @@ -57,7 +57,7 @@ data class Subject( /** * Serializes a [Subject] to its RWPM JSON representation. */ - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { putIfNotEmpty("name", localizedName) putIfNotEmpty("sortAs", localizedSortAs) put("scheme", scheme) @@ -65,19 +65,16 @@ data class Subject( putIfNotEmpty("links", links) } - companion object { + public companion object { /** * Parses a [Subject] from its RWPM JSON representation. * * A subject can be parsed from a single string, or a full-fledged object. - * The [links]' href and their children's will be normalized recursively using the - * provided [normalizeHref] closure. * If the subject can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSON( + public fun fromJSON( json: Any?, - normalizeHref: LinkHrefNormalizer = LinkHrefNormalizerIdentity, warnings: WarningLogger? = null ): Subject? { json ?: return null @@ -98,28 +95,33 @@ data class Subject( localizedSortAs = LocalizedString.fromJSON(jsonObject.remove("sortAs"), warnings), scheme = jsonObject.optNullableString("scheme"), code = jsonObject.optNullableString("code"), - links = Link.fromJSONArray(jsonObject.optJSONArray("links"), normalizeHref, warnings) + links = Link.fromJSONArray( + jsonObject.optJSONArray("links"), + warnings + ) ) } /** * Creates a list of [Subject] from its RWPM JSON representation. * - * The [links]' href and their children's will be normalized recursively using the - * provided [normalizeHref] closure. * If a subject can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSONArray( + public fun fromJSONArray( json: Any?, - normalizeHref: LinkHrefNormalizer = LinkHrefNormalizerIdentity, warnings: WarningLogger? = null ): List<Subject> { return when (json) { is String, is JSONObject -> - listOf(json).mapNotNull { fromJSON(it, normalizeHref, warnings) } + listOf(json).mapNotNull { + fromJSON( + it, + warnings + ) + } is JSONArray -> - json.parseObjects { fromJSON(it, normalizeHref, warnings) } + json.parseObjects { fromJSON(it, warnings) } else -> emptyList() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/archive/Properties.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/archive/Properties.kt index a26fb2c8d0..66c6f08c3d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/archive/Properties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/archive/Properties.kt @@ -6,56 +6,13 @@ package org.readium.r2.shared.publication.archive -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import org.json.JSONObject -import org.readium.r2.shared.JSONable -import org.readium.r2.shared.extensions.optNullableBoolean -import org.readium.r2.shared.extensions.optNullableLong import org.readium.r2.shared.publication.Properties -import org.readium.r2.shared.util.logging.WarningLogger -import org.readium.r2.shared.util.logging.log -// Archive Link Properties Extension - -/** - * Holds information about how the resource is stored in the publication archive. - * - * @param entryLength The length of the entry stored in the archive. It might be a compressed length - * if the entry is deflated. - * @param isEntryCompressed Indicates whether the entry was compressed before being stored in the - * archive. - */ -@Parcelize -data class ArchiveProperties( +public data class ArchiveProperties( val entryLength: Long, val isEntryCompressed: Boolean -) : JSONable, Parcelable { - - override fun toJSON(): JSONObject = JSONObject().apply { - put("entryLength", entryLength) - put("isEntryCompressed", isEntryCompressed) - } - - companion object { - fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): ArchiveProperties? { - json ?: return null +) - val entryLength = json.optNullableLong("entryLength") - val isEntryCompressed = json.optNullableBoolean("isEntryCompressed") - if (entryLength == null || isEntryCompressed == null) { - warnings?.log(ArchiveProperties::class.java, "[entryLength] and [isEntryCompressed] are required", json) - return null - } - - return ArchiveProperties(entryLength = entryLength, isEntryCompressed = isEntryCompressed) - } - } -} - -/** - * Provides information about how the resource is stored in the publication archive. - */ -val Properties.archive: ArchiveProperties? - get() = (this["archive"] as? Map<*, *>) - ?.let { ArchiveProperties.fromJSON(JSONObject(it)) } +@Deprecated("Not used anymore", level = DeprecationLevel.ERROR) +public val Properties.archive: ArchiveProperties? + get() = throw NotImplementedError() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/asset/FileAsset.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/asset/FileAsset.kt index df0a04c784..f95432898b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/asset/FileAsset.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/asset/FileAsset.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 Readium Foundation. All rights reserved. + * Copyright 2024 Readium Foundation. All rights reserved. * Use of this source code is governed by the BSD-style license * available in the top-level LICENSE file of the project. */ @@ -7,74 +7,14 @@ package org.readium.r2.shared.publication.asset import java.io.File -import java.io.FileNotFoundException -import org.readium.r2.shared.fetcher.ArchiveFetcher -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.fetcher.FileFetcher -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.mediatype.MediaType -/** - * Represents a publication stored as a file on the local file system. - * - * @param file File on the file system. - */ -class FileAsset private constructor( - val file: File, - private val knownMediaType: MediaType?, - private val mediaTypeHint: String? -) : PublicationAsset { - - /** - * Creates a [FileAsset] from a [File] and an optional media type, when known. - */ - constructor(file: File, mediaType: MediaType? = null) : - this(file, knownMediaType = mediaType, mediaTypeHint = null) - - /** - * Creates a [FileAsset] from a [File] and an optional media type hint. - * - * Providing a media type hint will improve performances when sniffing the media type. - */ - constructor(file: File, mediaTypeHint: String?) : - this(file, knownMediaType = null, mediaTypeHint = mediaTypeHint) - - override val name: String - get() = file.name - - override suspend fun mediaType(): MediaType { - if (!::_mediaType.isInitialized) { - _mediaType = knownMediaType - ?: MediaType.ofFile(file, mediaType = mediaTypeHint) - ?: MediaType.BINARY - } - - return _mediaType - } - - private lateinit var _mediaType: MediaType - - override suspend fun createFetcher( - dependencies: PublicationAsset.Dependencies, - credentials: String? - ): Try<Fetcher, Publication.OpeningException> { - return try { - val fetcher = when { - file.isDirectory -> FileFetcher(href = "/", file = file) - - file.exists() -> ArchiveFetcher.fromPath(file.path, dependencies.archiveFactory) - ?: FileFetcher(href = "/${file.name}", file = file) - - else -> throw FileNotFoundException(file.path) - } - Try.success(fetcher) - } catch (e: SecurityException) { - Try.failure(Publication.OpeningException.Forbidden(e)) - } catch (e: FileNotFoundException) { - Try.failure(Publication.OpeningException.NotFound(e)) - } - } - - override fun toString(): String = "FileAsset(${file.path})" -} +@Deprecated( + "Use an `AssetRetriever` to create an `Asset`.", + ReplaceWith("AssetRetriever().retrieve(file)"), + DeprecationLevel.ERROR +) +public data class FileAsset( + public val file: File, + public val mediaType: MediaType? = null +) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/asset/PublicationAsset.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/asset/PublicationAsset.kt deleted file mode 100644 index b99a851793..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/asset/PublicationAsset.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.publication.asset - -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.archive.ArchiveFactory -import org.readium.r2.shared.util.mediatype.MediaType - -/** Represents a digital medium (e.g. a file) offering access to a publication. */ -interface PublicationAsset { - - /** Name of the asset, e.g. a filename. */ - val name: String - - /** - * Media type of the asset. - * - * If unknown, fallback on `MediaType.BINARY`. - */ - suspend fun mediaType(): MediaType - - /** - * Creates a fetcher used to access the asset's content. - */ - suspend fun createFetcher(dependencies: Dependencies, credentials: String?): Try<Fetcher, Publication.OpeningException> - - data class Dependencies(val archiveFactory: ArchiveFactory) -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/encryption/Encryption.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/encryption/Encryption.kt index 7d7d32e61f..c1f2064432 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/encryption/Encryption.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/encryption/Encryption.kt @@ -30,7 +30,7 @@ import org.readium.r2.shared.util.logging.log * @param scheme Identifies the encryption scheme used to encrypt the resource (URI). */ @Parcelize -data class Encryption( +public data class Encryption( val algorithm: String, val compression: String? = null, val originalLength: Long? = null, @@ -41,7 +41,7 @@ data class Encryption( /** * Serializes an [Encryption] to its RWPM JSON representation. */ - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { put("algorithm", algorithm) put("compression", compression) put("originalLength", originalLength) @@ -49,13 +49,13 @@ data class Encryption( put("scheme", scheme) } - companion object { + public companion object { /** * Creates an [Encryption] from its RWPM JSON representation. * If the encryption can't be parsed, a warning will be logged with [warnings]. */ - fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Encryption? { + public fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Encryption? { val algorithm = json?.optNullableString("algorithm") if (algorithm.isNullOrEmpty()) { warnings?.log(Encryption::class.java, "[algorithm] is required", json) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/encryption/Properties.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/encryption/Properties.kt index c9ce898145..ef88f2bcb2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/encryption/Properties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/encryption/Properties.kt @@ -18,6 +18,6 @@ import org.readium.r2.shared.publication.Properties * Indicates that a resource is encrypted/obfuscated and provides relevant information for * decryption. */ -val Properties.encryption: Encryption? +public val Properties.encryption: Encryption? get() = (this["encrypted"] as? Map<*, *>) ?.let { Encryption.fromJSON(JSONObject(it)) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EncryptionParser.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/EpubEncryptionParser.kt similarity index 72% rename from readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EncryptionParser.kt rename to readium/shared/src/main/java/org/readium/r2/shared/publication/epub/EpubEncryptionParser.kt index c314b1b8b7..5e208554ce 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EncryptionParser.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/EpubEncryptionParser.kt @@ -4,27 +4,40 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.streamer.parser.epub +package org.readium.r2.shared.publication.epub -import org.readium.r2.shared.drm.DRM -import org.readium.r2.shared.parser.xml.ElementNode +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.encryption.Encryption -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.publication.protection.ContentProtection +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.xml.ElementNode -internal object EncryptionParser { - fun parse(document: ElementNode): Map<String, Encryption> = +@InternalReadiumApi +public object EpubEncryptionParser { + + private object Namespaces { + const val ENC = "http://www.w3.org/2001/04/xmlenc#" + const val SIG = "http://www.w3.org/2000/09/xmldsig#" + const val COMP = "http://www.idpf.org/2016/encryption#compression" + } + + public fun parse(document: ElementNode): Map<Url, Encryption> = document.get("EncryptedData", Namespaces.ENC) .mapNotNull { parseEncryptedData(it) } .toMap() - private fun parseEncryptedData(node: ElementNode): Pair<String, Encryption>? { + private fun parseEncryptedData(node: ElementNode): Pair<Url, Encryption>? { val resourceURI = node.getFirst("CipherData", Namespaces.ENC) ?.getFirst("CipherReference", Namespaces.ENC)?.getAttr("URI") + ?.let { Url(it) ?: Url.fromDecodedPath(it) } ?: return null val retrievalMethod = node.getFirst("KeyInfo", Namespaces.SIG) ?.getFirst("RetrievalMethod", Namespaces.SIG)?.getAttr("URI") - val scheme = if (retrievalMethod == "license.lcpl#/encryption/content_key") - DRM.Scheme.lcp.rawValue else null + val scheme = if (retrievalMethod == "license.lcpl#/encryption/content_key") { + ContentProtection.Scheme.Lcp.uri + } else { + null + } val algorithm = node.getFirst("EncryptionMethod", Namespaces.ENC) ?.getAttr("Algorithm") ?: return null @@ -40,7 +53,7 @@ internal object EncryptionParser { compression = compressionMethod, originalLength = originalLength ) - return Pair(Href(resourceURI).string, enc) + return Pair(resourceURI, enc) } private fun parseEncryptionProperties(encryptionProperties: ElementNode): Pair<Long, String>? { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/EpubLayout.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/EpubLayout.kt index 8222e5faa7..55ecaa11ae 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/EpubLayout.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/EpubLayout.kt @@ -18,15 +18,24 @@ import org.readium.r2.shared.util.MapCompanion * https://readium.org/webpub-manifest/schema/extensions/epub/metadata.schema.json */ @Parcelize -enum class EpubLayout(val value: String) : Parcelable { +public enum class EpubLayout(public val value: String) : Parcelable { FIXED("fixed"), REFLOWABLE("reflowable"); - companion object : MapCompanion<String, EpubLayout>(values(), EpubLayout::value) { + public companion object : MapCompanion<String, EpubLayout>(values(), EpubLayout::value) { - @Deprecated("Renamed to [FIXED]", ReplaceWith("EpubLayout.FIXED")) - val Fixed: EpubLayout get() = FIXED - @Deprecated("Renamed to [REFLOWABLE]", ReplaceWith("EpubLayout.REFLOWABLE")) - val Reflowable: EpubLayout get() = REFLOWABLE + @Deprecated( + "Renamed to [FIXED]", + ReplaceWith("EpubLayout.FIXED"), + level = DeprecationLevel.ERROR + ) + public val Fixed: EpubLayout get() = FIXED + + @Deprecated( + "Renamed to [REFLOWABLE]", + ReplaceWith("EpubLayout.REFLOWABLE"), + level = DeprecationLevel.ERROR + ) + public val Reflowable: EpubLayout get() = REFLOWABLE } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/Presentation.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/Presentation.kt index 98bf50a747..6bf6f2a1e8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/Presentation.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/Presentation.kt @@ -16,7 +16,7 @@ import org.readium.r2.shared.publication.presentation.Presentation * Get the layout of the given resource in this publication. * Falls back on REFLOWABLE. */ -fun Presentation.layoutOf(link: Link): EpubLayout { +public fun Presentation.layoutOf(link: Link): EpubLayout { return link.properties.layout ?: layout ?: EpubLayout.REFLOWABLE diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/Properties.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/Properties.kt index c1c2f3c1df..76de4043d4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/Properties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/Properties.kt @@ -18,7 +18,7 @@ import org.readium.r2.shared.publication.Properties * Identifies content contained in the linked resource, that cannot be strictly identified using a * media type. */ -val Properties.contains: Set<String> +public val Properties.contains: Set<String> get() = (this["contains"] as? List<*>) ?.filterIsInstance(String::class.java) ?.toSet() @@ -27,5 +27,5 @@ val Properties.contains: Set<String> /** * Hints how the layout of the resource should be presented. */ -val Properties.layout: EpubLayout? +public val Properties.layout: EpubLayout? get() = EpubLayout(this["layout"] as? String) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/Publication.kt index eb6ec49479..a369a11fc7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/Publication.kt @@ -20,15 +20,15 @@ import org.readium.r2.shared.publication.Publication * Provides navigation to positions in the Publication content that correspond to the locations of * page boundaries present in a print source being represented by this EPUB Publication. */ -val Publication.pageList: List<Link> get() = linksWithRole("pageList") +public val Publication.pageList: List<Link> get() = linksWithRole("pageList") /** * Identifies fundamental structural components of the publication in order to enable Reading * Systems to provide the User efficient access to them. */ -val Publication.landmarks: List<Link> get() = linksWithRole("landmarks") +public val Publication.landmarks: List<Link> get() = linksWithRole("landmarks") -val Publication.listOfAudioClips: List<Link> get() = linksWithRole("loa") -val Publication.listOfIllustrations: List<Link> get() = linksWithRole("loi") -val Publication.listOfTables: List<Link> get() = linksWithRole("lot") -val Publication.listOfVideoClips: List<Link> get() = linksWithRole("lov") +public val Publication.listOfAudioClips: List<Link> get() = linksWithRole("loa") +public val Publication.listOfIllustrations: List<Link> get() = linksWithRole("loi") +public val Publication.listOfTables: List<Link> get() = linksWithRole("lot") +public val Publication.listOfVideoClips: List<Link> get() = linksWithRole("lov") diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/html/DomRange.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/html/DomRange.kt index 0d39ee26de..889b001272 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/html/DomRange.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/html/DomRange.kt @@ -40,7 +40,7 @@ import org.readium.r2.shared.util.logging.log * @param end A serializable representation of the "end" boundary point of the DOM Range. */ @Parcelize -data class DomRange( +public data class DomRange( val start: Point, val end: Point? = null ) : JSONable, Parcelable { @@ -61,25 +61,29 @@ data class DomRange( * https://github.com/readium/architecture/blob/master/models/locators/extensions/html.md#the-start-and-end-object */ @Parcelize - data class Point( + public data class Point( val cssSelector: String, val textNodeIndex: Int, val charOffset: Int? = null ) : JSONable, Parcelable { - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { put("cssSelector", cssSelector) put("textNodeIndex", textNodeIndex) put("charOffset", charOffset) } - companion object { + public companion object { - fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Point? { + public fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Point? { val cssSelector = json?.optNullableString("cssSelector") val textNodeIndex = json?.optPositiveInt("textNodeIndex") if (cssSelector == null || textNodeIndex == null) { - warnings?.log(Point::class.java, "[cssSelector] and [textNodeIndex] are required", json) + warnings?.log( + Point::class.java, + "[cssSelector] and [textNodeIndex] are required", + json + ) return null } @@ -95,18 +99,22 @@ data class DomRange( } } - @Deprecated("Renamed into [charOffset]", ReplaceWith("charOffset")) + @Deprecated( + "Renamed into [charOffset]", + ReplaceWith("charOffset"), + level = DeprecationLevel.ERROR + ) val offset: Long? get() = charOffset?.toLong() } - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { putIfNotEmpty("start", start) putIfNotEmpty("end", end) } - companion object { + public companion object { - fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): DomRange? { + public fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): DomRange? { val start = Point.fromJSON(json?.optJSONObject("start")) if (start == null) { warnings?.log(DomRange::class.java, "[start] is required", json) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/html/Locator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/html/Locator.kt index 9d0d517cda..74928cb663 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/html/Locator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/html/Locator.kt @@ -18,7 +18,7 @@ import org.readium.r2.shared.publication.Locator /** * A CSS Selector. */ -val Locator.Locations.cssSelector: String? +public val Locator.Locations.cssSelector: String? get() = this["cssSelector"] as? String /** @@ -28,12 +28,12 @@ val Locator.Locations.cssSelector: String? * epubcfi(***) syntax is not used for the [partialCfi] string, i.e. the "fragment" part of the CFI * grammar is ignored. */ -val Locator.Locations.partialCfi: String? +public val Locator.Locations.partialCfi: String? get() = this["partialCfi"] as? String /** * An HTML DOM range. */ -val Locator.Locations.domRange: DomRange? +public val Locator.Locations.domRange: DomRange? get() = (this["domRange"] as? Map<*, *>) ?.let { DomRange.fromJSON(JSONObject(it)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/opds/Properties.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/opds/Properties.kt index d308094ed5..3be98b5b7f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/opds/Properties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/opds/Properties.kt @@ -10,7 +10,11 @@ package org.readium.r2.shared.publication.opds import org.json.JSONObject -import org.readium.r2.shared.opds.* +import org.readium.r2.shared.opds.Acquisition +import org.readium.r2.shared.opds.Availability +import org.readium.r2.shared.opds.Copies +import org.readium.r2.shared.opds.Holds +import org.readium.r2.shared.opds.Price import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Properties @@ -20,14 +24,14 @@ import org.readium.r2.shared.publication.Properties /** * Provides a hint about the expected number of items returned. */ -val Properties.numberOfItems: Int? +public val Properties.numberOfItems: Int? get() = (this["numberOfItems"] as? Int) ?.takeIf { it >= 0 } /** * The price of a publication is tied to its acquisition link. */ -val Properties.price: Price? +public val Properties.price: Price? get() = (this["price"] as? Map<*, *>) ?.let { Price.fromJSON(JSONObject(it)) } @@ -35,7 +39,7 @@ val Properties.price: Price? * Indirect acquisition provides a hint for the expected media type that will be acquired after * additional steps. */ -val Properties.indirectAcquisitions: List<Acquisition> +public val Properties.indirectAcquisitions: List<Acquisition> get() = (this["indirectAcquisition"] as? List<*>) ?.mapNotNull { if (it !is Map<*, *>) { @@ -46,28 +50,32 @@ val Properties.indirectAcquisitions: List<Acquisition> } ?: emptyList() -@Deprecated("Use [indirectAcquisitions] instead.", ReplaceWith("indirectAcquisitions")) -val Properties.indirectAcquisition: List<Acquisition> +@Deprecated( + "Use [indirectAcquisitions] instead.", + ReplaceWith("indirectAcquisitions"), + level = DeprecationLevel.ERROR +) +public val Properties.indirectAcquisition: List<Acquisition> get() = indirectAcquisitions /** * Library-specific features when a specific book is unavailable but provides a hold list. */ -val Properties.holds: Holds? +public val Properties.holds: Holds? get() = (this["holds"] as? Map<*, *>) ?.let { Holds.fromJSON(JSONObject(it)) } /** * Library-specific feature that contains information about the copies that a library has acquired. */ -val Properties.copies: Copies? +public val Properties.copies: Copies? get() = (this["copies"] as? Map<*, *>) ?.let { Copies.fromJSON(JSONObject(it)) } /** * Indicated the availability of a given resource. */ -val Properties.availability: Availability? +public val Properties.availability: Availability? get() = (this["availability"] as? Map<*, *>) ?.let { Availability.fromJSON(JSONObject(it)) } @@ -77,6 +85,6 @@ val Properties.availability: Availability? * * See https://drafts.opds.io/authentication-for-opds-1.0.html */ -val Properties.authenticate: Link? +public val Properties.authenticate: Link? get() = (this["authenticate"] as? Map<*, *>) ?.let { Link.fromJSON(JSONObject(it)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/opds/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/opds/Publication.kt index 2846d867c0..8803978b40 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/opds/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/opds/Publication.kt @@ -14,4 +14,4 @@ import org.readium.r2.shared.publication.Publication // OPDS extensions for [Publication] -val Publication.images: List<Link> get() = linksWithRole("images") +public val Publication.images: List<Link> get() = linksWithRole("images") diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/presentation/Metadata.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/presentation/Metadata.kt index 8ee3358449..2ba7bb03a4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/presentation/Metadata.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/presentation/Metadata.kt @@ -13,7 +13,7 @@ import org.readium.r2.shared.publication.Metadata // Presentation extensions for [Metadata] -val Metadata.presentation: Presentation +public val Metadata.presentation: Presentation get() = Presentation.fromJSON( (this["presentation"] as? Map<*, *>) ?.let { JSONObject(it) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/presentation/Presentation.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/presentation/Presentation.kt index c06cc66501..dfe687e34a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/presentation/Presentation.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/presentation/Presentation.kt @@ -27,12 +27,12 @@ import org.readium.r2.shared.util.MapCompanion * https://readium.org/webpub-manifest/schema/extensions/presentation/metadata.schema.json * * These properties are nullable to avoid having default values when it doesn't make sense for a - * given [Publication]. If a navigator needs a default value when not specified, + * given publication. If a navigator needs a default value when not specified, * Presentation.DEFAULT_X and Presentation.X.DEFAULT can be used. * * @param clipped Specifies whether or not the parts of a linked resource that flow out of the * viewport are clipped. - * @param continuous Indicates how the progression between resources from the [readingOrder] should + * @param continuous Indicates how the progression between resources from the reading order should * be handled. * @param fit Suggested method for constraining a resource inside the viewport. * @param orientation Suggested orientation for the device when displaying the linked resource. @@ -42,7 +42,7 @@ import org.readium.r2.shared.util.MapCompanion * @param layout Hints how the layout of the resource should be presented (EPUB extension). */ @Parcelize -data class Presentation( +public data class Presentation( val clipped: Boolean? = null, val continuous: Boolean? = null, val fit: Fit? = null, @@ -55,7 +55,7 @@ data class Presentation( /** * Serializes a [Presentation] to its RWPM JSON representation. */ - override fun toJSON() = JSONObject().apply { + override fun toJSON(): JSONObject = JSONObject().apply { put("clipped", clipped) put("continuous", continuous) put("fit", fit?.value) @@ -65,22 +65,22 @@ data class Presentation( put("layout", layout?.value) } - companion object { + public companion object { /** * Default value for [clipped], if not specified. */ - const val DEFAULT_CLIPPED = false + public const val DEFAULT_CLIPPED: Boolean = false /** * Default value for [continuous], if not specified. */ - const val DEFAULT_CONTINUOUS = true + public const val DEFAULT_CONTINUOUS: Boolean = true /** * Creates a [Properties] from its RWPM JSON representation. */ - fun fromJSON(json: JSONObject?): Presentation { + public fun fromJSON(json: JSONObject?): Presentation { if (json == null) { return Presentation() } @@ -101,18 +101,25 @@ data class Presentation( */ @Parcelize @Serializable - enum class Fit(val value: String) : Parcelable { - @SerialName("width") WIDTH("width"), - @SerialName("height") HEIGHT("height"), - @SerialName("contain") CONTAIN("contain"), - @SerialName("cover") COVER("cover"); + public enum class Fit(public val value: String) : Parcelable { + @SerialName("width") + WIDTH("width"), - companion object : MapCompanion<String, Fit>(values(), Fit::value) { + @SerialName("height") + HEIGHT("height"), + + @SerialName("contain") + CONTAIN("contain"), + + @SerialName("cover") + COVER("cover"); + + public companion object : MapCompanion<String, Fit>(values(), Fit::value) { /** * Default value for [Fit], if not specified. */ - val DEFAULT = CONTAIN + public val DEFAULT: Fit = CONTAIN } } @@ -121,24 +128,43 @@ data class Presentation( */ @Parcelize @Serializable - enum class Orientation(val value: String) : Parcelable { - @SerialName("auto") AUTO("auto"), - @SerialName("landscape") LANDSCAPE("landscape"), - @SerialName("portrait") PORTRAIT("portrait"); + public enum class Orientation(public val value: String) : Parcelable { + @SerialName("auto") + AUTO("auto"), + + @SerialName("landscape") + LANDSCAPE("landscape"), + + @SerialName("portrait") + PORTRAIT("portrait"); - companion object : MapCompanion<String, Orientation>(values(), Orientation::value) { + public companion object : MapCompanion<String, Orientation>(values(), Orientation::value) { /** * Default value for [Orientation], if not specified. */ - val DEFAULT = AUTO - - @Deprecated("Renamed to [AUTO]", ReplaceWith("Orientation.AUTO")) - val Auto: Orientation = AUTO - @Deprecated("Renamed to [LANDSCAPE]", ReplaceWith("Orientation.LANDSCAPE")) - val Landscape: Orientation = LANDSCAPE - @Deprecated("Renamed to [PORTRAIT]", ReplaceWith("Orientation.PORTRAIT")) - val Portrait: Orientation = PORTRAIT + public val DEFAULT: Orientation = AUTO + + @Deprecated( + "Renamed to [AUTO]", + ReplaceWith("Orientation.AUTO"), + level = DeprecationLevel.ERROR + ) + public val Auto: Orientation = AUTO + + @Deprecated( + "Renamed to [LANDSCAPE]", + ReplaceWith("Orientation.LANDSCAPE"), + level = DeprecationLevel.ERROR + ) + public val Landscape: Orientation = LANDSCAPE + + @Deprecated( + "Renamed to [PORTRAIT]", + ReplaceWith("Orientation.PORTRAIT"), + level = DeprecationLevel.ERROR + ) + public val Portrait: Orientation = PORTRAIT } } @@ -147,24 +173,43 @@ data class Presentation( */ @Parcelize @Serializable - enum class Overflow(val value: String) : Parcelable { - @SerialName("auto") AUTO("auto"), - @SerialName("paginated") PAGINATED("paginated"), - @SerialName("scrolled") SCROLLED("scrolled"); + public enum class Overflow(public val value: String) : Parcelable { + @SerialName("auto") + AUTO("auto"), + + @SerialName("paginated") + PAGINATED("paginated"), + + @SerialName("scrolled") + SCROLLED("scrolled"); - companion object : MapCompanion<String, Overflow>(values(), Overflow::value) { + public companion object : MapCompanion<String, Overflow>(values(), Overflow::value) { /** * Default value for [Overflow], if not specified. */ - val DEFAULT = AUTO - - @Deprecated("Renamed to [PAGINATED]", ReplaceWith("Overflow.PAGINATED")) - val Paginated: Overflow = PAGINATED - @Deprecated("Use [presentation.continuous] instead", ReplaceWith("presentation.continuous")) - val Continuous: Overflow = SCROLLED - @Deprecated("Renamed to [SCROLLED]", ReplaceWith("Overflow.SCROLLED")) - val Document: Overflow = SCROLLED + public val DEFAULT: Overflow = AUTO + + @Deprecated( + "Renamed to [PAGINATED]", + ReplaceWith("Overflow.PAGINATED"), + level = DeprecationLevel.ERROR + ) + public val Paginated: Overflow = PAGINATED + + @Deprecated( + "Use [presentation.continuous] instead", + ReplaceWith("presentation.continuous"), + level = DeprecationLevel.ERROR + ) + public val Continuous: Overflow = SCROLLED + + @Deprecated( + "Renamed to [SCROLLED]", + ReplaceWith("Overflow.SCROLLED"), + level = DeprecationLevel.ERROR + ) + public val Document: Overflow = SCROLLED } } @@ -174,12 +219,17 @@ data class Presentation( */ @Parcelize @Serializable - enum class Page(val value: String) : Parcelable { - @SerialName("left") LEFT("left"), - @SerialName("right") RIGHT("right"), - @SerialName("center") CENTER("center"); + public enum class Page(public val value: String) : Parcelable { + @SerialName("left") + LEFT("left"), + + @SerialName("right") + RIGHT("right"), - companion object : MapCompanion<String, Page>(values(), Page::value) + @SerialName("center") + CENTER("center"); + + public companion object : MapCompanion<String, Page>(values(), Page::value) } /** @@ -188,35 +238,66 @@ data class Presentation( */ @Parcelize @Serializable - enum class Spread(val value: String) : Parcelable { - @SerialName("auto") AUTO("auto"), - @SerialName("both") BOTH("both"), - @SerialName("none") NONE("none"), - @SerialName("landscape") LANDSCAPE("landscape"); + public enum class Spread(public val value: String) : Parcelable { + @SerialName("auto") + AUTO("auto"), + + @SerialName("both") + BOTH("both"), - companion object : MapCompanion<String, Spread>(values(), Spread::value) { + @SerialName("none") + NONE("none"), + + @SerialName("landscape") + LANDSCAPE("landscape"); + + public companion object : MapCompanion<String, Spread>(values(), Spread::value) { /** * Default value for [Spread], if not specified. */ - val DEFAULT = AUTO - - @Deprecated("Renamed to [AUTO]", ReplaceWith("Spread.AUTO")) - val Auto: Spread = AUTO - @Deprecated("Renamed to [LANDSCAPE]", ReplaceWith("Spread.LANDSCAPE")) - val Landscape: Spread = LANDSCAPE - @Deprecated("Renamed to [BOTH]", ReplaceWith("Spread.BOTH")) - val Portrait: Spread = BOTH - @Deprecated("Renamed to [BOTH]", ReplaceWith("Spread.BOTH")) - val Both: Spread = BOTH - @Deprecated("Renamed to [NONE]", ReplaceWith("Spread.NONE")) - val None: Spread = NONE + public val DEFAULT: Spread = AUTO + + @Deprecated( + "Renamed to [AUTO]", + ReplaceWith("Spread.AUTO"), + level = DeprecationLevel.ERROR + ) + public val Auto: Spread = AUTO + + @Deprecated( + "Renamed to [LANDSCAPE]", + ReplaceWith("Spread.LANDSCAPE"), + level = DeprecationLevel.ERROR + ) + public val Landscape: Spread = LANDSCAPE + + @Deprecated( + "Renamed to [BOTH]", + ReplaceWith("Spread.BOTH"), + level = DeprecationLevel.ERROR + ) + public val Portrait: Spread = BOTH + + @Deprecated( + "Renamed to [BOTH]", + ReplaceWith("Spread.BOTH"), + level = DeprecationLevel.ERROR + ) + public val Both: Spread = BOTH + + @Deprecated( + "Renamed to [NONE]", + ReplaceWith("Spread.NONE"), + level = DeprecationLevel.ERROR + ) + public val None: Spread = NONE } } - @Deprecated("Use [toJSON] instead", ReplaceWith("toJSON()")) - fun getJSON(): JSONObject = toJSON() + @Deprecated("Use [toJSON] instead", ReplaceWith("toJSON()"), level = DeprecationLevel.ERROR) + public fun getJSON(): JSONObject = toJSON() - @Deprecated("Use [overflow] instead", ReplaceWith("overflow")) + @Deprecated("Use [overflow] instead", ReplaceWith("overflow"), level = DeprecationLevel.ERROR) val flow: Overflow? get() = overflow } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/presentation/Properties.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/presentation/Properties.kt index 8b94ab72ce..823e7c9a64 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/presentation/Properties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/presentation/Properties.kt @@ -16,37 +16,37 @@ import org.readium.r2.shared.publication.Properties * Specifies whether or not the parts of a linked resource that flow out of the viewport are * clipped. */ -val Properties.clipped: Boolean? +public val Properties.clipped: Boolean? get() = this["clipped"] as? Boolean /** * Suggested method for constraining a resource inside the viewport. */ -val Properties.fit: Presentation.Fit? +public val Properties.fit: Presentation.Fit? get() = Presentation.Fit(this["fit"] as? String) /** * Suggested orientation for the device when displaying the linked resource. */ -val Properties.orientation: Presentation.Orientation? +public val Properties.orientation: Presentation.Orientation? get() = Presentation.Orientation(this["orientation"] as? String) /** * Suggested method for handling overflow while displaying the linked resource. */ -val Properties.overflow: Presentation.Overflow? +public val Properties.overflow: Presentation.Overflow? get() = Presentation.Overflow(this["overflow"] as? String) /** * Indicates how the linked resource should be displayed in a reading environment that displays * synthetic spreads. */ -val Properties.page: Presentation.Page? +public val Properties.page: Presentation.Page? get() = Presentation.Page(this["page"] as? String) /** * Indicates the condition to be met for the linked resource to be rendered within a synthetic * spread. */ -val Properties.spread: Presentation.Spread? +public val Properties.spread: Presentation.Spread? get() = Presentation.Spread(this["spread"] as? String) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt new file mode 100644 index 0000000000..d75f9bd937 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -0,0 +1,88 @@ +/* + * Module: r2-streamer-kotlin + * Developers: Quentin Gliosca + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.shared.publication.protection + +import org.readium.r2.shared.publication.LocalizedString +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.services.ContentProtectionService +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError + +/** + * Bridge between a Content Protection technology and the Readium toolkit. + * + * Its responsibilities are to: + * - Create a [Container] one can access the publication through. + * - Create a [ContentProtectionService] publication service. + */ +public interface ContentProtection { + + public sealed class OpenError( + override val message: String, + override val cause: Error? + ) : Error { + + public class Reading( + override val cause: ReadError + ) : OpenError("An error occurred while trying to read asset.", cause) + + public class AssetNotSupported( + override val cause: Error? = null + ) : OpenError("Asset is not supported.", cause) + } + + /** + * Holds the result of opening an [Asset] with a [ContentProtection]. + * + * @property asset Asset pointing to a publication. + * @property onCreatePublication Called on every parsed Publication.Builder + * It can be used to modify the `Manifest`, the root [Container] or the list of service + * factories of a [Publication]. + */ + public data class OpenResult( + val asset: Asset, + val onCreatePublication: Publication.Builder.() -> Unit = {} + ) + + /** + * Attempts to unlock a potentially protected publication asset. + * + * @return A [Asset] in case of success or an [OpenError] if the + * asset can't be successfully opened even in restricted mode. + */ + public suspend fun open( + asset: Asset, + credentials: String?, + allowUserInteraction: Boolean + ): Try<OpenResult, OpenError> + + /** + * Represents a specific Content Protection technology, uniquely identified with an [uri]. + */ + @JvmInline + public value class Scheme( + public val uri: String + ) { + + @Deprecated("Define yourself the name to print to users.", level = DeprecationLevel.ERROR) + public val name: LocalizedString? get() = null + + public companion object { + /** Readium LCP DRM scheme. */ + public val Lcp: Scheme = Scheme(uri = "http://readium.org/2014/01/lcp") + + /** Adobe ADEPT DRM scheme. */ + public val Adept: Scheme = Scheme(uri = "http://ns.adobe.com/adept") + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt new file mode 100644 index 0000000000..9e780c5fd4 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2021 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.publication.protection + +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.protection.ContentProtection.Scheme +import org.readium.r2.shared.publication.services.ContentProtectionService +import org.readium.r2.shared.publication.services.contentProtectionServiceFactory +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.format.AdeptSpecification +import org.readium.r2.shared.util.format.LcpSpecification + +/** + * [ContentProtection] implementation used as a fallback when detecting known DRMs + * not supported by the app. + */ +public class FallbackContentProtection : ContentProtection { + + override suspend fun open( + asset: Asset, + credentials: String?, + allowUserInteraction: Boolean + ): Try<ContentProtection.OpenResult, ContentProtection.OpenError> { + if (asset !is ContainerAsset) { + return Try.failure( + ContentProtection.OpenError.AssetNotSupported() + ) + } + + val protectionServiceFactory = when { + asset.format.conformsTo(LcpSpecification) -> + Service.createFactory(Scheme.Lcp, "Readium LCP") + asset.format.conformsTo(AdeptSpecification) -> + Service.createFactory(Scheme.Adept, "Adobe ADEPT") + else -> + return Try.failure(ContentProtection.OpenError.AssetNotSupported()) + } + + val protectedFile = ContentProtection.OpenResult( + asset = asset, + onCreatePublication = { + servicesBuilder.contentProtectionServiceFactory = protectionServiceFactory + } + ) + + return Try.success(protectedFile) + } + + public class SchemeNotSupportedError( + public val scheme: Scheme, + public val name: String + ) : Error { + + override val message: String = "$name DRM scheme is not supported." + + override val cause: Error? = null + } + + private class Service( + override val scheme: Scheme, + override val name: String + ) : ContentProtectionService { + + override val isRestricted: Boolean = + true + + override val credentials: String? = + null + + override val rights: ContentProtectionService.UserRights = + ContentProtectionService.UserRights.AllRestricted + + override val error: Error = + SchemeNotSupportedError(scheme, name) + + companion object { + + fun createFactory( + scheme: Scheme, + name: String + ): (Publication.Service.Context) -> ContentProtectionService = + { Service(scheme, name) } + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CacheService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CacheService.kt index 4289256a1c..35b073ca4d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CacheService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CacheService.kt @@ -26,20 +26,20 @@ import org.readium.r2.shared.util.cache.InMemoryCache * Provides publication-bound caches. */ @InternalReadiumApi -interface CacheService : Publication.Service { +public interface CacheService : Publication.Service { /** * Gets the cache for objects of [valueType] in the given [namespace]. */ - suspend fun <T : Any> cacheOf(valueType: KClass<T>, namespace: String): Cache<T> + public suspend fun <T : Any> cacheOf(valueType: KClass<T>, namespace: String): Cache<T> } @InternalReadiumApi -val PublicationServicesHolder.cacheService: CacheService? +public val PublicationServicesHolder.cacheService: CacheService? get() = findService(CacheService::class) /** Factory to build a [CacheService]. */ @InternalReadiumApi -var Publication.ServicesBuilder.cacheServiceFactory: ServiceFactory? +public var Publication.ServicesBuilder.cacheServiceFactory: ServiceFactory? get() = get(CacheService::class) set(value) = set(CacheService::class, value) @@ -47,10 +47,10 @@ var Publication.ServicesBuilder.cacheServiceFactory: ServiceFactory? * A basic [CacheService] implementation keeping the cached objects in memory. */ @InternalReadiumApi -class InMemoryCacheService(context: Context?) : CacheService, MemoryObserver { +public class InMemoryCacheService(context: Context?) : CacheService, MemoryObserver { - companion object { - fun createFactory(context: Context?): (Publication.Service.Context) -> InMemoryCacheService = { _ -> + public companion object { + public fun createFactory(context: Context?): (Publication.Service.Context) -> InMemoryCacheService = { _ -> InMemoryCacheService(context) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt index cfd3818d16..18acc14255 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt @@ -9,65 +9,54 @@ package org.readium.r2.shared.publication.services -import java.util.* -import org.json.JSONObject -import org.readium.r2.shared.UserException -import org.readium.r2.shared.extensions.putIfNotEmpty -import org.readium.r2.shared.extensions.queryParameters -import org.readium.r2.shared.fetcher.FailureResource -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.StringResource -import org.readium.r2.shared.publication.* +import org.readium.r2.shared.publication.LocalizedString +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.PublicationServicesHolder +import org.readium.r2.shared.publication.ServiceFactory +import org.readium.r2.shared.publication.protection.ContentProtection +import org.readium.r2.shared.util.Error /** * Provides information about a publication's content protection and manages user rights. */ -interface ContentProtectionService : Publication.Service { +public interface ContentProtectionService : Publication.Service { /** * Whether the [Publication] has a restricted access to its resources, and can't be rendered in * a Navigator. */ - val isRestricted: Boolean + public val isRestricted: Boolean /** * The error raised when trying to unlock the [Publication], if any. */ - val error: UserException? + public val error: Error? /** * Credentials used to unlock this [Publication]. */ - val credentials: String? + public val credentials: String? /** * Manages consumption of user rights and permissions. */ - val rights: UserRights + public val rights: UserRights /** * Known technology for this type of Content Protection. */ - val scheme: ContentProtection.Scheme? get() = null + public val scheme: ContentProtection.Scheme? get() = null /** * User-facing name for this Content Protection, e.g. "Readium LCP". * It could be used in a sentence such as "Protected by {name}" */ - val name: LocalizedString? get() = scheme?.name - - override val links: List<Link> - get() = RouteHandler.links - - override fun get(link: Link): Resource? { - val route = RouteHandler.route(link) ?: return null - return route.handleRequest(link, this) - } + public val name: String? get() = null /** * Manages consumption of user rights and permissions. */ - interface UserRights { + public interface UserRights { /** * Returns whether the user is currently allowed to copy content to the pasteboard. @@ -76,7 +65,7 @@ interface ContentProtectionService : Publication.Service { * out or not. This should be called every time the "Copy" action will be displayed, * because the value might change during runtime. */ - val canCopy: Boolean + public val canCopy: Boolean /** * Returns whether the user is allowed to copy the given text to the pasteboard. @@ -87,14 +76,14 @@ interface ContentProtectionService : Publication.Service { * To be used before presenting, for example, a pop-up to share a selected portion of * content. */ - fun canCopy(text: String): Boolean + public fun canCopy(text: String): Boolean /** * Consumes the given text with the copy right. * * Returns whether the user is allowed to copy the given text. */ - fun copy(text: String): Boolean + public suspend fun copy(text: String): Boolean /** * Returns whether the user is currently allowed to print the content. @@ -102,7 +91,7 @@ interface ContentProtectionService : Publication.Service { * Navigators and reading apps can use this to know if the "Print" action should be greyed * out or not. */ - val canPrint: Boolean + public val canPrint: Boolean /** * Returns whether the user is allowed to print the given amount of pages. @@ -112,47 +101,47 @@ interface ContentProtectionService : Publication.Service { * * To be used before attempting to launch a print job, for example. */ - fun canPrint(pageCount: Int): Boolean + public fun canPrint(pageCount: Int): Boolean /** * Consumes the given amount of pages with the print right. * * Returns whether the user is allowed to print the given amount of pages. */ - fun print(pageCount: Int): Boolean + public suspend fun print(pageCount: Int): Boolean /** * A [UserRights] without any restriction. */ - object Unrestricted : UserRights { + public object Unrestricted : UserRights { override val canCopy: Boolean = true override fun canCopy(text: String): Boolean = true - override fun copy(text: String): Boolean = true + override suspend fun copy(text: String): Boolean = true override val canPrint: Boolean = true override fun canPrint(pageCount: Int): Boolean = true - override fun print(pageCount: Int): Boolean = true + override suspend fun print(pageCount: Int): Boolean = true } /** * A [UserRights] which forbids any right. */ - object AllRestricted : UserRights { + public object AllRestricted : UserRights { override val canCopy: Boolean = false override fun canCopy(text: String): Boolean = false - override fun copy(text: String): Boolean = false + override suspend fun copy(text: String): Boolean = false override val canPrint: Boolean = false override fun canPrint(pageCount: Int): Boolean = false - override fun print(pageCount: Int): Boolean = false + override suspend fun print(pageCount: Int): Boolean = false } } } @@ -167,197 +156,65 @@ private val PublicationServicesHolder.protectionService: ContentProtectionServic } /** Factory to build a [ContentProtectionService]. */ -var Publication.ServicesBuilder.contentProtectionServiceFactory: ServiceFactory? +public var Publication.ServicesBuilder.contentProtectionServiceFactory: ServiceFactory? get() = get(ContentProtectionService::class) set(value) = set(ContentProtectionService::class, value) /** * Returns whether this Publication is protected by a Content Protection technology. */ -val Publication.isProtected: Boolean +public val Publication.isProtected: Boolean get() = protectionService != null /** * Whether the [Publication] has a restricted access to its resources, and can't be rendered in * a Navigator. */ -val Publication.isRestricted: Boolean +public val Publication.isRestricted: Boolean get() = protectionService?.isRestricted ?: false /** * The error raised when trying to unlock the [Publication], if any. */ -val Publication.protectionError: UserException? +public val Publication.protectionError: Error? get() = protectionService?.error /** * Credentials used to unlock this [Publication]. */ -val Publication.credentials: String? +public val Publication.credentials: String? get() = protectionService?.credentials /** * Manages consumption of user rights and permissions. */ -val Publication.rights: ContentProtectionService.UserRights +public val Publication.rights: ContentProtectionService.UserRights get() = protectionService?.rights ?: ContentProtectionService.UserRights.Unrestricted /** * Known technology for this type of Content Protection. */ -val Publication.protectionScheme: ContentProtection.Scheme? +public val Publication.protectionScheme: ContentProtection.Scheme? get() = protectionService?.scheme /** * User-facing localized name for this Content Protection, e.g. "Readium LCP". * It could be used in a sentence such as "Protected by {name}". */ -val Publication.protectionLocalizedName: LocalizedString? - get() = protectionService?.name +@Suppress("UnusedReceiverParameter") +@Deprecated( + "Localize protection names yourself.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("protectionName") +) +public val Publication.protectionLocalizedName: LocalizedString + get() = throw NotImplementedError() /** * User-facing name for this Content Protection, e.g. "Readium LCP". * It could be used in a sentence such as "Protected by {name}". */ -val Publication.protectionName: String? - get() = protectionLocalizedName?.string - -private sealed class RouteHandler { - - companion object { - - private val handlers = listOf( - ContentProtectionHandler, - RightsCopyHandler, - RightsPrintHandler - ) - - val links = handlers.map { it.link } - - fun route(link: Link): RouteHandler? = handlers.firstOrNull { it.acceptRequest(link) } - } - - abstract val link: Link - - abstract fun acceptRequest(link: Link): Boolean - - abstract fun handleRequest(link: Link, service: ContentProtectionService): Resource - - object ContentProtectionHandler : RouteHandler() { - - override val link = Link( - href = "/~readium/content-protection", - type = "application/vnd.readium.content-protection+json" - ) - - override fun acceptRequest(link: Link): Boolean = link.href == this.link.href - - override fun handleRequest(link: Link, service: ContentProtectionService): Resource = - StringResource(link) { - JSONObject().apply { - put("isRestricted", service.isRestricted) - putOpt("error", service.error?.localizedMessage) - putIfNotEmpty("name", service.name) - put("rights", service.rights.toJSON()) - }.toString() - } - } - - object RightsCopyHandler : RouteHandler() { - - override val link: Link = Link( - href = "/~readium/rights/copy{?text,peek}", - type = "application/vnd.readium.rights.copy+json", - templated = true - ) - - override fun acceptRequest(link: Link): Boolean = link.href.startsWith("/~readium/rights/copy") - - override fun handleRequest(link: Link, service: ContentProtectionService): Resource { - val parameters = link.href.queryParameters() - val text = parameters["text"] - ?: return FailureResource( - link, - Resource.Exception.BadRequest( - parameters, - IllegalArgumentException("'text' parameter is required") - ) - ) - val peek = (parameters["peek"] ?: "false").toBooleanOrNull() - ?: return FailureResource( - link, - Resource.Exception.BadRequest( - parameters, - IllegalArgumentException("if present, 'peek' must be true or false") - ) - ) - - val copyAllowed = with(service.rights) { if (peek) canCopy(text) else copy(text) } - - return if (!copyAllowed) - FailureResource(link, Resource.Exception.Forbidden()) - else - StringResource(link, "true") - } - } - - object RightsPrintHandler : RouteHandler() { - - override val link = Link( - href = "/~readium/rights/print{?pageCount,peek}", - type = "application/vnd.readium.rights.print+json", - templated = true - ) - - override fun acceptRequest(link: Link): Boolean = link.href.startsWith("/~readium/rights/print") - - override fun handleRequest(link: Link, service: ContentProtectionService): Resource { - val parameters = link.href.queryParameters() - val pageCountString = parameters["pageCount"] - ?: return FailureResource( - link, - Resource.Exception.BadRequest( - parameters, - IllegalArgumentException("'pageCount' parameter is required") - ) - ) - - val pageCount = pageCountString.toIntOrNull()?.takeIf { it >= 0 } - ?: return FailureResource( - link, - Resource.Exception.BadRequest( - parameters, - IllegalArgumentException("'pageCount' must be a positive integer") - ) - ) - val peek = (parameters["peek"] ?: "false").toBooleanOrNull() - ?: return FailureResource( - link, - Resource.Exception.BadRequest( - parameters, - IllegalArgumentException("if present, 'peek' must be true or false") - ) - ) - - val printAllowed = with(service.rights) { if (peek) canPrint(pageCount) else print(pageCount) } - - return if (!printAllowed) - FailureResource(link, Resource.Exception.Forbidden()) - else - StringResource(link, "true") - } - } - - fun String.toBooleanOrNull(): Boolean? = when (this.lowercase(Locale.getDefault())) { - "true" -> true - "false" -> false - else -> null - } - - fun ContentProtectionService.UserRights.toJSON() = JSONObject().apply { - put("canCopy", canCopy) - put("canPrint", canPrint) - } -} +public val Publication.protectionName: String? + get() = protectionService?.name diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index dff57253a7..0239c692e3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -10,17 +10,17 @@ package org.readium.r2.shared.publication.services import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.util.Size import org.readium.r2.shared.extensions.scaleToFit -import org.readium.r2.shared.extensions.toPng -import org.readium.r2.shared.fetcher.BytesResource -import org.readium.r2.shared.fetcher.FailureResource -import org.readium.r2.shared.fetcher.LazyResource -import org.readium.r2.shared.fetcher.Resource import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.ServiceFactory +import org.readium.r2.shared.publication.firstWithRel +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.decodeBitmap +import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.resource.Resource /** * Provides an easy access to a bitmap version of the publication cover. @@ -39,80 +39,63 @@ import org.readium.r2.shared.publication.ServiceFactory * - generating a bitmap from scratch using the publication's title * - using a cover selected by the user. */ -interface CoverService : Publication.Service { +public interface CoverService : Publication.Service { /** * Returns the publication cover as a [Bitmap] at its maximum size. * * If the cover is not a bitmap format (e.g. SVG), it should be scaled down to fit the screen. */ - suspend fun cover(): Bitmap? + public suspend fun cover(): Bitmap? /** * Returns the publication cover as a [Bitmap], scaled down to fit the given [maxSize]. */ - suspend fun coverFitting(maxSize: Size): Bitmap? = cover()?.scaleToFit(maxSize) -} - -private suspend fun Publication.coverFromManifest(): Bitmap? { - for (link in linksWithRel("cover")) { - val data = get(link).read().getOrNull() ?: continue - return BitmapFactory.decodeByteArray(data, 0, data.size) ?: continue - } - return null + public suspend fun coverFitting(maxSize: Size): Bitmap? = cover()?.scaleToFit(maxSize) } /** * Returns the publication cover as a [Bitmap] at its maximum size. */ -suspend fun Publication.cover(): Bitmap? { +public suspend fun Publication.cover(): Bitmap? = findService(CoverService::class)?.cover()?.let { return it } - return coverFromManifest() -} /** * Returns the publication cover as a [Bitmap], scaled down to fit the given [maxSize]. */ -suspend fun Publication.coverFitting(maxSize: Size): Bitmap? { +public suspend fun Publication.coverFitting(maxSize: Size): Bitmap? = findService(CoverService::class)?.coverFitting(maxSize)?.let { return it } - return coverFromManifest()?.scaleToFit(maxSize) -} /** Factory to build a [CoverService]. */ -var Publication.ServicesBuilder.coverServiceFactory: ServiceFactory? +public var Publication.ServicesBuilder.coverServiceFactory: ServiceFactory? get() = get(CoverService::class) set(value) = set(CoverService::class, value) -/** - * A [CoverService] which provides a unique cover for each Publication. - */ -abstract class GeneratedCoverService : CoverService { - - private val coverLink = Link( - href = "/~readium/cover", - type = "image/png", - rels = setOf("cover") - ) - - override val links: List<Link> = listOf(coverLink) - - abstract override suspend fun cover(): Bitmap - - override fun get(link: Link): Resource? { - if (link.href != coverLink.href) - return null - - return LazyResource { - val cover = cover() - val png = cover.toPng() - if (png == null) { - val error = Exception("Unable to convert cover to PNG.") - FailureResource(coverLink, error) - } else { - @Suppress("NAME_SHADOWING") - val link = coverLink.copy(width = cover.width, height = cover.height) - BytesResource(link, png) - } +internal class ResourceCoverService( + private val coverUrl: Url, + private val container: Container<Resource> +) : CoverService { + + override suspend fun cover(): Bitmap? { + val resource = container[coverUrl] + ?: return null + + return resource + .read() + .flatMap { it.decodeBitmap() } + .getOrNull() + } + + companion object { + + fun createFactory(): (Publication.Service.Context) -> ResourceCoverService? = { + val publicationContent: List<Link> = + it.manifest.resources + it.manifest.readingOrder + + publicationContent + .firstWithRel("cover") + ?.url() + ?.let { url -> ResourceCoverService(url, it.container) } } } } @@ -120,11 +103,13 @@ abstract class GeneratedCoverService : CoverService { /** * A [CoverService] which uses a provided in-memory bitmap. */ -class InMemoryCoverService internal constructor(private val cover: Bitmap) : GeneratedCoverService() { +public class InMemoryCoverService internal constructor(private val cover: Bitmap) : CoverService { - companion object { - fun createFactory(cover: Bitmap?): ServiceFactory? = { cover?.let { InMemoryCoverService(it) } } + public companion object { + public fun createFactory(cover: Bitmap?): ServiceFactory = { + cover?.let { InMemoryCoverService(it) } + } } - override suspend fun cover(): Bitmap = cover + public override suspend fun cover(): Bitmap = cover } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/LocatorService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/LocatorService.kt index 4b5ef8741e..f2ad560853 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/LocatorService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/LocatorService.kt @@ -18,34 +18,34 @@ import timber.log.Timber * - Converting a [Locator] which was created from an alternate manifest with a different reading * order. For example, when downloading a streamed manifest or offloading a package. */ -interface LocatorService : Publication.Service { +public interface LocatorService : Publication.Service { /** Locates the target of the given [locator]. */ - suspend fun locate(locator: Locator): Locator? + public suspend fun locate(locator: Locator): Locator? /** Locates the target at the given [totalProgression] relative to the whole publication. */ - suspend fun locateProgression(totalProgression: Double): Locator? + public suspend fun locateProgression(totalProgression: Double): Locator? } /** Locates the target of the given [locator]. */ -suspend fun Publication.locate(locator: Locator): Locator? = +public suspend fun Publication.locate(locator: Locator): Locator? = findService(LocatorService::class)?.locate(locator) -/** Locates the target at the given [progression] relative to the whole publication. */ -suspend fun Publication.locateProgression(totalProgression: Double): Locator? = +/** Locates the target at the given progression relative to the whole publication. */ +public suspend fun Publication.locateProgression(totalProgression: Double): Locator? = findService(LocatorService::class)?.locateProgression(totalProgression) /** Factory to build a [LocatorService] */ -var Publication.ServicesBuilder.locatorServiceFactory: ServiceFactory? +public var Publication.ServicesBuilder.locatorServiceFactory: ServiceFactory? get() = get(LocatorService::class) set(value) = set(LocatorService::class, value) -open class DefaultLocatorService( - val readingOrder: List<Link>, - val positionsByReadingOrder: suspend () -> List<List<Locator>> +public open class DefaultLocatorService( + public val readingOrder: List<Link>, + public val positionsByReadingOrder: suspend () -> List<List<Locator>> ) : LocatorService { - constructor(readingOrder: List<Link>, services: PublicationServicesHolder) : + public constructor(readingOrder: List<Link>, services: PublicationServicesHolder) : this(readingOrder, positionsByReadingOrder = { services.findService(PositionsService::class)?.positionsByReadingOrder() ?: emptyList() }) @@ -64,7 +64,11 @@ open class DefaultLocatorService( ?: return null return position.copyWithLocations( - progression = resourceProgressionFor(totalProgression, positions, readingOrderIndex = readingOrderIndex) + progression = resourceProgressionFor( + totalProgression, + positions, + readingOrderIndex = readingOrderIndex + ) ?: position.locations.progression, totalProgression = totalProgression ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt index f59fd811fa..ea35fd6505 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt @@ -11,88 +11,72 @@ package org.readium.r2.shared.publication.services import kotlinx.coroutines.runBlocking import org.json.JSONObject +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.extensions.mapNotNull import org.readium.r2.shared.extensions.toJsonOrNull -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.StringResource -import org.readium.r2.shared.publication.* -import org.readium.r2.shared.toJSON - -private val positionsLink = Link( - href = "/~readium/positions", - type = "application/vnd.readium.position-list+json" -) +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.PublicationServicesHolder +import org.readium.r2.shared.publication.ServiceFactory +import org.readium.r2.shared.publication.firstWithMediaType +import org.readium.r2.shared.publication.firstWithRel +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpRequest +import org.readium.r2.shared.util.http.fetchString +import org.readium.r2.shared.util.mediatype.MediaType + +private val positionsMediaType = + MediaType("application/vnd.readium.position-list+json")!! /** * Provides a list of discrete locations in the publication, no matter what the original format is. */ -interface PositionsService : Publication.Service { +public interface PositionsService : Publication.Service { /** * Returns the list of all the positions in the publication, grouped by the resource reading order index. */ - suspend fun positionsByReadingOrder(): List<List<Locator>> + public suspend fun positionsByReadingOrder(): List<List<Locator>> /** * Returns the list of all the positions in the publication. */ - suspend fun positions(): List<Locator> = positionsByReadingOrder().flatten() - - override val links get() = listOf(positionsLink) - - override fun get(link: Link): Resource? { - if (link.href != positionsLink.href) - return null - - return StringResource(positionsLink) { - val positions = positions() - JSONObject().apply { - put("total", positions.size) - put("positions", positions.toJSON()) - }.toString() - } - } + public suspend fun positions(): List<Locator> = positionsByReadingOrder().flatten() } -private suspend fun Publication.positionsFromManifest(): List<Locator> = - links.firstWithMediaType(positionsLink.mediaType) - ?.let { get(it) } - ?.readAsString() - ?.getOrNull() - ?.toJsonOrNull() - ?.optJSONArray("positions") - ?.mapNotNull { Locator.fromJSON(it as? JSONObject) } - .orEmpty() - /** * Returns the list of all the positions in the publication, grouped by the resource reading order index. */ -suspend fun Publication.positionsByReadingOrder(): List<List<Locator>> { - findService(PositionsService::class)?.let { - return it.positionsByReadingOrder() - } - - val locators = positionsFromManifest().groupBy(Locator::href) - return readingOrder.map { locators[it.href].orEmpty() } -} +public suspend fun PublicationServicesHolder.positionsByReadingOrder(): List<List<Locator>> = + findService(PositionsService::class) + ?.positionsByReadingOrder() + .orEmpty() /** * Returns the list of all the positions in the publication. */ -suspend fun Publication.positions(): List<Locator> { - return findService(PositionsService::class)?.positions() - ?: positionsFromManifest() -} +public suspend fun PublicationServicesHolder.positions(): List<Locator> = + findService(PositionsService::class) + ?.positions() + .orEmpty() /** * List of all the positions in each resource, indexed by their href. */ -@Deprecated("Use [positionsByReadingOrder] instead", ReplaceWith("positionsByReadingOrder")) -val Publication.positionsByResource: Map<String, List<Locator>> +@Deprecated( + "Use [positionsByReadingOrder] instead", + ReplaceWith("positionsByReadingOrder"), + level = DeprecationLevel.ERROR +) +public val Publication.positionsByResource: Map<Url, List<Locator>> get() = runBlocking { positions().groupBy { it.href } } /** Factory to build a [PositionsService] */ -var Publication.ServicesBuilder.positionsServiceFactory: ServiceFactory? +public var Publication.ServicesBuilder.positionsServiceFactory: ServiceFactory? get() = get(PositionsService::class) set(value) = set(PositionsService::class, value) @@ -103,9 +87,9 @@ var Publication.ServicesBuilder.positionsServiceFactory: ServiceFactory? * @param fallbackMediaType Media type that will be used as a fallback if the Link doesn't specify * any. */ -class PerResourcePositionsService( +public class PerResourcePositionsService( private val readingOrder: List<Link>, - private val fallbackMediaType: String + private val fallbackMediaType: MediaType ) : PositionsService { override suspend fun positionsByReadingOrder(): List<List<Locator>> { @@ -114,8 +98,8 @@ class PerResourcePositionsService( return readingOrder.mapIndexed { index, link -> listOf( Locator( - href = link.href, - type = link.type ?: fallbackMediaType, + href = link.url(), + mediaType = link.mediaType ?: fallbackMediaType, title = link.title, locations = Locator.Locations( position = index + 1, @@ -126,9 +110,9 @@ class PerResourcePositionsService( } } - companion object { + public companion object { - fun createFactory(fallbackMediaType: String): (Publication.Service.Context) -> PerResourcePositionsService = { + public fun createFactory(fallbackMediaType: MediaType): (Publication.Service.Context) -> PerResourcePositionsService = { PerResourcePositionsService( readingOrder = it.manifest.readingOrder, fallbackMediaType = fallbackMediaType @@ -136,3 +120,52 @@ class PerResourcePositionsService( } } } + +@InternalReadiumApi +public class WebPositionsService( + private val manifest: Manifest, + private val httpClient: HttpClient +) : PositionsService { + + private lateinit var _positions: List<Locator> + + private val links: List<Link> = + listOfNotNull( + manifest.links.firstWithMediaType(positionsMediaType) + ) + + override suspend fun positions(): List<Locator> { + if (!::_positions.isInitialized) { + _positions = computePositions() + } + + return _positions + } + + override suspend fun positionsByReadingOrder(): List<List<Locator>> { + val locators = positions().groupBy(Locator::href) + return manifest.readingOrder.map { locators[it.url()].orEmpty() } + } + + private suspend fun computePositions(): List<Locator> { + val positionsLink = links.firstOrNull() + ?: return emptyList() + val selfLink = manifest.links.firstWithRel("self") + val positionsUrl = (positionsLink.url(base = selfLink?.url()) as? AbsoluteUrl) + ?: return emptyList() + + return httpClient.fetchString(HttpRequest(positionsUrl)) + .getOrNull() + ?.toJsonOrNull() + ?.optJSONArray("positions") + ?.mapNotNull { Locator.fromJSON(it as? JSONObject) } + .orEmpty() + } + + public companion object { + + public fun createFactory(httpClient: HttpClient): (Publication.Service.Context) -> WebPositionsService = { + WebPositionsService(it.manifest, httpClient) + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/Content.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/Content.kt index d28ce9328d..62c966ba0e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/Content.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/Content.kt @@ -17,17 +17,17 @@ import org.readium.r2.shared.util.Language * Provides an iterable list of content [Element]s. */ @ExperimentalReadiumApi -interface Content { +public interface Content { /** * Represents a single semantic content element part of a publication. */ @ExperimentalReadiumApi - interface Element : AttributesHolder { + public interface Element : AttributesHolder { /** * Locator targeting this element in the Publication. */ - val locator: Locator + public val locator: Locator } /** @@ -35,29 +35,29 @@ interface Content { * * The default implementation returns the first accessibility label associated to the element. */ - interface TextualElement : Element { + public interface TextualElement : Element { /** Human-readable text representation for this element. */ - val text: String? get() = accessibilityLabel + public val text: String? get() = accessibilityLabel } /** An element referencing an embedded external resource. */ - interface EmbeddedElement : Element { + public interface EmbeddedElement : Element { /** Referenced resource in the publication. */ - val embeddedLink: Link + public val embeddedLink: Link } /** An audio clip. */ - data class AudioElement( + public data class AudioElement( override val locator: Locator, override val embeddedLink: Link, - override val attributes: List<Attribute<*>> = emptyList(), + override val attributes: List<Attribute<*>> = emptyList() ) : EmbeddedElement, TextualElement /** A video clip. */ - data class VideoElement( + public data class VideoElement( override val locator: Locator, override val embeddedLink: Link, - override val attributes: List<Attribute<*>> = emptyList(), + override val attributes: List<Attribute<*>> = emptyList() ) : EmbeddedElement, TextualElement /** @@ -65,11 +65,11 @@ interface Content { * * @param caption Short piece of text associated with the image. */ - data class ImageElement( + public data class ImageElement( override val locator: Locator, override val embeddedLink: Link, val caption: String?, - override val attributes: List<Attribute<*>> = emptyList(), + override val attributes: List<Attribute<*>> = emptyList() ) : EmbeddedElement, TextualElement { override val text: String? get() = @@ -85,11 +85,11 @@ interface Content { * @param role Purpose of this element in the broader context of the document. * @param segments Ranged portions of text with associated attributes. */ - data class TextElement( + public data class TextElement( override val locator: Locator, val role: Role, val segments: List<Segment>, - override val attributes: List<Attribute<*>> = emptyList(), + override val attributes: List<Attribute<*>> = emptyList() ) : TextualElement { override val text: String @@ -98,23 +98,23 @@ interface Content { /** * Represents a purpose of an element in the broader context of the document. */ - interface Role { + public interface Role { /** * Title of a section. * * @param level Heading importance, 1 being the highest. */ - data class Heading(val level: Int) : Role + public data class Heading(val level: Int) : Role /** * Normal body of content. */ - object Body : Role + public object Body : Role /** * A footnote at the bottom of a document. */ - object Footnote : Role + public object Footnote : Role /** * A quotation. @@ -122,7 +122,7 @@ interface Content { * @param referenceUrl URL to the source for this quote. * @param referenceTitle Name of the source for this quote. */ - data class Quote( + public data class Quote( val referenceUrl: URL?, val referenceTitle: String? ) : Role @@ -135,17 +135,17 @@ interface Content { * @param text Text in the segment. * @param attributes Attributes associated with this segment, e.g. language. */ - data class Segment( + public data class Segment( val locator: Locator, val text: String, - override val attributes: List<Attribute<*>>, + override val attributes: List<Attribute<*>> ) : AttributesHolder } /** * An attribute is an arbitrary key-value metadata pair. */ - data class Attribute<V>( + public data class Attribute<V>( val key: AttributeKey<V>, val value: V ) @@ -155,41 +155,43 @@ interface Content { * * The [V] phantom type is there to perform static type checking when requesting an attribute. */ - data class AttributeKey<V>(val id: String) { - companion object { - val ACCESSIBILITY_LABEL = AttributeKey<String>("accessibilityLabel") - val LANGUAGE = AttributeKey<Language>("language") + public data class AttributeKey<V>(val id: String) { + public companion object { + public val ACCESSIBILITY_LABEL: AttributeKey<String> = AttributeKey<String>( + "accessibilityLabel" + ) + public val LANGUAGE: AttributeKey<Language> = AttributeKey<Language>("language") } } /** * An object associated with a list of attributes. */ - interface AttributesHolder { + public interface AttributesHolder { /** * Associated list of attributes. */ - val attributes: List<Attribute<*>> + public val attributes: List<Attribute<*>> - val language: Language? + public val language: Language? get() = attribute(AttributeKey.LANGUAGE) - val accessibilityLabel: String? + public val accessibilityLabel: String? get() = attribute(AttributeKey.ACCESSIBILITY_LABEL) /** * Gets the first attribute with the given [key]. */ @Suppress("UNCHECKED_CAST") - fun <V> attribute(key: AttributeKey<V>): V? = + public fun <V> attribute(key: AttributeKey<V>): V? = attributes.firstOrNull { it.key == key }?.value as V /** * Gets all the attributes with the given [key]. */ @Suppress("UNCHECKED_CAST") - fun <V> attributes(key: AttributeKey<V>): List<V> = + public fun <V> attributes(key: AttributeKey<V>): List<V> = attributes .filter { it.key == key } .map { it.value as V } @@ -202,44 +204,44 @@ interface Content { * to any of both methods. */ @ExperimentalReadiumApi - interface Iterator { + public interface Iterator { /** * Returns true if the iterator has a next element, suspending the caller while processing * it. */ - suspend operator fun hasNext(): Boolean + public suspend operator fun hasNext(): Boolean /** * Retrieves the element computed by a preceding call to [hasNext], or throws an * [IllegalStateException] if [hasNext] was not invoked. This method should only be used in * pair with [hasNext]. */ - operator fun next(): Element + public operator fun next(): Element /** * Advances to the next item and returns it, or null if we reached the end. */ - suspend fun nextOrNull(): Element? = + public suspend fun nextOrNull(): Element? = if (hasNext()) next() else null /** * Returns true if the iterator has a previous element, suspending the caller while processing * it. */ - suspend fun hasPrevious(): Boolean + public suspend fun hasPrevious(): Boolean /** * Retrieves the element computed by a preceding call to [hasPrevious], or throws an * [IllegalStateException] if [hasPrevious] was not invoked. This method should only be used in * pair with [hasPrevious]. */ - fun previous(): Element + public fun previous(): Element /** * Advances to the previous item and returns it, or null if we reached the beginning. */ - suspend fun previousOrNull(): Element? = + public suspend fun previousOrNull(): Element? = if (hasPrevious()) previous() else null } @@ -247,7 +249,7 @@ interface Content { * Extracts the full raw text, or returns null if no text content can be found. * @param separator Separator to use between individual elements. Defaults to newline. */ - suspend fun text(separator: String = "\n"): String? = + public suspend fun text(separator: String = "\n"): String? = elements() .mapNotNull { (it as? TextualElement)?.text } .joinToString(separator = separator) @@ -256,12 +258,12 @@ interface Content { /** * Creates a new iterator for this content. */ - operator fun iterator(): Iterator + public operator fun iterator(): Iterator /** * Returns all the elements as a list. */ - suspend fun elements(): List<Element> = + public suspend fun elements(): List<Element> = buildList { for (element in this@Content) { add(element) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentService.kt index 00cd125816..d21f160040 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentService.kt @@ -7,25 +7,25 @@ package org.readium.r2.shared.publication.services.content import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.* import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.publication.services.content.iterators.PublicationContentIterator import org.readium.r2.shared.publication.services.content.iterators.ResourceContentIteratorFactory -import org.readium.r2.shared.util.Ref +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.resource.Resource /** * Provides a way to extract the raw [Content] of a [Publication]. */ @ExperimentalReadiumApi -interface ContentService : Publication.Service { +public interface ContentService : Publication.Service { /** * Creates a [Content] starting from the given [start] location. * * The implementation must be fast and non-blocking. Do the actual extraction inside the * [Content] implementation. */ - fun content(start: Locator?): Content + public fun content(start: Locator?): Content } /** @@ -33,7 +33,7 @@ interface ContentService : Publication.Service { * publication when missing. */ @ExperimentalReadiumApi -fun Publication.content(start: Locator? = null): Content? = +public fun Publication.content(start: Locator? = null): Content? = contentService?.content(start) @ExperimentalReadiumApi @@ -42,7 +42,7 @@ private val Publication.contentService: ContentService? /** Factory to build a [ContentService] */ @ExperimentalReadiumApi -var Publication.ServicesBuilder.contentServiceFactory: ServiceFactory? +public var Publication.ServicesBuilder.contentServiceFactory: ServiceFactory? get() = get(ContentService::class) set(value) = set(ContentService::class, value) @@ -51,31 +51,41 @@ var Publication.ServicesBuilder.contentServiceFactory: ServiceFactory? * [ResourceContentIteratorFactory]. */ @ExperimentalReadiumApi -class DefaultContentService( - private val publication: Ref<Publication>, +public class DefaultContentService( + private val manifest: Manifest, + private val container: Container<Resource>, + private val services: PublicationServicesHolder, private val resourceContentIteratorFactories: List<ResourceContentIteratorFactory> ) : ContentService { - companion object { - fun createFactory( + public companion object { + public fun createFactory( resourceContentIteratorFactories: List<ResourceContentIteratorFactory> ): (Publication.Service.Context) -> DefaultContentService = { context -> - DefaultContentService(context.publication, resourceContentIteratorFactories) + DefaultContentService( + context.manifest, + context.container, + context.services, + resourceContentIteratorFactories + ) } } override fun content(start: Locator?): Content { - val publication = publication() ?: throw IllegalStateException("No Publication object") - return ContentImpl(publication, start) + return ContentImpl(manifest, container, services, start) } private inner class ContentImpl( - val publication: Publication, - val start: Locator?, + val manifest: Manifest, + val container: Container<Resource>, + val services: PublicationServicesHolder, + val start: Locator? ) : Content { override fun iterator(): Content.Iterator = PublicationContentIterator( - publication = publication, + manifest = manifest, + container = container, + services = services, startLocator = start, resourceContentIteratorFactories = resourceContentIteratorFactories ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentTokenizer.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentTokenizer.kt index 0603894ba8..c90b1eb58e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentTokenizer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentTokenizer.kt @@ -16,7 +16,7 @@ import org.readium.r2.shared.util.tokenizer.Tokenizer /** A tokenizer splitting a [Content.Element] into smaller pieces. */ @ExperimentalReadiumApi -fun interface ContentTokenizer : Tokenizer<Content.Element, Content.Element> +public fun interface ContentTokenizer : Tokenizer<Content.Element, Content.Element> /** * A [ContentTokenizer] using a [TextTokenizer] to split the text of the [Content.Element] into smaller @@ -27,7 +27,7 @@ fun interface ContentTokenizer : Tokenizer<Content.Element, Content.Element> * content. If false, [language] will be used only as a default when there is no data-specific information. */ @ExperimentalReadiumApi -class TextContentTokenizer( +public class TextContentTokenizer( private val language: Language?, private val overrideContentLanguage: Boolean = false, private val contextSnippetLength: Int = 50, @@ -37,9 +37,18 @@ class TextContentTokenizer( /** * A [ContentTokenizer] using the default [TextTokenizer] to split the text of the [Content.Element]. */ - constructor(language: Language?, unit: TextUnit, overrideContentLanguage: Boolean = false) : this( + public constructor( + language: Language?, + unit: TextUnit, + overrideContentLanguage: Boolean = false + ) : this( language = language, - textTokenizerFactory = { contentLanguage -> DefaultTextContentTokenizer(unit, contentLanguage) }, + textTokenizerFactory = { contentLanguage -> + DefaultTextContentTokenizer( + unit, + contentLanguage + ) + }, overrideContentLanguage = overrideContentLanguage ) @@ -66,8 +75,14 @@ class TextContentTokenizer( segment.language.takeUnless { overrideContentLanguage } ?: language private fun extractTextContextIn(string: String, range: IntRange): Locator.Text { - val after = string.substring(range.last, (range.last + contextSnippetLength).coerceAtMost(string.length)) - val before = string.substring((range.first - contextSnippetLength).coerceAtLeast(0), range.first) + val after = string.substring( + range.last, + (range.last + contextSnippetLength).coerceAtMost(string.length) + ) + val before = string.substring( + (range.first - contextSnippetLength).coerceAtLeast(0), + range.first + ) return Locator.Text( after = after.takeIf { it.isNotEmpty() }, before = before.takeIf { it.isNotEmpty() }, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt index af8c534f30..49e956b1fa 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt @@ -6,8 +6,9 @@ package org.readium.r2.shared.publication.services.content.iterators +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.jsoup.Jsoup -import org.jsoup.internal.StringUtil import org.jsoup.nodes.Element import org.jsoup.nodes.Node import org.jsoup.nodes.TextNode @@ -15,19 +16,32 @@ import org.jsoup.parser.Parser import org.jsoup.select.NodeTraversor import org.jsoup.select.NodeVisitor import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.extensions.tryOrNull -import org.readium.r2.shared.fetcher.Resource import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.publication.PublicationServicesHolder import org.readium.r2.shared.publication.html.cssSelector import org.readium.r2.shared.publication.services.content.Content -import org.readium.r2.shared.publication.services.content.Content.* -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.publication.services.content.Content.Attribute +import org.readium.r2.shared.publication.services.content.Content.AttributeKey +import org.readium.r2.shared.publication.services.content.Content.AudioElement +import org.readium.r2.shared.publication.services.content.Content.ImageElement +import org.readium.r2.shared.publication.services.content.Content.TextElement +import org.readium.r2.shared.publication.services.content.Content.VideoElement +import org.readium.r2.shared.publication.services.positionsByReadingOrder +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Language +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.decodeString +import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.toDebugDescription import org.readium.r2.shared.util.use - -// FIXME: Support custom skipped elements? +import timber.log.Timber /** * Iterates an HTML [resource], starting from the given [locator]. @@ -40,20 +54,40 @@ import org.readium.r2.shared.util.use * Locators will contain a `before` context of up to `beforeMaxLength` characters. */ @ExperimentalReadiumApi -class HtmlResourceContentIterator( +public class HtmlResourceContentIterator internal constructor( private val resource: Resource, + private val totalProgressionRange: ClosedRange<Double>?, private val locator: Locator, private val beforeMaxLength: Int = 50 ) : Content.Iterator { - companion object { - /** - * Creates a new factory for [HtmlResourceContentIterator]. - */ - fun createFactory(): ResourceContentIteratorFactory = { res, locator -> - if (res.link().mediaType.matchesAny(MediaType.HTML, MediaType.XHTML)) - HtmlResourceContentIterator(res, locator) - else null + public class Factory : ResourceContentIteratorFactory { + override suspend fun create( + manifest: Manifest, + servicesHolder: PublicationServicesHolder, + readingOrderIndex: Int, + resource: Resource, + mediaType: MediaType, + locator: Locator + ): Content.Iterator? { + if (!mediaType.matchesAny(MediaType.HTML, MediaType.XHTML)) { + return null + } + + val positions = servicesHolder.positionsByReadingOrder() + return HtmlResourceContentIterator( + resource, + totalProgressionRange = positions.getOrNull(readingOrderIndex) + ?.firstOrNull()?.locations?.totalProgression + ?.let { start -> + val end = positions.getOrNull(readingOrderIndex + 1) + ?.firstOrNull()?.locations?.totalProgression + ?: 1.0 + + start..end + }, + locator = locator + ) } } @@ -85,7 +119,9 @@ class HtmlResourceContentIterator( currentElement ?.takeIf { it.delta == -1 }?.element ?.also { currentElement = null } - ?: throw IllegalStateException("Called previous() without a successful call to hasPrevious() first") + ?: throw IllegalStateException( + "Called previous() without a successful call to hasPrevious() first" + ) override suspend fun hasNext(): Boolean { if (currentElement?.delta == +1) return true @@ -105,7 +141,9 @@ class HtmlResourceContentIterator( currentElement ?.takeIf { it.delta == +1 }?.element ?.also { currentElement = null } - ?: throw IllegalStateException("Called next() without a successful call to hasNext() first") + ?: throw IllegalStateException( + "Called next() without a successful call to hasNext() first" + ) private var currentIndex: Int? = null @@ -115,21 +153,67 @@ class HtmlResourceContentIterator( private var parsedElements: ParsedElements? = null - private suspend fun parseElements(): ParsedElements { - val document = resource.use { res -> - val html = res.readAsString().getOrThrow() - Jsoup.parse(html) + private suspend fun parseElements(): ParsedElements = + withContext(Dispatchers.Default) { + val document = resource.use { res -> + val html = res + .read() + .flatMap { it.decodeString() } + .getOrElse { + val error = DebugError("Failed to read HTML resource", it.cause) + Timber.w(error.toDebugDescription()) + return@withContext ParsedElements() + } + + Jsoup.parse(html) + } + + val contentParser = ContentParser( + baseLocator = locator, + startElement = locator.locations.cssSelector?.let { + tryOrNull { document.selectFirst(it) } + }, + beforeMaxLength = beforeMaxLength + ) + NodeTraversor.traverse(contentParser, document.body()) + val elements = contentParser.result() + val elementCount = elements.elements.size + if (elementCount == 0) { + return@withContext elements + } + + elements.copy( + elements = elements.elements.mapIndexed { index, element -> + val progression = index.toDouble() / elementCount + element.copy( + progression = progression, + totalProgression = totalProgressionRange?.let { + totalProgressionRange.start + progression * (totalProgressionRange.endInclusive - totalProgressionRange.start) + } + ) + } + ) } - val contentParser = ContentParser( - baseLocator = locator, - startElement = locator.locations.cssSelector?.let { - tryOrNull { document.selectFirst(it) } - }, - beforeMaxLength = beforeMaxLength - ) - NodeTraversor.traverse(contentParser, document.body()) - return contentParser.result() + private fun Content.Element.copy(progression: Double?, totalProgression: Double?): Content.Element { + fun Locator.update(): Locator = + copyWithLocations( + progression = progression, + totalProgression = totalProgression + ) + + return when (this) { + is TextElement -> copy( + locator = locator.update(), + segments = segments.map { + it.copy(locator = it.locator.update()) + } + ) + is AudioElement -> copy(locator = locator.update()) + is VideoElement -> copy(locator = locator.update()) + is ImageElement -> copy(locator = locator.update()) + else -> this + } } /** @@ -138,9 +222,9 @@ class HtmlResourceContentIterator( * The [startIndex] will be calculated from the element matched by the base [locator], if * possible. Defaults to 0. */ - data class ParsedElements( - val elements: List<Content.Element>, - val startIndex: Int, + public data class ParsedElements( + val elements: List<Content.Element> = emptyList(), + val startIndex: Int = 0 ) private class ContentParser( @@ -151,8 +235,11 @@ class HtmlResourceContentIterator( fun result() = ParsedElements( elements = elements, - startIndex = if (baseLocator.locations.progression == 1.0) elements.size - else startIndex + startIndex = if (baseLocator.locations.progression == 1.0) { + elements.size + } else { + startIndex + } ) private val elements = mutableListOf<Content.Element>() @@ -160,26 +247,41 @@ class HtmlResourceContentIterator( /** Segments accumulated for the current element. */ private val segmentsAcc = mutableListOf<TextElement.Segment>() + /** Text since the beginning of the current segment, after coalescing whitespaces. */ private var textAcc = StringBuilder() + /** Text content since the beginning of the resource, including whitespaces. */ private var wholeRawTextAcc: String? = null + /** Text content since the beginning of the current element, including whitespaces. */ private var elementRawTextAcc: String = "" + /** Text content since the beginning of the current segment, including whitespaces. */ private var rawTextAcc: String = "" + /** Language of the current segment. */ private var currentLanguage: String? = null - /** CSS selector of the current element. */ - private var currentCssSelector: String? = null /** LIFO stack of the current element's block ancestors. */ - private val breadcrumbs = mutableListOf<Element>() + private val breadcrumbs = mutableListOf<ParentElement>() + + private data class ParentElement( + val element: Element, + val cssSelector: String? + ) { + constructor(element: Element) : this( + element = element, + cssSelector = tryOrLog { element.cssSelector() } + ) + } override fun head(node: Node, depth: Int) { if (node is Element) { + val parent = ParentElement(node) if (node.isBlock) { - breadcrumbs.add(node) + flushText() + breadcrumbs.add(parent) } val tag = node.normalName() @@ -188,7 +290,9 @@ class HtmlResourceContentIterator( baseLocator.copy( locations = Locator.Locations( otherLocations = buildMap { - put("cssSelector", node.cssSelector() as Any) + parent.cssSelector?.let { + put("cssSelector", it as Any) + } } ) ) @@ -202,11 +306,11 @@ class HtmlResourceContentIterator( tag == "img" -> { flushText() - node.srcRelativeToHref(baseLocator.href)?.let { href -> + node.srcRelativeToHref(baseLocator.href)?.let { url -> elements.add( ImageElement( locator = elementLocator, - embeddedLink = Link(href = href), + embeddedLink = Link(href = url), caption = null, // FIXME: Get the caption from figcaption attributes = buildList { val alt = node.attr("alt").takeIf { it.isNotBlank() } @@ -222,15 +326,18 @@ class HtmlResourceContentIterator( tag == "audio" || tag == "video" -> { flushText() - val href = node.srcRelativeToHref(baseLocator.href) + val url = node.srcRelativeToHref(baseLocator.href) val link: Link? = - if (href != null) { - Link(href = href) + if (url != null) { + Link(href = url) } else { val sources = node.select("source") .mapNotNull { source -> - source.srcRelativeToHref(baseLocator.href)?.let { href -> - Link(href = href, type = source.attr("type").takeUnless { it.isBlank() }) + source.srcRelativeToHref(baseLocator.href)?.let { url -> + Link( + href = url, + mediaType = MediaType(source.attr("type")) + ) } } @@ -239,8 +346,20 @@ class HtmlResourceContentIterator( if (link != null) { when (tag) { - "audio" -> elements.add(AudioElement(locator = elementLocator, embeddedLink = link, attributes = emptyList())) - "video" -> elements.add(VideoElement(locator = elementLocator, embeddedLink = link, attributes = emptyList())) + "audio" -> elements.add( + AudioElement( + locator = elementLocator, + embeddedLink = link, + attributes = emptyList() + ) + ) + "video" -> elements.add( + VideoElement( + locator = elementLocator, + embeddedLink = link, + attributes = emptyList() + ) + ) else -> {} } } @@ -248,7 +367,6 @@ class HtmlResourceContentIterator( node.isBlock -> { flushText() - currentCssSelector = node.cssSelector() } } } @@ -262,20 +380,20 @@ class HtmlResourceContentIterator( currentLanguage = language } - rawTextAcc += Parser.unescapeEntities(node.wholeText, false) - appendNormalisedText(node) + val text = Parser.unescapeEntities(node.wholeText, false) + rawTextAcc += text + appendNormalisedText(text) } else if (node is Element) { if (node.isBlock) { - assert(breadcrumbs.last() == node) + assert(breadcrumbs.last().element == node) flushText() breadcrumbs.removeLast() } } } - private fun appendNormalisedText(textNode: TextNode) { - val text = Parser.unescapeEntities(textNode.wholeText, false) - StringUtil.appendNormalisedWhitespace(textAcc, text, lastCharIsWhitespace()) + private fun appendNormalisedText(text: String) { + textAcc.appendNormalisedWhitespace(text, lastCharIsWhitespace()) } private fun lastCharIsWhitespace(): Boolean = @@ -284,7 +402,9 @@ class HtmlResourceContentIterator( private fun flushText() { flushSegment() - if (startIndex == 0 && startElement != null && breadcrumbs.lastOrNull() == startElement) { + val parent = breadcrumbs.lastOrNull() + + if (startIndex == 0 && startElement != null && parent?.element == startElement) { startIndex = elements.size } @@ -295,11 +415,11 @@ class HtmlResourceContentIterator( segmentsAcc[segmentsAcc.size - 1] = segmentsAcc.last().run { copy(text = text.trimEnd()) } elements.add( - Content.TextElement( + TextElement( locator = baseLocator.copy( locations = Locator.Locations( otherLocations = buildMap { - currentCssSelector?.let { + parent?.cssSelector?.let { put("cssSelector", it as Any) } } @@ -332,12 +452,14 @@ class HtmlResourceContentIterator( text = trimmedText + whitespaceSuffix } + val parent = breadcrumbs.lastOrNull() + segmentsAcc.add( TextElement.Segment( locator = baseLocator.copy( locations = Locator.Locations( otherLocations = buildMap { - currentCssSelector?.let { + parent?.cssSelector?.let { put("cssSelector", it as Any) } } @@ -350,9 +472,9 @@ class HtmlResourceContentIterator( text = text, attributes = buildList { currentLanguage?.let { - add(Attribute(Content.AttributeKey.LANGUAGE, Language(it))) + add(Attribute(AttributeKey.LANGUAGE, Language(it))) } - }, + } ) ) } @@ -367,19 +489,77 @@ class HtmlResourceContentIterator( } } -private fun Locator.Text.Companion.trimmingText(text: String, before: String?): Locator.Text = - Locator.Text( - before = ((before ?: "") + text.takeWhile { it.isWhitespace() }).takeUnless { it.isBlank() }, - highlight = text.trim(), - after = text.takeLastWhile { it.isWhitespace() }.takeUnless { it.isBlank() } +private fun Locator.Text.Companion.trimmingText(text: String, before: String?): Locator.Text { + val leadingWhitespace = text.takeWhile { it.isWhitespace() } + val trailingWhitespace = text.takeLastWhile { it.isWhitespace() } + return Locator.Text( + before = ((before ?: "") + leadingWhitespace).takeUnless { it.isBlank() }, + highlight = text.substring( + leadingWhitespace.length, + text.length - trailingWhitespace.length + ), + after = trailingWhitespace.takeUnless { it.isBlank() } ) +} private val Node.language: String? get() = attr("xml:lang").takeUnless { it.isBlank() } ?: attr("lang").takeUnless { it.isBlank() } ?: parent()?.language -private fun Node.srcRelativeToHref(baseHref: String): String? = +private fun Node.srcRelativeToHref(baseUrl: Url): Url? = attr("src") .takeIf { it.isNotBlank() } - ?.let { Href(it, baseHref).string } + ?.let { Url(it) } + ?.let { baseUrl.resolve(it) } + +/** + * After normalizing the whitespace within a string, appends it to a string builder. + * + * Largely inspired by JSoup's `StringUtil.appendNormalisedWhitespace`. + * + * Note that we don't use directly JSoup's method because we need to keep the non-breaking + * spaces in the text. Otherwise, they will be lost post-text tokenization and Hypothesis won't + * match the results. + * + * @param string String to normalize whitespace within. + * @param stripLeading Set to true if you wish to remove any leading whitespace. + */ +private fun StringBuilder.appendNormalisedWhitespace( + string: String, + stripLeading: Boolean +) { + var lastWasWhite = false + var reachedNonWhite = false + val len = string.length + var c: Int + var i = 0 + while (i < len) { + c = string.codePointAt(i) + if (isWhitespace(c)) { + if (stripLeading && !reachedNonWhite || lastWasWhite) { + i += Character.charCount(c) + continue + } + append(' ') + lastWasWhite = true + } else if (!isInvisibleChar(c)) { + appendCodePoint(c) + lastWasWhite = false + reachedNonWhite = true + } + i += Character.charCount(c) + } +} + +/** + * Tests if a code point is "whitespace" as defined in the HTML spec. + */ +private fun isWhitespace(c: Int): Boolean { + return c == ' '.code || c == '\t'.code || c == '\n'.code || c == '\u000c'.code || c == '\r'.code +} + +private fun isInvisibleChar(c: Int): Boolean { + return c == 8203 || c == 173 // zero width sp, soft hyphen + // previously also included zw non join, zw join - but removing those breaks semantic meaning of text +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt index 6127040281..6048bf4cdf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt @@ -7,36 +7,56 @@ package org.readium.r2.shared.publication.services.content.iterators import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.fetcher.Resource import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.publication.PublicationServicesHolder import org.readium.r2.shared.publication.indexOfFirstWithHref import org.readium.r2.shared.publication.services.content.Content import org.readium.r2.shared.util.Either +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource /** - * Creates a [Content.Iterator] instance for the [Resource], starting from the given [Locator]. + * Creates a [Content.Iterator] instance for the [Resource], starting from the + * given [Locator]. * * Returns null if the resource media type is not supported. */ @ExperimentalReadiumApi -typealias ResourceContentIteratorFactory = - suspend (resource: Resource, locator: Locator) -> Content.Iterator? +public fun interface ResourceContentIteratorFactory { + + /** + * Creates a [Content.Iterator] instance for the [resource], starting from the given [locator]. + * + * Returns null if the resource media type is not supported. + */ + public suspend fun create( + manifest: Manifest, + servicesHolder: PublicationServicesHolder, + readingOrderIndex: Int, + resource: Resource, + mediaType: MediaType, + locator: Locator + ): Content.Iterator? +} /** - * A composite [Content.Iterator] which iterates through a whole [publication] and delegates the + * A composite [Content.Iterator] which iterates through a whole [manifest] and delegates the * iteration inside a given resource to media type-specific iterators. * - * @param publication The [Publication] which will be iterated through. + * @param manifest The [Manifest] of the publication which will be iterated through. * @param startLocator Starting [Locator] in the publication. * @param resourceContentIteratorFactories List of [ResourceContentIteratorFactory] which will be * used to create the iterator for each resource. The factories are tried in order until there's a * match. */ @ExperimentalReadiumApi -class PublicationContentIterator( - private val publication: Publication, +public class PublicationContentIterator( + private val manifest: Manifest, + private val container: Container<Resource>, + private val services: PublicationServicesHolder, private val startLocator: Locator?, private val resourceContentIteratorFactories: List<ResourceContentIteratorFactory> ) : Content.Iterator { @@ -68,7 +88,9 @@ class PublicationContentIterator( override fun previous(): Content.Element = currentElement ?.takeIf { it.direction == Direction.Backward }?.element - ?: throw IllegalStateException("Called previous() without a successful call to hasPrevious() first") + ?: throw IllegalStateException( + "Called previous() without a successful call to hasPrevious() first" + ) override suspend fun hasNext(): Boolean { currentElement = nextIn(Direction.Forward) @@ -78,7 +100,9 @@ class PublicationContentIterator( override fun next(): Content.Element = currentElement ?.takeIf { it.direction == Direction.Forward }?.element - ?: throw IllegalStateException("Called next() without a successful call to hasNext() first") + ?: throw IllegalStateException( + "Called next() without a successful call to hasNext() first" + ) private suspend fun nextIn(direction: Direction): ElementInDirection? { val iterator = currentIterator() ?: return null @@ -93,7 +117,7 @@ class PublicationContentIterator( } /** - * Returns the [Content.Iterator] for the current [Resource] in the reading order. + * Returns the [Content.Iterator] for the current resource in the reading order. */ private suspend fun currentIterator(): IndexedIterator? { if (_currentIterator == null) { @@ -107,7 +131,7 @@ class PublicationContentIterator( */ private suspend fun initialIterator(): IndexedIterator? { val index: Int = - startLocator?.let { publication.readingOrder.indexOfFirstWithHref(it.href) } + startLocator?.let { manifest.readingOrder.indexOfFirstWithHref(it.href) } ?: 0 val locations = startLocator.orProgression(0.0) @@ -121,7 +145,7 @@ class PublicationContentIterator( */ private suspend fun nextIteratorIn(direction: Direction, fromIndex: Int): IndexedIterator? { val index = fromIndex + direction.delta - if (!publication.readingOrder.indices.contains(index)) { + if (!manifest.readingOrder.indices.contains(index)) { return null } @@ -140,13 +164,14 @@ class PublicationContentIterator( * The [location] will be used to compute the starting [Locator] for the iterator. */ private suspend fun loadIteratorAt(index: Int, location: LocatorOrProgression): IndexedIterator? { - val link = publication.readingOrder[index] + val link = manifest.readingOrder[index] val locator = location.toLocator(link) ?: return null - val resource = publication.get(link) + val resource = container[link.url()] ?: return null + val mediaType = link.mediaType ?: return null return resourceContentIteratorFactories .firstNotNullOfOrNull { factory -> - factory(resource, locator) + factory.create(manifest, services, index, resource, mediaType, locator) } ?.let { IndexedIterator(index, it) } } @@ -163,7 +188,7 @@ class PublicationContentIterator( private fun LocatorOrProgression.toLocator(link: Link): Locator? = left - ?: publication.locatorFromLink(link)?.copyWithLocations(progression = right) + ?: manifest.locatorFromLink(link)?.copyWithLocations(progression = right) } /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt index e4e5aa327a..6806e89938 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt @@ -7,85 +7,49 @@ package org.readium.r2.shared.publication.services.search import android.os.Parcelable -import androidx.annotation.StringRes -import kotlinx.coroutines.CancellationException import kotlinx.parcelize.Parcelize -import org.readium.r2.shared.R -import org.readium.r2.shared.Search -import org.readium.r2.shared.UserException -import org.readium.r2.shared.fetcher.Resource +import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.LocatorCollection import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.ServiceFactory +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.http.HttpException +import org.readium.r2.shared.util.data.ReadError -@Search -typealias SearchTry<SuccessT> = Try<SuccessT, SearchException> +@ExperimentalReadiumApi +public typealias SearchTry<SuccessT> = Try<SuccessT, SearchError> /** * Represents an error which might occur during a search activity. */ -@Search -sealed class SearchException(content: Content, cause: Throwable? = null) : UserException(content, cause) { - constructor(@StringRes userMessageId: Int, vararg args: Any, cause: Throwable? = null) : - this(Content(userMessageId, *args), cause) - constructor(cause: UserException) : - this(Content(cause), cause) - - /** - * The publication is not searchable. - */ - object PublicationNotSearchable : SearchException(R.string.r2_shared_search_exception_publication_not_searchable) - - /** - * The provided search query cannot be handled by the service. - */ - class BadQuery(cause: UserException) : SearchException(cause) +@ExperimentalReadiumApi +public sealed class SearchError( + override val message: String, + override val cause: Error? = null +) : Error { /** * An error occurred while accessing one of the publication's resources. */ - class ResourceError(cause: Resource.Exception) : SearchException(cause) - - /** - * An error occurred while performing an HTTP request. - */ - class NetworkError(cause: HttpException) : SearchException(cause) + public class Reading(override val cause: ReadError) : + SearchError( + "An error occurred while accessing one of the publication's resources.", + cause + ) /** - * The search was cancelled by the caller. - * - * For example, when a coroutine or a network request is cancelled. + * An error occurring in the search engine. */ - object Cancelled : SearchException(R.string.r2_shared_search_exception_cancelled) - - /** For any other custom service error. */ - class Other(cause: Throwable) : SearchException(R.string.r2_shared_search_exception_other, cause = cause) - - companion object { - fun wrap(e: Throwable): SearchException = - when (e) { - is SearchException -> e - is CancellationException, is Resource.Exception.Cancelled -> Cancelled - is Resource.Exception -> ResourceError(e) - is HttpException -> - if (e.kind == HttpException.Kind.Cancelled) { - Cancelled - } else { - NetworkError(e) - } - else -> Other(e) - } - } + public class Engine(cause: Error) : + SearchError("An error occurred while searching.", cause) } /** * Provides a way to search terms in a publication. */ -@Search -interface SearchService : Publication.Service { +@ExperimentalReadiumApi +public interface SearchService : Publication.Service { /** * Holds the available search options and their current values. @@ -104,7 +68,7 @@ interface SearchService : Publication.Service { * officially recognized by Readium. */ @Parcelize - data class Options( + public data class Options( val caseSensitive: Boolean? = null, val diacriticSensitive: Boolean? = null, val wholeWord: Boolean? = null, @@ -116,7 +80,7 @@ interface SearchService : Publication.Service { /** * Syntactic sugar to access the [otherOptions] values by subscripting [Options] directly. */ - operator fun get(key: String): String? = otherOptions[key] + public operator fun get(key: String): String? = otherOptions[key] } /** @@ -124,28 +88,28 @@ interface SearchService : Publication.Service { * * If an option does not have a value, it is not supported by the service. */ - val options: Options + public val options: Options /** * Starts a new search through the publication content, with the given [query]. * * If an option is nil when calling search(), its value is assumed to be the default one. */ - suspend fun search(query: String, options: Options? = null): SearchTry<SearchIterator> + public suspend fun search(query: String, options: Options? = null): SearchIterator } /** * Indicates whether the content of this publication can be searched. */ -@Search -val Publication.isSearchable get() = - findService(SearchService::class) != null +@ExperimentalReadiumApi +public val Publication.isSearchable: Boolean + get() = findService(SearchService::class) != null /** * Default value for the search options of this publication. */ -@Search -val Publication.searchOptions: SearchService.Options get() = +@ExperimentalReadiumApi +public val Publication.searchOptions: SearchService.Options get() = findService(SearchService::class)?.options ?: SearchService.Options() /** @@ -153,23 +117,24 @@ val Publication.searchOptions: SearchService.Options get() = * * If an option is nil when calling [search], its value is assumed to be the default one for the * search service. + * + * Returns null if the publication is not searchable. */ -@Search -suspend fun Publication.search(query: String, options: SearchService.Options? = null): SearchTry<SearchIterator> = +@ExperimentalReadiumApi +public suspend fun Publication.search(query: String, options: SearchService.Options? = null): SearchIterator? = findService(SearchService::class)?.search(query, options) - ?: Try.failure(SearchException.PublicationNotSearchable) /** Factory to build a [SearchService] */ -@Search -var Publication.ServicesBuilder.searchServiceFactory: ServiceFactory? +@ExperimentalReadiumApi +public var Publication.ServicesBuilder.searchServiceFactory: ServiceFactory? get() = get(SearchService::class) set(value) = set(SearchService::class, value) /** * Iterates through search results. */ -@Search -interface SearchIterator : SuspendingCloseable { +@ExperimentalReadiumApi +public interface SearchIterator : SuspendingCloseable { /** * Number of matches for this search, if known. @@ -179,14 +144,14 @@ interface SearchIterator : SuspendingCloseable { * * The count might be updated after each call to [next]. */ - val resultCount: Int? get() = null + public val resultCount: Int? get() = null /** * Retrieves the next page of results. * * @return Null when reaching the end of the publication, or an error in case of failure. */ - suspend fun next(): SearchTry<LocatorCollection?> + public suspend fun next(): SearchTry<LocatorCollection?> /** * Closes any resources allocated for the search query, such as a cursor. @@ -197,7 +162,7 @@ interface SearchIterator : SuspendingCloseable { /** * Performs the given operation on each result page of this [SearchIterator]. */ - suspend fun forEach(action: (LocatorCollection) -> Unit): SearchTry<Unit> { + public suspend fun forEach(action: (LocatorCollection) -> Unit): SearchTry<Unit> { while (true) { val res = next() res diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt index 86aea70efc..1db499bc6c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt @@ -14,19 +14,21 @@ import android.os.Build import androidx.annotation.RequiresApi import java.text.StringCharacterIterator import java.util.* +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.readium.r2.shared.Search -import org.readium.r2.shared.fetcher.DefaultResourceContentExtractorFactory -import org.readium.r2.shared.fetcher.ResourceContentExtractor -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.LocatorCollection -import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.* import org.readium.r2.shared.publication.services.positionsByReadingOrder import org.readium.r2.shared.publication.services.search.SearchService.Options -import org.readium.r2.shared.util.Ref +import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.content.DefaultResourceContentExtractorFactory +import org.readium.r2.shared.util.resource.content.ResourceContentExtractor import timber.log.Timber /** @@ -39,24 +41,28 @@ import timber.log.Timber * * The actual search is implemented by the provided [searchAlgorithm]. */ -@Search -class StringSearchService( - private val publication: Ref<Publication>, - val language: String?, +@ExperimentalReadiumApi +public class StringSearchService( + private val manifest: Manifest, + private val container: Container<Resource>, + private val services: PublicationServicesHolder, + private val language: String?, private val snippetLength: Int, private val searchAlgorithm: Algorithm, - private val extractorFactory: ResourceContentExtractor.Factory, + private val extractorFactory: ResourceContentExtractor.Factory ) : SearchService { - companion object { - fun createDefaultFactory( + public companion object { + public fun createDefaultFactory( snippetLength: Int = 200, searchAlgorithm: Algorithm? = null, - extractorFactory: ResourceContentExtractor.Factory = DefaultResourceContentExtractorFactory(), + extractorFactory: ResourceContentExtractor.Factory = DefaultResourceContentExtractorFactory() ): (Publication.Service.Context) -> StringSearchService = { context -> StringSearchService( - publication = context.publication, + manifest = context.manifest, + container = context.container, + services = context.services, language = context.manifest.metadata.languages.firstOrNull(), snippetLength = snippetLength, searchAlgorithm = searchAlgorithm @@ -71,22 +77,18 @@ class StringSearchService( override val options: Options = searchAlgorithm.options .copy(language = locale.toLanguageTag()) - override suspend fun search(query: String, options: Options?): SearchTry<SearchIterator> = - try { - Try.success( - Iterator( - publication = publication() ?: throw IllegalStateException("No Publication object"), - query = query, - options = options ?: Options(), - locale = options?.language?.let { Locale.forLanguageTag(it) } ?: locale, - ) - ) - } catch (e: Exception) { - Try.failure(SearchException.wrap(e)) - } + override suspend fun search(query: String, options: Options?): SearchIterator = + Iterator( + manifest = manifest, + container = container, + query = query, + options = options ?: Options(), + locale = options?.language?.let { Locale.forLanguageTag(it) } ?: locale + ) private inner class Iterator( - val publication: Publication, + val manifest: Manifest, + val container: Container<Resource>, val query: String, val options: Options, val locale: Locale @@ -102,16 +104,19 @@ class StringSearchService( override suspend fun next(): SearchTry<LocatorCollection?> { try { - if (index >= publication.readingOrder.count() - 1) { + if (index >= manifest.readingOrder.count() - 1) { return Try.success(null) } index += 1 - val link = publication.readingOrder[index] - val resource = publication.get(link) + val link = manifest.readingOrder[index] + val mediaType = link.mediaType ?: return next() + val text = + container[link.url()] + ?.let { extractorFactory.createExtractor(it, mediaType)?.extractText(it) } + ?.getOrElse { return Try.failure(SearchError.Reading(it)) } - val text = extractorFactory.createExtractor(resource)?.extractText(resource)?.getOrThrow() if (text == null) { Timber.w("Cannot extract text from resource: ${link.href}") return next() @@ -127,22 +132,32 @@ class StringSearchService( } return Try.success(LocatorCollection(locators = locators)) + } catch ( + e: CancellationException + ) { + throw e } catch (e: Exception) { - return Try.failure(SearchException.wrap(e)) + return Try.failure(SearchError.Engine(ThrowableError(e))) } } private suspend fun findLocators(resourceIndex: Int, link: Link, text: String): List<Locator> { - if (text == "") + if (text == "") { return emptyList() + } - val resourceTitle = publication.tableOfContents.titleMatching(link.href) - var resourceLocator = publication.locatorFromLink(link) ?: return emptyList() + val resourceTitle = manifest.tableOfContents.titleMatching(link.url()) + var resourceLocator = manifest.locatorFromLink(link) ?: return emptyList() resourceLocator = resourceLocator.copy(title = resourceTitle ?: resourceLocator.title) val locators = mutableListOf<Locator>() withContext(Dispatchers.IO) { - for (range in searchAlgorithm.findRanges(query = query, options = options, text = text, locale = locale)) { + for (range in searchAlgorithm.findRanges( + query = query, + options = options, + text = text, + locale = locale + )) { locators.add(createLocator(resourceIndex, resourceLocator, text, range)) } } @@ -169,9 +184,9 @@ class StringSearchService( return resourceLocator.copy( locations = resourceLocator.locations.copy( progression = progression, - totalProgression = totalProgression, + totalProgression = totalProgression ), - text = createSnippet(text, range), + text = createSnippet(text, range) ) } @@ -206,32 +221,32 @@ class StringSearchService( return Locator.Text( highlight = text.substring(range), before = before, - after = after, + after = after ) } private lateinit var _positions: List<List<Locator>> private suspend fun positions(): List<List<Locator>> { if (!::_positions.isInitialized) { - _positions = publication.positionsByReadingOrder() + _positions = services.positionsByReadingOrder() } return _positions } } /** Implements the actual search algorithm in sanitized text content. */ - interface Algorithm { + public interface Algorithm { /** * Default value for the search options available with this algorithm. * If an option does not have a value, it is not supported by the algorithm. */ - val options: Options + public val options: Options /** * Finds all the ranges of occurrences of the given [query] in the [text]. */ - suspend fun findRanges(query: String, options: Options, text: String, locale: Locale): List<IntRange> + public suspend fun findRanges(query: String, options: Options, text: String, locale: Locale): List<IntRange> } /** @@ -239,12 +254,12 @@ class StringSearchService( * while taking into account languages specificities. */ @RequiresApi(Build.VERSION_CODES.N) - class IcuAlgorithm : Algorithm { + public class IcuAlgorithm : Algorithm { override val options: Options = Options( caseSensitive = false, diacriticSensitive = false, - wholeWord = false, + wholeWord = false ) override suspend fun findRanges( @@ -284,19 +299,22 @@ class StringSearchService( val collator = Collator.getInstance(locale) as RuleBasedCollator if (!diacriticSensitive) { collator.strength = Collator.PRIMARY - if (caseSensitive) { - // FIXME: This doesn't seem to work despite the documentation indicating: - // > To ignore accents but take cases into account, set strength to primary and case level to on. - // > http://userguide.icu-project.org/collation/customization - collator.isCaseLevel = true - } + // if (caseSensitive) { + // FIXME: This doesn't seem to work despite the documentation indicating: + // > To ignore accents but take cases into account, set strength to primary and case level to on. + // > http://userguide.icu-project.org/collation/customization + // collator.isCaseLevel = true + // } } else if (!caseSensitive) { collator.strength = Collator.SECONDARY } val breakIterator: BreakIterator? = - if (wholeWord) BreakIterator.getWordInstance() - else null + if (wholeWord) { + BreakIterator.getWordInstance() + } else { + null + } return StringSearch(query, StringCharacterIterator(text), collator, breakIterator) } @@ -309,7 +327,7 @@ class StringSearchService( * all languages, so this [Algorithm] does not have any options. Use [IcuAlgorithm] for * better results. */ - class NaiveAlgorithm : Algorithm { + public class NaiveAlgorithm : Algorithm { override val options: Options get() = Options() @@ -330,15 +348,15 @@ class StringSearchService( } } -private fun List<Link>.titleMatching(href: String): String? { +private fun List<Link>.titleMatching(href: Url): String? { for (link in this) { link.titleMatching(href)?.let { return it } } return null } -private fun Link.titleMatching(targetHref: String): String? { - if (href.substringBeforeLast("#") == targetHref) { +private fun Link.titleMatching(targetHref: Url): String? { + if (url().removeFragment() == targetHref) { return title } return children.titleMatching(targetHref) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Benchmarking.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Benchmarking.kt index 63775f2d4e..f9f20d4554 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Benchmarking.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Benchmarking.kt @@ -6,11 +6,9 @@ package org.readium.r2.shared.util -import kotlin.time.ExperimentalTime import kotlin.time.measureTime import timber.log.Timber -@OptIn(ExperimentalTime::class) internal inline fun <T> benchmark(title: String, enabled: Boolean = true, closure: () -> T): T { if (!enabled) { return closure() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Closeable.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Closeable.kt index 87eba6d466..1e91b3892d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Closeable.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Closeable.kt @@ -9,31 +9,31 @@ package org.readium.r2.shared.util /** * A [Closeable] is an object holding closeable resources, such as open files or streams. */ -interface Closeable { +public interface Closeable { /** * Closes this object and releases any resources associated with it. * If the object is already closed then invoking this method has no effect. */ - fun close() + public fun close() } /** * A [SuspendingCloseable] is an object holding closeable resources, such as open files or streams. */ -interface SuspendingCloseable { +public interface SuspendingCloseable { /** * Closes this object and releases any resources associated with it. * If the object is already closed then invoking this method has no effect. */ - suspend fun close() + public suspend fun close() } /** * Executes the given block function on this resource and then closes it down correctly whether * an exception is thrown or not. */ -suspend inline fun <T : SuspendingCloseable?, R> T.use(block: (T) -> R): R { +public suspend inline fun <T : SuspendingCloseable?, R> T.use(block: (T) -> R): R { var exception: Throwable? = null try { return block(this) @@ -41,13 +41,14 @@ suspend inline fun <T : SuspendingCloseable?, R> T.use(block: (T) -> R): R { exception = e throw e } finally { - if (exception == null) + if (exception == null) { this?.close() - else + } else { try { this?.close() } catch (closeException: Throwable) { exception.addSuppressed(closeException) } + } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/CoroutineQueue.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/CoroutineQueue.kt new file mode 100644 index 0000000000..04998b0c20 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/CoroutineQueue.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope +import org.readium.r2.shared.InternalReadiumApi + +/** + * CoroutineScope-like util to execute coroutines in a sequential order (FIFO). + * As with a SupervisorJob, children can be cancelled or fail independently one from the other. + */ +@InternalReadiumApi +public class CoroutineQueue( + dispatcher: CoroutineDispatcher = Dispatchers.Main +) { + private val scope: CoroutineScope = + CoroutineScope(dispatcher + SupervisorJob()) + + private val tasks: Channel<Task<*>> = Channel(Channel.UNLIMITED) + + init { + scope.launch { + for (task in tasks) { + // Don't fail the root job if one task fails. + supervisorScope { + task() + } + } + } + } + + /** + * Launches a coroutine in the queue. + * + * Exceptions thrown by [block] will be ignored. + */ + public fun launch(block: suspend () -> Unit) { + tasks.trySendBlocking(Task(block)).getOrThrow() + } + + /** + * Creates a coroutine in the queue and returns its future result + * as an implementation of Deferred. + * + * Exceptions thrown by [block] will be caught and represented in the resulting [Deferred]. + */ + public fun <T> async(block: suspend () -> T): Deferred<T> { + val deferred = CompletableDeferred<T>() + val task = Task(block, deferred) + tasks.trySendBlocking(task).getOrThrow() + return deferred + } + + /** + * Launches a coroutine in the queue, and waits for its result. + * + * Exceptions thrown by [block] will be rethrown. + */ + public suspend fun <T> await(block: suspend () -> T): T = + async(block).await() + + /** + * Cancels this coroutine queue, including all its children with an optional cancellation cause. + */ + public fun cancel(cause: CancellationException? = null) { + scope.cancel(cause) + } + + private class Task<T>( + val task: suspend () -> T, + val deferred: CompletableDeferred<T>? = null + ) { + suspend operator fun invoke() { + try { + val result = task() + deferred?.complete(result) + } catch (e: Exception) { + deferred?.completeExceptionally(e) + } + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/CursorList.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/CursorList.kt index 41ebeaa398..19150d9244 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/CursorList.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/CursorList.kt @@ -1,3 +1,9 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + package org.readium.r2.shared.util import org.readium.r2.shared.InternalReadiumApi @@ -13,7 +19,7 @@ import org.readium.r2.shared.InternalReadiumApi * as the last returned element. May be -1 or the size of the list as well. */ @InternalReadiumApi -class CursorList<E>( +public class CursorList<E>( private val list: List<E> = emptyList(), private var index: Int = -1 ) : List<E> by list { @@ -22,31 +28,33 @@ class CursorList<E>( check(index in -1..list.size) } - fun hasPrevious(): Boolean { + public fun hasPrevious(): Boolean { return index > 0 } /** * Moves the cursor backward and returns the element, or null when reaching the beginning. */ - fun previous(): E? { - if (!hasPrevious()) + public fun previous(): E? { + if (!hasPrevious()) { return null + } index-- return list[index] } - fun hasNext(): Boolean { + public fun hasNext(): Boolean { return index + 1 < list.size } /** * Moves the cursor forward and returns the element, or null when reaching the end. */ - fun next(): E? { - if (!hasNext()) + public fun next(): E? { + if (!hasNext()) { return null + } index++ return list[index] diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Deprecated.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Deprecated.kt index 6f9c2d4fa8..ae9be4fd3a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Deprecated.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Deprecated.kt @@ -8,7 +8,15 @@ package org.readium.r2.shared.util -import org.readium.r2.shared.publication.asset.FileAsset - -@Deprecated("Renamed into `FileAsset`", ReplaceWith("FileAsset"), level = DeprecationLevel.ERROR) -typealias File = FileAsset +/** + * Returns the encapsulated result of the given transform function applied to the encapsulated |Throwable] exception + * if this instance represents failure or the original encapsulated value if it is success. + */ +@Suppress("Unused_parameter", "UnusedReceiverParameter") +@Deprecated( + message = "Use getOrElse instead.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("getOrElse") +) +public inline fun <R, S : R, F : Throwable> Try<S, F>.recover(transform: (exception: F) -> R): Try<R, Nothing> = + TODO() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Either.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Either.kt index 2142031e14..f51e34f319 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Either.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Either.kt @@ -9,33 +9,35 @@ package org.readium.r2.shared.util /** * Generic wrapper to store two mutually exclusive types. */ -sealed class Either<A, B> { - data class Left<A, B>(val value: A) : Either<A, B>() - data class Right<A, B>(val value: B) : Either<A, B>() +public sealed class Either<out A, out B> { + public data class Left<A, B>(val value: A) : Either<A, B>() + public data class Right<A, B>(val value: B) : Either<A, B>() - companion object { - inline operator fun <reified A, reified B> invoke(value: Any): Either<A, B> = + public companion object { + public inline operator fun <reified A, reified B> invoke(value: Any): Either<A, B> = when (value) { is A -> Left(value) is B -> Right(value) - else -> throw IllegalArgumentException("Provided value must be an instance of ${A::class.simpleName} or ${B::class.simpleName}") + else -> throw IllegalArgumentException( + "Provided value must be an instance of ${A::class.simpleName} or ${B::class.simpleName}" + ) } } - val left: A? + public val left: A? get() = (this as? Left)?.value - val right: B? + public val right: B? get() = (this as? Right)?.value - inline fun onLeft(action: (value: A) -> Unit): Either<A, B> { + public inline fun onLeft(action: (value: A) -> Unit): Either<A, B> { (this as? Left)?.let { action(it.value) } return this } - inline fun onRight(action: (value: B) -> Unit): Either<A, B> { + public inline fun onRight(action: (value: B) -> Unit): Either<A, B> { (this as? Right)?.let { action(it.value) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt new file mode 100644 index 0000000000..eda5617269 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util + +/** + * Describes an error. + */ +public interface Error { + + /** + * An error message. + */ + public val message: String + + /** + * The cause error or null if there is none. + */ + public val cause: Error? +} + +/** + * A basic [Error] implementation with a debug message. + */ +public class DebugError( + override val message: String, + override val cause: Error? = null +) : Error + +/** + * An error caused by the catch of a throwable. + */ +public class ThrowableError<E : Throwable>( + public val throwable: E +) : Error { + override val message: String = throwable.message ?: throwable.toString() + override val cause: Error? = throwable.cause?.let { ThrowableError(it) } +} + +/** + * A throwable caused by an [Error]. + */ +public class ErrorException( + public val error: Error +) : Exception(error.message, error.cause?.let { ErrorException(it) }) + +/** + * Convenience function to get the description of an error with its cause. + */ +public fun Error.toDebugDescription(): String = + if (this is ThrowableError<*>) { + throwable.toDebugDescription() + } else { + var desc = "${javaClass.nameWithEnclosingClasses()}: $message" + cause?.let { cause -> + desc += "\n${cause.toDebugDescription()}" + } + desc + } + +private fun Throwable.toDebugDescription(): String { + var desc = "${javaClass.nameWithEnclosingClasses()}: " + + desc += message ?: "" + desc += "\n" + stackTrace.take(2).joinToString("\n").prependIndent(" ") + cause?.let { cause -> + desc += "\n${cause.toDebugDescription()}" + } + return desc +} + +private fun Class<*>.nameWithEnclosingClasses(): String { + var name = simpleName + enclosingClass?.let { + name = "${it.nameWithEnclosingClasses()}.$name" + } + return name +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Href.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Href.kt deleted file mode 100644 index 312f704fd2..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Href.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2020 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util - -import android.net.Uri -import android.net.UrlQuerySanitizer -import android.webkit.URLUtil -import java.net.IDN -import java.net.URI -import java.net.URL -import org.readium.r2.shared.extensions.addPrefix -import timber.log.Timber - -/** - * Represents an HREF, optionally relative to another one. - * - * This is used to normalize the string representation. - */ -class Href( - private val href: String, - baseHref: String = "/" -) { - - data class QueryParameter(val name: String, val value: String?) - - private val baseHref = if (baseHref.isEmpty()) "/" else baseHref - - /** Returns the normalized string representation for this HREF. */ - val string: String get() { - val baseHref = baseHref.removePercentEncoding() - val href = href.removePercentEncoding() - - // HREF is just an anchor inside the base. - if (href.isBlank() || href.startsWith("#")) { - return baseHref + href - } - - // HREF is already absolute. - if (Uri.parse(href).isAbsolute) { - return href - } - - // Isolates the path from the anchor/query portion, which would be lost otherwise. - val splitIndex = href.indexOf("?").takeIf { it != -1 } - ?: href.indexOf("#").takeIf { it != -1 } - ?: (href.lastIndex + 1) - - val path = href.substring(0, splitIndex) - val suffix = href.substring(splitIndex) - - return try { - val uri = URI.create(baseHref.percentEncodedPath()).resolve(path.percentEncodedPath()) - val url = (if (URLUtil.isNetworkUrl(uri.toString())) uri.toString() else uri.path.addPrefix("/")) + suffix - return url.removePercentEncoding() - } catch (e: Exception) { - Timber.e(e) - "$baseHref/$href" - } - } - - /** - * Returns the normalized string representation for this HREF, encoded for URL uses. - * - * Taken from https://stackoverflow.com/a/49796882/1474476 - */ - val percentEncodedString: String get() { - var string = string - if (string.startsWith("/")) { - string = string.addPrefix("file://") - } - - return try { - val url = URL(string) - val uri = URI(url.protocol, url.userInfo, IDN.toASCII(url.host), url.port, url.path, url.query, url.ref) - uri.toASCIIString().removePrefix("file://") - } catch (e: Exception) { - Timber.e(e) - this.string - } - } - - /** Returns the query parameters present in this HREF, in the order they appear. */ - val queryParameters: List<QueryParameter> get() { - val url = percentEncodedString.substringBefore("#") - return UrlQuerySanitizer(url).parameterList - .map { p -> QueryParameter(name = p.mParameter, value = p.mValue.takeUnless { it.isBlank() }) } - } - - /** - * Percent-encodes an URL path section. - * - * Equivalent to Swift's `string.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)` - */ - private fun String.percentEncodedPath(): String = - Uri.encode(this, "$&+,/:=@") - - /** - * Expands percent-encoded characters. - */ - private fun String.removePercentEncoding(): String = - Uri.decode(this) - // If the string contains invalid percent-encoded characters, assumes that it is already - // percent-decoded. For example, if the string contains a standalone % character. - .takeIf { !it.contains("\uFFFD") } ?: this -} - -fun List<Href.QueryParameter>.firstNamedOrNull(name: String): String? = - firstOrNull { it.name == name }?.value - -fun List<Href.QueryParameter>.allNamed(name: String): List<String> = - filter { it.name == name }.mapNotNull { it.value } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Language.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Language.kt index b8393ec153..9b351dd7d9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Language.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Language.kt @@ -21,27 +21,27 @@ import kotlinx.serialization.encoding.Encoder * @param code BCP-47 language code */ @Serializable(with = Language.Serializer::class) -class Language(code: String) { +public class Language(code: String) { /** * Creates a [Language] from a Java [Locale]. */ - constructor(locale: Locale) : this(code = locale.toLanguageTag()) + public constructor(locale: Locale) : this(code = locale.toLanguageTag()) /** * BCP-47 language code. */ - val code = code.replace("_", "-") + public val code: String = code.replace("_", "-") - val locale: Locale by lazy { Locale.forLanguageTag(code) } + public val locale: Locale by lazy { Locale.forLanguageTag(code) } /** Indicates whether this language is a regional variant. */ - val isRegional: Boolean by lazy { + public val isRegional: Boolean by lazy { locale.country.isNotEmpty() } /** Returns this [Language] after stripping the region. */ - fun removeRegion(): Language = + public fun removeRegion(): Language = Language(code.split("-", limit = 2).first()) override fun toString(): String = @@ -57,8 +57,11 @@ class Language(code: String) { override fun hashCode(): Int = code.hashCode() - object Serializer : KSerializer<Language> { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Language", PrimitiveKind.STRING) + internal object Serializer : KSerializer<Language> { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( + "Language", + PrimitiveKind.STRING + ) override fun serialize(encoder: Encoder, value: Language) { encoder.encodeString(value.code) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Lazy.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Lazy.kt index aea45449aa..a2e758d9b5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Lazy.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Lazy.kt @@ -8,13 +8,15 @@ package org.readium.r2.shared.util import kotlin.reflect.KProperty0 import kotlin.reflect.jvm.isAccessible +import org.readium.r2.shared.InternalReadiumApi /** * Returns true if a lazy property reference has been initialized, or if the property is not lazy. * * Source: https://stackoverflow.com/a/42536189/1474476 */ -val KProperty0<*>.isLazyInitialized: Boolean +@InternalReadiumApi +public val KProperty0<*>.isLazyInitialized: Boolean get() { if (this !is Lazy<*>) return true diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/MapCompanion.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/MapCompanion.kt index 7a858d1860..65adee3494 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/MapCompanion.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/MapCompanion.kt @@ -9,6 +9,8 @@ package org.readium.r2.shared.util +import org.readium.r2.shared.InternalReadiumApi + /** * Encapsulates a [Map] into a more limited query API. * @@ -24,47 +26,55 @@ package org.readium.r2.shared.util * val layout: Layout? = Layout("reflowable") * ``` */ -open class MapCompanion<K, E>(protected val map: Map<K, E>) { +@InternalReadiumApi +public open class MapCompanion<K, E>(protected val map: Map<K, E>) { - constructor(elements: Array<E>, keySelector: (E) -> K) : + public constructor(elements: Array<E>, keySelector: (E) -> K) : this(elements.associateBy(keySelector)) /** * Returns the available [keys]. */ - val keys: Set<K> + public val keys: Set<K> get() = map.keys /** - * Returns the element matching the [key], or [null] if not found. + * Returns the element matching the [key], or null if not found. * - * To be overriden in subclasses if custom retrieval is needed – for example, testing lowercase + * To be overridden in subclasses if custom retrieval is needed – for example, testing lowercase * keys. */ - open fun get(key: K?): E? = + public open fun get(key: K?): E? = key?.let { map[key] } /** * Alias to [get], to be used like `keyMapper("a_key")`. */ - open operator fun invoke(key: K?): E? = get(key) + public open operator fun invoke(key: K?): E? = get(key) - @Deprecated("Use `Enum(\"value\")` instead", ReplaceWith("get(key)")) - open fun from(key: K?): E? = get(key) + @Deprecated( + "Use `Enum(\"value\")` instead", + ReplaceWith("get(key)"), + level = DeprecationLevel.ERROR + ) + public open fun from(key: K?): E? = get(key) } /** * Extends a [MapCompanion] by adding a [default] value as a fallback. */ -open class MapWithDefaultCompanion<K, E>(map: Map<K, E>, val default: E) : MapCompanion<K, E>(map) { +@InternalReadiumApi +public open class MapWithDefaultCompanion<K, E>(map: Map<K, E>, public val default: E) : MapCompanion<K, E>( + map +) { - constructor(elements: Array<E>, keySelector: (E) -> K, default: E) : + public constructor(elements: Array<E>, keySelector: (E) -> K, default: E) : this(elements.associateBy(keySelector), default) /** * Returns the element matching the [key], or the [default] value as a fallback. */ - fun getOrDefault(key: K?): E = + public fun getOrDefault(key: K?): E = get(key) ?: default /** @@ -72,6 +82,10 @@ open class MapWithDefaultCompanion<K, E>(map: Map<K, E>, val default: E) : MapCo */ override operator fun invoke(key: K?): E = getOrDefault(key) - @Deprecated("Use `Enum(\"value\")` instead", ReplaceWith("getOrDefault(key)")) + @Deprecated( + "Use `Enum(\"value\")` instead", + ReplaceWith("getOrDefault(key)"), + level = DeprecationLevel.ERROR + ) override fun from(key: K?): E? = getOrDefault(key) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/MemoryObserver.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/MemoryObserver.kt index 3f83b19610..b839e13264 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/MemoryObserver.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/MemoryObserver.kt @@ -8,16 +8,16 @@ import org.readium.r2.shared.InternalReadiumApi * A memory observer reacts to a device reclaiming memory by releasing unused resources. */ @InternalReadiumApi -interface MemoryObserver { +public interface MemoryObserver { /** * Level of memory trim. */ - enum class Level { + public enum class Level { Moderate, Low, Critical; - companion object { - fun fromLevel(level: Int): Level = + public companion object { + public fun fromLevel(level: Int): Level = when { level <= ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE -> Moderate level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW -> Low @@ -31,13 +31,13 @@ interface MemoryObserver { * * Release unused resources according to the level. */ - fun onTrimMemory(level: Level) + public fun onTrimMemory(level: Level) - companion object { + public companion object { /** - * Wraps the given [observer] into a [ComponentCallbacks2] usable with Android's [Context]. + * Wraps the given [observer] into a [ComponentCallbacks2] usable with Android's Context. */ - fun asComponentCallbacks2(observer: MemoryObserver): ComponentCallbacks2 = + public fun asComponentCallbacks2(observer: MemoryObserver): ComponentCallbacks2 = object : ComponentCallbacks2 { override fun onConfigurationChanged(config: Configuration) {} override fun onLowMemory() {} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Ref.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Ref.kt deleted file mode 100644 index c1249bab97..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Ref.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2021 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util - -/** - * Smart pointer holding a mutable reference to an object. - * - * Get the reference by calling `ref()` - * Conveniently, the reference can be reset by setting the `ref` property. - */ -class Ref<T>(var ref: T? = null) { - - operator fun invoke(): T? = ref -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/SingleJob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/SingleJob.kt new file mode 100644 index 0000000000..609ad697a5 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/SingleJob.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.readium.r2.shared.InternalReadiumApi + +/** + * Runs a single coroutine job at a time. + * + * If a previous job is running, cancels it before launching the new one. + */ +@InternalReadiumApi +public class SingleJob( + private val scope: CoroutineScope +) { + private var job: Job? = null + private val mutex = Mutex() + + /** + * Launches a coroutine job. + * + * If a previous job is running, cancels it before launching the new one. + */ + public fun launch(block: suspend CoroutineScope.() -> Unit) { + scope.launch { + mutex.withLock { + job?.cancelAndJoin() + job = launch { block() } + } + } + } + + /** + * Cancels the current job, if any. + */ + public fun cancel() { + job?.cancel() + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt index 10aa580641..0f6c192b26 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt @@ -1,146 +1,182 @@ /* - * Module: r2-shared-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. */ package org.readium.r2.shared.util +import org.readium.r2.shared.util.Try.Failure +import org.readium.r2.shared.util.Try.Success + /** A [Result] type which can be used as a return type. */ -sealed class Try<out Success, out Failure : Throwable> { +public sealed class Try<out Success, out Failure> { - companion object { + public companion object { /** Returns an instance that encapsulates the given value as successful value. */ - fun <Success> success(success: Success): Try<Success, Nothing> = Success(success) + public fun <Success> success(success: Success): Try<Success, Nothing> = Success(success) /** Returns the encapsulated Throwable exception if this instance represents failure or null if it is success. */ - fun <Failure : Throwable> failure(failure: Failure): Try<Nothing, Failure> = Failure(failure) + public fun <Failure> failure(failure: Failure): Try<Nothing, Failure> = Failure(failure) } - abstract val isSuccess: Boolean - abstract val isFailure: Boolean - - /** - * Returns the encapsulated value if this instance represents success - * or throws the encapsulated Throwable exception if it is failure. - */ - abstract fun getOrThrow(): Success + public abstract val isSuccess: Boolean + public abstract val isFailure: Boolean /** Returns the encapsulated value if this instance represents success or null if it is failure. */ - abstract fun getOrNull(): Success? + public abstract fun getOrNull(): Success? /** Returns the encapsulated [Throwable] exception if this instance represents failure or null if it is success. */ - abstract fun exceptionOrNull(): Failure? + public abstract fun failureOrNull(): Failure? - class Success<out S, out F : Throwable>(val value: S) : Try<S, F>() { + public class Success<out S, out F>(public val value: S) : Try<S, F>() { override val isSuccess: Boolean get() = true override val isFailure: Boolean get() = false - override fun getOrThrow(): S = value override fun getOrNull(): S? = value - override fun exceptionOrNull(): F? = null + override fun failureOrNull(): F? = null } - class Failure<out S, out F : Throwable>(val exception: F) : Try<S, F>() { + public class Failure<out S, out F>(public val value: F) : Try<S, F>() { override val isSuccess: Boolean get() = false override val isFailure: Boolean get() = true - override fun getOrThrow(): S { throw exception } override fun getOrNull(): S? = null - override fun exceptionOrNull(): F? = exception + override fun failureOrNull(): F = value + + @Deprecated( + "Renamed to value.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("value") + ) + public val exception: F + get() = value } /** * Returns the encapsulated result of the given transform function applied to the encapsulated value * if this instance represents success or the original encapsulated [Throwable] exception if it is failure. */ - inline fun <R> map(transform: (value: Success) -> R): Try<R, Failure> = - if (isSuccess) - success(transform(getOrThrow())) - else - failure(exceptionOrNull()!!) + public inline fun <R> map(transform: (value: Success) -> R): Try<R, Failure> = + when (this) { + is Try.Success -> success(transform(value)) + is Try.Failure -> failure(value) + } /** * Returns the encapsulated result of the given transform function applied to the encapsulated failure * if this instance represents failure or the original encapsulated success value if it is a success. */ - inline fun <F : Throwable> mapFailure(transform: (value: Failure) -> F): Try<Success, F> = - if (isSuccess) - success(getOrThrow()) - else - failure(transform(exceptionOrNull()!!)) + public inline fun <F> mapFailure(transform: (value: Failure) -> F): Try<Success, F> = + when (this) { + is Try.Success -> success(value) + is Try.Failure -> failure(transform(failureOrNull()!!)) + } /** * Returns the result of [onSuccess] for the encapsulated value if this instance represents success or - * the result of [onFailure] function for the encapsulated [Throwable] exception if it is failure. + * the result of [onFailure] function for the encapsulated value if it is failure. */ - inline fun <R> fold(onSuccess: (value: Success) -> R, onFailure: (exception: Throwable) -> R): R = - if (isSuccess) - onSuccess(getOrThrow()) - else - onFailure(exceptionOrNull()!!) + public inline fun <R> fold( + onSuccess: (value: Success) -> R, + onFailure: (exception: Failure) -> R + ): R = + when (this) { + is Try.Success -> onSuccess(value) + is Try.Failure -> onFailure(failureOrNull()!!) + } /** * Performs the given action on the encapsulated value if this instance represents success. * Returns the original [Try] unchanged. */ - inline fun onSuccess(action: (value: Success) -> Unit): Try<Success, Failure> { - if (isSuccess) action(getOrThrow()) + public inline fun onSuccess(action: (value: Success) -> Unit): Try<Success, Failure> { + if (this is Try.Success) action(value) return this } /** - * Performs the given action on the encapsulated [Throwable] exception if this instance represents failure. + * Performs the given action on the encapsulated value if this instance represents failure. * Returns the original [Try] unchanged. */ - inline fun onFailure(action: (exception: Failure) -> Unit): Try<Success, Failure> { - if (isFailure) action(exceptionOrNull()!!) + public inline fun onFailure(action: (exception: Failure) -> Unit): Try<Success, Failure> { + if (this is Try.Failure) action(failureOrNull()!!) return this } } +/** + * Returns the encapsulated value if this instance represents success + * or throws the encapsulated Throwable exception if it is failure. + */ +public fun <S, F : Throwable> Try<S, F>.getOrThrow(): S = + when (this) { + is Success -> value + is Failure -> throw value + } + +/** + * Returns the encapsulated value if this instance represents success + * or throws the encapsulated ThrowableError exception if it is failure. + */ +@Suppress("UnusedReceiverParameter") +@Deprecated( + "An `Error` is not a throwable object. Refactor or wrap in an `ErrorException`.", + ReplaceWith("getOrElse { throw ErrorException(it) }"), + DeprecationLevel.ERROR +) +@JvmName("getOrThrowError") +public fun <S, F : Error> Try<S, F>.getOrThrow(): S = + throw NotImplementedError() + /** * Returns the encapsulated value if this instance represents success or the [defaultValue] if it is failure. */ -fun <R, S : R, F : Throwable> Try<S, F>.getOrDefault(defaultValue: R): R = - if (isSuccess) - getOrThrow() - else - defaultValue +public fun <R, S : R, F> Try<S, F>.getOrDefault(defaultValue: R): R = + when (this) { + is Success -> value + is Failure -> defaultValue + } /** * Returns the encapsulated value if this instance represents success or the result of [onFailure] function - * for the encapsulated [Throwable] exception if it is failure. + * for the encapsulated value if it is failure. */ -inline fun <R, S : R, F : Throwable> Try<S, F>.getOrElse(onFailure: (exception: F) -> R): R = - if (isSuccess) - getOrThrow() - else - onFailure(exceptionOrNull()!!) - -inline fun <R, S, F : Throwable> Try<S, F>.flatMap(transform: (value: S) -> Try<R, F>): Try<R, F> = - if (isSuccess) - transform(getOrThrow()) - else - Try.failure(exceptionOrNull()!!) +public inline fun <R, S : R, F> Try<S, F>.getOrElse(onFailure: (exception: F) -> R): R = + when (this) { + is Success -> value + is Failure -> onFailure(value) + } /** - * Returns the encapsulated result of the given transform function applied to the encapsulated |Throwable] exception - * if this instance represents failure or the original encapsulated value if it is success. + * Returns the encapsulated result of the given transform function applied to the encapsulated + * value if this instance represents success or the original encapsulated value if it is failure. */ -inline fun <R, S : R, F : Throwable> Try<S, F>.recover(transform: (exception: F) -> R): Try<R, Nothing> = - if (isSuccess) - Try.success(getOrThrow()) - else - Try.success(transform(exceptionOrNull()!!)) +public inline fun <R, S, F> Try<S, F>.flatMap(transform: (value: S) -> Try<R, F>): Try<R, F> = + when (this) { + is Success -> transform(value) + is Failure -> Try.failure(value) + } /** - * Returns the encapsulated result of the given transform function applied to the encapsulated |Throwable] exception + * Returns the encapsulated result of the given transform function applied to the encapsulated value * if this instance represents failure or the original encapsulated value if it is success. */ -inline fun <R, S : R, F : Throwable> Try<S, F>.tryRecover(transform: (exception: F) -> Try<R, F>): Try<R, F> = - if (isSuccess) - Try.success(getOrThrow()) - else - transform(exceptionOrNull()!!) +public inline fun <R, S : R, F, T> Try<S, F>.tryRecover(transform: (exception: F) -> Try<R, T>): Try<R, T> = + when (this) { + is Success -> Try.success(value) + is Failure -> transform(value) + } + +/** + * Returns value in case of success and throws an [IllegalStateException] in case of failure. + */ +public fun <S, F> Try<S, F>.checkSuccess(): S = + when (this) { + is Success -> + value + is Failure -> { + throw IllegalStateException( + "Try was excepted to contain a success.", + value as? Throwable ?: (value as? Error)?.let { ErrorException(it) } + ) + } + } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/URITemplate.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/URITemplate.kt index 18fbfdcb9a..57d2b88981 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/URITemplate.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/URITemplate.kt @@ -9,16 +9,18 @@ package org.readium.r2.shared.util +import org.readium.r2.shared.extensions.percentEncodedQuery + /** * A lightweight implementation of URI Template (RFC 6570). * * Only handles simple cases, fitting Readium's use cases. * See https://tools.ietf.org/html/rfc6570 */ -data class URITemplate(val uri: String) { +internal data class URITemplate(val uri: String) { /** - * List of URI template parameter keys, if the [Link] is templated. + * List of URI template parameter keys, if the Link is templated. */ val parameters: List<String> by lazy { // Escaping the last } is somehow required, otherwise the regex can't be parsed on a Pixel @@ -32,14 +34,14 @@ data class URITemplate(val uri: String) { /** * Expands the HREF by replacing URI template variables by the given parameters. */ - fun expand(parameters: Map<String, String>): String { - @Suppress("NAME_SHADOWING") + public fun expand(parameters: Map<String, String>): String { // `+` is considered like an encoded space, and will not be properly encoded in parameters. // This is an issue for ISO 8601 date for example. // As a workaround, we encode manually this character. We don't do it in the full URI, // because it could contain some legitimate +-as-space characters. + @Suppress("NAME_SHADOWING") val parameters = parameters.mapValues { - it.value.replace("+", "~~+~~") + it.value.percentEncodedQuery().replace("+", "~~+~~") } fun expandSimpleString(string: String, parameters: Map<String, String>): String = @@ -51,13 +53,14 @@ data class URITemplate(val uri: String) { // Escaping the last } is somehow required, otherwise the regex can't be parsed on a Pixel // 3a. However, without it works with the unit tests. val expanded = "\\{(\\??)([^}]+)\\}".toRegex().replace(uri) { - if (it.groupValues[1].isEmpty()) + if (it.groupValues[1].isEmpty()) { expandSimpleString(it.groupValues[2], parameters) - else + } else { expandFormStyle(it.groupValues[2], parameters) + } } - return Href(expanded).percentEncodedString + return expanded .replace("~~%20~~", "%2B") .replace("~~+~~", "%2B") } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt new file mode 100644 index 0000000000..6d880631fe --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt @@ -0,0 +1,370 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util + +import android.net.Uri +import android.net.UrlQuerySanitizer +import android.os.Parcelable +import java.io.File +import java.net.URI +import java.net.URL +import kotlinx.parcelize.Parcelize +import org.readium.r2.shared.DelicateReadiumApi +import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.extensions.percentEncodedPath +import org.readium.r2.shared.extensions.tryOrNull + +/** + * A Uniform Resource Locator. + * + * https://url.spec.whatwg.org/ + */ +public sealed class Url : Parcelable { + + internal abstract val uri: Uri + + public companion object { + + /** + * Creates a [RelativeUrl] from a percent-decoded path. + */ + public fun fromDecodedPath(path: String): RelativeUrl? = + RelativeUrl(path.percentEncodedPath()) + + /** + * Creates a [Url] from its encoded string representation. + */ + public operator fun invoke(url: String): Url? { + if (!url.isValidUrl()) return null + return invoke(Uri.parse(url)) + } + + /** + * Creates an [Url] from a legacy HREF. + * + * For example, if it is a relative path such as `/dir/my chapter.html`, it will be + * converted to the valid relative URL `dir/my%20chapter.html`. + * + * Only use this API when you are upgrading to Readium 3.x and migrating the HREFs stored in + * your database. See the 3.0 migration guide for more information. + */ + @DelicateReadiumApi + public fun fromLegacyHref(href: String): Url? = + AbsoluteUrl(href) ?: fromDecodedPath(href.removePrefix("/")) + + internal operator fun invoke(uri: Uri): Url? = + if (uri.isAbsolute) { + AbsoluteUrl(uri) + } else { + RelativeUrl(uri) + } + } + + /** + * Decoded path segments identifying a location. + */ + public val path: String? + get() = uri.path?.takeUnless { it.isBlank() } + + /** + * Decoded filename portion of the URL path. + */ + public val filename: String? + get() = if (path?.endsWith("/") == true) { + null + } else { + uri.lastPathSegment + } + + /** + * Extension of the filename portion of the URL path. + */ + public val extension: FileExtension? + get() = filename?.substringAfterLast('.', "") + ?.takeIf { it.isNotEmpty() } + ?.let { FileExtension(it) } + + /** + * Represents a list of query parameters in a URL. + */ + public data class Query( + public val parameters: List<QueryParameter> + ) { + + /** + * Returns the first value for the parameter with the given [name]. + */ + public fun firstNamedOrNull(name: String): String? = + parameters.firstOrNull { it.name == name }?.value + + /** + * Returns all the values for the parameter with the given [name]. + */ + public fun allNamed(name: String): List<String> = + parameters.filter { it.name == name }.mapNotNull { it.value } + } + + /** + * Represents a single query parameter and its value in a URL. + */ + public data class QueryParameter( + public val name: String, + public val value: String? + ) + + /** + * Returns the decoded query parameters present in this URL, in the order they appear. + */ + @InternalReadiumApi + public val query: Query get() = + Query( + UrlQuerySanitizer(removeFragment().toString()).parameterList + .map { p -> + QueryParameter( + name = p.mParameter, + value = p.mValue.takeUnless { it.isBlank() } + ) + } + ) + + /** + * Returns a copy of this URL after dropping its query. + */ + public fun removeQuery(): Url = + if (uri.query == null) { + this + } else { + checkNotNull(invoke(uri.buildUpon().clearQuery().build())) + } + + /** + * Returns the decoded fragment present in this URL, if any. + */ + public val fragment: String? get() = + uri.fragment?.takeUnless { it.isBlank() } + + /** + * Returns a copy of this URL after dropping its fragment. + */ + public fun removeFragment(): Url = + if (fragment == null) { + this + } else { + // FIXME: Check URL with only a fragment #id + checkNotNull(invoke(uri.buildUpon().fragment(null).build())) + } + + /** + * Resolves the given [url] to this URL. + * + * For example: + * this = "http://example.com/foo/" + * url = "bar/baz" + * result = "http://example.com/foo/bar/baz" + */ + public open fun resolve(url: Url): Url = + when (url) { + is AbsoluteUrl -> url + is RelativeUrl -> checkNotNull(toURI().resolve(url.toURI()).toUrl()) + } + + /** + * Relativizes the given [url] against this URL. + * + * For example: + * this = "http://example.com/foo" + * url = "http://example.com/foo/bar/baz" + * result = "bar/baz" + */ + public open fun relativize(url: Url): Url = + checkNotNull(toURI().relativize(url.toURI()).toUrl()) + + override fun toString(): String = + uri.toString() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Url + + if (uri.toString() != other.uri.toString()) return false + + return true + } + + override fun hashCode(): Int = + uri.toString().hashCode() + + /** + * A URL scheme, e.g. http or file. + */ + @JvmInline + public value class Scheme private constructor(public val value: String) { + + public companion object { + public operator fun invoke(scheme: String): Scheme = + Scheme(scheme.lowercase()) + } + + override fun toString(): String = value + + public val isFile: Boolean + get() = value == "file" + + public val isHttp: Boolean + get() = value == "http" || value == "https" + + public val isContent: Boolean + get() = value == "content" + } +} + +/** + * Represents an absolute Uniform Resource Locator. + */ +@Parcelize +public class AbsoluteUrl private constructor(override val uri: Uri) : Url() { + + public companion object { + + /** + * Creates an [AbsoluteUrl] from its encoded string representation. + */ + public operator fun invoke(url: String): AbsoluteUrl? { + if (!url.isValidUrl()) return null + return invoke(Uri.parse(url)) + } + + internal operator fun invoke(uri: Uri): AbsoluteUrl? = + tryOrNull { + require(uri.isAbsolute) + require(uri.isHierarchical) + AbsoluteUrl(uri) + } + } + + public override fun resolve(url: Url): AbsoluteUrl = + super.resolve(url) as AbsoluteUrl + + /** + * Identifies the type of URL. + */ + public val scheme: Scheme + get() = Scheme(uri.scheme!!) + + /** + * Indicates whether this URL points to a HTTP resource. + */ + public val isHttp: Boolean get() = + scheme.isHttp + + /** + * Indicates whether this URL points to a file. + */ + public val isFile: Boolean get() = + scheme.isFile + + /** + * Indicates whether this URL points to an Android content resource. + */ + public val isContent: Boolean get() = + scheme.isContent + + /** + * Converts the URL to a [File], if it's a file URL. + */ + public fun toFile(): File? = + if (isFile) File(path!!) else null +} + +/** + * Represents a relative Uniform Resource Locator. + */ +@Parcelize +public class RelativeUrl private constructor(override val uri: Uri) : Url() { + + public companion object { + + /** + * Creates a [RelativeUrl] from its encoded string representation. + */ + public operator fun invoke(url: String): RelativeUrl? { + if (!url.isValidUrl()) return null + return invoke(Uri.parse(url)) + } + + internal operator fun invoke(uri: Uri): RelativeUrl? = + tryOrNull { + require(uri.isRelative) + RelativeUrl(uri) + } + } +} + +public fun File.toUrl(): AbsoluteUrl = + checkNotNull(AbsoluteUrl(Uri.fromFile(this))) + +public fun Uri.toUrl(): Url? = + Url(this) + +public fun Uri.toAbsoluteUrl(): AbsoluteUrl? = + AbsoluteUrl(this) + +public fun Uri.toRelativeUrl(): RelativeUrl? = + RelativeUrl(this) + +public fun Url.toUri(): Uri = + uri + +internal fun Url.toURI(): URI = + URI(toString()) + +public fun URL.toUrl(): Url? = + Url(toUri()) + +public fun URL.toAbsoluteUrl(): AbsoluteUrl? = + AbsoluteUrl(toUri()) + +public fun URL.toRelativeUrl(): RelativeUrl? = + RelativeUrl(toUri()) + +private fun URL.toUri(): Uri = + Uri.parse(toString()).addFileAuthority() + +public fun URI.toUrl(): Url? = + Url(Uri.parse(toString()).addFileAuthority()) + +/** + * [URL] and [URI] can return a file URL without the empty authority, which is invalid. + * + * This method adds the empty authority if needed, for example: + * `file:/path/to/file` becomes `file:///path/to/file` + */ +private fun Uri.addFileAuthority(): Uri = + if (scheme?.lowercase() != "file" || authority != null) { + this + } else { + buildUpon().authority("").build() + } + +private fun String.isValidUrl(): Boolean = + // Uri.parse doesn't really validate the URL, it could contain invalid characters. + isNotBlank() && tryOrNull { URI(this) } != null + +@JvmInline +public value class FileExtension( + public val value: String +) { + override fun toString(): String = value +} + +/** + * Appends this file extension to [filename]. + */ +public fun FileExtension?.appendToFilename(filename: String): String = + this?.let { "$filename.$value" } ?: filename diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/Archive.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/Archive.kt deleted file mode 100644 index 83983ea0a2..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/Archive.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared.util.archive - -import java.io.File -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.readium.r2.shared.extensions.tryOr -import org.readium.r2.shared.util.SuspendingCloseable - -interface ArchiveFactory { - - /** Opens an archive from a local [file]. */ - suspend fun open(file: File, password: String?): Archive -} - -class DefaultArchiveFactory : ArchiveFactory { - - private val javaZipFactory by lazy { JavaZipArchiveFactory() } - private val explodedArchiveFactory by lazy { ExplodedArchiveFactory() } - - /** Opens a ZIP or exploded archive. */ - override suspend fun open(file: File, password: String?): Archive = withContext(Dispatchers.IO) { - if (tryOr(false) { file.isDirectory }) { - explodedArchiveFactory.open(file, password) - } else { - javaZipFactory.open(file, password) - } - } -} - -/** - * Represents an immutable archive. - */ -interface Archive : SuspendingCloseable { - - /** - * Holds an archive entry's metadata. - */ - interface Entry : SuspendingCloseable { - - /** - * Absolute path to the entry in the archive. - * It MUST start with /. - */ - val path: String - - /** - * Uncompressed data length. - */ - val length: Long? - - /** - * Compressed data length. - */ - val compressedLength: Long? - - /** - * Reads the whole content of this entry. - * When [range] is null, the whole content is returned. Out-of-range indexes are clamped to the - * available length automatically. - */ - suspend fun read(range: LongRange? = null): ByteArray - } - - /** List of all the archived file entries. */ - suspend fun entries(): List<Entry> - - /** Gets the entry at the given `path`. */ - suspend fun entry(path: String): Entry -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveOpener.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveOpener.kt new file mode 100644 index 0000000000..13da05b50d --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveOpener.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.archive + +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.resource.Resource + +/** + * A factory to create [Container]s from archive [Resource]s. + */ +public interface ArchiveOpener { + + public sealed class OpenError( + override val message: String, + override val cause: Error? + ) : Error { + + public class FormatNotSupported( + public val format: Format, + cause: Error? = null + ) : OpenError("Format not supported.", cause) + + public class Reading( + override val cause: ReadError + ) : OpenError("An error occurred while attempting to read the resource.", cause) + } + + public sealed class SniffOpenError( + override val message: String, + override val cause: Error? + ) : Error { + + public data object NotRecognized : + SniffOpenError("Format of resource could not be inferred.", null) + + public data class Reading(override val cause: ReadError) : + SniffOpenError("An error occurred while trying to read content.", cause) + } + + /** + * Creates a new [Container] to access the entries of an archive with a known format. + */ + public suspend fun open( + format: Format, + source: Readable + ): Try<ContainerAsset, OpenError> + + /** + * Creates a new [ContainerAsset] to access the entries of an archive after sniffing its format. + */ + public suspend fun sniffOpen( + source: Readable + ): Try<ContainerAsset, SniffOpenError> +} + +/** + * A composite [ArchiveOpener] which tries several factories until it finds one which supports + * the format. +*/ +public class CompositeArchiveOpener( + private val openers: List<ArchiveOpener> +) : ArchiveOpener { + + public constructor(vararg factories: ArchiveOpener) : + this(factories.toList()) + + override suspend fun open( + format: Format, + source: Readable + ): Try<ContainerAsset, ArchiveOpener.OpenError> { + for (factory in openers) { + factory.open(format, source) + .getOrElse { error -> + when (error) { + is ArchiveOpener.OpenError.FormatNotSupported -> null + else -> return Try.failure(error) + } + } + ?.let { return Try.success(it) } + } + + return Try.failure(ArchiveOpener.OpenError.FormatNotSupported(format)) + } + + override suspend fun sniffOpen( + source: Readable + ): Try<ContainerAsset, ArchiveOpener.SniffOpenError> { + for (factory in openers) { + factory.sniffOpen(source) + .getOrElse { error -> + when (error) { + is ArchiveOpener.SniffOpenError.NotRecognized -> null + else -> return Try.failure(error) + } + } + ?.let { return Try.success(it) } + } + + return Try.failure(ArchiveOpener.SniffOpenError.NotRecognized) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt new file mode 100644 index 0000000000..a1920221bf --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.archive + +import org.json.JSONObject +import org.readium.r2.shared.JSONable +import org.readium.r2.shared.extensions.optNullableBoolean +import org.readium.r2.shared.extensions.optNullableLong +import org.readium.r2.shared.extensions.toMap +import org.readium.r2.shared.util.resource.Resource + +/** + * Holds information about how the resource is stored in the archive. + * + * @param entryLength The length of the entry stored in the archive. It might be a compressed length + * if the entry is deflated. + * @param isEntryCompressed Indicates whether the entry was compressed before being stored in the + * archive. + */ +public data class ArchiveProperties( + val entryLength: Long, + val isEntryCompressed: Boolean +) : JSONable { + + override fun toJSON(): JSONObject = JSONObject().apply { + put("entryLength", entryLength) + put("isEntryCompressed", isEntryCompressed) + } + + public companion object { + public fun fromJSON(json: JSONObject?): ArchiveProperties? { + json ?: return null + + val entryLength = json.optNullableLong("entryLength") + val isEntryCompressed = json.optNullableBoolean("isEntryCompressed") + if (entryLength == null || isEntryCompressed == null) { + return null + } + return ArchiveProperties( + entryLength = entryLength, + isEntryCompressed = isEntryCompressed + ) + } + } +} + +private const val ARCHIVE_KEY = "archive" + +public val Resource.Properties.archive: ArchiveProperties? + get() = (this[ARCHIVE_KEY] as? Map<*, *>) + ?.let { ArchiveProperties.fromJSON(JSONObject(it)) } + +public var Resource.Properties.Builder.archive: ArchiveProperties? + get() = (this[ARCHIVE_KEY] as? Map<*, *>) + ?.let { ArchiveProperties.fromJSON(JSONObject(it)) } + set(value) { + if (value == null) { + remove(ARCHIVE_KEY) + } else { + put(ARCHIVE_KEY, value.toJSON().toMap()) + } + } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ExplodedArchive.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ExplodedArchive.kt deleted file mode 100644 index b6b89d0d01..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ExplodedArchive.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared.util.archive - -import java.io.File -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.readium.r2.shared.extensions.isParentOf -import org.readium.r2.shared.extensions.readFully -import org.readium.r2.shared.extensions.readRange -import org.readium.r2.shared.extensions.tryOr - -/** - * An archive exploded on the file system as a directory. - */ -internal class ExplodedArchive(private val directory: File) : Archive { - - private inner class Entry(private val file: File) : Archive.Entry { - - override val path: String get() = file.relativeTo(directory).path - - override val length: Long? = file.length() - - override val compressedLength: Long? = null - - override suspend fun read(range: LongRange?): ByteArray { - val stream = withContext(Dispatchers.IO) { - file.inputStream() - } - - return stream.use { - if (range == null) - it.readFully() - else - it.readRange(range) - } - } - - override suspend fun close() {} - } - - override suspend fun entries(): List<Archive.Entry> = - directory.walk() - .filter { it.isFile } - .map { Entry(it) } - .toList() - - override suspend fun entry(path: String): Archive.Entry { - val file = File(directory, path) - - if (!directory.isParentOf(file) || !file.isFile) - throw Exception("No file entry at path $path.") - - return Entry(file) - } - - override suspend fun close() {} -} - -internal class ExplodedArchiveFactory : ArchiveFactory { - - override suspend fun open(file: File, password: String?): Archive = withContext(Dispatchers.IO) { - file.takeIf { tryOr(false) { it.isDirectory } } - ?.let { ExplodedArchive(it) } - ?: throw IllegalArgumentException("[path] must be a directory to be opened as an exploded archive") - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/JavaZip.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/JavaZip.kt deleted file mode 100644 index c0e5901e23..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/JavaZip.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -@file:OptIn(InternalReadiumApi::class) - -package org.readium.r2.shared.util.archive - -import java.io.File -import java.util.zip.ZipEntry -import java.util.zip.ZipFile -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.readium.r2.shared.InternalReadiumApi -import org.readium.r2.shared.extensions.readFully -import org.readium.r2.shared.util.io.CountingInputStream - -@OptIn(InternalReadiumApi::class) -internal class JavaZip(private val archive: ZipFile) : Archive { - - private inner class Entry(private val entry: ZipEntry) : Archive.Entry { - override val path: String get() = entry.name - - override val length: Long? get() = entry.size.takeUnless { it == -1L } - - override val compressedLength: Long? - get() = - if (entry.method == ZipEntry.STORED || entry.method == -1) - null - else - entry.compressedSize.takeUnless { it == -1L } - - override suspend fun read(range: LongRange?): ByteArray = - withContext(Dispatchers.IO) { - if (range == null) - readFully() - else - readRange(range) - } - - private suspend fun readFully(): ByteArray = - archive.getInputStream(entry).use { - it.readFully() - } - - private fun readRange(range: LongRange): ByteArray = - stream(range.first).readRange(range) - - /** - * Reading an entry in chunks (e.g. from the HTTP server) can be really slow if the entry - * is deflated in the archive, because we can't jump to an arbitrary offset in a deflated - * stream. This means that we need to read from the start of the entry for each chunk. - * - * To alleviate this issue, we cache a stream which will be reused as long as the chunks are - * requested in order. - * - * See this issue for more info: https://github.com/readium/r2-shared-kotlin/issues/129 - */ - private fun stream(fromIndex: Long): CountingInputStream { - // Reuse the current stream if it didn't exceed the requested index. - stream - ?.takeIf { it.count <= fromIndex } - ?.let { return it } - - stream?.close() - - return CountingInputStream(archive.getInputStream(entry)) - .also { stream = it } - } - - private var stream: CountingInputStream? = null - - override suspend fun close() { - withContext(Dispatchers.IO) { - stream?.close() - } - } - } - - override suspend fun entries(): List<Archive.Entry> = - archive.entries().toList().filterNot { it.isDirectory }.mapNotNull { Entry(it) } - - override suspend fun entry(path: String): Archive.Entry { - val entry = archive.getEntry(path) - ?: throw Exception("No file entry at path $path.") - - return Entry(entry) - } - - override suspend fun close() = withContext(Dispatchers.IO) { - archive.close() - } -} - -internal class JavaZipArchiveFactory : ArchiveFactory { - - override suspend fun open(file: File, password: String?): Archive = withContext(Dispatchers.IO) { - JavaZip(ZipFile(file)) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt new file mode 100644 index 0000000000..95dd31e2ba --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.asset + +import org.readium.r2.shared.util.SuspendingCloseable +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.resource.Resource + +/** + * An asset which is either a single resource or a container that holds multiple resources. + */ +public sealed class Asset : SuspendingCloseable { + + /** + * Format of the asset. + */ + public abstract val format: Format +} + +/** + * A container asset providing access to several resources. + * + * @param format Format of the asset. + * @param container Opened container to access asset resources. + */ +public class ContainerAsset( + override val format: Format, + public val container: Container<Resource> +) : Asset() { + + override suspend fun close() { + container.close() + } +} + +/** + * A single resource asset. + * + * @param format Format of the asset. + * @param resource Opened resource to access the asset. + */ +public class ResourceAsset( + override val format: Format, + public val resource: Resource +) : Asset() { + + override suspend fun close() { + resource.close() + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt new file mode 100644 index 0000000000..18ad90519f --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -0,0 +1,292 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.asset + +import android.content.ContentResolver +import java.io.File +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Either +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.FileExtension +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.archive.ArchiveOpener +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.file.FileResource +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatHints +import org.readium.r2.shared.util.format.FormatSniffer +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceFactory +import org.readium.r2.shared.util.resource.borrow +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.resource.mediaType +import org.readium.r2.shared.util.use + +/** + * Retrieves an [Asset] instance providing reading access to the resource(s) of an asset stored at + * a given [Url] as well as its [Format]. + */ +public class AssetRetriever private constructor( + private val assetSniffer: AssetSniffer, + private val resourceFactory: ResourceFactory, + private val archiveOpener: ArchiveOpener +) { + public constructor( + resourceFactory: ResourceFactory, + archiveOpener: ArchiveOpener, + formatSniffer: FormatSniffer + ) : this(AssetSniffer(formatSniffer, archiveOpener), resourceFactory, archiveOpener) + + public constructor( + contentResolver: ContentResolver, + httpClient: HttpClient + ) : this( + DefaultResourceFactory(contentResolver, httpClient), + DefaultArchiveOpener(), + DefaultFormatSniffer() + ) + + /** + * Error while trying to retrieve an asset from an URL. + */ + public sealed class RetrieveUrlError( + override val message: String, + override val cause: Error? + ) : Error { + + /** + * The scheme (e.g. http, file, content) for the requested [Url] is not supported. + */ + public class SchemeNotSupported( + public val scheme: Url.Scheme, + cause: Error? = null + ) : RetrieveUrlError("Url scheme $scheme is not supported.", cause) + + /** + * The format of the resource at the requested [Url] is not recognized. + */ + public class FormatNotSupported( + cause: Error? = null + ) : RetrieveUrlError("Asset format is not supported.", cause) + + /** + * An error occurred when trying to read the asset. + */ + public class Reading(override val cause: org.readium.r2.shared.util.data.ReadError) : + RetrieveUrlError("An error occurred when trying to read asset.", cause) + } + + /** + * Error while trying to retrieve an asset from a [Resource] or a [Container]. + */ + public sealed class RetrieveError( + override val message: String, + override val cause: Error? + ) : Error { + + /** + * The format of the resource is not recognized. + */ + public class FormatNotSupported( + cause: Error? = null + ) : RetrieveError("Asset format is not supported.", cause) + + /** + * An error occurred when trying to read the asset. + */ + public class Reading(override val cause: org.readium.r2.shared.util.data.ReadError) : + RetrieveError("An error occurred when trying to read asset.", cause) + } + + /** + * Retrieves an asset from an url and a known format. + */ + public suspend fun retrieve( + url: AbsoluteUrl, + format: Format + ): Try<Asset, RetrieveUrlError> { + val resource = resourceFactory.create(url) + .getOrElse { + when (it) { + is ResourceFactory.Error.SchemeNotSupported -> + return Try.failure(RetrieveUrlError.SchemeNotSupported(it.scheme, it)) + } + } + + val asset = archiveOpener + .open(format, resource) + .getOrElse { + return when (it) { + is ArchiveOpener.OpenError.Reading -> + Try.failure(RetrieveUrlError.Reading(it.cause)) + is ArchiveOpener.OpenError.FormatNotSupported -> + Try.success(ResourceAsset(format, resource)) + } + } + + return Try.success(asset) + } + + /** + * Retrieves an asset from a local file. + */ + public suspend fun retrieve( + file: File, + formatHints: FormatHints = FormatHints() + ): Try<Asset, RetrieveError> = + retrieve(FileResource(file), formatHints) + + /** + * Retrieves an asset from an [AbsoluteUrl]. + */ + public suspend fun retrieve( + url: AbsoluteUrl, + formatHints: FormatHints = FormatHints() + ): Try<Asset, RetrieveUrlError> { + val resource = resourceFactory.create(url) + .getOrElse { + return Try.failure( + when (it) { + is ResourceFactory.Error.SchemeNotSupported -> + RetrieveUrlError.SchemeNotSupported(it.scheme) + } + ) + } + + return retrieve(resource, formatHints) + .mapFailure { + when (it) { + is RetrieveError.FormatNotSupported -> RetrieveUrlError.FormatNotSupported( + it.cause + ) + is RetrieveError.Reading -> RetrieveUrlError.Reading(it.cause) + } + } + } + + /** + * Retrieves an asset from an [AbsoluteUrl]. + */ + public suspend fun retrieve( + url: AbsoluteUrl, + mediaType: MediaType + ): Try<Asset, RetrieveUrlError> = + retrieve(url, FormatHints(mediaType = mediaType)) + + /** + * Retrieves an asset from a local file. + */ + public suspend fun retrieve( + file: File, + mediaType: MediaType + ): Try<Asset, RetrieveError> = + retrieve(file, FormatHints(mediaType = mediaType)) + + /** + * Retrieves an asset from an already opened resource. + */ + public suspend fun retrieve( + resource: Resource, + hints: FormatHints = FormatHints() + ): Try<Asset, RetrieveError> { + val properties = resource.properties() + .getOrElse { return Try.failure(RetrieveError.Reading(it)) } + + val internalHints = FormatHints( + mediaType = properties.mediaType, + fileExtension = properties.filename + ?.substringAfterLast(".") + ?.let { FileExtension((it)) } + ) + + return assetSniffer + .sniff(Either.Left(resource), hints + internalHints) + .mapFailure { + when (it) { + AssetSniffer.SniffError.NotRecognized -> RetrieveError.FormatNotSupported(it) + is AssetSniffer.SniffError.Reading -> RetrieveError.Reading(it.cause) + } + } + } + + /** + * Retrieves an asset from an already opened container. + */ + public suspend fun retrieve( + container: Container<Resource>, + hints: FormatHints = FormatHints() + ): Try<Asset, RetrieveError> = + assetSniffer + .sniff(Either.Right(container), hints) + .mapFailure { + when (it) { + AssetSniffer.SniffError.NotRecognized -> RetrieveError.FormatNotSupported(it) + is AssetSniffer.SniffError.Reading -> RetrieveError.Reading(it.cause) + } + } + + /** + * Retrieves an asset from an already opened resource. + */ + public suspend fun retrieve( + resource: Resource, + mediaType: MediaType + ): Try<Asset, RetrieveError> = + retrieve(resource, FormatHints(mediaType = mediaType)) + + /** + * Retrieves an asset from an already opened container. + */ + public suspend fun retrieve( + container: Container<Resource>, + mediaType: MediaType + ): Try<Asset, RetrieveError> = + retrieve(container, FormatHints(mediaType = mediaType)) + + /** + * Sniffs the format of a file content. + */ + public suspend fun sniffFormat( + file: File, + hints: FormatHints = FormatHints() + ): Try<Format, RetrieveError> = + FileResource(file).use { sniffFormat(it, hints) } + + /** + * Sniffs the format of the content available at [url]. + */ + public suspend fun sniffFormat( + url: AbsoluteUrl, + hints: FormatHints = FormatHints() + ): Try<Format, RetrieveUrlError> = + retrieve(url, hints) + .map { asset -> asset.use { it.format } } + + /** + * Sniffs the format of a resource content. + */ + public suspend fun sniffFormat( + resource: Resource, + hints: FormatHints = FormatHints() + ): Try<Format, RetrieveError> = + retrieve(resource.borrow(), hints) + .map { asset -> asset.use { it.format } } + + /** + * Sniffs the format of a container content. + */ + public suspend fun sniffFormat( + container: Container<Resource>, + hints: FormatHints = FormatHints() + ): Try<Format, RetrieveError> = + retrieve(container, hints) + .map { asset -> asset.use { it.format } } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt new file mode 100644 index 0000000000..212a020500 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.asset + +import org.readium.r2.shared.util.Either +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.FileExtension +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.archive.ArchiveOpener +import org.readium.r2.shared.util.data.CachingContainer +import org.readium.r2.shared.util.data.CachingReadable +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatHints +import org.readium.r2.shared.util.format.FormatSniffer +import org.readium.r2.shared.util.format.FormatSpecification +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.tryRecover + +internal class AssetSniffer( + private val formatSniffer: FormatSniffer = DefaultFormatSniffer(), + private val archiveOpener: ArchiveOpener = DefaultArchiveOpener() +) { + + sealed class SniffError( + override val message: String, + override val cause: Error? + ) : Error { + + data object NotRecognized : + SniffError("Format of resource could not be inferred.", null) + + data class Reading(override val cause: ReadError) : + SniffError("An error occurred while trying to read content.", cause) + } + + suspend fun sniff( + source: Either<Resource, Container<Resource>>, + hints: FormatHints + ): Try<Asset, SniffError> { + val initialDescription = Format( + specification = FormatSpecification(emptySet()), + mediaType = MediaType.BINARY, + fileExtension = FileExtension("") + ) + + val cachingSource: Either<Readable, Container<Readable>> = when (source) { + is Either.Left -> Either.Left(CachingReadable(source.value)) + is Either.Right -> Either.Right(CachingContainer(source.value)) + } + + val asset = doSniff(initialDescription, source, cachingSource, hints) + .getOrElse { return Try.failure(SniffError.Reading(it)) } + + return asset + .takeIf { it.format.isValid() } + ?.let { Try.success(it) } + ?: Try.failure(SniffError.NotRecognized) + } + + private suspend fun doSniff( + format: Format, + source: Either<Resource, Container<Resource>>, + cache: Either<Readable, Container<Readable>>, + hints: FormatHints + ): Try<Asset, ReadError> { + formatSniffer + .sniffHints(format, hints) + .takeIf { it.conformsTo(format) } + ?.takeIf { it != format } + ?.let { return doSniff(it, source, cache, hints) } + + when (cache) { + is Either.Left -> + formatSniffer + .sniffBlob(format, cache.value) + .getOrElse { return Try.failure(it) } + .takeIf { it.conformsTo(format) } + ?.takeIf { it != format } + ?.let { return doSniff(it, source, cache, hints) } + + is Either.Right -> + formatSniffer + .sniffContainer(format, cache.value) + .getOrElse { return Try.failure(it) } + .takeIf { it.conformsTo(format) } + ?.takeIf { it != format } + ?.let { return doSniff(it, source, cache, hints) } + } + + if (source is Either.Left) { + tryOpenArchive(format, source.value) + .getOrElse { return Try.failure(it) } + ?.let { + return doSniff( + it.format, + Either.Right(it.container), + Either.Right(CachingContainer(it.container)), + hints + ) + } + } + + return Try.success( + when (source) { + is Either.Left -> ResourceAsset(format, source.value) + is Either.Right -> ContainerAsset(format, source.value) + } + ) + } + + private suspend fun tryOpenArchive( + format: Format, + source: Readable + ): Try<ContainerAsset?, ReadError> = + if (!format.isValid()) { + archiveOpener.sniffOpen(source) + .tryRecover { + when (it) { + is ArchiveOpener.SniffOpenError.NotRecognized -> + Try.success(null) + is ArchiveOpener.SniffOpenError.Reading -> + Try.failure(it.cause) + } + } + } else { + archiveOpener.open(format, source) + .tryRecover { + when (it) { + is ArchiveOpener.OpenError.FormatNotSupported -> + Try.success(null) + is ArchiveOpener.OpenError.Reading -> + Try.failure(it.cause) + } + } + } + + private fun Format.isValid(): Boolean = + specification.specifications.isNotEmpty() && + mediaType != MediaType.BINARY && + fileExtension.value.isNotBlank() +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Defaults.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Defaults.kt new file mode 100644 index 0000000000..62f3397d45 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Defaults.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.asset + +import android.content.ContentResolver +import org.readium.r2.shared.util.archive.ArchiveOpener +import org.readium.r2.shared.util.archive.CompositeArchiveOpener +import org.readium.r2.shared.util.content.ContentResourceFactory +import org.readium.r2.shared.util.file.FileResourceFactory +import org.readium.r2.shared.util.format.ArchiveSniffer +import org.readium.r2.shared.util.format.AudioSniffer +import org.readium.r2.shared.util.format.BitmapSniffer +import org.readium.r2.shared.util.format.CompositeFormatSniffer +import org.readium.r2.shared.util.format.EpubDrmSniffer +import org.readium.r2.shared.util.format.EpubSniffer +import org.readium.r2.shared.util.format.FormatSniffer +import org.readium.r2.shared.util.format.HtmlSniffer +import org.readium.r2.shared.util.format.JsonSniffer +import org.readium.r2.shared.util.format.LcpLicenseSniffer +import org.readium.r2.shared.util.format.LpfSniffer +import org.readium.r2.shared.util.format.Opds1Sniffer +import org.readium.r2.shared.util.format.Opds2Sniffer +import org.readium.r2.shared.util.format.PdfSniffer +import org.readium.r2.shared.util.format.RarSniffer +import org.readium.r2.shared.util.format.RpfSniffer +import org.readium.r2.shared.util.format.RwpmSniffer +import org.readium.r2.shared.util.format.W3cWpubSniffer +import org.readium.r2.shared.util.format.ZipSniffer +import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpResourceFactory +import org.readium.r2.shared.util.resource.CompositeResourceFactory +import org.readium.r2.shared.util.resource.ResourceFactory +import org.readium.r2.shared.util.zip.ZipArchiveOpener + +/** + * Default implementation of [ResourceFactory] supporting file, content and http schemes. + * + * @param contentResolver content resolver to use to support content scheme. + * @param httpClient Http client to use to support http scheme. + * @param additionalFactories Additional [ResourceFactory] to support additional schemes. + */ +public class DefaultResourceFactory( + contentResolver: ContentResolver, + httpClient: HttpClient, + additionalFactories: List<ResourceFactory> = emptyList() +) : ResourceFactory by CompositeResourceFactory( + *additionalFactories.toTypedArray(), + FileResourceFactory(), + ContentResourceFactory(contentResolver), + HttpResourceFactory(httpClient) +) + +/** + * Default implementation of [ArchiveOpener] supporting only ZIP archives. + * + * @param additionalOpeners Additional openers to be used. + */ +public class DefaultArchiveOpener( + additionalOpeners: List<ArchiveOpener> = emptyList() +) : ArchiveOpener by CompositeArchiveOpener( + *additionalOpeners.toTypedArray(), + ZipArchiveOpener() +) + +/** + * Default implementation of [FormatSniffer] guessing as well as possible all formats known by + * Readium. + * + * @param additionalSniffers Additional sniffers to be used to guess content format. + */ +public class DefaultFormatSniffer( + additionalSniffers: List<FormatSniffer> = emptyList() +) : FormatSniffer by CompositeFormatSniffer( + *additionalSniffers.toTypedArray(), + ZipSniffer, + RarSniffer, + EpubSniffer, + LpfSniffer, + ArchiveSniffer, + RpfSniffer, + PdfSniffer, + HtmlSniffer, + BitmapSniffer, + AudioSniffer, + JsonSniffer, + Opds1Sniffer, + Opds2Sniffer, + LcpLicenseSniffer, + EpubDrmSniffer, + W3cWpubSniffer, + RwpmSniffer +) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/cache/Cache.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/cache/Cache.kt index 511d3bc25f..09b96a11b6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/cache/Cache.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/cache/Cache.kt @@ -15,6 +15,7 @@ import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.util.Closeable import org.readium.r2.shared.util.MemoryObserver import org.readium.r2.shared.util.SuspendingCloseable +import org.readium.r2.shared.util.Try /** * A generic cache for objects of type [V]. @@ -22,53 +23,64 @@ import org.readium.r2.shared.util.SuspendingCloseable * It implements [MemoryObserver] to flush unused in-memory objects when necessary. */ @InternalReadiumApi -interface Cache<V> : SuspendingCloseable, MemoryObserver { +public interface Cache<V> : SuspendingCloseable, MemoryObserver { /** * Performs an atomic [block] transaction on this cache. */ - suspend fun <T> transaction(block: suspend CacheTransaction<V>.() -> T): T + public suspend fun <T> transaction(block: suspend CacheTransaction<V>.() -> T): T } /** * An atomic transaction run on a cache for objects of type [V]. */ @InternalReadiumApi -interface CacheTransaction<V> { +public interface CacheTransaction<V> { /** * Gets the current cached value for the given [key]. */ - suspend fun get(key: String): V? + public suspend fun get(key: String): V? /** * Writes the cached [value] for the given [key]. */ - suspend fun put(key: String, value: V?) - - /** - * Gets the current cached value for the given [key] or creates and caches a new one. - */ - suspend fun <V> CacheTransaction<V>.getOrPut(key: String, defaultValue: suspend () -> V): V = - get(key) - ?: defaultValue().also { put(key, it) } + public suspend fun put(key: String, value: V?) /** * Clears the cached value for the given [key]. * * @return The cached value if any. */ - suspend fun remove(key: String): V? + public suspend fun remove(key: String): V? /** * Clears all cached values. */ - suspend fun clear() + public suspend fun clear() } +/** + * Gets the current cached value for the given [key] or creates and caches a new one. + */ +public suspend fun <V> CacheTransaction<V>.getOrPut(key: String, defaultValue: suspend () -> V): V = + get(key) + ?: defaultValue().also { put(key, it) } + +/** + * Gets the current cached value for the given [key] or creates and caches a new one. + */ +public suspend fun <V, F> CacheTransaction<V>.getOrTryPut( + key: String, + defaultValue: suspend () -> Try<V, F> +): Try<V, F> = + get(key)?.let { Try.success(it) } + ?: defaultValue() + .onSuccess { put(key, it) } + /** * A basic in-memory cache. */ @InternalReadiumApi -class InMemoryCache<V> : Cache<V> { +public class InMemoryCache<V> : Cache<V> { private val values = mutableMapOf<String, V>() private val mutex = Mutex() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt new file mode 100644 index 0000000000..38e058eea7 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.content + +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.data.AccessError + +/** + * Errors wrapping Android ContentResolver errors. + */ +public sealed class ContentResolverError( + override val message: String, + override val cause: Error? = null +) : AccessError { + + public class FileNotFound( + cause: Error? = null + ) : ContentResolverError("File not found.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + public class NotAvailable( + cause: Error? = null + ) : ContentResolverError("Content Provider recently crashed.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + public class IO( + override val cause: Error + ) : ContentResolverError("An IO error occurred.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt new file mode 100644 index 0000000000..da202076b7 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.content + +import android.content.ContentResolver +import android.net.Uri +import android.provider.MediaStore +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.readium.r2.shared.extensions.* +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.resource.mediaType +import org.readium.r2.shared.util.toUrl + +/** + * A [Resource] to access content [uri] thanks to a [ContentResolver]. + * + * @param uri the [Uri] to read. + * @param contentResolver a ContentResolver. + */ +public class ContentResource( + private val uri: Uri, + private val contentResolver: ContentResolver +) : Resource { + + private lateinit var _length: Try<Long, ReadError> + + private lateinit var _properties: Try<Resource.Properties, ReadError> + + override val sourceUrl: AbsoluteUrl? = uri.toUrl() as? AbsoluteUrl + + override suspend fun close() { + } + + override suspend fun properties(): Try<Resource.Properties, ReadError> { + if (::_properties.isInitialized) { + return _properties + } + + val filename = + contentResolver.queryProjection(uri, MediaStore.MediaColumns.DISPLAY_NAME) + + val mediaType = + contentResolver.getType(uri) + ?.let { MediaType(it) } + ?.takeUnless { it.matches(MediaType.BINARY) } + + val properties = + Resource.Properties( + Resource.Properties.Builder() + .also { + it.filename = filename + it.mediaType = mediaType + } + ) + + _properties = Try.success(properties) + + return _properties + } + + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> { + if (range == null) { + return readFully() + } + + @Suppress("NAME_SHADOWING") + val range = range + .coerceFirstNonNegative() + .requireLengthFitInt() + + if (range.isEmpty()) { + return Try.success(ByteArray(0)) + } + + return readRange(range) + } + + private suspend fun readFully(): Try<ByteArray, ReadError> = + withStream { it.readFully() } + + private suspend fun readRange(range: LongRange): Try<ByteArray, ReadError> = + withStream { + withContext(Dispatchers.IO) { + var skipped: Long = 0 + + while (skipped != range.first) { + skipped += it.skip(range.first - skipped) + if (skipped == 0L) { + throw IOException("Could not skip InputStream to read ranges from $uri.") + } + } + + val length = range.last - range.first + 1 + it.read(length) + } + } + + override suspend fun length(): Try<Long, ReadError> { + if (!::_length.isInitialized) { + _length = Try.catching { + contentResolver.openFileDescriptor(uri, "r") + ?.use { fd -> fd.statSize.takeUnless { it == -1L } } + }.flatMap { + when (it) { + null -> Try.failure( + ReadError.UnsupportedOperation( + DebugError("Content provider does not provide length for uri $uri.") + ) + ) + else -> Try.success(it) + } + } + } + + return _length + } + + private suspend fun <T> withStream(block: suspend (InputStream) -> T): Try<T, ReadError> { + return Try.catching { + val stream = contentResolver.openInputStream(uri) + ?: return Try.failure( + ReadError.Access( + ContentResolverError.NotAvailable() + ) + ) + stream.use { block(stream) } + } + } + + private inline fun <T> Try.Companion.catching(closure: () -> T): Try<T, ReadError> = + try { + success(closure()) + } catch (e: FileNotFoundException) { + failure(ReadError.Access(ContentResolverError.FileNotFound(e))) + } catch (e: IOException) { + failure(ReadError.Access(ContentResolverError.IO(e))) + } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. + failure(ReadError.OutOfMemory(e)) + } + + override fun toString(): String = + "${javaClass.simpleName}(${runBlocking { length() } } bytes)" +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt new file mode 100644 index 0000000000..3136b17742 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.content + +import android.content.ContentResolver +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceFactory +import org.readium.r2.shared.util.toUri + +/** + * Creates [Resource] instances granting access to `content://` URLs provided by the given + * [contentResolver]. + */ +public class ContentResourceFactory( + private val contentResolver: ContentResolver +) : ResourceFactory { + + override suspend fun create( + url: AbsoluteUrl + ): Try<Resource, ResourceFactory.Error> { + if (!url.isContent) { + return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) + } + + val resource = ContentResource(url.toUri(), contentResolver) + + return Try.success(resource) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt new file mode 100644 index 0000000000..e38f64e029 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import org.readium.r2.shared.extensions.coerceIn +import org.readium.r2.shared.extensions.contains +import org.readium.r2.shared.extensions.requireLengthFitInt +import org.readium.r2.shared.util.Try + +/** + * Wraps this resource into a buffer to improve reading performances. + * + * Expensive interaction with the underlying resource is minimized, since most (smaller) requests + * can be satisfied by accessing the buffer alone. The drawback is that some extra space is required + * to hold the buffer and that copying takes place when filling that buffer, but this is usually + * outweighed by the performance benefits. + * + * Note that this implementation is pretty limited and the benefits are only apparent when reading + * forward and consecutively – e.g. when downloading the resource by chunks. The buffer is ignored + * when reading backward or far ahead. + * + * @param contentLength The total length of the resource, when known. This can improve performance + * by avoiding requesting the length from the underlying resource. + * @param bufferSize Size of the buffer chunks to read. + */ +public fun Readable.buffered( + contentLength: Long? = null, + bufferSize: Int = DEFAULT_BUFFER_SIZE +): Readable = + ReadableBuffer(source = this, contentLength = contentLength, bufferSize = bufferSize) + +/** + * Wraps a [Readable] and buffers its content. + * + * @param source Underlying readable which will be buffered. + * @param contentLength The total length of the readable, when known. This can improve performance + * by avoiding requesting the length from the underlying resource. + * @param bufferSize Size of the buffer chunks to read. + */ +internal class ReadableBuffer internal constructor( + private val source: Readable, + contentLength: Long? = null, + private val bufferSize: Int = DEFAULT_BUFFER_SIZE +) : Readable by source { + + /** + * The buffer containing the current bytes read from the wrapped [Readable], with the range it + * covers. + */ + private var buffer: Pair<ByteArray, LongRange>? = null + + private lateinit var _cachedLength: Try<Long, ReadError> + private suspend fun cachedLength(): Try<Long, ReadError> { + if (!::_cachedLength.isInitialized) { + _cachedLength = source.length() + } + return _cachedLength + } + + init { + if (contentLength != null) { + _cachedLength = Try.success(contentLength) + } + } + + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> { + val length = cachedLength().getOrNull() + // Reading the whole resource bypasses buffering to keep things simple. + if (range == null || length == null) { + return source.read(range) + } + + val requestedRange = range + .coerceIn(0L until length) + .requireLengthFitInt() + if (requestedRange.isEmpty()) { + return Try.success(ByteArray(0)) + } + + // Round up the range to be read to the next `bufferSize`, because we will buffer the + // excess. + val readLast = (requestedRange.last + 1).ceilMultipleOf(bufferSize.toLong()).coerceAtMost( + length + ) + var readRange = requestedRange.first until readLast + + // Attempt to serve parts or all of the request using the buffer. + buffer?.let { pair -> + var (buffer, bufferedRange) = pair + + // Everything already buffered? + if (bufferedRange.contains(requestedRange)) { + val data = extractRange(requestedRange, buffer, start = bufferedRange.first) + return Try.success(data) + + // Beginning of requested data is buffered? + } else if (bufferedRange.contains(requestedRange.first)) { + readRange = (bufferedRange.last + 1)..readRange.last + + return source.read(readRange).map { readData -> + buffer += readData + // Shift the current buffer to the tail of the read data. + saveBuffer(buffer, readRange) + + val bytes = extractRange(requestedRange, buffer, start = bufferedRange.first) + bytes + } + } + } + + // Fallback on reading the requested range from the original resource. + return source.read(readRange).map { data -> + saveBuffer(data, readRange) + + val res = if (data.count() > requestedRange.count()) { + data.copyOfRange(0, requestedRange.count()) + } else { + data + } + + res + } + } + + /** + * Keeps the last chunk of the given data as the buffer for next reads. + * + * @param data Data read from the original resource. + * @param range Range of the read data in the resource. + */ + private fun saveBuffer(data: ByteArray, range: LongRange) { + val lastChunk = data.takeLast(bufferSize).toByteArray() + val chunkRange = (range.last + 1 - lastChunk.count())..range.last + buffer = Pair(lastChunk, chunkRange) + } + + /** + * Reads a sub-range of the given [data] after shifting the given absolute (to the resource) + * ranges to be relative to [data]. + */ + private fun extractRange(requestedRange: LongRange, data: ByteArray, start: Long): ByteArray { + val first = requestedRange.first - start + val lastExclusive = first + requestedRange.count() + require(first >= 0) + require(lastExclusive <= data.count()) { "$lastExclusive > ${data.count()}" } + return data.copyOfRange(first.toInt(), lastExclusive.toInt()) + } + + private fun Long.ceilMultipleOf(divisor: Long) = + divisor * (this / divisor + if (this % divisor == 0L) 0 else 1) +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Caching.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Caching.kt new file mode 100644 index 0000000000..8274928b45 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Caching.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url + +internal class CachingReadable( + private val source: Readable +) : Readable by source { + + private var startCache: ByteArray? = null + + private var contentLength: Long? = null + + override suspend fun length(): Try<Long, ReadError> { + contentLength?.let { Try.success(it) } + + return source.length() + .onSuccess { contentLength = it } + } + + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> { + return when { + startCache == null -> { + source.read(range) + .onSuccess { + if (range == null || range.first == 0L) { + startCache = it + } + } + } + range == null -> { + if (contentLength == startCache!!.size.toLong()) { + Try.success(startCache!!) + } else { + source.read() + .onSuccess { + startCache = it + contentLength = it.size.toLong() + } + } + } + range.first == 0L -> { + if (range.last < startCache!!.size) { + Try.success(startCache!!.sliceArray(0..range.last.toInt())) + } else { + source.read(range) + .onSuccess { startCache = it } + } + } + else -> + return source.read(range) + } + } + + override suspend fun close() {} +} + +internal class CachingContainer( + private val container: Container<Readable> +) : Container<Readable> by container { + + private val cache: MutableMap<Url, CachingReadable> = + mutableMapOf() + + override fun get(url: Url): Readable? { + cache[url]?.let { return it } + + val entry = container[url] + ?: return null + + val blobContext = CachingReadable(entry) + + cache[url] = blobContext + + return blobContext + } + + override suspend fun close() { + cache.forEach { it.value.close() } + cache.clear() + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt new file mode 100644 index 0000000000..bf81816be4 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.SuspendingCloseable +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.use + +/** + * A container provides access to a list of [Readable] entries. + */ +public interface Container<out E : Readable> : Iterable<Url>, SuspendingCloseable { + + /** + * Direct source to this container, when available. + */ + public val sourceUrl: AbsoluteUrl? get() = null + + /** + * List of all the container entries. + */ + public val entries: Set<Url> + + override fun iterator(): Iterator<Url> = + entries.iterator() + + /** + * Returns the entry at the given [url] or null if there is none. + */ + public operator fun get(url: Url): E? +} + +/** A [Container] providing no entries at all. */ +public class EmptyContainer<E : Readable> : + Container<E> { + + override val entries: Set<Url> = emptySet() + + override fun get(url: Url): E? = null + + override suspend fun close() {} +} + +/** + * Concatenates several containers. + * + * This can be used for example to serve a publication containing both local and remote resources, + * and more generally to concatenate different content sources. + * + * The [containers] will be tested in the given order. + */ +public class CompositeContainer<E : Readable>( + private val containers: List<Container<E>> +) : Container<E> { + + public constructor(vararg containers: Container<E>) : + this(containers.toList()) + + override val entries: Set<Url> = + containers.fold(emptySet()) { acc, container -> acc + container.entries } + + override fun get(url: Url): E? = + containers.firstNotNullOfOrNull { it[url] } + + override suspend fun close() { + containers.forEach { it.close() } + } +} + +@InternalReadiumApi +public suspend inline fun<S> Container<Readable>.readDecodeOrNull( + url: Url, + decode: (ByteArray) -> Try<S, DecodeError> +): S? = + get(url)?.use { resource -> + resource.readDecodeOrNull(decode) + } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt new file mode 100644 index 0000000000..c2d0ddb180 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -0,0 +1,173 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import java.io.ByteArrayInputStream +import java.nio.charset.Charset +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.tryRecover +import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.shared.util.xml.XmlParser + +/** + * Errors produced when trying to decode content. + */ +public sealed class DecodeError( + override val message: String, + override val cause: Error +) : Error { + + /** + * Content could not be successfully decoded because there is not enough memory available. + */ + public class OutOfMemory(override val cause: ThrowableError<OutOfMemoryError>) : + DecodeError("The resource is too large to be read on this device.", cause) { + + public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) + } + + /** + * Content could not be successfully decoded because it doesn't match what was expected. + */ + public class Decoding(cause: Error) : + DecodeError("Decoding Error", cause) +} + +/** + * Decodes receiver properly wrapping exceptions into [DecodeError]s. + */ +@InternalReadiumApi +public suspend fun<R, S> S.decode( + block: (value: S) -> R, + wrapError: (Exception) -> Error +): Try<R, DecodeError> = + withContext(Dispatchers.Default) { + try { + Try.success(block(this@decode)) + } catch (e: Exception) { + Try.failure(DecodeError.Decoding(wrapError(e))) + } catch (e: OutOfMemoryError) { + Try.failure(DecodeError.OutOfMemory(e)) + } + } + +/** + * Content as plain text. + */ +public suspend fun ByteArray.decodeString( + charset: Charset = Charsets.UTF_8 +): Try<String, DecodeError> = + decode( + { String(it, charset = charset) }, + { DebugError("Content is not a valid $charset string.", ThrowableError(it)) } + ) + +/** Content as an XML document. */ +public suspend fun ByteArray.decodeXml(): Try<ElementNode, DecodeError> = + decode( + { XmlParser().parse(ByteArrayInputStream(it)) }, + { DebugError("Content is not a valid XML document.", ThrowableError(it)) } + ) + +/** + * Content parsed from JSON. + */ +public suspend fun ByteArray.decodeJson(): Try<JSONObject, DecodeError> = + decodeString().flatMap { string -> + decode( + { JSONObject(string) }, + { DebugError("Content is not valid JSON.", ThrowableError(it)) } + ) + } + +/** + * Readium Web Publication Manifest parsed from the content. + */ +public suspend fun ByteArray.decodeRwpm(): Try<Manifest, DecodeError> = + decodeJson().flatMap { it.decodeRwpm() } + +/** + * Readium Web Publication Manifest parsed from JSON. + */ +public suspend fun JSONObject.decodeRwpm(): Try<Manifest, DecodeError> = + decode( + { + Manifest.fromJSON(this) + ?: throw Exception("Manifest.fromJSON returned null") + }, + { DebugError("Content is not a valid RWPM.") } + ) + +/** + * Reads the full content as a [Bitmap]. + */ +public suspend fun ByteArray.decodeBitmap(): Try<Bitmap, DecodeError> = + decode( + { + BitmapFactory.decodeByteArray(this, 0, size) + ?: throw Exception("BitmapFactory returned null.") + }, + { DebugError("Could not decode content as a bitmap.") } + ) + +@Suppress("RedundantSuspendModifier") +@InternalReadiumApi +public suspend inline fun<R> Try<ByteArray, ReadError>.decodeOrElse( + decode: (value: ByteArray) -> Try<R, DecodeError>, + recover: (DecodeError.Decoding) -> R +): Try<R, ReadError> = + flatMap { + decode(it) + .tryRecover { error -> + when (error) { + is DecodeError.OutOfMemory -> + Try.failure(ReadError.OutOfMemory(error.cause)) + is DecodeError.Decoding -> + Try.success(recover(error)) + } + } + } + +@Suppress("RedundantSuspendModifier") +@InternalReadiumApi +public suspend inline fun<R> Try<ByteArray, ReadError>.decodeOrNull( + decode: (value: ByteArray) -> Try<R, DecodeError> +): R? = + flatMap { decode(it) }.getOrNull() + +@InternalReadiumApi +public suspend inline fun<R> Readable.readDecodeOrElse( + decode: (value: ByteArray) -> Try<R, DecodeError>, + recoverRead: (ReadError) -> R, + recoverDecode: (DecodeError.Decoding) -> R +): R = + read().decodeOrElse(decode, recoverDecode).getOrElse(recoverRead) + +@InternalReadiumApi +public suspend inline fun<R> Readable.readDecodeOrElse( + decode: (value: ByteArray) -> Try<R, DecodeError>, + recover: (ReadError) -> R +): R = + readDecodeOrElse(decode, recover) { recover(ReadError.Decoding(it)) } + +@InternalReadiumApi +public suspend inline fun<R> Readable.readDecodeOrNull( + decode: (value: ByteArray) -> Try<R, DecodeError> +): R? = + read().decodeOrNull(decode) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/InputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/InputStream.kt new file mode 100644 index 0000000000..9cf44ff8ce --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/InputStream.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import java.io.IOException +import java.io.InputStream +import kotlinx.coroutines.runBlocking +import org.readium.r2.shared.util.Try + +/** + * Wraps a [Readable] into an [InputStream]. + * + * Ownership of the [Readable] is transferred to the returned [InputStream]. + */ +public fun Readable.asInputStream( + range: LongRange? = null, + wrapError: (ReadError) -> IOException = { ReadException(it) } +): InputStream = + ReadableInputStreamAdapter(this, range, wrapError) + +/** + * Input stream reading through a [Readable] and taking ownership of it. + * + * If you experience bad performances, consider wrapping the stream in a BufferedInputStream. This + * is particularly useful when streaming deflated ZIP entries. + */ +private class ReadableInputStreamAdapter( + private val readable: Readable, + private val range: LongRange? = null, + private val wrapError: (ReadError) -> IOException = { ReadException(it) } +) : InputStream() { + + private var isClosed = false + + private val end: Long by lazy { + val resourceLength = + runBlocking { readable.length() } + .recover() + + if (range == null) { + resourceLength + } else { + kotlin.math.min(resourceLength, range.last + 1) + } + } + + /** Current position in the resource. */ + private var position: Long = range?.start ?: 0 + + /** + * The currently marked position in the stream. Defaults to 0. + */ + private var mark: Long = range?.start ?: 0 + + override fun available(): Int { + checkNotClosed() + return (end - position).toInt() + } + + override fun skip(n: Long): Long = synchronized(this) { + checkNotClosed() + + val newPosition = (position + n).coerceAtMost(end) + val skipped = newPosition - position + position = newPosition + skipped + } + + override fun read(): Int = synchronized(this) { + checkNotClosed() + + if (available() <= 0) { + return -1 + } + + val bytes = runBlocking { + readable.read(position until (position + 1)) + .recover() + } + position += 1 + return bytes.first().toUByte().toInt() + } + + override fun read(b: ByteArray, off: Int, len: Int): Int = synchronized(this) { + checkNotClosed() + + if (available() <= 0) { + return -1 + } + + val bytesToRead = len.coerceAtMost(available()) + val bytes = runBlocking { + readable.read(position until (position + bytesToRead)) + .recover() + } + check(bytes.size <= bytesToRead) + bytes.copyInto( + destination = b, + destinationOffset = off, + startIndex = 0, + endIndex = bytes.size + ) + position += bytes.size + return bytes.size + } + + override fun markSupported(): Boolean = true + + override fun mark(readlimit: Int) { + synchronized(this) { + checkNotClosed() + mark = position + } + } + + override fun reset() { + synchronized(this) { + checkNotClosed() + position = mark + } + } + + /** + * Closes the underlying resource. + */ + override fun close() { + synchronized(this) { + if (isClosed) { + return + } + + runBlocking { readable.close() } + + isClosed = true + } + } + + private fun checkNotClosed() { + if (isClosed) { + throw IllegalStateException("InputStream is closed.") + } + } + + private fun<S> Try<S, ReadError>.recover(): S = + when (this) { + is Try.Success -> { + value + } + is Try.Failure -> { + throw wrapError(value) + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt new file mode 100644 index 0000000000..100aca8470 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import java.io.IOException +import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.SuspendingCloseable +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse + +/** + * Acts as a proxy to an actual data source by handling read access. + */ +public interface Readable : SuspendingCloseable { + + /** + * Returns data length from metadata if available, or calculated from reading the bytes otherwise. + * + * This value must be treated as a hint, as it might not reflect the actual bytes length. To get + * the real length, you need to read the whole resource. + */ + public suspend fun length(): Try<Long, ReadError> + + /** + * Reads the bytes at the given range. + * + * When [range] is null, the whole content is returned. Out-of-range indexes are clamped to the + * available length automatically. + */ + public suspend fun read(range: LongRange? = null): Try<ByteArray, ReadError> +} + +public typealias ReadTry<SuccessT> = Try<SuccessT, ReadError> + +/** + * Errors occurring while reading a resource. + */ +public sealed class ReadError( + override val message: String, + override val cause: Error +) : Error { + + /** + * An error occurred while trying to access the content. + * + * At the moment, [AccessError]s constructed by the toolkit can be either a FileSystemError, + * a ContentResolverError or an HttpError. + */ + public class Access(public override val cause: AccessError) : + ReadError("An error occurred while attempting to access data.", cause) + + /** + * Content doesn't match what was expected and cannot be interpreted. + * + * For instance, this error can be reported if a ZIP archive looks invalid, + * a publication doesn't conform to its format, or a JSON resource cannot be decoded. + */ + public class Decoding(cause: Error) : + ReadError("An error occurred while attempting to decode the content.", cause) { + + public constructor(message: String) : this(DebugError(message)) + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + /** + * Content could not be successfully read because there is not enough memory available. + * + * This error can be produced while trying to put the content into memory or while + * trying to decode it. + */ + public class OutOfMemory(override val cause: ThrowableError<OutOfMemoryError>) : + ReadError("The resource is too large to be read on this device.", cause) { + + public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) + } + + /** + * An operation could not be performed at some point. + * + * For instance, this error can occur no matter the level of indirection when trying + * to read ranges or getting length if any component the data has to pass through + * doesn't support that. + */ + public class UnsupportedOperation(cause: Error) : + ReadError("Could not proceed because an operation was not supported.", cause) { + + public constructor(message: String) : this(DebugError(message)) + } +} + +/** + * Marker interface for source-specific access errors. + */ +public interface AccessError : Error + +/** + * An [IOException] wrapping a [ReadError]. + * + * This is meant to be used in contexts where [IOException] are expected. + */ +public class ReadException( + public val error: ReadError +) : IOException(error.message, ErrorException(error)) + +/** + * Returns a new [Readable] accessing the same data but not owning them. + */ +public fun Readable.borrow(): Readable = + BorrowedReadable(this) + +private class BorrowedReadable( + private val readable: Readable +) : Readable by readable { + + override suspend fun close() { + // Do nothing + } +} + +@InternalReadiumApi +public suspend inline fun Readable.readOrElse( + recover: (ReadError) -> ByteArray +): ByteArray = + read().getOrElse(recover) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt new file mode 100644 index 0000000000..1beab01aae --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.downloads + +import java.io.File +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager +import org.readium.r2.shared.util.downloads.foreground.ForegroundDownloadManager +import org.readium.r2.shared.util.file.FileSystemError +import org.readium.r2.shared.util.mediatype.MediaType + +/** + * Manages a set of concurrent files downloaded through HTTP. + * + * Choose the implementation that best fits your needs: + * - [AndroidDownloadManager] for downloading files in the background with the Android system + * service, even if the app is stopped. + * - [ForegroundDownloadManager] for a simpler implementation based on HttpClient which cancels + * the on-going download when the app is closed. + */ +public interface DownloadManager { + + public data class Request( + val url: AbsoluteUrl, + val headers: Map<String, List<String>> = emptyMap() + ) + + public data class Download( + val file: File, + val mediaType: MediaType? + ) + + @JvmInline + public value class RequestId(public val value: String) + + public sealed class DownloadError( + override val message: String, + override val cause: Error? = null + ) : Error { + + public class Http( + cause: org.readium.r2.shared.util.http.HttpError + ) : DownloadError(cause.message, cause) + + public class CannotResume( + cause: Error? = null + ) : DownloadError("Download couldn't be resumed.", cause) + + public class FileSystem( + override val cause: FileSystemError + ) : DownloadError("IO error on the local device.", cause) + + public class Unknown( + cause: Error? = null + ) : DownloadError("An unknown error occurred.", cause) + } + + public interface Listener { + + /** + * The download with ID [requestId] has been successfully completed. + */ + public fun onDownloadCompleted(requestId: RequestId, download: Download) + + /** + * The request with ID [requestId] has downloaded [downloaded] out of [expected] bytes. + */ + public fun onDownloadProgressed(requestId: RequestId, downloaded: Long, expected: Long?) + + /** + * The download with ID [requestId] failed due to [error]. + */ + public fun onDownloadFailed(requestId: RequestId, error: DownloadError) + + /** + * The download with ID [requestId] has been cancelled. + */ + public fun onDownloadCancelled(requestId: RequestId) + } + + /** + * Submits a new request to this [DownloadManager]. The given [listener] will automatically be + * registered. + * + * Returns the ID of the download request, which can be used to cancel it. + */ + public fun submit(request: Request, listener: Listener): RequestId + + /** + * Registers a listener for the download with the given [requestId]. + * + * If your [DownloadManager] supports background downloading, this should typically be used when + * you create a new instance after the app restarted. + */ + public fun register(requestId: RequestId, listener: Listener) + + /** + * Cancels the download with the given [requestId]. + */ + public fun cancel(requestId: RequestId) + + /** + * Releases any in-memory resource associated with this [DownloadManager]. + * + * If the pending downloads cannot continue in the background, they will be cancelled. + */ + public fun close() +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt new file mode 100644 index 0000000000..92b0ef2f39 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -0,0 +1,328 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.downloads.android + +import android.app.DownloadManager as SystemDownloadManager +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Environment +import androidx.core.net.toFile +import java.io.File +import java.util.UUID +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.readium.r2.shared.extensions.tryOr +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.file.FileSystemError +import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.http.HttpStatus +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.toUri +import org.readium.r2.shared.util.units.Hz +import org.readium.r2.shared.util.units.hz + +/** + * A [DownloadManager] implementation using the Android download service. + */ +public class AndroidDownloadManager internal constructor( + private val context: Context, + private val destStorage: Storage, + private val dirType: String, + private val refreshRate: Hz, + private val allowDownloadsOverMetered: Boolean +) : DownloadManager { + + /** + * Creates a new instance of [AndroidDownloadManager]. + * + * Because of discrepancies across different devices, default notifications are disabled. + * If you want to use this [DownloadManager], you will need permission + * android.permission.DOWNLOAD_WITHOUT_NOTIFICATION. + * + * @param context Android context + * communicates it. + * @param destStorage Location where downloads should be stored + * @param refreshRate Frequency with which download status will be checked and + * listeners notified + * @param allowDownloadsOverMetered If downloads must be paused when only metered connexions + * are available + */ + public constructor( + context: Context, + destStorage: Storage = Storage.App, + refreshRate: Hz = 60.0.hz, + allowDownloadsOverMetered: Boolean = true + ) : this( + context = context, + destStorage = destStorage, + dirType = Environment.DIRECTORY_DOWNLOADS, + refreshRate = refreshRate, + allowDownloadsOverMetered = allowDownloadsOverMetered + ) + + public enum class Storage { + /** + * App internal storage. + */ + App, + + /** + * Shared storage, accessible by users. + */ + Shared; + } + + private val coroutineScope: CoroutineScope = + MainScope() + + private val downloadManager: SystemDownloadManager = + context.getSystemService(Context.DOWNLOAD_SERVICE) as SystemDownloadManager + + private var observeProgressJob: Job? = + null + + private val listeners: MutableMap<DownloadManager.RequestId, MutableList<DownloadManager.Listener>> = + mutableMapOf() + + public override fun register( + requestId: DownloadManager.RequestId, + listener: DownloadManager.Listener + ) { + listeners.getOrPut(requestId) { mutableListOf() }.add(listener) + + if (observeProgressJob == null) { + maybeStartObservingProgress() + } + } + + public override fun submit( + request: DownloadManager.Request, + listener: DownloadManager.Listener + ): DownloadManager.RequestId { + maybeStartObservingProgress() + + val androidRequest = createRequest( + uri = request.url.toUri(), + filename = generateFileName(extension = request.url.extension?.value), + headers = request.headers + ) + val downloadId = downloadManager.enqueue(androidRequest) + val requestId = DownloadManager.RequestId(downloadId.toString()) + register(requestId, listener) + return requestId + } + + private fun generateFileName(extension: String?): String { + val dottedExtension = extension + ?.let { ".$it" } + .orEmpty() + return "${UUID.randomUUID()}$dottedExtension" + } + + public override fun cancel(requestId: DownloadManager.RequestId) { + val longId = requestId.value.toLong() + downloadManager.remove(longId) + val listenersForId = listeners[requestId].orEmpty() + listenersForId.forEach { it.onDownloadCancelled(requestId) } + listeners.remove(requestId) + if (!listeners.any { it.value.isNotEmpty() }) { + maybeStopObservingProgress() + } + } + + private fun createRequest( + uri: Uri, + filename: String, + headers: Map<String, List<String>> + ): SystemDownloadManager.Request = + SystemDownloadManager.Request(uri) + .setNotificationVisibility(SystemDownloadManager.Request.VISIBILITY_HIDDEN) + .setDestination(filename) + .setHeaders(headers) + .setAllowedOverMetered(allowDownloadsOverMetered) + + private fun SystemDownloadManager.Request.setHeaders( + headers: Map<String, List<String>> + ): SystemDownloadManager.Request { + for (header in headers) { + for (value in header.value) { + addRequestHeader(header.key, value) + } + } + return this + } + + private fun SystemDownloadManager.Request.setDestination( + filename: String + ): SystemDownloadManager.Request { + when (destStorage) { + Storage.App -> + setDestinationInExternalFilesDir(context, dirType, filename) + + Storage.Shared -> + setDestinationInExternalPublicDir(dirType, filename) + } + return this + } + + private fun maybeStartObservingProgress() { + if (observeProgressJob != null || listeners.all { it.value.isEmpty() }) { + return + } + + observeProgressJob = coroutineScope.launch { + while (true) { + val cursor = downloadManager.query(SystemDownloadManager.Query()) + notify(cursor) + delay((1.0 / refreshRate.value).seconds) + } + } + } + + private fun maybeStopObservingProgress() { + if (listeners.all { it.value.isEmpty() }) { + observeProgressJob?.cancel() + observeProgressJob = null + } + } + + private suspend fun notify(cursor: Cursor) = cursor.use { + val knownDownloads = mutableSetOf<DownloadManager.RequestId>() + + // Notify about known downloads + while (cursor.moveToNext()) { + val facade = DownloadCursorFacade(cursor) + val id = DownloadManager.RequestId(facade.id.toString()) + val listenersForId = listeners[id].orEmpty() + if (listenersForId.isNotEmpty()) { + notifyDownload(id, facade, listenersForId) + } + knownDownloads.add(id) + } + + // Missing downloads have been cancelled. + val unknownDownloads = listeners - knownDownloads + unknownDownloads.forEach { entry -> + entry.value.forEach { it.onDownloadCancelled(entry.key) } + listeners.remove(entry.key) + } + maybeStopObservingProgress() + } + + private suspend fun notifyDownload( + id: DownloadManager.RequestId, + facade: DownloadCursorFacade, + listenersForId: List<DownloadManager.Listener> + ) { + when (facade.status) { + SystemDownloadManager.STATUS_FAILED -> { + listenersForId.forEach { + it.onDownloadFailed(id, mapErrorCode(facade.reason!!)) + } + downloadManager.remove(facade.id) + listeners.remove(id) + maybeStopObservingProgress() + } + SystemDownloadManager.STATUS_PAUSED -> {} + SystemDownloadManager.STATUS_PENDING -> {} + SystemDownloadManager.STATUS_SUCCESSFUL -> { + prepareResult( + Uri.parse(facade.localUri!!)!!.toFile(), + mediaType = facade.mediaType?.let { MediaType(it) } + ) + .onSuccess { download -> + listenersForId.forEach { it.onDownloadCompleted(id, download) } + }.onFailure { error -> + listenersForId.forEach { it.onDownloadFailed(id, error) } + } + downloadManager.remove(facade.id) + listeners.remove(id) + maybeStopObservingProgress() + } + SystemDownloadManager.STATUS_RUNNING -> { + listenersForId.forEach { + it.onDownloadProgressed(id, facade.downloadedSoFar, facade.expected) + } + } + } + } + + private suspend fun prepareResult(destFile: File, mediaType: MediaType?): Try<DownloadManager.Download, DownloadManager.DownloadError> = + withContext(Dispatchers.IO) { + val extension = destFile.extension.takeUnless { it.isEmpty() } + + val newDest = File(destFile.parent, generateFileName(extension)) + val renamed = tryOr(false) { destFile.renameTo(newDest) } + + if (renamed) { + val download = DownloadManager.Download( + file = newDest, + mediaType = mediaType + ) + Try.success(download) + } else { + Try.failure( + DownloadManager.DownloadError.FileSystem( + FileSystemError.IO(DebugError("Failed to rename the downloaded file.")) + ) + ) + } + } + + private fun mapErrorCode(code: Int): DownloadManager.DownloadError = + when (code) { + in 400 until 1000 -> + DownloadManager.DownloadError.Http(httpErrorForCode(code)) + SystemDownloadManager.ERROR_UNHANDLED_HTTP_CODE -> + DownloadManager.DownloadError.Http(httpErrorForCode(code)) + SystemDownloadManager.ERROR_HTTP_DATA_ERROR -> + DownloadManager.DownloadError.Http(HttpError.MalformedResponse(null)) + SystemDownloadManager.ERROR_TOO_MANY_REDIRECTS -> + DownloadManager.DownloadError.Http( + HttpError.Redirection(DebugError("Too many redirects.")) + ) + SystemDownloadManager.ERROR_CANNOT_RESUME -> + DownloadManager.DownloadError.CannotResume() + SystemDownloadManager.ERROR_DEVICE_NOT_FOUND -> + DownloadManager.DownloadError.FileSystem( + FileSystemError.FileNotFound( + DebugError("Missing device.") + ) + ) + SystemDownloadManager.ERROR_FILE_ERROR -> + DownloadManager.DownloadError.FileSystem( + FileSystemError.IO(DebugError("An error occurred on the filesystem.")) + ) + SystemDownloadManager.ERROR_INSUFFICIENT_SPACE -> + DownloadManager.DownloadError.FileSystem(FileSystemError.InsufficientSpace()) + SystemDownloadManager.ERROR_UNKNOWN -> + DownloadManager.DownloadError.Unknown() + else -> + DownloadManager.DownloadError.Unknown() + } + + private fun httpErrorForCode(code: Int): HttpError = + when (code) { + in 0 until 1000 -> HttpError.ErrorResponse(HttpStatus(code)) + else -> HttpError.MalformedResponse(DebugError("Unknown HTTP status code.")) + } + + public override fun close() { + listeners.clear() + coroutineScope.cancel() + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/DownloadCursorFacade.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/DownloadCursorFacade.kt new file mode 100644 index 0000000000..9096032cba --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/DownloadCursorFacade.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.downloads.android + +import android.app.DownloadManager +import android.database.Cursor + +internal class DownloadCursorFacade( + private val cursor: Cursor +) { + + val id: Long = cursor + .getColumnIndex(DownloadManager.COLUMN_ID) + .also { require(it != -1) } + .let { cursor.getLong(it) } + + val localUri: String? = cursor + .getColumnIndex(DownloadManager.COLUMN_LOCAL_URI) + .also { require(it != -1) } + .takeUnless { cursor.isNull(it) } + ?.let { cursor.getString(it) } + + val status: Int = cursor + .getColumnIndex(DownloadManager.COLUMN_STATUS) + .also { require(it != -1) } + .let { cursor.getInt(it) } + + val expected: Long? = cursor + .getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES) + .also { require(it != -1) } + .takeUnless { cursor.isNull(it) } + ?.let { cursor.getLong(it) } + ?.takeUnless { it == -1L } + + val downloadedSoFar: Long = cursor + .getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) + .also { require(it != -1) } + .let { cursor.getLong(it) } + + val mediaType: String? = cursor + .getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE) + .also { require(it != -1) } + .takeUnless { cursor.isNull(it) } + ?.let { cursor.getString(it) } + + val reason: Int? = cursor + .getColumnIndex(DownloadManager.COLUMN_REASON) + .also { require(it != -1) } + .takeIf { status == DownloadManager.STATUS_FAILED || status == DownloadManager.STATUS_PAUSED } + ?.let { cursor.getInt(it) } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt new file mode 100644 index 0000000000..a6b065e9a8 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.downloads.foreground + +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.readium.r2.shared.extensions.tryOrLog +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.file.FileSystemError +import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.http.HttpRequest +import org.readium.r2.shared.util.http.HttpResponse + +/** + * A [DownloadManager] implementation using a [HttpClient]. + * + * If the app is killed, downloads will stop and you won't be able to resume them later. + */ +public class ForegroundDownloadManager( + private val httpClient: HttpClient, + private val downloadsDirectory: File +) : DownloadManager { + + private val coroutineScope: CoroutineScope = + MainScope() + + private val jobs: MutableMap<DownloadManager.RequestId, Job> = + mutableMapOf() + + private val listeners: MutableMap<DownloadManager.RequestId, MutableList<DownloadManager.Listener>> = + mutableMapOf() + + public override fun submit( + request: DownloadManager.Request, + listener: DownloadManager.Listener + ): DownloadManager.RequestId { + val requestId = DownloadManager.RequestId(UUID.randomUUID().toString()) + register(requestId, listener) + jobs[requestId] = coroutineScope.launch { doRequest(request, requestId) } + return requestId + } + + private suspend fun doRequest(request: DownloadManager.Request, id: DownloadManager.RequestId) { + val destination: File + try { + destination = withContext(Dispatchers.IO) { + File.createTempFile(UUID.randomUUID().toString(), null, downloadsDirectory) + } + } catch (exception: IOException) { + val error = DownloadManager.DownloadError.FileSystem(FileSystemError.IO(exception)) + forEachListener(id) { + onDownloadFailed(id, error) + } + return + } + + httpClient + .download( + request = HttpRequest( + url = request.url, + headers = request.headers + ), + destination = destination, + onProgress = { downloaded, expected -> + forEachListener(id) { + onDownloadProgressed(id, downloaded = downloaded, expected = expected) + } + } + ) + .onSuccess { response -> + forEachListener(id) { + onDownloadCompleted( + id, + DownloadManager.Download( + file = destination, + mediaType = response.mediaType + ) + ) + } + } + .onFailure { error -> + forEachListener(id) { + onDownloadFailed(id, error) + } + } + + listeners.remove(id) + } + + private fun forEachListener( + id: DownloadManager.RequestId, + task: DownloadManager.Listener.() -> Unit + ) { + listeners[id].orEmpty().forEach { + tryOrLog { it.task() } + } + } + + public override fun cancel(requestId: DownloadManager.RequestId) { + jobs.remove(requestId)?.cancel() + forEachListener(requestId) { onDownloadCancelled(requestId) } + listeners.remove(requestId) + } + + public override fun register( + requestId: DownloadManager.RequestId, + listener: DownloadManager.Listener + ) { + listeners.getOrPut(requestId) { mutableListOf() }.add(listener) + } + + public override fun close() { + jobs.forEach { cancel(it.key) } + } + + private suspend fun HttpClient.download( + request: HttpRequest, + destination: File, + onProgress: (downloaded: Long, expected: Long?) -> Unit + ): Try<HttpResponse, DownloadManager.DownloadError> = + try { + stream(request) + .mapFailure { DownloadManager.DownloadError.Http(it) } + .flatMap { res -> + withContext(Dispatchers.IO) { + res.body.use { input -> + val expected = res.response.contentLength?.takeIf { it > 0 } + val freespace = destination.freeSpace.takeUnless { it == 0L } + + if (expected != null && freespace != null && destination.freeSpace < expected) { + return@withContext Try.failure( + DownloadManager.DownloadError.FileSystem( + FileSystemError.InsufficientSpace( + requiredSpace = expected, + freespace = freespace + ) + ) + ) + } + + FileOutputStream(destination).use { output -> + val buf = ByteArray(DEFAULT_BUFFER_SIZE) + var n: Int + var downloadedBytes = 0L + while (-1 != input.read(buf).also { n = it }) { + ensureActive() + downloadedBytes += n + output.write(buf, 0, n) + onProgress(downloadedBytes, expected) + } + } + } + + Try.success(res.response) + } + } + } catch (e: IOException) { + Try.failure(DownloadManager.DownloadError.Http(HttpError.IO(e))) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/file/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/DirectoryContainer.kt new file mode 100644 index 0000000000..98b877386b --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/DirectoryContainer.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.file + +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.readium.r2.shared.util.RelativeUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.toUrl + +/** + * A file system directory as a [Container]. + */ +public class DirectoryContainer( + private val root: File, + override val entries: Set<Url> +) : Container<Resource> { + + override fun get(url: Url): Resource? = url + .takeIf { it in entries } + ?.let { (it as? RelativeUrl)?.path } + ?.let { File(root, it) } + ?.let { FileResource(it) } + + override suspend fun close() {} + + public companion object { + + public suspend operator fun invoke(root: File): Try<DirectoryContainer, FileSystemError> { + val rootUrl = root.toUrl() + val entries = + try { + withContext(Dispatchers.IO) { + root.walk() + .filter { it.isFile } + .map { rootUrl.relativize(it.toUrl()) } + .toSet() + } + } catch (e: SecurityException) { + return Try.failure(FileSystemError.Forbidden(e)) + } + val container = DirectoryContainer(root, entries) + return Try.success(container) + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt new file mode 100644 index 0000000000..6a08da2509 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.file + +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.io.RandomAccessFile +import java.nio.channels.Channels +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.readium.r2.shared.extensions.* +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.isLazyInitialized +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.toUrl + +/** + * A [Resource] to access a [File]. + * + * @param file the file to read. + */ +public class FileResource( + private val file: File +) : Resource { + + private val randomAccessFile by lazy { + try { + Try.success(RandomAccessFile(file, "r")) + } catch (e: FileNotFoundException) { + Try.failure(e) + } + } + + private val properties = + Resource.Properties( + Resource.Properties.Builder() + .also { + it.filename = file.name + } + ) + + override val sourceUrl: AbsoluteUrl = file.toUrl() + + public override suspend fun properties(): Try<Resource.Properties, ReadError> { + return Try.success(properties) + } + + override suspend fun close() { + withContext(Dispatchers.IO) { + if (::randomAccessFile.isLazyInitialized) { + randomAccessFile.onSuccess { + tryOrLog { it.close() } + } + } + } + } + + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> = + withContext(Dispatchers.IO) { + Try.catching { + readSync(range) + } + } + + private fun readSync(range: LongRange?): ByteArray { + if (range == null) { + return file.readBytes() + } + + @Suppress("NAME_SHADOWING") + val range = range + .coerceFirstNonNegative() + .requireLengthFitInt() + + if (range.isEmpty()) { + return ByteArray(0) + } + + return randomAccessFile.getOrThrow().run { + channel.position(range.first) + + // The stream must not be closed here because it would close the underlying + // [FileChannel] too. Instead, [close] is responsible for that. + Channels.newInputStream(channel).run { + val length = range.last - range.first + 1 + read(length) + } + } + } + + override suspend fun length(): Try<Long, ReadError> = + metadataLength?.let { Try.success(it) } + ?: Try.failure( + ReadError.UnsupportedOperation( + DebugError("Length not available for file at ${file.path}.") + ) + ) + + private val metadataLength: Long? = + tryOrNull { + if (file.isFile) { + file.length() + } else { + null + } + } + + private inline fun <T> Try.Companion.catching(closure: () -> T): Try<T, ReadError> = + try { + success(closure()) + } catch (e: FileNotFoundException) { + failure(ReadError.Access(FileSystemError.FileNotFound(e))) + } catch (e: SecurityException) { + failure(ReadError.Access(FileSystemError.Forbidden(e))) + } catch (e: IOException) { + failure(ReadError.Access(FileSystemError.IO(e))) + } catch (e: Exception) { + failure(ReadError.Access(FileSystemError.IO(e))) + } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. + failure(ReadError.OutOfMemory(e)) + } + + override fun toString(): String = + "${javaClass.simpleName}(${file.path})" +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt new file mode 100644 index 0000000000..bbe39406d7 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.file + +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceFactory + +/** + * Creates [FileResource] instances granting access to `file://` URLs stored on the file system. + */ +public class FileResourceFactory : ResourceFactory { + + override suspend fun create( + url: AbsoluteUrl + ): Try<Resource, ResourceFactory.Error> { + val file = url.toFile() + ?: return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) + + val resource = FileResource(file) + + return Try.success(resource) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileSystemError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileSystemError.kt new file mode 100644 index 0000000000..be96d1ccb8 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileSystemError.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.file + +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.data.AccessError + +/** + * Errors wrapping file system exceptions. + */ +public sealed class FileSystemError( + override val message: String, + override val cause: Error? = null +) : AccessError { + + public class FileNotFound( + cause: Error? + ) : FileSystemError("File not found.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + public class Forbidden( + cause: Error? + ) : FileSystemError("You are not allowed to access this file.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + public class IO( + cause: Error? + ) : FileSystemError("An unexpected IO error occurred on the filesystem.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + public class InsufficientSpace( + public val requiredSpace: Long? = null, + public val freespace: Long? = null, + cause: Error? = null + ) : FileSystemError("There is not enough space to do the operation.", cause) +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt new file mode 100644 index 0000000000..92f89f4df9 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.format + +import org.readium.r2.shared.util.FileExtension +import org.readium.r2.shared.util.mediatype.MediaType + +public data class Format( + public val specification: FormatSpecification, + public val mediaType: MediaType, + public val fileExtension: FileExtension +) { + public fun conformsTo(specification: Specification): Boolean = + this.specification.conformsTo(specification) + + public fun conformsToAny(specifications: Collection<Specification>): Boolean = + this.specification.conformsToAny(specifications) + + public fun conformsToAll(specifications: Collection<Specification>): Boolean = + this.specification.conformsToAll(specifications) + + internal fun conformsTo(other: Format): Boolean = + specification.conformsTo(other.specification) +} + +@JvmInline +public value class FormatSpecification(public val specifications: Set<Specification>) { + + public constructor(vararg specifications: Specification) : this(specifications.toSet()) + + public operator fun plus(specification: Specification): FormatSpecification = + FormatSpecification(specifications + specification) + + public operator fun plus(specifications: Collection<Specification>): FormatSpecification = + FormatSpecification(this.specifications + specifications) + + public fun conformsTo(specification: Specification): Boolean = + specification in specifications + + public fun conformsToAny(vararg specifications: Specification): Boolean = + conformsToAny(*specifications) + + public fun conformsToAny(specifications: Collection<Specification>): Boolean = + specifications.any { spec -> this.specifications.contains(spec) } + + public fun conformsToAll(specifications: Collection<Specification>): Boolean = + this.specifications.containsAll(specifications) + + public fun conformsToAll(vararg specifications: Specification): Boolean = + conformsToAll(*specifications) + + internal fun conformsTo(other: FormatSpecification): Boolean = + specifications.containsAll(other.specifications) +} + +public interface Specification + +/* + * Archive specifications + */ +public object ZipSpecification : Specification +public object RarSpecification : Specification + +/* + * Syntax specifications + */ +public object JsonSpecification : Specification +public object XmlSpecification : Specification + +/* + * Publication manifest specifications + */ +public object W3cPubManifestSpecification : Specification +public object RwpmSpecification : Specification + +/* + * Technical document specifications + */ +public object ProblemDetailsSpecification : Specification +public object LcpLicenseSpecification : Specification + +/* + * Media format specifications + */ +public object PdfSpecification : Specification +public object HtmlSpecification : Specification + +/* + * Drm specifications + */ +public object LcpSpecification : Specification +public object AdeptSpecification : Specification + +/* + * Bitmap specifications + */ +public object AvifSpecification : Specification +public object BmpSpecification : Specification +public object GifSpecification : Specification +public object JpegSpecification : Specification +public object JxlSpecification : Specification +public object PngSpecification : Specification +public object TiffSpecification : Specification +public object WebpSpecification : Specification + +/* + * Audio specifications + */ +public object AacSpecification : Specification +public object AiffSpecification : Specification +public object FlacSpecification : Specification +public object Mp4Specification : Specification +public object Mp3Specification : Specification +public object OggSpecification : Specification +public object OpusSpecification : Specification +public object WavSpecification : Specification +public object WebmSpecification : Specification + +/* + * Publication package specifications + */ +public object EpubSpecification : Specification +public object RpfSpecification : Specification +public object LpfSpecification : Specification +public object InformalAudiobookSpecification : Specification +public object InformalComicSpecification : Specification + +/* + * Opds specifications + */ +public object Opds1CatalogSpecification : Specification +public object Opds1EntrySpecification : Specification +public object Opds2CatalogSpecification : Specification +public object Opds2PublicationSpecification : Specification +public object OpdsAuthenticationSpecification : Specification diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/Sniffers.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Sniffers.kt new file mode 100644 index 0000000000..9138a425da --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Sniffers.kt @@ -0,0 +1,1295 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.format + +import java.util.Locale +import org.json.JSONObject +import org.readium.r2.shared.extensions.tryOrNull +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.encryption.encryption +import org.readium.r2.shared.util.FileExtension +import org.readium.r2.shared.util.RelativeUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.data.decodeJson +import org.readium.r2.shared.util.data.decodeRwpm +import org.readium.r2.shared.util.data.decodeString +import org.readium.r2.shared.util.data.decodeXml +import org.readium.r2.shared.util.data.readDecodeOrElse +import org.readium.r2.shared.util.getOrDefault +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.mediatype.MediaType + +/** Sniffs an HTML or XHTML document. */ +public object HtmlSniffer : FormatSniffer { + override fun sniffHints( + format: Format, + hints: FormatHints + ): Format { + if (format.hasMoreThan(XmlSpecification)) { + return format + } + + if ( + hints.hasFileExtension("htm", "html") || + hints.hasMediaType("text/html") + ) { + return htmlFormat + } + + if ( + hints.hasFileExtension("xht", "xhtml") || + hints.hasMediaType("application/xhtml+xml") + ) { + return xhtmlFormat + } + + return format + } + + override suspend fun sniffBlob( + format: Format, + source: Readable + ): Try<Format, ReadError> { + if (format.hasMoreThan(XmlSpecification) || !source.canReadWholeBlob()) { + return Try.success(format) + } + + // decodeXml will fail if the HTML is not a proper XML document, hence the doctype check. + source.readDecodeOrElse( + decode = { it.decodeXml() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + ) + ?.takeIf { it.name.lowercase(Locale.ROOT) == "html" } + ?.let { + return Try.success( + if (it.namespace.lowercase(Locale.ROOT).contains("xhtml")) { + xhtmlFormat + } else { + htmlFormat + } + ) + } + + source.readDecodeOrElse( + decode = { it.decodeString() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + ) + ?.takeIf { it.trimStart().take(15).lowercase() == "<!doctype html>" } + ?.let { return Try.success(htmlFormat) } + + return Try.success(format) + } + + private val htmlFormat = Format( + specification = FormatSpecification(HtmlSpecification), + fileExtension = FileExtension("html"), + mediaType = MediaType.HTML + ) + + private val xhtmlFormat = Format( + specification = FormatSpecification(XmlSpecification, HtmlSpecification), + fileExtension = FileExtension("xhtml"), + mediaType = MediaType.XHTML + ) +} + +/** Sniffs an OPDS1 document. */ +public object Opds1Sniffer : FormatSniffer { + + override fun sniffHints( + format: Format, + hints: FormatHints + ): Format { + if (format.hasMoreThan(XmlSpecification)) { + return format + } + + // OPDS 1 + if (hints.hasMediaType("application/atom+xml;type=entry;profile=opds-catalog")) { + return opds1EntryFormat + } + if (hints.hasMediaType("application/atom+xml;profile=opds-catalog;kind=navigation")) { + return opds1NavigationFeedFormat + } + if (hints.hasMediaType("application/atom+xml;profile=opds-catalog;kind=acquisition")) { + return opds1AcquisitionFeedFormat + } + if (hints.hasMediaType("application/atom+xml;profile=opds-catalog")) { + return opds1CatalogFormat + } + + return format + } + + override suspend fun sniffBlob( + format: Format, + source: Readable + ): Try<Format, ReadError> { + if (format.hasMoreThan(XmlSpecification) || !source.canReadWholeBlob()) { + return Try.success(format) + } + + // OPDS 1 + source.readDecodeOrElse( + decode = { it.decodeXml() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + )?.takeIf { it.namespace == "http://www.w3.org/2005/Atom" } + ?.let { xml -> + if (xml.name == "feed") { + return Try.success(opds1CatalogFormat) + } else if (xml.name == "entry") { + return Try.success(opds1EntryFormat) + } + } + + return Try.success(format) + } + + private val opds1CatalogFormat = Format( + specification = FormatSpecification(XmlSpecification, Opds1CatalogSpecification), + mediaType = MediaType.OPDS1, + fileExtension = FileExtension("xml") + ) + + private val opds1NavigationFeedFormat = Format( + specification = FormatSpecification(XmlSpecification, Opds1CatalogSpecification), + mediaType = MediaType.OPDS1_NAVIGATION_FEED, + fileExtension = FileExtension("xml") + ) + + private val opds1AcquisitionFeedFormat = Format( + specification = FormatSpecification(XmlSpecification, Opds1CatalogSpecification), + mediaType = MediaType.OPDS1_ACQUISITION_FEED, + fileExtension = FileExtension("xml") + ) + + private val opds1EntryFormat = Format( + specification = FormatSpecification(XmlSpecification, Opds1EntrySpecification), + mediaType = MediaType.OPDS1_ENTRY, + fileExtension = FileExtension("xml") + ) +} + +/** + * Sniffs an OPDS 2 document. + */ +public object Opds2Sniffer : FormatSniffer { + + override fun sniffHints(format: Format, hints: FormatHints): Format { + if (format.hasMoreThan(JsonSpecification)) { + return format + } + + // OPDS 2 + if (hints.hasMediaType("application/opds+json")) { + return opds2CatalogFormat + } + if (hints.hasMediaType("application/opds-publication+json")) { + return opds2PublicationFormat + } + + // OPDS Authentication Document. + if ( + hints.hasMediaType("application/opds-authentication+json") || + hints.hasMediaType("application/vnd.opds.authentication.v1.0+json") + ) { + return opdsAuthenticationFormat + } + + return format + } + + override suspend fun sniffBlob( + format: Format, + source: Readable + ): Try<Format, ReadError> { + if (format.hasMoreThan(JsonSpecification)) { + return Try.success(format) + } + + // OPDS 2 + source.readDecodeOrElse( + decode = { it.decodeRwpm() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + ) + ?.let { rwpm -> + if (rwpm.linkWithRel("self")?.mediaType?.matches("application/opds+json") == true + ) { + return Try.success(opds2CatalogFormat) + } + + /** + * Finds the first [Link] having a relation matching the given [predicate]. + */ + fun List<Link>.firstWithRelMatching(predicate: (String) -> Boolean): Link? = + firstOrNull { it.rels.any(predicate) } + + if (rwpm.links.firstWithRelMatching { + it.startsWith( + "http://opds-spec.org/acquisition" + ) + } != null + ) { + return Try.success(opds2PublicationFormat) + } + } + + // OPDS Authentication Document. + source.containsJsonKeys("id", "title", "authentication") + .getOrElse { return Try.failure(it) } + .takeIf { it } + ?.let { return Try.success(opdsAuthenticationFormat) } + + return Try.success(format) + } + + private val opdsAuthenticationFormat = Format( + specification = FormatSpecification(JsonSpecification, OpdsAuthenticationSpecification), + mediaType = MediaType.OPDS_AUTHENTICATION, + fileExtension = FileExtension("json") + ) + + private val opds2CatalogFormat = Format( + specification = FormatSpecification(JsonSpecification, Opds2CatalogSpecification), + mediaType = MediaType.OPDS2, + fileExtension = FileExtension("json") + ) + + private val opds2PublicationFormat = Format( + specification = FormatSpecification(JsonSpecification, Opds2PublicationSpecification), + mediaType = MediaType.OPDS2_PUBLICATION, + fileExtension = FileExtension("json") + ) +} + +/** Sniffs an LCP License Document. */ +public object LcpLicenseSniffer : FormatSniffer { + override fun sniffHints( + format: Format, + hints: FormatHints + ): Format { + if (format.hasMoreThan(JsonSpecification)) { + return format + } + + if ( + hints.hasFileExtension("lcpl") || + hints.hasMediaType("application/vnd.readium.lcp.license.v1.0+json") + ) { + return lcplFormat + } + + return format + } + + override suspend fun sniffBlob( + format: Format, + source: Readable + ): Try<Format, ReadError> { + if ( + format.hasMoreThan(JsonSpecification) || + !source.canReadWholeBlob() + ) { + return Try.success(format) + } + + source.containsJsonKeys("id", "issued", "provider", "encryption") + .getOrElse { return Try.failure(it) } + .takeIf { it } + ?.let { return Try.success(lcplFormat) } + + return Try.success(format) + } + + private val lcplFormat = Format( + specification = FormatSpecification(JsonSpecification, LcpLicenseSpecification), + mediaType = MediaType.LCP_LICENSE_DOCUMENT, + fileExtension = FileExtension("lcpl") + ) +} + +/** Sniffs a bitmap image. */ +public object BitmapSniffer : FormatSniffer { + + override fun sniffHints( + format: Format, + hints: FormatHints + ): Format { + if ( + hints.hasFileExtension("avif") || + hints.hasMediaType("image/avif") + ) { + return Format( + specification = FormatSpecification(AvifSpecification), + mediaType = MediaType.AVIF, + fileExtension = FileExtension("avif") + ) + } + if ( + hints.hasFileExtension("bmp", "dib") || + hints.hasMediaType("image/bmp", "image/x-bmp") + ) { + return Format( + specification = FormatSpecification(BmpSpecification), + mediaType = MediaType.BMP, + fileExtension = FileExtension("bmp") + ) + } + if ( + hints.hasFileExtension("gif") || + hints.hasMediaType("image/gif") + ) { + return Format( + specification = FormatSpecification(GifSpecification), + mediaType = MediaType.GIF, + fileExtension = FileExtension("gif") + ) + } + if ( + hints.hasFileExtension("jpg", "jpeg", "jpe", "jif", "jfif", "jfi") || + hints.hasMediaType("image/jpeg") + ) { + return Format( + specification = FormatSpecification(JpegSpecification), + mediaType = MediaType.JPEG, + fileExtension = FileExtension("jpg") + ) + } + if ( + hints.hasFileExtension("jxl") || + hints.hasMediaType("image/jxl") + ) { + return Format( + specification = FormatSpecification(JxlSpecification), + mediaType = MediaType.JXL, + fileExtension = FileExtension("jxl") + ) + } + if ( + hints.hasFileExtension("png") || + hints.hasMediaType("image/png") + ) { + return Format( + specification = FormatSpecification(PngSpecification), + mediaType = MediaType.PNG, + fileExtension = FileExtension("png") + ) + } + if ( + hints.hasFileExtension("tiff", "tif") || + hints.hasMediaType("image/tiff", "image/tiff-fx") + ) { + return Format( + specification = FormatSpecification(TiffSpecification), + mediaType = MediaType.TIFF, + fileExtension = FileExtension("tiff") + ) + } + if ( + hints.hasFileExtension("webp") || + hints.hasMediaType("image/webp") + ) { + return Format( + specification = FormatSpecification(WebpSpecification), + mediaType = MediaType.WEBP, + fileExtension = FileExtension("webp") + ) + } + + return format + } +} + +/** Sniffs audio files. */ +public object AudioSniffer : FormatSniffer { + override fun sniffHints(format: Format, hints: FormatHints): Format { + if ( + hints.hasFileExtension("aac") + ) { + return Format( + specification = FormatSpecification(AacSpecification), + mediaType = MediaType.AAC, + fileExtension = FileExtension("aac") + ) + } + + if ( + hints.hasFileExtension("aiff") + ) { + return Format( + specification = FormatSpecification(AiffSpecification), + mediaType = MediaType.AIFF, + fileExtension = FileExtension("aiff") + ) + } + + if ( + hints.hasFileExtension("flac") + ) { + return Format( + specification = FormatSpecification(FlacSpecification), + mediaType = MediaType.FLAC, + fileExtension = FileExtension("flac") + ) + } + + if ( + hints.hasFileExtension("m4a", "m4b", "alac") + ) { + return Format( + specification = FormatSpecification(Mp4Specification), + mediaType = MediaType.MP4, + fileExtension = FileExtension("m4a") + ) + } + + if ( + hints.hasFileExtension("mp3") + ) { + return Format( + specification = FormatSpecification(Mp3Specification), + mediaType = MediaType.MP3, + fileExtension = FileExtension("mp3") + ) + } + + if ( + hints.hasFileExtension("ogg", "oga") + ) { + return Format( + specification = FormatSpecification(OggSpecification), + mediaType = MediaType.OGG, + fileExtension = FileExtension("oga") + ) + } + + if ( + hints.hasFileExtension("opus") + ) { + return Format( + specification = FormatSpecification(OpusSpecification), + mediaType = MediaType.OPUS, + fileExtension = FileExtension("opus") + ) + } + + if ( + hints.hasFileExtension("wav") + ) { + return Format( + specification = FormatSpecification(WavSpecification), + mediaType = MediaType.WAV, + fileExtension = FileExtension("wav") + ) + } + + if ( + hints.hasFileExtension("webm") + ) { + return Format( + specification = FormatSpecification(WebmSpecification), + mediaType = MediaType.WEBM_AUDIO, + fileExtension = FileExtension("webm") + ) + } + + return format + } +} + +/** Sniffs a Readium Web Manifest. */ +public object RwpmSniffer : FormatSniffer { + override fun sniffHints( + format: Format, + hints: FormatHints + ): Format { + if (format.hasMoreThan(JsonSpecification)) { + return format + } + + if (hints.hasMediaType("application/audiobook+json")) { + return rwpmAudioFormat + } + + if (hints.hasMediaType("application/divina+json")) { + return rwpmDivinaFormat + } + + if (hints.hasMediaType("application/webpub+json")) { + return rwpmFormat + } + + return format + } + + public override suspend fun sniffBlob( + format: Format, + source: Readable + ): Try<Format, ReadError> { + if ( + format.hasMoreThan(JsonSpecification) || + !source.canReadWholeBlob() + ) { + return Try.success(format) + } + + val manifest: Manifest = + source.readDecodeOrElse( + decode = { it.decodeRwpm() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + ) ?: return Try.success(format) + + if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { + return Try.success(rwpmAudioFormat) + } + + if (manifest.conformsTo(Publication.Profile.DIVINA)) { + return Try.success(rwpmDivinaFormat) + } + if (manifest.linkWithRel("self")?.mediaType?.matches("application/webpub+json") == true) { + return Try.success(rwpmFormat) + } + + return Try.success(format) + } + + private val rwpmFormat = Format( + specification = FormatSpecification(JsonSpecification, RwpmSpecification), + mediaType = MediaType.READIUM_WEBPUB_MANIFEST, + fileExtension = FileExtension("json") + ) + + private val rwpmAudioFormat = Format( + specification = FormatSpecification(JsonSpecification, RwpmSpecification), + mediaType = MediaType.READIUM_AUDIOBOOK_MANIFEST, + fileExtension = FileExtension("json") + ) + + private val rwpmDivinaFormat = Format( + specification = FormatSpecification(JsonSpecification, RwpmSpecification), + mediaType = MediaType.DIVINA_MANIFEST, + fileExtension = FileExtension("json") + ) +} + +/** Sniffs a Readium Web Publication, protected or not by LCP. */ +public object RpfSniffer : FormatSniffer { + + override fun sniffHints( + format: Format, + hints: FormatHints + ): Format { + if (format.hasMoreThan(ZipSpecification, RpfSpecification, LcpSpecification)) { + return format + } + + if ( + hints.hasFileExtension("audiobook") || + hints.hasMediaType("application/audiobook+zip") + ) { + return rpfAudioFormat + } + + if ( + hints.hasFileExtension("divina") || + hints.hasMediaType("application/divina+zip") + ) { + return rpfDivinaFormat + } + + if ( + hints.hasFileExtension("webpub") || + hints.hasMediaType("application/webpub+zip") + ) { + return rpfFormat + } + + if ( + hints.hasFileExtension("lcpa") || + hints.hasMediaType("application/audiobook+lcp") + ) { + return lcpaFormat + } + if ( + hints.hasFileExtension("lcpdf") || + hints.hasMediaType("application/pdf+lcp") + ) { + return lcpdfFormat + } + + return format + } + + override suspend fun sniffContainer( + format: Format, + container: Container<Readable> + ): Try<Format, ReadError> { + if ( + format.hasMoreThan(ZipSpecification, RpfSpecification, LcpSpecification) + ) { + return Try.success(format) + } + + // Reads a RWPM from a manifest.json archive entry. + val manifest: Manifest = + container[RelativeUrl("manifest.json")!!] + ?.read() + ?.getOrElse { return Try.failure(it) } + ?.let { tryOrNull { Manifest.fromJSON(JSONObject(String(it))) } } + ?: return Try.success(format) + + val isLcpProtected = RelativeUrl("license.lcpl")!! in container || + hasLcpSchemeInManifest(manifest) + + val newFormat = when { + manifest.conformsTo(Publication.Profile.AUDIOBOOK) -> { + if (isLcpProtected) { + lcpaFormat + } else { + rpfAudioFormat + } + } + manifest.conformsTo(Publication.Profile.DIVINA) -> { + if (isLcpProtected) { + rpfDivinaFormat.addSpecifications(LcpSpecification) + } else { + rpfDivinaFormat + } + } + manifest.conformsTo(Publication.Profile.PDF) -> { + if (isLcpProtected) { + lcpdfFormat + } else { + rpfFormat + } + } + else -> + if (isLcpProtected) { + rpfFormat.addSpecifications(LcpSpecification) + } else { + rpfFormat + } + } + + return Try.success(newFormat) + } + + private fun hasLcpSchemeInManifest(manifest: Manifest): Boolean = manifest + .readingOrder + .any { it.properties.encryption?.scheme == "http://readium.org/2014/01/lcp" } + + private val rpfFormat = Format( + specification = FormatSpecification(ZipSpecification, RpfSpecification), + mediaType = MediaType.READIUM_WEBPUB, + fileExtension = FileExtension("webpub") + ) + + private val rpfAudioFormat = Format( + specification = FormatSpecification(ZipSpecification, RpfSpecification), + mediaType = MediaType.READIUM_AUDIOBOOK, + fileExtension = FileExtension("audiobook") + ) + + private val rpfDivinaFormat = Format( + specification = FormatSpecification(ZipSpecification, RpfSpecification), + mediaType = MediaType.DIVINA, + fileExtension = FileExtension("divina") + ) + + private val lcpaFormat = Format( + specification = FormatSpecification(ZipSpecification, RpfSpecification, LcpSpecification), + mediaType = MediaType.LCP_PROTECTED_AUDIOBOOK, + fileExtension = FileExtension("lcpa") + ) + + private val lcpdfFormat = Format( + specification = FormatSpecification(ZipSpecification, RpfSpecification, LcpSpecification), + mediaType = MediaType.LCP_PROTECTED_PDF, + fileExtension = FileExtension("lcpdf") + ) +} + +/** Sniffs a W3C Web Publication Manifest. */ +public object W3cWpubSniffer : FormatSniffer { + + override suspend fun sniffBlob( + format: Format, + source: Readable + ): Try<Format, ReadError> { + if (format.hasMoreThan(JsonSpecification) || !source.canReadWholeBlob()) { + return Try.success(format) + } + + // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. + val string = source.readDecodeOrElse( + decode = { it.decodeString() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { "" } + ) + if ( + string.contains("@context") && + string.contains("https://www.w3.org/ns/wp-context") + ) { + return Try.success( + Format( + specification = FormatSpecification( + JsonSpecification, + W3cPubManifestSpecification + ), + mediaType = MediaType.W3C_WPUB_MANIFEST, + fileExtension = FileExtension("json") + ) + ) + } + + return Try.success(format) + } +} + +/** + * Sniffs an EPUB publication. + * + * Reference: https://www.w3.org/publishing/epub3/epub-ocf.html#sec-zip-container-mime + */ +public object EpubSniffer : FormatSniffer { + override fun sniffHints( + format: Format, + hints: FormatHints + ): Format { + if (format.hasMoreThan(ZipSpecification)) { + return format + } + + if ( + hints.hasFileExtension("epub") || + hints.hasMediaType("application/epub+zip") + ) { + return epubFormatSpecification + } + + return format + } + + override suspend fun sniffContainer( + format: Format, + container: Container<Readable> + ): Try<Format, ReadError> { + if (format.hasMoreThan(ZipSpecification)) { + return Try.success(format) + } + + val mimetype = container[RelativeUrl("mimetype")!!] + ?.readDecodeOrElse( + decode = { it.decodeString() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + )?.trim() + + if (mimetype == "application/epub+zip") { + return Try.success(epubFormatSpecification) + } + + return Try.success(format) + } + + private val epubFormatSpecification = Format( + specification = FormatSpecification(ZipSpecification, EpubSpecification), + mediaType = MediaType.EPUB, + fileExtension = FileExtension("epub") + ) +} + +/** + * Sniffs a Lightweight Packaging Format (LPF). + * + * References: + * - https://www.w3.org/TR/lpf/ + * - https://www.w3.org/TR/pub-manifest/ + */ +public object LpfSniffer : FormatSniffer { + override fun sniffHints( + format: Format, + hints: FormatHints + ): Format { + if (format.hasMoreThan(ZipSpecification)) { + return format + } + + if ( + hints.hasFileExtension("lpf") || + hints.hasMediaType("application/lpf+zip") + ) { + return lpfFormat + } + + return format + } + + override suspend fun sniffContainer( + format: Format, + container: Container<Readable> + ): Try<Format, ReadError> { + if (format.hasMoreThan(ZipSpecification)) { + return Try.success(format) + } + + if (RelativeUrl("index.html")!! in container) { + return Try.success(lpfFormat) + } + + // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. + container[RelativeUrl("publication.json")!!] + ?.read() + ?.getOrElse { return Try.failure(it) } + ?.let { tryOrNull { String(it) } } + ?.let { manifest -> + if ( + manifest.contains("@context") && + manifest.contains("https://www.w3.org/ns/pub-context") + ) { + return Try.success(lpfFormat) + } + } + + return Try.success(format) + } + + private val lpfFormat = Format( + specification = FormatSpecification(ZipSpecification, LpfSpecification), + mediaType = MediaType.LPF, + fileExtension = FileExtension("lpf") + ) +} + +/** + * Sniffs a RAR archive. + * + * At the moment, only hints are supported. + */ +public object RarSniffer : FormatSniffer { + + override fun sniffHints( + format: Format, + hints: FormatHints + ): Format { + if ( + hints.hasFileExtension("rar") || + hints.hasMediaType("application/vnd.rar") || + hints.hasMediaType("application/x-rar") || + hints.hasMediaType("application/x-rar-compressed") + ) { + return Format( + specification = FormatSpecification(RarSpecification), + mediaType = MediaType.RAR, + fileExtension = FileExtension("rar") + ) + } + + return format + } +} + +/** + * Sniffs a ZIP archive. + */ +public object ZipSniffer : FormatSniffer { + + override fun sniffHints( + format: Format, + hints: FormatHints + ): Format { + if (hints.hasMediaType("application/zip") || + hints.hasFileExtension("zip") + ) { + return Format( + specification = FormatSpecification(ZipSpecification), + mediaType = MediaType.ZIP, + fileExtension = FileExtension("zip") + ) + } + + return format + } +} + +/** + * Sniffs a simple Archive-based publication format, like Comic Book Archive or Zipped Audio Book. + * + * Reference: https://wiki.mobileread.com/wiki/CBR_and_CBZ + */ +public object ArchiveSniffer : FormatSniffer { + + /** + * Authorized extensions for resources in a CBZ archive. + * Reference: https://wiki.mobileread.com/wiki/CBR_and_CBZ + */ + private val cbzExtensions = listOf( + // bitmap + "bmp", "dib", "gif", "jif", "jfi", "jfif", "jpg", "jpeg", "png", "tif", "tiff", "webp", + // metadata + "acbf", "xml" + ) + + /** + * Authorized extensions for resources in a ZAB archive (Zipped Audio Book). + */ + private val zabExtensions = listOf( + // audio + "aac", + "aiff", + "alac", + "flac", + "m4a", + "m4b", + "mp3", + "ogg", + "oga", + "mogg", + "opus", + "wav", + "webm", + // playlist + "asx", + "bio", + "m3u", + "m3u8", + "pla", + "pls", + "smil", + "vlc", + "wpl", + "xspf", + "zpl" + ) + + override fun sniffHints( + format: Format, + hints: FormatHints + ): Format { + if (format.hasMoreThan(ZipSpecification, RarSpecification)) { + return format + } + + if ( + hints.hasFileExtension("cbz") || + hints.hasMediaType( + "application/vnd.comicbook+zip", + "application/x-cbz" + ) + ) { + return Format( + specification = FormatSpecification(ZipSpecification, InformalComicSpecification), + mediaType = MediaType.CBZ, + fileExtension = FileExtension("cbz") + ) + } + + if ( + hints.hasFileExtension("cbr") || + hints.hasMediaType("application/vnd.comicbook-rar") || + hints.hasMediaType("application/x-cbr") + ) { + return Format( + specification = FormatSpecification(RarSpecification, InformalComicSpecification), + mediaType = MediaType.CBR, + fileExtension = FileExtension("cbr") + ) + } + + if (hints.hasFileExtension("zab")) { + return Format( + specification = FormatSpecification( + ZipSpecification, + InformalAudiobookSpecification + ), + mediaType = MediaType.ZAB, + fileExtension = FileExtension("zab") + ) + } + + return format + } + + override suspend fun sniffContainer( + format: Format, + container: Container<Readable> + ): Try<Format, ReadError> { + if (format.hasMoreThan(ZipSpecification, RarSpecification)) { + return Try.success(format) + } + + fun isIgnored(url: Url): Boolean = + url.filename?.startsWith(".") == true || url.filename == "Thumbs.db" + + fun archiveContainsOnlyExtensions(fileExtensions: List<String>): Boolean = + container.all { url -> + isIgnored(url) || url.extension?.value?.let { + fileExtensions.contains( + it.lowercase(Locale.ROOT) + ) + } == true + } + + if (container.entries.isEmpty()) { + return Try.success(format) + } + + if (archiveContainsOnlyExtensions(cbzExtensions)) { + val mediaType = + if (format.conformsTo(RarSpecification)) { + MediaType.CBR + } else { + MediaType.CBZ + } + + val extension = + if (format.conformsTo(RarSpecification)) { + FileExtension("cbr") + } else { + FileExtension("cbz") + } + + return Try.success( + Format( + specification = format.specification + InformalComicSpecification, + mediaType = mediaType, + fileExtension = extension + ) + ) + } + + if (archiveContainsOnlyExtensions(zabExtensions)) { + val mediaType = + if (format.conformsTo(ZipSpecification)) { + MediaType.ZAB + } else { + format.mediaType + } + + val extension = + if (format.conformsTo(ZipSpecification)) { + FileExtension("zab") + } else { + format.fileExtension + } + + return Try.success( + Format( + specification = format.specification + InformalAudiobookSpecification, + mediaType = mediaType, + fileExtension = extension + ) + ) + } + + return Try.success(format) + } +} + +/** + * Sniffs a PDF document. + * + * Reference: https://www.loc.gov/preservation/digital/formats/fdd/fdd000123.shtml + */ +public object PdfSniffer : FormatSniffer { + override fun sniffHints( + format: Format, + hints: FormatHints + ): Format { + if (format.hasAnySpecification()) { + return format + } + + if ( + hints.hasFileExtension("pdf") || + hints.hasMediaType("application/pdf") + ) { + return pdfFormat + } + + return format + } + + override suspend fun sniffBlob( + format: Format, + source: Readable + ): Try<Format, ReadError> { + if (format.hasAnySpecification()) { + return Try.success(format) + } + + source.read(0L until 5L) + .getOrElse { return Try.failure(it) } + .let { tryOrNull { it.toString(Charsets.UTF_8) } } + .takeIf { it == "%PDF-" } + ?.let { return Try.success(pdfFormat) } + + return Try.success(format) + } + + private val pdfFormat = Format( + specification = FormatSpecification(PdfSpecification), + mediaType = MediaType.PDF, + fileExtension = FileExtension("pdf") + ) +} + +/** Sniffs a JSON document. */ +public object JsonSniffer : FormatSniffer { + override fun sniffHints( + format: Format, + hints: FormatHints + ): Format { + if (format.hasAnySpecification()) { + return format + } + + if (hints.hasFileExtension("json") || + hints.hasMediaType("application/json") + ) { + return Format( + specification = FormatSpecification(JsonSpecification), + mediaType = MediaType.JSON, + fileExtension = FileExtension("json") + ) + } + + if (hints.hasMediaType("application/problem+json")) { + return Format( + specification = FormatSpecification(JsonSpecification, ProblemDetailsSpecification), + mediaType = MediaType.JSON_PROBLEM_DETAILS, + fileExtension = FileExtension("json") + ) + } + + return format + } + + override suspend fun sniffBlob( + format: Format, + source: Readable + ): Try<Format, ReadError> { + if (format.hasMoreThan() || !source.canReadWholeBlob()) { + return Try.success(format) + } + + source.readDecodeOrElse( + decode = { it.decodeJson() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + )?.let { + return Try.success( + Format( + specification = FormatSpecification(JsonSpecification), + mediaType = MediaType.JSON, + fileExtension = FileExtension("json") + ) + ) + } + + return Try.success(format) + } +} + +/** + * Sniffs LCP and Adept protection on EPUBs. + */ +public object EpubDrmSniffer : FormatSniffer { + + override suspend fun sniffContainer( + format: Format, + container: Container<Readable> + ): Try<Format, ReadError> { + if ( + !format.conformsTo(EpubSpecification) || + format.conformsTo(AdeptSpecification) || + format.conformsTo(LcpSpecification) + ) { + return Try.success(format) + } + + if (RelativeUrl("META-INF/license.lcpl")!! in container) { + return Try.success(format.addSpecifications(LcpSpecification)) + } + + val encryptionDocument = container[Url("META-INF/encryption.xml")!!] + ?.readDecodeOrElse( + decode = { it.decodeXml() }, + recover = { return Try.failure(it) } + ) + + encryptionDocument + ?.get("EncryptedData", EpubEncryption.ENC) + ?.flatMap { it.get("KeyInfo", EpubEncryption.SIG) } + ?.flatMap { it.get("RetrievalMethod", EpubEncryption.SIG) } + ?.any { it.getAttr("URI") == "license.lcpl#/encryption/content_key" } + ?.takeIf { it } + ?.let { return Try.success(format.addSpecifications(LcpSpecification)) } + + encryptionDocument + ?.get("EncryptedData", EpubEncryption.ENC) + ?.flatMap { it.get("KeyInfo", EpubEncryption.SIG) } + ?.flatMap { it.get("resource", "http://ns.adobe.com/adept") } + ?.takeIf { it.isNotEmpty() } + ?.let { return Try.success(format.addSpecifications(AdeptSpecification)) } + + container[Url("META-INF/rights.xml")!!] + ?.readDecodeOrElse( + decode = { it.decodeXml() }, + recover = { null } + ) + ?.takeIf { it.namespace == "http://ns.adobe.com/adept" } + ?.let { return Try.success(format.addSpecifications(AdeptSpecification)) } + + return Try.success(format) + } +} + +private suspend fun Readable.canReadWholeBlob() = + length().getOrDefault(0) < 5 * 1000 * 1000 + +/** + * Returns whether the content is a JSON object containing all of the given root keys. + */ +@Suppress("SameParameterValue") +private suspend fun Readable.containsJsonKeys( + vararg keys: String +): Try<Boolean, ReadError> { + val json = readDecodeOrElse( + decode = { it.decodeJson() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { return Try.success(false) } + ) + return Try.success(json.keys().asSequence().toSet().containsAll(keys.toList())) +} + +private fun Format.addSpecifications( + vararg specifications: Specification +): Format = + copy(specification = specification + specifications.toSet()) + +private fun Format.hasAnySpecification() = + specification.specifications.isNotEmpty() + +private fun Format.hasMoreThan(vararg specifications: Specification) = + !specifications.toSet().containsAll(specification.specifications) + +private object EpubEncryption { + const val ENC = "http://www.w3.org/2001/04/xmlenc#" + const val SIG = "http://www.w3.org/2000/09/xmldsig#" +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/Sniffing.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Sniffing.kt new file mode 100644 index 0000000000..1b3fe24669 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Sniffing.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.format + +import java.nio.charset.Charset +import org.readium.r2.shared.util.FileExtension +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.mediatype.MediaType + +/** + * Bundle of media type and file extension hints for the [FormatHintsSniffer]. + */ +public data class FormatHints( + val mediaTypes: List<MediaType> = emptyList(), + val fileExtensions: List<FileExtension> = emptyList() +) { + public companion object { + public operator fun invoke( + mediaType: MediaType? = null, + fileExtension: FileExtension? = null + ): FormatHints = + FormatHints( + mediaTypes = listOfNotNull(mediaType), + fileExtensions = listOfNotNull(fileExtension) + ) + + public operator fun invoke( + mediaTypes: List<String> = emptyList(), + fileExtensions: List<String> = emptyList() + ): FormatHints = + FormatHints( + mediaTypes = mediaTypes.mapNotNull { MediaType(it) }, + fileExtensions = fileExtensions.map { FileExtension(it) } + ) + } + + public operator fun plus(other: FormatHints): FormatHints = + FormatHints( + mediaTypes = mediaTypes + other.mediaTypes, + fileExtensions = fileExtensions + other.fileExtensions + ) + + /** + * Returns a new [FormatHints] after appending the given [fileExtension] hint. + */ + public fun addFileExtension(fileExtension: String?): FormatHints { + fileExtension ?: return this + return copy(fileExtensions = fileExtensions + FileExtension(fileExtension)) + } + + /** Finds the first [Charset] declared in the media types' `charset` parameter. */ + public val charset: Charset? get() = + mediaTypes.firstNotNullOfOrNull { it.charset } + + /** Returns whether this context has any of the given file extensions, ignoring case. */ + public fun hasFileExtension(vararg fileExtensions: String): Boolean { + val fileExtensionsHints = this.fileExtensions.map { it.value.lowercase() } + for (fileExtension in fileExtensions.map { it.lowercase() }) { + if (fileExtensionsHints.contains(fileExtension)) { + return true + } + } + return false + } + + /** + * Returns whether this context has any of the given media type, ignoring case and extra + * parameters. + * + * Implementation note: Use [MediaType] to handle the comparison to avoid edge cases. + */ + public fun hasMediaType(vararg mediaTypes: String): Boolean { + @Suppress("NAME_SHADOWING") + val mediaTypes = mediaTypes.mapNotNull { MediaType(it) } + for (mediaType in mediaTypes) { + if (this.mediaTypes.any { mediaType.contains(it) }) { + return true + } + } + return false + } +} + +/** + * Tries to refine a [Format] from media type and file extension hints. + */ +public interface FormatHintsSniffer { + + public fun sniffHints( + format: Format, + hints: FormatHints + ): Format +} + +/** + * Tries to refine a [Format] by sniffing a [Readable] blob. + */ +public interface BlobSniffer { + + public suspend fun sniffBlob( + format: Format, + source: Readable + ): Try<Format, ReadError> +} + +/** + * Tries to refine a [Format] by sniffing a [Container]. + */ +public interface ContainerSniffer { + + public suspend fun sniffContainer( + format: Format, + container: Container<Readable> + ): Try<Format, ReadError> +} + +/** + * Tries to refine a [Format] by sniffing format hints or content. + */ +public interface FormatSniffer : + FormatHintsSniffer, + BlobSniffer, + ContainerSniffer { + + public override fun sniffHints( + format: Format, + hints: FormatHints + ): Format = + format + + public override suspend fun sniffBlob( + format: Format, + source: Readable + ): Try<Format, ReadError> = + Try.success(format) + + public override suspend fun sniffContainer( + format: Format, + container: Container<Readable> + ): Try<Format, ReadError> = + Try.success(format) +} + +public class CompositeFormatSniffer( + private val sniffers: List<FormatSniffer> +) : FormatSniffer { + + public constructor(vararg sniffers: FormatSniffer) : this(sniffers.toList()) + + override fun sniffHints(format: Format, hints: FormatHints): Format = + sniffers.fold(format) { acc, sniffer -> + sniffer.sniffHints(acc, hints) + } + + override suspend fun sniffBlob(format: Format, source: Readable): Try<Format, ReadError> = + sniffers.fold(Try.success(format)) { acc: Try<Format, ReadError>, sniffer -> + when (acc) { + is Try.Failure -> acc + is Try.Success -> sniffer.sniffBlob(acc.value, source) + } + } + + override suspend fun sniffContainer( + format: Format, + container: Container<Readable> + ): Try<Format, ReadError> = + sniffers.fold(Try.success(format)) { acc: Try<Format, ReadError>, sniffer -> + when (acc) { + is Try.Failure -> acc + is Try.Success -> sniffer.sniffContainer(acc.value, container) + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index 639e9cea20..f655062d59 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -9,16 +9,28 @@ package org.readium.r2.shared.util.http import android.os.Bundle import java.io.ByteArrayInputStream import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import java.net.ConnectException import java.net.HttpURLConnection +import java.net.NoRouteToHostException +import java.net.SocketTimeoutException import java.net.URL +import java.net.UnknownHostException +import javax.net.ssl.SSLHandshakeException import kotlin.time.Duration import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.readium.r2.shared.extensions.joinValues +import org.readium.r2.shared.extensions.lowerCaseKeys +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.http.HttpRequest.Method import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.sniffMediaType +import org.readium.r2.shared.util.toDebugDescription import org.readium.r2.shared.util.tryRecover import timber.log.Timber @@ -26,30 +38,47 @@ import timber.log.Timber * An implementation of [HttpClient] using the native [HttpURLConnection]. * * @param userAgent Custom user agent to use for requests. - * @param additionalHeaders A dictionary of additional headers to send with requests. * @param connectTimeout Timeout used when establishing a connection to the resource. A null timeout * is interpreted as the default value, while a timeout of zero as an infinite timeout. * @param readTimeout Timeout used when reading the input stream. A null timeout is interpreted * as the default value, while a timeout of zero as an infinite timeout. */ -class DefaultHttpClient constructor( +public class DefaultHttpClient( private val userAgent: String? = null, - private val additionalHeaders: Map<String, String> = mapOf(), private val connectTimeout: Duration? = null, private val readTimeout: Duration? = null, - var callback: Callback = object : Callback {}, + public var callback: Callback = object : Callback {} ) : HttpClient { - companion object { + + @Suppress("UNUSED_PARAMETER") + @Deprecated( + "If you used [additionalHeaders], pass all headers when building your request or modify it in Callback.onStartRequest instead.", + level = DeprecationLevel.ERROR + ) + public constructor( + userAgent: String? = null, + additionalHeaders: Map<String, String> = mapOf(), + connectTimeout: Duration? = null, + readTimeout: Duration? = null, + callback: Callback = object : Callback {} + ) : this( + userAgent = userAgent, + connectTimeout = connectTimeout, + readTimeout = readTimeout, + callback = callback + ) + + public companion object { /** * [HttpRequest.extras] key for the number of redirections performed for a request. */ - const val EXTRA_REDIRECT_COUNT = "redirectCount" + private const val EXTRA_REDIRECT_COUNT: String = "redirectCount" } /** * Callbacks allowing to override some behavior of the [DefaultHttpClient]. */ - interface Callback { + public interface Callback { /** * Called when the HTTP client will start a new [request]. @@ -57,7 +86,7 @@ class DefaultHttpClient constructor( * You can modify the [request], for example by adding additional HTTP headers or * redirecting to a different URL, before returning it. */ - suspend fun onStartRequest(request: HttpRequest): HttpTry<HttpRequest> = + public suspend fun onStartRequest(request: HttpRequest): HttpTry<HttpRequest> = Try.success(request) /** @@ -67,9 +96,9 @@ class DefaultHttpClient constructor( * You can return either: * - a new recovery request to start * - the [error] argument, if you cannot recover from it - * - a new [HttpException] to provide additional information + * - a new [HttpError] to provide additional information */ - suspend fun onRecoverRequest(request: HttpRequest, error: HttpException): HttpTry<HttpRequest> = + public suspend fun onRecoverRequest(request: HttpRequest, error: HttpError): HttpTry<HttpRequest> = Try.failure(error) /** @@ -83,14 +112,17 @@ class DefaultHttpClient constructor( * You can return either: * - the provided [newRequest] to proceed with the redirection * - a different redirection request - * - a [HttpException.CANCELLED] error to abort the redirection */ - suspend fun onFollowUnsafeRedirect( + public suspend fun onFollowUnsafeRedirect( request: HttpRequest, response: HttpResponse, newRequest: HttpRequest ): HttpTry<HttpRequest> = - Try.failure(HttpException.CANCELLED) + Try.failure( + HttpError.Redirection( + DebugError("Request cancelled because of an unsafe redirect.") + ) + ) /** * Called when the HTTP client received an HTTP response for the given [request]. @@ -99,7 +131,7 @@ class DefaultHttpClient constructor( * This is merely for informational purposes. For example, you could implement this to * confirm that request credentials were successful. */ - suspend fun onResponseReceived(request: HttpRequest, response: HttpResponse) {} + public suspend fun onResponseReceived(request: HttpRequest, response: HttpResponse) {} /** * Called when the HTTP client received an [error] for the given [request]. @@ -109,13 +141,11 @@ class DefaultHttpClient constructor( * * This will be called only if [onRecoverRequest] is not implemented, or returns an error. */ - suspend fun onRequestFailed(request: HttpRequest, error: HttpException) {} + public suspend fun onRequestFailed(request: HttpRequest, error: HttpError) {} } // We are using Dispatchers.IO but we still get this warning... - @Suppress("BlockingMethodInNonBlockingContext", "NAME_SHADOWING") override suspend fun stream(request: HttpRequest): HttpTry<HttpStreamResponse> { - suspend fun tryStream(request: HttpRequest): HttpTry<HttpStreamResponse> = withContext(Dispatchers.IO) { Timber.i("HTTP ${request.method.name} ${request.url}, headers: ${request.headers}") @@ -124,7 +154,7 @@ class DefaultHttpClient constructor( var connection = request.toHttpURLConnection() val statusCode = connection.responseCode - HttpException.Kind.ofStatusCode(statusCode)?.let { kind -> + if (statusCode >= 400) { // It was a HEAD request? We need to query the resource again to get the error body. // The body is needed for example when the response is an OPDS Authentication // Document. @@ -138,18 +168,22 @@ class DefaultHttpClient constructor( // Reads the full body, since it might contain an error representation such as // JSON Problem Details or OPDS Authentication Document - val body = connection.errorStream.use { it.readBytes() } - val mediaType = connection.sniffMediaType(bytes = { body }) - throw HttpException(kind, mediaType, body) + val body = connection.errorStream?.use { it.readBytes() } + + val mediaType = connection.contentType?.let { MediaType(it) } + return@withContext Try.failure( + HttpError.ErrorResponse(HttpStatus(statusCode), mediaType, body) + ) } val response = HttpResponse( request = request, - url = connection.url.toString(), - statusCode = statusCode, + url = request.url, + statusCode = HttpStatus(statusCode), headers = connection.safeHeaders, - mediaType = connection.sniffMediaType() ?: MediaType.BINARY, + mediaType = connection.contentType?.let { MediaType(it) } ) + callback.onResponseReceived(request, response) if (statusCode in 300..399) { @@ -158,28 +192,25 @@ class DefaultHttpClient constructor( Try.success( HttpStreamResponse( response = response, - body = connection.inputStream, + body = HttpURLConnectionInputStream(connection) ) ) } - } catch (e: Exception) { - Try.failure(HttpException.wrap(e)) + } catch (e: IOException) { + Try.failure(wrap(e)) } } return callback.onStartRequest(request) .flatMap { tryStream(it) } .tryRecover { error -> - if (error.kind != HttpException.Kind.Cancelled) { - callback.onRecoverRequest(request, error) - .flatMap { stream(it) } - } else { - Try.failure(error) - } + callback.onRecoverRequest(request, error) + .flatMap { stream(it) } } .onFailure { callback.onRequestFailed(request, it) - Timber.e(it, "HTTP request failed ${request.url}") + val error = DebugError("HTTP request failed ${request.url}", it) + Timber.e(error.toDebugDescription()) } } @@ -198,18 +229,29 @@ class DefaultHttpClient constructor( // > https://www.rfc-editor.org/rfc/rfc1945.html#section-9.3 val redirectCount = request.extras.getInt(EXTRA_REDIRECT_COUNT) if (redirectCount > 5) { - return Try.failure(HttpException.CANCELLED) + return Try.failure( + HttpError.Redirection( + DebugError("There were too many redirects to follow.") + ) + ) } - val location = response.valueForHeader("Location") - ?: return Try.failure(HttpException(kind = HttpException.Kind.MalformedResponse)) + val location = response.header("Location") + ?.let { Url(it) } + ?.let { request.url.resolve(it) } + ?: return Try.failure( + HttpError.MalformedResponse( + DebugError("Location of redirect is missing or invalid.") + ) + ) val newRequest = HttpRequest( url = location, body = request.body, method = request.method, headers = buildMap { - response.valueForHeader("Set-Cookie") + response.headers("Set-Cookie") + .takeUnless { it.isEmpty() } ?.let { put("Cookie", it) } }, extras = Bundle().apply { @@ -223,7 +265,7 @@ class DefaultHttpClient constructor( } private fun HttpRequest.toHttpURLConnection(): HttpURLConnection { - val url = URL(url) + val url = URL(url.toString()) val connection = (url.openConnection() as HttpURLConnection) connection.requestMethod = method.name @@ -242,10 +284,11 @@ class DefaultHttpClient constructor( connection.setRequestProperty("User-Agent", userAgent) } - for ((k, v) in this@DefaultHttpClient.additionalHeaders) { - connection.setRequestProperty(k, v) - } - for ((k, v) in headers) { + val normalizedHeaders = headers + .lowerCaseKeys() + .joinValues(",") + + for ((k, v) in normalizedHeaders) { connection.setRequestProperty(k, v) } @@ -274,3 +317,57 @@ class DefaultHttpClient constructor( key == null || value == null } } + +/** + * Creates an HTTP error from a generic exception. + */ +private fun wrap(cause: IOException): HttpError = + when (cause) { + is UnknownHostException, is NoRouteToHostException, is ConnectException -> + HttpError.Unreachable(ThrowableError(cause)) + is SocketTimeoutException -> + HttpError.Timeout(ThrowableError(cause)) + is SSLHandshakeException -> + HttpError.SslHandshake(ThrowableError(cause)) + else -> + HttpError.IO(cause) + } + +/** + * [HttpURLConnection]'s input stream which disconnects when closed. + */ +private class HttpURLConnectionInputStream( + private val connection: HttpURLConnection +) : InputStream() { + + private val inputStream = connection.inputStream + + override fun close() { + super.close() + connection.disconnect() + } + + override fun read(): Int = + inputStream.read() + + override fun read(b: ByteArray): Int = + inputStream.read(b) + + override fun read(b: ByteArray, off: Int, len: Int): Int = + inputStream.read(b, off, len) + + override fun skip(n: Long): Long = + inputStream.skip(n) + + override fun available(): Int = + inputStream.available() + + override fun mark(readlimit: Int) = + inputStream.mark(readlimit) + + override fun reset() = + inputStream.reset() + + override fun markSupported(): Boolean = + inputStream.markSupported() +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt index 4f18fbb3ab..e3940012b3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt @@ -6,14 +6,19 @@ package org.readium.r2.shared.util.http +import java.io.IOException import java.io.InputStream import java.nio.charset.Charset import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.flatMap -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.tryRecover + +public typealias HttpTry<SuccessT> = Try<SuccessT, HttpError> /** * An HTTP client performs HTTP requests. @@ -21,53 +26,74 @@ import org.readium.r2.shared.util.mediatype.MediaType * You may provide a custom implementation, or use the [DefaultHttpClient] one which relies on * native APIs. */ -interface HttpClient { +public interface HttpClient { /** * Streams the resource from the given [request]. */ - suspend fun stream(request: HttpRequest): HttpTry<HttpStreamResponse> - - /** - * Fetches the resource from the given [request]. - */ - suspend fun fetch(request: HttpRequest): HttpTry<HttpFetchResponse> = - stream(request) - .flatMap { response -> - try { - val body = withContext(Dispatchers.IO) { - response.body.use { it.readBytes() } - } - Try.success(HttpFetchResponse(response.response, body)) - } catch (e: Exception) { - Try.failure(HttpException.wrap(e)) - } - } + public suspend fun stream(request: HttpRequest): HttpTry<HttpStreamResponse> // Declare a companion object to allow reading apps to extend it. For example, by adding a // HttpClient.get(Context) constructor. - companion object + public companion object } +/** + * HTTP response with streamable content. + * + * You MUST close the [body] to terminate the HTTP connection when you're done. + */ +public class HttpStreamResponse( + public val response: HttpResponse, + public val body: InputStream +) + +/** + * Fetches the resource from the given [request]. + */ +public suspend fun HttpClient.fetch(request: HttpRequest): HttpTry<HttpFetchResponse> = + stream(request) + .flatMap { response -> + try { + val body = withContext(Dispatchers.IO) { + response.body.use { it.readBytes() } + } + Try.success( + HttpFetchResponse(response.response, body) + ) + } catch (e: IOException) { + Try.failure( + HttpError.IO(e) + ) + } + } + /** * Fetches the resource from the given [request] before decoding it with the provided [decoder]. * * If the decoder fails, a MalformedResponse error is returned. */ -suspend fun <T> HttpClient.fetchWithDecoder(request: HttpRequest, decoder: (HttpFetchResponse) -> T): HttpTry<T> = +public suspend fun <T> HttpClient.fetchWithDecoder( + request: HttpRequest, + decoder: (HttpFetchResponse) -> T +): HttpTry<T> = fetch(request) .flatMap { try { - Try.success(decoder(it)) + Try.success( + decoder(it) + ) } catch (e: Exception) { - Try.failure(HttpException(kind = HttpException.Kind.MalformedResponse, cause = e)) + Try.failure( + HttpError.MalformedResponse(ThrowableError(e)) + ) } } /** * Fetches the resource from the given [request] as a [String]. */ -suspend fun HttpClient.fetchString(request: HttpRequest, charset: Charset = Charsets.UTF_8): HttpTry<String> = +public suspend fun HttpClient.fetchString(request: HttpRequest, charset: Charset = Charsets.UTF_8): HttpTry<String> = fetchWithDecoder(request) { response -> String(response.body, charset) } @@ -75,63 +101,47 @@ suspend fun HttpClient.fetchString(request: HttpRequest, charset: Charset = Char /** * Fetches the resource from the given [request] as a [JSONObject]. */ -suspend fun HttpClient.fetchJSONObject(request: HttpRequest): HttpTry<JSONObject> = +public suspend fun HttpClient.fetchJSONObject(request: HttpRequest): HttpTry<JSONObject> = fetchWithDecoder(request) { response -> JSONObject(String(response.body)) } -class HttpStreamResponse( - val response: HttpResponse, - val body: InputStream, -) - -class HttpFetchResponse( - val response: HttpResponse, - val body: ByteArray, +/** + * HTTP response with the whole [body] as a [ByteArray]. + */ +public class HttpFetchResponse( + public val response: HttpResponse, + public val body: ByteArray ) /** - * Represents a successful HTTP response received from a server. + * Performs a HEAD request to retrieve only the response headers. * - * @param request Request associated with the response. - * @param url Final URL of the response. - * @param statusCode Response status code. - * @param headers HTTP response headers, indexed by their name. - * @param mediaType Media type sniffed from the `Content-Type` header and response body. Falls back - * on `application/octet-stream`. + * This helpers falls back on a GET request with 0-length byte range if the server doesn't support + * HEAD requests. */ -data class HttpResponse( - val request: HttpRequest, - val url: String, - val statusCode: Int, - val headers: Map<String, List<String>>, - val mediaType: MediaType, -) { - - private val httpHeaders = HttpHeaders(headers) - - /** - * Finds the first value of the first header matching the given name. - * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. - */ - fun valueForHeader(name: String): String? = httpHeaders[name] - - /** - * Finds all the values of the first header matching the given name. - * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. - */ - fun valuesForHeader(name: String): List<String> = httpHeaders.getAll(name) +@ExperimentalReadiumApi +public suspend fun HttpClient.head(request: HttpRequest): HttpTry<HttpResponse> { + suspend fun HttpRequest.response(): HttpTry<HttpResponse> = + stream(this) + .map { response -> + response.body.close() + response.response + } - /** - * Indicates whether this server supports byte range requests. - */ - val acceptsByteRanges: Boolean get() = httpHeaders.acceptsByteRanges + return request + .copy { method = HttpRequest.Method.HEAD } + .response() + .tryRecover { error -> + if (error !is HttpError.ErrorResponse || error.status != HttpStatus.MethodNotAllowed) { + return@tryRecover Try.failure(error) + } - /** - * The expected content length for this response, when known. - * - * Warning: For byte range requests, this will be the length of the chunk, not the full - * resource. - */ - val contentLength: Long? get() = httpHeaders.contentLength + request + .copy { + method = HttpRequest.Method.GET + setRange(0L..0L) + } + .response() + } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt new file mode 100644 index 0000000000..3df0449dad --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2021 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.http + +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.resource.Resource + +/** + * Fetches remote resources through HTTP. + * + * Since this container is used when doing progressive download streaming (e.g. audiobook), the HTTP + * byte range requests are open-ended and reused. This helps to avoid issuing too many requests. + * + * @param baseUrl Base URL from which relative URLs are served. + * @param entries Entries of this container as Urls absolute or relative to [baseUrl]. + * @param client HTTP client used to perform HTTP requests. + */ +public class HttpContainer( + private val baseUrl: Url? = null, + override val entries: Set<Url>, + private val client: HttpClient +) : Container<Resource> { + + override fun get(url: Url): Resource? { + // We don't check that url matches any entry because that might save us from edge cases. + + val absoluteUrl = (baseUrl?.resolve(url) ?: url) as? AbsoluteUrl + + return if (absoluteUrl == null || !absoluteUrl.isHttp) { + null + } else { + HttpResource(absoluteUrl, client) + } + } + + override suspend fun close() {} +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt new file mode 100644 index 0000000000..a40c1733f5 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.http + +import org.json.JSONObject +import org.readium.r2.shared.extensions.tryOrLog +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.data.AccessError +import org.readium.r2.shared.util.mediatype.MediaType + +/** + * Represents an error occurring during an HTTP activity. + */ +public sealed class HttpError( + public override val message: String, + public override val cause: Error? = null +) : AccessError { + + /** Malformed HTTP response. */ + public class MalformedResponse(cause: Error?) : + HttpError("The received response could not be decoded.", cause) + + /** The client, server or gateways timed out. */ + public class Timeout(cause: Error) : + HttpError("Request timed out.", cause) + + /** Server could not be reached. */ + public class Unreachable(cause: Error) : + HttpError("Server could not be reached.", cause) + + /** Redirection failed. */ + public class Redirection(cause: Error) : + HttpError("Redirection failed.", cause) + + /** SSL Handshake failed. */ + public class SslHandshake(cause: Error) : + HttpError("SSL handshake failed.", cause) + + /** An unknown networking error. */ + public class IO(cause: Error) : + HttpError("An IO error occurred.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + /** + * Server responded with an error status code. + * + * @param status HTTP status code. + * @param mediaType Response media type. + * @param body Response body. + */ + public class ErrorResponse( + public val status: HttpStatus, + public val mediaType: MediaType? = null, + public val body: ByteArray? = null + ) : HttpError("HTTP Error ${status.code}", null) { + + /** Response body parsed as a JSON problem details. */ + public val problemDetails: ProblemDetails? by lazy { + if (body == null || mediaType?.matches(MediaType.JSON_PROBLEM_DETAILS) != true) { + return@lazy null + } + + tryOrLog { ProblemDetails.fromJSON(JSONObject(String(body))) } + } + } + + public companion object { + @Suppress("UNUSED_PARAMETER") + @Deprecated("Not publicly available anymore.", level = DeprecationLevel.ERROR) + public fun wrap(exception: Exception): HttpError = + throw NotImplementedError() + } +} + +@Deprecated("Renamed to `HttpError`", ReplaceWith("HttpError"), DeprecationLevel.ERROR) +public typealias HttpException = HttpError diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpException.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpException.kt deleted file mode 100644 index 66077e5802..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpException.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2021 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.http - -import android.content.Context -import androidx.annotation.StringRes -import java.net.MalformedURLException -import java.net.SocketTimeoutException -import java.util.concurrent.CancellationException -import org.json.JSONObject -import org.readium.r2.shared.R -import org.readium.r2.shared.UserException -import org.readium.r2.shared.extensions.tryOrLog -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.mediatype.MediaType - -typealias HttpTry<SuccessT> = Try<SuccessT, HttpException> - -/** - * Represents an error occurring during an HTTP activity. - * - * @param kind Category of HTTP error. - * @param mediaType Response media type. - * @param body Response body. - * @param cause Underlying error, if any. - */ -class HttpException( - val kind: Kind, - val mediaType: MediaType? = null, - val body: ByteArray? = null, - cause: Throwable? = null, -) : UserException(kind.userMessageId, cause = cause) { - - enum class Kind(@StringRes val userMessageId: Int) { - /** The provided request was not valid. */ - MalformedRequest(R.string.r2_shared_http_exception_malformed_request), - /** The received response couldn't be decoded. */ - MalformedResponse(R.string.r2_shared_http_exception_malformed_response), - /** The client, server or gateways timed out. */ - Timeout(R.string.r2_shared_http_exception_timeout), - /** (400) The server cannot or will not process the request due to an apparent client error. */ - BadRequest(R.string.r2_shared_http_exception_bad_request), - /** (401) Authentication is required and has failed or has not yet been provided. */ - Unauthorized(R.string.r2_shared_http_exception_unauthorized), - /** (403) The server refuses the action, probably because we don't have the necessary permissions. */ - Forbidden(R.string.r2_shared_http_exception_forbidden), - /** (404) The requested resource could not be found. */ - NotFound(R.string.r2_shared_http_exception_not_found), - /** (4xx) Other client errors */ - ClientError(R.string.r2_shared_http_exception_client_error), - /** (5xx) Server errors */ - ServerError(R.string.r2_shared_http_exception_server_error), - /** The device is offline. */ - Offline(R.string.r2_shared_http_exception_offline), - /** The request was cancelled. */ - Cancelled(R.string.r2_shared_http_exception_cancelled), - /** An error whose kind is not recognized. */ - Other(R.string.r2_shared_http_exception_other); - - companion object { - - /** Resolves the kind of the HTTP error associated with the given [statusCode]. */ - fun ofStatusCode(statusCode: Int): Kind? = - when (statusCode) { - in 200..399 -> null - 400 -> BadRequest - 401 -> Unauthorized - 403 -> Forbidden - 404 -> NotFound - in 405..498 -> ClientError - 499 -> Cancelled - in 500..599 -> ServerError - else -> MalformedResponse - } - } - } - - override fun getUserMessage(context: Context, includesCauses: Boolean): String { - problemDetails?.let { error -> - var message = error.title - if (error.detail != null) { - message += "\n" + error.detail - } - return message - } - - return super.getUserMessage(context, includesCauses) - } - - override fun getLocalizedMessage(): String? { - var message = "HTTP error: ${kind.name}" - problemDetails?.let { details -> - message += ": ${details.title} ${details.detail}" - } - return message - } - - /** Response body parsed as a JSON problem details. */ - val problemDetails: ProblemDetails? by lazy { - if (body == null || mediaType?.matches(MediaType.JSON_PROBLEM_DETAILS) != true) { - return@lazy null - } - - tryOrLog { ProblemDetails.fromJSON(JSONObject(String(body))) } - } - - companion object { - - /** - * Shortcut for a cancelled HTTP error. - */ - val CANCELLED = HttpException(kind = Kind.Cancelled) - - /** - * Creates an HTTP error from a status code. - * - * Returns null if the status code is a success. - */ - operator fun invoke(statusCode: Int, mediaType: MediaType? = null, body: ByteArray? = null): HttpException? = - Kind.ofStatusCode(statusCode)?.let { kind -> - HttpException(kind, mediaType, body) - } - - /** - * Creates an HTTP error from a generic exception. - */ - fun wrap(cause: Throwable): HttpException { - val kind = when (cause) { - is HttpException -> return cause - is MalformedURLException -> Kind.MalformedRequest - is CancellationException -> Kind.Cancelled - is SocketTimeoutException -> Kind.Timeout - else -> Kind.Other - } - - return HttpException(kind = kind, cause = cause) - } - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpHeaders.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpHeaders.kt index 954a4dfcd7..8ef714ea68 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpHeaders.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpHeaders.kt @@ -12,38 +12,32 @@ import org.readium.r2.shared.InternalReadiumApi * Helper to parse HTTP request/response headers. */ @InternalReadiumApi -data class HttpHeaders(val headers: Map<String, List<String>>) { +public data class HttpHeaders(val headers: Map<String, List<String>>) { - companion object { - operator fun invoke(headers: Map<String, String>): HttpHeaders = + public companion object { + public operator fun invoke(headers: Map<String, String>): HttpHeaders = HttpHeaders(headers.mapValues { (_, value) -> listOf(value) }) } /** - * Finds the first value of the first header matching the given name. + * Finds the last header matching the given name. * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. + * The returned string can contain a single value or a comma-separated list of values if + * the field supports it. */ - operator fun get(name: String): String? { - val n = name.lowercase() - return headers.firstNotNullOfOrNull { (key, value) -> - if (key.lowercase() == n) value.firstOrNull() - else null - } - } + public operator fun get(name: String): String? = getAll(name) + .lastOrNull() /** - * Finds all the values of the first header matching the given name. + * Finds all the headers matching the given name. * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. + * Each item of the returned list can contain a single value or a comma-separated list of + * values if the field supports it. */ - fun getAll(name: String): List<String> { - val n = name.lowercase() - return headers - .mapNotNull { (key, value) -> - if (key.lowercase() == n) value - else null - } - .flatten() - } + public fun getAll(name: String): List<String> = headers + .filter { it.key.lowercase() == name.lowercase() } + .values + .flatten() /** * Indicates whether this server supports byte range requests. @@ -92,10 +86,10 @@ data class HttpHeaders(val headers: Map<String, List<String>>) { * * [end] is inclusive. */ -data class HttpRange( +public data class HttpRange( val start: Long, val end: Long? ) { - fun toLongRange(contentLength: Long): LongRange = + public fun toLongRange(contentLength: Long): LongRange = start..(end ?: (contentLength - 1)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt index a85e5c0ccf..d30d3ca22d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt @@ -1,3 +1,9 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + package org.readium.r2.shared.util.http import android.net.Uri @@ -5,6 +11,9 @@ import android.os.Bundle import java.io.Serializable import java.net.URLEncoder import kotlin.time.Duration +import org.readium.r2.shared.extensions.toMutable +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.toUri /** * Holds the information about an HTTP request performed by an [HttpClient]. @@ -22,32 +31,57 @@ import kotlin.time.Duration * @param allowUserInteraction If true, the user might be presented with interactive dialogs, such * as popping up an authentication dialog. */ -class HttpRequest( - val url: String, - val method: Method = Method.GET, - val headers: Map<String, String> = mapOf(), - val body: Body? = null, - val extras: Bundle = Bundle(), - val connectTimeout: Duration? = null, - val readTimeout: Duration? = null, - val allowUserInteraction: Boolean = false, +public class HttpRequest( + public val url: AbsoluteUrl, + public val method: Method = Method.GET, + public val headers: Map<String, List<String>> = mapOf(), + public val body: Body? = null, + public val extras: Bundle = Bundle(), + public val connectTimeout: Duration? = null, + public val readTimeout: Duration? = null, + public val allowUserInteraction: Boolean = false ) : Serializable { + @Deprecated( + message = "Provide an instance of `AbsoluteUrl` instead of a string.", + replaceWith = ReplaceWith("HttpRequest(AbsoluteUrl(url)!!)"), + level = DeprecationLevel.ERROR + ) + public constructor( + url: String, + method: Method = Method.GET, + headers: Map<String, String> = mapOf(), + body: Body? = null, + extras: Bundle = Bundle(), + connectTimeout: Duration? = null, + readTimeout: Duration? = null, + allowUserInteraction: Boolean = false + ) : this( + url = AbsoluteUrl(url)!!, + method = method, + headers = headers.mapValues { (_, value) -> listOf(value) }, + body = body, + extras = extras, + connectTimeout = connectTimeout, + readTimeout = readTimeout, + allowUserInteraction = allowUserInteraction + ) + /** Supported HTTP methods. */ - enum class Method : Serializable { + public enum class Method : Serializable { DELETE, GET, HEAD, PATCH, POST, PUT; } /** Supported body values. */ - sealed class Body : Serializable { - class Bytes(val bytes: ByteArray) : Body() - class File(val file: java.io.File) : Body() + public sealed class Body : Serializable { + public class Bytes(public val bytes: ByteArray) : Body() + public class File(public val file: java.io.File) : Body() } - fun buildUpon() = Builder( + public fun buildUpon(): Builder = Builder( url = url, method = method, - headers = headers.toMutableMap(), + headers = headers.toMutable(), body = body, extras = extras, connectTimeout = connectTimeout, @@ -55,51 +89,69 @@ class HttpRequest( allowUserInteraction = allowUserInteraction ) - companion object { - operator fun invoke(url: String, build: Builder.() -> Unit): HttpRequest = + public fun copy(build: Builder.() -> Unit): HttpRequest = + buildUpon().apply(build).build() + + public companion object { + public operator fun invoke(url: AbsoluteUrl, build: Builder.() -> Unit): HttpRequest = Builder(url).apply(build).build() } - class Builder( - url: String, - var method: Method = Method.GET, - var headers: MutableMap<String, String> = mutableMapOf(), - var body: Body? = null, - var extras: Bundle = Bundle(), - var connectTimeout: Duration? = null, - var readTimeout: Duration? = null, - var allowUserInteraction: Boolean = false, + public class Builder( + public val url: AbsoluteUrl, + public var method: Method = Method.GET, + public var headers: MutableMap<String, MutableList<String>> = mutableMapOf(), + public var body: Body? = null, + public var extras: Bundle = Bundle(), + public var connectTimeout: Duration? = null, + public var readTimeout: Duration? = null, + public var allowUserInteraction: Boolean = false ) { - var url: String - get() = uriBuilder.build().toString() - set(value) { uriBuilder = Uri.parse(value).buildUpon() } + private var uriBuilder: Uri.Builder = url.toUri().buildUpon() - private var uriBuilder: Uri.Builder = Uri.parse(url).buildUpon() - - fun appendQueryParameter(key: String, value: String?): Builder { + public fun appendQueryParameter(key: String, value: String?): Builder { if (value != null) { uriBuilder.appendQueryParameter(key, value) } return this } - fun appendQueryParameters(params: Map<String, String?>): Builder { + public fun appendQueryParameters(params: Map<String, String?>): Builder { for ((key, value) in params) { appendQueryParameter(key, value) } return this } - fun setHeader(key: String, value: String): Builder { - headers[key] = value + /** + * Sets header with key [key] to [values] overriding current values, if any. + */ + public fun setHeader(key: String, values: List<String>): Builder { + headers[key] = values.toMutableList() + return this + } + + /** + * Sets header with [key] to [value] overriding current values, if any. + */ + public fun setHeader(key: String, value: String): Builder { + headers[key] = mutableListOf(value) + return this + } + + /** + * Adds [value] to header values associated with [key]. + */ + public fun addHeader(key: String, value: String): Builder { + headers.getOrPut(key) { mutableListOf() }.add(value) return this } /** * Issue a byte range request. Use -1 to download until the end. */ - fun setRange(range: LongRange): Builder { + public fun setRange(range: LongRange): Builder { val start = range.first.coerceAtLeast(0) var value = "$start-" if (range.last >= start) { @@ -112,7 +164,7 @@ class HttpRequest( /** * Initializes a POST request with the given form data. */ - fun setPostForm(form: Map<String, String?>): Builder { + public fun setPostForm(form: Map<String, String?>): Builder { method = Method.POST setHeader("Content-Type", "application/x-www-form-urlencoded") @@ -128,7 +180,7 @@ class HttpRequest( return this } - fun build(): HttpRequest = HttpRequest( + public fun build(): HttpRequest = HttpRequest( url = url, method = method, headers = headers.toMap(), @@ -136,7 +188,7 @@ class HttpRequest( extras = extras, connectTimeout = connectTimeout, readTimeout = readTimeout, - allowUserInteraction = allowUserInteraction, + allowUserInteraction = allowUserInteraction ) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt new file mode 100644 index 0000000000..4f71d06c94 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.http + +import java.io.IOException +import java.io.InputStream +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.extensions.read +import org.readium.r2.shared.extensions.tryOrLog +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.io.CountingInputStream +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.resource.mediaType + +/** Provides access to an external URL through HTTP. */ +@OptIn(ExperimentalReadiumApi::class) +public class HttpResource( + override val sourceUrl: AbsoluteUrl, + private val client: HttpClient, + private val maxSkipBytes: Long = MAX_SKIP_BYTES +) : Resource { + + override suspend fun properties(): Try<Resource.Properties, ReadError> = + headResponse().map { + Resource.Properties( + Resource.Properties.Builder() + .apply { + mediaType = it.mediaType + filename = it.url.filename + } + ) + } + + override suspend fun length(): Try<Long, ReadError> = + headResponse().flatMap { + val contentLength = it.contentLength + return if (contentLength != null) { + Try.success(contentLength) + } else { + Try.failure( + ReadError.UnsupportedOperation( + DebugError( + "Server did not provide content length in its response to request to $sourceUrl." + ) + ) + ) + } + } + + override suspend fun close() {} + + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> = withContext( + Dispatchers.IO + ) { + try { + stream(range?.first.takeUnless { it == 0L }).map { stream -> + if (range != null) { + stream.read(range.count().toLong()) + } else { + stream.readBytes() + } + } + } catch (e: IOException) { + Try.failure(ReadError.Access(HttpError.IO(e))) + } + } + + /** Cached HEAD response to get the expected content length and other metadata. */ + private lateinit var _headResponse: Try<HttpResponse, ReadError> + + private suspend fun headResponse(): Try<HttpResponse, ReadError> { + if (::_headResponse.isInitialized) { + return _headResponse + } + + _headResponse = client.head(HttpRequest(sourceUrl)) + .mapFailure { ReadError.Access(it) } + + return _headResponse + } + + /** + * Returns an HTTP stream for the resource, starting at the [from] byte offset. + * + * The stream is cached and reused for next calls, if the next [from] offset is not too far + * and in a forward direction. + */ + private suspend fun stream(from: Long? = null): Try<InputStream, ReadError> { + val stream = inputStream + if (from != null && stream != null) { + tryOrLog { + val bytesToSkip = from - (inputStreamStart + stream.count) + if (bytesToSkip in 0 until maxSkipBytes) { + stream.skip(bytesToSkip) + return Try.success(stream) + } + } + } + tryOrLog { inputStream?.close() } + + val request = HttpRequest(sourceUrl) { + from?.let { setRange(from..-1) } + } + + return client.stream(request) + .mapFailure { ReadError.Access(it) } + .flatMap { response -> + if (from != null && response.response.statusCode.code != 206) { + val error = DebugError( + "Server seems not to support range requests to $sourceUrl." + ) + Try.failure(ReadError.UnsupportedOperation(error)) + } else { + Try.success(response) + } + } + .map { CountingInputStream(it.body) } + .onSuccess { + inputStream = it + inputStreamStart = from ?: 0 + } + } + + private var inputStream: CountingInputStream? = null + private var inputStreamStart = 0L + + public companion object { + + private const val MAX_SKIP_BYTES: Long = 8192 + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt new file mode 100644 index 0000000000..ef787bf800 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.http + +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceFactory + +/** + * Creates [HttpResource] instances granting access to `http://` URLs using an [HttpClient]. + */ +public class HttpResourceFactory( + private val httpClient: HttpClient +) : ResourceFactory { + + override suspend fun create( + url: AbsoluteUrl + ): Try<Resource, ResourceFactory.Error> { + if (!url.isHttp) { + return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) + } + + val resource = HttpResource(url, httpClient) + return Try.success(resource) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt new file mode 100644 index 0000000000..a13c03ec12 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.http + +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.mediatype.MediaType + +/** + * Represents a successful HTTP response received from a server. + * + * @param request Request associated with the response. + * @param url Final URL of the response. + * @param statusCode Response status code. + * @param headers HTTP response headers, indexed by their name. + * @param mediaType Media type from the `Content-Type` header. + */ +public data class HttpResponse( + val request: HttpRequest, + val url: AbsoluteUrl, + val statusCode: HttpStatus, + val headers: Map<String, List<String>>, + val mediaType: MediaType? +) { + + private val httpHeaders = HttpHeaders(headers) + + /** + * Finds the first value of the first header matching the given name. + * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. + */ + @Deprecated("Use the header method instead.", level = DeprecationLevel.ERROR) + @Suppress("Unused_parameter") + public fun valueForHeader(name: String): String? { + throw NotImplementedError() + } + + /** + * Finds all the values of the first header matching the given name. + * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. + */ + @Deprecated("Use the headers method instead.", level = DeprecationLevel.ERROR) + @Suppress("Unused_parameter") + public fun valuesForHeader(name: String): List<String> { + throw NotImplementedError() + } + + /** + * Finds the last header matching the given name. + * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. + * The returned string can contain a single value or a comma-separated list of values if + * the field supports it. + */ + public fun header(name: String): String? = httpHeaders[name] + + /** + * Finds all the headers matching the given name. + * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. + * Each item of the returned list can contain a single value or a comma-separated list of + * values if the field supports it. + */ + public fun headers(name: String): List<String> = httpHeaders.getAll(name) + + /** + * Indicates whether this server supports byte range requests. + */ + val acceptsByteRanges: Boolean get() = httpHeaders.acceptsByteRanges + + /** + * The expected content length for this response, when known. + * + * Warning: For byte range requests, this will be the length of the chunk, not the full + * resource. + */ + val contentLength: Long? get() = httpHeaders.contentLength +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt new file mode 100644 index 0000000000..78824ab07c --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.http + +@JvmInline +/** + * Status code of an HTTP response. + */ +public value class HttpStatus( + public val code: Int +) : Comparable<HttpStatus> { + + override fun compareTo(other: HttpStatus): Int = + code.compareTo(other.code) + + public companion object { + + public val Success: HttpStatus = HttpStatus(200) + + /** (400) The server cannot or will not process the request due to an apparent client error. */ + public val BadRequest: HttpStatus = HttpStatus(400) + + /** (401) Authentication is required and has failed or has not yet been provided. */ + public val Unauthorized: HttpStatus = HttpStatus(401) + + /** (403) The server refuses the action, probably because we don't have the necessary permissions. */ + public val Forbidden: HttpStatus = HttpStatus(403) + + /** (404) The requested resource could not be found. */ + public val NotFound: HttpStatus = HttpStatus(404) + + /** (405) Method not allowed. */ + public val MethodNotAllowed: HttpStatus = HttpStatus(405) + + /** (500) Internal Server Error */ + public val InternalServerError: HttpStatus = HttpStatus(500) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/ProblemDetails.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/ProblemDetails.kt index b290c212da..36a219c4c8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/ProblemDetails.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/ProblemDetails.kt @@ -20,17 +20,17 @@ import org.readium.r2.shared.util.logging.log * https://tools.ietf.org/html/rfc7807 * * @param title A short, human-readable summary of the problem type. - * @param type A URI reference [RFC3986] that identifies the problem type. This specification + * @param type A URI reference RFC3986 that identifies the problem type. This specification * encourages that, when dereferenced, it provide human-readable documentation for the * problem type. - * @param status The HTTP status code ([RFC7231], Section 6) generated by the origin server for this + * @param status The HTTP status code (RFC7231, Section 6) generated by the origin server for this * occurrence of the problem. * @param detail A human-readable explanation specific to this occurrence of the problem. * @param instance A URI reference that identifies the specific occurrence of the problem. It may or * may not yield further information if dereferenced. */ @Parcelize -data class ProblemDetails( +public data class ProblemDetails( val title: String, val type: String? = null, val status: Int? = null, @@ -38,12 +38,12 @@ data class ProblemDetails( val instance: String? = null ) : Parcelable { - companion object { + public companion object { /** * Creates a [ProblemDetails] from its JSON representation. */ - fun fromJSON(json: JSONObject, warnings: WarningLogger? = null): ProblemDetails? { + public fun fromJSON(json: JSONObject, warnings: WarningLogger? = null): ProblemDetails? { val title = json.optNullableString("title") if (title == null) { warnings?.log(ProblemDetails::class.java, "[title] is required", json) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/io/CountingInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/io/CountingInputStream.kt index b2f0ed2567..7712eabd4e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/io/CountingInputStream.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/io/CountingInputStream.kt @@ -18,9 +18,12 @@ import org.readium.r2.shared.extensions.requireLengthFitInt * An [InputStream] counting the number of bytes read from a wrapped [inputStream]. */ @InternalReadiumApi -class CountingInputStream(inputStream: InputStream) : FilterInputStream(inputStream) { +public class CountingInputStream( + inputStream: InputStream, + initialCount: Long = 0 +) : FilterInputStream(inputStream) { - var count: Long = 0 + public var count: Long = initialCount private set private var mark: Long = -1 @@ -62,14 +65,15 @@ class CountingInputStream(inputStream: InputStream) : FilterInputStream(inputStr count = mark.coerceAtLeast(0) } - fun readRange(range: LongRange): ByteArray { + public fun readRange(range: LongRange): ByteArray { @Suppress("NAME_SHADOWING") val range = range .coerceFirstNonNegative() .requireLengthFitInt() - if (range.isEmpty()) + if (range.isEmpty()) { return ByteArray(0) + } skip(range.first - count) val length = range.last - range.first + 1 diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/logging/WarningLogger.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/logging/WarningLogger.kt index d982714722..87c2ca84c1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/logging/WarningLogger.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/logging/WarningLogger.kt @@ -13,27 +13,26 @@ import org.json.JSONObject import org.readium.r2.shared.util.logging.Warning.SeverityLevel.* import timber.log.Timber -// FIXME: Mark this interface as functional to benefit from the SAM-conversion in Kotlin 1.4 https://blog.jetbrains.com/kotlin/2020/03/kotlin-1-4-m1-released/#new-type-inference /** * Interface to be implemented by third-party apps if they want to observe warnings raised, for - * example, during the parsing of a [Publication]. + * example, during the parsing of a publication. */ -interface WarningLogger { +public fun interface WarningLogger { /** Notifies that a warning occurred. */ - fun log(warning: Warning) + public fun log(warning: Warning) } /** * Implementation of a [WarningLogger] that accumulates the warnings in a list, to be used as a * convenience by third-party apps. */ -class ListWarningLogger : WarningLogger { +public class ListWarningLogger : WarningLogger { /** * The list of accumulated [Warning]s. */ - val warnings: List<Warning> get() = _warnings + public val warnings: List<Warning> get() = _warnings private val _warnings = mutableListOf<Warning>() override fun log(warning: Warning) { @@ -44,7 +43,7 @@ class ListWarningLogger : WarningLogger { /** * Implementation of a [WarningLogger] printing the warnings to the console. */ -class ConsoleWarningLogger : WarningLogger { +public class ConsoleWarningLogger : WarningLogger { override fun log(warning: Warning) { val message = "[${warning.tag}] ${warning.message}" @@ -61,7 +60,7 @@ class ConsoleWarningLogger : WarningLogger { * For example, while parsing an EPUB we, might want to report issues in the publication without * failing the whole parsing. */ -interface Warning { +public interface Warning { /** * Indicates how the user experience might be affected by a warning. @@ -70,7 +69,7 @@ interface Warning { * @property MODERATE The user experience might be affected, but it shouldn't prevent the user from enjoying the publication. * @property MAJOR The user experience will most likely be disturbed, for example with rendering issues. */ - enum class SeverityLevel { + public enum class SeverityLevel { MINOR, MODERATE, MAJOR @@ -81,17 +80,17 @@ interface Warning { * * For example json, metadata, etc. */ - val tag: String + public val tag: String /** * Localized user-facing message describing the issue. */ - val message: String + public val message: String /** * Indicates the severity level of this warning. */ - val severity: SeverityLevel + public val severity: SeverityLevel } /** @@ -101,7 +100,7 @@ interface Warning { * @param reason Details about the failure. * @param json Source [JSONObject]. */ -data class JsonWarning( +public data class JsonWarning( val modelClass: Class<*>, val reason: String, override val severity: Warning.SeverityLevel, @@ -110,7 +109,7 @@ data class JsonWarning( override val tag: String = "json" - override val message: String get() = "${javaClass.name} ${modelClass.name}: $reason" + override val message: String get() = "${javaClass.simpleName} ${modelClass.name}: $reason" } /** @@ -121,7 +120,7 @@ data class JsonWarning( * @param severity The severity level of this warning. * @param json Source [JSONObject]. */ -fun WarningLogger.log( +public fun WarningLogger.log( modelClass: Class<*>, reason: String, json: JSONObject? = null, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/Extensions.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/Extensions.kt index e06fd7b67f..7d32da42bb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/Extensions.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/Extensions.kt @@ -6,63 +6,18 @@ package org.readium.r2.shared.util.mediatype -import com.github.kittinunf.fuel.core.Response +import java.io.File import java.net.HttpURLConnection -import org.readium.r2.shared.extensions.extension -/** - * Resolves the format for this [HttpURLConnection], with optional extra file extension and media type - * hints. - */ -suspend fun HttpURLConnection.sniffMediaType( +@Suppress("UnusedReceiverParameter", "RedundantSuspendModifier", "UNUSED_PARAMETER") +@Deprecated("Use your own solution instead", level = DeprecationLevel.ERROR) +public suspend fun HttpURLConnection.sniffMediaType( bytes: (() -> ByteArray)? = null, mediaTypes: List<String> = emptyList(), - fileExtensions: List<String> = emptyList(), - sniffers: List<Sniffer> = MediaType.sniffers -): MediaType? { - val allMediaTypes = mediaTypes.toMutableList() - val allFileExtensions = fileExtensions.toMutableList() - - // The value of the `Content-Type` HTTP header. - contentType?.let { - allMediaTypes.add(0, it) - } - - // The URL file extension. - url.extension?.let { - allFileExtensions.add(0, it) - } - - // TODO: The suggested filename extension, part of the HTTP header `Content-Disposition`. - - return if (bytes != null) { - MediaType.ofBytes(bytes, mediaTypes = allMediaTypes, fileExtensions = allFileExtensions, sniffers = sniffers) - } else { - MediaType.of(mediaTypes = allMediaTypes, fileExtensions = allFileExtensions, sniffers = sniffers) - } -} - -/** - * Resolves the format for this [Response], with optional extra file extension and media type - * hints. - */ -suspend fun Response.sniffMediaType( - mediaTypes: List<String> = emptyList(), - fileExtensions: List<String> = emptyList(), - sniffers: List<Sniffer> = MediaType.sniffers -): MediaType? { - val allMediaTypes = mediaTypes.toMutableList() - val allFileExtensions = fileExtensions.toMutableList() - - // The value of the `Content-Type` HTTP header. - allMediaTypes.addAll(0, headers["Content-Type"]) - - // The URL file extension. - url.extension?.let { - allFileExtensions.add(0, it) - } - - // TODO: The suggested filename extension, part of the HTTP header `Content-Disposition`. + fileExtensions: List<String> = emptyList() +): MediaType = throw NotImplementedError() - return MediaType.ofBytes({ data }, mediaTypes = allMediaTypes, fileExtensions = allFileExtensions, sniffers = sniffers) -} +@Suppress("UnusedReceiverParameter", "RedundantSuspendModifier", "UNUSED_PARAMETER") +@Deprecated("Use your own solution instead", level = DeprecationLevel.ERROR) +public suspend fun File.mediaType(mediaTypeHint: String? = null): MediaType = + throw NotImplementedError() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt index 69eb194f93..4a98bb7b3d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt @@ -11,14 +11,11 @@ package org.readium.r2.shared.util.mediatype import android.content.ContentResolver import android.net.Uri -import android.provider.MediaStore -import android.webkit.MimeTypeMap +import android.os.Parcelable import java.io.File import java.nio.charset.Charset import java.util.* -import org.readium.r2.shared.BuildConfig.DEBUG -import org.readium.r2.shared.extensions.queryProjection -import org.readium.r2.shared.extensions.tryOrNull +import kotlinx.parcelize.Parcelize /** * Represents a document format, identified by a unique RFC 6838 media type. @@ -33,68 +30,16 @@ import org.readium.r2.shared.extensions.tryOrNull * * Specification: https://tools.ietf.org/html/rfc6838 * - * @param string String representation for this media type. - * @param name A human readable name identifying the media type, which may be presented to the user. - * @param fileExtension The default file extension to use for this media type. + * @param type The type component, e.g. `application` in `application/epub+zip`. + * @param subtype The subtype component, e.g. `epub+zip` in `application/epub+zip`. + * @param parameters The parameters in the media type, such as `charset=utf-8`. */ -class MediaType( - string: String, - val name: String? = null, - val fileExtension: String? = null -) { - - /** The type component, e.g. `application` in `application/epub+zip`. */ - val type: String - - /** The subtype component, e.g. `epub+zip` in `application/epub+zip`. */ - val subtype: String - - /** The parameters in the media type, such as `charset=utf-8`. */ - val parameters: Map<String, String> - - init { - if (string.isEmpty()) { - throw IllegalArgumentException("Invalid media type: $string") - } - - // Grammar: https://tools.ietf.org/html/rfc2045#section-5.1 - val components = string.split(";") - .map { it.trim() } - val types = components[0].split("/") - if (types.size != 2) { - throw IllegalArgumentException("Invalid media type: $string") - } - - // > Both top-level type and subtype names are case-insensitive. - this.type = types[0].lowercase(Locale.ROOT) - this.subtype = types[1].lowercase(Locale.ROOT) - - // > Parameter names are case-insensitive and no meaning is attached to the order in which - // > they appear. - val parameters = components.drop(1) - .map { it.split("=") } - .filter { it.size == 2 } - .associate { Pair(it[0].lowercase(Locale.ROOT), it[1]) } - .toMutableMap() - - // For now, we only support case-insensitive `charset`. - // - // > Parameter values might or might not be case-sensitive, depending on the semantics of - // > the parameter name. - // > https://tools.ietf.org/html/rfc2616#section-3.7 - // - // > The character set names may be up to 40 characters taken from the printable characters - // > of US-ASCII. However, no distinction is made between use of upper and lower case - // > letters. - // > https://www.iana.org/assignments/character-sets/character-sets.xhtml - parameters["charset"]?.let { - parameters["charset"] = - (try { Charset.forName(it).name() } catch (e: Exception) { it }) - .uppercase(Locale.ROOT) - } - - this.parameters = parameters - } +@Parcelize +public class MediaType private constructor( + public val type: String, + public val subtype: String, + public val parameters: Map<String, String> +) : Parcelable { /** * Structured syntax suffix, e.g. `+zip` in `application/epub+zip`. @@ -102,7 +47,7 @@ class MediaType( * Gives a hint on the underlying structure of this media type. * See. https://tools.ietf.org/html/rfc6838#section-4.2.8 */ - val structuredSyntaxSuffix: String? get() { + public val structuredSyntaxSuffix: String? get() { val parts = subtype.split("+") return if (parts.size > 1) "+${parts.last()}" else null } @@ -110,7 +55,7 @@ class MediaType( /** * Encoding as declared in the `charset` parameter, if there's any. */ - val charset: Charset? get() = + public val charset: Charset? get() = parameters["charset"]?.let { Charset.forName(it) } /** @@ -122,8 +67,12 @@ class MediaType( * * Non-significant parameters are also discarded. */ - suspend fun canonicalMediaType(): MediaType = - of(mediaType = toString()) ?: this + @Deprecated( + "Use FormatRegistry.canonicalize() instead", + replaceWith = ReplaceWith("formatRegistry.canonicalize(this)"), + level = DeprecationLevel.ERROR + ) + public fun canonicalMediaType(): MediaType = TODO() /** The string representation of this media type. */ override fun toString(): String { @@ -167,7 +116,7 @@ class MediaType( * - Wildcards are supported, meaning that `image/*` contains `image/png` and `*/*` contains * everything. */ - fun contains(other: MediaType?): Boolean { + public fun contains(other: MediaType?): Boolean { if (other == null || (type != "*" && type != other.type) || (subtype != "*" && subtype != other.subtype)) { return false } @@ -179,8 +128,8 @@ class MediaType( /** * Returns whether the given [other] media type is included in this media type. */ - fun contains(other: String?): Boolean { - val mediaType = other?.let { parse(it) } + public fun contains(other: String?): Boolean { + val mediaType = other?.let { MediaType(it) } ?: return false return contains(mediaType) @@ -193,145 +142,224 @@ class MediaType( * For example, `text/html` matches `text/html;charset=utf-8`, but `text/html;charset=ascii` * doesn't. This is basically like `contains`, but working in both direction. */ - fun matches(other: MediaType?): Boolean = + public fun matches(other: MediaType?): Boolean = contains(other) || (other?.contains(this) == true) /** * Returns whether this media type and `other` are the same, ignoring parameters that are not * in both media types. */ - fun matches(other: String?): Boolean = - matches(other?.let { parse(it) }) + public fun matches(other: String?): Boolean = + matches(other?.let { MediaType(it) }) /** * Returns whether this media type matches any of the `others` media types. */ - fun matchesAny(vararg others: MediaType?): Boolean = + public fun matchesAny(vararg others: MediaType?): Boolean = others.any { matches(it) } /** * Returns whether this media type matches any of the `others` media types. */ - fun matchesAny(vararg others: String?): Boolean = + public fun matchesAny(vararg others: String?): Boolean = others.any { matches(it) } /** Returns whether this media type is structured as a ZIP archive. */ - val isZip: Boolean get() = + public val isZip: Boolean get() = matchesAny(ZIP, LCP_PROTECTED_AUDIOBOOK, LCP_PROTECTED_PDF) || structuredSyntaxSuffix == "+zip" /** Returns whether this media type is structured as a JSON file. */ - val isJson: Boolean get() = + public val isJson: Boolean get() = matches(JSON) || structuredSyntaxSuffix == "+json" /** Returns whether this media type is of an OPDS feed. */ - val isOpds: Boolean get() = + public val isOpds: Boolean get() = matchesAny(OPDS1, OPDS1_ENTRY, OPDS2, OPDS2_PUBLICATION, OPDS_AUTHENTICATION) /** Returns whether this media type is of an HTML document. */ - val isHtml: Boolean get() = + public val isHtml: Boolean get() = matchesAny(HTML, XHTML) /** Returns whether this media type is of a bitmap image, so excluding vectorial formats. */ - val isBitmap: Boolean get() = + public val isBitmap: Boolean get() = matchesAny(BMP, GIF, JPEG, PNG, TIFF, WEBP) /** Returns whether this media type is of an audio clip. */ - val isAudio: Boolean get() = + public val isAudio: Boolean get() = type == "audio" /** Returns whether this media type is of a video clip. */ - val isVideo: Boolean get() = + public val isVideo: Boolean get() = type == "video" /** Returns whether this media type is of a Readium Web Publication Manifest. */ - val isRwpm: Boolean get() = + public val isRwpm: Boolean get() = matchesAny(READIUM_AUDIOBOOK_MANIFEST, DIVINA_MANIFEST, READIUM_WEBPUB_MANIFEST) - /** Returns whether this media type is of a publication file. */ - val isPublication: Boolean get() = matchesAny( - READIUM_AUDIOBOOK, READIUM_AUDIOBOOK_MANIFEST, CBZ, DIVINA, DIVINA_MANIFEST, EPUB, LCP_PROTECTED_AUDIOBOOK, - LCP_PROTECTED_PDF, LPF, PDF, W3C_WPUB_MANIFEST, READIUM_WEBPUB, READIUM_WEBPUB_MANIFEST, ZAB + public val isRpf: Boolean get() = matchesAny( + READIUM_WEBPUB, + READIUM_AUDIOBOOK, + DIVINA, + LCP_PROTECTED_PDF, + LCP_PROTECTED_AUDIOBOOK ) - @Deprecated("Format and MediaType got merged together", replaceWith = ReplaceWith(""), level = DeprecationLevel.ERROR) - val mediaType: MediaType - get() = this + /** Returns whether this media type is of a publication file. */ + public val isPublication: Boolean get() = + matchesAny(CBZ, EPUB, LPF, PDF, W3C_WPUB_MANIFEST, ZAB) || isRwpm || isRpf + + @Suppress("RedundantNullableReturnType") + @Deprecated( + message = "The file extension is now in `Format`, which you can sniff using an `AssetRetriever`", + level = DeprecationLevel.ERROR + ) + public val fileExtension: String? get() = + throw NotImplementedError() - companion object { + public companion object { /** * Creates a [MediaType] from its RFC 6838 string representation. - * - * @param name A human readable name identifying the media type, which may be presented to the user. - * @param fileExtension The default file extension to use for this media type. */ - fun parse(string: String, name: String? = null, fileExtension: String? = null): MediaType? = - tryOrNull { MediaType(string = string, name = name, fileExtension = fileExtension) } + public operator fun invoke(string: String): MediaType? { + if (string.isEmpty()) { + return null + } + + // Grammar: https://tools.ietf.org/html/rfc2045#section-5.1 + val components = string.split(";") + .map { it.trim() } + val types = components[0].split("/") + if (types.size != 2) { + return null + } + + // > Both top-level type and subtype names are case-insensitive. + val type = types[0].lowercase(Locale.ROOT) + val subtype = types[1].lowercase(Locale.ROOT) + + // > Parameter names are case-insensitive and no meaning is attached to the order in which + // > they appear. + val parameters = components.drop(1) + .map { it.split("=") } + .filter { it.size == 2 } + .associate { Pair(it[0].lowercase(Locale.ROOT), it[1]) } + .toMutableMap() + + // For now, we only support case-insensitive `charset`. + // + // > Parameter values might or might not be case-sensitive, depending on the semantics of + // > the parameter name. + // > https://tools.ietf.org/html/rfc2616#section-3.7 + // + // > The character set names may be up to 40 characters taken from the printable characters + // > of US-ASCII. However, no distinction is made between use of upper and lower case + // > letters. + // > https://www.iana.org/assignments/character-sets/character-sets.xhtml + parameters["charset"]?.let { + parameters["charset"] = + (try { Charset.forName(it).name() } catch (e: Exception) { it }) + .uppercase(Locale.ROOT) + } + + return MediaType( + type = type, + subtype = subtype, + parameters = parameters + ) + } + + @Suppress("UNUSED_PARAMETER") + @Deprecated( + "Use `MediaType(string)` instead", + replaceWith = ReplaceWith("MediaType(string)"), + level = DeprecationLevel.ERROR + ) + public fun parse(string: String, name: String? = null, fileExtension: String? = null): MediaType? = + MediaType(string) // Known Media Types // // Reading apps are welcome to extend the static constants with additional media types. - val AAC = MediaType("audio/aac", fileExtension = "aac") - val ACSM = MediaType("application/vnd.adobe.adept+xml", name = "Adobe Content Server Message", fileExtension = "acsm") - val AIFF = MediaType("audio/aiff", fileExtension = "aiff") - val AVI = MediaType("video/x-msvideo", fileExtension = "avi") - val AVIF = MediaType("image/avif", fileExtension = "avif") - val BINARY = MediaType("application/octet-stream") - val BMP = MediaType("image/bmp", fileExtension = "bmp") - val CBZ = MediaType("application/vnd.comicbook+zip", name = "Comic Book Archive", fileExtension = "cbz") - val CSS = MediaType("text/css", fileExtension = "css") - val DIVINA = MediaType("application/divina+zip", name = "Digital Visual Narratives", fileExtension = "divina") - val DIVINA_MANIFEST = MediaType("application/divina+json", name = "Digital Visual Narratives", fileExtension = "json") - val EPUB = MediaType("application/epub+zip", name = "EPUB", fileExtension = "epub") - val GIF = MediaType("image/gif", fileExtension = "gif") - val GZ = MediaType("application/gzip", fileExtension = "gz") - val HTML = MediaType("text/html", fileExtension = "html") - val JAVASCRIPT = MediaType("text/javascript", fileExtension = "js") - val JPEG = MediaType("image/jpeg", fileExtension = "jpeg") - val JSON = MediaType("application/json") - val JSON_PROBLEM_DETAILS = MediaType("application/problem+json", name = "HTTP Problem Details", fileExtension = "json") - val JXL = MediaType("image/jxl", fileExtension = "jxl") - val LCP_LICENSE_DOCUMENT = MediaType("application/vnd.readium.lcp.license.v1.0+json", name = "LCP License", fileExtension = "lcpl") - val LCP_PROTECTED_AUDIOBOOK = MediaType("application/audiobook+lcp", name = "LCP Protected Audiobook", fileExtension = "lcpa") - val LCP_PROTECTED_PDF = MediaType("application/pdf+lcp", name = "LCP Protected PDF", fileExtension = "lcpdf") - val LCP_STATUS_DOCUMENT = MediaType("application/vnd.readium.license.status.v1.0+json") - val LPF = MediaType("application/lpf+zip", fileExtension = "lpf") - val MP3 = MediaType("audio/mpeg", fileExtension = "mp3") - val MPEG = MediaType("video/mpeg", fileExtension = "mpeg") - val NCX = MediaType("application/x-dtbncx+xml", fileExtension = "ncx") - val OGG = MediaType("audio/ogg", fileExtension = "oga") - val OGV = MediaType("video/ogg", fileExtension = "ogv") - val OPDS1 = MediaType("application/atom+xml;profile=opds-catalog") - val OPDS1_ENTRY = MediaType("application/atom+xml;type=entry;profile=opds-catalog") - val OPDS2 = MediaType("application/opds+json") - val OPDS2_PUBLICATION = MediaType("application/opds-publication+json") - val OPDS_AUTHENTICATION = MediaType("application/opds-authentication+json") - val OPUS = MediaType("audio/opus", fileExtension = "opus") - val OTF = MediaType("font/otf", fileExtension = "otf") - val PDF = MediaType("application/pdf", name = "PDF", fileExtension = "pdf") - val PNG = MediaType("image/png", fileExtension = "png") - val READIUM_AUDIOBOOK = MediaType("application/audiobook+zip", name = "Readium Audiobook", fileExtension = "audiobook") - val READIUM_AUDIOBOOK_MANIFEST = MediaType("application/audiobook+json", name = "Readium Audiobook", fileExtension = "json") - val READIUM_WEBPUB = MediaType("application/webpub+zip", name = "Readium Web Publication", fileExtension = "webpub") - val READIUM_WEBPUB_MANIFEST = MediaType("application/webpub+json", name = "Readium Web Publication", fileExtension = "json") - val SMIL = MediaType("application/smil+xml", fileExtension = "smil") - val SVG = MediaType("image/svg+xml", fileExtension = "svg") - val TEXT = MediaType("text/plain", fileExtension = "txt") - val TIFF = MediaType("image/tiff", fileExtension = "tiff") - val TTF = MediaType("font/ttf", fileExtension = "ttf") - val W3C_WPUB_MANIFEST = MediaType("application/x.readium.w3c.wpub+json", name = "Web Publication", fileExtension = "json") // non-existent - val WAV = MediaType("audio/wav", fileExtension = "wav") - val WEBM_AUDIO = MediaType("audio/webm", fileExtension = "webm") - val WEBM_VIDEO = MediaType("video/webm", fileExtension = "webm") - val WEBP = MediaType("image/webp", fileExtension = "webp") - val WOFF = MediaType("font/woff", fileExtension = "woff") - val WOFF2 = MediaType("font/woff2", fileExtension = "woff2") - val XHTML = MediaType("application/xhtml+xml", fileExtension = "xhtml") - val XML = MediaType("application/xml", fileExtension = "xml") - val ZAB = MediaType("application/x.readium.zab+zip", name = "Zipped Audio Book", fileExtension = "zab") // non-existent - val ZIP = MediaType("application/zip", fileExtension = "zip") + public val AAC: MediaType = MediaType("audio/aac")!! + public val ACSM: MediaType = MediaType("application/vnd.adobe.adept+xml")!! + public val AIFF: MediaType = MediaType("audio/aiff")!! + public val AVI: MediaType = MediaType("video/x-msvideo")!! + public val AVIF: MediaType = MediaType("image/avif")!! + public val BINARY: MediaType = MediaType("application/octet-stream")!! + public val BMP: MediaType = MediaType("image/bmp")!! + public val CBR: MediaType = MediaType("application/vnd.comicbook-rar")!! + public val CBZ: MediaType = MediaType("application/vnd.comicbook+zip")!! + public val CSS: MediaType = MediaType("text/css")!! + public val DIVINA: MediaType = MediaType("application/divina+zip")!! + public val DIVINA_MANIFEST: MediaType = MediaType("application/divina+json")!! + public val EPUB: MediaType = MediaType("application/epub+zip")!! + public val FLAC: MediaType = MediaType("audio/flac")!! + public val GIF: MediaType = MediaType("image/gif")!! + public val GZ: MediaType = MediaType("application/gzip")!! + public val HTML: MediaType = MediaType("text/html")!! + public val JAVASCRIPT: MediaType = MediaType("text/javascript")!! + public val JPEG: MediaType = MediaType("image/jpeg")!! + public val JSON: MediaType = MediaType("application/json")!! + public val JSON_PROBLEM_DETAILS: MediaType = MediaType("application/problem+json")!! + public val JXL: MediaType = MediaType("image/jxl")!! + public val LCP_LICENSE_DOCUMENT: MediaType = MediaType( + "application/vnd.readium.lcp.license.v1.0+json" + )!! + public val LCP_PROTECTED_AUDIOBOOK: MediaType = MediaType("application/audiobook+lcp")!! + public val LCP_PROTECTED_PDF: MediaType = MediaType("application/pdf+lcp")!! + public val LCP_STATUS_DOCUMENT: MediaType = MediaType( + "application/vnd.readium.license.status.v1.0+json" + )!! + public val LPF: MediaType = MediaType("application/lpf+zip")!! + public val MP3: MediaType = MediaType("audio/mpeg")!! + public val MP4: MediaType = MediaType("audio/mp4")!! + public val MPEG: MediaType = MediaType("video/mpeg")!! + public val NCX: MediaType = MediaType("application/x-dtbncx+xml")!! + public val OGG: MediaType = MediaType("audio/ogg")!! + public val OGV: MediaType = MediaType("video/ogg")!! + public val OPDS1: MediaType = MediaType("application/atom+xml;profile=opds-catalog")!! + public val OPDS1_NAVIGATION_FEED: MediaType = MediaType( + "application/atom+xml;profile=opds-catalog;kind=navigation" + )!! + public val OPDS1_ACQUISITION_FEED: MediaType = MediaType( + "application/atom+xml;profile=opds-catalog;kind=acquisition" + )!! + public val OPDS1_ENTRY: MediaType = MediaType( + "application/atom+xml;type=entry;profile=opds-catalog" + )!! + public val OPDS2: MediaType = MediaType("application/opds+json")!! + public val OPDS2_PUBLICATION: MediaType = MediaType("application/opds-publication+json")!! + public val OPDS_AUTHENTICATION: MediaType = MediaType( + "application/opds-authentication+json" + )!! + public val OPUS: MediaType = MediaType("audio/opus")!! + public val OTF: MediaType = MediaType("font/otf")!! + public val PDF: MediaType = MediaType("application/pdf")!! + public val PNG: MediaType = MediaType("image/png")!! + public val RAR: MediaType = MediaType("application/vnd.rar")!! + public val READIUM_AUDIOBOOK: MediaType = MediaType("application/audiobook+zip")!! + public val READIUM_AUDIOBOOK_MANIFEST: MediaType = MediaType("application/audiobook+json")!! + public val READIUM_WEBPUB: MediaType = MediaType("application/webpub+zip")!! + public val READIUM_WEBPUB_MANIFEST: MediaType = MediaType("application/webpub+json")!! + public val SMIL: MediaType = MediaType("application/smil+xml")!! + public val SVG: MediaType = MediaType("image/svg+xml")!! + public val TEXT: MediaType = MediaType("text/plain")!! + public val TIFF: MediaType = MediaType("image/tiff")!! + public val TTF: MediaType = MediaType("font/ttf")!! + public val W3C_WPUB_MANIFEST: MediaType = MediaType("application/x.readium.w3c.wpub+json")!! // non-existent + public val WAV: MediaType = MediaType("audio/wav")!! + public val WEBM_AUDIO: MediaType = MediaType("audio/webm")!! + public val WEBM_VIDEO: MediaType = MediaType("video/webm")!! + public val WEBP: MediaType = MediaType("image/webp")!! + public val WOFF: MediaType = MediaType("font/woff")!! + public val WOFF2: MediaType = MediaType("font/woff2")!! + public val XHTML: MediaType = MediaType("application/xhtml+xml")!! + public val XML: MediaType = MediaType("application/xml")!! + public val ZAB: MediaType = MediaType("application/x.readium.zab+zip")!! // non-existent + public val ZIP: MediaType = MediaType("application/zip")!! // Sniffing @@ -340,286 +368,297 @@ class MediaType( * You can register additional sniffers globally by modifying this list. * The sniffers order is important, because some formats are subsets of other formats. */ - val sniffers = Sniffers.all.toMutableList() + @Deprecated(message = "Use FormatRegistry instead", level = DeprecationLevel.ERROR) + public val sniffers: MutableList<Any> = mutableListOf() + + @Deprecated( + message = "Create the `MediaType` directly instead", + replaceWith = ReplaceWith("MediaType(mediaType)"), + level = DeprecationLevel.ERROR + ) + public fun of(mediaType: String): MediaType? = MediaType(mediaType) /** * Resolves a format from a single file extension and media type hint, without checking the actual * content. */ - suspend fun of( + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + @Suppress("UNUSED_PARAMETER") + public fun of( mediaType: String? = null, - fileExtension: String? = null, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtension: String? = null ): MediaType? { - if (DEBUG && mediaType?.startsWith("/") == true) { - throw IllegalArgumentException("The provided media type is incorrect: $mediaType. To pass a file path, you must wrap it in a File().") - } - return of(content = null, mediaTypes = listOfNotNull(mediaType), fileExtensions = listOfNotNull(fileExtension), sniffers = sniffers) + TODO() } /** * Resolves a format from file extension and media type hints, without checking the actual * content. */ - suspend fun of( + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + @Suppress("UNUSED_PARAMETER") + public fun of( mediaTypes: List<String>, - fileExtensions: List<String>, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtensions: List<String> ): MediaType? { - return of(content = null, mediaTypes = mediaTypes, fileExtensions = fileExtensions, sniffers = sniffers) + TODO() } /** * Resolves a format from a local file path. */ - suspend fun ofFile( + @Suppress("UNUSED_PARAMETER") + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + public fun ofFile( file: File, mediaType: String? = null, - fileExtension: String? = null, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtension: String? = null ): MediaType? { - return ofFile(file, mediaTypes = listOfNotNull(mediaType), fileExtensions = listOfNotNull(fileExtension), sniffers = sniffers) + TODO() } /** * Resolves a format from a local file path. */ - suspend fun ofFile( + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + @Suppress("UNUSED_PARAMETER") + public fun ofFile( file: File, mediaTypes: List<String>, - fileExtensions: List<String>, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtensions: List<String> ): MediaType? { - return of(content = SnifferFileContent(file), mediaTypes = mediaTypes, fileExtensions = listOf(file.extension) + fileExtensions, sniffers = sniffers) + TODO() } /** * Resolves a format from a local file path. */ - suspend fun ofFile( + @Suppress("UNUSED_PARAMETER") + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + public fun ofFile( path: String, mediaType: String? = null, - fileExtension: String? = null, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtension: String? = null ): MediaType? { - return ofFile(File(path), mediaType = mediaType, fileExtension = fileExtension, sniffers = sniffers) + TODO() } /** * Resolves a format from a local file path. */ - suspend fun ofFile( + @Suppress("UNUSED_PARAMETER") + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + public fun ofFile( path: String, mediaTypes: List<String>, - fileExtensions: List<String>, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtensions: List<String> ): MediaType? { - return ofFile(File(path), mediaTypes = mediaTypes, fileExtensions = fileExtensions, sniffers = sniffers) + TODO() } /** * Resolves a format from bytes, e.g. from an HTTP response. */ - suspend fun ofBytes( + @Suppress("UNUSED_PARAMETER") + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + public fun ofBytes( bytes: () -> ByteArray, mediaType: String? = null, - fileExtension: String? = null, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtension: String? = null ): MediaType? { - return ofBytes(bytes, mediaTypes = listOfNotNull(mediaType), fileExtensions = listOfNotNull(fileExtension), sniffers = sniffers) + TODO() } /** * Resolves a format from bytes, e.g. from an HTTP response. */ - suspend fun ofBytes( + @Suppress("UNUSED_PARAMETER") + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + public fun ofBytes( bytes: () -> ByteArray, mediaTypes: List<String>, - fileExtensions: List<String>, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtensions: List<String> ): MediaType? { - return of(content = SnifferBytesContent(bytes), mediaTypes = mediaTypes, fileExtensions = fileExtensions, sniffers = sniffers) + TODO() } /** * Resolves a format from a content URI and a [ContentResolver]. * Accepts the following URI schemes: content, android.resource, file. */ - suspend fun ofUri( + @Suppress("UNUSED_PARAMETER") + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + public fun ofUri( uri: Uri, contentResolver: ContentResolver, mediaType: String? = null, - fileExtension: String? = null, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtension: String? = null ): MediaType? { - return ofUri(uri, contentResolver, mediaTypes = listOfNotNull(mediaType), fileExtensions = listOfNotNull(fileExtension), sniffers = sniffers) + TODO() } /** * Resolves a format from a content URI and a [ContentResolver]. * Accepts the following URI schemes: content, android.resource, file. */ - suspend fun ofUri( + @Suppress("UNUSED_PARAMETER") + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + public fun ofUri( uri: Uri, contentResolver: ContentResolver, mediaTypes: List<String>, - fileExtensions: List<String>, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtensions: List<String> ): MediaType? { - val allMediaTypes = mediaTypes.toMutableList() - val allFileExtensions = fileExtensions.toMutableList() - - MimeTypeMap.getFileExtensionFromUrl(uri.toString()).ifEmpty { null }?.let { - allFileExtensions.add(0, it) - } - - if (uri.scheme == ContentResolver.SCHEME_CONTENT) { - contentResolver.getType(uri) - ?.takeUnless { MediaType.BINARY.matches(it) } - ?.let { allMediaTypes.add(0, it) } - - contentResolver.queryProjection(uri, MediaStore.MediaColumns.DISPLAY_NAME)?.let { filename -> - allFileExtensions.add(0, File(filename).extension) - } - } - - val content = SnifferUriContent(uri = uri, contentResolver = contentResolver) - return of(content = content, mediaTypes = allMediaTypes, fileExtensions = allFileExtensions, sniffers = sniffers) - } - - /** - * Resolves a media type from a sniffer context. - * - * Sniffing a media type is done in two rounds, because we want to give an opportunity to all - * sniffers to return a [MediaType] quickly before inspecting the content itself: - * - Light Sniffing checks only the provided file extension or media type hints. - * - Heavy Sniffing reads the bytes to perform more advanced sniffing. - */ - private suspend fun of( - content: SnifferContent?, - mediaTypes: List<String>, - fileExtensions: List<String>, - sniffers: List<Sniffer> - ): MediaType? { - // Light sniffing with only media type hints - if (mediaTypes.isNotEmpty()) { - val context = SnifferContext(mediaTypes = mediaTypes) - for (sniffer in sniffers) { - val mediaType = sniffer(context) - if (mediaType != null) { - return mediaType - } - } - } - - // Light sniffing with both media type hints and file extensions - if (fileExtensions.isNotEmpty()) { - val context = SnifferContext(mediaTypes = mediaTypes, fileExtensions = fileExtensions) - for (sniffer in sniffers) { - val mediaType = sniffer(context) - if (mediaType != null) { - return mediaType - } - } - } - - // Heavy sniffing - if (content != null) { - val context = SnifferContext(content = content, mediaTypes = mediaTypes, fileExtensions = fileExtensions) - for (sniffer in sniffers) { - val mediaType = sniffer(context) - if (mediaType != null) { - return mediaType - } - } - } - - // Falls back on the system-wide registered media types using [MimeTypeMap]. - // Note: This is done after the heavy sniffing of the provided [sniffers], because - // otherwise it will detect JSON, XML or ZIP formats before we have a chance of sniffing - // their content (for example, for RWPM). - val context = SnifferContext(content = content, mediaTypes = mediaTypes, fileExtensions = fileExtensions) - Sniffers.system(context)?.let { return it } - - // If nothing else worked, we try to parse the first valid media type hint. - for (mediaType in mediaTypes) { - parse(mediaType)?.let { return it } - } - - return null + TODO() } /* Deprecated */ - @Deprecated("Use [READIUM_AUDIOBOOK] instead", ReplaceWith("MediaType.READIUM_AUDIOBOOK"), level = DeprecationLevel.ERROR) - val AUDIOBOOK: MediaType get() = READIUM_AUDIOBOOK - @Deprecated("Use [READIUM_AUDIOBOOK_MANIFEST] instead", ReplaceWith("MediaType.READIUM_AUDIOBOOK_MANIFEST"), level = DeprecationLevel.ERROR) - val AUDIOBOOK_MANIFEST: MediaType get() = READIUM_AUDIOBOOK_MANIFEST - @Deprecated("Use [READIUM_WEBPUB] instead", ReplaceWith("MediaType.READIUM_WEBPUB"), level = DeprecationLevel.ERROR) - val WEBPUB: MediaType get() = READIUM_WEBPUB - @Deprecated("Use [READIUM_WEBPUB_MANIFEST] instead", ReplaceWith("MediaType.READIUM_WEBPUB_MANIFEST"), level = DeprecationLevel.ERROR) - val WEBPUB_MANIFEST: MediaType get() = READIUM_WEBPUB_MANIFEST - @Deprecated("Use [OPDS1] instead", ReplaceWith("MediaType.OPDS1"), level = DeprecationLevel.ERROR) - val OPDS1_FEED: MediaType get() = OPDS1 - @Deprecated("Use [OPDS2] instead", ReplaceWith("MediaType.OPDS2"), level = DeprecationLevel.ERROR) - val OPDS2_FEED: MediaType get() = OPDS2 - @Deprecated("Use [LCP_LICENSE_DOCUMENT] instead", ReplaceWith("MediaType.LCP_LICENSE_DOCUMENT"), level = DeprecationLevel.ERROR) - val LCP_LICENSE: MediaType get() = LCP_LICENSE_DOCUMENT + @Deprecated( + "Use [READIUM_AUDIOBOOK] instead", + ReplaceWith("MediaType.READIUM_AUDIOBOOK"), + level = DeprecationLevel.ERROR + ) + public val AUDIOBOOK: MediaType get() = READIUM_AUDIOBOOK + + @Deprecated( + "Use [READIUM_AUDIOBOOK_MANIFEST] instead", + ReplaceWith("MediaType.READIUM_AUDIOBOOK_MANIFEST"), + level = DeprecationLevel.ERROR + ) + public val AUDIOBOOK_MANIFEST: MediaType get() = READIUM_AUDIOBOOK_MANIFEST + + @Deprecated( + "Use [READIUM_WEBPUB] instead", + ReplaceWith("MediaType.READIUM_WEBPUB"), + level = DeprecationLevel.ERROR + ) + public val WEBPUB: MediaType get() = READIUM_WEBPUB + + @Deprecated( + "Use [READIUM_WEBPUB_MANIFEST] instead", + ReplaceWith("MediaType.READIUM_WEBPUB_MANIFEST"), + level = DeprecationLevel.ERROR + ) + public val WEBPUB_MANIFEST: MediaType get() = READIUM_WEBPUB_MANIFEST + + @Deprecated( + "Use [OPDS1] instead", + ReplaceWith("MediaType.OPDS1"), + level = DeprecationLevel.ERROR + ) + public val OPDS1_FEED: MediaType get() = OPDS1 + + @Deprecated( + "Use [OPDS2] instead", + ReplaceWith("MediaType.OPDS2"), + level = DeprecationLevel.ERROR + ) + public val OPDS2_FEED: MediaType get() = OPDS2 + + @Deprecated( + "Use [LCP_LICENSE_DOCUMENT] instead", + ReplaceWith("MediaType.LCP_LICENSE_DOCUMENT"), + level = DeprecationLevel.ERROR + ) + public val LCP_LICENSE: MediaType get() = LCP_LICENSE_DOCUMENT @Suppress("UNUSED_PARAMETER") - @Deprecated("Renamed to [ofFile()]", ReplaceWith("MediaType.ofFile(file, mediaType, fileExtension, sniffers)"), level = DeprecationLevel.ERROR) - fun of( + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + public fun of( file: File, mediaType: String? = null, - fileExtension: String? = null, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtension: String? = null ): MediaType? = null @Suppress("UNUSED_PARAMETER") - @Deprecated("Renamed to [ofFile()]", ReplaceWith("MediaType.ofFile(file, mediaTypes, fileExtensions, sniffers)"), level = DeprecationLevel.ERROR) - fun of( + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + public fun of( file: File, mediaTypes: List<String>, - fileExtensions: List<String>, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtensions: List<String> ): MediaType? = null @Suppress("UNUSED_PARAMETER") - @Deprecated("Renamed to [ofBytes()]", ReplaceWith("MediaType.ofBytes(bytes, mediaType, fileExtension, sniffers)"), level = DeprecationLevel.ERROR) - fun of( + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + public fun of( bytes: () -> ByteArray, mediaType: String? = null, - fileExtension: String? = null, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtension: String? = null ): MediaType? = null @Suppress("UNUSED_PARAMETER") - @Deprecated("Renamed to [ofBytes()]", ReplaceWith("MediaType.ofBytes(bytes, mediaTypes, fileExtensions, sniffers)"), level = DeprecationLevel.ERROR) - fun of( + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + public fun of( bytes: () -> ByteArray, mediaTypes: List<String>, - fileExtensions: List<String>, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtensions: List<String> ): MediaType? = null @Suppress("UNUSED_PARAMETER") - @Deprecated("Renamed to [ofUri()]", ReplaceWith("MediaType.ofUri(uri, contentResolver, mediaType, fileExtension, sniffers)"), level = DeprecationLevel.ERROR) - fun of( + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + public fun of( uri: Uri, contentResolver: ContentResolver, mediaType: String? = null, - fileExtension: String? = null, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtension: String? = null ): MediaType? = null @Suppress("UNUSED_PARAMETER") - @Deprecated("Renamed to [ofUri()]", ReplaceWith("MediaType.ofUri(uri, contentResolver, mediaTypes, fileExtensions, sniffers)"), level = DeprecationLevel.ERROR) - fun of( + @Deprecated( + message = "Use an `AssetRetriever` instead to retrieve the format of a file. See the migration guide.", + level = DeprecationLevel.ERROR + ) + public fun of( uri: Uri, contentResolver: ContentResolver, mediaTypes: List<String>, - fileExtensions: List<String>, - sniffers: List<Sniffer> = MediaType.sniffers + fileExtensions: List<String> ): MediaType? = null } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/Sniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/Sniffer.kt deleted file mode 100644 index ae58444d95..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/Sniffer.kt +++ /dev/null @@ -1,411 +0,0 @@ -/* - * Copyright 2020 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.mediatype - -import android.webkit.MimeTypeMap -import java.io.File -import java.net.URLConnection -import java.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.json.JSONObject -import org.readium.r2.shared.extensions.tryOrNull -import org.readium.r2.shared.publication.* - -/** - * Determines if the provided content matches a known media type. - * - * @param context Holds the file metadata and cached content, which are shared among the sniffers. - */ -typealias Sniffer = suspend (context: SnifferContext) -> MediaType? - -/** - * Default media type sniffers provided by Readium. - */ -object Sniffers { - - /** - * The default sniffers provided by Readium 2 to resolve a [MediaType]. - * The sniffers order is important, because some formats are subsets of other formats. - */ - val all: List<Sniffer> = listOf( - ::xhtml, ::html, ::opds, ::lcpLicense, ::bitmap, ::webpub, ::w3cWPUB, ::epub, ::lpf, ::archive, - ::pdf, ::json - ) - - /** - * Sniffs an XHTML document. - * - * Must precede the HTML sniffer. - */ - suspend fun xhtml(context: SnifferContext): MediaType? { - if (context.hasFileExtension("xht", "xhtml") || context.hasMediaType("application/xhtml+xml")) { - return MediaType.XHTML - } - context.contentAsXml()?.let { - if (it.name.lowercase(Locale.ROOT) == "html" && it.namespace.lowercase(Locale.ROOT).contains("xhtml")) { - return MediaType.XHTML - } - } - return null - } - - /** Sniffs an HTML document. */ - suspend fun html(context: SnifferContext): MediaType? { - if (context.hasFileExtension("htm", "html") || context.hasMediaType("text/html")) { - return MediaType.HTML - } - // [contentAsXml] will fail if the HTML is not a proper XML document, hence the doctype check. - if ( - context.contentAsXml()?.name?.lowercase(Locale.ROOT) == "html" || - context.contentAsString()?.trimStart()?.take(15)?.lowercase() == "<!doctype html>" - ) { - return MediaType.HTML - } - return null - } - - /** Sniffs an OPDS document. */ - suspend fun opds(context: SnifferContext): MediaType? { - // OPDS 1 - if (context.hasMediaType("application/atom+xml;type=entry;profile=opds-catalog")) { - return MediaType.OPDS1_ENTRY - } - if (context.hasMediaType("application/atom+xml;profile=opds-catalog")) { - return MediaType.OPDS1 - } - context.contentAsXml()?.let { xml -> - if (xml.namespace == "http://www.w3.org/2005/Atom") { - if (xml.name == "feed") { - return MediaType.OPDS1 - } else if (xml.name == "entry") { - return MediaType.OPDS1_ENTRY - } - } - } - - // OPDS 2 - if (context.hasMediaType("application/opds+json")) { - return MediaType.OPDS2 - } - if (context.hasMediaType("application/opds-publication+json")) { - return MediaType.OPDS2_PUBLICATION - } - context.contentAsRwpm()?.let { rwpm -> - if (rwpm.linkWithRel("self")?.mediaType?.matches("application/opds+json") == true) { - return MediaType.OPDS2 - } - if (rwpm.links.firstWithRelMatching { it.startsWith("http://opds-spec.org/acquisition") } != null) { - return MediaType.OPDS2_PUBLICATION - } - } - - // OPDS Authentication Document. - if (context.hasMediaType("application/opds-authentication+json") || context.hasMediaType("application/vnd.opds.authentication.v1.0+json")) { - return MediaType.OPDS_AUTHENTICATION - } - if (context.containsJsonKeys("id", "title", "authentication")) { - return MediaType.OPDS_AUTHENTICATION - } - - return null - } - - /** Sniffs an LCP License Document. */ - suspend fun lcpLicense(context: SnifferContext): MediaType? { - if (context.hasFileExtension("lcpl") || context.hasMediaType("application/vnd.readium.lcp.license.v1.0+json")) { - return MediaType.LCP_LICENSE_DOCUMENT - } - if (context.containsJsonKeys("id", "issued", "provider", "encryption")) { - return MediaType.LCP_LICENSE_DOCUMENT - } - return null - } - - /** Sniffs a bitmap image. */ - @Suppress("RedundantSuspendModifier") - suspend fun bitmap(context: SnifferContext): MediaType? { - if (context.hasFileExtension("avif") || context.hasMediaType("image/avif")) { - return MediaType.AVIF - } - if (context.hasFileExtension("bmp", "dib") || context.hasMediaType("image/bmp", "image/x-bmp")) { - return MediaType.BMP - } - if (context.hasFileExtension("gif") || context.hasMediaType("image/gif")) { - return MediaType.GIF - } - if (context.hasFileExtension("jpg", "jpeg", "jpe", "jif", "jfif", "jfi") || context.hasMediaType("image/jpeg")) { - return MediaType.JPEG - } - if (context.hasFileExtension("jxl") || context.hasMediaType("image/jxl")) { - return MediaType.JXL - } - if (context.hasFileExtension("png") || context.hasMediaType("image/png")) { - return MediaType.PNG - } - if (context.hasFileExtension("tiff", "tif") || context.hasMediaType("image/tiff", "image/tiff-fx")) { - return MediaType.TIFF - } - if (context.hasFileExtension("webp") || context.hasMediaType("image/webp")) { - return MediaType.WEBP - } - return null - } - - /** Sniffs a Readium Web Publication, protected or not by LCP. */ - suspend fun webpub(context: SnifferContext): MediaType? { - if (context.hasFileExtension("audiobook") || context.hasMediaType("application/audiobook+zip")) { - return MediaType.READIUM_AUDIOBOOK - } - if (context.hasMediaType("application/audiobook+json")) { - return MediaType.READIUM_AUDIOBOOK_MANIFEST - } - - if (context.hasFileExtension("divina") || context.hasMediaType("application/divina+zip")) { - return MediaType.DIVINA - } - if (context.hasMediaType("application/divina+json")) { - return MediaType.DIVINA_MANIFEST - } - - if (context.hasFileExtension("webpub") || context.hasMediaType("application/webpub+zip")) { - return MediaType.READIUM_WEBPUB - } - if (context.hasMediaType("application/webpub+json")) { - return MediaType.READIUM_WEBPUB_MANIFEST - } - - if (context.hasFileExtension("lcpa") || context.hasMediaType("application/audiobook+lcp")) { - return MediaType.LCP_PROTECTED_AUDIOBOOK - } - if (context.hasFileExtension("lcpdf") || context.hasMediaType("application/pdf+lcp")) { - return MediaType.LCP_PROTECTED_PDF - } - - // Reads a RWPM, either from a manifest.json file, or from a manifest.json archive entry, if - // the file is an archive. - var isManifest = true - val manifest: Manifest? = - try { - // manifest.json - context.contentAsRwpm() - // Archive package - ?: context.readArchiveEntryAt("manifest.json") - ?.let { Manifest.fromJSON(JSONObject(String(it))) } - ?.also { isManifest = false } - } catch (e: Exception) { - null - } - - if (manifest != null) { - val isLcpProtected = context.containsArchiveEntryAt("license.lcpl") - - if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { - return if (isManifest) MediaType.READIUM_AUDIOBOOK_MANIFEST - else (if (isLcpProtected) MediaType.LCP_PROTECTED_AUDIOBOOK else MediaType.READIUM_AUDIOBOOK) - } - if (manifest.conformsTo(Publication.Profile.DIVINA)) { - return if (isManifest) MediaType.DIVINA_MANIFEST else MediaType.DIVINA - } - if (isLcpProtected && manifest.conformsTo(Publication.Profile.PDF)) { - return MediaType.LCP_PROTECTED_PDF - } - if (manifest.linkWithRel("self")?.mediaType?.matches("application/webpub+json") == true) { - return if (isManifest) MediaType.READIUM_WEBPUB_MANIFEST else MediaType.READIUM_WEBPUB - } - } - - return null - } - - /** Sniffs a W3C Web Publication Manifest. */ - suspend fun w3cWPUB(context: SnifferContext): MediaType? { - // Somehow, [JSONObject] can't access JSON-LD keys such as `@context`. - val content = context.contentAsString() ?: "" - if (content.contains("@context") && content.contains("https://www.w3.org/ns/wp-context")) { - return MediaType.W3C_WPUB_MANIFEST - } - - return null - } - - /** - * Sniffs an EPUB publication. - * - * Reference: https://www.w3.org/publishing/epub3/epub-ocf.html#sec-zip-container-mime - */ - suspend fun epub(context: SnifferContext): MediaType? { - if (context.hasFileExtension("epub") || context.hasMediaType("application/epub+zip")) { - return MediaType.EPUB - } - - val mimetype = context.readArchiveEntryAt("mimetype") - ?.let { String(it, charset = Charsets.US_ASCII).trim() } - if (mimetype == "application/epub+zip") { - return MediaType.EPUB - } - - return null - } - - /** - * Sniffs a Lightweight Packaging Format (LPF). - * - * References: - * - https://www.w3.org/TR/lpf/ - * - https://www.w3.org/TR/pub-manifest/ - */ - suspend fun lpf(context: SnifferContext): MediaType? { - if (context.hasFileExtension("lpf") || context.hasMediaType("application/lpf+zip")) { - return MediaType.LPF - } - if (context.containsArchiveEntryAt("index.html")) { - return MediaType.LPF - } - - // Somehow, [JSONObject] can't access JSON-LD keys such as `@context`. - context.readArchiveEntryAt("publication.json") - ?.let { String(it) } - ?.let { manifest -> - if (manifest.contains("@context") && manifest.contains("https://www.w3.org/ns/pub-context")) { - return MediaType.LPF - } - } - - return null - } - - /** - * Authorized extensions for resources in a CBZ archive. - * Reference: https://wiki.mobileread.com/wiki/CBR_and_CBZ - */ - private val CBZ_EXTENSIONS = listOf( - // bitmap - "bmp", "dib", "gif", "jif", "jfi", "jfif", "jpg", "jpeg", "png", "tif", "tiff", "webp", - // metadata - "acbf", "xml" - ) - - /** - * Authorized extensions for resources in a ZAB archive (Zipped Audio Book). - */ - private val ZAB_EXTENSIONS = listOf( - // audio - "aac", "aiff", "alac", "flac", "m4a", "m4b", "mp3", "ogg", "oga", "mogg", "opus", "wav", "webm", - // playlist - "asx", "bio", "m3u", "m3u8", "pla", "pls", "smil", "vlc", "wpl", "xspf", "zpl" - ) - - /** - * Sniffs a simple Archive-based format, like Comic Book Archive or Zipped Audio Book. - * - * Reference: https://wiki.mobileread.com/wiki/CBR_and_CBZ - */ - suspend fun archive(context: SnifferContext): MediaType? { - if (context.hasFileExtension("cbz") || context.hasMediaType("application/vnd.comicbook+zip", "application/x-cbz", "application/x-cbr")) { - return MediaType.CBZ - } - if (context.hasFileExtension("zab")) { - return MediaType.ZAB - } - - if (context.contentAsArchive() != null) { - fun isIgnored(file: File): Boolean = - file.name.startsWith(".") || file.name == "Thumbs.db" - - suspend fun archiveContainsOnlyExtensions(fileExtensions: List<String>): Boolean = - context.archiveEntriesAllSatisfy { entry -> - val file = File(entry.path) - isIgnored(file) || fileExtensions.contains(file.extension.lowercase(Locale.ROOT)) - } - - if (archiveContainsOnlyExtensions(CBZ_EXTENSIONS)) { - return MediaType.CBZ - } - if (archiveContainsOnlyExtensions(ZAB_EXTENSIONS)) { - return MediaType.ZAB - } - } - - return null - } - - /** - * Sniffs a PDF document. - * - * Reference: https://www.loc.gov/preservation/digital/formats/fdd/fdd000123.shtml - */ - suspend fun pdf(context: SnifferContext): MediaType? { - if (context.hasFileExtension("pdf") || context.hasMediaType("application/pdf")) { - return MediaType.PDF - } - if (context.read(0L until 5L)?.toString(Charsets.UTF_8) == "%PDF-") { - return MediaType.PDF - } - - return null - } - - /** Sniffs a JSON document. */ - suspend fun json(context: SnifferContext): MediaType? { - if (context.hasMediaType("application/problem+json")) { - return MediaType.JSON_PROBLEM_DETAILS - } - if (context.hasMediaType("application/json")) { - return MediaType.JSON - } - if (context.contentAsJson() != null) { - return MediaType.JSON - } - return null - } - - /** - * Sniffs the system-wide registered media types using [MimeTypeMap] and - * [URLConnection.guessContentTypeFromStream]. - */ - suspend fun system(context: SnifferContext): MediaType? { - val mimetypes = tryOrNull { MimeTypeMap.getSingleton() } - ?: return null - - fun sniffExtension(extension: String): MediaType? { - val type = mimetypes.getMimeTypeFromExtension(extension) - ?: return null - val preferredExtension = mimetypes.getExtensionFromMimeType(type) - ?: return null - return MediaType.parse(type, fileExtension = preferredExtension) - } - - fun sniffType(type: String): MediaType? { - val extension = mimetypes.getExtensionFromMimeType(type) - ?: return null - val preferredType = mimetypes.getMimeTypeFromExtension(extension) - ?: return null - return MediaType.parse(preferredType, fileExtension = extension) - } - - for (mediaType in context.mediaTypes) { - return sniffType(mediaType.toString()) ?: continue - } - - for (extension in context.fileExtensions) { - return sniffExtension(extension) ?: continue - } - - return withContext(Dispatchers.IO) { - context.stream() - ?.let { URLConnection.guessContentTypeFromStream(it) } - ?.let { sniffType(it) } - } - } -} - -/** - * Finds the first [Link] having a relation matching the given [predicate]. - */ -private fun List<Link>.firstWithRelMatching(predicate: (String) -> Boolean): Link? = - firstOrNull { it.rels.any(predicate) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SnifferContent.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SnifferContent.kt deleted file mode 100644 index c210082c42..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SnifferContent.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2020 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.mediatype - -import android.content.ContentResolver -import android.net.Uri -import java.io.ByteArrayInputStream -import java.io.File -import java.io.InputStream -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber - -/** Provides an access to a file's content to sniff its format. */ -internal interface SnifferContent { - - /** Reads the whole content as raw bytes. */ - suspend fun read(): ByteArray? - - /** - * Raw bytes stream of the content. - * - * A byte stream can be useful when sniffers only need to read a few bytes at the beginning of - * the file. - */ - suspend fun stream(): InputStream? -} - -/** Used to sniff a local file. */ -internal class SnifferFileContent(val file: File) : SnifferContent { - - override suspend fun read(): ByteArray? = withContext(Dispatchers.IO) { - try { - // We only read files smaller than 5MB to avoid an [OutOfMemoryError]. - if (file.length() > 5 * 1000 * 1000) { - null - } else { - file.readBytes() - } - } catch (e: Exception) { - Timber.e(e) - null - } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - Timber.e(e) - null - } - } - - override suspend fun stream(): InputStream? = - try { - file.inputStream().buffered() - } catch (e: Exception) { - Timber.e(e) - null - } -} - -/** Used to sniff a bytes array. */ -internal class SnifferBytesContent(val getBytes: () -> ByteArray) : SnifferContent { - - private lateinit var _bytes: ByteArray - - private suspend fun bytes(): ByteArray { - if (!this::_bytes.isInitialized) { - _bytes = withContext(Dispatchers.IO) { getBytes() } - } - return _bytes - } - - override suspend fun read(): ByteArray? = bytes() - - override suspend fun stream(): InputStream? = - ByteArrayInputStream(bytes()) -} - -/** Used to sniff a content URI. */ -internal class SnifferUriContent(val uri: Uri, val contentResolver: ContentResolver) : SnifferContent { - - override suspend fun read(): ByteArray? = withContext(Dispatchers.IO) { - try { - stream()?.readBytes() - } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - Timber.e(e) - null - } - } - - override suspend fun stream(): InputStream? = withContext(Dispatchers.IO) { - try { - contentResolver.openInputStream(uri) - } catch (e: Exception) { - Timber.e(e) - null - } - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SnifferContext.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SnifferContext.kt deleted file mode 100644 index 53ef4b20f7..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SnifferContext.kt +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2020 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.mediatype - -import java.io.InputStream -import java.nio.charset.Charset -import java.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.json.JSONObject -import org.readium.r2.shared.extensions.readFully -import org.readium.r2.shared.extensions.readRange -import org.readium.r2.shared.extensions.tryOr -import org.readium.r2.shared.extensions.tryOrNull -import org.readium.r2.shared.parser.xml.ElementNode -import org.readium.r2.shared.parser.xml.XmlParser -import org.readium.r2.shared.publication.Manifest -import org.readium.r2.shared.util.archive.Archive -import org.readium.r2.shared.util.archive.DefaultArchiveFactory -import timber.log.Timber - -/** - * A companion type of [Sniffer] holding the type hints (file extensions, media types) and - * providing an access to the file content. - * - * @param content Underlying content holder. - * @param mediaTypes Media type hints. - * @param fileExtensions File extension hints. - */ -class SnifferContext internal constructor( - private val content: SnifferContent? = null, - mediaTypes: List<String> = emptyList(), - fileExtensions: List<String> = emptyList() -) { - - /** Media type hints. */ - val mediaTypes: List<MediaType> = mediaTypes - .mapNotNull { MediaType.parse(it) } - - /** File extension hints. */ - val fileExtensions: List<String> = fileExtensions - .map { it.lowercase(Locale.ROOT) } - - // Metadata - - /** Finds the first [Charset] declared in the media types' `charset` parameter. */ - val charset: Charset? by lazy { - this.mediaTypes.mapNotNull { it.charset }.firstOrNull() - } - - /** Returns whether this context has any of the given file extensions, ignoring case. */ - fun hasFileExtension(vararg fileExtensions: String): Boolean { - for (fileExtension in fileExtensions) { - if (this.fileExtensions.contains(fileExtension.lowercase(Locale.ROOT))) { - return true - } - } - return false - } - - /** - * Returns whether this context has any of the given media type, ignoring case and extra - * parameters. - * - * Implementation note: Use [MediaType] to handle the comparison to avoid edge cases. - */ - fun hasMediaType(vararg mediaTypes: String): Boolean { - @Suppress("NAME_SHADOWING") - val mediaTypes = mediaTypes.mapNotNull { MediaType.parse(it) } - for (mediaType in mediaTypes) { - if (this.mediaTypes.any { mediaType.contains(it) }) { - return true - } - } - return false - } - - // Content - - /** - * Content as plain text. - * - * It will extract the charset parameter from the media type hints to figure out an encoding. - * Otherwise, fallback on UTF-8. - */ - suspend fun contentAsString(): String? = - try { - if (!loadedContentAsString) { - loadedContentAsString = true - _contentAsString = content?.read()?.toString(charset ?: Charset.defaultCharset()) - } - _contentAsString - } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - Timber.e(e) - null - } - - private var loadedContentAsString: Boolean = false - private var _contentAsString: String? = null - - /** Content as an XML document. */ - suspend fun contentAsXml(): ElementNode? { - if (!loadedContentAsXml) { - loadedContentAsXml = true - _contentAsXml = withContext(Dispatchers.IO) { - try { - stream()?.let { XmlParser().parse(it) } - } catch (e: Exception) { - null - } - } - } - - return _contentAsXml - } - - private var loadedContentAsXml: Boolean = false - private var _contentAsXml: ElementNode? = null - - /** - * Content as an Archive instance. - * Warning: Archive is only supported for a local file, for now. - */ - suspend fun contentAsArchive(): Archive? { - if (!loadedContentAsArchive) { - loadedContentAsArchive = true - _contentAsArchive = withContext(Dispatchers.IO) { - (content as? SnifferFileContent)?.let { - DefaultArchiveFactory().open(it.file, password = null) - } - } - } - - return _contentAsArchive - } - - private var loadedContentAsArchive: Boolean = false - private var _contentAsArchive: Archive? = null - - /** - * Content parsed from JSON. - */ - suspend fun contentAsJson(): JSONObject? = - try { - contentAsString()?.let { JSONObject(it) } - } catch (e: Exception) { - null - } - - /** Readium Web Publication Manifest parsed from the content. */ - suspend fun contentAsRwpm(): Manifest? = - Manifest.fromJSON(contentAsJson()) - - /** - * Raw bytes stream of the content. - * - * A byte stream can be useful when sniffers only need to read a few bytes at the beginning of - * the file. - */ - suspend fun stream(): InputStream? = content?.stream() - - /** - * Reads all the bytes or the given [range]. - * - * It can be used to check a file signature, aka magic number. - * See https://en.wikipedia.org/wiki/List_of_file_signatures - */ - suspend fun read(range: LongRange? = null): ByteArray? = - tryOrNull { - if (range != null) stream()?.readRange(range) - else stream()?.readFully() - } - - /** - * Returns whether the content is a JSON object containing all of the given root keys. - */ - internal suspend fun containsJsonKeys(vararg keys: String): Boolean { - val json = contentAsJson() ?: return false - return json.keys().asSequence().toSet().containsAll(keys.toList()) - } - - /** - * Returns whether an Archive entry exists in this file. - */ - internal suspend fun containsArchiveEntryAt(path: String): Boolean = - tryOrNull { contentAsArchive()?.entry(path) } != null - - /** - * Returns the Archive entry data at the given [path] in this file. - */ - internal suspend fun readArchiveEntryAt(path: String): ByteArray? { - val archive = contentAsArchive() ?: return null - - return withContext(Dispatchers.IO) { - tryOrNull { - val entry = archive.entry(path) - val bytes = entry.read() - entry.close() - bytes - } - } - } - - /** - * Returns whether all the Archive entry paths satisfy the given `predicate`. - */ - internal suspend fun archiveEntriesAllSatisfy(predicate: (Archive.Entry) -> Boolean): Boolean = - tryOr(false) { contentAsArchive()?.entries()?.all(predicate) == true } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt index f4b63e8f5b..fdd56a1b48 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt @@ -11,29 +11,28 @@ package org.readium.r2.shared.util.pdf import android.content.Context import android.graphics.Bitmap -import java.io.File import kotlin.reflect.KClass import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.fetcher.Resource import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationServicesHolder import org.readium.r2.shared.publication.ReadingProgression import org.readium.r2.shared.publication.services.cacheService import org.readium.r2.shared.util.SuspendingCloseable +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.cache.Cache +import org.readium.r2.shared.util.cache.getOrTryPut +import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource -interface PdfDocumentFactory<T : PdfDocument> { +public interface PdfDocumentFactory<T : PdfDocument> { /** Class for the type of document this factory produces. */ - val documentType: KClass<T> + public val documentType: KClass<T> - /** Opens a PDF from a [file]. */ - suspend fun open(file: File, password: String?): T - - /** Opens a PDF from a Fetcher resource. */ - suspend fun open(resource: Resource, password: String?): T + /** Opens a PDF from a [resource]. */ + public suspend fun open(resource: Resource, password: String?): ReadTry<T> } /** @@ -44,7 +43,9 @@ interface PdfDocumentFactory<T : PdfDocument> { * around. */ @ExperimentalReadiumApi -suspend fun <T : PdfDocument> PdfDocumentFactory<T>.cachedIn(holder: PublicationServicesHolder): PdfDocumentFactory<T> { +public suspend fun <T : PdfDocument> PdfDocumentFactory<T>.cachedIn( + holder: PublicationServicesHolder +): PdfDocumentFactory<T> { val namespace = requireNotNull(documentType.qualifiedName) val cache = holder.cacheService?.cacheOf(documentType, namespace) ?: return this return CachingPdfDocumentFactory(this, cache) @@ -55,82 +56,77 @@ private class CachingPdfDocumentFactory<T : PdfDocument>( private val cache: Cache<T> ) : PdfDocumentFactory<T> by factory { - override suspend fun open(file: File, password: String?): T = - cache.transaction { - getOrPut(file.path) { - factory.open(file, password) - } - } - - override suspend fun open(resource: Resource, password: String?): T = - cache.transaction { - getOrPut(resource.link().href) { + override suspend fun open(resource: Resource, password: String?): ReadTry<T> { + val key = resource.sourceUrl?.toString() ?: return factory.open(resource, password) + return cache.transaction { + getOrTryPut(key) { factory.open(resource, password) } } + } } /** * Represents a PDF document. */ -interface PdfDocument : SuspendingCloseable { +public interface PdfDocument : SuspendingCloseable { /** * Permanent identifier based on the contents of the file at the time it was originally * created. */ - val identifier: String? get() = null + public val identifier: String? get() = null /** * Number of pages in the document. */ - val pageCount: Int + public val pageCount: Int /** * Default reading progression of the document. */ - val readingProgression: ReadingProgression get() = ReadingProgression.AUTO + public val readingProgression: ReadingProgression? get() = null /** * The first page rendered as a cover. */ - suspend fun cover(context: Context): Bitmap? = null + public suspend fun cover(context: Context): Bitmap? = null // Values extracted from the document information dictionary, defined in PDF specification. /** * The document's title. */ - val title: String? get() = null + public val title: String? get() = null /** * The name of the person who created the document. */ - val author: String? get() = null + public val author: String? get() = null /** * The subject of the document. */ - val subject: String? get() = null + public val subject: String? get() = null /** * Keywords associated with the document. */ - val keywords: List<String> get() = emptyList() + public val keywords: List<String> get() = emptyList() /** * Outline to build the table of contents. */ - val outline: List<OutlineNode> get() = emptyList() + public val outline: List<OutlineNode> get() = emptyList() - data class OutlineNode( + public data class OutlineNode( val title: String?, val pageNumber: Int?, // Starts from 1. val children: List<OutlineNode> ) // To allow extensions on the Companion object. - companion object + public companion object } /** @@ -140,14 +136,14 @@ interface PdfDocument : SuspendingCloseable { * relative to. */ @ExperimentalReadiumApi -fun List<PdfDocument.OutlineNode>.toLinks(documentHref: String): List<Link> = +public fun List<PdfDocument.OutlineNode>.toLinks(documentHref: Url): List<Link> = map { it.toLink(documentHref) } @ExperimentalReadiumApi -fun PdfDocument.OutlineNode.toLink(documentHref: String): Link = +public fun PdfDocument.OutlineNode.toLink(documentHref: Url): Link = Link( - href = "$documentHref#page=$pageNumber", - type = MediaType.PDF.toString(), + href = documentHref.resolve(Url("#page=$pageNumber")!!), + mediaType = MediaType.PDF, title = title, children = children.toLinks(documentHref) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt new file mode 100644 index 0000000000..421a176193 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ReadableBuffer + +/** + * Wraps a [Resource] and buffers its content. + * + * Expensive interaction with the underlying resource is minimized, since most (smaller) requests + * can be satisfied by accessing the buffer alone. The drawback is that some extra space is required + * to hold the buffer and that copying takes place when filling that buffer, but this is usually + * outweighed by the performance benefits. + * + * Note that this implementation is pretty limited and the benefits are only apparent when reading + * forward and consecutively – e.g. when downloading the resource by chunks. The buffer is ignored + * when reading backward or far ahead. + * + * @param resource Underlying resource which will be buffered. + * @param resourceLength The total length of the resource, when known. This can improve performance + * by avoiding requesting the length from the underlying resource. + * @param bufferSize Size of the buffer chunks to read. + */ +public class BufferingResource( + private val resource: Resource, + resourceLength: Long? = null, + private val bufferSize: Int = DEFAULT_BUFFER_SIZE +) : Resource by resource { + + private val buffer: ReadableBuffer = + ReadableBuffer(resource, resourceLength, bufferSize) + + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> = + buffer.read(range) +} + +/** + * Wraps this resource in a [BufferingResource] to improve reading performances. + * + * @param resourceLength The total length of the resource, when known. This can improve performance + * by avoiding requesting the length from the underlying resource. + * @param bufferSize Size of the buffer chunks to read. + */ +public fun Resource.buffered( + resourceLength: Long? = null, + bufferSize: Int = DEFAULT_BUFFER_SIZE +): BufferingResource = + BufferingResource(resource = this, resourceLength = resourceLength, bufferSize = bufferSize) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt new file mode 100644 index 0000000000..037f05ce3c --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError + +/** + * Resource that will act as a proxy to a fallback resource if the [originalResource] errors out. + */ +public class FallbackResource( + private val originalResource: Resource, + private val fallbackResourceFactory: (ReadError) -> Resource? +) : Resource { + + override val sourceUrl: AbsoluteUrl? = null + + override suspend fun properties(): Try<Resource.Properties, ReadError> = + withResource { properties() } + + override suspend fun length(): Try<Long, ReadError> = + withResource { length() } + + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> = + withResource { read(range) } + + override suspend fun close() { + if (::_resource.isInitialized) { + _resource.close() + } + } + + private lateinit var _resource: Resource + + private suspend fun <T> withResource(action: suspend Resource.() -> Try<T, ReadError>): Try<T, ReadError> { + if (::_resource.isInitialized) { + return _resource.action() + } + + var resource = originalResource + + var result = resource.action() + result.onFailure { error -> + fallbackResourceFactory(error)?.let { fallbackResource -> + resource = fallbackResource + result = resource.action() + } + } + + _resource = resource + return result + } +} + +/** + * Falls back to alternative resources when the receiver fails. + */ +public fun Resource.fallback( + fallbackResourceFactory: (ReadError) -> Resource? +): Resource = + FallbackResource(this, fallbackResourceFactory) + +/** + * Falls back to the given alternative [Resource] when the receiver fails. + */ +public fun Resource.fallback(fallbackResource: Resource): Resource = + FallbackResource(this) { fallbackResource } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt new file mode 100644 index 0000000000..6ec5837be4 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.util.mediatype.MediaType + +private const val FILENAME_KEY = "filename" + +private const val MEDIA_TYPE_KEY = "mediaType" + +public val Resource.Properties.filename: String? + get() = this[FILENAME_KEY] as? String + +public var Resource.Properties.Builder.filename: String? + get() = this[FILENAME_KEY] as? String? + set(value) { + if (value == null) { + remove(FILENAME_KEY) + } else { + put(FILENAME_KEY, value) + } + } + +public val Resource.Properties.mediaType: MediaType? + get() = (this[MEDIA_TYPE_KEY] as? String?) + ?.let { MediaType(it) } + +public var Resource.Properties.Builder.mediaType: MediaType? + get() = (this[MEDIA_TYPE_KEY] as? String?) + ?.let { MediaType(it) } + set(value) { + if (value == null) { + remove(MEDIA_TYPE_KEY) + } else { + put(MEDIA_TYPE_KEY, value.toString()) + } + } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/InMemoryResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/InMemoryResource.kt new file mode 100644 index 0000000000..e85e2d379b --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/InMemoryResource.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import kotlinx.coroutines.runBlocking +import org.readium.r2.shared.extensions.coerceFirstNonNegative +import org.readium.r2.shared.extensions.read +import org.readium.r2.shared.extensions.requireLengthFitInt +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError + +/** Creates a [Resource] serving a [ByteArray]. */ +public class InMemoryResource( + override val sourceUrl: AbsoluteUrl?, + private val properties: Resource.Properties, + private val bytes: suspend () -> Try<ByteArray, ReadError> +) : Resource { + + public constructor( + bytes: ByteArray, + source: AbsoluteUrl? = null, + properties: Resource.Properties = Resource.Properties() + ) : this(sourceUrl = source, properties = properties, { Try.success(bytes) }) + + private lateinit var _bytes: Try<ByteArray, ReadError> + + override suspend fun properties(): Try<Resource.Properties, ReadError> { + return Try.success(properties) + } + + override suspend fun length(): Try<Long, ReadError> = + read().map { it.size.toLong() } + + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> { + if (!::_bytes.isInitialized) { + _bytes = bytes() + } + + if (range == null) { + return _bytes + } + + @Suppress("NAME_SHADOWING") + val range = range + .coerceFirstNonNegative() + .requireLengthFitInt() + + if (range.isEmpty()) { + return Try.success(ByteArray(0)) + } + + return _bytes.map { it.read(range) } + } + + override suspend fun close() {} + + override fun toString(): String = + "${javaClass.simpleName}(${runBlocking { length() }} bytes)" +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt new file mode 100644 index 0000000000..11d6eddf05 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError + +/** + * Wraps a [Resource] which will be created only when first accessing one of its members. + */ +public open class LazyResource( + override val sourceUrl: AbsoluteUrl? = null, + private val factory: suspend () -> Resource +) : Resource { + + private lateinit var _resource: Resource + + protected suspend fun resource(): Resource { + if (!::_resource.isInitialized) { + _resource = factory() + } + + return _resource + } + + override suspend fun properties(): Try<Resource.Properties, ReadError> = + resource().properties() + + override suspend fun length(): Try<Long, ReadError> = + resource().length() + + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> = + resource().read(range) + + override suspend fun close() { + if (::_resource.isInitialized) { + _resource.close() + } + } + + override fun toString(): String = + if (::_resource.isInitialized) { + "${javaClass.simpleName}($_resource)" + } else { + "${javaClass.simpleName}(...)" + } +} + +public fun <R : Resource> Resource.flatMap(transform: suspend (Resource) -> R): LazyResource = + LazyResource { transform(this) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt new file mode 100644 index 0000000000..7324ebe72d --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ReadTry +import org.readium.r2.shared.util.data.Readable + +/** + * Acts as a proxy to an actual resource by handling read access. + */ +public interface Resource : Readable { + + /** + * URL locating this resource, if any. + */ + public val sourceUrl: AbsoluteUrl? + + /** + * Properties associated to the resource. + * + * This is opened for extensions. + */ + public suspend fun properties(): Try<Properties, ReadError> + + public class Properties( + properties: Map<String, Any> = emptyMap() + ) : Map<String, Any> by properties { + + public companion object { + public inline operator fun invoke(build: Builder.() -> Unit): Properties = + Properties(Builder().apply(build)) + } + + public inline fun copy(build: Builder.() -> Unit): Properties = + Properties(Builder(this).apply(build)) + + public class Builder(properties: Map<String, Any> = emptyMap()) : + MutableMap<String, Any> by properties.toMutableMap() + } + + @Deprecated( + "`Resource.Exception` was split into several `Error` classes. You probably need `ReadError`.", + ReplaceWith("org.readium.r2.shared.util.data.ReadError"), + DeprecationLevel.ERROR + ) + public class Exception +} + +/** Creates a Resource that will always return the given [error]. */ +public class FailureResource( + private val error: ReadError +) : Resource { + + override val sourceUrl: AbsoluteUrl? = null + override suspend fun properties(): Try<Resource.Properties, ReadError> = Try.failure(error) + override suspend fun length(): Try<Long, ReadError> = Try.failure(error) + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> = Try.failure(error) + override suspend fun close() {} + + override fun toString(): String = + "${javaClass.simpleName}($error)" +} + +/** + * Returns a new [Resource] accessing the same data but not owning them. + * + * This is useful when you want to pass a [Resource] to a component which might close it, but you + * want to keep using it after. + */ +public fun Resource.borrow(): Resource = + BorrowedResource(this) + +private class BorrowedResource( + private val resource: Resource +) : Resource by resource { + + override suspend fun close() { + // Do nothing + } +} + +@Deprecated( + "Catch exceptions yourself to the most suitable ReadError.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("map(transform)") +) +@Suppress("UnusedReceiverParameter") +public fun <R, S, E> Try<S, E>.mapCatching(): ReadTry<R> = + throw NotImplementedError() + +@Suppress("UnusedReceiverParameter") +public fun <R, S, E> Try<S, E>.flatMapCatching(): ReadTry<R> = + throw NotImplementedError() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt new file mode 100644 index 0000000000..0d222fb9b6 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url + +/** + * A factory to read [Resource]s from [Url]s. + */ +public interface ResourceFactory { + + public sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { + + public class SchemeNotSupported( + public val scheme: Url.Scheme, + cause: org.readium.r2.shared.util.Error? = null + ) : Error("Url scheme $scheme is not supported.", cause) + } + + /** + * Creates a [Resource] to access [url]. + * + * @param url The url the resource will access. + */ + public suspend fun create( + url: AbsoluteUrl + ): Try<Resource, Error> +} + +/** + * A composite [ResourceFactory] which tries several factories until it finds one which supports + * the url scheme. + */ +public class CompositeResourceFactory( + private val factories: List<ResourceFactory> +) : ResourceFactory { + + public constructor(vararg factories: ResourceFactory) : this(factories.toList()) + + override suspend fun create( + url: AbsoluteUrl + ): Try<Resource, ResourceFactory.Error> { + for (factory in factories) { + factory.create(url) + .getOrNull() + ?.let { return Try.success(it) } + } + + return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt new file mode 100644 index 0000000000..084b87e68f --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.Container + +/** A [Container] for a single [Resource]. */ +public class SingleResourceContainer( + private val entryUrl: Url, + private val resource: Resource +) : Container<Resource> { + + override val entries: Set<Url> = setOf(entryUrl) + + override fun get(url: Url): Resource? { + if (url.removeFragment().removeQuery() != entryUrl) { + return null + } + + return resource.borrow() + } + + override suspend fun close() { + resource.close() + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt new file mode 100644 index 0000000000..149bb8ebcc --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import kotlinx.coroutines.runBlocking +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError + +/** Creates a Resource serving a [String]. */ +public class StringResource private constructor( + private val resource: Resource +) : Resource by resource { + + public constructor( + source: AbsoluteUrl? = null, + properties: Resource.Properties = Resource.Properties(), + string: suspend () -> Try<String, ReadError> + ) : this(InMemoryResource(source, properties) { string().map { it.toByteArray() } }) + + public constructor( + string: String, + source: AbsoluteUrl? = null, + properties: Resource.Properties = Resource.Properties() + ) : this(source, properties, { Try.success(string) }) + + override fun toString(): String = + "${javaClass.simpleName}(${runBlocking { read().map { it.decodeToString() } } }})" +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt new file mode 100644 index 0000000000..cfb2f55d62 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError + +/** + * Protects the access to a wrapped resource with a mutex to make it thread-safe. + */ +public class SynchronizedResource( + private val resource: Resource +) : Resource { + + // This doesn't use `Resource by resource` to avoid forgetting the synchronization for a future API. + + private val mutex = Mutex() + + override val sourceUrl: AbsoluteUrl? get() = resource.sourceUrl + + override suspend fun properties(): Try<Resource.Properties, ReadError> = + mutex.withLock { resource.properties() } + + override suspend fun length(): Try<Long, ReadError> = + mutex.withLock { resource.length() } + + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> = + mutex.withLock { resource.read(range) } + + override suspend fun close() { + mutex.withLock { resource.close() } + } + + override fun toString(): String = + "${javaClass.simpleName}($resource)" +} + +/** + * Wraps this resource in a [SynchronizedResource] to protect the access from multiple threads. + */ +public fun Resource.synchronized(): SynchronizedResource = + SynchronizedResource(this) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt new file mode 100644 index 0000000000..058d2f9c41 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.Container + +/** + * Implements the transformation of a Resource. It can be used, for example, to decrypt, + * deobfuscate, inject CSS or JavaScript, correct content – e.g. adding a missing dir="rtl" in an + * HTML document, pre-process – e.g. before indexing a publication's content, etc. + * + * If the transformation doesn't apply, simply return the resource unchanged. + */ +public typealias EntryTransformer = (Url, Resource) -> Resource + +/** + * Transforms the resources' content of a child fetcher using a list of [EntryTransformer] + * functions. + */ +public class TransformingContainer( + private val container: Container<Resource>, + private val transformers: List<EntryTransformer> +) : Container<Resource> { + + public constructor(container: Container<Resource>, transformer: EntryTransformer) : + this(container, listOf(transformer)) + + override val entries: Set<Url> = + container.entries + + override fun get(url: Url): Resource? { + val originalResource = container[url] + ?: return null + + return transformers + .fold(originalResource) { acc: Resource, transformer: EntryTransformer -> + transformer(url, acc) + } + } + + override suspend fun close() { + container.close() + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt new file mode 100644 index 0000000000..bf2172aa60 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.extensions.coerceIn +import org.readium.r2.shared.extensions.requireLengthFitInt +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.flatMap + +/** + * Transforms the bytes of [resource] on-the-fly. + * + * If you set [cacheBytes] to false, consider providing your own implementation of [length] to avoid + * unnecessary transformations. + * + * Warning: The transformation runs on the full content of [resource], so it's not appropriate for + * large resources which can't be held in memory. + */ +public abstract class TransformingResource( + private val resource: Resource, + private val cacheBytes: Boolean = true +) : Resource by resource { + + public companion object { + /** + * Creates a [TransformingResource] using the given [transform] function. + */ + public operator fun invoke( + resource: Resource, + transform: suspend (ByteArray) -> Try<ByteArray, ReadError> + ): TransformingResource = + object : TransformingResource(resource) { + override suspend fun transform(data: Try<ByteArray, ReadError>): Try<ByteArray, ReadError> = + data.flatMap { + try { + transform(it) + } catch (e: OutOfMemoryError) { + Try.failure(ReadError.OutOfMemory(e)) + } + } + } + } + + override val sourceUrl: AbsoluteUrl? = null + + private lateinit var _bytes: Try<ByteArray, ReadError> + + public abstract suspend fun transform(data: Try<ByteArray, ReadError>): Try<ByteArray, ReadError> + + private suspend fun bytes(): Try<ByteArray, ReadError> { + if (::_bytes.isInitialized) { + return _bytes + } + + val bytes = transform(resource.read()) + if (cacheBytes) { + _bytes = bytes + } + + return bytes + } + + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> = + bytes().map { + if (range == null) { + return bytes() + } + + @Suppress("NAME_SHADOWING") + val range = range + .coerceIn(0L until it.size) + .requireLengthFitInt() + + it.sliceArray(range.map(Long::toInt)) + } + + override suspend fun length(): Try<Long, ReadError> = + bytes().map { it.size.toLong() } +} + +public fun Resource.map(transform: suspend (ByteArray) -> Try<ByteArray, ReadError>): Resource = + TransformingResource(this, transform = transform) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt new file mode 100644 index 0000000000..a87a6b9858 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2021 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource.content + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jsoup.Jsoup +import org.jsoup.parser.Parser +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.DecodeError +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.decodeString +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.tryRecover + +/** + * Extracts pure content from a marked-up (e.g. HTML) or binary (e.g. PDF) resource. + */ +@ExperimentalReadiumApi +public interface ResourceContentExtractor { + + /** + * Extracts the text content of the given [resource]. + */ + public suspend fun extractText(resource: Resource): Try<String, ReadError> = Try.success("") + + public interface Factory { + /** + * Creates a [ResourceContentExtractor] instance for the given [resource]. + * + * Return null if the resource format is not supported. + */ + public suspend fun createExtractor(resource: Resource, mediaType: MediaType): ResourceContentExtractor? + } +} + +@ExperimentalReadiumApi +public class DefaultResourceContentExtractorFactory : ResourceContentExtractor.Factory { + + override suspend fun createExtractor(resource: Resource, mediaType: MediaType): ResourceContentExtractor? = + when (mediaType) { + MediaType.HTML, MediaType.XHTML -> HtmlResourceContentExtractor() + else -> null + } +} + +/** + * [ResourceContentExtractor] implementation for HTML resources. + */ +@ExperimentalReadiumApi +public class HtmlResourceContentExtractor : ResourceContentExtractor { + + override suspend fun extractText(resource: Resource): Try<String, ReadError> = + withContext(Dispatchers.IO) { + resource + .read() + .getOrElse { return@withContext Try.failure(it) } + .decodeString() + .tryRecover { + when (it) { + is DecodeError.OutOfMemory -> + return@withContext Try.failure(ReadError.OutOfMemory(it.cause)) + is DecodeError.Decoding -> + Try.success("") + } + } + .map { html -> + val body = Jsoup.parse(html).body().text() + // Transform HTML entities into their actual characters. + Parser.unescapeEntities(body, false) + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/tokenizer/TextTokenizer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/tokenizer/TextTokenizer.kt index 99cd9b95de..346ea7dc0f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/tokenizer/TextTokenizer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/tokenizer/TextTokenizer.kt @@ -15,11 +15,11 @@ import org.readium.r2.shared.util.Language /** A tokenizer splitting a String into range tokens (e.g. words, sentences, etc.). */ @ExperimentalReadiumApi -typealias TextTokenizer = Tokenizer<String, IntRange> +public typealias TextTokenizer = Tokenizer<String, IntRange> /** A text token unit which can be used with a [TextTokenizer]. */ @ExperimentalReadiumApi -enum class TextUnit { +public enum class TextUnit { Word, Sentence, Paragraph } @@ -28,14 +28,15 @@ enum class TextUnit { * version. */ @ExperimentalReadiumApi -class DefaultTextContentTokenizer private constructor( +public class DefaultTextContentTokenizer private constructor( private val tokenizer: TextTokenizer ) : TextTokenizer by tokenizer { - constructor(unit: TextUnit, language: Language?) : this( - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + public constructor(unit: TextUnit, language: Language?) : this( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { IcuTextTokenizer(language = language, unit = unit) - else + } else { NaiveTextTokenizer(unit = unit) + } ) } @@ -45,7 +46,7 @@ class DefaultTextContentTokenizer private constructor( */ @ExperimentalReadiumApi @RequiresApi(Build.VERSION_CODES.N) -class IcuTextTokenizer(language: Language?, unit: TextUnit) : TextTokenizer { +public class IcuTextTokenizer(language: Language?, unit: TextUnit) : TextTokenizer { private val iterator: BreakIterator @@ -54,7 +55,9 @@ class IcuTextTokenizer(language: Language?, unit: TextUnit) : TextTokenizer { iterator = when (unit) { TextUnit.Word -> BreakIterator.getWordInstance(loc) TextUnit.Sentence -> BreakIterator.getSentenceInstance(loc) - TextUnit.Paragraph -> throw IllegalArgumentException("IcuTextTokenizer does not handle TextContentUnit.Paragraph") + TextUnit.Paragraph -> throw IllegalArgumentException( + "IcuTextTokenizer does not handle TextContentUnit.Paragraph" + ) } } @@ -80,11 +83,13 @@ class IcuTextTokenizer(language: Language?, unit: TextUnit) : TextTokenizer { * Use [IcuTextTokenizer] for better results. */ @ExperimentalReadiumApi -class NaiveTextTokenizer(unit: TextUnit) : TextTokenizer { +public class NaiveTextTokenizer(unit: TextUnit) : TextTokenizer { private val iterator: java.text.BreakIterator = when (unit) { TextUnit.Word -> java.text.BreakIterator.getWordInstance() TextUnit.Sentence -> java.text.BreakIterator.getSentenceInstance() - TextUnit.Paragraph -> throw IllegalArgumentException("NaiveTextTokenizer does not handle TextContentUnit.Paragraph") + TextUnit.Paragraph -> throw IllegalArgumentException( + "NaiveTextTokenizer does not handle TextContentUnit.Paragraph" + ) } override fun tokenize(data: String): List<IntRange> { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/tokenizer/Tokenizer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/tokenizer/Tokenizer.kt index f15b864e54..5d6398993a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/tokenizer/Tokenizer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/tokenizer/Tokenizer.kt @@ -10,6 +10,6 @@ import org.readium.r2.shared.ExperimentalReadiumApi /** A tokenizer splits a piece of data [D] into a list of [T] tokens. */ @ExperimentalReadiumApi -fun interface Tokenizer<D, T> { - fun tokenize(data: D): List<T> +public fun interface Tokenizer<D, T> { + public fun tokenize(data: D): List<T> } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/units/Hertz.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/units/Hertz.kt new file mode 100644 index 0000000000..f4622720e3 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/units/Hertz.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.units + +@JvmInline +public value class Hz(public val value: Double) + +public val Double.hz: Hz get() = Hz(this) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/parser/xml/XmlParser.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/xml/XmlParser.kt similarity index 78% rename from readium/shared/src/main/java/org/readium/r2/shared/parser/xml/XmlParser.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/xml/XmlParser.kt index ea4b60e561..14df5a0cdb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/parser/xml/XmlParser.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/xml/XmlParser.kt @@ -7,12 +7,13 @@ * LICENSE file present in the project repository where this source code is maintained. */ -package org.readium.r2.shared.parser.xml +package org.readium.r2.shared.util.xml import java.io.IOException import java.io.InputStream import java.util.* import javax.xml.XMLConstants +import org.readium.r2.shared.InternalReadiumApi import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserFactory @@ -22,15 +23,19 @@ import org.xmlpull.v1.XmlPullParserFactory * [isNamespaceAware] behaves as defined in XmlPullParser specification. * If [isCaseSensitive] is false, attribute and tag names are lowercased during the parsing */ -class XmlParser(val isNamespaceAware: Boolean = true, val isCaseSensitive: Boolean = true) { +@InternalReadiumApi +public class XmlParser( + private val isNamespaceAware: Boolean = true, + private val isCaseSensitive: Boolean = true +) { - val parser: XmlPullParser = XmlPullParserFactory.newInstance().let { + private val parser: XmlPullParser = XmlPullParserFactory.newInstance().let { it.isNamespaceAware = isNamespaceAware it.newPullParser() } @Throws(XmlPullParserException::class, IOException::class) - fun parse(stream: InputStream): ElementNode { + public fun parse(stream: InputStream): ElementNode { parser.setInput(stream, null) // let the parser try to determine input encoding val stack = Stack<Triple<MutableList<Node>, AttributeMap, String>>() @@ -45,8 +50,11 @@ class XmlParser(val isNamespaceAware: Boolean = true, val isCaseSensitive: Boole text = "" val attributes = buildAttributeMap(parser) val langAttr = - if (isNamespaceAware) attributes[XMLConstants.XML_NS_URI]?.get("lang") - else attributes[""]?.get("xml:lang") + if (isNamespaceAware) { + attributes[XMLConstants.XML_NS_URI]?.get("lang") + } else { + attributes[""]?.get("xml:lang") + } stack.push(Triple(mutableListOf(), attributes, langAttr ?: stack.peek().third)) } XmlPullParser.END_TAG -> { @@ -103,17 +111,22 @@ class XmlParser(val isNamespaceAware: Boolean = true, val isCaseSensitive: Boole } } -data class Attribute(val name: String, val namespace: String, val value: String) +@InternalReadiumApi +public data class Attribute(val name: String, val namespace: String, val value: String) -typealias AttributeMap = Map<String, Map<String, String>> +@InternalReadiumApi +public typealias AttributeMap = Map<String, Map<String, String>> -sealed class Node +@InternalReadiumApi +public sealed class Node /** Container for text in the XML tree */ -data class TextNode(val text: String) : Node() +@InternalReadiumApi +public data class TextNode(val text: String) : Node() /** Represents a node with children in the XML tree */ -data class ElementNode( +@InternalReadiumApi +public data class ElementNode( val name: String, val namespace: String = "", val lang: String = "", @@ -131,23 +144,25 @@ data class ElementNode( /** Return the value of an attribute picked in the same namespace as this [ElementNode], * fallback to no namespace and at last to null. */ - fun getAttr(name: String) = getAttrNs(name, namespace) ?: getAttrNs(name, "") + public fun getAttr(name: String): String? = getAttrNs(name, namespace) ?: getAttrNs(name, "") /** Return the value of an attribute picked in a specific namespace or null if it does not exist */ - fun getAttrNs(name: String, namespace: String) = attributes[namespace]?.get(name) + public fun getAttrNs(name: String, namespace: String): String? = attributes[namespace]?.get( + name + ) /** Return a list of all ElementNode children */ - fun getAll() = children.filterIsInstance<ElementNode>() + public fun getAll(): List<ElementNode> = children.filterIsInstance<ElementNode>() /** Return a list of [ElementNode] children with the given name and namespace */ - fun get(name: String, namespace: String) = + public fun get(name: String, namespace: String): List<ElementNode> = getAll().filter { it.name == name && it.namespace == namespace } /** Return the first [ElementNode] child with the given name and namespace, or null if there is none */ - fun getFirst(name: String, namespace: String) = get(name, namespace).firstOrNull() + public fun getFirst(name: String, namespace: String): ElementNode? = get(name, namespace).firstOrNull() /** Recursively collect all descendent [ElementNode] with the given name and namespace into a list */ - fun collect(name: String, namespace: String): List<ElementNode> { + public fun collect(name: String, namespace: String): List<ElementNode> { val founded: MutableList<ElementNode> = mutableListOf() for (c in getAll()) { if (c.name == name && c.namespace == namespace) founded.add(c) @@ -157,7 +172,7 @@ data class ElementNode( } /** Recursively collect and concatenate all descendent [TextNode] in depth-first order */ - fun collectText(): String { + public fun collectText(): String { val text = StringBuilder() for (c in children) { when (c) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/BufferedReadableChannel.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/BufferedReadableChannel.kt new file mode 100644 index 0000000000..9e0d401ede --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/BufferedReadableChannel.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.zip + +import java.nio.ByteBuffer +import org.readium.r2.shared.util.zip.jvm.NonWritableChannelException +import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel + +internal class BufferedReadableChannel( + private val innerChannel: SeekableByteChannel, + bufferSize: Int +) : SeekableByteChannel { + + private val dataBuffer: ByteBuffer = + ByteBuffer.allocate(bufferSize) + .apply { limit(0) } + + private val lock: Any = + Any() + + override fun close() { + synchronized(lock) { + innerChannel.close() + } + } + + override fun isOpen(): Boolean { + synchronized(lock) { + return innerChannel.isOpen + } + } + + override fun read(buffer: ByteBuffer): Int { + synchronized(lock) { + val sizeToRead = buffer.remaining() + val sizeToReadFromBuffer = sizeToRead.coerceAtMost(dataBuffer.remaining()) + + val temp = ByteArray(sizeToReadFromBuffer) + dataBuffer.get(temp, 0, sizeToReadFromBuffer) + buffer.put(temp) + + if (sizeToReadFromBuffer == sizeToRead) { + return sizeToReadFromBuffer + } + + dataBuffer.clear() + innerChannel.read(dataBuffer) + dataBuffer.flip() + + if (!dataBuffer.hasRemaining()) { + return sizeToReadFromBuffer + } + + return sizeToReadFromBuffer + read(buffer) + } + } + + override fun write(buffer: ByteBuffer): Int { + throw NonWritableChannelException() + } + + override fun position(): Long { + synchronized(lock) { + return innerChannel.position() - dataBuffer.remaining() + } + } + + override fun position(newPosition: Long): BufferedReadableChannel { + synchronized(lock) { + val innerPosition = innerChannel.position() + if (newPosition in innerPosition - dataBuffer.limit() until innerPosition) { + val newBufferPosition = (dataBuffer.limit() - (innerPosition - newPosition)).toInt() + dataBuffer.position(newBufferPosition) + } else { + dataBuffer.limit(0) + innerChannel.position(newPosition) + } + return this + } + } + + override fun size(): Long { + synchronized(lock) { + return innerChannel.size() + } + } + + override fun truncate(size: Long): BufferedReadableChannel { + throw NonWritableChannelException() + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/CachingReadableChannel.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/CachingReadableChannel.kt new file mode 100644 index 0000000000..7aef0453d8 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/CachingReadableChannel.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.zip + +import java.nio.ByteBuffer +import org.readium.r2.shared.util.zip.jvm.NonWritableChannelException +import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel + +internal class CachingReadableChannel( + private val innerChannel: SeekableByteChannel, + private val cacheFrom: Long = 0 +) : SeekableByteChannel { + + init { + require(cacheFrom < innerChannel.size()) + } + + private val tail: ByteBuffer = + ByteBuffer.allocate((innerChannel.size() - cacheFrom).toInt()) + .apply { limit(0) } + + private val lock: Any = + Any() + + override fun close() { + synchronized(lock) { + innerChannel.close() + } + } + + override fun isOpen(): Boolean { + synchronized(lock) { + return innerChannel.isOpen + } + } + + fun cache() { + synchronized(lock) { + cacheTail() + } + } + + override fun read(buffer: ByteBuffer): Int { + synchronized(lock) { + val channelPosition = innerChannel.position() + if (channelPosition in cacheFrom until innerChannel.size()) { + if (tail.limit() == 0) { + cacheTail() + } + + return readFromTail(buffer, channelPosition - cacheFrom) + } + + return innerChannel.read(buffer) + } + } + + private fun readFromTail(buffer: ByteBuffer, start: Long): Int { + tail.position(start.toInt()) + val sizeToRead = buffer.remaining().coerceAtMost(tail.remaining()) + val temp = ByteArray(sizeToRead) + tail.get(temp) + buffer.put(temp) + innerChannel.position(innerChannel.position() + sizeToRead) + return sizeToRead + } + + private fun cacheTail() { + tail.clear() + innerChannel.position(cacheFrom) + innerChannel.read(tail) + tail.flip() + } + + override fun write(buffer: ByteBuffer): Int { + throw NonWritableChannelException() + } + + override fun position(): Long { + synchronized(lock) { + return innerChannel.position() + } + } + + override fun position(newPosition: Long): CachingReadableChannel { + synchronized(lock) { + innerChannel.position(newPosition) + return this + } + } + + override fun size(): Long { + synchronized(lock) { + return innerChannel.size() + } + } + + override fun truncate(size: Long): CachingReadableChannel { + throw NonWritableChannelException() + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileChannelAdapter.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileChannelAdapter.java new file mode 100644 index 0000000000..172c03dbad --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileChannelAdapter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.zip; + +import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +public class FileChannelAdapter implements SeekableByteChannel { + + private final FileChannel channel; + + FileChannelAdapter(final File file, final String mode) throws FileNotFoundException { + channel = new RandomAccessFile(file, mode).getChannel(); + } + + private FileChannelAdapter(final FileChannel channel) { + this.channel = channel; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + return channel.read(dst); + } + + @Override + public int write(ByteBuffer src) throws IOException { + return channel.write(src); + } + + @Override + public long position() throws IOException { + return channel.position(); + } + + @Override + public SeekableByteChannel position(long newPosition) throws IOException { + return new FileChannelAdapter(channel.position(newPosition)); + } + + @Override + public long size() throws IOException { + return channel.size(); + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + return new FileChannelAdapter(channel.truncate(size)); + } + + @Override + public boolean isOpen() { + return channel.isOpen(); + } + + @Override + public void close() throws IOException { + channel.close(); + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt new file mode 100644 index 0000000000..4989b5c5dc --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.zip + +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.util.zip.ZipException +import java.util.zip.ZipFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.archive.ArchiveOpener +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.file.FileSystemError +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.ZipSpecification +import org.readium.r2.shared.util.resource.Resource + +/** + * An [ArchiveOpener] to open local ZIP files with Java's [ZipFile]. + */ +internal class FileZipArchiveProvider { + + suspend fun sniffOpen(file: File): Try<Container<Resource>, ArchiveOpener.SniffOpenError> { + return withContext(Dispatchers.IO) { + try { + val container = FileZipContainer(ZipFile(file), file) + Try.success(container) + } catch (e: ZipException) { + Try.failure(ArchiveOpener.SniffOpenError.NotRecognized) + } catch (e: SecurityException) { + Try.failure( + ArchiveOpener.SniffOpenError.Reading( + ReadError.Access(FileSystemError.Forbidden(e)) + ) + ) + } catch (e: IOException) { + Try.failure( + ArchiveOpener.SniffOpenError.Reading( + ReadError.Access(FileSystemError.IO(e)) + ) + ) + } + } + } + + suspend fun open( + format: Format, + file: File + ): Try<Container<Resource>, ArchiveOpener.OpenError> { + if (!format.conformsTo(ZipSpecification)) { + return Try.failure( + ArchiveOpener.OpenError.FormatNotSupported(format) + ) + } + + return open(file) + } + + // Internal for testing purpose + internal suspend fun open(file: File): Try<Container<Resource>, ArchiveOpener.OpenError> = + withContext(Dispatchers.IO) { + try { + val archive = FileZipContainer(ZipFile(file), file) + Try.success(archive) + } catch (e: FileNotFoundException) { + Try.failure( + ArchiveOpener.OpenError.Reading( + ReadError.Access(FileSystemError.FileNotFound(e)) + ) + ) + } catch (e: ZipException) { + Try.failure( + ArchiveOpener.OpenError.Reading( + ReadError.Decoding(e) + ) + ) + } catch (e: SecurityException) { + Try.failure( + ArchiveOpener.OpenError.Reading( + ReadError.Access(FileSystemError.Forbidden(e)) + ) + ) + } catch (e: IOException) { + Try.failure( + ArchiveOpener.OpenError.Reading( + ReadError.Access(FileSystemError.IO(e)) + ) + ) + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt new file mode 100644 index 0000000000..7b175ebad5 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.zip + +import java.io.File +import java.io.IOException +import java.util.zip.ZipEntry +import java.util.zip.ZipException +import java.util.zip.ZipFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.readium.r2.shared.extensions.readFully +import org.readium.r2.shared.extensions.tryOrLog +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.RelativeUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.archive.ArchiveProperties +import org.readium.r2.shared.util.archive.archive +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.file.FileSystemError +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.io.CountingInputStream +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.toUrl + +internal class FileZipContainer( + private val archive: ZipFile, + file: File +) : Container<Resource> { + + private inner class Entry(private val url: Url, private val entry: ZipEntry) : + Resource { + + override val sourceUrl: AbsoluteUrl? = null + + override suspend fun properties(): Try<Resource.Properties, ReadError> = + Try.success( + Resource.Properties { + filename = url.filename + archive = ArchiveProperties( + entryLength = compressedLength + ?: length().getOrElse { return Try.failure(it) }, + isEntryCompressed = compressedLength != null + ) + } + ) + + override suspend fun length(): Try<Long, ReadError> = + entry.size.takeUnless { it == -1L } + ?.let { Try.success(it) } + ?: Try.failure( + ReadError.UnsupportedOperation( + DebugError("ZIP entry doesn't provide length for entry $url.") + ) + ) + + private val compressedLength: Long? = + if (entry.method == ZipEntry.STORED || entry.method == -1) { + null + } else { + entry.compressedSize.takeUnless { it == -1L } + } + + override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> = + try { + withContext(Dispatchers.IO) { + val bytes = + if (range == null) { + readFully() + } else { + readRange(range) + } + Try.success(bytes) + } + } catch (e: ZipException) { + Try.failure(ReadError.Decoding(e)) + } catch (e: IOException) { + Try.failure(ReadError.Access(FileSystemError.IO(e))) + } + + private suspend fun readFully(): ByteArray = + withContext(Dispatchers.IO) { + archive.getInputStream(entry) + .use { + it.readFully() + } + } + + private fun readRange(range: LongRange): ByteArray = + stream(range.first).readRange(range) + + /** + * Reading an entry in chunks (e.g. from the HTTP server) can be really slow if the entry + * is deflated in the archive, because we can't jump to an arbitrary offset in a deflated + * stream. This means that we need to read from the start of the entry for each chunk. + * + * To alleviate this issue, we cache a stream which will be reused as long as the chunks are + * requested in order. + * + * See this issue for more info: https://github.com/readium/r2-shared-kotlin/issues/129 + */ + private fun stream(fromIndex: Long): CountingInputStream { + // Reuse the current stream if it didn't exceed the requested index. + stream + ?.takeIf { it.count <= fromIndex } + ?.let { return it } + + stream?.close() + + return CountingInputStream(archive.getInputStream(entry)) + .also { stream = it } + } + + private var stream: CountingInputStream? = null + + override suspend fun close() { + withContext(Dispatchers.IO) { + tryOrLog { stream?.close() } + } + } + } + + override val sourceUrl: AbsoluteUrl = file.toUrl() + + override val entries: Set<Url> = + tryOrLog { archive.entries().toList() } + .orEmpty() + .filterNot { it.isDirectory } + .mapNotNull { entry -> Url.fromDecodedPath(entry.name) } + .toSet() + + override fun get(url: Url): Resource? = + (url as? RelativeUrl)?.path + ?.let { + tryOrLog { archive.getEntry(it) } + } + ?.let { Entry(url, it) } + + override suspend fun close() { + tryOrLog { + withContext(Dispatchers.IO) { + archive.close() + } + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ReadableChannelAdapter.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ReadableChannelAdapter.kt new file mode 100644 index 0000000000..4a6aae8edd --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ReadableChannelAdapter.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.zip + +import java.io.IOException +import java.nio.ByteBuffer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.zip.jvm.ClosedChannelException +import org.readium.r2.shared.util.zip.jvm.NonWritableChannelException +import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel + +internal class ReadableChannelAdapter( + private val readable: Readable, + private val wrapError: (ReadError) -> IOException +) : SeekableByteChannel { + + private val coroutineScope: CoroutineScope = + MainScope() + + private var isClosed: Boolean = + false + + private var position: Long = + 0 + + override fun close() { + if (isClosed) { + return + } + + isClosed = true + coroutineScope.launch { readable.close() } + } + + override fun isOpen(): Boolean { + return !isClosed + } + + override fun read(dst: ByteBuffer): Int { + return runBlocking { + if (isClosed) { + throw ClosedChannelException() + } + + withContext(Dispatchers.IO) { + val size = readable.length() + .mapFailure(wrapError) + .getOrThrow() + + if (position >= size) { + return@withContext -1 + } + + val available = size - position + val toBeRead = dst.remaining().coerceAtMost(available.toInt()) + check(toBeRead > 0) + val bytes = readable.read(position until position + toBeRead) + .mapFailure(wrapError) + .getOrThrow() + check(bytes.size == toBeRead) + dst.put(bytes, 0, toBeRead) + position += toBeRead + return@withContext toBeRead + } + } + } + + override fun write(buffer: ByteBuffer): Int { + throw NonWritableChannelException() + } + + override fun position(): Long { + return position + } + + override fun position(newPosition: Long): SeekableByteChannel { + if (isClosed) { + throw ClosedChannelException() + } + + position = newPosition + return this + } + + override fun size(): Long { + if (isClosed) { + throw ClosedChannelException() + } + + return runBlocking { readable.length() } + .mapFailure { wrapError(it) } + .getOrThrow() + } + + override fun truncate(size: Long): SeekableByteChannel { + throw NonWritableChannelException() + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt new file mode 100644 index 0000000000..cdc63c5864 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.zip + +import java.io.File +import java.io.IOException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.readium.r2.shared.extensions.findInstance +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.archive.ArchiveOpener +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.ZipSpecification +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.toUrl +import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile +import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel + +/** + * An [ArchiveOpener] able to open a ZIP archive served through a stream (e.g. HTTP server, + * content URI, etc.). + */ +internal class StreamingZipArchiveProvider { + + suspend fun sniffOpen(source: Readable): Try<Container<Resource>, ArchiveOpener.SniffOpenError> { + return try { + val container = openBlob(source, ::ReadException, null) + Try.success(container) + } catch (exception: Exception) { + exception.findInstance(ReadException::class.java) + ?.let { Try.failure(ArchiveOpener.SniffOpenError.Reading(it.error)) } + ?: Try.failure(ArchiveOpener.SniffOpenError.NotRecognized) + } + } + + suspend fun open( + format: Format, + source: Readable + ): Try<Container<Resource>, ArchiveOpener.OpenError> { + if (!format.conformsTo(ZipSpecification)) { + return Try.failure( + ArchiveOpener.OpenError.FormatNotSupported(format) + ) + } + + return try { + val container = openBlob( + source, + ::ReadException, + (source as? Resource)?.sourceUrl + ) + Try.success(container) + } catch (exception: Exception) { + val error = exception.findInstance(ReadException::class.java) + ?.let { ArchiveOpener.OpenError.Reading(it.error) } + ?: ArchiveOpener.OpenError.Reading(ReadError.Decoding(exception)) + + Try.failure(error) + } + } + + private suspend fun openBlob( + readable: Readable, + wrapError: (ReadError) -> IOException, + sourceUrl: AbsoluteUrl? + ): Container<Resource> = withContext(Dispatchers.IO) { + val datasourceChannel = ReadableChannelAdapter(readable, wrapError) + val channel = wrapBaseChannel(datasourceChannel) + val zipFile = ZipFile(channel, true) + StreamingZipContainer(zipFile, sourceUrl) + } + + internal suspend fun openFile(file: File): Container<Resource> = withContext(Dispatchers.IO) { + val fileChannel = FileChannelAdapter(file, "r") + val channel = wrapBaseChannel(fileChannel) + StreamingZipContainer(ZipFile(channel), file.toUrl()) + } + + private fun wrapBaseChannel(channel: SeekableByteChannel): SeekableByteChannel { + val size = channel.size() + return if (size < CACHE_ALL_MAX_SIZE) { + CachingReadableChannel(channel, 0) + } else { + val cacheStart = size - CACHED_TAIL_SIZE + val cachingChannel = CachingReadableChannel(channel, cacheStart) + cachingChannel.cache() + BufferedReadableChannel(cachingChannel, DEFAULT_BUFFER_SIZE) + } + } + + companion object { + + private const val CACHE_ALL_MAX_SIZE = 5242880 + + private const val CACHED_TAIL_SIZE = 65557 + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt new file mode 100644 index 0000000000..584183d606 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.zip + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.readium.r2.shared.extensions.findInstance +import org.readium.r2.shared.extensions.readFully +import org.readium.r2.shared.extensions.tryOrLog +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.RelativeUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.archive.ArchiveProperties +import org.readium.r2.shared.util.archive.archive +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.data.ReadTry +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.io.CountingInputStream +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipArchiveEntry +import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile + +internal class StreamingZipContainer( + private val zipFile: ZipFile, + override val sourceUrl: AbsoluteUrl? +) : Container<Resource> { + + private inner class Entry( + private val url: Url, + private val entry: ZipArchiveEntry + ) : Resource { + + override val sourceUrl: AbsoluteUrl? get() = null + + override suspend fun properties(): ReadTry<Resource.Properties> = + Try.success( + Resource.Properties { + filename = url.filename + archive = ArchiveProperties( + entryLength = compressedLength + ?: length().getOrElse { return Try.failure(it) }, + isEntryCompressed = compressedLength != null + ) + } + ) + + override suspend fun length(): ReadTry<Long> = + entry.size.takeUnless { it == -1L } + ?.let { Try.success(it) } + ?: Try.failure( + ReadError.UnsupportedOperation( + DebugError("ZIP entry doesn't provide length for entry $url.") + ) + ) + + private val compressedLength: Long? + get() = + if (entry.method == ZipArchiveEntry.STORED || entry.method == -1) { + null + } else { + entry.compressedSize.takeUnless { it == -1L } + } + + override suspend fun read(range: LongRange?): ReadTry<ByteArray> = + withContext(Dispatchers.IO) { + try { + val bytes = + if (range == null) { + readFully() + } else { + readRange(range) + } + Try.success(bytes) + } catch (exception: Exception) { + exception.findInstance(ReadException::class.java) + ?.let { Try.failure(it.error) } + ?: Try.failure(ReadError.Decoding(exception)) + } + } + + private suspend fun readFully(): ByteArray = + zipFile.getInputStream(entry).use { + it.readFully() + } + + private fun readRange(range: LongRange): ByteArray = + stream(range.first).readRange(range) + + /** + * Reading an entry in chunks (e.g. from the HTTP server) can be really slow if the entry + * is deflated in the archive, because we can't jump to an arbitrary offset in a deflated + * stream. This means that we need to read from the start of the entry for each chunk. + * + * To alleviate this issue, we cache a stream which will be reused as long as the chunks are + * requested in order. + * + * See this issue for more info: https://github.com/readium/r2-shared-kotlin/issues/129 + * + * In case of a stored entry, we create a new stream starting at the desired index in order + * to prevent downloading of data until [fromIndex]. + * + */ + private fun stream(fromIndex: Long): CountingInputStream { + if (entry.method == ZipArchiveEntry.STORED && fromIndex < entry.size) { + return CountingInputStream(zipFile.getRawInputStream(entry, fromIndex), fromIndex) + } + + // Reuse the current stream if it didn't exceed the requested index. + stream + ?.takeIf { it.count <= fromIndex } + ?.let { return it } + + stream?.close() + + return CountingInputStream(zipFile.getInputStream(entry)) + .also { stream = it } + } + + private var stream: CountingInputStream? = null + + override suspend fun close() { + tryOrLog { + withContext(Dispatchers.IO) { + stream?.close() + } + } + } + } + + override val entries: Set<Url> = + zipFile.entries.toList() + .filterNot { it.isDirectory } + .mapNotNull { entry -> Url.fromDecodedPath(entry.name) } + .toSet() + + override fun get(url: Url): Resource? = + (url as? RelativeUrl)?.path + ?.let { zipFile.getEntry(it) } + ?.takeUnless { it.isDirectory } + ?.let { Entry(url, it) } + + override suspend fun close() { + withContext(Dispatchers.IO) { + tryOrLog { zipFile.close() } + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt new file mode 100644 index 0000000000..84f4f56c06 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.zip + +import org.readium.r2.shared.util.FileExtension +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.archive.ArchiveOpener +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatSpecification +import org.readium.r2.shared.util.format.ZipSpecification +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource + +public class ZipArchiveOpener : ArchiveOpener { + + private val fileZipArchiveProvider = FileZipArchiveProvider() + + private val streamingZipArchiveProvider = StreamingZipArchiveProvider() + + override suspend fun open( + format: Format, + source: Readable + ): Try<ContainerAsset, ArchiveOpener.OpenError> { + val container = (source as? Resource)?.sourceUrl?.toFile() + ?.let { fileZipArchiveProvider.open(format, it) } + ?: streamingZipArchiveProvider.open(format, source) + + return container.map { ContainerAsset(format, it) } + } + + override suspend fun sniffOpen( + source: Readable + ): Try<ContainerAsset, ArchiveOpener.SniffOpenError> { + val container = (source as? Resource)?.sourceUrl?.toFile() + ?.let { fileZipArchiveProvider.sniffOpen(it) } + ?: streamingZipArchiveProvider.sniffOpen(source) + + return container.map { + ContainerAsset( + format = Format( + specification = FormatSpecification(ZipSpecification), + mediaType = MediaType.ZIP, + fileExtension = FileExtension("zip") + ), + container = it + ) + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/ArchiveEntry.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/ArchiveEntry.java new file mode 100644 index 0000000000..fac7721cc6 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/ArchiveEntry.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.readium.r2.shared.util.zip.compress.archivers; + +import java.util.Date; + +/** + * Represents an entry of an archive. + */ +public interface ArchiveEntry { + + /** Special value indicating that the size is unknown */ + long SIZE_UNKNOWN = -1; + + /** + * Gets the last modified date of this entry. + * + * @return the last modified date of this entry. + * @since 1.1 + */ + Date getLastModifiedDate(); + + /** + * Gets the name of the entry in this archive. May refer to a file or directory or other item. + * + * <p>This method returns the raw name as it is stored inside of the archive.</p> + * + * @return The name of this entry in the archive. + */ + String getName(); + + /** + * Gets the uncompressed size of this entry. May be -1 (SIZE_UNKNOWN) if the size is unknown + * + * @return the uncompressed size of this entry. + */ + long getSize(); + + /** + * Returns true if this entry refers to a directory. + * + * @return true if this entry refers to a directory. + */ + boolean isDirectory(); +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/ArchiveInputStream.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/ArchiveInputStream.java new file mode 100644 index 0000000000..d1a7c7caab --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/ArchiveInputStream.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.readium.r2.shared.util.zip.compress.archivers; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Archive input streams <b>MUST</b> override the + * {@link #read(byte[], int, int)} - or {@link #read()} - + * method so that reading from the stream generates EOF for the end of + * data in each entry as well as at the end of the file proper. + * <p> + * The {@link #getNextEntry()} method is used to reset the input stream + * ready for reading the data from the next entry. + * <p> + * The input stream classes must also implement a method with the signature: + * <pre> + * public static boolean matches(byte[] signature, int length) + * </pre> + * which is used by the ArchiveStreamFactory to autodetect + * the archive type from the first few bytes of a stream. + */ +public abstract class ArchiveInputStream extends InputStream { + + private static final int BYTE_MASK = 0xFF; + private final byte[] single = new byte[1]; + + /** holds the number of bytes read in this stream */ + private long bytesRead; + + /** + * Whether this stream is able to read the given entry. + * + * <p> + * Some archive formats support variants or details that are not supported (yet). + * </p> + * + * @param archiveEntry + * the entry to test + * @return This implementation always returns true. + * + * @since 1.1 + */ + public boolean canReadEntryData(final ArchiveEntry archiveEntry) { + return true; + } + + /* + * Note that subclasses also implement specific get() methods which + * return the appropriate class without need for a cast. + * See SVN revision r743259 + * @return + * @throws IOException + */ + // public abstract XXXArchiveEntry getNextXXXEntry() throws IOException; + + /** + * Increments the counter of already read bytes. + * Doesn't increment if the EOF has been hit (read == -1) + * + * @param read the number of bytes read + */ + protected void count(final int read) { + count((long) read); + } + + /** + * Increments the counter of already read bytes. + * Doesn't increment if the EOF has been hit (read == -1) + * + * @param read the number of bytes read + * @since 1.1 + */ + protected void count(final long read) { + if (read != -1) { + bytesRead = bytesRead + read; + } + } + + /** + * Returns the current number of bytes read from this stream. + * @return the number of read bytes + * @since 1.1 + */ + public long getBytesRead() { + return bytesRead; + } + + /** + * Returns the current number of bytes read from this stream. + * @return the number of read bytes + * @deprecated this method may yield wrong results for large + * archives, use #getBytesRead instead + */ + @Deprecated + public int getCount() { + return (int) bytesRead; + } + + /** + * Returns the next Archive Entry in this Stream. + * + * @return the next entry, + * or {@code null} if there are no more entries + * @throws IOException if the next entry could not be read + */ + public abstract ArchiveEntry getNextEntry() throws IOException; + + /** + * Decrements the counter of already read bytes. + * + * @param pushedBack the number of bytes pushed back. + * @since 1.1 + */ + protected void pushedBackBytes(final long pushedBack) { + bytesRead -= pushedBack; + } + + /** + * Reads a byte of data. This method will block until enough input is + * available. + * + * Simply calls the {@link #read(byte[], int, int)} method. + * + * MUST be overridden if the {@link #read(byte[], int, int)} method + * is not overridden; may be overridden otherwise. + * + * @return the byte read, or -1 if end of input is reached + * @throws IOException + * if an I/O error has occurred + */ + @Override + public int read() throws IOException { + final int num = read(single, 0, 1); + return num == -1 ? -1 : single[0] & BYTE_MASK; + } + +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/ArchiveOutputStream.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/ArchiveOutputStream.java new file mode 100644 index 0000000000..4fc9fad419 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/ArchiveOutputStream.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.readium.r2.shared.util.zip.compress.archivers; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Archive output stream implementations are expected to override the + * {@link #write(byte[], int, int)} method to improve performance. + * They should also override {@link #close()} to ensure that any necessary + * trailers are added. + * + * <p>The normal sequence of calls when working with ArchiveOutputStreams is:</p> + * <ul> + * <li>Create ArchiveOutputStream object,</li> + * <li>optionally write SFX header (Zip only),</li> + * <li>repeat as needed: + * <ul> + * <li>{@link #putArchiveEntry(ArchiveEntry)} (writes entry header), + * <li>{@link #write(byte[])} (writes entry data, as often as needed), + * <li>{@link #closeArchiveEntry()} (closes entry), + * </ul> + * </li> + * <li> {@link #finish()} (ends the addition of entries),</li> + * <li> optionally write additional data, provided format supports it,</li> + * <li>{@link #close()}.</li> + * </ul> + */ +public abstract class ArchiveOutputStream extends OutputStream { + + static final int BYTE_MASK = 0xFF; + /** Temporary buffer used for the {@link #write(int)} method */ + private final byte[] oneByte = new byte[1]; + + /** holds the number of bytes written to this stream */ + private long bytesWritten; + // Methods specific to ArchiveOutputStream + + /** + * Whether this stream is able to write the given entry. + * + * <p>Some archive formats support variants or details that are + * not supported (yet).</p> + * + * @param archiveEntry + * the entry to test + * @return This implementation always returns true. + * @since 1.1 + */ + public boolean canWriteEntryData(final ArchiveEntry archiveEntry) { + return true; + } + + /** + * Closes the archive entry, writing any trailer information that may + * be required. + * @throws IOException if an I/O error occurs + */ + public abstract void closeArchiveEntry() throws IOException; + + /** + * Increments the counter of already written bytes. + * Doesn't increment if EOF has been hit ({@code written == -1}). + * + * @param written the number of bytes written + */ + protected void count(final int written) { + count((long) written); + } + + /** + * Increments the counter of already written bytes. + * Doesn't increment if EOF has been hit ({@code written == -1}). + * + * @param written the number of bytes written + * @since 1.1 + */ + protected void count(final long written) { + if (written != -1) { + bytesWritten = bytesWritten + written; + } + } + + /** + * Create an archive entry using the inputFile and entryName provided. + * + * @param inputFile the file to create the entry from + * @param entryName name to use for the entry + * @return the ArchiveEntry set up with details from the file + * + * @throws IOException if an I/O error occurs + */ + public abstract ArchiveEntry createArchiveEntry(File inputFile, String entryName) throws IOException; + + // Generic implementations of OutputStream methods that may be useful to sub-classes + + /** + * Finishes the addition of entries to this stream, without closing it. + * Additional data can be written, if the format supports it. + * + * @throws IOException if the user forgets to close the entry. + */ + public abstract void finish() throws IOException; + + /** + * Returns the current number of bytes written to this stream. + * @return the number of written bytes + * @since 1.1 + */ + public long getBytesWritten() { + return bytesWritten; + } + + /** + * Returns the current number of bytes written to this stream. + * @return the number of written bytes + * @deprecated this method may yield wrong results for large + * archives, use #getBytesWritten instead + */ + @Deprecated + public int getCount() { + return (int) bytesWritten; + } + + /** + * Writes the headers for an archive entry to the output stream. + * The caller must then write the content to the stream and call + * {@link #closeArchiveEntry()} to complete the process. + * + * @param entry describes the entry + * @throws IOException if an I/O error occurs + */ + public abstract void putArchiveEntry(ArchiveEntry entry) throws IOException; + + /** + * Writes a byte to the current archive entry. + * + * <p>This method simply calls {@code write( byte[], 0, 1 )}. + * + * <p>MUST be overridden if the {@link #write(byte[], int, int)} method + * is not overridden; may be overridden otherwise. + * + * @param b The byte to be written. + * @throws IOException on error + */ + @Override + public void write(final int b) throws IOException { + oneByte[0] = (byte) (b & BYTE_MASK); + write(oneByte, 0, 1); + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/EntryStreamOffsets.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/EntryStreamOffsets.java new file mode 100644 index 0000000000..faf1c296ba --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/EntryStreamOffsets.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.readium.r2.shared.util.zip.compress.archivers; + +/** + * Provides information about ArchiveEntry stream offsets. + */ +public interface EntryStreamOffsets { + + /** Special value indicating that the offset is unknown. */ + long OFFSET_UNKNOWN = -1; + + /** + * Gets the offset of data stream within the archive file, + * + * @return + * the offset of entry data stream, {@code OFFSET_UNKNOWN} if not known. + */ + long getDataOffset(); + + /** + * Indicates whether the stream is contiguous, i.e. not split among + * several archive parts, interspersed with control blocks, etc. + * + * @return + * true if stream is contiguous, false otherwise. + */ + boolean isStreamContiguous(); +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/AbstractUnicodeExtraField.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/AbstractUnicodeExtraField.java new file mode 100644 index 0000000000..318335985b --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/AbstractUnicodeExtraField.java @@ -0,0 +1,193 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.util.zip.CRC32; +import java.util.zip.ZipException; + +/** + * A common base class for Unicode extra information extra fields. + * @NotThreadSafe + */ +public abstract class AbstractUnicodeExtraField implements ZipExtraField { + private long nameCRC32; + private byte[] unicodeName; + private byte[] data; + + protected AbstractUnicodeExtraField() { + } + + /** + * Assemble as unicode extension from the name/comment and + * encoding of the original zip entry. + * + * @param text The file name or comment. + * @param bytes The encoded of the file name or comment in the zip + * file. + */ + protected AbstractUnicodeExtraField(final String text, final byte[] bytes) { + this(text, bytes, 0, bytes.length); + } + + /** + * Assemble as unicode extension from the name/comment and + * encoding of the original zip entry. + * + * @param text The file name or comment. + * @param bytes The encoded of the file name or comment in the zip + * file. + * @param off The offset of the encoded file name or comment in + * {@code bytes}. + * @param len The length of the encoded file name or comment in + * {@code bytes}. + */ + protected AbstractUnicodeExtraField(final String text, final byte[] bytes, final int off, final int len) { + final CRC32 crc32 = new CRC32(); + crc32.update(bytes, off, len); + nameCRC32 = crc32.getValue(); + + unicodeName = text.getBytes(UTF_8); + } + + private void assembleData() { + if (unicodeName == null) { + return; + } + + data = new byte[5 + unicodeName.length]; + // version 1 + data[0] = 0x01; + System.arraycopy(ZipLong.getBytes(nameCRC32), 0, data, 1, 4); + System.arraycopy(unicodeName, 0, data, 5, unicodeName.length); + } + + @Override + public byte[] getCentralDirectoryData() { + if (data == null) { + this.assembleData(); + } + byte[] b = null; + if (data != null) { + b = new byte[data.length]; + System.arraycopy(data, 0, b, 0, b.length); + } + return b; + } + + @Override + public ZipShort getCentralDirectoryLength() { + if (data == null) { + assembleData(); + } + return new ZipShort(data != null ? data.length : 0); + } + + @Override + public byte[] getLocalFileDataData() { + return getCentralDirectoryData(); + } + + @Override + public ZipShort getLocalFileDataLength() { + return getCentralDirectoryLength(); + } + + /** + * @return The CRC32 checksum of the file name or comment as + * encoded in the central directory of the zip file. + */ + public long getNameCRC32() { + return nameCRC32; + } + + /** + * @return The UTF-8 encoded name. + */ + public byte[] getUnicodeName() { + byte[] b = null; + if (unicodeName != null) { + b = new byte[unicodeName.length]; + System.arraycopy(unicodeName, 0, b, 0, b.length); + } + return b; + } + + /** + * Doesn't do anything special since this class always uses the + * same data in central directory and local file data. + */ + @Override + public void parseFromCentralDirectoryData(final byte[] buffer, final int offset, + final int length) + throws ZipException { + parseFromLocalFileData(buffer, offset, length); + } + + @Override + public void parseFromLocalFileData(final byte[] buffer, final int offset, final int length) + throws ZipException { + + if (length < 5) { + throw new ZipException("UniCode path extra data must have at least 5 bytes."); + } + + final int version = buffer[offset]; + + if (version != 0x01) { + throw new ZipException("Unsupported version [" + version + + "] for UniCode path extra data."); + } + + nameCRC32 = ZipLong.getValue(buffer, offset + 1); + unicodeName = new byte[length - 5]; + System.arraycopy(buffer, offset + 5, unicodeName, 0, length - 5); + data = null; + } + + /** + * @param nameCRC32 The CRC32 checksum of the file name as encoded + * in the central directory of the zip file to set. + */ + public void setNameCRC32(final long nameCRC32) { + this.nameCRC32 = nameCRC32; + data = null; + } + + /** + * @param unicodeName The UTF-8 encoded name to set. + */ + public void setUnicodeName(final byte[] unicodeName) { + if (unicodeName != null) { + this.unicodeName = new byte[unicodeName.length]; + System.arraycopy(unicodeName, 0, this.unicodeName, 0, + unicodeName.length); + } else { + this.unicodeName = null; + } + data = null; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/CharsetAccessor.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/CharsetAccessor.java new file mode 100644 index 0000000000..c1ff6936bf --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/CharsetAccessor.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.nio.charset.Charset; + +/** + * An interface added to allow access to the character set associated with an {@link NioZipEncoding}, + * without requiring a new method to be added to {@link ZipEncoding}. + * <p> + * This avoids introducing a + * potentially breaking change, or making {@link NioZipEncoding} a public class. + * </p> + * @since 1.15 + */ +public interface CharsetAccessor { + + /** + * Provides access to the character set associated with an object. + * <p> + * This allows nio oriented code to use more natural character encoding/decoding methods, + * whilst allowing existing code to continue to rely on special-case error handling for UTF-8. + * </p> + * @return the character set associated with this object + */ + Charset getCharset(); +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ExtraFieldParsingBehavior.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ExtraFieldParsingBehavior.java new file mode 100644 index 0000000000..a3002d28cd --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ExtraFieldParsingBehavior.java @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.util.zip.ZipException; + +/** + * Controls details of parsing zip extra fields. + * + * @since 1.19 + */ +public interface ExtraFieldParsingBehavior extends UnparseableExtraFieldBehavior { + /** + * Creates an instance of ZipExtraField for the given id. + * + * <p>A good default implementation would be {@link + * ExtraFieldUtils#createExtraField}.</p> + * + * @param headerId the id for the extra field + * @return an instance of ZipExtraField, must not be {@code null} + * @throws ZipException if an error occurs + * @throws InstantiationException if unable to instantiate the class + * @throws IllegalAccessException if not allowed to instantiate the class + */ + ZipExtraField createExtraField(final ZipShort headerId) + throws ZipException, InstantiationException, IllegalAccessException; + + /** + * Fills in the extra field data for a single extra field. + * + * <p>A good default implementation would be {@link + * ExtraFieldUtils#fillExtraField}.</p> + * + * @param field the extra field instance to fill + * @param data the array of extra field data + * @param off offset into data where this field's data starts + * @param len the length of this field's data + * @param local whether the extra field data stems from the local + * file header. If this is false then the data is part if the + * central directory header extra data. + * @return the filled field. Usually this is the same as {@code + * field} but it could be a replacement extra field as well. Must + * not be {@code null}. + * @throws ZipException if an error occurs + */ + ZipExtraField fill(ZipExtraField field, byte[] data, int off, int len, boolean local) + throws ZipException; +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ExtraFieldUtils.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ExtraFieldUtils.java new file mode 100644 index 0000000000..d69a0da0f2 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ExtraFieldUtils.java @@ -0,0 +1,414 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.zip.ZipException; + +/** + * ZipExtraField related methods + * @NotThreadSafe because the HashMap is not synch. + */ +// CheckStyle:HideUtilityClassConstructorCheck OFF (bc) +public class ExtraFieldUtils { + + /** + * "enum" for the possible actions to take if the extra field + * cannot be parsed. + * + * <p>This class has been created long before Java 5 and would + * have been a real enum ever since.</p> + * + * @since 1.1 + */ + public static final class UnparseableExtraField implements UnparseableExtraFieldBehavior { + /** + * Key for "throw an exception" action. + */ + public static final int THROW_KEY = 0; + /** + * Key for "skip" action. + */ + public static final int SKIP_KEY = 1; + /** + * Key for "read" action. + */ + public static final int READ_KEY = 2; + + /** + * Throw an exception if field cannot be parsed. + */ + public static final UnparseableExtraField THROW + = new UnparseableExtraField(THROW_KEY); + + /** + * Skip the extra field entirely and don't make its data + * available - effectively removing the extra field data. + */ + public static final UnparseableExtraField SKIP + = new UnparseableExtraField(SKIP_KEY); + + /** + * Read the extra field data into an instance of {@link + * UnparseableExtraFieldData UnparseableExtraFieldData}. + */ + public static final UnparseableExtraField READ + = new UnparseableExtraField(READ_KEY); + + private final int key; + + private UnparseableExtraField(final int k) { + key = k; + } + + /** + * Key of the action to take. + * @return the key + */ + public int getKey() { return key; } + + @Override + public ZipExtraField onUnparseableExtraField(final byte[] data, final int off, final int len, final boolean local, + final int claimedLength) throws ZipException { + switch(key) { + case THROW_KEY: + throw new ZipException("Bad extra field starting at " + + off + ". Block length of " + + claimedLength + " bytes exceeds remaining" + + " data of " + + (len - WORD) + + " bytes."); + case READ_KEY: + final UnparseableExtraFieldData field = new UnparseableExtraFieldData(); + if (local) { + field.parseFromLocalFileData(data, off, len); + } else { + field.parseFromCentralDirectoryData(data, off, len); + } + return field; + case SKIP_KEY: + return null; + default: + throw new ZipException("Unknown UnparseableExtraField key: " + key); + } + } + + } + + private static final int WORD = 4; + + /** + * Static registry of known extra fields. + */ + private static final Map<ZipShort, Class<?>> IMPLEMENTATIONS; + + static { + IMPLEMENTATIONS = new ConcurrentHashMap<>(); + } + + static final ZipExtraField[] EMPTY_ZIP_EXTRA_FIELD_ARRAY = {}; + + /** + * Create an instance of the appropriate ExtraField, falls back to + * {@link UnrecognizedExtraField UnrecognizedExtraField}. + * @param headerId the header identifier + * @return an instance of the appropriate ExtraField + * @throws InstantiationException if unable to instantiate the class + * @throws IllegalAccessException if not allowed to instantiate the class + */ + public static ZipExtraField createExtraField(final ZipShort headerId) + throws InstantiationException, IllegalAccessException { + final ZipExtraField field = createExtraFieldNoDefault(headerId); + if (field != null) { + return field; + } + final UnrecognizedExtraField u = new UnrecognizedExtraField(); + u.setHeaderId(headerId); + return u; + } + + /** + * Create an instance of the appropriate ExtraField. + * @param headerId the header identifier + * @return an instance of the appropriate ExtraField or null if + * the id is not supported + * @throws InstantiationException if unable to instantiate the class + * @throws IllegalAccessException if not allowed to instantiate the class + * @since 1.19 + */ + public static ZipExtraField createExtraFieldNoDefault(final ZipShort headerId) + throws InstantiationException, IllegalAccessException { + final Class<?> c = IMPLEMENTATIONS.get(headerId); + if (c != null) { + return (ZipExtraField) c.newInstance(); + } + return null; + } + + /** + * Fills in the extra field data into the given instance. + * + * <p>Calls {@link ZipExtraField#parseFromCentralDirectoryData} or {@link ZipExtraField#parseFromLocalFileData} internally and wraps any {@link ArrayIndexOutOfBoundsException} thrown into a {@link ZipException}.</p> + * + * @param ze the extra field instance to fill + * @param data the array of extra field data + * @param off offset into data where this field's data starts + * @param len the length of this field's data + * @param local whether the extra field data stems from the local + * file header. If this is false then the data is part if the + * central directory header extra data. + * @return the filled field, will never be {@code null} + * @throws ZipException if an error occurs + * + * @since 1.19 + */ + public static ZipExtraField fillExtraField(final ZipExtraField ze, final byte[] data, final int off, + final int len, final boolean local) throws ZipException { + try { + if (local) { + ze.parseFromLocalFileData(data, off, len); + } else { + ze.parseFromCentralDirectoryData(data, off, len); + } + return ze; + } catch (final ArrayIndexOutOfBoundsException aiobe) { + throw (ZipException) new ZipException("Failed to parse corrupt ZIP extra field of type " + + Integer.toHexString(ze.getHeaderId().getValue())).initCause(aiobe); + } + } + + /** + * Merges the central directory fields of the given ZipExtraFields. + * @param data an array of ExtraFields + * @return an array of bytes + */ + public static byte[] mergeCentralDirectoryData(final ZipExtraField[] data) { + final int dataLength = data.length; + final boolean lastIsUnparseableHolder = dataLength > 0 + && data[dataLength - 1] instanceof UnparseableExtraFieldData; + final int regularExtraFieldCount = + lastIsUnparseableHolder ? dataLength - 1 : dataLength; + + int sum = WORD * regularExtraFieldCount; + for (final ZipExtraField element : data) { + sum += element.getCentralDirectoryLength().getValue(); + } + final byte[] result = new byte[sum]; + int start = 0; + for (int i = 0; i < regularExtraFieldCount; i++) { + System.arraycopy(data[i].getHeaderId().getBytes(), + 0, result, start, 2); + System.arraycopy(data[i].getCentralDirectoryLength().getBytes(), + 0, result, start + 2, 2); + start += WORD; + final byte[] central = data[i].getCentralDirectoryData(); + if (central != null) { + System.arraycopy(central, 0, result, start, central.length); + start += central.length; + } + } + if (lastIsUnparseableHolder) { + final byte[] central = data[dataLength - 1].getCentralDirectoryData(); + if (central != null) { + System.arraycopy(central, 0, result, start, central.length); + } + } + return result; + } + + /** + * Merges the local file data fields of the given ZipExtraFields. + * @param data an array of ExtraFiles + * @return an array of bytes + */ + public static byte[] mergeLocalFileDataData(final ZipExtraField[] data) { + final int dataLength = data.length; + final boolean lastIsUnparseableHolder = dataLength > 0 + && data[dataLength - 1] instanceof UnparseableExtraFieldData; + final int regularExtraFieldCount = + lastIsUnparseableHolder ? dataLength - 1 : dataLength; + + int sum = WORD * regularExtraFieldCount; + for (final ZipExtraField element : data) { + sum += element.getLocalFileDataLength().getValue(); + } + + final byte[] result = new byte[sum]; + int start = 0; + for (int i = 0; i < regularExtraFieldCount; i++) { + System.arraycopy(data[i].getHeaderId().getBytes(), + 0, result, start, 2); + System.arraycopy(data[i].getLocalFileDataLength().getBytes(), + 0, result, start + 2, 2); + start += WORD; + final byte[] local = data[i].getLocalFileDataData(); + if (local != null) { + System.arraycopy(local, 0, result, start, local.length); + start += local.length; + } + } + if (lastIsUnparseableHolder) { + final byte[] local = data[dataLength - 1].getLocalFileDataData(); + if (local != null) { + System.arraycopy(local, 0, result, start, local.length); + } + } + return result; + } + + /** + * Split the array into ExtraFields and populate them with the + * given data as local file data, throwing an exception if the + * data cannot be parsed. + * @param data an array of bytes as it appears in local file data + * @return an array of ExtraFields + * @throws ZipException on error + */ + public static ZipExtraField[] parse(final byte[] data) throws ZipException { + return parse(data, true, UnparseableExtraField.THROW); + } + + /** + * Split the array into ExtraFields and populate them with the + * given data, throwing an exception if the data cannot be parsed. + * @param data an array of bytes + * @param local whether data originates from the local file data + * or the central directory + * @return an array of ExtraFields + * @throws ZipException on error + */ + public static ZipExtraField[] parse(final byte[] data, final boolean local) + throws ZipException { + return parse(data, local, UnparseableExtraField.THROW); + } + + /** + * Split the array into ExtraFields and populate them with the + * given data. + * @param data an array of bytes + * @param parsingBehavior controls parsing of extra fields. + * @param local whether data originates from the local file data + * or the central directory + * @return an array of ExtraFields + * @throws ZipException on error + * + * @since 1.19 + */ + public static ZipExtraField[] parse(final byte[] data, final boolean local, + final ExtraFieldParsingBehavior parsingBehavior) + throws ZipException { + final List<ZipExtraField> v = new ArrayList<>(); + int start = 0; + final int dataLength = data.length; + LOOP: + while (start <= dataLength - WORD) { + final ZipShort headerId = new ZipShort(data, start); + final int length = new ZipShort(data, start + 2).getValue(); + if (start + WORD + length > dataLength) { + final ZipExtraField field = parsingBehavior.onUnparseableExtraField(data, start, dataLength - start, + local, length); + if (field != null) { + v.add(field); + } + // since we cannot parse the data we must assume + // the extra field consumes the whole rest of the + // available data + break LOOP; + } + try { + final ZipExtraField ze = Objects.requireNonNull(parsingBehavior.createExtraField(headerId), + "createExtraField must not return null"); + v.add(Objects.requireNonNull(parsingBehavior.fill(ze, data, start + WORD, length, local), + "fill must not return null")); + start += length + WORD; + } catch (final InstantiationException | IllegalAccessException ie) { + throw (ZipException) new ZipException(ie.getMessage()).initCause(ie); + } + } + + return v.toArray(EMPTY_ZIP_EXTRA_FIELD_ARRAY); + } + + /** + * Split the array into ExtraFields and populate them with the + * given data. + * @param data an array of bytes + * @param local whether data originates from the local file data + * or the central directory + * @param onUnparseableData what to do if the extra field data + * cannot be parsed. + * @return an array of ExtraFields + * @throws ZipException on error + * + * @since 1.1 + */ + public static ZipExtraField[] parse(final byte[] data, final boolean local, + final UnparseableExtraField onUnparseableData) + throws ZipException { + return parse(data, local, new ExtraFieldParsingBehavior() { + @Override + public ZipExtraField createExtraField(final ZipShort headerId) + throws ZipException, InstantiationException, IllegalAccessException { + return ExtraFieldUtils.createExtraField(headerId); + } + + @Override + public ZipExtraField fill(final ZipExtraField field, final byte[] data, final int off, final int len, final boolean local) + throws ZipException { + return fillExtraField(field, data, off, len, local); + } + + @Override + public ZipExtraField onUnparseableExtraField(final byte[] data, final int off, final int len, final boolean local, + final int claimedLength) throws ZipException { + return onUnparseableData.onUnparseableExtraField(data, off, len, local, claimedLength); + } + }); + } + + /** + * Register a ZipExtraField implementation. + * + * <p>The given class must have a no-arg constructor and implement + * the {@link ZipExtraField ZipExtraField interface}.</p> + * @param c the class to register + */ + public static void register(final Class<?> c) { + try { + final ZipExtraField ze = (ZipExtraField) c.newInstance(); + IMPLEMENTATIONS.put(ze.getHeaderId(), c); + } catch (final ClassCastException cc) { // NOSONAR + throw new IllegalArgumentException(c + " doesn't implement ZipExtraField"); //NOSONAR + } catch (final InstantiationException ie) { // NOSONAR + throw new IllegalArgumentException(c + " is not a concrete class"); //NOSONAR + } catch (final IllegalAccessException ie) { // NOSONAR + throw new IllegalArgumentException(c + "'s no-arg constructor is not public"); //NOSONAR + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/GeneralPurposeBit.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/GeneralPurposeBit.java new file mode 100644 index 0000000000..61fed42c14 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/GeneralPurposeBit.java @@ -0,0 +1,252 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +/** + * Parser/encoder for the "general purpose bit" field in ZIP's local + * file and central directory headers. + * + * @since 1.1 + * @NotThreadSafe + */ +public final class GeneralPurposeBit implements Cloneable { + + /** + * Indicates that the file is encrypted. + */ + private static final int ENCRYPTION_FLAG = 1 << 0; + + /** + * Indicates the size of the sliding dictionary used by the compression method 6 (imploding). + * <ul> + * <li>0: 4096 bytes</li> + * <li>1: 8192 bytes</li> + * </ul> + */ + private static final int SLIDING_DICTIONARY_SIZE_FLAG = 1 << 1; + + /** + * Indicates the number of Shannon-Fano trees used by the compression method 6 (imploding). + * <ul> + * <li>0: 2 trees (lengths, distances)</li> + * <li>1: 3 trees (literals, lengths, distances)</li> + * </ul> + */ + private static final int NUMBER_OF_SHANNON_FANO_TREES_FLAG = 1 << 2; + + /** + * Indicates that a data descriptor stored after the file contents + * will hold CRC and size information. + */ + private static final int DATA_DESCRIPTOR_FLAG = 1 << 3; + + /** + * Indicates strong encryption. + */ + private static final int STRONG_ENCRYPTION_FLAG = 1 << 6; + + /** + * Indicates that file names are written in UTF-8. + * + * <p>The only reason this is public is that {@link + * ZipArchiveOutputStream#EFS_FLAG} was public in Apache Commons + * Compress 1.0 and we needed a substitute for it.</p> + */ + public static final int UFT8_NAMES_FLAG = 1 << 11; + + /** + * Parses the supported flags from the given archive data. + * + * @param data local file header or a central directory entry. + * @param offset offset at which the general purpose bit starts + * @return parsed flags + */ + public static GeneralPurposeBit parse(final byte[] data, final int offset) { + final int generalPurposeFlag = ZipShort.getValue(data, offset); + final GeneralPurposeBit b = new GeneralPurposeBit(); + b.useDataDescriptor((generalPurposeFlag & DATA_DESCRIPTOR_FLAG) != 0); + b.useUTF8ForNames((generalPurposeFlag & UFT8_NAMES_FLAG) != 0); + b.useStrongEncryption((generalPurposeFlag & STRONG_ENCRYPTION_FLAG) != 0); + b.useEncryption((generalPurposeFlag & ENCRYPTION_FLAG) != 0); + b.slidingDictionarySize = (generalPurposeFlag & SLIDING_DICTIONARY_SIZE_FLAG) != 0 ? 8192 : 4096; + b.numberOfShannonFanoTrees = (generalPurposeFlag & NUMBER_OF_SHANNON_FANO_TREES_FLAG) != 0 ? 3 : 2; + return b; + } + private boolean languageEncodingFlag; + private boolean dataDescriptorFlag; + private boolean encryptionFlag; + private boolean strongEncryptionFlag; + private int slidingDictionarySize; + + private int numberOfShannonFanoTrees; + + public GeneralPurposeBit() { + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (final CloneNotSupportedException ex) { + // impossible + throw new IllegalStateException("GeneralPurposeBit is not Cloneable?", ex); //NOSONAR + } + } + + /** + * Encodes the set bits in a form suitable for ZIP archives. + * @return the encoded general purpose bits + */ + public byte[] encode() { + final byte[] result = new byte[2]; + encode(result, 0); + return result; + } + + /** + * Encodes the set bits in a form suitable for ZIP archives. + * + * @param buf the output buffer + * @param offset + * The offset within the output buffer of the first byte to be written. + * must be non-negative and no larger than {@code buf.length-2} + */ + public void encode(final byte[] buf, final int offset) { + ZipShort.putShort((dataDescriptorFlag ? DATA_DESCRIPTOR_FLAG : 0) + | + (languageEncodingFlag ? UFT8_NAMES_FLAG : 0) + | + (encryptionFlag ? ENCRYPTION_FLAG : 0) + | + (strongEncryptionFlag ? STRONG_ENCRYPTION_FLAG : 0) + , buf, offset); + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof GeneralPurposeBit)) { + return false; + } + final GeneralPurposeBit g = (GeneralPurposeBit) o; + return g.encryptionFlag == encryptionFlag + && g.strongEncryptionFlag == strongEncryptionFlag + && g.languageEncodingFlag == languageEncodingFlag + && g.dataDescriptorFlag == dataDescriptorFlag; + } + + /** + * Returns the number of trees used by the compression method 6 (imploding). + */ + int getNumberOfShannonFanoTrees() { + return numberOfShannonFanoTrees; + } + + /** + * Returns the sliding dictionary size used by the compression method 6 (imploding). + */ + int getSlidingDictionarySize() { + return slidingDictionarySize; + } + + @Override + public int hashCode() { + return 3 * (7 * (13 * (17 * (encryptionFlag ? 1 : 0) + + (strongEncryptionFlag ? 1 : 0)) + + (languageEncodingFlag ? 1 : 0)) + + (dataDescriptorFlag ? 1 : 0)); + } + + /** + * whether the current entry will use the data descriptor to store + * CRC and size information. + * @param b whether the current entry will use the data descriptor to store + * CRC and size information + */ + public void useDataDescriptor(final boolean b) { + dataDescriptorFlag = b; + } + + /** + * whether the current entry will be encrypted. + * @param b whether the current entry will be encrypted + */ + public void useEncryption(final boolean b) { + encryptionFlag = b; + } + + /** + * whether the current entry uses the data descriptor to store CRC + * and size information. + * @return whether the current entry uses the data descriptor to store CRC + * and size information + */ + public boolean usesDataDescriptor() { + return dataDescriptorFlag; + } + + + /** + * whether the current entry is encrypted. + * @return whether the current entry is encrypted + */ + public boolean usesEncryption() { + return encryptionFlag; + } + + /** + * whether the current entry is encrypted using strong encryption. + * @return whether the current entry is encrypted using strong encryption + */ + public boolean usesStrongEncryption() { + return encryptionFlag && strongEncryptionFlag; + } + + /** + * whether the current entry will be encrypted using strong encryption. + * @param b whether the current entry will be encrypted using strong encryption + */ + public void useStrongEncryption(final boolean b) { + strongEncryptionFlag = b; + if (b) { + useEncryption(true); + } + } + + /** + * whether the current entry uses UTF8 for file name and comment. + * @return whether the current entry uses UTF8 for file name and comment. + */ + public boolean usesUTF8ForNames() { + return languageEncodingFlag; + } + + /** + * whether the current entry will use UTF8 for file name and comment. + * @param b whether the current entry will use UTF8 for file name and comment. + */ + public void useUTF8ForNames(final boolean b) { + languageEncodingFlag = b; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/InflaterInputStreamWithStatistics.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/InflaterInputStreamWithStatistics.java new file mode 100644 index 0000000000..1436b6dd70 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/InflaterInputStreamWithStatistics.java @@ -0,0 +1,89 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import org.readium.r2.shared.util.zip.compress.utils.InputStreamStatistics; + +/** + * Helper class to provide statistics + * + * @since 1.17 + */ +/* package */ class InflaterInputStreamWithStatistics extends InflaterInputStream + implements InputStreamStatistics { + private long compressedCount; + private long uncompressedCount; + + public InflaterInputStreamWithStatistics(final InputStream in) { + super(in); + } + + public InflaterInputStreamWithStatistics(final InputStream in, final Inflater inf) { + super(in, inf); + } + + public InflaterInputStreamWithStatistics(final InputStream in, final Inflater inf, final int size) { + super(in, inf, size); + } + + @Override + protected void fill() throws IOException { + super.fill(); + compressedCount += inf.getRemaining(); + } + + @Override + public long getCompressedCount() { + return compressedCount; + } + + @Override + public long getUncompressedCount() { + return uncompressedCount; + } + + @Override + public int read() throws IOException { + final int b = super.read(); + if (b > -1) { + uncompressedCount++; + } + return b; + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + final int bytes = super.read(b, off, len); + if (bytes > -1) { + uncompressedCount += bytes; + } + return bytes; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/NioZipEncoding.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/NioZipEncoding.java new file mode 100644 index 0000000000..874173f894 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/NioZipEncoding.java @@ -0,0 +1,223 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; + +/** + * A ZipEncoding, which uses a java.nio {@link + * java.nio.charset.Charset Charset} to encode names. + * <p>The methods of this class are reentrant.</p> + * @Immutable + */ +class NioZipEncoding implements ZipEncoding, CharsetAccessor { + + private static final char REPLACEMENT = '?'; + private static final byte[] REPLACEMENT_BYTES = { (byte) REPLACEMENT }; + private static final String REPLACEMENT_STRING = String.valueOf(REPLACEMENT); + private static final char[] HEX_CHARS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + private static ByteBuffer encodeFully(final CharsetEncoder enc, final CharBuffer cb, final ByteBuffer out) { + ByteBuffer o = out; + while (cb.hasRemaining()) { + final CoderResult result = enc.encode(cb, o, false); + if (result.isOverflow()) { + final int increment = estimateIncrementalEncodingSize(enc, cb.remaining()); + o = ZipEncodingHelper.growBufferBy(o, increment); + } + } + return o; + } + private static CharBuffer encodeSurrogate(final CharBuffer cb, final char c) { + cb.position(0).limit(6); + cb.put('%'); + cb.put('U'); + + cb.put(HEX_CHARS[(c >> 12) & 0x0f]); + cb.put(HEX_CHARS[(c >> 8) & 0x0f]); + cb.put(HEX_CHARS[(c >> 4) & 0x0f]); + cb.put(HEX_CHARS[c & 0x0f]); + cb.flip(); + return cb; + } + + + /** + * Estimate the size needed for remaining characters + * + * @param enc encoder to use for estimates + * @param charCount number of characters remaining + * @return estimated size in bytes. + */ + private static int estimateIncrementalEncodingSize(final CharsetEncoder enc, final int charCount) { + return (int) Math.ceil(charCount * enc.averageBytesPerChar()); + } + + /** + * Estimate the initial encoded size (in bytes) for a character buffer. + * <p> + * The estimate assumes that one character consumes uses the maximum length encoding, + * whilst the rest use an average size encoding. This accounts for any BOM for UTF-16, at + * the expense of a couple of extra bytes for UTF-8 encoded ASCII. + * </p> + * + * @param enc encoder to use for estimates + * @param charChount number of characters in string + * @return estimated size in bytes. + */ + private static int estimateInitialBufferSize(final CharsetEncoder enc, final int charChount) { + final float first = enc.maxBytesPerChar(); + final float rest = (charChount - 1) * enc.averageBytesPerChar(); + return (int) Math.ceil(first + rest); + } + + private final Charset charset; + + private final boolean useReplacement; + + /** + * Construct an NioZipEncoding using the given charset. + * @param charset The character set to use. + * @param useReplacement should invalid characters be replaced, or reported. + */ + NioZipEncoding(final Charset charset, final boolean useReplacement) { + this.charset = charset; + this.useReplacement = useReplacement; + } + + /** + * @see ZipEncoding#canEncode(String) + */ + @Override + public boolean canEncode(final String name) { + final CharsetEncoder enc = newEncoder(); + + return enc.canEncode(name); + } + + /** + * @see + * ZipEncoding#decode(byte[]) + */ + @Override + public String decode(final byte[] data) throws IOException { + return newDecoder() + .decode(ByteBuffer.wrap(data)).toString(); + } + + /** + * @see ZipEncoding#encode(String) + */ + @Override + public ByteBuffer encode(final String name) { + final CharsetEncoder enc = newEncoder(); + + final CharBuffer cb = CharBuffer.wrap(name); + CharBuffer tmp = null; + ByteBuffer out = ByteBuffer.allocate(estimateInitialBufferSize(enc, cb.remaining())); + + while (cb.hasRemaining()) { + final CoderResult res = enc.encode(cb, out, false); + + if (res.isUnmappable() || res.isMalformed()) { + + // write the unmappable characters in utf-16 + // pseudo-URL encoding style to ByteBuffer. + + final int spaceForSurrogate = estimateIncrementalEncodingSize(enc, 6 * res.length()); + if (spaceForSurrogate > out.remaining()) { + // if the destination buffer isn't over sized, assume that the presence of one + // unmappable character makes it likely that there will be more. Find all the + // un-encoded characters and allocate space based on those estimates. + int charCount = 0; + for (int i = cb.position() ; i < cb.limit(); i++) { + charCount += !enc.canEncode(cb.get(i)) ? 6 : 1; + } + final int totalExtraSpace = estimateIncrementalEncodingSize(enc, charCount); + out = ZipEncodingHelper.growBufferBy(out, totalExtraSpace - out.remaining()); + } + if (tmp == null) { + tmp = CharBuffer.allocate(6); + } + for (int i = 0; i < res.length(); ++i) { + out = encodeFully(enc, encodeSurrogate(tmp, cb.get()), out); + } + + } else if (res.isOverflow()) { + final int increment = estimateIncrementalEncodingSize(enc, cb.remaining()); + out = ZipEncodingHelper.growBufferBy(out, increment); + + } else if (res.isUnderflow() || res.isError()) { + break; + } + } + // tell the encoder we are done + enc.encode(cb, out, true); + // may have caused underflow, but that's been ignored traditionally + + out.limit(out.position()); + out.rewind(); + return out; + } + + @Override + public Charset getCharset() { + return charset; + } + + private CharsetDecoder newDecoder() { + if (!useReplacement) { + return this.charset.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + } + return charset.newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE) + .replaceWith(REPLACEMENT_STRING); + } + + private CharsetEncoder newEncoder() { + if (useReplacement) { + return charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE) + .replaceWith(REPLACEMENT_BYTES); + } + return charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + } + +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ResourceAlignmentExtraField.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ResourceAlignmentExtraField.java new file mode 100644 index 0000000000..06be121b4f --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ResourceAlignmentExtraField.java @@ -0,0 +1,147 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + + +import java.util.zip.ZipException; + +/** + * An extra field who's sole purpose is to align and pad the local file header + * so that the entry's data starts at a certain position. + * + * <p>The padding content of the padding is ignored and not retained + * when reading a padding field.</p> + * + * <p>This enables Commons Compress to create "aligned" archives + * similar to Android's zipalign command line tool.</p> + * + * @since 1.14 + * @see "https://developer.android.com/studio/command-line/zipalign.html" + * @see ZipArchiveEntry#setAlignment + */ +public class ResourceAlignmentExtraField implements ZipExtraField { + + /** + * Extra field id used for storing alignment and padding. + */ + public static final ZipShort ID = new ZipShort(0xa11e); + + public static final int BASE_SIZE = 2; + + private static final int ALLOW_METHOD_MESSAGE_CHANGE_FLAG = 0x8000; + + private short alignment; + + private boolean allowMethodChange; + + private int padding; + + public ResourceAlignmentExtraField() { + } + + public ResourceAlignmentExtraField(final int alignment) { + this(alignment, false); + } + + public ResourceAlignmentExtraField(final int alignment, final boolean allowMethodChange) { + this(alignment, allowMethodChange, 0); + } + + public ResourceAlignmentExtraField(final int alignment, final boolean allowMethodChange, final int padding) { + if (alignment < 0 || alignment > 0x7fff) { + throw new IllegalArgumentException("Alignment must be between 0 and 0x7fff, was: " + alignment); + } + if (padding < 0) { + throw new IllegalArgumentException("Padding must not be negative, was: " + padding); + } + this.alignment = (short) alignment; + this.allowMethodChange = allowMethodChange; + this.padding = padding; + } + + /** + * Indicates whether method change is allowed when re-compressing the ZIP file. + * + * @return + * true if method change is allowed, false otherwise. + */ + public boolean allowMethodChange() { + return allowMethodChange; + } + + /** + * Gets requested alignment. + * + * @return + * requested alignment. + */ + public short getAlignment() { + return alignment; + } + + @Override + public byte[] getCentralDirectoryData() { + return ZipShort.getBytes(alignment | (allowMethodChange ? ALLOW_METHOD_MESSAGE_CHANGE_FLAG : 0)); + } + + @Override + public ZipShort getCentralDirectoryLength() { + return new ZipShort(BASE_SIZE); + } + + @Override + public ZipShort getHeaderId() { + return ID; + } + + @Override + public byte[] getLocalFileDataData() { + final byte[] content = new byte[BASE_SIZE + padding]; + ZipShort.putShort(alignment | (allowMethodChange ? ALLOW_METHOD_MESSAGE_CHANGE_FLAG : 0), + content, 0); + return content; + } + + @Override + public ZipShort getLocalFileDataLength() { + return new ZipShort(BASE_SIZE + padding); + } + + @Override + public void parseFromCentralDirectoryData(final byte[] buffer, final int offset, final int length) throws ZipException { + if (length < BASE_SIZE) { + throw new ZipException("Too short content for ResourceAlignmentExtraField (0xa11e): " + length); + } + final int alignmentValue = ZipShort.getValue(buffer, offset); + this.alignment = (short) (alignmentValue & (ALLOW_METHOD_MESSAGE_CHANGE_FLAG - 1)); + this.allowMethodChange = (alignmentValue & ALLOW_METHOD_MESSAGE_CHANGE_FLAG) != 0; + } + + @Override + public void parseFromLocalFileData(final byte[] buffer, final int offset, final int length) throws ZipException { + parseFromCentralDirectoryData(buffer, offset, length); + this.padding = length - BASE_SIZE; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ScatterStatistics.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ScatterStatistics.java new file mode 100644 index 0000000000..6228f1e7a5 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ScatterStatistics.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + package org.readium.r2.shared.util.zip.compress.archivers.zip; + +/** + * Provides information about a scatter compression run. + * + * @since 1.10 + */ +public class ScatterStatistics { + private final long compressionElapsed; + private final long mergingElapsed; + + ScatterStatistics(final long compressionElapsed, final long mergingElapsed) { + this.compressionElapsed = compressionElapsed; + this.mergingElapsed = mergingElapsed; + } + + /** + * The number of milliseconds elapsed in the parallel compression phase + * @return The number of milliseconds elapsed + */ + public long getCompressionElapsed() { + return compressionElapsed; + } + + /** + * The number of milliseconds elapsed in merging the results of the parallel compression, the IO phase + * @return The number of milliseconds elapsed + */ + public long getMergingElapsed() { + return mergingElapsed; + } + + @Override + public String toString() { + return "compressionElapsed=" + compressionElapsed + "ms, mergingElapsed=" + mergingElapsed + "ms"; + } + +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ScatterZipOutputStream.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ScatterZipOutputStream.java new file mode 100644 index 0000000000..6ab714827c --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ScatterZipOutputStream.java @@ -0,0 +1,184 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import org.readium.r2.shared.util.zip.compress.parallel.ScatterGatherBackingStore; +import org.readium.r2.shared.util.zip.compress.utils.BoundedInputStream; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A zip output stream that is optimized for multi-threaded scatter/gather construction of zip files. + * <p> + * The internal data format of the entries used by this class are entirely private to this class + * and are not part of any public api whatsoever. + * </p> + * <p>It is possible to extend this class to support different kinds of backing storage, the default + * implementation only supports file-based backing. + * </p> + * Thread safety: This class supports multiple threads. But the "writeTo" method must be called + * by the thread that originally created the {@link ZipArchiveEntry}. + * + * @since 1.10 + */ +public class ScatterZipOutputStream implements Closeable { + private static class CompressedEntry { + final ZipArchiveEntryRequest zipArchiveEntryRequest; + final long crc; + final long compressedSize; + final long size; + + public CompressedEntry(final ZipArchiveEntryRequest zipArchiveEntryRequest, final long crc, final long compressedSize, final long size) { + this.zipArchiveEntryRequest = zipArchiveEntryRequest; + this.crc = crc; + this.compressedSize = compressedSize; + this.size = size; + } + + /** + * Update the original {@link ZipArchiveEntry} with sizes/crc + * Do not use this methods from threads that did not create the instance itself ! + * @return the zipArchiveEntry that is basis for this request + */ + + public ZipArchiveEntry transferToArchiveEntry(){ + final ZipArchiveEntry entry = zipArchiveEntryRequest.getZipArchiveEntry(); + entry.setCompressedSize(compressedSize); + entry.setSize(size); + entry.setCrc(crc); + entry.setMethod(zipArchiveEntryRequest.getMethod()); + return entry; + } + } + public static class ZipEntryWriter implements Closeable { + private final Iterator<CompressedEntry> itemsIterator; + private final InputStream itemsIteratorData; + + public ZipEntryWriter(final ScatterZipOutputStream scatter) throws IOException { + scatter.backingStore.closeForWriting(); + itemsIterator = scatter.items.iterator(); + itemsIteratorData = scatter.backingStore.getInputStream(); + } + + @Override + public void close() throws IOException { + if (itemsIteratorData != null) { + itemsIteratorData.close(); + } + } + + public void writeNextZipEntry(final ZipArchiveOutputStream target) throws IOException { + final CompressedEntry compressedEntry = itemsIterator.next(); + try (final BoundedInputStream rawStream = new BoundedInputStream(itemsIteratorData, compressedEntry.compressedSize)) { + target.addRawArchiveEntry(compressedEntry.transferToArchiveEntry(), rawStream); + } + } + } + + private final Queue<CompressedEntry> items = new ConcurrentLinkedQueue<>(); + + private final ScatterGatherBackingStore backingStore; + + private final StreamCompressor streamCompressor; + + private final AtomicBoolean isClosed = new AtomicBoolean(); + + private ZipEntryWriter zipEntryWriter; + + public ScatterZipOutputStream(final ScatterGatherBackingStore backingStore, + final StreamCompressor streamCompressor) { + this.backingStore = backingStore; + this.streamCompressor = streamCompressor; + } + + /** + * Add an archive entry to this scatter stream. + * + * @param zipArchiveEntryRequest The entry to write. + * @throws IOException If writing fails + */ + public void addArchiveEntry(final ZipArchiveEntryRequest zipArchiveEntryRequest) throws IOException { + try (final InputStream payloadStream = zipArchiveEntryRequest.getPayloadStream()) { + streamCompressor.deflate(payloadStream, zipArchiveEntryRequest.getMethod()); + } + items.add(new CompressedEntry(zipArchiveEntryRequest, streamCompressor.getCrc32(), + streamCompressor.getBytesWrittenForLastEntry(), streamCompressor.getBytesRead())); + } + + /** + * Closes this stream, freeing all resources involved in the creation of this stream. + * @throws IOException If closing fails + */ + @Override + public void close() throws IOException { + if (!isClosed.compareAndSet(false, true)) { + return; + } + try { + if (zipEntryWriter != null) { + zipEntryWriter.close(); + } + backingStore.close(); + } finally { + streamCompressor.close(); + } + } + + /** + * Write the contents of this scatter stream to a target archive. + * + * @param target The archive to receive the contents of this {@link ScatterZipOutputStream}. + * @throws IOException If writing fails + * @see #zipEntryWriter() + */ + public void writeTo(final ZipArchiveOutputStream target) throws IOException { + backingStore.closeForWriting(); + try (final InputStream data = backingStore.getInputStream()) { + for (final CompressedEntry compressedEntry : items) { + try (final BoundedInputStream rawStream = new BoundedInputStream(data, + compressedEntry.compressedSize)) { + target.addRawArchiveEntry(compressedEntry.transferToArchiveEntry(), rawStream); + } + } + } + } + + /** + * Get a zip entry writer for this scatter stream. + * @throws IOException If getting scatter stream input stream + * @return the ZipEntryWriter created on first call of the method + */ + public ZipEntryWriter zipEntryWriter() throws IOException { + if (zipEntryWriter == null) { + zipEntryWriter = new ZipEntryWriter(this); + } + return zipEntryWriter; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/StreamCompressor.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/StreamCompressor.java new file mode 100644 index 0000000000..e51f1b42fa --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/StreamCompressor.java @@ -0,0 +1,346 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel; +import org.readium.r2.shared.util.zip.compress.parallel.ScatterGatherBackingStore; + +import java.io.Closeable; +import java.io.DataOutput; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.zip.CRC32; +import java.util.zip.Deflater; +import java.util.zip.ZipEntry; + +/** + * Encapsulates a {@link Deflater} and crc calculator, handling multiple types of output streams. + * Currently {@link java.util.zip.ZipEntry#DEFLATED} and {@link java.util.zip.ZipEntry#STORED} are the only + * supported compression methods. + * + * @since 1.10 + */ +public abstract class StreamCompressor implements Closeable { + + private static final class DataOutputCompressor extends StreamCompressor { + private final DataOutput raf; + + public DataOutputCompressor(final Deflater deflater, final DataOutput raf) { + super(deflater); + this.raf = raf; + } + + @Override + protected void writeOut(final byte[] data, final int offset, final int length) + throws IOException { + raf.write(data, offset, length); + } + } + + private static final class OutputStreamCompressor extends StreamCompressor { + private final OutputStream os; + + public OutputStreamCompressor(final Deflater deflater, final OutputStream os) { + super(deflater); + this.os = os; + } + + @Override + protected void writeOut(final byte[] data, final int offset, final int length) + throws IOException { + os.write(data, offset, length); + } + } + + private static final class ScatterGatherBackingStoreCompressor extends StreamCompressor { + private final ScatterGatherBackingStore bs; + + public ScatterGatherBackingStoreCompressor(final Deflater deflater, final ScatterGatherBackingStore bs) { + super(deflater); + this.bs = bs; + } + + @Override + protected void writeOut(final byte[] data, final int offset, final int length) + throws IOException { + bs.writeOut(data, offset, length); + } + } + + private static final class SeekableByteChannelCompressor extends StreamCompressor { + private final SeekableByteChannel channel; + + public SeekableByteChannelCompressor(final Deflater deflater, + final SeekableByteChannel channel) { + super(deflater); + this.channel = channel; + } + + @Override + protected void writeOut(final byte[] data, final int offset, final int length) + throws IOException { + channel.write(ByteBuffer.wrap(data, offset, length)); + } + } + /* + * Apparently Deflater.setInput gets slowed down a lot on Sun JVMs + * when it gets handed a really big buffer. See + * https://issues.apache.org/bugzilla/show_bug.cgi?id=45396 + * + * Using a buffer size of 8 kB proved to be a good compromise + */ + private static final int DEFLATER_BLOCK_SIZE = 8192; + private static final int BUFFER_SIZE = 4096; + + /** + * Create a stream compressor with the given compression level. + * + * @param os The DataOutput to receive output + * @param deflater The deflater to use for the compressor + * @return A stream compressor + */ + static StreamCompressor create(final DataOutput os, final Deflater deflater) { + return new DataOutputCompressor(deflater, os); + } + /** + * Create a stream compressor with the given compression level. + * + * @param compressionLevel The {@link Deflater} compression level + * @param bs The ScatterGatherBackingStore to receive output + * @return A stream compressor + */ + public static StreamCompressor create(final int compressionLevel, final ScatterGatherBackingStore bs) { + final Deflater deflater = new Deflater(compressionLevel, true); + return new ScatterGatherBackingStoreCompressor(deflater, bs); + } + /** + * Create a stream compressor with the default compression level. + * + * @param os The stream to receive output + * @return A stream compressor + */ + static StreamCompressor create(final OutputStream os) { + return create(os, new Deflater(Deflater.DEFAULT_COMPRESSION, true)); + } + + /** + * Create a stream compressor with the given compression level. + * + * @param os The stream to receive output + * @param deflater The deflater to use + * @return A stream compressor + */ + static StreamCompressor create(final OutputStream os, final Deflater deflater) { + return new OutputStreamCompressor(deflater, os); + } + + /** + * Create a stream compressor with the default compression level. + * + * @param bs The ScatterGatherBackingStore to receive output + * @return A stream compressor + */ + public static StreamCompressor create(final ScatterGatherBackingStore bs) { + return create(Deflater.DEFAULT_COMPRESSION, bs); + } + + /** + * Create a stream compressor with the given compression level. + * + * @param os The SeekableByteChannel to receive output + * @param deflater The deflater to use for the compressor + * @return A stream compressor + * @since 1.13 + */ + static StreamCompressor create(final SeekableByteChannel os, final Deflater deflater) { + return new SeekableByteChannelCompressor(deflater, os); + } + + private final Deflater def; + + private final CRC32 crc = new CRC32(); + + private long writtenToOutputStreamForLastEntry; + + private long sourcePayloadLength; + + private long totalWrittenToOutputStream; + + private final byte[] outputBuffer = new byte[BUFFER_SIZE]; + + private final byte[] readerBuf = new byte[BUFFER_SIZE]; + + StreamCompressor(final Deflater deflater) { + this.def = deflater; + } + + + @Override + public void close() throws IOException { + def.end(); + } + + void deflate() throws IOException { + final int len = def.deflate(outputBuffer, 0, outputBuffer.length); + if (len > 0) { + writeCounted(outputBuffer, 0, len); + } + } + + + /** + * Deflate the given source using the supplied compression method + * + * @param source The source to compress + * @param method The #ZipArchiveEntry compression method + * @throws IOException When failures happen + */ + + public void deflate(final InputStream source, final int method) throws IOException { + reset(); + int length; + + while ((length = source.read(readerBuf, 0, readerBuf.length)) >= 0) { + write(readerBuf, 0, length, method); + } + if (method == ZipEntry.DEFLATED) { + flushDeflater(); + } + } + + private void deflateUntilInputIsNeeded() throws IOException { + while (!def.needsInput()) { + deflate(); + } + } + + void flushDeflater() throws IOException { + def.finish(); + while (!def.finished()) { + deflate(); + } + } + + /** + * Return the number of bytes read from the source stream + * + * @return The number of bytes read, never negative + */ + public long getBytesRead() { + return sourcePayloadLength; + } + + /** + * The number of bytes written to the output for the last entry + * + * @return The number of bytes, never negative + */ + public long getBytesWrittenForLastEntry() { + return writtenToOutputStreamForLastEntry; + } + + /** + * The crc32 of the last deflated file + * + * @return the crc32 + */ + + public long getCrc32() { + return crc.getValue(); + } + + /** + * The total number of bytes written to the output for all files + * + * @return The number of bytes, never negative + */ + public long getTotalBytesWritten() { + return totalWrittenToOutputStream; + } + + void reset() { + crc.reset(); + def.reset(); + sourcePayloadLength = 0; + writtenToOutputStreamForLastEntry = 0; + } + + /** + * Writes bytes to ZIP entry. + * + * @param b the byte array to write + * @param offset the start position to write from + * @param length the number of bytes to write + * @param method the comrpession method to use + * @return the number of bytes written to the stream this time + * @throws IOException on error + */ + long write(final byte[] b, final int offset, final int length, final int method) throws IOException { + final long current = writtenToOutputStreamForLastEntry; + crc.update(b, offset, length); + if (method == ZipEntry.DEFLATED) { + writeDeflated(b, offset, length); + } else { + writeCounted(b, offset, length); + } + sourcePayloadLength += length; + return writtenToOutputStreamForLastEntry - current; + } + + public void writeCounted(final byte[] data) throws IOException { + writeCounted(data, 0, data.length); + } + + public void writeCounted(final byte[] data, final int offset, final int length) throws IOException { + writeOut(data, offset, length); + writtenToOutputStreamForLastEntry += length; + totalWrittenToOutputStream += length; + } + + private void writeDeflated(final byte[] b, final int offset, final int length) + throws IOException { + if (length > 0 && !def.finished()) { + if (length <= DEFLATER_BLOCK_SIZE) { + def.setInput(b, offset, length); + deflateUntilInputIsNeeded(); + } else { + final int fullblocks = length / DEFLATER_BLOCK_SIZE; + for (int i = 0; i < fullblocks; i++) { + def.setInput(b, offset + i * DEFLATER_BLOCK_SIZE, + DEFLATER_BLOCK_SIZE); + deflateUntilInputIsNeeded(); + } + final int done = fullblocks * DEFLATER_BLOCK_SIZE; + if (done < length) { + def.setInput(b, offset + done, length - done); + deflateUntilInputIsNeeded(); + } + } + } + } + + protected abstract void writeOut(byte[] data, int offset, int length) throws IOException; +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnicodeCommentExtraField.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnicodeCommentExtraField.java new file mode 100644 index 0000000000..dbd283f09d --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnicodeCommentExtraField.java @@ -0,0 +1,76 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +/** + * Info-ZIP Unicode Comment Extra Field (0x6375): + * + * <p>Stores the UTF-8 version of the file comment as stored in the + * central directory header.</p> + * + * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">PKWARE + * APPNOTE.TXT, section 4.6.8</a> + * + * @NotThreadSafe super-class is not thread-safe + */ +public class UnicodeCommentExtraField extends AbstractUnicodeExtraField { + + public static final ZipShort UCOM_ID = new ZipShort(0x6375); + + public UnicodeCommentExtraField () { + } + + /** + * Assemble as unicode comment extension from the comment given as + * text as well as the bytes actually written to the archive. + * + * @param comment The file comment + * @param bytes the bytes actually written to the archive + */ + public UnicodeCommentExtraField(final String comment, final byte[] bytes) { + super(comment, bytes); + } + + /** + * Assemble as unicode comment extension from the name given as + * text as well as the encoded bytes actually written to the archive. + * + * @param text The file name + * @param bytes the bytes actually written to the archive + * @param off The offset of the encoded comment in {@code bytes}. + * @param len The length of the encoded comment or comment in + * {@code bytes}. + */ + public UnicodeCommentExtraField(final String text, final byte[] bytes, final int off, + final int len) { + super(text, bytes, off, len); + } + + @Override + public ZipShort getHeaderId() { + return UCOM_ID; + } + +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnicodePathExtraField.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnicodePathExtraField.java new file mode 100644 index 0000000000..44cd3a40d3 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnicodePathExtraField.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +/** + * Info-ZIP Unicode Path Extra Field (0x7075): + * + * <p>Stores the UTF-8 version of the file name field as stored in the + * local header and central directory header.</p> + * + * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">PKWARE + * APPNOTE.TXT, section 4.6.9</a> + * + * @NotThreadSafe super-class is not thread-safe + */ +public class UnicodePathExtraField extends AbstractUnicodeExtraField { + + public static final ZipShort UPATH_ID = new ZipShort(0x7075); + + public UnicodePathExtraField () { + } + + /** + * Assemble as unicode path extension from the name given as + * text as well as the encoded bytes actually written to the archive. + * + * @param name The file name + * @param bytes the bytes actually written to the archive + */ + public UnicodePathExtraField(final String name, final byte[] bytes) { + super(name, bytes); + } + + /** + * Assemble as unicode path extension from the name given as + * text as well as the encoded bytes actually written to the archive. + * + * @param text The file name + * @param bytes the bytes actually written to the archive + * @param off The offset of the encoded file name in {@code bytes}. + * @param len The length of the encoded file name or comment in + * {@code bytes}. + */ + public UnicodePathExtraField(final String text, final byte[] bytes, final int off, final int len) { + super(text, bytes, off, len); + } + + @Override + public ZipShort getHeaderId() { + return UPATH_ID; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnixStat.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnixStat.java new file mode 100644 index 0000000000..7fbc208870 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnixStat.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +/** + * Constants from stat.h on Unix systems. + */ +// CheckStyle:InterfaceIsTypeCheck OFF - backward compatible +public interface UnixStat { + + /** + * Bits used for permissions (and sticky bit) + */ + int PERM_MASK = 07777; + /** + * Bits used to indicate the file system object type. + * @since 1.14 + */ + int FILE_TYPE_FLAG = 0170000; + /** + * Indicates symbolic links. + */ + int LINK_FLAG = 0120000; + /** + * Indicates plain files. + */ + int FILE_FLAG = 0100000; + /** + * Indicates directories. + */ + int DIR_FLAG = 040000; + + // ---------------------------------------------------------- + // somewhat arbitrary choices that are quite common for shared + // installations + // ----------------------------------------------------------- + + /** + * Default permissions for symbolic links. + */ + int DEFAULT_LINK_PERM = 0777; + + /** + * Default permissions for directories. + */ + int DEFAULT_DIR_PERM = 0755; + + /** + * Default permissions for plain files. + */ + int DEFAULT_FILE_PERM = 0644; +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnparseableExtraFieldBehavior.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnparseableExtraFieldBehavior.java new file mode 100644 index 0000000000..13733f0d6d --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnparseableExtraFieldBehavior.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.util.zip.ZipException; + +/** + * Handles extra field data that doesn't follow the recommended + * pattern for extra fields with a two-byte key and a two-byte length. + * + * @since 1.19 + */ +public interface UnparseableExtraFieldBehavior { + /** + * Decides what to do with extra field data that doesn't follow the recommended pattern. + * + * @param data the array of extra field data + * @param off offset into data where the unparseable data starts + * @param len the length of unparseable data + * @param local whether the extra field data stems from the local + * file header. If this is false then the data is part if the + * central directory header extra data. + * @param claimedLength length of the extra field claimed by the + * third and forth byte if it did follow the recommended pattern + * + * @return null if the data should be ignored or an extra field + * implementation that represents the data + * @throws ZipException if an error occurs or unparseable extra + * fields must not be accepted + */ + ZipExtraField onUnparseableExtraField(byte[] data, int off, int len, boolean local, + int claimedLength) throws ZipException; +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnparseableExtraFieldData.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnparseableExtraFieldData.java new file mode 100644 index 0000000000..6c36c57420 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnparseableExtraFieldData.java @@ -0,0 +1,127 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.util.Arrays; + +/** + * Wrapper for extra field data that doesn't conform to the recommended format of header-tag + size + data. + * + * <p>The header-id is artificial (and not listed as a known ID in <a + * href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">APPNOTE.TXT</a>). Since it isn't used anywhere + * except to satisfy the ZipExtraField contract it shouldn't matter anyway.</p> + * + * @since 1.1 + * @NotThreadSafe + */ +public final class UnparseableExtraFieldData implements ZipExtraField { + private static final ZipShort HEADER_ID = new ZipShort(0xACC1); + + private byte[] localFileData; + private byte[] centralDirectoryData; + + /** + * The actual data to put into central directory. + * + * @return The CentralDirectoryData value + */ + @Override + public byte[] getCentralDirectoryData() { + return centralDirectoryData == null + ? getLocalFileDataData() : ZipUtil.copy(centralDirectoryData); + } + + /** + * Length of the complete extra field in the central directory. + * + * @return The CentralDirectoryLength value + */ + @Override + public ZipShort getCentralDirectoryLength() { + return centralDirectoryData == null + ? getLocalFileDataLength() + : new ZipShort(centralDirectoryData.length); + } + + /** + * The Header-ID. + * + * @return a completely arbitrary value that should be ignored. + */ + @Override + public ZipShort getHeaderId() { + return HEADER_ID; + } + + /** + * The actual data to put into local file data. + * + * @return The LocalFileDataData value + */ + @Override + public byte[] getLocalFileDataData() { + return ZipUtil.copy(localFileData); + } + + /** + * Length of the complete extra field in the local file data. + * + * @return The LocalFileDataLength value + */ + @Override + public ZipShort getLocalFileDataLength() { + return new ZipShort(localFileData == null ? 0 : localFileData.length); + } + + /** + * Populate data from this array as if it was in central directory data. + * + * @param buffer the buffer to read data from + * @param offset offset into buffer to read data + * @param length the length of data + */ + @Override + public void parseFromCentralDirectoryData(final byte[] buffer, final int offset, + final int length) { + centralDirectoryData = Arrays.copyOfRange(buffer, offset, offset + length); + if (localFileData == null) { + parseFromLocalFileData(buffer, offset, length); + } + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param buffer the buffer to read data from + * @param offset offset into buffer to read data + * @param length the length of data + */ + @Override + public void parseFromLocalFileData(final byte[] buffer, final int offset, final int length) { + localFileData = Arrays.copyOfRange(buffer, offset, offset + length); + } + +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnrecognizedExtraField.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnrecognizedExtraField.java new file mode 100644 index 0000000000..2715d07e68 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnrecognizedExtraField.java @@ -0,0 +1,160 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.util.Arrays; + +/** + * Simple placeholder for all those extra fields we don't want to deal + * with. + * + * <p>Assumes local file data and central directory entries are + * identical - unless told the opposite.</p> + * @NotThreadSafe + */ +public class UnrecognizedExtraField implements ZipExtraField { + + /** + * The Header-ID. + */ + private ZipShort headerId; + + /** + * Extra field data in local file data - without + * Header-ID or length specifier. + */ + private byte[] localData; + + /** + * Extra field data in central directory - without + * Header-ID or length specifier. + */ + private byte[] centralData; + + /** + * Get the central data. + * @return the central data if present, else return the local file data + */ + @Override + public byte[] getCentralDirectoryData() { + if (centralData != null) { + return ZipUtil.copy(centralData); + } + return getLocalFileDataData(); + } + + /** + * Get the central data length. + * If there is no central data, get the local file data length. + * @return the central data length + */ + @Override + public ZipShort getCentralDirectoryLength() { + if (centralData != null) { + return new ZipShort(centralData.length); + } + return getLocalFileDataLength(); + } + + /** + * Get the header id. + * @return the header id + */ + @Override + public ZipShort getHeaderId() { + return headerId; + } + + /** + * Get the local data. + * @return the local data + */ + @Override + public byte[] getLocalFileDataData() { + return ZipUtil.copy(localData); + } + + /** + * Get the length of the local data. + * @return the length of the local data + */ + @Override + public ZipShort getLocalFileDataLength() { + return new ZipShort(localData != null ? localData.length : 0); + } + + /** + * @param data the array of bytes. + * @param offset the source location in the data array. + * @param length the number of bytes to use in the data array. + * @see ZipExtraField#parseFromCentralDirectoryData(byte[], int, int) + */ + @Override + public void parseFromCentralDirectoryData(final byte[] data, final int offset, + final int length) { + final byte[] tmp = Arrays.copyOfRange(data, offset, offset + length); + setCentralDirectoryData(tmp); + if (localData == null) { + setLocalFileDataData(tmp); + } + } + + /** + * @param data the array of bytes. + * @param offset the source location in the data array. + * @param length the number of bytes to use in the data array. + * @see ZipExtraField#parseFromLocalFileData(byte[], int, int) + */ + @Override + public void parseFromLocalFileData(final byte[] data, final int offset, final int length) { + setLocalFileDataData(Arrays.copyOfRange(data, offset, offset + length)); + } + + /** + * Set the extra field data in central directory. + * @param data the data to use + */ + public void setCentralDirectoryData(final byte[] data) { + centralData = ZipUtil.copy(data); + } + + /** + * Set the header id. + * @param headerId the header id to use + */ + public void setHeaderId(final ZipShort headerId) { + this.headerId = headerId; + } + + /** + * Set the extra field data in the local file data - + * without Header-ID or length specifier. + * @param data the field data to use + */ + public void setLocalFileDataData(final byte[] data) { + localData = ZipUtil.copy(data); + } + +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnsupportedZipFeatureException.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnsupportedZipFeatureException.java new file mode 100644 index 0000000000..dc7e55d975 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/UnsupportedZipFeatureException.java @@ -0,0 +1,141 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.io.Serializable; +import java.util.zip.ZipException; + +/** + * Exception thrown when attempting to read or write data for a zip + * entry that uses ZIP features not supported by this library. + * @since 1.1 + */ +public class UnsupportedZipFeatureException extends ZipException { + + /** + * ZIP Features that may or may not be supported. + * @since 1.1 + */ + public static class Feature implements Serializable { + + private static final long serialVersionUID = 4112582948775420359L; + /** + * The entry is encrypted. + */ + public static final Feature ENCRYPTION = new Feature("encryption"); + /** + * The entry used an unsupported compression method. + */ + public static final Feature METHOD = new Feature("compression method"); + /** + * The entry uses a data descriptor. + */ + public static final Feature DATA_DESCRIPTOR = new Feature("data descriptor"); + /** + * The archive uses splitting or spanning. + * @since 1.5 + */ + public static final Feature SPLITTING = new Feature("splitting"); + /** + * The archive contains entries with unknown compressed size + * for a compression method that doesn't support detection of + * the end of the compressed stream. + * @since 1.16 + */ + public static final Feature UNKNOWN_COMPRESSED_SIZE = new Feature("unknown compressed size"); + + private final String name; + + private Feature(final String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + private static final long serialVersionUID = 20161219L; + private final Feature reason; + + private transient final ZipArchiveEntry entry; + + /** + * Creates an exception when the whole archive uses an unsupported + * feature. + * + * @param reason the feature that is not supported + * @since 1.5 + */ + public UnsupportedZipFeatureException(final Feature reason) { + super("Unsupported feature " + reason + " used in archive."); + this.reason = reason; + this.entry = null; + } + + /** + * Creates an exception. + * @param reason the feature that is not supported + * @param entry the entry using the feature + */ + public UnsupportedZipFeatureException(final Feature reason, + final ZipArchiveEntry entry) { + super("Unsupported feature " + reason + " used in entry " + + entry.getName()); + this.reason = reason; + this.entry = entry; + } + + /** + * Creates an exception for archives that use an unsupported + * compression algorithm. + * @param method the method that is not supported + * @param entry the entry using the feature + * @since 1.5 + */ + public UnsupportedZipFeatureException(final ZipMethod method, + final ZipArchiveEntry entry) { + super("Unsupported compression method " + entry.getMethod() + + " (" + method.name() + ") used in entry " + entry.getName()); + this.reason = Feature.METHOD; + this.entry = entry; + } + + /** + * The entry using the unsupported feature. + * @return The entry using the unsupported feature. + */ + public ZipArchiveEntry getEntry() { + return entry; + } + + /** + * The unsupported feature that has been used. + * @return The unsupported feature that has been used. + */ + public Feature getFeature() { + return reason; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/Zip64ExtendedInformationExtraField.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/Zip64ExtendedInformationExtraField.java new file mode 100644 index 0000000000..eb30e49e45 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/Zip64ExtendedInformationExtraField.java @@ -0,0 +1,343 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import org.readium.r2.shared.util.zip.compress.utils.ByteUtils; + +import java.util.zip.ZipException; + +/** + * Holds size and other extended information for entries that use Zip64 + * features. + * + * <p>Currently Commons Compress doesn't support encrypting the + * central directory so the note in APPNOTE.TXT about masking doesn't + * apply.</p> + * + * <p>The implementation relies on data being read from the local file + * header and assumes that both size values are always present.</p> + * + * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">PKWARE + * APPNOTE.TXT, section 4.5.3</a> + * + * @since 1.2 + * @NotThreadSafe + */ +public class Zip64ExtendedInformationExtraField implements ZipExtraField { + + static final ZipShort HEADER_ID = new ZipShort(0x0001); + + private static final String LFH_MUST_HAVE_BOTH_SIZES_MSG = + "Zip64 extended information must contain" + + " both size values in the local file header."; + private ZipEightByteInteger size, compressedSize, relativeHeaderOffset; + private ZipLong diskStart; + + /** + * Stored in {@link #parseFromCentralDirectoryData + * parseFromCentralDirectoryData} so it can be reused when ZipFile + * calls {@link #reparseCentralDirectoryData + * reparseCentralDirectoryData}. + * + * <p>Not used for anything else</p> + * + * @since 1.3 + */ + private byte[] rawCentralDirectoryData; + + /** + * This constructor should only be used by the code that reads + * archives inside of Commons Compress. + */ + public Zip64ExtendedInformationExtraField() { } + + /** + * Creates an extra field based on the original and compressed size. + * + * @param size the entry's original size + * @param compressedSize the entry's compressed size + * + * @throws IllegalArgumentException if size or compressedSize is null + */ + public Zip64ExtendedInformationExtraField(final ZipEightByteInteger size, + final ZipEightByteInteger compressedSize) { + this(size, compressedSize, null, null); + } + + /** + * Creates an extra field based on all four possible values. + * + * @param size the entry's original size + * @param compressedSize the entry's compressed size + * @param relativeHeaderOffset the entry's offset + * @param diskStart the disk start + * + * @throws IllegalArgumentException if size or compressedSize is null + */ + public Zip64ExtendedInformationExtraField(final ZipEightByteInteger size, + final ZipEightByteInteger compressedSize, + final ZipEightByteInteger relativeHeaderOffset, + final ZipLong diskStart) { + this.size = size; + this.compressedSize = compressedSize; + this.relativeHeaderOffset = relativeHeaderOffset; + this.diskStart = diskStart; + } + + private int addSizes(final byte[] data) { + int off = 0; + if (size != null) { + System.arraycopy(size.getBytes(), 0, data, 0, ZipConstants.DWORD); + off += ZipConstants.DWORD; + } + if (compressedSize != null) { + System.arraycopy(compressedSize.getBytes(), 0, data, off, ZipConstants.DWORD); + off += ZipConstants.DWORD; + } + return off; + } + + @Override + public byte[] getCentralDirectoryData() { + final byte[] data = new byte[getCentralDirectoryLength().getValue()]; + int off = addSizes(data); + if (relativeHeaderOffset != null) { + System.arraycopy(relativeHeaderOffset.getBytes(), 0, data, off, ZipConstants.DWORD); + off += ZipConstants.DWORD; + } + if (diskStart != null) { + System.arraycopy(diskStart.getBytes(), 0, data, off, ZipConstants.WORD); + off += ZipConstants.WORD; // NOSONAR - assignment as documentation + } + return data; + } + + @Override + public ZipShort getCentralDirectoryLength() { + return new ZipShort((size != null ? ZipConstants.DWORD : 0) + + (compressedSize != null ? ZipConstants.DWORD : 0) + + (relativeHeaderOffset != null ? ZipConstants.DWORD : 0) + + (diskStart != null ? ZipConstants.WORD : 0)); + } + + /** + * The compressed size stored in this extra field. + * @return The compressed size stored in this extra field. + */ + public ZipEightByteInteger getCompressedSize() { + return compressedSize; + } + + /** + * The disk start number stored in this extra field. + * @return The disk start number stored in this extra field. + */ + public ZipLong getDiskStartNumber() { + return diskStart; + } + + @Override + public ZipShort getHeaderId() { + return HEADER_ID; + } + + @Override + public byte[] getLocalFileDataData() { + if (size != null || compressedSize != null) { + if (size == null || compressedSize == null) { + throw new IllegalArgumentException(LFH_MUST_HAVE_BOTH_SIZES_MSG); + } + final byte[] data = new byte[2 * ZipConstants.DWORD]; + addSizes(data); + return data; + } + return ByteUtils.EMPTY_BYTE_ARRAY; + } + + @Override + public ZipShort getLocalFileDataLength() { + return new ZipShort(size != null ? 2 * ZipConstants.DWORD : 0); + } + + /** + * The relative header offset stored in this extra field. + * @return The relative header offset stored in this extra field. + */ + public ZipEightByteInteger getRelativeHeaderOffset() { + return relativeHeaderOffset; + } + + /** + * The uncompressed size stored in this extra field. + * @return The uncompressed size stored in this extra field. + */ + public ZipEightByteInteger getSize() { + return size; + } + + @Override + public void parseFromCentralDirectoryData(final byte[] buffer, int offset, + final int length) + throws ZipException { + // store for processing in reparseCentralDirectoryData + rawCentralDirectoryData = new byte[length]; + System.arraycopy(buffer, offset, rawCentralDirectoryData, 0, length); + + // if there is no size information in here, we are screwed and + // can only hope things will get resolved by LFH data later + // But there are some cases that can be detected + // * all data is there + // * length == 24 -> both sizes and offset + // * length % 8 == 4 -> at least we can identify the diskStart field + if (length >= 3 * ZipConstants.DWORD + ZipConstants.WORD) { + parseFromLocalFileData(buffer, offset, length); + } else if (length == 3 * ZipConstants.DWORD) { + size = new ZipEightByteInteger(buffer, offset); + offset += ZipConstants.DWORD; + compressedSize = new ZipEightByteInteger(buffer, offset); + offset += ZipConstants.DWORD; + relativeHeaderOffset = new ZipEightByteInteger(buffer, offset); + } else if (length % ZipConstants.DWORD == ZipConstants.WORD) { + diskStart = new ZipLong(buffer, offset + length - ZipConstants.WORD); + } + } + + @Override + public void parseFromLocalFileData(final byte[] buffer, int offset, final int length) + throws ZipException { + if (length == 0) { + // no local file data at all, may happen if an archive + // only holds a ZIP64 extended information extra field + // inside the central directory but not inside the local + // file header + return; + } + if (length < 2 * ZipConstants.DWORD) { + throw new ZipException(LFH_MUST_HAVE_BOTH_SIZES_MSG); + } + size = new ZipEightByteInteger(buffer, offset); + offset += ZipConstants.DWORD; + compressedSize = new ZipEightByteInteger(buffer, offset); + offset += ZipConstants.DWORD; + int remaining = length - 2 * ZipConstants.DWORD; + if (remaining >= ZipConstants.DWORD) { + relativeHeaderOffset = new ZipEightByteInteger(buffer, offset); + offset += ZipConstants.DWORD; + remaining -= ZipConstants.DWORD; + } + if (remaining >= ZipConstants.WORD) { + diskStart = new ZipLong(buffer, offset); + offset += ZipConstants.WORD; // NOSONAR - assignment as documentation + remaining -= ZipConstants.WORD; // NOSONAR - assignment as documentation + } + } + + /** + * Parses the raw bytes read from the central directory extra + * field with knowledge which fields are expected to be there. + * + * <p>All four fields inside the zip64 extended information extra + * field are optional and must only be present if their corresponding + * entry inside the central directory contains the correct magic + * value.</p> + * + * @param hasUncompressedSize flag to read from central directory + * @param hasCompressedSize flag to read from central directory + * @param hasRelativeHeaderOffset flag to read from central directory + * @param hasDiskStart flag to read from central directory + * @throws ZipException on error + */ + public void reparseCentralDirectoryData(final boolean hasUncompressedSize, + final boolean hasCompressedSize, + final boolean hasRelativeHeaderOffset, + final boolean hasDiskStart) + throws ZipException { + if (rawCentralDirectoryData != null) { + final int expectedLength = (hasUncompressedSize ? ZipConstants.DWORD : 0) + + (hasCompressedSize ? ZipConstants.DWORD : 0) + + (hasRelativeHeaderOffset ? ZipConstants.DWORD : 0) + + (hasDiskStart ? ZipConstants.WORD : 0); + if (rawCentralDirectoryData.length < expectedLength) { + throw new ZipException("Central directory zip64 extended" + + " information extra field's length" + + " doesn't match central directory" + + " data. Expected length " + + expectedLength + " but is " + + rawCentralDirectoryData.length); + } + int offset = 0; + if (hasUncompressedSize) { + size = new ZipEightByteInteger(rawCentralDirectoryData, offset); + offset += ZipConstants.DWORD; + } + if (hasCompressedSize) { + compressedSize = new ZipEightByteInteger(rawCentralDirectoryData, + offset); + offset += ZipConstants.DWORD; + } + if (hasRelativeHeaderOffset) { + relativeHeaderOffset = + new ZipEightByteInteger(rawCentralDirectoryData, offset); + offset += ZipConstants.DWORD; + } + if (hasDiskStart) { + diskStart = new ZipLong(rawCentralDirectoryData, offset); + offset += ZipConstants.WORD; // NOSONAR - assignment as documentation + } + } + } + + /** + * The uncompressed size stored in this extra field. + * @param compressedSize The uncompressed size stored in this extra field. + */ + public void setCompressedSize(final ZipEightByteInteger compressedSize) { + this.compressedSize = compressedSize; + } + + /** + * The disk start number stored in this extra field. + * @param ds The disk start number stored in this extra field. + */ + public void setDiskStartNumber(final ZipLong ds) { + diskStart = ds; + } + + /** + * The relative header offset stored in this extra field. + * @param rho The relative header offset stored in this extra field. + */ + public void setRelativeHeaderOffset(final ZipEightByteInteger rho) { + relativeHeaderOffset = rho; + } + + /** + * The uncompressed size stored in this extra field. + * @param size The uncompressed size stored in this extra field. + */ + public void setSize(final ZipEightByteInteger size) { + this.size = size; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/Zip64Mode.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/Zip64Mode.java new file mode 100644 index 0000000000..6d6d306f61 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/Zip64Mode.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +/** + * The different modes {@link ZipArchiveOutputStream} can operate in. + * + * @see ZipArchiveOutputStream#setUseZip64 + * + * @since 1.3 + */ +public enum Zip64Mode { + /** + * Use Zip64 extensions for all entries, even if it is clear it is + * not required. + */ + Always, + /** + * Don't use Zip64 extensions for any entries. + * + * <p>This will cause a {@link Zip64RequiredException} to be + * thrown if {@link ZipArchiveOutputStream} detects it needs Zip64 + * support.</p> + */ + Never, + /** + * Use Zip64 extensions for all entries where they are required, + * don't use them for entries that clearly don't require them. + */ + AsNeeded, + /** + * Always use Zip64 extensions for LFH and central directory as + * {@link Zip64Mode#Always} did, and at the meantime encode + * the relative offset of LFH and disk number start as needed in + * CFH as {@link Zip64Mode#AsNeeded} did. + * <p> + * This is a compromise for some libraries including 7z and + * Expand-Archive Powershell utility(and likely Excel). + */ + AlwaysWithCompatibility +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/Zip64RequiredException.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/Zip64RequiredException.java new file mode 100644 index 0000000000..1d17bdce36 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/Zip64RequiredException.java @@ -0,0 +1,67 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.util.zip.ZipException; + +/** + * Exception thrown when attempting to write data that requires Zip64 + * support to an archive and {@link ZipArchiveOutputStream#setUseZip64 + * UseZip64} has been set to {@link Zip64Mode#Never Never}. + * @since 1.3 + */ +public class Zip64RequiredException extends ZipException { + + private static final long serialVersionUID = 20110809L; + + static final String NUMBER_OF_THIS_DISK_TOO_BIG_MESSAGE = + "Number of the disk of End Of Central Directory exceeds the limit of 65535."; + + static final String NUMBER_OF_THE_DISK_OF_CENTRAL_DIRECTORY_TOO_BIG_MESSAGE = + "Number of the disk with the start of Central Directory exceeds the limit of 65535."; + + static final String TOO_MANY_ENTRIES_ON_THIS_DISK_MESSAGE = + "Number of entries on this disk exceeds the limit of 65535."; + + static final String SIZE_OF_CENTRAL_DIRECTORY_TOO_BIG_MESSAGE = + "The size of the entire central directory exceeds the limit of 4GByte."; + + static final String ARCHIVE_TOO_BIG_MESSAGE = + "Archive's size exceeds the limit of 4GByte."; + + static final String TOO_MANY_ENTRIES_MESSAGE = + "Archive contains more than 65535 entries."; + + /** + * Helper to format "entry too big" messages. + */ + static String getEntryTooBigMessage(final ZipArchiveEntry ze) { + return ze.getName() + "'s size exceeds the limit of 4GByte."; + } + + public Zip64RequiredException(final String reason) { + super(reason); + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipArchiveEntry.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipArchiveEntry.java new file mode 100644 index 0000000000..ce4f767bae --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipArchiveEntry.java @@ -0,0 +1,1230 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import org.readium.r2.shared.util.zip.compress.archivers.ArchiveEntry; +import org.readium.r2.shared.util.zip.compress.archivers.EntryStreamOffsets; +import org.readium.r2.shared.util.zip.compress.utils.ByteUtils; + +import java.io.File; +import java.nio.file.attribute.FileTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.zip.ZipException; + +/** + * Extension that adds better handling of extra fields and provides + * access to the internal and external file attributes. + * + * <p>The extra data is expected to follow the recommendation of + * <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">APPNOTE.TXT</a>:</p> + * <ul> + * <li>the extra byte array consists of a sequence of extra fields</li> + * <li>each extra fields starts by a two byte header id followed by + * a two byte sequence holding the length of the remainder of + * data.</li> + * </ul> + * + * <p>Any extra data that cannot be parsed by the rules above will be + * consumed as "unparseable" extra data and treated differently by the + * methods of this class. Versions prior to Apache Commons Compress + * 1.1 would have thrown an exception if any attempt was made to read + * or write extra data not conforming to the recommendation.</p> + * + * @NotThreadSafe + */ +public class ZipArchiveEntry extends java.util.zip.ZipEntry implements ArchiveEntry, EntryStreamOffsets { + + /** + * Indicates how the comment of this entry has been determined. + * @since 1.16 + */ + public enum CommentSource { + /** + * The comment has been read from the archive using the encoding + * of the archive specified when creating the {@link ZipFile} (defaults to the + * platform's default encoding). + */ + COMMENT, + /** + * The comment has been read from an {@link UnicodeCommentExtraField + * Unicode Extra Field}. + */ + UNICODE_EXTRA_FIELD + } + + /** + * How to try to parse the extra fields. + * + * <p>Configures the behavior for:</p> + * <ul> + * <li>What shall happen if the extra field content doesn't + * follow the recommended pattern of two-byte id followed by a + * two-byte length?</li> + * <li>What shall happen if an extra field is generally supported + * by Commons Compress but its content cannot be parsed + * correctly? This may for example happen if the archive is + * corrupt, it triggers a bug in Commons Compress or the extra + * field uses a version not (yet) supported by Commons + * Compress.</li> + * </ul> + * + * @since 1.19 + */ + public enum ExtraFieldParsingMode implements ExtraFieldParsingBehavior { + /** + * Try to parse as many extra fields as possible and wrap + * unknown extra fields as well as supported extra fields that + * cannot be parsed in {@link UnrecognizedExtraField}. + * + * <p>Wrap extra data that doesn't follow the recommended + * pattern in an {@link UnparseableExtraFieldData} + * instance.</p> + * + * <p>This is the default behavior starting with Commons Compress 1.19.</p> + */ + BEST_EFFORT(ExtraFieldUtils.UnparseableExtraField.READ) { + @Override + public ZipExtraField fill(final ZipExtraField field, final byte[] data, final int off, final int len, final boolean local) { + return fillAndMakeUnrecognizedOnError(field, data, off, len, local); + } + }, + /** + * Try to parse as many extra fields as possible and wrap + * unknown extra fields in {@link UnrecognizedExtraField}. + * + * <p>Wrap extra data that doesn't follow the recommended + * pattern in an {@link UnparseableExtraFieldData} + * instance.</p> + * + * <p>Throw an exception if an extra field that is generally + * supported cannot be parsed.</p> + * + * <p>This used to be the default behavior prior to Commons + * Compress 1.19.</p> + */ + STRICT_FOR_KNOW_EXTRA_FIELDS(ExtraFieldUtils.UnparseableExtraField.READ), + /** + * Try to parse as many extra fields as possible and wrap + * unknown extra fields as well as supported extra fields that + * cannot be parsed in {@link UnrecognizedExtraField}. + * + * <p>Ignore extra data that doesn't follow the recommended + * pattern.</p> + */ + ONLY_PARSEABLE_LENIENT(ExtraFieldUtils.UnparseableExtraField.SKIP) { + @Override + public ZipExtraField fill(final ZipExtraField field, final byte[] data, final int off, final int len, final boolean local) { + return fillAndMakeUnrecognizedOnError(field, data, off, len, local); + } + }, + /** + * Try to parse as many extra fields as possible and wrap + * unknown extra fields in {@link UnrecognizedExtraField}. + * + * <p>Ignore extra data that doesn't follow the recommended + * pattern.</p> + * + * <p>Throw an exception if an extra field that is generally + * supported cannot be parsed.</p> + */ + ONLY_PARSEABLE_STRICT(ExtraFieldUtils.UnparseableExtraField.SKIP), + /** + * Throw an exception if any of the recognized extra fields + * cannot be parsed or any extra field violates the + * recommended pattern. + */ + DRACONIC(ExtraFieldUtils.UnparseableExtraField.THROW); + + private static ZipExtraField fillAndMakeUnrecognizedOnError(final ZipExtraField field, final byte[] data, final int off, + final int len, final boolean local) { + try { + return ExtraFieldUtils.fillExtraField(field, data, off, len, local); + } catch (final ZipException ex) { + final UnrecognizedExtraField u = new UnrecognizedExtraField(); + u.setHeaderId(field.getHeaderId()); + if (local) { + u.setLocalFileDataData(Arrays.copyOfRange(data, off, off + len)); + } else { + u.setCentralDirectoryData(Arrays.copyOfRange(data, off, off + len)); + } + return u; + } + } + + private final ExtraFieldUtils.UnparseableExtraField onUnparseableData; + + ExtraFieldParsingMode(final ExtraFieldUtils.UnparseableExtraField onUnparseableData) { + this.onUnparseableData = onUnparseableData; + } + + @Override + public ZipExtraField createExtraField(final ZipShort headerId) + throws ZipException, InstantiationException, IllegalAccessException { + return ExtraFieldUtils.createExtraField(headerId); + } + + @Override + public ZipExtraField fill(final ZipExtraField field, final byte[] data, final int off, final int len, final boolean local) + throws ZipException { + return ExtraFieldUtils.fillExtraField(field, data, off, len, local); + } + + @Override + public ZipExtraField onUnparseableExtraField(final byte[] data, final int off, final int len, final boolean local, + final int claimedLength) throws ZipException { + return onUnparseableData.onUnparseableExtraField(data, off, len, local, claimedLength); + } + } + /** + * Indicates how the name of this entry has been determined. + * @since 1.16 + */ + public enum NameSource { + /** + * The name has been read from the archive using the encoding + * of the archive specified when creating the {@link ZipFile} (defaults to the + * platform's default encoding). + */ + NAME, + /** + * The name has been read from the archive and the archive + * specified the EFS flag which indicates the name has been + * encoded as UTF-8. + */ + NAME_WITH_EFS_FLAG, + /** + * The name has been read from an {@link UnicodePathExtraField + * Unicode Extra Field}. + */ + UNICODE_EXTRA_FIELD + } + static final ZipArchiveEntry[] EMPTY_ARRAY = {}; + public static final int PLATFORM_UNIX = 3; + public static final int PLATFORM_FAT = 0; + public static final int CRC_UNKNOWN = -1; + + private static final int SHORT_MASK = 0xFFFF; + + private static final int SHORT_SHIFT = 16; + + /** + * The {@link java.util.zip.ZipEntry} base class only supports + * the compression methods STORED and DEFLATED. We override the + * field so that any compression methods can be used. + * <p> + * The default value -1 means that the method has not been specified. + * + * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-93" + * >COMPRESS-93</a> + */ + private int method = ZipMethod.UNKNOWN_CODE; + /** + * The {@link java.util.zip.ZipEntry#setSize} method in the base + * class throws an IllegalArgumentException if the size is bigger + * than 2GB for Java versions < 7 and even in Java 7+ if the + * implementation in java.util.zip doesn't support Zip64 itself + * (it is an optional feature). + * + * <p>We need to keep our own size information for Zip64 support.</p> + */ + private long size = SIZE_UNKNOWN; + private int internalAttributes; + private int versionRequired; + private int versionMadeBy; + private int platform = PLATFORM_FAT; + private int rawFlag; + private long externalAttributes; + private int alignment; + private ZipExtraField[] extraFields; + private UnparseableExtraFieldData unparseableExtra; + private String name; + private byte[] rawName; + private GeneralPurposeBit gpb = new GeneralPurposeBit(); + private long localHeaderOffset = OFFSET_UNKNOWN; + private long dataOffset = OFFSET_UNKNOWN; + private boolean isStreamContiguous; + private NameSource nameSource = NameSource.NAME; + + private CommentSource commentSource = CommentSource.COMMENT; + + private long diskNumberStart; + + private long time = -1; + + /** + */ + protected ZipArchiveEntry() { + this(""); + } + + /** + * Creates a new ZIP entry taking some information from the given + * file and using the provided name. + * + * <p>The name will be adjusted to end with a forward slash "/" if + * the file is a directory. If the file is not a directory a + * potential trailing forward slash will be stripped from the + * entry name.</p> + * @param inputFile file to create the entry from + * @param entryName name of the entry + */ + public ZipArchiveEntry(final File inputFile, final String entryName) { + this(inputFile.isDirectory() && !entryName.endsWith("/") ? + entryName + "/" : entryName); + if (inputFile.isFile()){ + setSize(inputFile.length()); + } + setTime(inputFile.lastModified()); + } + + /** + * Creates a new ZIP entry with fields taken from the specified ZIP entry. + * + * <p>Assumes the entry represents a directory if and only if the + * name ends with a forward slash "/".</p> + * + * @param entry the entry to get fields from + * @throws ZipException on error + */ + public ZipArchiveEntry(final java.util.zip.ZipEntry entry) throws ZipException { + super(entry); + setName(entry.getName()); + final byte[] extra = entry.getExtra(); + if (extra != null) { + setExtraFields(ExtraFieldUtils.parse(extra, true, ExtraFieldParsingMode.BEST_EFFORT)); + } else { + // initializes extra data to an empty byte array + setExtra(); + } + setMethod(entry.getMethod()); + this.size = entry.getSize(); + } + + /** + * Creates a new ZIP entry with the specified name. + * + * <p>Assumes the entry represents a directory if and only if the + * name ends with a forward slash "/".</p> + * + * @param name the name of the entry + */ + public ZipArchiveEntry(final String name) { + super(name); + setName(name); + } + + /** + * Creates a new ZIP entry with fields taken from the specified ZIP entry. + * + * <p>Assumes the entry represents a directory if and only if the + * name ends with a forward slash "/".</p> + * + * @param entry the entry to get fields from + * @throws ZipException on error + */ + public ZipArchiveEntry(final ZipArchiveEntry entry) throws ZipException { + this((java.util.zip.ZipEntry) entry); + setInternalAttributes(entry.getInternalAttributes()); + setExternalAttributes(entry.getExternalAttributes()); + setExtraFields(entry.getAllExtraFieldsNoCopy()); + setPlatform(entry.getPlatform()); + final GeneralPurposeBit other = entry.getGeneralPurposeBit(); + setGeneralPurposeBit(other == null ? null : + (GeneralPurposeBit) other.clone()); + } + + /** + * Adds an extra field - replacing an already present extra field + * of the same type. + * + * <p>The new extra field will be the first one.</p> + * @param ze an extra field + */ + public void addAsFirstExtraField(final ZipExtraField ze) { + if (ze instanceof UnparseableExtraFieldData) { + unparseableExtra = (UnparseableExtraFieldData) ze; + } else { + if (getExtraField(ze.getHeaderId()) != null) { + internalRemoveExtraField(ze.getHeaderId()); + } + final ZipExtraField[] copy = extraFields; + final int newLen = extraFields != null ? extraFields.length + 1 : 1; + extraFields = new ZipExtraField[newLen]; + extraFields[0] = ze; + if (copy != null){ + System.arraycopy(copy, 0, extraFields, 1, extraFields.length - 1); + } + } + setExtra(); + } + + /** + * Adds an extra field - replacing an already present extra field + * of the same type. + * + * <p>If no extra field of the same type exists, the field will be + * added as last field.</p> + * @param ze an extra field + */ + public void addExtraField(final ZipExtraField ze) { + internalAddExtraField(ze); + setExtra(); + } + + /** + * Overwrite clone. + * @return a cloned copy of this ZipArchiveEntry + */ + @Override + public Object clone() { + final ZipArchiveEntry e = (ZipArchiveEntry) super.clone(); + + e.setInternalAttributes(getInternalAttributes()); + e.setExternalAttributes(getExternalAttributes()); + e.setExtraFields(getAllExtraFieldsNoCopy()); + return e; + } + + private ZipExtraField[] copyOf(final ZipExtraField[] src, final int length) { + return Arrays.copyOf(src, length); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final ZipArchiveEntry other = (ZipArchiveEntry) obj; + final String myName = getName(); + final String otherName = other.getName(); + if (!Objects.equals(myName, otherName)) { + return false; + } + String myComment = getComment(); + String otherComment = other.getComment(); + if (myComment == null) { + myComment = ""; + } + if (otherComment == null) { + otherComment = ""; + } + return myComment.equals(otherComment) + && getInternalAttributes() == other.getInternalAttributes() + && getPlatform() == other.getPlatform() + && getExternalAttributes() == other.getExternalAttributes() + && getMethod() == other.getMethod() + && getSize() == other.getSize() + && getCrc() == other.getCrc() + && getCompressedSize() == other.getCompressedSize() + && Arrays.equals(getCentralDirectoryExtra(), + other.getCentralDirectoryExtra()) + && Arrays.equals(getLocalFileDataExtra(), + other.getLocalFileDataExtra()) + && localHeaderOffset == other.localHeaderOffset + && dataOffset == other.dataOffset + && gpb.equals(other.gpb); + } + + private ZipExtraField findMatching(final ZipShort headerId, final List<ZipExtraField> fs) { + for (ZipExtraField field: fs) { + if (headerId.equals(field.getHeaderId())) { + return field; + } + } + return null; + } + + private ZipExtraField findUnparseable(final List<ZipExtraField> fs) { + for (ZipExtraField field: fs) { + if (field instanceof UnparseableExtraFieldData) { + return field; + } + } + return null; + } + + /** + * Gets currently configured alignment. + * + * @return + * alignment for this entry. + * @since 1.14 + */ + protected int getAlignment() { + return this.alignment; + } + + private ZipExtraField[] getAllExtraFields() { + final ZipExtraField[] allExtraFieldsNoCopy = getAllExtraFieldsNoCopy(); + return (allExtraFieldsNoCopy == extraFields) ? copyOf(allExtraFieldsNoCopy, allExtraFieldsNoCopy.length) + : allExtraFieldsNoCopy; + } + + /** + * Get all extra fields, including unparseable ones. + * @return An array of all extra fields. Not necessarily a copy of internal data structures, hence private method + */ + private ZipExtraField[] getAllExtraFieldsNoCopy() { + if (extraFields == null) { + return getUnparseableOnly(); + } + return unparseableExtra != null ? getMergedFields() : extraFields; + } + + /** + * Retrieves the extra data for the central directory. + * @return the central directory extra data + */ + public byte[] getCentralDirectoryExtra() { + return ExtraFieldUtils.mergeCentralDirectoryData(getAllExtraFieldsNoCopy()); + } + + /** + * The source of the comment field value. + * @return source of the comment field value + * @since 1.16 + */ + public CommentSource getCommentSource() { + return commentSource; + } + + @Override + public long getDataOffset() { + return dataOffset; + } + + /** + * The number of the split segment this entry starts at. + * + * @return the number of the split segment this entry starts at. + * @since 1.20 + */ + public long getDiskNumberStart() { + return diskNumberStart; + } + + /** + * Retrieves the external file attributes. + * + * @return the external file attributes + */ + public long getExternalAttributes() { + return externalAttributes; + } + + /** + * Looks up an extra field by its header id. + * + * @param type the header id + * @return null if no such field exists. + */ + public ZipExtraField getExtraField(final ZipShort type) { + if (extraFields != null) { + for (final ZipExtraField extraField : extraFields) { + if (type.equals(extraField.getHeaderId())) { + return extraField; + } + } + } + return null; + } + + /** + * Retrieves all extra fields that have been parsed successfully. + * + * @return an array of the extra fields + */ + public ZipExtraField[] getExtraFields() { + return getParseableExtraFields(); + } + + /** + * Retrieves extra fields. + * @param includeUnparseable whether to also return unparseable + * extra fields as {@link UnparseableExtraFieldData} if such data + * exists. + * @return an array of the extra fields + * + * @since 1.1 + */ + public ZipExtraField[] getExtraFields(final boolean includeUnparseable) { + return includeUnparseable ? + getAllExtraFields() : + getParseableExtraFields(); + } + + /** + * Retrieves extra fields. + * @param parsingBehavior controls parsing of extra fields. + * @return an array of the extra fields + * + * @throws ZipException if parsing fails, can not happen if {@code + * parsingBehavior} is {@link ExtraFieldParsingMode#BEST_EFFORT}. + * + * @since 1.19 + */ + public ZipExtraField[] getExtraFields(final ExtraFieldParsingBehavior parsingBehavior) + throws ZipException { + if (parsingBehavior == ExtraFieldParsingMode.BEST_EFFORT) { + return getExtraFields(true); + } + if (parsingBehavior == ExtraFieldParsingMode.ONLY_PARSEABLE_LENIENT) { + return getExtraFields(false); + } + final byte[] local = getExtra(); + final List<ZipExtraField> localFields = new ArrayList<>(Arrays.asList(ExtraFieldUtils.parse(local, true, + parsingBehavior))); + final byte[] central = getCentralDirectoryExtra(); + final List<ZipExtraField> centralFields = new ArrayList<>(Arrays.asList(ExtraFieldUtils.parse(central, false, + parsingBehavior))); + final List<ZipExtraField> merged = new ArrayList<>(); + for (final ZipExtraField l : localFields) { + ZipExtraField c; + if (l instanceof UnparseableExtraFieldData) { + c = findUnparseable(centralFields); + } else { + c = findMatching(l.getHeaderId(), centralFields); + } + if (c != null) { + final byte[] cd = c.getCentralDirectoryData(); + if (cd != null && cd.length > 0) { + l.parseFromCentralDirectoryData(cd, 0, cd.length); + } + centralFields.remove(c); + } + merged.add(l); + } + merged.addAll(centralFields); + return merged.toArray(ExtraFieldUtils.EMPTY_ZIP_EXTRA_FIELD_ARRAY); + } + + /** + * The "general purpose bit" field. + * @return the general purpose bit + * @since 1.1 + */ + public GeneralPurposeBit getGeneralPurposeBit() { + return gpb; + } + + /** + * Retrieves the internal file attributes. + * + * @return the internal file attributes + */ + public int getInternalAttributes() { + return internalAttributes; + } + + /** + * Wraps {@link java.util.zip.ZipEntry#getTime} with a {@link Date} as the + * entry's last modified date. + * + * <p>Changes to the implementation of {@link java.util.zip.ZipEntry#getTime} + * leak through and the returned value may depend on your local + * time zone as well as your version of Java.</p> + */ + @Override + public Date getLastModifiedDate() { + return new Date(getTime()); + } + + /** + * Retrieves the extra data for the local file data. + * @return the extra data for local file + */ + public byte[] getLocalFileDataExtra() { + final byte[] extra = getExtra(); + return extra != null ? extra : ByteUtils.EMPTY_BYTE_ARRAY; + } + + protected long getLocalHeaderOffset() { + return this.localHeaderOffset; + } + + private ZipExtraField[] getMergedFields() { + final ZipExtraField[] zipExtraFields = copyOf(extraFields, extraFields.length + 1); + zipExtraFields[extraFields.length] = unparseableExtra; + return zipExtraFields; + } + + /** + * Returns the compression method of this entry, or -1 if the + * compression method has not been specified. + * + * @return compression method + * + * @since 1.1 + */ + @Override + public int getMethod() { + return method; + } + + /** + * Get the name of the entry. + * + * <p>This method returns the raw name as it is stored inside of the archive.</p> + * + * @return the entry name + */ + @Override + public String getName() { + return name == null ? super.getName() : name; + } + + /** + * The source of the name field value. + * @return source of the name field value + * @since 1.16 + */ + public NameSource getNameSource() { + return nameSource; + } + + private ZipExtraField[] getParseableExtraFields() { + final ZipExtraField[] parseableExtraFields = getParseableExtraFieldsNoCopy(); + return (parseableExtraFields == extraFields) ? copyOf(parseableExtraFields, parseableExtraFields.length) + : parseableExtraFields; + } + + private ZipExtraField[] getParseableExtraFieldsNoCopy() { + if (extraFields == null) { + return ExtraFieldUtils.EMPTY_ZIP_EXTRA_FIELD_ARRAY; + } + return extraFields; + } + + /** + * Platform specification to put into the "version made + * by" part of the central file header. + * + * @return PLATFORM_FAT unless {@link #setUnixMode setUnixMode} + * has been called, in which case PLATFORM_UNIX will be returned. + */ + public int getPlatform() { + return platform; + } + + /** + * The content of the flags field. + * @return content of the flags field + * @since 1.11 + */ + public int getRawFlag() { + return rawFlag; + } + + /** + * Returns the raw bytes that made up the name before it has been + * converted using the configured or guessed encoding. + * + * <p>This method will return null if this instance has not been + * read from an archive.</p> + * + * @return the raw name bytes + * @since 1.2 + */ + public byte[] getRawName() { + if (rawName != null) { + return Arrays.copyOf(rawName, rawName.length); + } + return null; + } + + /** + * Gets the uncompressed size of the entry data. + * + * @return the entry size + */ + @Override + public long getSize() { + return size; + } + + /** + * {@inheritDoc} + * + * <p>Override to work around bug <a href="https://bugs.openjdk.org/browse/JDK-8130914">JDK-8130914</a></p> + * + * @return The last modification time of the entry in milliseconds + * since the epoch, or -1 if not specified + * + * @see #setTime(long) + * @see #setLastModifiedTime(FileTime) + */ + @Override + public long getTime() { + return time != -1 ? time : super.getTime(); + } + + /** + * Unix permission. + * @return the unix permissions + */ + public int getUnixMode() { + return platform != PLATFORM_UNIX ? 0 : + (int) ((getExternalAttributes() >> SHORT_SHIFT) & SHORT_MASK); + } + + private ZipExtraField[] getUnparseableOnly() { + return unparseableExtra == null ? ExtraFieldUtils.EMPTY_ZIP_EXTRA_FIELD_ARRAY : new ZipExtraField[] { unparseableExtra }; + } + + /** + * The "version made by" field. + * @return "version made by" field + * @since 1.11 + */ + public int getVersionMadeBy() { + return versionMadeBy; + } + + /** + * The "version required to expand" field. + * @return "version required to expand" field + * @since 1.11 + */ + public int getVersionRequired() { + return versionRequired; + } + + /** + * Get the hash code of the entry. + * This uses the name as the hash code. + * @return a hash code. + */ + @Override + public int hashCode() { + // this method has severe consequences on performance. We cannot rely + // on the super.hashCode() method since super.getName() always return + // the empty string in the current implementation (there's no setter) + // so it is basically draining the performance of a hashmap lookup + return getName().hashCode(); + } + + private void internalAddExtraField(final ZipExtraField ze) { + if (ze instanceof UnparseableExtraFieldData) { + unparseableExtra = (UnparseableExtraFieldData) ze; + } else if (extraFields == null) { + extraFields = new ZipExtraField[]{ze}; + } else { + if (getExtraField(ze.getHeaderId()) != null) { + internalRemoveExtraField(ze.getHeaderId()); + } + final ZipExtraField[] zipExtraFields = copyOf(extraFields, extraFields.length + 1); + zipExtraFields[zipExtraFields.length - 1] = ze; + extraFields = zipExtraFields; + } + } + + private void internalRemoveExtraField(final ZipShort type) { + if (extraFields == null) { + return; + } + final List<ZipExtraField> newResult = new ArrayList<>(); + for (final ZipExtraField extraField : extraFields) { + if (!type.equals(extraField.getHeaderId())) { + newResult.add(extraField); + } + } + if (extraFields.length == newResult.size()) { + return; + } + extraFields = newResult.toArray(ExtraFieldUtils.EMPTY_ZIP_EXTRA_FIELD_ARRAY); + } + + /** + * Is this entry a directory? + * @return true if the entry is a directory + */ + @Override + public boolean isDirectory() { + return getName().endsWith("/"); + } + + @Override + public boolean isStreamContiguous() { + return isStreamContiguous; + } + + /** + * Returns true if this entry represents a unix symlink, + * in which case the entry's content contains the target path + * for the symlink. + * + * @since 1.5 + * @return true if the entry represents a unix symlink, false otherwise. + */ + public boolean isUnixSymlink() { + return (getUnixMode() & UnixStat.FILE_TYPE_FLAG) == UnixStat.LINK_FLAG; + } + + /** + * If there are no extra fields, use the given fields as new extra + * data - otherwise merge the fields assuming the existing fields + * and the new fields stem from different locations inside the + * archive. + * @param f the extra fields to merge + * @param local whether the new fields originate from local data + */ + private void mergeExtraFields(final ZipExtraField[] f, final boolean local) { + if (extraFields == null) { + setExtraFields(f); + } else { + for (final ZipExtraField element : f) { + final ZipExtraField existing; + if (element instanceof UnparseableExtraFieldData) { + existing = unparseableExtra; + } else { + existing = getExtraField(element.getHeaderId()); + } + if (existing == null) { + internalAddExtraField(element); + } else { + final byte[] b = local ? element.getLocalFileDataData() + : element.getCentralDirectoryData(); + try { + if (local) { + existing.parseFromLocalFileData(b, 0, b.length); + } else { + existing.parseFromCentralDirectoryData(b, 0, b.length); + } + } catch (final ZipException ex) { + // emulate ExtraFieldParsingMode.fillAndMakeUnrecognizedOnError + final UnrecognizedExtraField u = new UnrecognizedExtraField(); + u.setHeaderId(existing.getHeaderId()); + if (local) { + u.setLocalFileDataData(b); + u.setCentralDirectoryData(existing.getCentralDirectoryData()); + } else { + u.setLocalFileDataData(existing.getLocalFileDataData()); + u.setCentralDirectoryData(b); + } + internalRemoveExtraField(existing.getHeaderId()); + internalAddExtraField(u); + } + } + } + setExtra(); + } + } + + /** + * Remove an extra field. + * @param type the type of extra field to remove + */ + public void removeExtraField(final ZipShort type) { + if (getExtraField(type) == null) { + throw new NoSuchElementException(); + } + internalRemoveExtraField(type); + setExtra(); + } + + /** + * Sets alignment for this entry. + * + * @param alignment + * requested alignment, 0 for default. + * @since 1.14 + */ + public void setAlignment(final int alignment) { + if ((alignment & (alignment - 1)) != 0 || alignment > 0xffff) { + throw new IllegalArgumentException("Invalid value for alignment, must be power of two and no bigger than " + + 0xffff + " but is " + alignment); + } + this.alignment = alignment; + } + + /** + * Sets the central directory part of extra fields. + * @param b an array of bytes to be parsed into extra fields + */ + public void setCentralDirectoryExtra(final byte[] b) { + try { + mergeExtraFields(ExtraFieldUtils.parse(b, false, ExtraFieldParsingMode.BEST_EFFORT), false); + } catch (final ZipException e) { + // actually this is not possible as of Commons Compress 1.19 + throw new IllegalArgumentException(e.getMessage(), e); // NOSONAR + } + } + + /** + * Sets the source of the comment field value. + * @param commentSource source of the comment field value + * @since 1.16 + */ + public void setCommentSource(final CommentSource commentSource) { + this.commentSource = commentSource; + } + + /* (non-Javadoc) + * @see Object#equals(Object) + */ + + /** + * Sets the data offset. + * + * @param dataOffset + * new value of data offset. + */ + protected void setDataOffset(final long dataOffset) { + this.dataOffset = dataOffset; + } + + /** + * The number of the split segment this entry starts at. + * + * @param diskNumberStart the number of the split segment this entry starts at. + * @since 1.20 + */ + public void setDiskNumberStart(final long diskNumberStart) { + this.diskNumberStart = diskNumberStart; + } + + /** + * Sets the external file attributes. + * @param value an {@code long} value + */ + public void setExternalAttributes(final long value) { + externalAttributes = value; + } + + /** + * Unfortunately {@link java.util.zip.ZipOutputStream} seems to + * access the extra data directly, so overriding getExtra doesn't + * help - we need to modify super's data directly and on every update. + */ + protected void setExtra() { + // ZipEntry will update the time fields here, so we need to reprocess them afterwards + super.setExtra(ExtraFieldUtils.mergeLocalFileDataData(getAllExtraFieldsNoCopy())); + } + + /** + * Parses the given bytes as extra field data and consumes any + * unparseable data as an {@link UnparseableExtraFieldData} + * instance. + * @param extra an array of bytes to be parsed into extra fields + * @throws RuntimeException if the bytes cannot be parsed + * @throws RuntimeException on error + */ + @Override + public void setExtra(final byte[] extra) throws RuntimeException { + try { + mergeExtraFields(ExtraFieldUtils.parse(extra, true, ExtraFieldParsingMode.BEST_EFFORT), true); + } catch (final ZipException e) { + // actually this is not possible as of Commons Compress 1.1 + throw new IllegalArgumentException("Error parsing extra fields for entry: " // NOSONAR + + getName() + " - " + e.getMessage(), e); + } + } + + /** + * Replaces all currently attached extra fields with the new array. + * @param fields an array of extra fields + */ + public void setExtraFields(final ZipExtraField[] fields) { + unparseableExtra = null; + final List<ZipExtraField> newFields = new ArrayList<>(); + if (fields != null) { + for (final ZipExtraField field : fields) { + if (field instanceof UnparseableExtraFieldData) { + unparseableExtra = (UnparseableExtraFieldData) field; + } else { + newFields.add(field); + } + } + } + extraFields = newFields.toArray(ExtraFieldUtils.EMPTY_ZIP_EXTRA_FIELD_ARRAY); + setExtra(); + } + + /** + * The "general purpose bit" field. + * @param b the general purpose bit + * @since 1.1 + */ + public void setGeneralPurposeBit(final GeneralPurposeBit b) { + gpb = b; + } + + /** + * Sets the internal file attributes. + * @param value an {@code int} value + */ + public void setInternalAttributes(final int value) { + internalAttributes = value; + } + + protected void setLocalHeaderOffset(final long localHeaderOffset) { + this.localHeaderOffset = localHeaderOffset; + } + + /** + * Sets the compression method of this entry. + * + * @param method compression method + * + * @since 1.t1 + */ + @Override + public void setMethod(final int method) { + if (method < 0) { + throw new IllegalArgumentException( + "ZIP compression method can not be negative: " + method); + } + this.method = method; + } + + /** + * Set the name of the entry. + * @param name the name to use + */ + protected void setName(String name) { + if (name != null && getPlatform() == PLATFORM_FAT + && !name.contains("/")) { + name = name.replace('\\', '/'); + } + this.name = name; + } + + /** + * Sets the name using the raw bytes and the string created from + * it by guessing or using the configured encoding. + * @param name the name to use created from the raw bytes using + * the guessed or configured encoding + * @param rawName the bytes originally read as name from the + * archive + * @since 1.2 + */ + protected void setName(final String name, final byte[] rawName) { + setName(name); + this.rawName = rawName; + } + + /** + * Sets the source of the name field value. + * @param nameSource source of the name field value + * @since 1.16 + */ + public void setNameSource(final NameSource nameSource) { + this.nameSource = nameSource; + } + + /** + * Set the platform (UNIX or FAT). + * @param platform an {@code int} value - 0 is FAT, 3 is UNIX + */ + protected void setPlatform(final int platform) { + this.platform = platform; + } + + /** + * Sets the content of the flags field. + * @param rawFlag content of the flags field + * @since 1.11 + */ + public void setRawFlag(final int rawFlag) { + this.rawFlag = rawFlag; + } + + /** + * Sets the uncompressed size of the entry data. + * @param size the uncompressed size in bytes + * @throws IllegalArgumentException if the specified size is less + * than 0 + */ + @Override + public void setSize(final long size) { + if (size < 0) { + throw new IllegalArgumentException("Invalid entry size"); + } + this.size = size; + } + + protected void setStreamContiguous(final boolean isStreamContiguous) { + this.isStreamContiguous = isStreamContiguous; + } + + /** + * + * {@inheritDoc} + * + * <p>Override to work around bug <a href="https://bugs.openjdk.org/browse/JDK-8130914">JDK-8130914</a></p> + * + * @param time + * The last modification time of the entry in milliseconds + * since the epoch + * @see #getTime() + * @see #getLastModifiedTime() + */ + @Override + public void setTime(final long time) { + super.setTime(time); + this.time = time; + } + + /** + * Sets Unix permissions in a way that is understood by Info-Zip's + * unzip command. + * @param mode an {@code int} value + */ + public void setUnixMode(final int mode) { + // CheckStyle:MagicNumberCheck OFF - no point + setExternalAttributes((mode << SHORT_SHIFT) + // MS-DOS read-only attribute + | ((mode & 0200) == 0 ? 1 : 0) + // MS-DOS directory flag + | (isDirectory() ? 0x10 : 0)); + // CheckStyle:MagicNumberCheck ON + platform = PLATFORM_UNIX; + } + + /** + * Sets the "version made by" field. + * @param versionMadeBy "version made by" field + * @since 1.11 + */ + public void setVersionMadeBy(final int versionMadeBy) { + this.versionMadeBy = versionMadeBy; + } + + /** + * Sets the "version required to expand" field. + * @param versionRequired "version required to expand" field + * @since 1.11 + */ + public void setVersionRequired(final int versionRequired) { + this.versionRequired = versionRequired; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipArchiveEntryPredicate.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipArchiveEntryPredicate.java new file mode 100644 index 0000000000..dd330cf36f --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipArchiveEntryPredicate.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +/** + * A predicate to test if a #ZipArchiveEntry matches a criteria. + * Some day this can extend java.util.function.Predicate + * + * @since 1.10 + */ +public interface ZipArchiveEntryPredicate { + /** + * Indicate if the given entry should be included in the operation + * @param zipArchiveEntry the entry to test + * @return true if the entry should be included + */ + boolean test(ZipArchiveEntry zipArchiveEntry); +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipArchiveEntryRequest.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipArchiveEntryRequest.java new file mode 100644 index 0000000000..9066ebb792 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipArchiveEntryRequest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import org.readium.r2.shared.util.zip.compress.parallel.InputStreamSupplier; + +import java.io.InputStream; + +/** + * A Thread-safe representation of a ZipArchiveEntry that is used to add entries to parallel archives. + * + * @since 1.10 + */ +public class ZipArchiveEntryRequest { + /** + * Create a ZipArchiveEntryRequest + * @param zipArchiveEntry The entry to use + * @param payloadSupplier The payload that will be added to the zip entry. + * @return The newly created request + */ + public static ZipArchiveEntryRequest createZipArchiveEntryRequest(final ZipArchiveEntry zipArchiveEntry, final InputStreamSupplier payloadSupplier) { + return new ZipArchiveEntryRequest(zipArchiveEntry, payloadSupplier); + } + /* + The zipArchiveEntry is not thread safe, and cannot be safely accessed by the getters of this class. + It is safely accessible during the construction part of this class and also after the + thread pools have been shut down. + */ + private final ZipArchiveEntry zipArchiveEntry; + private final InputStreamSupplier payloadSupplier; + + + private final int method; + + private ZipArchiveEntryRequest(final ZipArchiveEntry zipArchiveEntry, final InputStreamSupplier payloadSupplier) { + // this constructor has "safe" access to all member variables on zipArchiveEntry + this.zipArchiveEntry = zipArchiveEntry; + this.payloadSupplier = payloadSupplier; + this.method = zipArchiveEntry.getMethod(); + } + + /** + * The compression method to use + * @return The compression method to use + */ + public int getMethod(){ + return method; + } + + /** + * The payload that will be added to this zip entry + * @return The input stream. + */ + public InputStream getPayloadStream() { + return payloadSupplier.get(); + } + + + /** + * Gets the underlying entry. Do not use this methods from threads that did not create the instance itself ! + * @return the zipArchiveEntry that is basis for this request + */ + ZipArchiveEntry getZipArchiveEntry() { + return zipArchiveEntry; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipArchiveOutputStream.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipArchiveOutputStream.java new file mode 100644 index 0000000000..08a93d5180 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipArchiveOutputStream.java @@ -0,0 +1,1929 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import static org.readium.r2.shared.util.zip.compress.archivers.zip.ZipLong.putLong; + +import org.readium.r2.shared.util.zip.compress.archivers.ArchiveEntry; +import org.readium.r2.shared.util.zip.compress.archivers.ArchiveOutputStream; +import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel; +import org.readium.r2.shared.util.zip.compress.utils.ByteUtils; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Calendar; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.zip.Deflater; +import java.util.zip.ZipException; + +/** + * Reimplementation of {@link java.util.zip.ZipOutputStream + * java.util.zip.ZipOutputStream} that does handle the extended + * functionality of this package, especially internal/external file + * attributes and extra fields with different layouts for local file + * data and central directory entries. + * + * <p>This class will try to use {@link + * java.nio.channels.SeekableByteChannel} when it knows that the + * output is going to go to a file and no split archive shall be + * created.</p> + * + * <p>If SeekableByteChannel cannot be used, this implementation will use + * a Data Descriptor to store size and CRC information for {@link + * #DEFLATED DEFLATED} entries, this means, you don't need to + * calculate them yourself. Unfortunately this is not possible for + * the {@link #STORED STORED} method, here setting the CRC and + * uncompressed size information is required before {@link + * #putArchiveEntry(ArchiveEntry)} can be called.</p> + * + * <p>As of Apache Commons Compress 1.3 it transparently supports Zip64 + * extensions and thus individual entries and archives larger than 4 + * GB or with more than 65536 entries in most cases but explicit + * control is provided via {@link #setUseZip64}. If the stream can not + * use SeekableByteChannel and you try to write a ZipArchiveEntry of + * unknown size then Zip64 extensions will be disabled by default.</p> + * + * @NotThreadSafe + */ +public class ZipArchiveOutputStream extends ArchiveOutputStream { + + /** + * Structure collecting information for the entry that is + * currently being written. + */ + private static final class CurrentEntry { + /** + * Current ZIP entry. + */ + private final ZipArchiveEntry entry; + /** + * Offset for CRC entry in the local file header data for the + * current entry starts here. + */ + private long localDataStart; + /** + * Data for local header data + */ + private long dataStart; + /** + * Number of bytes read for the current entry (can't rely on + * Deflater#getBytesRead) when using DEFLATED. + */ + private long bytesRead; + /** + * Whether current entry was the first one using ZIP64 features. + */ + private boolean causedUseOfZip64; + /** + * Whether write() has been called at all. + * + * <p>In order to create a valid archive {@link + * #closeArchiveEntry closeArchiveEntry} will write an empty + * array to get the CRC right if nothing has been written to + * the stream at all.</p> + */ + private boolean hasWritten; + private CurrentEntry(final ZipArchiveEntry entry) { + this.entry = entry; + } + } + private static final class EntryMetaData { + private final long offset; + private final boolean usesDataDescriptor; + private EntryMetaData(final long offset, final boolean usesDataDescriptor) { + this.offset = offset; + this.usesDataDescriptor = usesDataDescriptor; + } + } + /** + * enum that represents the possible policies for creating Unicode + * extra fields. + */ + public static final class UnicodeExtraFieldPolicy { + /** + * Always create Unicode extra fields. + */ + public static final UnicodeExtraFieldPolicy ALWAYS = new UnicodeExtraFieldPolicy("always"); + /** + * Never create Unicode extra fields. + */ + public static final UnicodeExtraFieldPolicy NEVER = new UnicodeExtraFieldPolicy("never"); + /** + * Create Unicode extra fields for file names that cannot be + * encoded using the specified encoding. + */ + public static final UnicodeExtraFieldPolicy NOT_ENCODEABLE = + new UnicodeExtraFieldPolicy("not encodeable"); + + private final String name; + private UnicodeExtraFieldPolicy(final String n) { + name = n; + } + @Override + public String toString() { + return name; + } + } + static final int BUFFER_SIZE = 512; + private static final int LFH_SIG_OFFSET = 0; + private static final int LFH_VERSION_NEEDED_OFFSET = 4; + private static final int LFH_GPB_OFFSET = 6; + private static final int LFH_METHOD_OFFSET = 8; + private static final int LFH_TIME_OFFSET = 10; + private static final int LFH_CRC_OFFSET = 14; + private static final int LFH_COMPRESSED_SIZE_OFFSET = 18; + private static final int LFH_ORIGINAL_SIZE_OFFSET = 22; + private static final int LFH_FILENAME_LENGTH_OFFSET = 26; + private static final int LFH_EXTRA_LENGTH_OFFSET = 28; + private static final int LFH_FILENAME_OFFSET = 30; + private static final int CFH_SIG_OFFSET = 0; + private static final int CFH_VERSION_MADE_BY_OFFSET = 4; + private static final int CFH_VERSION_NEEDED_OFFSET = 6; + private static final int CFH_GPB_OFFSET = 8; + private static final int CFH_METHOD_OFFSET = 10; + private static final int CFH_TIME_OFFSET = 12; + private static final int CFH_CRC_OFFSET = 16; + private static final int CFH_COMPRESSED_SIZE_OFFSET = 20; + private static final int CFH_ORIGINAL_SIZE_OFFSET = 24; + private static final int CFH_FILENAME_LENGTH_OFFSET = 28; + private static final int CFH_EXTRA_LENGTH_OFFSET = 30; + private static final int CFH_COMMENT_LENGTH_OFFSET = 32; + private static final int CFH_DISK_NUMBER_OFFSET = 34; + private static final int CFH_INTERNAL_ATTRIBUTES_OFFSET = 36; + + private static final int CFH_EXTERNAL_ATTRIBUTES_OFFSET = 38; + + private static final int CFH_LFH_OFFSET = 42; + + private static final int CFH_FILENAME_OFFSET = 46; + + /** + * Compression method for deflated entries. + */ + public static final int DEFLATED = java.util.zip.ZipEntry.DEFLATED; + + /** + * Default compression level for deflated entries. + */ + public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION; + + /** + * Compression method for stored entries. + */ + public static final int STORED = java.util.zip.ZipEntry.STORED; + + /** + * default encoding for file names and comment. + */ + static final String DEFAULT_ENCODING = ZipEncodingHelper.UTF8; + + /** + * General purpose flag, which indicates that file names are + * written in UTF-8. + * @deprecated use {@link GeneralPurposeBit#UFT8_NAMES_FLAG} instead + */ + @Deprecated + public static final int EFS_FLAG = GeneralPurposeBit.UFT8_NAMES_FLAG; + + /** + * Helper, a 0 as ZipShort. + */ + private static final byte[] ZERO = {0, 0}; + + /** + * Helper, a 0 as ZipLong. + */ + private static final byte[] LZERO = {0, 0, 0, 0}; + + private static final byte[] ONE = ZipLong.getBytes(1L); + + /* + * Various ZIP constants shared between this class, ZipArchiveInputStream and ZipFile + */ + /** + * local file header signature + */ + static final byte[] LFH_SIG = ZipLong.LFH_SIG.getBytes(); //NOSONAR + + /** + * data descriptor signature + */ + static final byte[] DD_SIG = ZipLong.DD_SIG.getBytes(); //NOSONAR + + /** + * central file header signature + */ + static final byte[] CFH_SIG = ZipLong.CFH_SIG.getBytes(); //NOSONAR + + /** + * end of central dir signature + */ + static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L); //NOSONAR + + /** + * ZIP64 end of central dir signature + */ + static final byte[] ZIP64_EOCD_SIG = ZipLong.getBytes(0X06064B50L); //NOSONAR + + /** + * ZIP64 end of central dir locator signature + */ + static final byte[] ZIP64_EOCD_LOC_SIG = ZipLong.getBytes(0X07064B50L); //NOSONAR + + /** indicates if this archive is finished. protected for use in Jar implementation */ + protected boolean finished; + + /** + * Current entry. + */ + private CurrentEntry entry; + + /** + * The file comment. + */ + private String comment = ""; + + /** + * Compression level for next entry. + */ + private int level = DEFAULT_COMPRESSION; + + /** + * Has the compression level changed when compared to the last + * entry? + */ + private boolean hasCompressionLevelChanged; + + /** + * Default compression method for next entry. + */ + private int method = java.util.zip.ZipEntry.DEFLATED; + + + /** + * List of ZipArchiveEntries written so far. + */ + private final List<ZipArchiveEntry> entries = + new LinkedList<>(); + private final StreamCompressor streamCompressor; + + /** + * Start of central directory. + */ + private long cdOffset; + + /** + * Length of central directory. + */ + private long cdLength; + + /** + * Disk number start of central directory. + */ + private long cdDiskNumberStart; + + /** + * Length of end of central directory + */ + private long eocdLength; + + /** + * Holds some book-keeping data for each entry. + */ + private final Map<ZipArchiveEntry, EntryMetaData> metaData = + new HashMap<>(); + + /** + * The encoding to use for file names and the file comment. + * + * <p>For a list of possible values see <a + * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>. + * Defaults to UTF-8.</p> + */ + private String encoding = DEFAULT_ENCODING; + + /** + * The zip encoding to use for file names and the file comment. + * + * This field is of internal use and will be set in {@link + * #setEncoding(String)}. + */ + private ZipEncoding zipEncoding = + ZipEncodingHelper.getZipEncoding(DEFAULT_ENCODING); + /** + * This Deflater object is used for output. + * + */ + protected final Deflater def; + + /** + * Optional random access output. + */ + private final SeekableByteChannel channel; + + private final OutputStream outputStream; + + /** + * whether to use the general purpose bit flag when writing UTF-8 + * file names or not. + */ + private boolean useUTF8Flag = true; + + /** + * Whether to encode non-encodable file names as UTF-8. + */ + private boolean fallbackToUTF8; + + /** + * whether to create UnicodePathExtraField-s for each entry. + */ + private UnicodeExtraFieldPolicy createUnicodeExtraFields = UnicodeExtraFieldPolicy.NEVER; + + /** + * Whether anything inside this archive has used a ZIP64 feature. + * + * @since 1.3 + */ + private boolean hasUsedZip64; + + private Zip64Mode zip64Mode = Zip64Mode.AsNeeded; + + private final byte[] copyBuffer = new byte[32768]; + + private final Calendar calendarInstance = Calendar.getInstance(); + + /** + * Whether we are creating a split zip + */ + private final boolean isSplitZip; + + /** + * Holds the number of Central Directories on each disk, this is used + * when writing Zip64 End Of Central Directory and End Of Central Directory + */ + private final Map<Integer, Integer> numberOfCDInDiskData = new HashMap<>(); + + /** + * Creates a new ZIP OutputStream filtering the underlying stream. + * @param out the outputstream to zip + */ + public ZipArchiveOutputStream(final OutputStream out) { + this.outputStream = out; + this.channel = null; + def = new Deflater(level, true); + streamCompressor = StreamCompressor.create(out, def); + isSplitZip = false; + } + + /** + * Creates a split ZIP Archive. + * + * <p>The files making up the archive will use Z01, Z02, + * ... extensions and the last part of it will be the given {@code + * file}.</p> + * + * <p>Even though the stream writes to a file this stream will + * behave as if no random access was possible. This means the + * sizes of stored entries need to be known before the actual + * entry data is written.</p> + * + * @param file the file that will become the last part of the split archive + * @param zipSplitSize maximum size of a single part of the split + * archive created by this stream. Must be between 64kB and about + * 4GB. + * + * @throws IOException on error + * @throws IllegalArgumentException if zipSplitSize is not in the required range + * @since 1.20 + */ + public ZipArchiveOutputStream(final File file, final long zipSplitSize) throws IOException { + def = new Deflater(level, true); + this.outputStream = new ZipSplitOutputStream(file, zipSplitSize); + streamCompressor = StreamCompressor.create(this.outputStream, def); + channel = null; + isSplitZip = true; + } + + /** + * Creates a new ZIP OutputStream writing to a SeekableByteChannel. + * + * @param channel the channel to zip to + * @since 1.13 + */ + public ZipArchiveOutputStream(final SeekableByteChannel channel) { + this.channel = channel; + def = new Deflater(level, true); + streamCompressor = StreamCompressor.create(channel, def); + outputStream = null; + isSplitZip = false; + } + + /** + * Adds an archive entry with a raw input stream. + * + * If crc, size and compressed size are supplied on the entry, these values will be used as-is. + * Zip64 status is re-established based on the settings in this stream, and the supplied value + * is ignored. + * + * The entry is put and closed immediately. + * + * @param entry The archive entry to add + * @param rawStream The raw input stream of a different entry. May be compressed/encrypted. + * @throws IOException If copying fails + */ + public void addRawArchiveEntry(final ZipArchiveEntry entry, final InputStream rawStream) + throws IOException { + final ZipArchiveEntry ae = new ZipArchiveEntry(entry); + if (hasZip64Extra(ae)) { + // Will be re-added as required. this may make the file generated with this method + // somewhat smaller than standard mode, + // since standard mode is unable to remove the zip 64 header. + ae.removeExtraField(Zip64ExtendedInformationExtraField.HEADER_ID); + } + final boolean is2PhaseSource = ae.getCrc() != ZipArchiveEntry.CRC_UNKNOWN + && ae.getSize() != ArchiveEntry.SIZE_UNKNOWN + && ae.getCompressedSize() != ArchiveEntry.SIZE_UNKNOWN; + putArchiveEntry(ae, is2PhaseSource); + copyFromZipInputStream(rawStream); + closeCopiedEntry(is2PhaseSource); + } + + /** + * Adds UnicodeExtra fields for name and file comment if mode is + * ALWAYS or the data cannot be encoded using the configured + * encoding. + */ + private void addUnicodeExtraFields(final ZipArchiveEntry ze, final boolean encodable, + final ByteBuffer name) + throws IOException { + if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS + || !encodable) { + ze.addExtraField(new UnicodePathExtraField(ze.getName(), + name.array(), + name.arrayOffset(), + name.limit() + - name.position())); + } + + final String comm = ze.getComment(); + if (comm != null && !comm.isEmpty()) { + + final boolean commentEncodable = zipEncoding.canEncode(comm); + + if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS + || !commentEncodable) { + final ByteBuffer commentB = getEntryEncoding(ze).encode(comm); + ze.addExtraField(new UnicodeCommentExtraField(comm, + commentB.array(), + commentB.arrayOffset(), + commentB.limit() + - commentB.position()) + ); + } + } + } + + /** + * Whether this stream is able to write the given entry. + * + * <p>May return false if it is set up to use encryption or a + * compression method that hasn't been implemented yet.</p> + * @since 1.1 + */ + @Override + public boolean canWriteEntryData(final ArchiveEntry ae) { + if (ae instanceof ZipArchiveEntry) { + final ZipArchiveEntry zae = (ZipArchiveEntry) ae; + return zae.getMethod() != ZipMethod.IMPLODING.getCode() + && zae.getMethod() != ZipMethod.UNSHRINKING.getCode() + && ZipUtil.canHandleEntryData(zae); + } + return false; + } + + /** + * Verifies the sizes aren't too big in the Zip64Mode.Never case + * and returns whether the entry would require a Zip64 extra + * field. + */ + private boolean checkIfNeedsZip64(final Zip64Mode effectiveMode) + throws ZipException { + final boolean actuallyNeedsZip64 = isZip64Required(entry.entry, effectiveMode); + if (actuallyNeedsZip64 && effectiveMode == Zip64Mode.Never) { + throw new Zip64RequiredException(Zip64RequiredException.getEntryTooBigMessage(entry.entry)); + } + return actuallyNeedsZip64; + } + + /** + * Closes this output stream and releases any system resources + * associated with the stream. + * + * @throws IOException if an I/O error occurs. + * @throws Zip64RequiredException if the archive's size exceeds 4 + * GByte or there are more than 65535 entries inside the archive + * and {@link #setUseZip64} is {@link Zip64Mode#Never}. + */ + @Override + public void close() throws IOException { + try { + if (!finished) { + finish(); + } + } finally { + destroy(); + } + } + + /** + * Writes all necessary data for this entry. + * @throws IOException on error + * @throws Zip64RequiredException if the entry's uncompressed or + * compressed size exceeds 4 GByte and {@link #setUseZip64} + * is {@link Zip64Mode#Never}. + */ + @Override + public void closeArchiveEntry() throws IOException { + preClose(); + + flushDeflater(); + + final long bytesWritten = streamCompressor.getTotalBytesWritten() - entry.dataStart; + final long realCrc = streamCompressor.getCrc32(); + entry.bytesRead = streamCompressor.getBytesRead(); + final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry); + final boolean actuallyNeedsZip64 = handleSizesAndCrc(bytesWritten, realCrc, effectiveMode); + closeEntry(actuallyNeedsZip64, false); + streamCompressor.reset(); + } + + /** + * Writes all necessary data for this entry. + * + * @param phased This entry is second phase of a 2-phase zip creation, size, compressed size and crc + * are known in ZipArchiveEntry + * @throws IOException on error + * @throws Zip64RequiredException if the entry's uncompressed or + * compressed size exceeds 4 GByte and {@link #setUseZip64} + * is {@link Zip64Mode#Never}. + */ + private void closeCopiedEntry(final boolean phased) throws IOException { + preClose(); + entry.bytesRead = entry.entry.getSize(); + final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry); + final boolean actuallyNeedsZip64 = checkIfNeedsZip64(effectiveMode); + closeEntry(actuallyNeedsZip64, phased); + } + + private void closeEntry(final boolean actuallyNeedsZip64, final boolean phased) throws IOException { + if (!phased && channel != null) { + rewriteSizesAndCrc(actuallyNeedsZip64); + } + + if (!phased) { + writeDataDescriptor(entry.entry); + } + entry = null; + } + + private void copyFromZipInputStream(final InputStream src) throws IOException { + if (entry == null) { + throw new IllegalStateException("No current entry"); + } + ZipUtil.checkRequestedFeatures(entry.entry); + entry.hasWritten = true; + int length; + while ((length = src.read(copyBuffer)) >= 0 ) + { + streamCompressor.writeCounted(copyBuffer, 0, length); + count( length ); + } + } + + /** + * Creates a new zip entry taking some information from the given + * file and using the provided name. + * + * <p>The name will be adjusted to end with a forward slash "/" if + * the file is a directory. If the file is not a directory a + * potential trailing forward slash will be stripped from the + * entry name.</p> + * + * <p>Must not be used if the stream has already been closed.</p> + */ + @Override + public ArchiveEntry createArchiveEntry(final File inputFile, final String entryName) + throws IOException { + if (finished) { + throw new IOException("Stream has already been finished"); + } + return new ZipArchiveEntry(inputFile, entryName); + } + + private byte[] createCentralFileHeader(final ZipArchiveEntry ze) throws IOException { + + final EntryMetaData entryMetaData = metaData.get(ze); + final boolean needsZip64Extra = hasZip64Extra(ze) + || ze.getCompressedSize() >= ZipConstants.ZIP64_MAGIC + || ze.getSize() >= ZipConstants.ZIP64_MAGIC + || entryMetaData.offset >= ZipConstants.ZIP64_MAGIC + || ze.getDiskNumberStart() >= ZipConstants.ZIP64_MAGIC_SHORT + || zip64Mode == Zip64Mode.Always + || zip64Mode == Zip64Mode.AlwaysWithCompatibility; + + if (needsZip64Extra && zip64Mode == Zip64Mode.Never) { + // must be the offset that is too big, otherwise an + // exception would have been throw in putArchiveEntry or + // closeArchiveEntry + throw new Zip64RequiredException(Zip64RequiredException + .ARCHIVE_TOO_BIG_MESSAGE); + } + + + handleZip64Extra(ze, entryMetaData.offset, needsZip64Extra); + + return createCentralFileHeader(ze, getName(ze), entryMetaData, needsZip64Extra); + } + + /** + * Writes the central file header entry. + * @param ze the entry to write + * @param name The encoded name + * @param entryMetaData meta data for this file + * @throws IOException on error + */ + private byte[] createCentralFileHeader(final ZipArchiveEntry ze, final ByteBuffer name, + final EntryMetaData entryMetaData, + final boolean needsZip64Extra) throws IOException { + if(isSplitZip) { + // calculate the disk number for every central file header, + // this will be used in writing End Of Central Directory and Zip64 End Of Central Directory + final int currentSplitSegment = ((ZipSplitOutputStream)this.outputStream).getCurrentSplitSegmentIndex(); + if(numberOfCDInDiskData.get(currentSplitSegment) == null) { + numberOfCDInDiskData.put(currentSplitSegment, 1); + } else { + final int originalNumberOfCD = numberOfCDInDiskData.get(currentSplitSegment); + numberOfCDInDiskData.put(currentSplitSegment, originalNumberOfCD + 1); + } + } + + final byte[] extra = ze.getCentralDirectoryExtra(); + final int extraLength = extra.length; + + // file comment length + String comm = ze.getComment(); + if (comm == null) { + comm = ""; + } + + final ByteBuffer commentB = getEntryEncoding(ze).encode(comm); + final int nameLen = name.limit() - name.position(); + final int commentLen = commentB.limit() - commentB.position(); + final int len= CFH_FILENAME_OFFSET + nameLen + extraLength + commentLen; + final byte[] buf = new byte[len]; + + System.arraycopy(CFH_SIG, 0, buf, CFH_SIG_OFFSET, ZipConstants.WORD); + + // version made by + // CheckStyle:MagicNumber OFF + ZipShort.putShort((ze.getPlatform() << 8) | (!hasUsedZip64 ? ZipConstants.DATA_DESCRIPTOR_MIN_VERSION : ZipConstants.ZIP64_MIN_VERSION), + buf, CFH_VERSION_MADE_BY_OFFSET); + + final int zipMethod = ze.getMethod(); + final boolean encodable = zipEncoding.canEncode(ze.getName()); + ZipShort.putShort(versionNeededToExtract(zipMethod, needsZip64Extra, entryMetaData.usesDataDescriptor), + buf, CFH_VERSION_NEEDED_OFFSET); + getGeneralPurposeBits(!encodable && fallbackToUTF8, entryMetaData.usesDataDescriptor).encode(buf, CFH_GPB_OFFSET); + + // compression method + ZipShort.putShort(zipMethod, buf, CFH_METHOD_OFFSET); + + + // last mod. time and date + ZipUtil.toDosTime(calendarInstance, ze.getTime(), buf, CFH_TIME_OFFSET); + + // CRC + // compressed length + // uncompressed length + putLong(ze.getCrc(), buf, CFH_CRC_OFFSET); + if (ze.getCompressedSize() >= ZipConstants.ZIP64_MAGIC + || ze.getSize() >= ZipConstants.ZIP64_MAGIC + || zip64Mode == Zip64Mode.Always + || zip64Mode == Zip64Mode.AlwaysWithCompatibility) { + ZipLong.ZIP64_MAGIC.putLong(buf, CFH_COMPRESSED_SIZE_OFFSET); + ZipLong.ZIP64_MAGIC.putLong(buf, CFH_ORIGINAL_SIZE_OFFSET); + } else { + putLong(ze.getCompressedSize(), buf, CFH_COMPRESSED_SIZE_OFFSET); + putLong(ze.getSize(), buf, CFH_ORIGINAL_SIZE_OFFSET); + } + + ZipShort.putShort(nameLen, buf, CFH_FILENAME_LENGTH_OFFSET); + + // extra field length + ZipShort.putShort(extraLength, buf, CFH_EXTRA_LENGTH_OFFSET); + + ZipShort.putShort(commentLen, buf, CFH_COMMENT_LENGTH_OFFSET); + + // disk number start + if(isSplitZip) { + if (ze.getDiskNumberStart() >= ZipConstants.ZIP64_MAGIC_SHORT || zip64Mode == Zip64Mode.Always) { + ZipShort.putShort(ZipConstants.ZIP64_MAGIC_SHORT, buf, CFH_DISK_NUMBER_OFFSET); + } else { + ZipShort.putShort((int) ze.getDiskNumberStart(), buf, CFH_DISK_NUMBER_OFFSET); + } + } else { + System.arraycopy(ZERO, 0, buf, CFH_DISK_NUMBER_OFFSET, ZipConstants.SHORT); + } + + // internal file attributes + ZipShort.putShort(ze.getInternalAttributes(), buf, CFH_INTERNAL_ATTRIBUTES_OFFSET); + + // external file attributes + putLong(ze.getExternalAttributes(), buf, CFH_EXTERNAL_ATTRIBUTES_OFFSET); + + // relative offset of LFH + if (entryMetaData.offset >= ZipConstants.ZIP64_MAGIC || zip64Mode == Zip64Mode.Always) { + putLong(ZipConstants.ZIP64_MAGIC, buf, CFH_LFH_OFFSET); + } else { + putLong(Math.min(entryMetaData.offset, ZipConstants.ZIP64_MAGIC), buf, CFH_LFH_OFFSET); + } + + // file name + System.arraycopy(name.array(), name.arrayOffset(), buf, CFH_FILENAME_OFFSET, nameLen); + + final int extraStart = CFH_FILENAME_OFFSET + nameLen; + System.arraycopy(extra, 0, buf, extraStart, extraLength); + + final int commentStart = extraStart + extraLength; + + // file comment + System.arraycopy(commentB.array(), commentB.arrayOffset(), buf, commentStart, commentLen); + return buf; + } + + private byte[] createLocalFileHeader(final ZipArchiveEntry ze, final ByteBuffer name, final boolean encodable, + final boolean phased, final long archiveOffset) { + final ZipExtraField oldEx = ze.getExtraField(ResourceAlignmentExtraField.ID); + if (oldEx != null) { + ze.removeExtraField(ResourceAlignmentExtraField.ID); + } + final ResourceAlignmentExtraField oldAlignmentEx = + oldEx instanceof ResourceAlignmentExtraField ? (ResourceAlignmentExtraField) oldEx : null; + + int alignment = ze.getAlignment(); + if (alignment <= 0 && oldAlignmentEx != null) { + alignment = oldAlignmentEx.getAlignment(); + } + + if (alignment > 1 || (oldAlignmentEx != null && !oldAlignmentEx.allowMethodChange())) { + final int oldLength = LFH_FILENAME_OFFSET + + name.limit() - name.position() + + ze.getLocalFileDataExtra().length; + + final int padding = (int) ((-archiveOffset - oldLength - ZipExtraField.EXTRAFIELD_HEADER_SIZE + - ResourceAlignmentExtraField.BASE_SIZE) & + (alignment - 1)); + ze.addExtraField(new ResourceAlignmentExtraField(alignment, + oldAlignmentEx != null && oldAlignmentEx.allowMethodChange(), padding)); + } + + final byte[] extra = ze.getLocalFileDataExtra(); + final int nameLen = name.limit() - name.position(); + final int len = LFH_FILENAME_OFFSET + nameLen + extra.length; + final byte[] buf = new byte[len]; + + System.arraycopy(LFH_SIG, 0, buf, LFH_SIG_OFFSET, ZipConstants.WORD); + + //store method in local variable to prevent multiple method calls + final int zipMethod = ze.getMethod(); + final boolean dataDescriptor = usesDataDescriptor(zipMethod, phased); + + ZipShort.putShort(versionNeededToExtract(zipMethod, hasZip64Extra(ze), dataDescriptor), buf, LFH_VERSION_NEEDED_OFFSET); + + final GeneralPurposeBit generalPurposeBit = getGeneralPurposeBits(!encodable && fallbackToUTF8, dataDescriptor); + generalPurposeBit.encode(buf, LFH_GPB_OFFSET); + + // compression method + ZipShort.putShort(zipMethod, buf, LFH_METHOD_OFFSET); + + ZipUtil.toDosTime(calendarInstance, ze.getTime(), buf, LFH_TIME_OFFSET); + + // CRC + if (phased || !(zipMethod == DEFLATED || channel != null)){ + putLong(ze.getCrc(), buf, LFH_CRC_OFFSET); + } else { + System.arraycopy(LZERO, 0, buf, LFH_CRC_OFFSET, ZipConstants.WORD); + } + + // compressed length + // uncompressed length + if (hasZip64Extra(entry.entry)){ + // point to ZIP64 extended information extra field for + // sizes, may get rewritten once sizes are known if + // stream is seekable + ZipLong.ZIP64_MAGIC.putLong(buf, LFH_COMPRESSED_SIZE_OFFSET); + ZipLong.ZIP64_MAGIC.putLong(buf, LFH_ORIGINAL_SIZE_OFFSET); + } else if (phased) { + putLong(ze.getCompressedSize(), buf, LFH_COMPRESSED_SIZE_OFFSET); + putLong(ze.getSize(), buf, LFH_ORIGINAL_SIZE_OFFSET); + } else if (zipMethod == DEFLATED || channel != null) { + System.arraycopy(LZERO, 0, buf, LFH_COMPRESSED_SIZE_OFFSET, ZipConstants.WORD); + System.arraycopy(LZERO, 0, buf, LFH_ORIGINAL_SIZE_OFFSET, ZipConstants.WORD); + } else { // Stored + putLong(ze.getSize(), buf, LFH_COMPRESSED_SIZE_OFFSET); + putLong(ze.getSize(), buf, LFH_ORIGINAL_SIZE_OFFSET); + } + // file name length + ZipShort.putShort(nameLen, buf, LFH_FILENAME_LENGTH_OFFSET); + + // extra field length + ZipShort.putShort(extra.length, buf, LFH_EXTRA_LENGTH_OFFSET); + + // file name + System.arraycopy( name.array(), name.arrayOffset(), buf, LFH_FILENAME_OFFSET, nameLen); + + // extra fields + System.arraycopy(extra, 0, buf, LFH_FILENAME_OFFSET + nameLen, extra.length); + + return buf; + } + + /** + * Writes next block of compressed data to the output stream. + * @throws IOException on error + */ + protected final void deflate() throws IOException { + streamCompressor.deflate(); + } + + /** + * Closes the underlying stream/file without finishing the + * archive, the result will likely be a corrupt archive. + * + * <p>This method only exists to support tests that generate + * corrupt archives so they can clean up any temporary files.</p> + */ + void destroy() throws IOException { + try { + if (channel != null) { + channel.close(); + } + } finally { + if (outputStream != null) { + outputStream.close(); + } + } + } + + /** + * {@inheritDoc} + * @throws Zip64RequiredException if the archive's size exceeds 4 + * GByte or there are more than 65535 entries inside the archive + * and {@link #setUseZip64} is {@link Zip64Mode#Never}. + */ + @Override + public void finish() throws IOException { + if (finished) { + throw new IOException("This archive has already been finished"); + } + + if (entry != null) { + throw new IOException("This archive contains unclosed entries."); + } + + final long cdOverallOffset = streamCompressor.getTotalBytesWritten(); + cdOffset = cdOverallOffset; + if (isSplitZip) { + // when creating a split zip, the offset should be + // the offset to the corresponding segment disk + final ZipSplitOutputStream zipSplitOutputStream = (ZipSplitOutputStream)this.outputStream; + cdOffset = zipSplitOutputStream.getCurrentSplitSegmentBytesWritten(); + cdDiskNumberStart = zipSplitOutputStream.getCurrentSplitSegmentIndex(); + } + writeCentralDirectoryInChunks(); + + cdLength = streamCompressor.getTotalBytesWritten() - cdOverallOffset; + + // calculate the length of end of central directory, as it may be used in writeZip64CentralDirectory + final ByteBuffer commentData = this.zipEncoding.encode(comment); + final long commentLength = (long) commentData.limit() - commentData.position(); + eocdLength = ZipConstants.WORD /* length of EOCD_SIG */ + + ZipConstants.SHORT /* number of this disk */ + + ZipConstants.SHORT /* disk number of start of central directory */ + + ZipConstants.SHORT /* total number of entries on this disk */ + + ZipConstants.SHORT /* total number of entries */ + + ZipConstants.WORD /* size of central directory */ + + ZipConstants.WORD /* offset of start of central directory */ + + ZipConstants.SHORT /* zip comment length */ + + commentLength /* zip comment */; + + writeZip64CentralDirectory(); + writeCentralDirectoryEnd(); + metaData.clear(); + entries.clear(); + streamCompressor.close(); + if (isSplitZip) { + // trigger the ZipSplitOutputStream to write the final split segment + outputStream.close(); + } + finished = true; + } + + /** + * Flushes this output stream and forces any buffered output bytes + * to be written out to the stream. + * + * @throws IOException if an I/O error occurs. + */ + @Override + public void flush() throws IOException { + if (outputStream != null) { + outputStream.flush(); + } + } + + /** + * Ensures all bytes sent to the deflater are written to the stream. + */ + private void flushDeflater() throws IOException { + if (entry.entry.getMethod() == DEFLATED) { + streamCompressor.flushDeflater(); + } + } + + /** + * Returns the total number of bytes written to this stream. + * @return the number of written bytes + * @since 1.22 + */ + @Override + public long getBytesWritten() { + return streamCompressor.getTotalBytesWritten(); + } + + /** + * If the mode is AsNeeded and the entry is a compressed entry of + * unknown size that gets written to a non-seekable stream then + * change the default to Never. + * + * @since 1.3 + */ + private Zip64Mode getEffectiveZip64Mode(final ZipArchiveEntry ze) { + if (zip64Mode != Zip64Mode.AsNeeded + || channel != null + || ze.getMethod() != DEFLATED + || ze.getSize() != ArchiveEntry.SIZE_UNKNOWN) { + return zip64Mode; + } + return Zip64Mode.Never; + } + + /** + * The encoding to use for file names and the file comment. + * + * @return null if using the platform's default character encoding. + */ + public String getEncoding() { + return encoding; + } + + private ZipEncoding getEntryEncoding(final ZipArchiveEntry ze) { + final boolean encodable = zipEncoding.canEncode(ze.getName()); + return !encodable && fallbackToUTF8 + ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding; + } + + private GeneralPurposeBit getGeneralPurposeBits(final boolean utfFallback, final boolean usesDataDescriptor) { + final GeneralPurposeBit b = new GeneralPurposeBit(); + b.useUTF8ForNames(useUTF8Flag || utfFallback); + if (usesDataDescriptor) { + b.useDataDescriptor(true); + } + return b; + } + + private ByteBuffer getName(final ZipArchiveEntry ze) throws IOException { + return getEntryEncoding(ze).encode(ze.getName()); + } + + /** + * Get the existing ZIP64 extended information extra field or + * create a new one and add it to the entry. + * + * @since 1.3 + */ + private Zip64ExtendedInformationExtraField + getZip64Extra(final ZipArchiveEntry ze) { + if (entry != null) { + entry.causedUseOfZip64 = !hasUsedZip64; + } + hasUsedZip64 = true; + final ZipExtraField extra = ze.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID); + Zip64ExtendedInformationExtraField z64 = extra instanceof Zip64ExtendedInformationExtraField + ? (Zip64ExtendedInformationExtraField) extra : null; + if (z64 == null) { + /* + System.err.println("Adding z64 for " + ze.getName() + + ", method: " + ze.getMethod() + + " (" + (ze.getMethod() == STORED) + ")" + + ", channel: " + (channel != null)); + */ + z64 = new Zip64ExtendedInformationExtraField(); + } + + // even if the field is there already, make sure it is the first one + ze.addAsFirstExtraField(z64); + + return z64; + } + + /** + * Ensures the current entry's size and CRC information is set to + * the values just written, verifies it isn't too big in the + * Zip64Mode.Never case and returns whether the entry would + * require a Zip64 extra field. + */ + private boolean handleSizesAndCrc(final long bytesWritten, final long crc, + final Zip64Mode effectiveMode) + throws ZipException { + if (entry.entry.getMethod() == DEFLATED) { + /* It turns out def.getBytesRead() returns wrong values if + * the size exceeds 4 GB on Java < Java7 + entry.entry.setSize(def.getBytesRead()); + */ + entry.entry.setSize(entry.bytesRead); + entry.entry.setCompressedSize(bytesWritten); + entry.entry.setCrc(crc); + + } else if (channel == null) { + if (entry.entry.getCrc() != crc) { + throw new ZipException("Bad CRC checksum for entry " + + entry.entry.getName() + ": " + + Long.toHexString(entry.entry.getCrc()) + + " instead of " + + Long.toHexString(crc)); + } + + if (entry.entry.getSize() != bytesWritten) { + throw new ZipException("Bad size for entry " + + entry.entry.getName() + ": " + + entry.entry.getSize() + + " instead of " + + bytesWritten); + } + } else { /* method is STORED and we used SeekableByteChannel */ + entry.entry.setSize(bytesWritten); + entry.entry.setCompressedSize(bytesWritten); + entry.entry.setCrc(crc); + } + + return checkIfNeedsZip64(effectiveMode); + } + + /** + * If the entry needs Zip64 extra information inside the central + * directory then configure its data. + */ + private void handleZip64Extra(final ZipArchiveEntry ze, final long lfhOffset, + final boolean needsZip64Extra) { + if (needsZip64Extra) { + final Zip64ExtendedInformationExtraField z64 = getZip64Extra(ze); + if (ze.getCompressedSize() >= ZipConstants.ZIP64_MAGIC + || ze.getSize() >= ZipConstants.ZIP64_MAGIC + || zip64Mode == Zip64Mode.Always + || zip64Mode == Zip64Mode.AlwaysWithCompatibility) { + z64.setCompressedSize(new ZipEightByteInteger(ze.getCompressedSize())); + z64.setSize(new ZipEightByteInteger(ze.getSize())); + } else { + // reset value that may have been set for LFH + z64.setCompressedSize(null); + z64.setSize(null); + } + + final boolean needsToEncodeLfhOffset = + lfhOffset >= ZipConstants.ZIP64_MAGIC || zip64Mode == Zip64Mode.Always; + final boolean needsToEncodeDiskNumberStart = + ze.getDiskNumberStart() >= ZipConstants.ZIP64_MAGIC_SHORT || zip64Mode == Zip64Mode.Always; + + if (needsToEncodeLfhOffset || needsToEncodeDiskNumberStart) { + z64.setRelativeHeaderOffset(new ZipEightByteInteger(lfhOffset)); + } + if (needsToEncodeDiskNumberStart) { + z64.setDiskStartNumber(new ZipLong(ze.getDiskNumberStart())); + } + ze.setExtra(); + } + } + + /** + * Is there a ZIP64 extended information extra field for the + * entry? + * + * @since 1.3 + */ + private boolean hasZip64Extra(final ZipArchiveEntry ze) { + return ze.getExtraField(Zip64ExtendedInformationExtraField + .HEADER_ID) + instanceof Zip64ExtendedInformationExtraField; + } + /** + * This method indicates whether this archive is writing to a + * seekable stream (i.e., to a random access file). + * + * <p>For seekable streams, you don't need to calculate the CRC or + * uncompressed size for {@link #STORED} entries before + * invoking {@link #putArchiveEntry(ArchiveEntry)}. + * @return true if seekable + */ + public boolean isSeekable() { + return channel != null; + } + private boolean isTooLargeForZip32(final ZipArchiveEntry zipArchiveEntry){ + return zipArchiveEntry.getSize() >= ZipConstants.ZIP64_MAGIC || zipArchiveEntry.getCompressedSize() >= ZipConstants.ZIP64_MAGIC; + } + private boolean isZip64Required(final ZipArchiveEntry entry1, final Zip64Mode requestedMode) { + return requestedMode == Zip64Mode.Always || requestedMode == Zip64Mode.AlwaysWithCompatibility + || isTooLargeForZip32(entry1); + } + private void preClose() throws IOException { + if (finished) { + throw new IOException("Stream has already been finished"); + } + + if (entry == null) { + throw new IOException("No current entry to close"); + } + + if (!entry.hasWritten) { + write(ByteUtils.EMPTY_BYTE_ARRAY, 0, 0); + } + } + /** + * {@inheritDoc} + * @throws ClassCastException if entry is not an instance of ZipArchiveEntry + * @throws Zip64RequiredException if the entry's uncompressed or + * compressed size is known to exceed 4 GByte and {@link #setUseZip64} + * is {@link Zip64Mode#Never}. + */ + @Override + public void putArchiveEntry(final ArchiveEntry archiveEntry) throws IOException { + putArchiveEntry(archiveEntry, false); + } + + /** + * Writes the headers for an archive entry to the output stream. + * The caller must then write the content to the stream and call + * {@link #closeArchiveEntry()} to complete the process. + + * @param archiveEntry The archiveEntry + * @param phased If true size, compressedSize and crc required to be known up-front in the archiveEntry + * @throws ClassCastException if entry is not an instance of ZipArchiveEntry + * @throws Zip64RequiredException if the entry's uncompressed or + * compressed size is known to exceed 4 GByte and {@link #setUseZip64} + * is {@link Zip64Mode#Never}. + */ + private void putArchiveEntry(final ArchiveEntry archiveEntry, final boolean phased) throws IOException { + if (finished) { + throw new IOException("Stream has already been finished"); + } + + if (entry != null) { + closeArchiveEntry(); + } + + entry = new CurrentEntry((ZipArchiveEntry) archiveEntry); + entries.add(entry.entry); + + setDefaults(entry.entry); + + final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry); + validateSizeInformation(effectiveMode); + + if (shouldAddZip64Extra(entry.entry, effectiveMode)) { + + final Zip64ExtendedInformationExtraField z64 = getZip64Extra(entry.entry); + + final ZipEightByteInteger size; + final ZipEightByteInteger compressedSize; + if (phased) { + // sizes are already known + size = new ZipEightByteInteger(entry.entry.getSize()); + compressedSize = new ZipEightByteInteger(entry.entry.getCompressedSize()); + } else if (entry.entry.getMethod() == STORED + && entry.entry.getSize() != ArchiveEntry.SIZE_UNKNOWN) { + // actually, we already know the sizes + compressedSize = size = new ZipEightByteInteger(entry.entry.getSize()); + } else { + // just a placeholder, real data will be in data + // descriptor or inserted later via SeekableByteChannel + compressedSize = size = ZipEightByteInteger.ZERO; + } + z64.setSize(size); + z64.setCompressedSize(compressedSize); + entry.entry.setExtra(); + } + + if (entry.entry.getMethod() == DEFLATED && hasCompressionLevelChanged) { + def.setLevel(level); + hasCompressionLevelChanged = false; + } + writeLocalFileHeader((ZipArchiveEntry) archiveEntry, phased); + } + + /** + * When using random access output, write the local file header + * and potentially the ZIP64 extra containing the correct CRC and + * compressed/uncompressed sizes. + */ + private void rewriteSizesAndCrc(final boolean actuallyNeedsZip64) + throws IOException { + final long save = channel.position(); + + channel.position(entry.localDataStart); + writeOut(ZipLong.getBytes(entry.entry.getCrc())); + if (!hasZip64Extra(entry.entry) || !actuallyNeedsZip64) { + writeOut(ZipLong.getBytes(entry.entry.getCompressedSize())); + writeOut(ZipLong.getBytes(entry.entry.getSize())); + } else { + writeOut(ZipLong.ZIP64_MAGIC.getBytes()); + writeOut(ZipLong.ZIP64_MAGIC.getBytes()); + } + + if (hasZip64Extra(entry.entry)) { + final ByteBuffer name = getName(entry.entry); + final int nameLen = name.limit() - name.position(); + // seek to ZIP64 extra, skip header and size information + channel.position(entry.localDataStart + 3 * ZipConstants.WORD + 2 * ZipConstants.SHORT + + nameLen + 2 * ZipConstants.SHORT); + // inside the ZIP64 extra uncompressed size comes + // first, unlike the LFH, CD or data descriptor + writeOut(ZipEightByteInteger.getBytes(entry.entry.getSize())); + writeOut(ZipEightByteInteger.getBytes(entry.entry.getCompressedSize())); + + if (!actuallyNeedsZip64) { + // do some cleanup: + // * rewrite version needed to extract + channel.position(entry.localDataStart - 5 * ZipConstants.SHORT); + writeOut(ZipShort.getBytes(versionNeededToExtract(entry.entry.getMethod(), false, false))); + + // * remove ZIP64 extra so it doesn't get written + // to the central directory + entry.entry.removeExtraField(Zip64ExtendedInformationExtraField + .HEADER_ID); + entry.entry.setExtra(); + + // * reset hasUsedZip64 if it has been set because + // of this entry + if (entry.causedUseOfZip64) { + hasUsedZip64 = false; + } + } + } + channel.position(save); + } + + /** + * Set the file comment. + * @param comment the comment + */ + public void setComment(final String comment) { + this.comment = comment; + } + + + /** + * Whether to create Unicode Extra Fields. + * + * <p>Defaults to NEVER.</p> + * + * @param b whether to create Unicode Extra Fields. + */ + public void setCreateUnicodeExtraFields(final UnicodeExtraFieldPolicy b) { + createUnicodeExtraFields = b; + } + + + /** + * Provides default values for compression method and last + * modification time. + */ + private void setDefaults(final ZipArchiveEntry entry) { + if (entry.getMethod() == -1) { // not specified + entry.setMethod(method); + } + + if (entry.getTime() == -1) { // not specified + entry.setTime(System.currentTimeMillis()); + } + } + + /** + * The encoding to use for file names and the file comment. + * + * <p>For a list of possible values see <a + * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>. + * Defaults to UTF-8.</p> + * @param encoding the encoding to use for file names, use null + * for the platform's default encoding + */ + public void setEncoding(final String encoding) { + this.encoding = encoding; + this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); + if (useUTF8Flag && !ZipEncodingHelper.isUTF8(encoding)) { + useUTF8Flag = false; + } + } + + /** + * Whether to fall back to UTF and the language encoding flag if + * the file name cannot be encoded using the specified encoding. + * + * <p>Defaults to false.</p> + * + * @param b whether to fall back to UTF and the language encoding + * flag if the file name cannot be encoded using the specified + * encoding. + */ + public void setFallbackToUTF8(final boolean b) { + fallbackToUTF8 = b; + } + + /** + * Sets the compression level for subsequent entries. + * + * <p>Default is Deflater.DEFAULT_COMPRESSION.</p> + * @param level the compression level. + * @throws IllegalArgumentException if an invalid compression + * level is specified. + */ + public void setLevel(final int level) { + if (level < Deflater.DEFAULT_COMPRESSION + || level > Deflater.BEST_COMPRESSION) { + throw new IllegalArgumentException("Invalid compression level: " + + level); + } + if (this.level == level) { + return; + } + hasCompressionLevelChanged = true; + this.level = level; + } + + /** + * Sets the default compression method for subsequent entries. + * + * <p>Default is DEFLATED.</p> + * @param method an {@code int} from java.util.zip.ZipEntry + */ + public void setMethod(final int method) { + this.method = method; + } + + /** + * Whether to set the language encoding flag if the file name + * encoding is UTF-8. + * + * <p>Defaults to true.</p> + * + * @param b whether to set the language encoding flag if the file + * name encoding is UTF-8 + */ + public void setUseLanguageEncodingFlag(final boolean b) { + useUTF8Flag = b && ZipEncodingHelper.isUTF8(encoding); + } + + /** + * Whether Zip64 extensions will be used. + * + * <p>When setting the mode to {@link Zip64Mode#Never Never}, + * {@link #putArchiveEntry}, {@link #closeArchiveEntry}, {@link + * #finish} or {@link #close} may throw a {@link + * Zip64RequiredException} if the entry's size or the total size + * of the archive exceeds 4GB or there are more than 65536 entries + * inside the archive. Any archive created in this mode will be + * readable by implementations that don't support Zip64.</p> + * + * <p>When setting the mode to {@link Zip64Mode#Always Always}, + * Zip64 extensions will be used for all entries. Any archive + * created in this mode may be unreadable by implementations that + * don't support Zip64 even if all its contents would be.</p> + * + * <p>When setting the mode to {@link Zip64Mode#AsNeeded + * AsNeeded}, Zip64 extensions will transparently be used for + * those entries that require them. This mode can only be used if + * the uncompressed size of the {@link ZipArchiveEntry} is known + * when calling {@link #putArchiveEntry} - + * this mode is not valid when the output stream is not seekable + * and the uncompressed size is unknown when {@link + * #putArchiveEntry} is called.</p> + * + * <p>If no entry inside the resulting archive requires Zip64 + * extensions then {@link Zip64Mode#Never Never} will create the + * smallest archive. {@link Zip64Mode#AsNeeded AsNeeded} will + * create a slightly bigger archive if the uncompressed size of + * any entry has initially been unknown and create an archive + * identical to {@link Zip64Mode#Never Never} otherwise. {@link + * Zip64Mode#Always Always} will create an archive that is at + * least 24 bytes per entry bigger than the one {@link + * Zip64Mode#Never Never} would create.</p> + * + * <p>Defaults to {@link Zip64Mode#AsNeeded AsNeeded} unless + * {@link #putArchiveEntry} is called with an entry of unknown + * size and data is written to a non-seekable stream - in this + * case the default is {@link Zip64Mode#Never Never}.</p> + * + * @since 1.3 + * @param mode Whether Zip64 extensions will be used. + */ + public void setUseZip64(final Zip64Mode mode) { + zip64Mode = mode; + } + + /** + * Whether to add a Zip64 extended information extra field to the + * local file header. + * + * <p>Returns true if</p> + * + * <ul> + * <li>mode is Always</li> + * <li>or we already know it is going to be needed</li> + * <li>or the size is unknown and we can ensure it won't hurt + * other implementations if we add it (i.e. we can erase its + * usage</li> + * </ul> + */ + private boolean shouldAddZip64Extra(final ZipArchiveEntry entry, final Zip64Mode mode) { + return mode == Zip64Mode.Always + || mode == Zip64Mode.AlwaysWithCompatibility + || entry.getSize() >= ZipConstants.ZIP64_MAGIC + || entry.getCompressedSize() >= ZipConstants.ZIP64_MAGIC + || (entry.getSize() == ArchiveEntry.SIZE_UNKNOWN + && channel != null && mode != Zip64Mode.Never); + } + + /** + * 4.4.1.4 If one of the fields in the end of central directory + * record is too small to hold required data, the field SHOULD be + * set to -1 (0xFFFF or 0xFFFFFFFF) and the ZIP64 format record + * SHOULD be created. + * @return true if zip64 End Of Central Directory is needed + */ + private boolean shouldUseZip64EOCD() { + int numberOfThisDisk = 0; + if(isSplitZip) { + numberOfThisDisk = ((ZipSplitOutputStream)this.outputStream).getCurrentSplitSegmentIndex(); + } + final int numOfEntriesOnThisDisk = numberOfCDInDiskData.get(numberOfThisDisk) == null ? 0 : numberOfCDInDiskData.get(numberOfThisDisk); + return numberOfThisDisk >= ZipConstants.ZIP64_MAGIC_SHORT /* number of this disk */ + || cdDiskNumberStart >= ZipConstants.ZIP64_MAGIC_SHORT /* number of the disk with the start of the central directory */ + || numOfEntriesOnThisDisk >= ZipConstants.ZIP64_MAGIC_SHORT /* total number of entries in the central directory on this disk */ + || entries.size() >= ZipConstants.ZIP64_MAGIC_SHORT /* total number of entries in the central directory */ + || cdLength >= ZipConstants.ZIP64_MAGIC /* size of the central directory */ + || cdOffset >= ZipConstants.ZIP64_MAGIC; /* offset of start of central directory with respect to + the starting disk number */ + } + + private boolean usesDataDescriptor(final int zipMethod, final boolean phased) { + return !phased && zipMethod == DEFLATED && channel == null; + } + + /** + * If the Zip64 mode is set to never, then all the data in End Of Central Directory + * should not exceed their limits. + * @throws Zip64RequiredException if Zip64 is actually needed + */ + private void validateIfZip64IsNeededInEOCD() throws Zip64RequiredException { + // exception will only be thrown if the Zip64 mode is never while Zip64 is actually needed + if (zip64Mode != Zip64Mode.Never) { + return; + } + + int numberOfThisDisk = 0; + if (isSplitZip) { + numberOfThisDisk = ((ZipSplitOutputStream)this.outputStream).getCurrentSplitSegmentIndex(); + } + if (numberOfThisDisk >= ZipConstants.ZIP64_MAGIC_SHORT) { + throw new Zip64RequiredException(Zip64RequiredException + .NUMBER_OF_THIS_DISK_TOO_BIG_MESSAGE); + } + + if (cdDiskNumberStart >= ZipConstants.ZIP64_MAGIC_SHORT) { + throw new Zip64RequiredException(Zip64RequiredException + .NUMBER_OF_THE_DISK_OF_CENTRAL_DIRECTORY_TOO_BIG_MESSAGE); + } + + final int numOfEntriesOnThisDisk = numberOfCDInDiskData.get(numberOfThisDisk) == null + ? 0 : numberOfCDInDiskData.get(numberOfThisDisk); + if (numOfEntriesOnThisDisk >= ZipConstants.ZIP64_MAGIC_SHORT) { + throw new Zip64RequiredException(Zip64RequiredException + .TOO_MANY_ENTRIES_ON_THIS_DISK_MESSAGE); + } + + // number of entries + if (entries.size() >= ZipConstants.ZIP64_MAGIC_SHORT) { + throw new Zip64RequiredException(Zip64RequiredException + .TOO_MANY_ENTRIES_MESSAGE); + } + + if (cdLength >= ZipConstants.ZIP64_MAGIC) { + throw new Zip64RequiredException(Zip64RequiredException + .SIZE_OF_CENTRAL_DIRECTORY_TOO_BIG_MESSAGE); + } + + if (cdOffset >= ZipConstants.ZIP64_MAGIC) { + throw new Zip64RequiredException(Zip64RequiredException + .ARCHIVE_TOO_BIG_MESSAGE); + } + } + + + /** + * Throws an exception if the size is unknown for a stored entry + * that is written to a non-seekable output or the entry is too + * big to be written without Zip64 extra but the mode has been set + * to Never. + */ + private void validateSizeInformation(final Zip64Mode effectiveMode) + throws ZipException { + // Size/CRC not required if SeekableByteChannel is used + if (entry.entry.getMethod() == STORED && channel == null) { + if (entry.entry.getSize() == ArchiveEntry.SIZE_UNKNOWN) { + throw new ZipException("Uncompressed size is required for" + + " STORED method when not writing to a" + + " file"); + } + if (entry.entry.getCrc() == ZipArchiveEntry.CRC_UNKNOWN) { + throw new ZipException("CRC checksum is required for STORED" + + " method when not writing to a file"); + } + entry.entry.setCompressedSize(entry.entry.getSize()); + } + + if ((entry.entry.getSize() >= ZipConstants.ZIP64_MAGIC + || entry.entry.getCompressedSize() >= ZipConstants.ZIP64_MAGIC) + && effectiveMode == Zip64Mode.Never) { + throw new Zip64RequiredException(Zip64RequiredException + .getEntryTooBigMessage(entry.entry)); + } + } + + + private int versionNeededToExtract(final int zipMethod, final boolean zip64, final boolean usedDataDescriptor) { + if (zip64) { + return ZipConstants.ZIP64_MIN_VERSION; + } + if (usedDataDescriptor) { + return ZipConstants.DATA_DESCRIPTOR_MIN_VERSION; + } + return versionNeededToExtractMethod(zipMethod); + } + + private int versionNeededToExtractMethod(final int zipMethod) { + return zipMethod == DEFLATED ? ZipConstants.DEFLATE_MIN_VERSION : ZipConstants.INITIAL_VERSION; + } + + /** + * Writes bytes to ZIP entry. + * @param b the byte array to write + * @param offset the start position to write from + * @param length the number of bytes to write + * @throws IOException on error + */ + @Override + public void write(final byte[] b, final int offset, final int length) throws IOException { + if (entry == null) { + throw new IllegalStateException("No current entry"); + } + ZipUtil.checkRequestedFeatures(entry.entry); + final long writtenThisTime = streamCompressor.write(b, offset, length, entry.entry.getMethod()); + count(writtenThisTime); + } + + /** + * Writes the "End of central dir record". + * @throws IOException on error + * @throws Zip64RequiredException if the archive's size exceeds 4 + * GByte or there are more than 65535 entries inside the archive + * and {@link #setUseZip64(Zip64Mode)} is {@link Zip64Mode#Never}. + */ + protected void writeCentralDirectoryEnd() throws IOException { + if(!hasUsedZip64 && isSplitZip) { + ((ZipSplitOutputStream)this.outputStream).prepareToWriteUnsplittableContent(eocdLength); + } + + validateIfZip64IsNeededInEOCD(); + + writeCounted(EOCD_SIG); + + // number of this disk + int numberOfThisDisk = 0; + if(isSplitZip) { + numberOfThisDisk = ((ZipSplitOutputStream)this.outputStream).getCurrentSplitSegmentIndex(); + } + writeCounted(ZipShort.getBytes(numberOfThisDisk)); + + // disk number of the start of central directory + writeCounted(ZipShort.getBytes((int)cdDiskNumberStart)); + + // number of entries + final int numberOfEntries = entries.size(); + + // total number of entries in the central directory on this disk + final int numOfEntriesOnThisDisk = isSplitZip + ? (numberOfCDInDiskData.get(numberOfThisDisk) == null ? 0 : numberOfCDInDiskData.get(numberOfThisDisk)) + : numberOfEntries; + final byte[] numOfEntriesOnThisDiskData = ZipShort + .getBytes(Math.min(numOfEntriesOnThisDisk, ZipConstants.ZIP64_MAGIC_SHORT)); + writeCounted(numOfEntriesOnThisDiskData); + + // number of entries + final byte[] num = ZipShort.getBytes(Math.min(numberOfEntries, + ZipConstants.ZIP64_MAGIC_SHORT)); + writeCounted(num); + + // length and location of CD + writeCounted(ZipLong.getBytes(Math.min(cdLength, ZipConstants.ZIP64_MAGIC))); + writeCounted(ZipLong.getBytes(Math.min(cdOffset, ZipConstants.ZIP64_MAGIC))); + + // ZIP file comment + final ByteBuffer data = this.zipEncoding.encode(comment); + final int dataLen = data.limit() - data.position(); + writeCounted(ZipShort.getBytes(dataLen)); + streamCompressor.writeCounted(data.array(), data.arrayOffset(), dataLen); + } + + private void writeCentralDirectoryInChunks() throws IOException { + final int NUM_PER_WRITE = 1000; + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(70 * NUM_PER_WRITE); + int count = 0; + for (final ZipArchiveEntry ze : entries) { + byteArrayOutputStream.write(createCentralFileHeader(ze)); + if (++count > NUM_PER_WRITE){ + writeCounted(byteArrayOutputStream.toByteArray()); + byteArrayOutputStream.reset(); + count = 0; + } + } + writeCounted(byteArrayOutputStream.toByteArray()); + } + + /** + * Writes the central file header entry. + * @param ze the entry to write + * @throws IOException on error + * @throws Zip64RequiredException if the archive's size exceeds 4 + * GByte and {@link #setUseZip64(Zip64Mode)} is {@link + * Zip64Mode#Never}. + */ + protected void writeCentralFileHeader(final ZipArchiveEntry ze) throws IOException { + final byte[] centralFileHeader = createCentralFileHeader(ze); + writeCounted(centralFileHeader); + } + + /** + * Write bytes to output or random access file. + * @param data the byte array to write + * @throws IOException on error + */ + private void writeCounted(final byte[] data) throws IOException { + streamCompressor.writeCounted(data); + } + + /** + * Writes the data descriptor entry. + * @param ze the entry to write + * @throws IOException on error + */ + protected void writeDataDescriptor(final ZipArchiveEntry ze) throws IOException { + if (!usesDataDescriptor(ze.getMethod(), false)) { + return; + } + writeCounted(DD_SIG); + writeCounted(ZipLong.getBytes(ze.getCrc())); + if (!hasZip64Extra(ze)) { + writeCounted(ZipLong.getBytes(ze.getCompressedSize())); + writeCounted(ZipLong.getBytes(ze.getSize())); + } else { + writeCounted(ZipEightByteInteger.getBytes(ze.getCompressedSize())); + writeCounted(ZipEightByteInteger.getBytes(ze.getSize())); + } + } + + /** + * Writes the local file header entry + * @param ze the entry to write + * @throws IOException on error + */ + protected void writeLocalFileHeader(final ZipArchiveEntry ze) throws IOException { + writeLocalFileHeader(ze, false); + } + + private void writeLocalFileHeader(final ZipArchiveEntry ze, final boolean phased) throws IOException { + final boolean encodable = zipEncoding.canEncode(ze.getName()); + final ByteBuffer name = getName(ze); + + if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER) { + addUnicodeExtraFields(ze, encodable, name); + } + + long localHeaderStart = streamCompressor.getTotalBytesWritten(); + if (isSplitZip) { + // when creating a split zip, the offset should be + // the offset to the corresponding segment disk + final ZipSplitOutputStream splitOutputStream = (ZipSplitOutputStream)this.outputStream; + ze.setDiskNumberStart(splitOutputStream.getCurrentSplitSegmentIndex()); + localHeaderStart = splitOutputStream.getCurrentSplitSegmentBytesWritten(); + } + + final byte[] localHeader = createLocalFileHeader(ze, name, encodable, phased, localHeaderStart); + metaData.put(ze, new EntryMetaData(localHeaderStart, usesDataDescriptor(ze.getMethod(), phased))); + entry.localDataStart = localHeaderStart + LFH_CRC_OFFSET; // At crc offset + writeCounted(localHeader); + entry.dataStart = streamCompressor.getTotalBytesWritten(); + } + + /** + * Write bytes to output or random access file. + * @param data the byte array to write + * @throws IOException on error + */ + protected final void writeOut(final byte[] data) throws IOException { + streamCompressor.writeOut(data, 0, data.length); + } + + /** + * Write bytes to output or random access file. + * @param data the byte array to write + * @param offset the start position to write from + * @param length the number of bytes to write + * @throws IOException on error + */ + protected final void writeOut(final byte[] data, final int offset, final int length) + throws IOException { + streamCompressor.writeOut(data, offset, length); + } + + /** + * Write preamble data. For most of time, this is used to + * make self-extracting zips. + * + * @param preamble data to write + * @throws IOException if an entry already exists + * @since 1.21 + */ + public void writePreamble(final byte[] preamble) throws IOException { + writePreamble(preamble, 0, preamble.length); + } + + /** + * Write preamble data. For most of time, this is used to + * make self-extracting zips. + * + * @param preamble data to write + * @param offset the start offset in the data + * @param length the number of bytes to write + * @throws IOException if an entry already exists + * @since 1.21 + */ + public void writePreamble(final byte[] preamble, final int offset, final int length) throws IOException { + if (entry != null) { + throw new IllegalStateException("Preamble must be written before creating an entry"); + } + this.streamCompressor.writeCounted(preamble, offset, length); + } + + /** + * Writes the "ZIP64 End of central dir record" and + * "ZIP64 End of central dir locator". + * @throws IOException on error + * @since 1.3 + */ + protected void writeZip64CentralDirectory() throws IOException { + if (zip64Mode == Zip64Mode.Never) { + return; + } + + if (!hasUsedZip64 && shouldUseZip64EOCD()) { + // actually "will use" + hasUsedZip64 = true; + } + + if (!hasUsedZip64) { + return; + } + + long offset = streamCompressor.getTotalBytesWritten(); + long diskNumberStart = 0L; + if(isSplitZip) { + // when creating a split zip, the offset of should be + // the offset to the corresponding segment disk + final ZipSplitOutputStream zipSplitOutputStream = (ZipSplitOutputStream)this.outputStream; + offset = zipSplitOutputStream.getCurrentSplitSegmentBytesWritten(); + diskNumberStart = zipSplitOutputStream.getCurrentSplitSegmentIndex(); + } + + + writeOut(ZIP64_EOCD_SIG); + // size of zip64 end of central directory, we don't have any variable length + // as we don't support the extensible data sector, yet + writeOut(ZipEightByteInteger + .getBytes(ZipConstants.SHORT /* version made by */ + + ZipConstants.SHORT /* version needed to extract */ + + ZipConstants.WORD /* disk number */ + + ZipConstants.WORD /* disk with central directory */ + + ZipConstants.DWORD /* number of entries in CD on this disk */ + + ZipConstants.DWORD /* total number of entries */ + + ZipConstants.DWORD /* size of CD */ + + (long) ZipConstants.DWORD /* offset of CD */ + )); + + // version made by and version needed to extract + writeOut(ZipShort.getBytes(ZipConstants.ZIP64_MIN_VERSION)); + writeOut(ZipShort.getBytes(ZipConstants.ZIP64_MIN_VERSION)); + + // number of this disk + int numberOfThisDisk = 0; + if (isSplitZip) { + numberOfThisDisk = ((ZipSplitOutputStream)this.outputStream).getCurrentSplitSegmentIndex(); + } + writeOut(ZipLong.getBytes(numberOfThisDisk)); + + // disk number of the start of central directory + writeOut(ZipLong.getBytes(cdDiskNumberStart)); + + // total number of entries in the central directory on this disk + final int numOfEntriesOnThisDisk = isSplitZip + ? (numberOfCDInDiskData.get(numberOfThisDisk) == null ? 0 : numberOfCDInDiskData.get(numberOfThisDisk)) + : entries.size(); + final byte[] numOfEntriesOnThisDiskData = ZipEightByteInteger.getBytes(numOfEntriesOnThisDisk); + writeOut(numOfEntriesOnThisDiskData); + + // number of entries + final byte[] num = ZipEightByteInteger.getBytes(entries.size()); + writeOut(num); + + // length and location of CD + writeOut(ZipEightByteInteger.getBytes(cdLength)); + writeOut(ZipEightByteInteger.getBytes(cdOffset)); + + // no "zip64 extensible data sector" for now + + if(isSplitZip) { + // based on the zip specification, the End Of Central Directory record and + // the Zip64 End Of Central Directory locator record must be on the same segment + final int zip64EOCDLOCLength = ZipConstants.WORD /* length of ZIP64_EOCD_LOC_SIG */ + + ZipConstants.WORD /* disk number of ZIP64_EOCD_SIG */ + + ZipConstants.DWORD /* offset of ZIP64_EOCD_SIG */ + + ZipConstants.WORD /* total number of disks */; + + final long unsplittableContentSize = zip64EOCDLOCLength + eocdLength; + ((ZipSplitOutputStream)this.outputStream).prepareToWriteUnsplittableContent(unsplittableContentSize); + } + + // and now the "ZIP64 end of central directory locator" + writeOut(ZIP64_EOCD_LOC_SIG); + + // disk number holding the ZIP64 EOCD record + writeOut(ZipLong.getBytes(diskNumberStart)); + // relative offset of ZIP64 EOCD record + writeOut(ZipEightByteInteger.getBytes(offset)); + // total number of disks + if(isSplitZip) { + // the Zip64 End Of Central Directory Locator and the End Of Central Directory must be + // in the same split disk, it means they must be located in the last disk + final int totalNumberOfDisks = ((ZipSplitOutputStream)this.outputStream).getCurrentSplitSegmentIndex() + 1; + writeOut(ZipLong.getBytes(totalNumberOfDisks)); + } else { + writeOut(ONE); + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipConstants.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipConstants.java new file mode 100644 index 0000000000..cb9c972af3 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipConstants.java @@ -0,0 +1,73 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +/** + * Various constants used throughout the package. + * + * @since 1.3 + */ +final class ZipConstants { + /** Masks last eight bits */ + static final int BYTE_MASK = 0xFF; + + /** length of a ZipShort in bytes */ + static final int SHORT = 2; + + /** length of a ZipLong in bytes */ + static final int WORD = 4; + + /** length of a ZipEightByteInteger in bytes */ + static final int DWORD = 8; + + /** Initial ZIP specification version */ + static final int INITIAL_VERSION = 10; + + /** + * ZIP specification version that introduced DEFLATE compression method. + * @since 1.15 + */ + static final int DEFLATE_MIN_VERSION = 20; + + /** ZIP specification version that introduced data descriptor method */ + static final int DATA_DESCRIPTOR_MIN_VERSION = 20; + + /** ZIP specification version that introduced ZIP64 */ + static final int ZIP64_MIN_VERSION = 45; + + /** + * Value stored in two-byte size and similar fields if ZIP64 + * extensions are used. + */ + static final int ZIP64_MAGIC_SHORT = 0xFFFF; + + /** + * Value stored in four-byte size and similar fields if ZIP64 + * extensions are used. + */ + static final long ZIP64_MAGIC = 0xFFFFFFFFL; + + private ZipConstants() { } + +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipEightByteInteger.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipEightByteInteger.java new file mode 100644 index 0000000000..b1cabdde09 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipEightByteInteger.java @@ -0,0 +1,238 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.io.Serializable; +import java.math.BigInteger; + +/** + * Utility class that represents an eight byte integer with conversion + * rules for the little endian byte order of ZIP files. + * @Immutable + * + * @since 1.2 + */ +public final class ZipEightByteInteger implements Serializable { + private static final long serialVersionUID = 1L; + + private static final int BYTE_1 = 1; + private static final int BYTE_1_MASK = 0xFF00; + private static final int BYTE_1_SHIFT = 8; + + private static final int BYTE_2 = 2; + private static final int BYTE_2_MASK = 0xFF0000; + private static final int BYTE_2_SHIFT = 16; + + private static final int BYTE_3 = 3; + private static final long BYTE_3_MASK = 0xFF000000L; + private static final int BYTE_3_SHIFT = 24; + + private static final int BYTE_4 = 4; + private static final long BYTE_4_MASK = 0xFF00000000L; + private static final int BYTE_4_SHIFT = 32; + + private static final int BYTE_5 = 5; + private static final long BYTE_5_MASK = 0xFF0000000000L; + private static final int BYTE_5_SHIFT = 40; + + private static final int BYTE_6 = 6; + private static final long BYTE_6_MASK = 0xFF000000000000L; + private static final int BYTE_6_SHIFT = 48; + + private static final int BYTE_7 = 7; + private static final long BYTE_7_MASK = 0x7F00000000000000L; + private static final int BYTE_7_SHIFT = 56; + + private static final int LEFTMOST_BIT_SHIFT = 63; + private static final byte LEFTMOST_BIT = (byte) 0x80; + + public static final ZipEightByteInteger ZERO = new ZipEightByteInteger(0); + + /** + * Get value as eight bytes in big endian byte order. + * @param value the value to convert + * @return value as eight bytes in big endian byte order + */ + public static byte[] getBytes(final BigInteger value) { + final byte[] result = new byte[8]; + final long val = value.longValue(); + result[0] = (byte) ((val & ZipConstants.BYTE_MASK)); + result[BYTE_1] = (byte) ((val & BYTE_1_MASK) >> BYTE_1_SHIFT); + result[BYTE_2] = (byte) ((val & BYTE_2_MASK) >> BYTE_2_SHIFT); + result[BYTE_3] = (byte) ((val & BYTE_3_MASK) >> BYTE_3_SHIFT); + result[BYTE_4] = (byte) ((val & BYTE_4_MASK) >> BYTE_4_SHIFT); + result[BYTE_5] = (byte) ((val & BYTE_5_MASK) >> BYTE_5_SHIFT); + result[BYTE_6] = (byte) ((val & BYTE_6_MASK) >> BYTE_6_SHIFT); + result[BYTE_7] = (byte) ((val & BYTE_7_MASK) >> BYTE_7_SHIFT); + if (value.testBit(LEFTMOST_BIT_SHIFT)) { + result[BYTE_7] |= LEFTMOST_BIT; + } + return result; + } + + /** + * Get value as eight bytes in big endian byte order. + * @param value the value to convert + * @return value as eight bytes in big endian byte order + */ + public static byte[] getBytes(final long value) { + return getBytes(BigInteger.valueOf(value)); + } + + /** + * Helper method to get the value as a Java long from an eight-byte array + * @param bytes the array of bytes + * @return the corresponding Java long value + */ + public static long getLongValue(final byte[] bytes) { + return getLongValue(bytes, 0); + } + + /** + * Helper method to get the value as a Java long from eight bytes + * starting at given array offset + * @param bytes the array of bytes + * @param offset the offset to start + * @return the corresponding Java long value + */ + public static long getLongValue(final byte[] bytes, final int offset) { + return getValue(bytes, offset).longValue(); + } + + /** + * Helper method to get the value as a Java long from an eight-byte array + * @param bytes the array of bytes + * @return the corresponding Java BigInteger value + */ + public static BigInteger getValue(final byte[] bytes) { + return getValue(bytes, 0); + } + + /** + * Helper method to get the value as a Java BigInteger from eight + * bytes starting at given array offset + * @param bytes the array of bytes + * @param offset the offset to start + * @return the corresponding Java BigInteger value + */ + public static BigInteger getValue(final byte[] bytes, final int offset) { + long value = ((long) bytes[offset + BYTE_7] << BYTE_7_SHIFT) & BYTE_7_MASK; + value += ((long) bytes[offset + BYTE_6] << BYTE_6_SHIFT) & BYTE_6_MASK; + value += ((long) bytes[offset + BYTE_5] << BYTE_5_SHIFT) & BYTE_5_MASK; + value += ((long) bytes[offset + BYTE_4] << BYTE_4_SHIFT) & BYTE_4_MASK; + value += ((long) bytes[offset + BYTE_3] << BYTE_3_SHIFT) & BYTE_3_MASK; + value += ((long) bytes[offset + BYTE_2] << BYTE_2_SHIFT) & BYTE_2_MASK; + value += ((long) bytes[offset + BYTE_1] << BYTE_1_SHIFT) & BYTE_1_MASK; + value += ((long) bytes[offset] & ZipConstants.BYTE_MASK); + final BigInteger val = BigInteger.valueOf(value); + return (bytes[offset + BYTE_7] & LEFTMOST_BIT) == LEFTMOST_BIT + ? val.setBit(LEFTMOST_BIT_SHIFT) : val; + } + + private final BigInteger value; + + /** + * Create instance from a number. + * @param value the BigInteger to store as a ZipEightByteInteger + */ + public ZipEightByteInteger(final BigInteger value) { + this.value = value; + } + + /** + * Create instance from bytes. + * @param bytes the bytes to store as a ZipEightByteInteger + */ + public ZipEightByteInteger (final byte[] bytes) { + this(bytes, 0); + } + + /** + * Create instance from the eight bytes starting at offset. + * @param bytes the bytes to store as a ZipEightByteInteger + * @param offset the offset to start + */ + public ZipEightByteInteger (final byte[] bytes, final int offset) { + value = ZipEightByteInteger.getValue(bytes, offset); + } + + /** + * Create instance from a number. + * @param value the long to store as a ZipEightByteInteger + */ + public ZipEightByteInteger(final long value) { + this(BigInteger.valueOf(value)); + } + + /** + * Override to make two instances with same value equal. + * @param o an object to compare + * @return true if the objects are equal + */ + @Override + public boolean equals(final Object o) { + if (!(o instanceof ZipEightByteInteger)) { + return false; + } + return value.equals(((ZipEightByteInteger) o).getValue()); + } + + /** + * Get value as eight bytes in big endian byte order. + * @return value as eight bytes in big endian order + */ + public byte[] getBytes() { + return ZipEightByteInteger.getBytes(value); + } + + /** + * Get value as Java long. + * @return value as a long + */ + public long getLongValue() { + return value.longValue(); + } + + /** + * Get value as Java BigInteger. + * @return value as a BigInteger + */ + public BigInteger getValue() { + return value; + } + + /** + * Override to make two instances with same value equal. + * @return the hashCode of the value stored in the ZipEightByteInteger + */ + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return "ZipEightByteInteger value: " + value; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipEncoding.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipEncoding.java new file mode 100644 index 0000000000..3a4271fa19 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipEncoding.java @@ -0,0 +1,90 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * An interface for encoders that do a pretty encoding of ZIP + * file names. + * + * <p>There are mostly two implementations, one that uses java.nio + * {@link java.nio.charset.Charset Charset} and one implementation, + * which copes with simple 8 bit charsets, because java-1.4 did not + * support Cp437 in java.nio.</p> + * + * <p>The main reason for defining an own encoding layer comes from + * the problems with {@link java.lang.String#getBytes(String) + * String.getBytes}, which encodes unknown characters as ASCII + * quotation marks ('?'). Quotation marks are per definition an + * invalid file name on some operating systems like Windows, which + * leads to ignored ZIP entries.</p> + * + * <p>All implementations should implement this interface in a + * reentrant way.</p> + */ +public interface ZipEncoding { + /** + * Check, whether the given string may be losslessly encoded using this + * encoding. + * + * @param name A file name or ZIP comment. + * @return Whether the given name may be encoded with out any losses. + */ + boolean canEncode(String name); + + /** + * @param data The byte values to decode. + * @return The decoded string. + * @throws IOException on error + */ + String decode(byte [] data) throws IOException; + + /** + * Encode a file name or a comment to a byte array suitable for + * storing it to a serialized zip entry. + * + * <p>Examples for CP 437 (in pseudo-notation, right hand side is + * C-style notation):</p> + * <pre> + * encode("\u20AC_for_Dollar.txt") = "%U20AC_for_Dollar.txt" + * encode("\u00D6lf\u00E4sser.txt") = "\231lf\204sser.txt" + * </pre> + * + * @param name A file name or ZIP comment. + * @return A byte buffer with a backing array containing the + * encoded name. Unmappable characters or malformed + * character sequences are mapped to a sequence of utf-16 + * words encoded in the format {@code %Uxxxx}. It is + * assumed, that the byte buffer is positioned at the + * beginning of the encoded result, the byte buffer has a + * backing array and the limit of the byte buffer points + * to the end of the encoded result. + * @throws IOException on error + */ + ByteBuffer encode(String name) throws IOException; +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipEncodingHelper.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipEncodingHelper.java new file mode 100644 index 0000000000..7fbd472f76 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipEncodingHelper.java @@ -0,0 +1,100 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; + +/** + * Static helper functions for robustly encoding file names in zip files. + */ +public abstract class ZipEncodingHelper { + + + /** + * name of the encoding UTF-8 + */ + static final String UTF8 = "UTF8"; + + /** + * the encoding UTF-8 + */ + static final ZipEncoding UTF8_ZIP_ENCODING = getZipEncoding(UTF8); + + /** + * Instantiates a zip encoding. An NIO based character set encoder/decoder will be returned. + * As a special case, if the character set is UTF-8, the nio encoder will be configured replace malformed and + * unmappable characters with '?'. This matches existing behavior from the older fallback encoder. + * <p> + * If the requested character set cannot be found, the platform default will + * be used instead. + * </p> + * @param name The name of the zip encoding. Specify {@code null} for + * the platform's default encoding. + * @return A zip encoding for the given encoding name. + */ + public static ZipEncoding getZipEncoding(final String name) { + Charset cs = Charset.defaultCharset(); + if (name != null) { + try { + cs = Charset.forName(name); + } catch (final UnsupportedCharsetException e) { // NOSONAR we use the default encoding instead + } + } + final boolean useReplacement = isUTF8(cs.name()); + return new NioZipEncoding(cs, useReplacement); + } + + static ByteBuffer growBufferBy(final ByteBuffer buffer, final int increment) { + buffer.limit(buffer.position()); + buffer.rewind(); + + final ByteBuffer on = ByteBuffer.allocate(buffer.capacity() + increment); + + on.put(buffer); + return on; + } + + /** + * Returns whether a given encoding is UTF-8. If the given name is null, then check the platform's default encoding. + * + * @param charsetName If the given name is null, then check the platform's default encoding. + */ + static boolean isUTF8(final String charsetName) { + final String actual = charsetName != null ? charsetName : Charset.defaultCharset().name(); + if (UTF_8.name().equalsIgnoreCase(actual)) { + return true; + } + for (String alias: UTF_8.aliases()) { + if (alias.equalsIgnoreCase(actual)) { + return true; + } + } + return false; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipExtraField.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipExtraField.java new file mode 100644 index 0000000000..176e6cdecd --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipExtraField.java @@ -0,0 +1,102 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.util.zip.ZipException; + +/** + * General format of extra field data. + * + * <p>Extra fields usually appear twice per file, once in the local + * file data and once in the central directory. Usually they are the + * same, but they don't have to be. {@link + * java.util.zip.ZipOutputStream java.util.zip.ZipOutputStream} will + * only use the local file data in both places.</p> + * + */ +public interface ZipExtraField { + /** + * Size of an extra field field header (id + length). + * @since 1.14 + */ + int EXTRAFIELD_HEADER_SIZE = 4; + + /** + * The actual data to put into central directory - without Header-ID or + * length specifier. + * @return the data + */ + byte[] getCentralDirectoryData(); + + /** + * Length of the extra field in the central directory - without + * Header-ID or length specifier. + * @return the length of the field in the central directory + */ + ZipShort getCentralDirectoryLength(); + + /** + * The Header-ID. + * + * @return The HeaderId value + */ + ZipShort getHeaderId(); + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * @return the data + */ + byte[] getLocalFileDataData(); + + /** + * Length of the extra field in the local file data - without + * Header-ID or length specifier. + * @return the length of the field in the local file data + */ + ZipShort getLocalFileDataLength(); + + /** + * Populate data from this array as if it was in central directory data. + * + * @param buffer the buffer to read data from + * @param offset offset into buffer to read data + * @param length the length of data + * @throws ZipException on error + */ + void parseFromCentralDirectoryData(byte[] buffer, int offset, int length) + throws ZipException; + + /** + * Populate data from this array as if it was in local file data. + * + * @param buffer the buffer to read data from + * @param offset offset into buffer to read data + * @param length the length of data + * @throws ZipException on error + */ + void parseFromLocalFileData(byte[] buffer, int offset, int length) + throws ZipException; +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipFile.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipFile.java new file mode 100644 index 0000000000..69c2a03b4a --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipFile.java @@ -0,0 +1,1418 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import org.readium.r2.shared.util.zip.compress.archivers.EntryStreamOffsets; +import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel; +import org.readium.r2.shared.util.zip.compress.utils.BoundedArchiveInputStream; +import org.readium.r2.shared.util.zip.compress.utils.BoundedSeekableByteChannelInputStream; +import org.readium.r2.shared.util.zip.compress.utils.CountingInputStream; +import org.readium.r2.shared.util.zip.compress.utils.IOUtils; +import org.readium.r2.shared.util.zip.compress.utils.InputStreamStatistics; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.zip.Inflater; +import java.util.zip.ZipException; + +/** + * Replacement for {@code java.util.ZipFile}. + * + * <p>This class adds support for file name encodings other than UTF-8 + * (which is required to work on ZIP files created by native ZIP tools + * and is able to skip a preamble like the one found in self + * extracting archives. Furthermore it returns instances of + * {@code org.readium.r2.shared.util.archive.remote.zip.ZipArchiveEntry} + * instead of {@code java.util.zip.ZipEntry}.</p> + * + * <p>It doesn't extend {@code java.util.zip.ZipFile} as it would + * have to reimplement all methods anyway. Like + * {@code java.util.ZipFile}, it uses SeekableByteChannel under the + * covers and supports compressed and uncompressed entries. As of + * Apache Commons Compress 1.3 it also transparently supports Zip64 + * extensions and thus individual entries and archives larger than 4 + * GB or with more than 65536 entries.</p> + * + * <p>The method signatures mimic the ones of + * {@code java.util.zip.ZipFile}, with a couple of exceptions: + * + * <ul> + * <li>There is no getName method.</li> + * <li>entries has been renamed to getEntries.</li> + * <li>getEntries and getEntry return + * {@code org.readium.r2.shared.util.archive.remote.zip.ZipArchiveEntry} + * instances.</li> + * <li>close is allowed to throw IOException.</li> + * </ul> + */ +public class ZipFile implements Closeable { + + /** + * Extends ZipArchiveEntry to store the offset within the archive. + */ + private static class Entry extends ZipArchiveEntry { + + Entry() { + } + + @Override + public boolean equals(final Object other) { + if (super.equals(other)) { + // super.equals would return false if other were not an Entry + final Entry otherEntry = (Entry) other; + return getLocalHeaderOffset() + == otherEntry.getLocalHeaderOffset() + && super.getDataOffset() + == otherEntry.getDataOffset() + && super.getDiskNumberStart() + == otherEntry.getDiskNumberStart(); + } + return false; + } + + @Override + public int hashCode() { + return 3 * super.hashCode() + + (int) getLocalHeaderOffset()+(int)(getLocalHeaderOffset()>>32); + } + } + private static final class NameAndComment { + private final byte[] name; + private final byte[] comment; + private NameAndComment(final byte[] name, final byte[] comment) { + this.name = name; + this.comment = comment; + } + } + private static class StoredStatisticsStream extends CountingInputStream implements InputStreamStatistics { + StoredStatisticsStream(final InputStream in) { + super(in); + } + + @Override + public long getCompressedCount() { + return super.getBytesRead(); + } + + @Override + public long getUncompressedCount() { + return getCompressedCount(); + } + } + private static final int HASH_SIZE = 509; + static final int NIBLET_MASK = 0x0f; + static final int BYTE_SHIFT = 8; + private static final int POS_0 = 0; + + private static final int POS_1 = 1; + + private static final int POS_2 = 2; + + private static final int POS_3 = 3; + + private static final byte[] ONE_ZERO_BYTE = new byte[1]; + + /** + * Length of a "central directory" entry structure without file + * name, extra fields or comment. + */ + private static final int CFH_LEN = + /* version made by */ ZipConstants.SHORT + /* version needed to extract */ + ZipConstants.SHORT + /* general purpose bit flag */ + ZipConstants.SHORT + /* compression method */ + ZipConstants.SHORT + /* last mod file time */ + ZipConstants.SHORT + /* last mod file date */ + ZipConstants.SHORT + /* crc-32 */ + ZipConstants.WORD + /* compressed size */ + ZipConstants.WORD + /* uncompressed size */ + ZipConstants.WORD + /* file name length */ + ZipConstants. SHORT + /* extra field length */ + ZipConstants.SHORT + /* file comment length */ + ZipConstants.SHORT + /* disk number start */ + ZipConstants.SHORT + /* internal file attributes */ + ZipConstants.SHORT + /* external file attributes */ + ZipConstants.WORD + /* relative offset of local header */ + ZipConstants.WORD; + + private static final long CFH_SIG = + ZipLong.getValue(ZipArchiveOutputStream.CFH_SIG); + + /** + * Length of the "End of central directory record" - which is + * supposed to be the last structure of the archive - without file + * comment. + */ + static final int MIN_EOCD_SIZE = + /* end of central dir signature */ ZipConstants.WORD + /* number of this disk */ + ZipConstants.SHORT + /* number of the disk with the */ + /* start of the central directory */ + ZipConstants.SHORT + /* total number of entries in */ + /* the central dir on this disk */ + ZipConstants.SHORT + /* total number of entries in */ + /* the central dir */ + ZipConstants.SHORT + /* size of the central directory */ + ZipConstants.WORD + /* offset of start of central */ + /* directory with respect to */ + /* the starting disk number */ + ZipConstants.WORD + /* zipfile comment length */ + ZipConstants.SHORT; + + /** + * Maximum length of the "End of central directory record" with a + * file comment. + */ + private static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE + /* maximum length of zipfile comment */ + ZipConstants.ZIP64_MAGIC_SHORT; + + /** + * Offset of the field that holds the location of the length of + * the central directory inside the "End of central directory + * record" relative to the start of the "End of central directory + * record". + */ + private static final int CFD_LENGTH_OFFSET = + /* end of central dir signature */ ZipConstants.WORD + /* number of this disk */ + ZipConstants.SHORT + /* number of the disk with the */ + /* start of the central directory */ + ZipConstants.SHORT + /* total number of entries in */ + /* the central dir on this disk */ + ZipConstants.SHORT + /* total number of entries in */ + /* the central dir */ + ZipConstants.SHORT; + + /** + * Offset of the field that holds the disk number of the first + * central directory entry inside the "End of central directory + * record" relative to the start of the "End of central directory + * record". + */ + private static final int CFD_DISK_OFFSET = + /* end of central dir signature */ ZipConstants.WORD + /* number of this disk */ + ZipConstants.SHORT; + /** + * Offset of the field that holds the location of the first + * central directory entry inside the "End of central directory + * record" relative to the "number of the disk with the start + * of the central directory". + */ + private static final int CFD_LOCATOR_RELATIVE_OFFSET = + /* total number of entries in */ + /* the central dir on this disk */ + ZipConstants.SHORT + /* total number of entries in */ + /* the central dir */ + ZipConstants.SHORT + /* size of the central directory */ + ZipConstants.WORD; + /** + * Length of the "Zip64 end of central directory locator" - which + * should be right in front of the "end of central directory + * record" if one is present at all. + */ + private static final int ZIP64_EOCDL_LENGTH = + /* zip64 end of central dir locator sig */ ZipConstants.WORD + /* number of the disk with the start */ + /* start of the zip64 end of */ + /* central directory */ + ZipConstants.WORD + /* relative offset of the zip64 */ + /* end of central directory record */ + ZipConstants.DWORD + /* total number of disks */ + ZipConstants.WORD; + /** + * Offset of the field that holds the location of the "Zip64 end + * of central directory record" inside the "Zip64 end of central + * directory locator" relative to the start of the "Zip64 end of + * central directory locator". + */ + private static final int ZIP64_EOCDL_LOCATOR_OFFSET = + /* zip64 end of central dir locator sig */ ZipConstants.WORD + /* number of the disk with the start */ + /* start of the zip64 end of */ + /* central directory */ + ZipConstants.WORD; + /** + * Offset of the field that holds the location of the first + * central directory entry inside the "Zip64 end of central + * directory record" relative to the start of the "Zip64 end of + * central directory record". + */ + private static final int ZIP64_EOCD_CFD_LOCATOR_OFFSET = + /* zip64 end of central dir */ + /* signature */ ZipConstants.WORD + /* size of zip64 end of central */ + /* directory record */ + ZipConstants.DWORD + /* version made by */ + ZipConstants.SHORT + /* version needed to extract */ + ZipConstants.SHORT + /* number of this disk */ + ZipConstants.WORD + /* number of the disk with the */ + /* start of the central directory */ + ZipConstants.WORD + /* total number of entries in the */ + /* central directory on this disk */ + ZipConstants.DWORD + /* total number of entries in the */ + /* central directory */ + ZipConstants.DWORD + /* size of the central directory */ + ZipConstants.DWORD; + /** + * Offset of the field that holds the disk number of the first + * central directory entry inside the "Zip64 end of central + * directory record" relative to the start of the "Zip64 end of + * central directory record". + */ + private static final int ZIP64_EOCD_CFD_DISK_OFFSET = + /* zip64 end of central dir */ + /* signature */ ZipConstants.WORD + /* size of zip64 end of central */ + /* directory record */ + ZipConstants.DWORD + /* version made by */ + ZipConstants.SHORT + /* version needed to extract */ + ZipConstants.SHORT + /* number of this disk */ + ZipConstants.WORD; + /** + * Offset of the field that holds the location of the first + * central directory entry inside the "Zip64 end of central + * directory record" relative to the "number of the disk + * with the start of the central directory". + */ + private static final int ZIP64_EOCD_CFD_LOCATOR_RELATIVE_OFFSET = + /* total number of entries in the */ + /* central directory on this disk */ ZipConstants.DWORD + /* total number of entries in the */ + /* central directory */ + ZipConstants.DWORD + /* size of the central directory */ + ZipConstants.DWORD; + /** + * Number of bytes in local file header up to the "length of + * file name" entry. + */ + private static final long LFH_OFFSET_FOR_FILENAME_LENGTH = + /* local file header signature */ ZipConstants.WORD + /* version needed to extract */ + ZipConstants.SHORT + /* general purpose bit flag */ + ZipConstants.SHORT + /* compression method */ + ZipConstants.SHORT + /* last mod file time */ + ZipConstants.SHORT + /* last mod file date */ + ZipConstants.SHORT + /* crc-32 */ + ZipConstants.WORD + /* compressed size */ + ZipConstants.WORD + /* uncompressed size */ + (long) ZipConstants.WORD; + + /** + * close a zipfile quietly; throw no io fault, do nothing + * on a null parameter + * @param zipfile file to close, can be null + */ + public static void closeQuietly(final ZipFile zipfile) { + IOUtils.closeQuietly(zipfile); + } + /** + * List of entries in the order they appear inside the central + * directory. + */ + private final List<ZipArchiveEntry> entries = + new LinkedList<>(); + /** + * Maps String to list of ZipArchiveEntrys, name -> actual entries. + */ + private final Map<String, LinkedList<ZipArchiveEntry>> nameMap = + new HashMap<>(HASH_SIZE); + + /** + * The encoding to use for file names and the file comment. + * + * <p>For a list of possible values see <a + * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>. + * Defaults to UTF-8.</p> + */ + private final String encoding; + + /** + * The ZIP encoding to use for file names and the file comment. + */ + private final ZipEncoding zipEncoding; + + /** + * File name of actual source. + */ + private final String archiveName; + + /** + * The actual data source. + */ + private final SeekableByteChannel archive; + + /** + * Whether to look for and use Unicode extra fields. + */ + private final boolean useUnicodeExtraFields; + + /** + * Whether the file is closed. + */ + private volatile boolean closed = true; + + /** + * Whether the ZIP archive is a split ZIP archive + */ + private final boolean isSplitZipArchive; + + // cached buffers - must only be used locally in the class (COMPRESS-172 - reduce garbage collection) + private final byte[] dwordBuf = new byte[ZipConstants.DWORD]; + + private final byte[] wordBuf = new byte[ZipConstants.WORD]; + + private final byte[] cfhBuf = new byte[CFH_LEN]; + + private final byte[] shortBuf = new byte[ZipConstants.SHORT]; + + private final ByteBuffer dwordBbuf = ByteBuffer.wrap(dwordBuf); + + private final ByteBuffer wordBbuf = ByteBuffer.wrap(wordBuf); + + private final ByteBuffer cfhBbuf = ByteBuffer.wrap(cfhBuf); + + private final ByteBuffer shortBbuf = ByteBuffer.wrap(shortBuf); + + private long centralDirectoryStartDiskNumber, centralDirectoryStartRelativeOffset; + + private long centralDirectoryStartOffset; + + private long firstLocalFileHeaderOffset; + + /** + * Compares two ZipArchiveEntries based on their offset within the archive. + * + * <p>Won't return any meaningful results if one of the entries + * isn't part of the archive at all.</p> + * + * @since 1.1 + */ + private static final Comparator<ZipArchiveEntry> offsetComparator = + (e1, e2) -> { + final int diskNumberStartComparison = + Long.compare(e1.getDiskNumberStart(), e2.getDiskNumberStart()); + if (diskNumberStartComparison != 0) { + return diskNumberStartComparison; + } else { + return Long.compare(e1.getLocalHeaderOffset(), e2.getLocalHeaderOffset()); + } + }; + + /** + * Opens the given channel for reading, assuming "UTF8" for file names. + * + * @param channel the archive. + * + * @throws IOException if an error occurs while reading the file. + * @since 1.13 + */ + public ZipFile(final SeekableByteChannel channel) + throws IOException { + this(channel, "unknown archive", ZipEncodingHelper.UTF8, true); + } + + /** + * Opens the given channel for reading, assuming the specified + * encoding for file names. + * + * @param channel the archive. + * @param encoding the encoding to use for file names, use null + * for the platform's default encoding + * + * @throws IOException if an error occurs while reading the file. + * @since 1.13 + */ + public ZipFile(final SeekableByteChannel channel, final String encoding) + throws IOException { + this(channel, "unknown archive", encoding, true); + } + + public ZipFile(final SeekableByteChannel channel, final boolean ignoreLocalFileHeader) + throws IOException { + this(channel, "unknown archive", ZipEncodingHelper.UTF8, true, false, ignoreLocalFileHeader); + } + + /** + * Opens the given channel for reading, assuming the specified + * encoding for file names. + * + * @param channel the archive. + * @param archiveName name of the archive, used for error messages only. + * @param encoding the encoding to use for file names, use null + * for the platform's default encoding + * @param useUnicodeExtraFields whether to use InfoZIP Unicode + * Extra Fields (if present) to set the file names. + * + * @throws IOException if an error occurs while reading the file. + * @since 1.13 + */ + public ZipFile(final SeekableByteChannel channel, final String archiveName, + final String encoding, final boolean useUnicodeExtraFields) + throws IOException { + this(channel, archiveName, encoding, useUnicodeExtraFields, false, false); + } + + /** + * Opens the given channel for reading, assuming the specified + * encoding for file names. + * + * <p>By default the central directory record and all local file headers of the archive will be read immediately + * which may take a considerable amount of time when the archive is big. The {@code ignoreLocalFileHeader} parameter + * can be set to {@code true} which restricts parsing to the central directory. Unfortunately the local file header + * may contain information not present inside of the central directory which will not be available when the argument + * is set to {@code true}. This includes the content of the Unicode extra field, so setting {@code + * ignoreLocalFileHeader} to {@code true} means {@code useUnicodeExtraFields} will be ignored effectively.</p> + * + * @param channel the archive. + * @param archiveName name of the archive, used for error messages only. + * @param encoding the encoding to use for file names, use null + * for the platform's default encoding + * @param useUnicodeExtraFields whether to use InfoZIP Unicode + * Extra Fields (if present) to set the file names. + * @param ignoreLocalFileHeader whether to ignore information + * stored inside the local file header (see the notes in this method's javadoc) + * + * @throws IOException if an error occurs while reading the file. + * @since 1.19 + */ + public ZipFile(final SeekableByteChannel channel, final String archiveName, + final String encoding, final boolean useUnicodeExtraFields, + final boolean ignoreLocalFileHeader) + throws IOException { + this(channel, archiveName, encoding, useUnicodeExtraFields, false, ignoreLocalFileHeader); + } + + private ZipFile(final SeekableByteChannel channel, final String archiveName, + final String encoding, final boolean useUnicodeExtraFields, + final boolean closeOnError, final boolean ignoreLocalFileHeader) + throws IOException { + isSplitZipArchive = (channel instanceof ZipSplitReadOnlySeekableByteChannel); + + this.archiveName = archiveName; + this.encoding = encoding; + this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); + this.useUnicodeExtraFields = useUnicodeExtraFields; + archive = channel; + boolean success = false; + try { + final Map<ZipArchiveEntry, NameAndComment> entriesWithoutUTF8Flag = + populateFromCentralDirectory(); + if (!ignoreLocalFileHeader) { + resolveLocalFileHeaderData(entriesWithoutUTF8Flag); + } + fillNameMap(); + success = true; + } catch (final IOException e) { + throw new IOException("Error on ZipFile " + archiveName, e); + } finally { + closed = !success; + if (!success && closeOnError) { + IOUtils.closeQuietly(archive); + } + } + } + + /** + * Whether this class is able to read the given entry. + * + * <p>May return false if it is set up to use encryption or a + * compression method that hasn't been implemented yet.</p> + * @since 1.1 + * @param ze the entry + * @return whether this class is able to read the given entry. + */ + public boolean canReadEntryData(final ZipArchiveEntry ze) { + return ZipUtil.canHandleEntryData(ze); + } + + /** + * Closes the archive. + * @throws IOException if an error occurs closing the archive. + */ + @Override + public void close() throws IOException { + // this flag is only written here and read in finalize() which + // can never be run in parallel. + // no synchronization needed. + closed = true; + + archive.close(); + } + + /** + * Transfer selected entries from this zipfile to a given #ZipArchiveOutputStream. + * Compression and all other attributes will be as in this file. + * <p>This method transfers entries based on the central directory of the ZIP file.</p> + * + * @param target The zipArchiveOutputStream to write the entries to + * @param predicate A predicate that selects which entries to write + * @throws IOException on error + */ + public void copyRawEntries(final ZipArchiveOutputStream target, final ZipArchiveEntryPredicate predicate) + throws IOException { + final Enumeration<ZipArchiveEntry> src = getEntriesInPhysicalOrder(); + while (src.hasMoreElements()) { + final ZipArchiveEntry entry = src.nextElement(); + if (predicate.test( entry)) { + target.addRawArchiveEntry(entry, getRawInputStream(entry)); + } + } + } + + /** + * Creates new BoundedInputStream, according to implementation of + * underlying archive channel. + */ + private BoundedArchiveInputStream createBoundedInputStream(final long start, final long remaining) { + if (start < 0 || remaining < 0 || start + remaining < start) { + throw new IllegalArgumentException("Corrupted archive, stream boundaries" + + " are out of range"); + } + return new BoundedSeekableByteChannelInputStream(start, remaining, archive); + } + + private void fillNameMap() { + for (ZipArchiveEntry ze: entries) { + // entries is filled in populateFromCentralDirectory and + // never modified + final String name = ze.getName(); + if (!nameMap.containsKey(name)) { + nameMap.put(name, new LinkedList<>()); + } + final LinkedList<ZipArchiveEntry> entriesOfThatName = + Objects.requireNonNull(nameMap.get(name)); + entriesOfThatName.addLast(ze); + } + } + + /** + * Ensures that the close method of this zipfile is called when + * there are no more references to it. + * @see #close() + */ + @Override + protected void finalize() throws Throwable { + try { + if (!closed) { + System.err.println("Cleaning up unclosed ZipFile for archive " + + archiveName); + close(); + } + } finally { + super.finalize(); + } + } + + /** + * Gets an InputStream for reading the content before the first local file header. + * + * @return null if there is no content before the first local file header. + * Otherwise returns a stream to read the content before the first local file header. + * @since 1.23 + */ + public InputStream getContentBeforeFirstLocalFileHeader() { + return firstLocalFileHeaderOffset == 0 + ? null : createBoundedInputStream(0, firstLocalFileHeaderOffset); + } + + private long getDataOffset(final ZipArchiveEntry ze) throws IOException { + final long s = ze.getDataOffset(); + if (s == EntryStreamOffsets.OFFSET_UNKNOWN) { + setDataOffset(ze); + return ze.getDataOffset(); + } + return s; + } + + /** + * Gets the encoding to use for file names and the file comment. + * + * @return null if using the platform's default character encoding. + */ + public String getEncoding() { + return encoding; + } + + /** + * Gets all entries. + * + * <p>Entries will be returned in the same order they appear + * within the archive's central directory.</p> + * + * @return all entries as {@link ZipArchiveEntry} instances + */ + public Enumeration<ZipArchiveEntry> getEntries() { + return Collections.enumeration(entries); + } + + /** + * Gets all named entries in the same order they appear within + * the archive's central directory. + * + * @param name name of the entry. + * @return the Iterable<ZipArchiveEntry> corresponding to the + * given name + * @since 1.6 + */ + public Iterable<ZipArchiveEntry> getEntries(final String name) { + final List<ZipArchiveEntry> entriesOfThatName = nameMap.get(name); + return entriesOfThatName != null ? entriesOfThatName + : Collections.emptyList(); + } + + /** + * Gets all entries in physical order. + * + * <p>Entries will be returned in the same order their contents + * appear within the archive.</p> + * + * @return all entries as {@link ZipArchiveEntry} instances + * + * @since 1.1 + */ + public Enumeration<ZipArchiveEntry> getEntriesInPhysicalOrder() { + final ZipArchiveEntry[] allEntries = entries.toArray(ZipArchiveEntry.EMPTY_ARRAY); + Arrays.sort(allEntries, offsetComparator); + return Collections.enumeration(Arrays.asList(allEntries)); + } + + /** + * Gets all named entries in the same order their contents + * appear within the archive. + * + * @param name name of the entry. + * @return the Iterable<ZipArchiveEntry> corresponding to the + * given name + * @since 1.6 + */ + public Iterable<ZipArchiveEntry> getEntriesInPhysicalOrder(final String name) { + ZipArchiveEntry[] entriesOfThatName = ZipArchiveEntry.EMPTY_ARRAY; + final LinkedList<ZipArchiveEntry> linkedList = nameMap.get(name); + if (linkedList != null) { + entriesOfThatName = linkedList.toArray(entriesOfThatName); + Arrays.sort(entriesOfThatName, offsetComparator); + } + return Arrays.asList(entriesOfThatName); + } + + /** + * Gets a named entry or {@code null} if no entry by + * that name exists. + * + * <p>If multiple entries with the same name exist the first entry + * in the archive's central directory by that name is + * returned.</p> + * + * @param name name of the entry. + * @return the ZipArchiveEntry corresponding to the given name - or + * {@code null} if not present. + */ + public ZipArchiveEntry getEntry(final String name) { + final LinkedList<ZipArchiveEntry> entriesOfThatName = nameMap.get(name); + return entriesOfThatName != null ? entriesOfThatName.getFirst() : null; + } + + /** + * Gets the offset of the first local file header in the file. + * + * @return the length of the content before the first local file header + * @since 1.23 + */ + public long getFirstLocalFileHeaderOffset() { + return firstLocalFileHeaderOffset; + } + + /** + * Gets an InputStream for reading the contents of the given entry. + * + * @param ze the entry to get the stream for. + * @return a stream to read the entry from. The returned stream + * implements {@link InputStreamStatistics}. + * @throws IOException if unable to create an input stream from the zipentry + */ + public InputStream getInputStream(final ZipArchiveEntry ze) + throws IOException { + if (!(ze instanceof Entry)) { + return null; + } + // cast validity is checked just above + ZipUtil.checkRequestedFeatures(ze); + + // doesn't get closed if the method is not supported - which + // should never happen because of the checkRequestedFeatures + // call above + final InputStream is = new BufferedInputStream(getRawInputStream(ze)); //NOSONAR + switch (ZipMethod.getMethodByCode(ze.getMethod())) { + case STORED: + return new StoredStatisticsStream(is); + case DEFLATED: + final Inflater inflater = new Inflater(true); + // Inflater with nowrap=true has this odd contract for a zero padding + // byte following the data stream; this used to be zlib's requirement + // and has been fixed a long time ago, but the contract persists so + // we comply. + // https://docs.oracle.com/javase/7/docs/api/java/util/zip/Inflater.html#Inflater(boolean) + return new InflaterInputStreamWithStatistics(new SequenceInputStream(is, new ByteArrayInputStream(ONE_ZERO_BYTE)), + inflater) { + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + inflater.end(); + } + } + }; + case BZIP2: + case ENHANCED_DEFLATED: + case AES_ENCRYPTED: + case EXPANDING_LEVEL_1: + case EXPANDING_LEVEL_2: + case EXPANDING_LEVEL_3: + case EXPANDING_LEVEL_4: + case IMPLODING: + case JPEG: + case LZMA: + case PKWARE_IMPLODING: + case PPMD: + case TOKENIZATION: + case UNKNOWN: + case UNSHRINKING: + case WAVPACK: + case XZ: + default: + throw new UnsupportedZipFeatureException(ZipMethod.getMethodByCode(ze.getMethod()), ze); + } + } + + /** + * Gets the raw stream of the archive entry (compressed form). + * + * <p>This method does not relate to how/if we understand the payload in the + * stream, since we really only intend to move it on to somewhere else.</p> + * + * <p>Since version 1.22, this method will make an attempt to read the entry's data + * stream offset, even if the {@code ignoreLocalFileHeader} parameter was {@code true} + * in the constructor. An IOException can also be thrown from the body of the method + * if this lookup fails for some reason.</p> + * + * @param ze The entry to get the stream for + * @return The raw input stream containing (possibly) compressed data. + * @since 1.11 + * @throws IOException if there is a problem reading data offset (added in version 1.22). + */ + public InputStream getRawInputStream(final ZipArchiveEntry ze) throws IOException { + if (!(ze instanceof Entry)) { + return null; + } + + final long start = getDataOffset(ze); + if (start == EntryStreamOffsets.OFFSET_UNKNOWN) { + return null; + } + return createBoundedInputStream(start, ze.getCompressedSize()); + } + + // Readium-added + /** + * <p></p>Gets the raw stream of the stored archive entry starting from fromIndex.</p> + * + * <p>This method does not relate to how/if we understand the payload in the + * stream, since we really only intend to move it on to somewhere else.</p> + * + * @param ze The stored entry to get the stream for + * @param fromIndex The index in the entry that the stream will start from + * @return The raw input stream containing data. + * @throws IOException if there is a problem reading data offset. + */ + public InputStream getRawInputStream(final ZipArchiveEntry ze, final long fromIndex) throws IOException { + if (!(ze instanceof Entry)) { + return null; + } + + final long start = getDataOffset(ze); + if (start == EntryStreamOffsets.OFFSET_UNKNOWN) { + return null; + } + + if (ZipMethod.getMethodByCode(ze.getMethod()) != ZipMethod.STORED) { + throw new IllegalArgumentException("Cannot begin a stream at a specific index in compressed entries."); + } + + if (fromIndex >= ze.getSize()) { + throw new IllegalArgumentException("fromIndex out of bounds."); + } + + return createBoundedInputStream(start + fromIndex, ze.getSize() - fromIndex); + } + + /** + * Gets the entry's content as a String if isUnixSymlink() + * returns true for it, otherwise returns null. + * <p>This method assumes the symbolic link's file name uses the + * same encoding that as been specified for this ZipFile.</p> + * + * @param entry ZipArchiveEntry object that represents the symbolic link + * @return entry's content as a String + * @throws IOException problem with content's input stream + * @since 1.5 + */ + public String getUnixSymlink(final ZipArchiveEntry entry) throws IOException { + if (entry != null && entry.isUnixSymlink()) { + try (InputStream in = getInputStream(entry)) { + return zipEncoding.decode(IOUtils.toByteArray(in)); + } + } + return null; + } + + /** + * Reads the central directory of the given archive and populates + * the internal tables with ZipArchiveEntry instances. + * + * <p>The ZipArchiveEntrys will know all data that can be obtained from + * the central directory alone, but not the data that requires the + * local file header or additional data to be read.</p> + * + * @return a map of zipentries that didn't have the language + * encoding flag set when read. + */ + private Map<ZipArchiveEntry, NameAndComment> populateFromCentralDirectory() + throws IOException { + final HashMap<ZipArchiveEntry, NameAndComment> noUTF8Flag = + new HashMap<>(); + + positionAtCentralDirectory(); + centralDirectoryStartOffset = archive.position(); + + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + long sig = ZipLong.getValue(wordBuf); + + if (sig != CFH_SIG && startsWithLocalFileHeader()) { + throw new IOException("Central directory is empty, can't expand" + + " corrupt archive."); + } + + while (sig == CFH_SIG) { + readCentralDirectoryEntry(noUTF8Flag); + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + sig = ZipLong.getValue(wordBuf); + } + return noUTF8Flag; + } + + /** + * Searches for either the "Zip64 end of central directory + * locator" or the "End of central dir record", parses + * it and positions the stream at the first central directory + * record. + */ + private void positionAtCentralDirectory() + throws IOException { + positionAtEndOfCentralDirectoryRecord(); + boolean found = false; + final boolean searchedForZip64EOCD = + archive.position() > ZIP64_EOCDL_LENGTH; + if (searchedForZip64EOCD) { + archive.position(archive.position() - ZIP64_EOCDL_LENGTH); + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + found = Arrays.equals(ZipArchiveOutputStream.ZIP64_EOCD_LOC_SIG, + wordBuf); + } + if (!found) { + // not a ZIP64 archive + if (searchedForZip64EOCD) { + skipBytes(ZIP64_EOCDL_LENGTH - ZipConstants.WORD); + } + positionAtCentralDirectory32(); + } else { + positionAtCentralDirectory64(); + } + } + + /** + * Parses the "End of central dir record" and positions + * the stream at the first central directory record. + * + * Expects stream to be positioned at the beginning of the + * "End of central dir record". + */ + private void positionAtCentralDirectory32() + throws IOException { + final long endOfCentralDirectoryRecordOffset = archive.position(); + if (isSplitZipArchive) { + skipBytes(CFD_DISK_OFFSET); + shortBbuf.rewind(); + IOUtils.readFully(archive, shortBbuf); + centralDirectoryStartDiskNumber = ZipShort.getValue(shortBuf); + + skipBytes(CFD_LOCATOR_RELATIVE_OFFSET); + + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + centralDirectoryStartRelativeOffset = ZipLong.getValue(wordBuf); + ((ZipSplitReadOnlySeekableByteChannel) archive) + .position(centralDirectoryStartDiskNumber, centralDirectoryStartRelativeOffset); + } else { + skipBytes(CFD_LENGTH_OFFSET); + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + final long centralDirectoryLength = ZipLong.getValue(wordBuf); + + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + centralDirectoryStartDiskNumber = 0; + centralDirectoryStartRelativeOffset = ZipLong.getValue(wordBuf); + + firstLocalFileHeaderOffset = Long.max( + endOfCentralDirectoryRecordOffset - centralDirectoryLength - centralDirectoryStartRelativeOffset, + 0L); + archive.position(centralDirectoryStartRelativeOffset + firstLocalFileHeaderOffset); + } + } + + /** + * Parses the "Zip64 end of central directory locator", + * finds the "Zip64 end of central directory record" using the + * parsed information, parses that and positions the stream at the + * first central directory record. + * + * Expects stream to be positioned right behind the "Zip64 + * end of central directory locator"'s signature. + */ + private void positionAtCentralDirectory64() + throws IOException { + if (isSplitZipArchive) { + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + final long diskNumberOfEOCD = ZipLong.getValue(wordBuf); + + dwordBbuf.rewind(); + IOUtils.readFully(archive, dwordBbuf); + final long relativeOffsetOfEOCD = ZipEightByteInteger.getLongValue(dwordBuf); + ((ZipSplitReadOnlySeekableByteChannel) archive) + .position(diskNumberOfEOCD, relativeOffsetOfEOCD); + } else { + skipBytes(ZIP64_EOCDL_LOCATOR_OFFSET + - ZipConstants.WORD /* signature has already been read */); + dwordBbuf.rewind(); + IOUtils.readFully(archive, dwordBbuf); + archive.position(ZipEightByteInteger.getLongValue(dwordBuf)); + } + + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + if (!Arrays.equals(wordBuf, ZipArchiveOutputStream.ZIP64_EOCD_SIG)) { + throw new ZipException("Archive's ZIP64 end of central " + + "directory locator is corrupt."); + } + + if (isSplitZipArchive) { + skipBytes(ZIP64_EOCD_CFD_DISK_OFFSET + - ZipConstants.WORD /* signature has already been read */); + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + centralDirectoryStartDiskNumber = ZipLong.getValue(wordBuf); + + skipBytes(ZIP64_EOCD_CFD_LOCATOR_RELATIVE_OFFSET); + + dwordBbuf.rewind(); + IOUtils.readFully(archive, dwordBbuf); + centralDirectoryStartRelativeOffset = ZipEightByteInteger.getLongValue(dwordBuf); + ((ZipSplitReadOnlySeekableByteChannel) archive) + .position(centralDirectoryStartDiskNumber, centralDirectoryStartRelativeOffset); + } else { + skipBytes(ZIP64_EOCD_CFD_LOCATOR_OFFSET + - ZipConstants.WORD /* signature has already been read */); + dwordBbuf.rewind(); + IOUtils.readFully(archive, dwordBbuf); + centralDirectoryStartDiskNumber = 0; + centralDirectoryStartRelativeOffset = ZipEightByteInteger.getLongValue(dwordBuf); + archive.position(centralDirectoryStartRelativeOffset); + } + } + + /** + * Searches for the and positions the stream at the start of the + * "End of central dir record". + */ + private void positionAtEndOfCentralDirectoryRecord() + throws IOException { + final boolean found = tryToLocateSignature(MIN_EOCD_SIZE, MAX_EOCD_SIZE, + ZipArchiveOutputStream.EOCD_SIG); + if (!found) { + throw new ZipException("Archive is not a ZIP archive"); + } + } + + /** + * Reads an individual entry of the central directory, creats an + * ZipArchiveEntry from it and adds it to the global maps. + * + * @param noUTF8Flag map used to collect entries that don't have + * their UTF-8 flag set and whose name will be set by data read + * from the local file header later. The current entry may be + * added to this map. + */ + private void + readCentralDirectoryEntry(final Map<ZipArchiveEntry, NameAndComment> noUTF8Flag) + throws IOException { + cfhBbuf.rewind(); + IOUtils.readFully(archive, cfhBbuf); + int off = 0; + final Entry ze = new Entry(); + + final int versionMadeBy = ZipShort.getValue(cfhBuf, off); + off += ZipConstants.SHORT; + ze.setVersionMadeBy(versionMadeBy); + ze.setPlatform((versionMadeBy >> BYTE_SHIFT) & NIBLET_MASK); + + ze.setVersionRequired(ZipShort.getValue(cfhBuf, off)); + off += ZipConstants.SHORT; // version required + + final GeneralPurposeBit gpFlag = GeneralPurposeBit.parse(cfhBuf, off); + final boolean hasUTF8Flag = gpFlag.usesUTF8ForNames(); + final ZipEncoding entryEncoding = + hasUTF8Flag ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding; + if (hasUTF8Flag) { + ze.setNameSource(ZipArchiveEntry.NameSource.NAME_WITH_EFS_FLAG); + } + ze.setGeneralPurposeBit(gpFlag); + ze.setRawFlag(ZipShort.getValue(cfhBuf, off)); + + off += ZipConstants.SHORT; + + //noinspection MagicConstant + ze.setMethod(ZipShort.getValue(cfhBuf, off)); + off += ZipConstants.SHORT; + + final long time = ZipUtil.dosToJavaTime(ZipLong.getValue(cfhBuf, off)); + ze.setTime(time); + off += ZipConstants.WORD; + + ze.setCrc(ZipLong.getValue(cfhBuf, off)); + off += ZipConstants.WORD; + + long size = ZipLong.getValue(cfhBuf, off); + if (size < 0) { + throw new IOException("broken archive, entry with negative compressed size"); + } + ze.setCompressedSize(size); + off += ZipConstants.WORD; + + size = ZipLong.getValue(cfhBuf, off); + if (size < 0) { + throw new IOException("broken archive, entry with negative size"); + } + ze.setSize(size); + off += ZipConstants.WORD; + + final int fileNameLen = ZipShort.getValue(cfhBuf, off); + off += ZipConstants.SHORT; + if (fileNameLen < 0) { + throw new IOException("broken archive, entry with negative fileNameLen"); + } + + final int extraLen = ZipShort.getValue(cfhBuf, off); + off += ZipConstants.SHORT; + if (extraLen < 0) { + throw new IOException("broken archive, entry with negative extraLen"); + } + + final int commentLen = ZipShort.getValue(cfhBuf, off); + off += ZipConstants.SHORT; + if (commentLen < 0) { + throw new IOException("broken archive, entry with negative commentLen"); + } + + ze.setDiskNumberStart(ZipShort.getValue(cfhBuf, off)); + off += ZipConstants.SHORT; + + ze.setInternalAttributes(ZipShort.getValue(cfhBuf, off)); + off += ZipConstants.SHORT; + + ze.setExternalAttributes(ZipLong.getValue(cfhBuf, off)); + off += ZipConstants.WORD; + + final byte[] fileName = IOUtils.readRange(archive, fileNameLen); + if (fileName.length < fileNameLen) { + throw new EOFException(); + } + ze.setName(entryEncoding.decode(fileName), fileName); + + // LFH offset, + ze.setLocalHeaderOffset(ZipLong.getValue(cfhBuf, off) + firstLocalFileHeaderOffset); + // data offset will be filled later + entries.add(ze); + + final byte[] cdExtraData = IOUtils.readRange(archive, extraLen); + if (cdExtraData.length < extraLen) { + throw new EOFException(); + } + try { + ze.setCentralDirectoryExtra(cdExtraData); + } catch (final RuntimeException ex) { + final ZipException z = new ZipException("Invalid extra data in entry " + ze.getName()); + z.initCause(ex); + throw z; + } + + setSizesAndOffsetFromZip64Extra(ze); + sanityCheckLFHOffset(ze); + + final byte[] comment = IOUtils.readRange(archive, commentLen); + if (comment.length < commentLen) { + throw new EOFException(); + } + ze.setComment(entryEncoding.decode(comment)); + + if (!hasUTF8Flag && useUnicodeExtraFields) { + noUTF8Flag.put(ze, new NameAndComment(fileName, comment)); + } + + ze.setStreamContiguous(true); + } + + /** + * Walks through all recorded entries and adds the data available + * from the local file header. + * + * <p>Also records the offsets for the data to read from the + * entries.</p> + */ + private void resolveLocalFileHeaderData(final Map<ZipArchiveEntry, NameAndComment> + entriesWithoutUTF8Flag) + throws IOException { + for (final ZipArchiveEntry zipArchiveEntry : entries) { + // entries is filled in populateFromCentralDirectory and + // never modified + final Entry ze = (Entry) zipArchiveEntry; + final int[] lens = setDataOffset(ze); + final int fileNameLen = lens[0]; + final int extraFieldLen = lens[1]; + skipBytes(fileNameLen); + final byte[] localExtraData = IOUtils.readRange(archive, extraFieldLen); + if (localExtraData.length < extraFieldLen) { + throw new EOFException(); + } + try { + ze.setExtra(localExtraData); + } catch (final RuntimeException ex) { + final ZipException z = new ZipException("Invalid extra data in entry " + ze.getName()); + z.initCause(ex); + throw z; + } + + if (entriesWithoutUTF8Flag.containsKey(ze)) { + final NameAndComment nc = entriesWithoutUTF8Flag.get(ze); + ZipUtil.setNameAndCommentFromExtraFields(ze, nc.name, + nc.comment); + } + } + } + + private void sanityCheckLFHOffset(final ZipArchiveEntry ze) throws IOException { + if (ze.getDiskNumberStart() < 0) { + throw new IOException("broken archive, entry with negative disk number"); + } + if (ze.getLocalHeaderOffset() < 0) { + throw new IOException("broken archive, entry with negative local file header offset"); + } + if (isSplitZipArchive) { + if (ze.getDiskNumberStart() > centralDirectoryStartDiskNumber) { + throw new IOException("local file header for " + ze.getName() + " starts on a later disk than central directory"); + } + if (ze.getDiskNumberStart() == centralDirectoryStartDiskNumber + && ze.getLocalHeaderOffset() > centralDirectoryStartRelativeOffset) { + throw new IOException("local file header for " + ze.getName() + " starts after central directory"); + } + } else if (ze.getLocalHeaderOffset() > centralDirectoryStartOffset) { + throw new IOException("local file header for " + ze.getName() + " starts after central directory"); + } + } + + private int[] setDataOffset(final ZipArchiveEntry ze) throws IOException { + long offset = ze.getLocalHeaderOffset(); + if (isSplitZipArchive) { + ((ZipSplitReadOnlySeekableByteChannel) archive) + .position(ze.getDiskNumberStart(), offset + LFH_OFFSET_FOR_FILENAME_LENGTH); + // the offset should be updated to the global offset + offset = archive.position() - LFH_OFFSET_FOR_FILENAME_LENGTH; + } else { + archive.position(offset + LFH_OFFSET_FOR_FILENAME_LENGTH); + } + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + wordBbuf.flip(); + wordBbuf.get(shortBuf); + final int fileNameLen = ZipShort.getValue(shortBuf); + wordBbuf.get(shortBuf); + final int extraFieldLen = ZipShort.getValue(shortBuf); + ze.setDataOffset(offset + LFH_OFFSET_FOR_FILENAME_LENGTH + + ZipConstants.SHORT + ZipConstants.SHORT + fileNameLen + extraFieldLen); + if (ze.getDataOffset() + ze.getCompressedSize() > centralDirectoryStartOffset) { + throw new IOException("data for " + ze.getName() + " overlaps with central directory."); + } + return new int[] { fileNameLen, extraFieldLen }; + } + + /** + * If the entry holds a Zip64 extended information extra field, + * read sizes from there if the entry's sizes are set to + * 0xFFFFFFFFF, do the same for the offset of the local file + * header. + * + * <p>Ensures the Zip64 extra either knows both compressed and + * uncompressed size or neither of both as the internal logic in + * ExtraFieldUtils forces the field to create local header data + * even if they are never used - and here a field with only one + * size would be invalid.</p> + */ + private void setSizesAndOffsetFromZip64Extra(final ZipArchiveEntry ze) + throws IOException { + final ZipExtraField extra = + ze.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID); + if (extra != null && !(extra instanceof Zip64ExtendedInformationExtraField)) { + throw new ZipException("archive contains unparseable zip64 extra field"); + } + final Zip64ExtendedInformationExtraField z64 = + (Zip64ExtendedInformationExtraField) extra; + if (z64 != null) { + final boolean hasUncompressedSize = ze.getSize() == ZipConstants.ZIP64_MAGIC; + final boolean hasCompressedSize = ze.getCompressedSize() == ZipConstants.ZIP64_MAGIC; + final boolean hasRelativeHeaderOffset = + ze.getLocalHeaderOffset() == ZipConstants.ZIP64_MAGIC; + final boolean hasDiskStart = ze.getDiskNumberStart() == ZipConstants.ZIP64_MAGIC_SHORT; + z64.reparseCentralDirectoryData(hasUncompressedSize, + hasCompressedSize, + hasRelativeHeaderOffset, + hasDiskStart); + + if (hasUncompressedSize) { + final long size = z64.getSize().getLongValue(); + if (size < 0) { + throw new IOException("broken archive, entry with negative size"); + } + ze.setSize(size); + } else if (hasCompressedSize) { + z64.setSize(new ZipEightByteInteger(ze.getSize())); + } + + if (hasCompressedSize) { + final long size = z64.getCompressedSize().getLongValue(); + if (size < 0) { + throw new IOException("broken archive, entry with negative compressed size"); + } + ze.setCompressedSize(size); + } else if (hasUncompressedSize) { + z64.setCompressedSize(new ZipEightByteInteger(ze.getCompressedSize())); + } + + if (hasRelativeHeaderOffset) { + ze.setLocalHeaderOffset(z64.getRelativeHeaderOffset().getLongValue()); + } + + if (hasDiskStart) { + ze.setDiskNumberStart(z64.getDiskStartNumber().getValue()); + } + } + } + + /** + * Skips the given number of bytes or throws an EOFException if + * skipping failed. + */ + private void skipBytes(final int count) throws IOException { + final long currentPosition = archive.position(); + final long newPosition = currentPosition + count; + if (newPosition > archive.size()) { + throw new EOFException(); + } + archive.position(newPosition); + } + + /** + * Checks whether the archive starts with a LFH. If it doesn't, + * it may be an empty archive. + */ + private boolean startsWithLocalFileHeader() throws IOException { + archive.position(firstLocalFileHeaderOffset); + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + return Arrays.equals(wordBuf, ZipArchiveOutputStream.LFH_SIG); + } + + /** + * Searches the archive backwards from minDistance to maxDistance + * for the given signature, positions the RandomaccessFile right + * at the signature if it has been found. + */ + private boolean tryToLocateSignature(final long minDistanceFromEnd, + final long maxDistanceFromEnd, + final byte[] sig) throws IOException { + boolean found = false; + long off = archive.size() - minDistanceFromEnd; + final long stopSearching = + Math.max(0L, archive.size() - maxDistanceFromEnd); + if (off >= 0) { + for (; off >= stopSearching; off--) { + archive.position(off); + try { + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + wordBbuf.flip(); + } catch (final EOFException ex) { // NOSONAR + break; + } + int curr = wordBbuf.get(); + if (curr == sig[POS_0]) { + curr = wordBbuf.get(); + if (curr == sig[POS_1]) { + curr = wordBbuf.get(); + if (curr == sig[POS_2]) { + curr = wordBbuf.get(); + if (curr == sig[POS_3]) { + found = true; + break; + } + } + } + } + } + } + if (found) { + archive.position(off); + } + return found; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipLong.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipLong.java new file mode 100644 index 0000000000..aceb66da69 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipLong.java @@ -0,0 +1,220 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.io.Serializable; + +import org.readium.r2.shared.util.zip.compress.utils.ByteUtils; + +/** + * Utility class that represents a four byte integer with conversion + * rules for the little endian byte order of ZIP files. + * @Immutable + */ +public final class ZipLong implements Cloneable, Serializable { + private static final long serialVersionUID = 1L; + + /** Central File Header Signature */ + public static final ZipLong CFH_SIG = new ZipLong(0X02014B50L); + + /** Local File Header Signature */ + public static final ZipLong LFH_SIG = new ZipLong(0X04034B50L); + + /** + * Data Descriptor signature. + * + * <p>Actually, PKWARE uses this as marker for split/spanned + * archives and other archivers have started to use it as Data + * Descriptor signature (as well).</p> + * @since 1.1 + */ + public static final ZipLong DD_SIG = new ZipLong(0X08074B50L); + + /** + * Value stored in size and similar fields if ZIP64 extensions are + * used. + * @since 1.3 + */ + static final ZipLong ZIP64_MAGIC = new ZipLong(ZipConstants.ZIP64_MAGIC); + + /** + * Marks ZIP archives that were supposed to be split or spanned + * but only needed a single segment in then end (so are actually + * neither split nor spanned). + * + * <p>This is the "PK00" prefix found in some archives.</p> + * @since 1.5 + */ + public static final ZipLong SINGLE_SEGMENT_SPLIT_MARKER = + new ZipLong(0X30304B50L); + + /** + * Archive extra data record signature. + * @since 1.5 + */ + public static final ZipLong AED_SIG = new ZipLong(0X08064B50L); + + /** + * Get value as four bytes in big endian byte order. + * @param value the value to convert + * @return value as four bytes in big endian byte order + */ + public static byte[] getBytes(final long value) { + final byte[] result = new byte[ZipConstants.WORD]; + putLong(value, result, 0); + return result; + } + + /** + * Helper method to get the value as a Java long from a four-byte array + * @param bytes the array of bytes + * @return the corresponding Java long value + */ + public static long getValue(final byte[] bytes) { + return getValue(bytes, 0); + } + + /** + * Helper method to get the value as a Java long from four bytes starting at given array offset + * @param bytes the array of bytes + * @param offset the offset to start + * @return the corresponding Java long value + */ + public static long getValue(final byte[] bytes, final int offset) { + return ByteUtils.fromLittleEndian(bytes, offset, 4); + } + + /** + * put the value as four bytes in big endian byte order. + * @param value the Java long to convert to bytes + * @param buf the output buffer + * @param offset + * The offset within the output buffer of the first byte to be written. + * must be non-negative and no larger than {@code buf.length-4} + */ + + public static void putLong(final long value, final byte[] buf, final int offset) { + ByteUtils.toLittleEndian(buf, value, offset, 4); + } + + private final long value; + + /** + * Create instance from bytes. + * @param bytes the bytes to store as a ZipLong + */ + public ZipLong (final byte[] bytes) { + this(bytes, 0); + } + + /** + * Create instance from the four bytes starting at offset. + * @param bytes the bytes to store as a ZipLong + * @param offset the offset to start + */ + public ZipLong (final byte[] bytes, final int offset) { + value = ZipLong.getValue(bytes, offset); + } + + /** + * create instance from a java int. + * @param value the int to store as a ZipLong + * @since 1.15 + */ + public ZipLong(final int value) { + this.value = value; + } + + /** + * Create instance from a number. + * @param value the long to store as a ZipLong + */ + public ZipLong(final long value) { + this.value = value; + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (final CloneNotSupportedException cnfe) { + // impossible + throw new IllegalStateException(cnfe); //NOSONAR + } + } + + /** + * Override to make two instances with same value equal. + * @param o an object to compare + * @return true if the objects are equal + */ + @Override + public boolean equals(final Object o) { + if (!(o instanceof ZipLong)) { + return false; + } + return value == ((ZipLong) o).getValue(); + } + + /** + * Get value as four bytes in big endian byte order. + * @return value as four bytes in big endian order + */ + public byte[] getBytes() { + return ZipLong.getBytes(value); + } + + /** + * Get value as a (signed) java int + * @return value as int + * @since 1.15 + */ + public int getIntValue() { return (int)value;} + + /** + * Get value as Java long. + * @return value as a long + */ + public long getValue() { + return value; + } + + /** + * Override to make two instances with same value equal. + * @return the value stored in the ZipLong + */ + @Override + public int hashCode() { + return (int) value; + } + + public void putLong(final byte[] buf, final int offset) { + putLong(value, buf, offset); + } + + @Override + public String toString() { + return "ZipLong value: " + value; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipMethod.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipMethod.java new file mode 100644 index 0000000000..a1fb23b805 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipMethod.java @@ -0,0 +1,230 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipEntry; + +/** + * List of known compression methods + * + * Many of these methods are currently not supported by commons compress + * + * @since 1.5 + */ +public enum ZipMethod { + + /** + * Compression method 0 for uncompressed entries. + * + * @see ZipEntry#STORED + */ + STORED(ZipEntry.STORED), + + /** + * UnShrinking. + * dynamic Lempel-Ziv-Welch-Algorithm + * + * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">Explanation of fields: compression + * method: (2 bytes)</a> + */ + UNSHRINKING(1), + + /** + * Reduced with compression factor 1. + * + * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">Explanation of fields: compression + * method: (2 bytes)</a> + */ + EXPANDING_LEVEL_1(2), + + /** + * Reduced with compression factor 2. + * + * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">Explanation of fields: compression + * method: (2 bytes)</a> + */ + EXPANDING_LEVEL_2(3), + + /** + * Reduced with compression factor 3. + * + * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">Explanation of fields: compression + * method: (2 bytes)</a> + */ + EXPANDING_LEVEL_3(4), + + /** + * Reduced with compression factor 4. + * + * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">Explanation of fields: compression + * method: (2 bytes)</a> + */ + EXPANDING_LEVEL_4(5), + + /** + * Imploding. + * + * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">Explanation of fields: compression + * method: (2 bytes)</a> + */ + IMPLODING(6), + + /** + * Tokenization. + * + * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">Explanation of fields: compression + * method: (2 bytes)</a> + */ + TOKENIZATION(7), + + /** + * Compression method 8 for compressed (deflated) entries. + * + * @see ZipEntry#DEFLATED + */ + DEFLATED(ZipEntry.DEFLATED), + + /** + * Compression Method 9 for enhanced deflate. + * + * @see <a href="https://www.winzip.com/wz54.htm">https://www.winzip.com/wz54.htm</a> + */ + ENHANCED_DEFLATED(9), + + /** + * PKWARE Data Compression Library Imploding. + * + * @see <a href="https://www.winzip.com/wz54.htm">https://www.winzip.com/wz54.htm</a> + */ + PKWARE_IMPLODING(10), + + /** + * Compression Method 12 for bzip2. + * + * @see <a href="https://www.winzip.com/wz54.htm">https://www.winzip.com/wz54.htm</a> + */ + BZIP2(12), + + /** + * Compression Method 14 for LZMA. + * + * @see <a href="https://www.7-zip.org/sdk.html">https://www.7-zip.org/sdk.html</a> + * @see <a href="https://www.winzip.com/wz54.htm">https://www.winzip.com/wz54.htm</a> + */ + LZMA(14), + + + /** + * Compression Method 95 for XZ. + * + * @see <a href="https://www.winzip.com/wz54.htm">https://www.winzip.com/wz54.htm</a> + */ + XZ(95), + + /** + * Compression Method 96 for Jpeg compression. + * + * @see <a href="https://www.winzip.com/wz54.htm">https://www.winzip.com/wz54.htm</a> + */ + JPEG(96), + + /** + * Compression Method 97 for WavPack. + * + * @see <a href="https://www.winzip.com/wz54.htm">https://www.winzip.com/wz54.htm</a> + */ + WAVPACK(97), + + /** + * Compression Method 98 for PPMd. + * + * @see <a href="https://www.winzip.com/wz54.htm">https://www.winzip.com/wz54.htm</a> + */ + PPMD(98), + + + /** + * Compression Method 99 for AES encryption. + * + * @see <a href="https://www.winzip.com/wz54.htm">https://www.winzip.com/wz54.htm</a> + */ + AES_ENCRYPTED(99), + + /** + * Unknown compression method. + */ + UNKNOWN(); + + static final int UNKNOWN_CODE = -1; + + private static final Map<Integer, ZipMethod> codeToEnum; + + static { + final Map<Integer, ZipMethod> cte = new HashMap<>(); + for (final ZipMethod method : values()) { + cte.put(method.getCode(), method); + } + codeToEnum = Collections.unmodifiableMap(cte); + } + + /** + * returns the {@link ZipMethod} for the given code or null if the + * method is not known. + * @param code the code + * @return the {@link ZipMethod} for the given code or null if the + * method is not known. + */ + public static ZipMethod getMethodByCode(final int code) { + return codeToEnum.get(code); + } + + private final int code; + + ZipMethod() { + this(UNKNOWN_CODE); + } + + /** + * private constructor for enum style class. + */ + ZipMethod(final int code) { + this.code = code; + } + + + /** + * the code of the compression method. + * + * @see ZipArchiveEntry#getMethod() + * + * @return an integer code for the method + */ + public int getCode() { + return code; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipShort.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipShort.java new file mode 100644 index 0000000000..8f3e5377d4 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipShort.java @@ -0,0 +1,167 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import org.readium.r2.shared.util.zip.compress.utils.ByteUtils; + +import java.io.Serializable; + +/** + * Utility class that represents a two byte integer with conversion + * rules for the little endian byte order of ZIP files. + * @Immutable + */ +public final class ZipShort implements Cloneable, Serializable { + /** + * ZipShort with a value of 0. + * @since 1.14 + */ + public static final ZipShort ZERO = new ZipShort(0); + + private static final long serialVersionUID = 1L; + + /** + * Get value as two bytes in big endian byte order. + * @param value the Java int to convert to bytes + * @return the converted int as a byte array in big endian byte order + */ + public static byte[] getBytes(final int value) { + final byte[] result = new byte[2]; + putShort(value, result, 0); + return result; + } + + /** + * Helper method to get the value as a java int from a two-byte array + * @param bytes the array of bytes + * @return the corresponding java int value + */ + public static int getValue(final byte[] bytes) { + return getValue(bytes, 0); + } + + /** + * Helper method to get the value as a java int from two bytes starting at given array offset + * @param bytes the array of bytes + * @param offset the offset to start + * @return the corresponding java int value + */ + public static int getValue(final byte[] bytes, final int offset) { + return (int) ByteUtils.fromLittleEndian(bytes, offset, 2); + } + + /** + * put the value as two bytes in big endian byte order. + * @param value the Java int to convert to bytes + * @param buf the output buffer + * @param offset + * The offset within the output buffer of the first byte to be written. + * must be non-negative and no larger than {@code buf.length-2} + */ + public static void putShort(final int value, final byte[] buf, final int offset) { + ByteUtils.toLittleEndian(buf, value, offset, 2); + } + + private final int value; + + /** + * Create instance from bytes. + * @param bytes the bytes to store as a ZipShort + */ + public ZipShort (final byte[] bytes) { + this(bytes, 0); + } + + /** + * Create instance from the two bytes starting at offset. + * @param bytes the bytes to store as a ZipShort + * @param offset the offset to start + */ + public ZipShort (final byte[] bytes, final int offset) { + value = ZipShort.getValue(bytes, offset); + } + + /** + * Create instance from a number. + * @param value the int to store as a ZipShort + */ + public ZipShort (final int value) { + this.value = value; + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (final CloneNotSupportedException cnfe) { + // impossible + throw new IllegalStateException(cnfe); //NOSONAR + } + } + + /** + * Override to make two instances with same value equal. + * @param o an object to compare + * @return true if the objects are equal + */ + @Override + public boolean equals(final Object o) { + if (!(o instanceof ZipShort)) { + return false; + } + return value == ((ZipShort) o).getValue(); + } + + /** + * Get value as two bytes in big endian byte order. + * @return the value as a a two byte array in big endian byte order + */ + public byte[] getBytes() { + final byte[] result = new byte[2]; + ByteUtils.toLittleEndian(result, value, 0, 2); + return result; + } + + /** + * Get value as Java int. + * @return value as a Java int + */ + public int getValue() { + return value; + } + + /** + * Override to make two instances with same value equal. + * @return the value stored in the ZipShort + */ + @Override + public int hashCode() { + return value; + } + + @Override + public String toString() { + return "ZipShort value: " + value; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipSplitOutputStream.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipSplitOutputStream.java new file mode 100644 index 0000000000..97695a3bc3 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipSplitOutputStream.java @@ -0,0 +1,252 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import org.readium.r2.shared.util.zip.compress.utils.FileNameUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Used internally by {@link ZipArchiveOutputStream} when creating a split archive. + * + * @since 1.20 + */ +class ZipSplitOutputStream extends OutputStream { + private OutputStream outputStream; + private File zipFile; + private final long splitSize; + private int currentSplitSegmentIndex = 0; + private long currentSplitSegmentBytesWritten = 0; + private boolean finished = false; + private final byte[] singleByte = new byte[1]; + + /** + * 8.5.1 Capacities for split archives are as follows: + * <p> + * Maximum number of segments = 4,294,967,295 - 1 + * Maximum .ZIP segment size = 4,294,967,295 bytes (refer to section 8.5.6) + * Minimum segment size = 64K + * Maximum PKSFX segment size = 2,147,483,647 bytes + */ + private final static long ZIP_SEGMENT_MIN_SIZE = 64 * 1024L; + private final static long ZIP_SEGMENT_MAX_SIZE = 4294967295L; + + /** + * Create a split zip. If the zip file is smaller than the split size, + * then there will only be one split zip, and its suffix is .zip, + * otherwise the split segments should be like .z01, .z02, ... .z(N-1), .zip + * + * @param zipFile the zip file to write to + * @param splitSize the split size + */ + public ZipSplitOutputStream(final File zipFile, final long splitSize) throws IllegalArgumentException, IOException { + if (splitSize < ZIP_SEGMENT_MIN_SIZE || splitSize > ZIP_SEGMENT_MAX_SIZE) { + throw new IllegalArgumentException("zip split segment size should between 64K and 4,294,967,295"); + } + + this.zipFile = zipFile; + this.splitSize = splitSize; + + this.outputStream = new FileOutputStream(zipFile); + // write the zip split signature 0x08074B50 to the zip file + writeZipSplitSignature(); + } + + /** + * Some data can not be written to different split segments, for example: + * <p> + * 4.4.1.5 The end of central directory record and the Zip64 end + * of central directory locator record MUST reside on the same + * disk when splitting or spanning an archive. + * + * @param unsplittableContentSize + * @throws IllegalArgumentException + * @throws IOException + */ + public void prepareToWriteUnsplittableContent(final long unsplittableContentSize) throws IllegalArgumentException, IOException { + if (unsplittableContentSize > this.splitSize) { + throw new IllegalArgumentException("The unsplittable content size is bigger than the split segment size"); + } + + final long bytesRemainingInThisSegment = this.splitSize - this.currentSplitSegmentBytesWritten; + if (bytesRemainingInThisSegment < unsplittableContentSize) { + openNewSplitSegment(); + } + } + + @Override + public void write(final int i) throws IOException { + singleByte[0] = (byte)(i & 0xff); + write(singleByte); + } + + @Override + public void write(final byte[] b) throws IOException { + write(b, 0, b.length); + } + + /** + * Write the data to zip split segments, if the remaining space of current split segment + * is not enough, then a new split segment should be created + * + * @param b data to write + * @param off offset of the start of data in param b + * @param len the length of data to write + * @throws IOException + */ + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + if (len <= 0) { + return; + } + + if (currentSplitSegmentBytesWritten >= splitSize) { + openNewSplitSegment(); + write(b, off, len); + } else if (currentSplitSegmentBytesWritten + len > splitSize) { + final int bytesToWriteForThisSegment = (int) splitSize - (int) currentSplitSegmentBytesWritten; + write(b, off, bytesToWriteForThisSegment); + openNewSplitSegment(); + write(b, off + bytesToWriteForThisSegment, len - bytesToWriteForThisSegment); + } else { + outputStream.write(b, off, len); + currentSplitSegmentBytesWritten += len; + } + } + + @Override + public void close() throws IOException { + if (!finished) { + finish(); + } + } + + /** + * The last zip split segment's suffix should be .zip + * + * @throws IOException + */ + private void finish() throws IOException { + if (finished) { + throw new IOException("This archive has already been finished"); + } + + final String zipFileBaseName = FileNameUtils.getBaseName(zipFile.getName()); + final File lastZipSplitSegmentFile = new File(zipFile.getParentFile(), zipFileBaseName + ".zip"); + outputStream.close(); + if (!zipFile.renameTo(lastZipSplitSegmentFile)) { + throw new IOException("Failed to rename " + zipFile + " to " + lastZipSplitSegmentFile); + } + finished = true; + } + + /** + * Create a new zip split segment and prepare to write to the new segment + * + * @return + * @throws IOException + */ + private OutputStream openNewSplitSegment() throws IOException { + File newFile; + if (currentSplitSegmentIndex == 0) { + outputStream.close(); + newFile = createNewSplitSegmentFile(1); + if (!zipFile.renameTo(newFile)) { + throw new IOException("Failed to rename " + zipFile + " to " + newFile); + } + } + + newFile = createNewSplitSegmentFile(null); + + outputStream.close(); + outputStream = new FileOutputStream(newFile); + currentSplitSegmentBytesWritten = 0; + zipFile = newFile; + currentSplitSegmentIndex++; + + return outputStream; + } + + /** + * Write the zip split signature (0x08074B50) to the head of the first zip split segment + * + * @throws IOException + */ + private void writeZipSplitSignature() throws IOException { + outputStream.write(ZipArchiveOutputStream.DD_SIG); + currentSplitSegmentBytesWritten += ZipArchiveOutputStream.DD_SIG.length; + } + + /** + * Create the new zip split segment, the last zip segment should be .zip, and the zip split segments' suffix should be + * like .z01, .z02, .z03, ... .z99, .z100, ..., .z(N-1), .zip + * <p> + * 8.3.3 Split ZIP files are typically written to the same location + * and are subject to name collisions if the spanned name + * format is used since each segment will reside on the same + * drive. To avoid name collisions, split archives are named + * as follows. + * <p> + * Segment 1 = filename.z01 + * Segment n-1 = filename.z(n-1) + * Segment n = filename.zip + * <p> + * NOTE: + * The zip split segment begin from 1,2,3,... , and we're creating a new segment, + * so the new segment suffix should be (currentSplitSegmentIndex + 2) + * + * @param zipSplitSegmentSuffixIndex + * @return + * @throws IOException + */ + private File createNewSplitSegmentFile(final Integer zipSplitSegmentSuffixIndex) throws IOException { + final int newZipSplitSegmentSuffixIndex = zipSplitSegmentSuffixIndex == null ? (currentSplitSegmentIndex + 2) : zipSplitSegmentSuffixIndex; + final String baseName = FileNameUtils.getBaseName(zipFile.getName()); + String extension = ".z"; + if (newZipSplitSegmentSuffixIndex <= 9) { + extension += "0" + newZipSplitSegmentSuffixIndex; + } else { + extension += newZipSplitSegmentSuffixIndex; + } + + final File newFile = new File(zipFile.getParent(), baseName + extension); + + if (newFile.exists()) { + throw new IOException("split zip segment " + baseName + extension + " already exists"); + } + return newFile; + } + + public int getCurrentSplitSegmentIndex() { + return currentSplitSegmentIndex; + } + + public long getCurrentSplitSegmentBytesWritten() { + return currentSplitSegmentBytesWritten; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipSplitReadOnlySeekableByteChannel.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipSplitReadOnlySeekableByteChannel.java new file mode 100644 index 0000000000..fcd0419394 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipSplitReadOnlySeekableByteChannel.java @@ -0,0 +1,149 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel; +import org.readium.r2.shared.util.zip.compress.utils.MultiReadOnlySeekableByteChannel; + +/** + * {@link MultiReadOnlySeekableByteChannel} that knows what a split ZIP archive should look like. + * + * <p>If you want to read a split archive using {@link ZipFile} then create an instance of this class from the parts of + * the archive.</p> + * + * @since 1.20 + */ +public class ZipSplitReadOnlySeekableByteChannel extends MultiReadOnlySeekableByteChannel { + + private static final Path[] EMPTY_PATH_ARRAY = {}; + private static final int ZIP_SPLIT_SIGNATURE_LENGTH = 4; + + /** + * Concatenates the given channels. + * + * @param channels the channels to concatenate, note that the LAST CHANNEL of channels should be the LAST SEGMENT(.zip) + * and theses channels should be added in correct order (e.g. .z01, .z02... .z99, .zip) + * @return SeekableByteChannel that concatenates all provided channels + * @throws NullPointerException if channels is null + * @throws IOException if reading channels fails + */ + public static SeekableByteChannel forOrderedSeekableByteChannels(final SeekableByteChannel... channels) throws IOException { + if (Objects.requireNonNull(channels, "channels must not be null").length == 1) { + return channels[0]; + } + return new ZipSplitReadOnlySeekableByteChannel(Arrays.asList(channels)); + } + + /** + * Concatenates the given channels. + * + * @param lastSegmentChannel channel of the last segment of split zip segments, its extension should be .zip + * @param channels the channels to concatenate except for the last segment, + * note theses channels should be added in correct order (e.g. .z01, .z02... .z99) + * @return SeekableByteChannel that concatenates all provided channels + * @throws NullPointerException if lastSegmentChannel or channels is null + * @throws IOException if the first channel doesn't seem to hold + * the beginning of a split archive + */ + public static SeekableByteChannel forOrderedSeekableByteChannels(final SeekableByteChannel lastSegmentChannel, + final Iterable<SeekableByteChannel> channels) throws IOException { + Objects.requireNonNull(channels, "channels"); + Objects.requireNonNull(lastSegmentChannel, "lastSegmentChannel"); + + final List<SeekableByteChannel> channelsList = new ArrayList<>(); + for (SeekableByteChannel channel: channels) { + channelsList.add(channel); + } + channelsList.add(lastSegmentChannel); + + return forOrderedSeekableByteChannels(channelsList.toArray(new SeekableByteChannel[0])); + } + + private final ByteBuffer zipSplitSignatureByteBuffer = + ByteBuffer.allocate(ZIP_SPLIT_SIGNATURE_LENGTH); + + /** + * Concatenates the given channels. + * + * <p>The channels should be add in ascending order, e.g. z01, + * z02, ... z99, zip please note that the .zip file is the last + * segment and should be added as the last one in the channels</p> + * + * @param channels the channels to concatenate + * @throws NullPointerException if channels is null + * @throws IOException if the first channel doesn't seem to hold + * the beginning of a split archive + */ + public ZipSplitReadOnlySeekableByteChannel(final List<SeekableByteChannel> channels) + throws IOException { + super(channels); + + // the first split zip segment should begin with zip split signature + assertSplitSignature(channels); + } + + /** + * Based on the zip specification: + * + * <p> + * 8.5.3 Spanned/Split archives created using PKZIP for Windows + * (V2.50 or greater), PKZIP Command Line (V2.50 or greater), + * or PKZIP Explorer will include a special spanning + * signature as the first 4 bytes of the first segment of + * the archive. This signature (0x08074b50) will be + * followed immediately by the local header signature for + * the first file in the archive. + * + * <p> + * the first 4 bytes of the first zip split segment should be the zip split signature(0x08074B50) + * + * @param channels channels to be validated + * @throws IOException + */ + private void assertSplitSignature(final List<SeekableByteChannel> channels) + throws IOException { + final SeekableByteChannel channel = channels.get(0); + // the zip split file signature is at the beginning of the first split segment + channel.position(0L); + + zipSplitSignatureByteBuffer.rewind(); + channel.read(zipSplitSignatureByteBuffer); + final ZipLong signature = new ZipLong(zipSplitSignatureByteBuffer.array()); + if (!signature.equals(ZipLong.DD_SIG)) { + channel.position(0L); + throw new IOException("The first zip split segment does not begin with split zip file signature"); + } + + channel.position(0L); + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipUtil.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipUtil.java new file mode 100644 index 0000000000..838b9e1f38 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/archivers/zip/ZipUtil.java @@ -0,0 +1,366 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ +package org.readium.r2.shared.util.zip.compress.archivers.zip; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; + +/** + * Utility class for handling DOS and Java time conversions. + * @Immutable + */ +public abstract class ZipUtil { + /** + * Smallest date/time ZIP can handle. + */ + private static final byte[] DOS_TIME_MIN = ZipLong.getBytes(0x00002100L); + + /** + * Assumes a negative integer really is a positive integer that + * has wrapped around and re-creates the original value. + * + * @param i the value to treat as unsigned int. + * @return the unsigned int as a long. + */ + public static long adjustToLong(final int i) { + if (i < 0) { + return 2 * ((long) Integer.MAX_VALUE) + 2 + i; + } + return i; + } + + /** + * Converts a BigInteger into a long, and blows up + * (NumberFormatException) if the BigInteger is too big. + * + * @param big BigInteger to convert. + * @return long representation of the BigInteger. + */ + static long bigToLong(final BigInteger big) { + if (big.bitLength() <= 63) { // bitLength() doesn't count the sign bit. + return big.longValue(); + } + throw new NumberFormatException("The BigInteger cannot fit inside a 64 bit java long: [" + big + "]"); + } + + /** + * Whether this library is able to read or write the given entry. + */ + static boolean canHandleEntryData(final ZipArchiveEntry entry) { + return supportsEncryptionOf(entry) && supportsMethodOf(entry); + } + + /** + * Checks whether the entry requires features not (yet) supported + * by the library and throws an exception if it does. + */ + static void checkRequestedFeatures(final ZipArchiveEntry ze) + throws UnsupportedZipFeatureException { + if (!supportsEncryptionOf(ze)) { + throw + new UnsupportedZipFeatureException(UnsupportedZipFeatureException + .Feature.ENCRYPTION, ze); + } + if (!supportsMethodOf(ze)) { + final ZipMethod m = ZipMethod.getMethodByCode(ze.getMethod()); + if (m == null) { + throw + new UnsupportedZipFeatureException(UnsupportedZipFeatureException + .Feature.METHOD, ze); + } + throw new UnsupportedZipFeatureException(m, ze); + } + } + + + /** + * Create a copy of the given array - or return null if the + * argument is null. + */ + static byte[] copy(final byte[] from) { + if (from != null) { + return Arrays.copyOf(from, from.length); + } + return null; + } + + static void copy(final byte[] from, final byte[] to, final int offset) { + if (from != null) { + System.arraycopy(from, 0, to, offset, from.length); + } + } + + /** + * Converts DOS time to Java time (number of milliseconds since + * epoch). + * @param dosTime time to convert + * @return converted time + */ + public static long dosToJavaTime(final long dosTime) { + final Calendar cal = Calendar.getInstance(); + // CheckStyle:MagicNumberCheck OFF - no point + cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980); + cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1); + cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f); + cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f); + cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f); + cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e); + cal.set(Calendar.MILLISECOND, 0); + // CheckStyle:MagicNumberCheck ON + return cal.getTime().getTime(); + } + + /** + * Convert a DOS date/time field to a Date object. + * + * @param zipDosTime contains the stored DOS time. + * @return a Date instance corresponding to the given time. + */ + public static Date fromDosTime(final ZipLong zipDosTime) { + final long dosTime = zipDosTime.getValue(); + return new Date(dosToJavaTime(dosTime)); + } + + /** + * If the stored CRC matches the one of the given name, return the + * Unicode name of the given field. + * + * <p>If the field is null or the CRCs don't match, return null + * instead.</p> + */ + private static + String getUnicodeStringIfOriginalMatches(final AbstractUnicodeExtraField f, + final byte[] orig) { + if (f != null) { + final CRC32 crc32 = new CRC32(); + crc32.update(orig); + final long origCRC32 = crc32.getValue(); + + if (origCRC32 == f.getNameCRC32()) { + try { + return ZipEncodingHelper + .UTF8_ZIP_ENCODING.decode(f.getUnicodeName()); + } catch (final IOException ex) { + // UTF-8 unsupported? should be impossible the + // Unicode*ExtraField must contain some bad bytes + } + } + } + // TODO log this anywhere? + return null; + } + + /** + * <p> + * Converts a long into a BigInteger. Negative numbers between -1 and + * -2^31 are treated as unsigned 32 bit (e.g., positive) integers. + * Negative numbers below -2^31 cause an IllegalArgumentException + * to be thrown. + * </p> + * + * @param l long to convert to BigInteger. + * @return BigInteger representation of the provided long. + */ + static BigInteger longToBig(long l) { + if (l < Integer.MIN_VALUE) { + throw new IllegalArgumentException("Negative longs < -2^31 not permitted: [" + l + "]"); + } + if (l < 0 && l >= Integer.MIN_VALUE) { + // If someone passes in a -2, they probably mean 4294967294 + // (For example, Unix UID/GID's are 32 bit unsigned.) + l = ZipUtil.adjustToLong((int) l); + } + return BigInteger.valueOf(l); + } + + /** + * Reverses a byte[] array. Reverses in-place (thus provided array is + * mutated), but also returns same for convenience. + * + * @param array to reverse (mutated in-place, but also returned for + * convenience). + * + * @return the reversed array (mutated in-place, but also returned for + * convenience). + * @since 1.5 + */ + public static byte[] reverse(final byte[] array) { + final int z = array.length - 1; // position of last element + for (int i = 0; i < array.length / 2; i++) { + final byte x = array[i]; + array[i] = array[z - i]; + array[z - i] = x; + } + return array; + } + + /** + * If the entry has Unicode*ExtraFields and the CRCs of the + * names/comments match those of the extra fields, transfer the + * known Unicode values from the extra field. + */ + static void setNameAndCommentFromExtraFields(final ZipArchiveEntry ze, + final byte[] originalNameBytes, + final byte[] commentBytes) { + final ZipExtraField nameCandidate = ze.getExtraField(UnicodePathExtraField.UPATH_ID); + final UnicodePathExtraField name = nameCandidate instanceof UnicodePathExtraField + ? (UnicodePathExtraField) nameCandidate : null; + final String newName = getUnicodeStringIfOriginalMatches(name, + originalNameBytes); + if (newName != null) { + ze.setName(newName); + ze.setNameSource(ZipArchiveEntry.NameSource.UNICODE_EXTRA_FIELD); + } + + if (commentBytes != null && commentBytes.length > 0) { + final ZipExtraField cmtCandidate = ze.getExtraField(UnicodeCommentExtraField.UCOM_ID); + final UnicodeCommentExtraField cmt = cmtCandidate instanceof UnicodeCommentExtraField + ? (UnicodeCommentExtraField) cmtCandidate : null; + final String newComment = + getUnicodeStringIfOriginalMatches(cmt, commentBytes); + if (newComment != null) { + ze.setComment(newComment); + ze.setCommentSource(ZipArchiveEntry.CommentSource.UNICODE_EXTRA_FIELD); + } + } + } + + /** + * Converts a signed byte into an unsigned integer representation + * (e.g., -1 becomes 255). + * + * @param b byte to convert to int + * @return int representation of the provided byte + * @since 1.5 + */ + public static int signedByteToUnsignedInt(final byte b) { + if (b >= 0) { + return b; + } + return 256 + b; + } + + /** + * Whether this library supports the encryption used by the given + * entry. + * + * @return true if the entry isn't encrypted at all + */ + private static boolean supportsEncryptionOf(final ZipArchiveEntry entry) { + return !entry.getGeneralPurposeBit().usesEncryption(); + } + + /** + * Whether this library supports the compression method used by + * the given entry. + * + * @return true if the compression method is supported + */ + private static boolean supportsMethodOf(final ZipArchiveEntry entry) { + return entry.getMethod() == ZipEntry.STORED + || entry.getMethod() == ZipMethod.UNSHRINKING.getCode() + || entry.getMethod() == ZipMethod.IMPLODING.getCode() + || entry.getMethod() == ZipEntry.DEFLATED + || entry.getMethod() == ZipMethod.ENHANCED_DEFLATED.getCode() + || entry.getMethod() == ZipMethod.BZIP2.getCode(); + } + + static void toDosTime(final Calendar c, final long t, final byte[] buf, final int offset) { + c.setTimeInMillis(t); + + final int year = c.get(Calendar.YEAR); + if (year < 1980) { + copy(DOS_TIME_MIN, buf, offset); // stop callers from changing the array + return; + } + final int month = c.get(Calendar.MONTH) + 1; + final long value = ((year - 1980) << 25) + | (month << 21) + | (c.get(Calendar.DAY_OF_MONTH) << 16) + | (c.get(Calendar.HOUR_OF_DAY) << 11) + | (c.get(Calendar.MINUTE) << 5) + | (c.get(Calendar.SECOND) >> 1); + ZipLong.putLong(value, buf, offset); + } + + + /** + * Convert a Date object to a DOS date/time field. + * @param time the {@code Date} to convert + * @return the date as a {@code ZipLong} + */ + public static ZipLong toDosTime(final Date time) { + return new ZipLong(toDosTime(time.getTime())); + } + + /** + * Convert a Date object to a DOS date/time field. + * + * <p>Stolen from InfoZip's {@code fileio.c}</p> + * @param t number of milliseconds since the epoch + * @return the date as a byte array + */ + public static byte[] toDosTime(final long t) { + final byte[] result = new byte[4]; + toDosTime(t, result, 0); + return result; + } + + /** + * Convert a Date object to a DOS date/time field. + * + * <p>Stolen from InfoZip's {@code fileio.c}</p> + * @param t number of milliseconds since the epoch + * @param buf the output buffer + * @param offset + * The offset within the output buffer of the first byte to be written. + * must be non-negative and no larger than {@code buf.length-4} + */ + public static void toDosTime(final long t, final byte[] buf, final int offset) { + toDosTime(Calendar.getInstance(), t, buf, offset); + } + + /** + * Converts an unsigned integer to a signed byte (e.g., 255 becomes -1). + * + * @param i integer to convert to byte + * @return byte representation of the provided int + * @throws IllegalArgumentException if the provided integer is not inside the range [0,255]. + * @since 1.5 + */ + public static byte unsignedIntToSignedByte(final int i) { + if (i > 255 || i < 0) { + throw new IllegalArgumentException("Can only convert non-negative integers between [0,255] to byte: [" + i + "]"); + } + if (i < 128) { + return (byte) i; + } + return (byte) (i - 256); + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/parallel/InputStreamSupplier.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/parallel/InputStreamSupplier.java new file mode 100644 index 0000000000..7d9f8f314b --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/parallel/InputStreamSupplier.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.readium.r2.shared.util.zip.compress.parallel; + +import java.io.InputStream; + +/** + * Supplies input streams. + * + * Implementations are required to support thread-handover. While an instance will + * not be accessed concurrently by multiple threads, it will be called by + * a different thread than it was created on. + * + * @since 1.10 + */ +public interface InputStreamSupplier { + + /** + * Supply an input stream for a resource. + * @return the input stream. Should never null, but may be an empty stream. + */ + InputStream get(); +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/parallel/ScatterGatherBackingStore.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/parallel/ScatterGatherBackingStore.java new file mode 100644 index 0000000000..5a5d7a6d7f --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/parallel/ScatterGatherBackingStore.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ +package org.readium.r2.shared.util.zip.compress.parallel; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; + +/** + * <p>Store intermediate payload in a scatter-gather scenario. + * Multiple threads write their payload to a backing store, which can + * subsequently be reversed to an {@link InputStream} to be used as input in the + * gather phase.</p> + * + * <p>It is the responsibility of the allocator of an instance of this class + * to close this. Closing it should clear off any allocated structures + * and preferably delete files.</p> + * + * @since 1.10 + */ +public interface ScatterGatherBackingStore extends Closeable { + + /** + * An input stream that contains the scattered payload + * + * @return An InputStream, should be closed by the caller of this method. + * @throws IOException when something fails + */ + InputStream getInputStream() throws IOException; + + /** + * Writes a piece of payload. + * + * @param data the data to write + * @param offset offset inside data to start writing from + * @param length the amount of data to write + * @throws IOException when something fails + */ + void writeOut(byte[] data, int offset, int length) throws IOException; + + /** + * Closes this backing store for further writing. + * @throws IOException when something fails + */ + void closeForWriting() throws IOException; +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/parallel/ScatterGatherBackingStoreSupplier.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/parallel/ScatterGatherBackingStoreSupplier.java new file mode 100644 index 0000000000..f9a3a5b528 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/parallel/ScatterGatherBackingStoreSupplier.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.readium.r2.shared.util.zip.compress.parallel; + +import java.io.IOException; + +/** + * Supplies {@link ScatterGatherBackingStore} instances. + * + * @since 1.10 + */ +public interface ScatterGatherBackingStoreSupplier { + /** + * Create a ScatterGatherBackingStore. + * + * @return a ScatterGatherBackingStore, not null + * @throws IOException when something fails + */ + ScatterGatherBackingStore get() throws IOException; +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/BoundedArchiveInputStream.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/BoundedArchiveInputStream.java new file mode 100644 index 0000000000..926df52ea2 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/BoundedArchiveInputStream.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * NIO backed bounded input stream for reading a predefined amount of data from. + * @ThreadSafe this base class is thread safe but implementations must not be. + * @since 1.21 + */ +public abstract class BoundedArchiveInputStream extends InputStream { + + private final long end; + private ByteBuffer singleByteBuffer; + private long loc; + + /** + * Create a new bounded input stream. + * + * @param start position in the stream from where the reading of this bounded stream starts. + * @param remaining amount of bytes which are allowed to read from the bounded stream. + */ + public BoundedArchiveInputStream(final long start, final long remaining) { + this.end = start + remaining; + if (this.end < start) { + // check for potential vulnerability due to overflow + throw new IllegalArgumentException("Invalid length of stream at offset=" + start + ", length=" + remaining); + } + loc = start; + } + + @Override + public synchronized int read() throws IOException { + if (loc >= end) { + return -1; + } + if (singleByteBuffer == null) { + singleByteBuffer = ByteBuffer.allocate(1); + } else { + singleByteBuffer.rewind(); + } + final int read = read(loc, singleByteBuffer); + if (read < 1) { + return -1; + } + loc++; + return singleByteBuffer.get() & 0xff; + } + + @Override + public synchronized int read(final byte[] b, final int off, final int len) throws IOException { + if (loc >= end) { + return -1; + } + final long maxLen = Math.min(len, end - loc); + if (maxLen <= 0) { + return 0; + } + if (off < 0 || off > b.length || maxLen > b.length - off) { + throw new IndexOutOfBoundsException("offset or len are out of bounds"); + } + + final ByteBuffer buf = ByteBuffer.wrap(b, off, (int) maxLen); + final int ret = read(loc, buf); + if (ret > 0) { + loc += ret; + } + return ret; + } + + /** + * Read content of the stream into a {@link ByteBuffer}. + * @param pos position to start the read. + * @param buf buffer to add the read content. + * @return number of read bytes. + * @throws IOException if I/O fails. + */ + protected abstract int read(long pos, ByteBuffer buf) throws IOException; +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/BoundedInputStream.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/BoundedInputStream.java new file mode 100644 index 0000000000..23bd67b0f5 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/BoundedInputStream.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ +package org.readium.r2.shared.util.zip.compress.utils; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A stream that limits reading from a wrapped stream to a given number of bytes. + * @NotThreadSafe + * @since 1.6 + */ +public class BoundedInputStream extends InputStream { + private final InputStream in; + private long bytesRemaining; + + /** + * Creates the stream that will at most read the given amount of + * bytes from the given stream. + * @param in the stream to read from + * @param size the maximum amount of bytes to read + */ + public BoundedInputStream(final InputStream in, final long size) { + this.in = in; + bytesRemaining = size; + } + + @Override + public void close() { + // there isn't anything to close in this stream and the nested + // stream is controlled externally + } + + /** + * @return bytes remaining to read + * @since 1.21 + */ + public long getBytesRemaining() { + return bytesRemaining; + } + + @Override + public int read() throws IOException { + if (bytesRemaining > 0) { + --bytesRemaining; + return in.read(); + } + return -1; + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } + if (bytesRemaining == 0) { + return -1; + } + int bytesToRead = len; + if (bytesToRead > bytesRemaining) { + bytesToRead = (int) bytesRemaining; + } + final int bytesRead = in.read(b, off, bytesToRead); + if (bytesRead >= 0) { + bytesRemaining -= bytesRead; + } + return bytesRead; + } + + /** + * @since 1.20 + */ + @Override + public long skip(final long n) throws IOException { + final long bytesToSkip = Math.min(bytesRemaining, n); + final long bytesSkipped = in.skip(bytesToSkip); + bytesRemaining -= bytesSkipped; + + return bytesSkipped; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/BoundedSeekableByteChannelInputStream.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/BoundedSeekableByteChannelInputStream.java new file mode 100644 index 0000000000..6fe865ee3c --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/BoundedSeekableByteChannelInputStream.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.utils; + +import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * InputStream that delegates requests to the underlying SeekableByteChannel, making sure that only bytes from a certain + * range can be read. + * @ThreadSafe + * @since 1.21 + */ +public class BoundedSeekableByteChannelInputStream extends BoundedArchiveInputStream { + + private final SeekableByteChannel channel; + + /** + * Create a bounded stream on the underlying {@link SeekableByteChannel} + * + * @param start Position in the stream from where the reading of this bounded stream starts + * @param remaining Amount of bytes which are allowed to read from the bounded stream + * @param channel Channel which the reads will be delegated to + */ + public BoundedSeekableByteChannelInputStream(final long start, final long remaining, + final SeekableByteChannel channel) { + super(start, remaining); + this.channel = channel; + } + + @Override + protected int read(final long pos, final ByteBuffer buf) throws IOException { + int read; + synchronized (channel) { + channel.position(pos); + read = channel.read(buf); + } + buf.flip(); + return read; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/ByteUtils.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/ByteUtils.java new file mode 100644 index 0000000000..8ef7152905 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/ByteUtils.java @@ -0,0 +1,269 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.utils; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Utility methods for reading and writing bytes. + * @since 1.14 + */ +public final class ByteUtils { + + /** + * Used to consume bytes. + * @since 1.14 + */ + public interface ByteConsumer { + /** + * The contract is similar to {@link OutputStream#write(int)}, + * consume the lower eight bytes of the int as a byte. + * @param b the byte to consume + * @throws IOException if consuming fails + */ + void accept(int b) throws IOException; + } + + /** + * Used to supply bytes. + * @since 1.14 + */ + public interface ByteSupplier { + /** + * The contract is similar to {@link InputStream#read()}, return + * the byte as an unsigned int, -1 if there are no more bytes. + * @return the supplied byte or -1 if there are no more bytes + * @throws IOException if supplying fails + */ + int getAsByte() throws IOException; + } + + /** + * {@link ByteSupplier} based on {@link InputStream}. + * @since 1.14 + */ + public static class InputStreamByteSupplier implements ByteSupplier { + private final InputStream is; + public InputStreamByteSupplier(final InputStream is) { + this.is = is; + } + @Override + public int getAsByte() throws IOException { + return is.read(); + } + } + + /** + * {@link ByteConsumer} based on {@link OutputStream}. + * @since 1.14 + */ + public static class OutputStreamByteConsumer implements ByteConsumer { + private final OutputStream os; + public OutputStreamByteConsumer(final OutputStream os) { + this.os = os; + } + @Override + public void accept(final int b) throws IOException { + os.write(b); + } + } + + /** + * Empty array. + * + * @since 1.21 + */ + public static final byte[] EMPTY_BYTE_ARRAY = {}; + + private static void checkReadLength(final int length) { + if (length > 8) { + throw new IllegalArgumentException("Can't read more than eight bytes into a long value"); + } + } + + /** + * Reads the given byte array as a little endian long. + * @param bytes the byte array to convert + * @return the number read + */ + public static long fromLittleEndian(final byte[] bytes) { + return fromLittleEndian(bytes, 0, bytes.length); + } + + /** + * Reads the given byte array as a little endian long. + * @param bytes the byte array to convert + * @param off the offset into the array that starts the value + * @param length the number of bytes representing the value + * @return the number read + * @throws IllegalArgumentException if len is bigger than eight + */ + public static long fromLittleEndian(final byte[] bytes, final int off, final int length) { + checkReadLength(length); + long l = 0; + for (int i = 0; i < length; i++) { + l |= (bytes[off + i] & 0xffL) << (8 * i); + } + return l; + } + + /** + * Reads the given number of bytes from the given supplier as a little endian long. + * + * <p>Typically used by our InputStreams that need to count the + * bytes read as well.</p> + * + * @param supplier the supplier for bytes + * @param length the number of bytes representing the value + * @return the number read + * @throws IllegalArgumentException if len is bigger than eight + * @throws IOException if the supplier fails or doesn't supply the + * given number of bytes anymore + */ + public static long fromLittleEndian(final ByteSupplier supplier, final int length) throws IOException { + checkReadLength(length); + long l = 0; + for (int i = 0; i < length; i++) { + final long b = supplier.getAsByte(); + if (b == -1) { + throw new IOException("Premature end of data"); + } + l |= (b << (i * 8)); + } + return l; + } + + /** + * Reads the given number of bytes from the given input as little endian long. + * @param in the input to read from + * @param length the number of bytes representing the value + * @return the number read + * @throws IllegalArgumentException if len is bigger than eight + * @throws IOException if reading fails or the stream doesn't + * contain the given number of bytes anymore + */ + public static long fromLittleEndian(final DataInput in, final int length) throws IOException { + // somewhat duplicates the ByteSupplier version in order to save the creation of a wrapper object + checkReadLength(length); + long l = 0; + for (int i = 0; i < length; i++) { + final long b = in.readUnsignedByte(); + l |= (b << (i * 8)); + } + return l; + } + + /** + * Reads the given number of bytes from the given stream as a little endian long. + * @param in the stream to read from + * @param length the number of bytes representing the value + * @return the number read + * @throws IllegalArgumentException if len is bigger than eight + * @throws IOException if reading fails or the stream doesn't + * contain the given number of bytes anymore + */ + public static long fromLittleEndian(final InputStream in, final int length) throws IOException { + // somewhat duplicates the ByteSupplier version in order to save the creation of a wrapper object + checkReadLength(length); + long l = 0; + for (int i = 0; i < length; i++) { + final long b = in.read(); + if (b == -1) { + throw new IOException("Premature end of data"); + } + l |= (b << (i * 8)); + } + return l; + } + + /** + * Inserts the given value into the array as a little endian + * sequence of the given length starting at the given offset. + * @param b the array to write into + * @param value the value to insert + * @param off the offset into the array that receives the first byte + * @param length the number of bytes to use to represent the value + */ + public static void toLittleEndian(final byte[] b, final long value, final int off, final int length) { + long num = value; + for (int i = 0; i < length; i++) { + b[off + i] = (byte) (num & 0xff); + num >>= 8; + } + } + + /** + * Provides the given value to the given consumer as a little endian + * sequence of the given length. + * @param consumer the consumer to provide the bytes to + * @param value the value to provide + * @param length the number of bytes to use to represent the value + * @throws IOException if writing fails + */ + public static void toLittleEndian(final ByteConsumer consumer, final long value, final int length) + throws IOException { + long num = value; + for (int i = 0; i < length; i++) { + consumer.accept((int) (num & 0xff)); + num >>= 8; + } + } + + /** + * Writes the given value to the given stream as a little endian + * array of the given length. + * @param out the output to write to + * @param value the value to write + * @param length the number of bytes to use to represent the value + * @throws IOException if writing fails + */ + public static void toLittleEndian(final DataOutput out, final long value, final int length) + throws IOException { + // somewhat duplicates the ByteConsumer version in order to save the creation of a wrapper object + long num = value; + for (int i = 0; i < length; i++) { + out.write((int) (num & 0xff)); + num >>= 8; + } + } + + /** + * Writes the given value to the given stream as a little endian + * array of the given length. + * @param out the stream to write to + * @param value the value to write + * @param length the number of bytes to use to represent the value + * @throws IOException if writing fails + */ + public static void toLittleEndian(final OutputStream out, final long value, final int length) + throws IOException { + // somewhat duplicates the ByteConsumer version in order to save the creation of a wrapper object + long num = value; + for (int i = 0; i < length; i++) { + out.write((int) (num & 0xff)); + num >>= 8; + } + } + + private ByteUtils() { /* no instances */ } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/CountingInputStream.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/CountingInputStream.java new file mode 100644 index 0000000000..016f5f7c43 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/CountingInputStream.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.readium.r2.shared.util.zip.compress.utils; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Input stream that tracks the number of bytes read. + * @since 1.3 + * @NotThreadSafe + */ +public class CountingInputStream extends FilterInputStream { + private long bytesRead; + + public CountingInputStream(final InputStream in) { + super(in); + } + + /** + * Increments the counter of already read bytes. + * Doesn't increment if the EOF has been hit (read == -1) + * + * @param read the number of bytes read + */ + protected final void count(final long read) { + if (read != -1) { + bytesRead += read; + } + } + + /** + * Returns the current number of bytes read from this stream. + * @return the number of read bytes + */ + public long getBytesRead() { + return bytesRead; + } + + @Override + public int read() throws IOException { + final int r = in.read(); + if (r >= 0) { + count(1); + } + return r; + } + + @Override + public int read(final byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } + final int r = in.read(b, off, len); + if (r >= 0) { + count(r); + } + return r; + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/FileNameUtils.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/FileNameUtils.java new file mode 100644 index 0000000000..27dd488ecf --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/FileNameUtils.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.utils; + +import java.io.File; + +/** + * Generic file name utilities. + * @since 1.20 + */ +public class FileNameUtils { + + /** + * Returns the extension (i.e. the part after the last ".") of a file. + * + * <p>Will return an empty string if the file name doesn't contain + * any dots. Only the last segment of a the file name is consulted + * - i.e. all leading directories of the {@code filename} + * parameter are skipped.</p> + * + * @return the extension of filename + * @param filename the name of the file to obtain the extension of. + */ + public static String getExtension(final String filename) { + if (filename == null) { + return null; + } + + final String name = new File(filename).getName(); + final int extensionPosition = name.lastIndexOf('.'); + if (extensionPosition < 0) { + return ""; + } + return name.substring(extensionPosition + 1); + } + + /** + * Returns the basename (i.e. the part up to and not including the + * last ".") of the last path segment of a filename. + * + * <p>Will return the file name itself if it doesn't contain any + * dots. All leading directories of the {@code filename} parameter + * are skipped.</p> + * + * @return the basename of filename + * @param filename the name of the file to obtain the basename of. + */ + public static String getBaseName(final String filename) { + if (filename == null) { + return null; + } + + final String name = new File(filename).getName(); + + final int extensionPosition = name.lastIndexOf('.'); + if (extensionPosition < 0) { + return name; + } + + return name.substring(0, extensionPosition); + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/IOUtils.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/IOUtils.java new file mode 100644 index 0000000000..a27ae59897 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/IOUtils.java @@ -0,0 +1,348 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.readium.r2.shared.util.zip.compress.utils; + +import org.readium.r2.shared.util.zip.jvm.ReadableByteChannel; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * Utility functions + * @Immutable (has mutable data but it is write-only) + */ +public final class IOUtils { + + private static final int COPY_BUF_SIZE = 8024; + private static final int SKIP_BUF_SIZE = 4096; + + // This buffer does not need to be synchronized because it is write only; the contents are ignored + // Does not affect Immutability + private static final byte[] SKIP_BUF = new byte[SKIP_BUF_SIZE]; + + /** + * Closes the given Closeable and swallows any IOException that may occur. + * @param c Closeable to close, can be null + * @since 1.7 + */ + public static void closeQuietly(final Closeable c) { + if (c != null) { + try { + c.close(); + } catch (final IOException ignored) { // NOPMD NOSONAR + } + } + } + + /** + * Copies the content of a InputStream into an OutputStream. + * Uses a default buffer size of 8024 bytes. + * + * @param input + * the InputStream to copy + * @param output + * the target, may be null to simulate output to dev/null on Linux and NUL on Windows + * @return the number of bytes copied + * @throws IOException + * if an error occurs + */ + public static long copy(final InputStream input, final OutputStream output) throws IOException { + return copy(input, output, COPY_BUF_SIZE); + } + + /** + * Copies the content of a InputStream into an OutputStream + * + * @param input + * the InputStream to copy + * @param output + * the target, may be null to simulate output to dev/null on Linux and NUL on Windows + * @param buffersize + * the buffer size to use, must be bigger than 0 + * @return the number of bytes copied + * @throws IOException + * if an error occurs + * @throws IllegalArgumentException + * if buffersize is smaller than or equal to 0 + */ + public static long copy(final InputStream input, final OutputStream output, final int buffersize) throws IOException { + if (buffersize < 1) { + throw new IllegalArgumentException("buffersize must be bigger than 0"); + } + final byte[] buffer = new byte[buffersize]; + int n = 0; + long count=0; + while (-1 != (n = input.read(buffer))) { + if (output != null) { + output.write(buffer, 0, n); + } + count += n; + } + return count; + } + + /** + * Copies part of the content of a InputStream into an OutputStream. + * Uses a default buffer size of 8024 bytes. + * + * @param input + * the InputStream to copy + * @param output + * the target Stream + * @param len + * maximum amount of bytes to copy + * @return the number of bytes copied + * @throws IOException + * if an error occurs + * @since 1.21 + */ + public static long copyRange(final InputStream input, final long len, final OutputStream output) + throws IOException { + return copyRange(input, len, output, COPY_BUF_SIZE); + } + + /** + * Copies part of the content of a InputStream into an OutputStream + * + * @param input + * the InputStream to copy + * @param len + * maximum amount of bytes to copy + * @param output + * the target, may be null to simulate output to dev/null on Linux and NUL on Windows + * @param buffersize + * the buffer size to use, must be bigger than 0 + * @return the number of bytes copied + * @throws IOException + * if an error occurs + * @throws IllegalArgumentException + * if buffersize is smaller than or equal to 0 + * @since 1.21 + */ + public static long copyRange(final InputStream input, final long len, final OutputStream output, + final int buffersize) throws IOException { + if (buffersize < 1) { + throw new IllegalArgumentException("buffersize must be bigger than 0"); + } + final byte[] buffer = new byte[(int) Math.min(buffersize, len)]; + int n = 0; + long count = 0; + while (count < len && -1 != (n = input.read(buffer, 0, (int) Math.min(len - count, buffer.length)))) { + if (output != null) { + output.write(buffer, 0, n); + } + count += n; + } + return count; + } + + /** + * Reads as much from input as possible to fill the given array. + * + * <p>This method may invoke read repeatedly to fill the array and + * only read less bytes than the length of the array if the end of + * the stream has been reached.</p> + * + * @param input stream to read from + * @param array buffer to fill + * @return the number of bytes actually read + * @throws IOException on error + */ + public static int readFully(final InputStream input, final byte[] array) throws IOException { + return readFully(input, array, 0, array.length); + } + + // toByteArray(InputStream) copied from: + // commons/proper/io/trunk/src/main/java/org/apache/commons/io/IOUtils.java?revision=1428941 + // January 8th, 2013 + // + // Assuming our copy() works just as well as theirs! :-) + + /** + * Reads as much from input as possible to fill the given array + * with the given amount of bytes. + * + * <p>This method may invoke read repeatedly to read the bytes and + * only read less bytes than the requested length if the end of + * the stream has been reached.</p> + * + * @param input stream to read from + * @param array buffer to fill + * @param offset offset into the buffer to start filling at + * @param len of bytes to read + * @return the number of bytes actually read + * @throws IOException + * if an I/O error has occurred + */ + public static int readFully(final InputStream input, final byte[] array, final int offset, final int len) + throws IOException { + if (len < 0 || offset < 0 || len + offset > array.length || len + offset < 0) { + throw new IndexOutOfBoundsException(); + } + int count = 0, x = 0; + while (count != len) { + x = input.read(array, offset + count, len - count); + if (x == -1) { + break; + } + count += x; + } + return count; + } + + /** + * Reads {@code b.remaining()} bytes from the given channel + * starting at the current channel's position. + * + * <p>This method reads repeatedly from the channel until the + * requested number of bytes are read. This method blocks until + * the requested number of bytes are read, the end of the channel + * is detected, or an exception is thrown.</p> + * + * @param channel the channel to read from + * @param byteBuffer the buffer into which the data is read. + * @throws IOException - if an I/O error occurs. + * @throws EOFException - if the channel reaches the end before reading all the bytes. + */ + public static void readFully(final ReadableByteChannel channel, final ByteBuffer byteBuffer) throws IOException { + final int expectedLength = byteBuffer.remaining(); + int read = 0; + while (read < expectedLength) { + final int readNow = channel.read(byteBuffer); + if (readNow <= 0) { + break; + } + read += readNow; + } + if (read < expectedLength) { + throw new EOFException(); + } + } + + /** + * Gets part of the contents of an {@code InputStream} as a {@code byte[]}. + * + * @param input the {@code InputStream} to read from + * @param len + * maximum amount of bytes to copy + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 1.21 + */ + public static byte[] readRange(final InputStream input, final int len) throws IOException { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + copyRange(input, len, output); + return output.toByteArray(); + } + + /** + * Gets part of the contents of an {@code ReadableByteChannel} as a {@code byte[]}. + * + * @param input the {@code ReadableByteChannel} to read from + * @param len + * maximum amount of bytes to copy + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 1.21 + */ + public static byte[] readRange(final ReadableByteChannel input, final int len) throws IOException { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + final ByteBuffer b = ByteBuffer.allocate(Math.min(len, COPY_BUF_SIZE)); + int read = 0; + while (read < len) { + // Make sure we never read more than len bytes + b.limit(Math.min(len - read, b.capacity())); + final int readNow = input.read(b); + if (readNow <= 0) { + break; + } + output.write(b.array(), 0, readNow); + b.rewind(); + read += readNow; + } + return output.toByteArray(); + } + + /** + * Skips the given number of bytes by repeatedly invoking skip on + * the given input stream if necessary. + * + * <p>In a case where the stream's skip() method returns 0 before + * the requested number of bytes has been skip this implementation + * will fall back to using the read() method.</p> + * + * <p>This method will only skip less than the requested number of + * bytes if the end of the input stream has been reached.</p> + * + * @param input stream to skip bytes in + * @param numToSkip the number of bytes to skip + * @return the number of bytes actually skipped + * @throws IOException on error + */ + public static long skip(final InputStream input, long numToSkip) throws IOException { + final long available = numToSkip; + while (numToSkip > 0) { + final long skipped = input.skip(numToSkip); + if (skipped == 0) { + break; + } + numToSkip -= skipped; + } + + while (numToSkip > 0) { + final int read = readFully(input, SKIP_BUF, 0, + (int) Math.min(numToSkip, SKIP_BUF_SIZE)); + if (read < 1) { + break; + } + numToSkip -= read; + } + return available - numToSkip; + } + + /** + * Gets the contents of an {@code InputStream} as a {@code byte[]}. + * <p> + * This method buffers the input internally, so there is no need to use a + * {@code BufferedInputStream}. + * + * @param input the {@code InputStream} to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 1.5 + */ + public static byte[] toByteArray(final InputStream input) throws IOException { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output); + return output.toByteArray(); + } + + /** Private constructor to prevent instantiation of this utility class. */ + private IOUtils(){ + } + +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/InputStreamStatistics.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/InputStreamStatistics.java new file mode 100644 index 0000000000..7edce418f2 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/InputStreamStatistics.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.utils; + +/** + * This interface provides statistics on the current decompression stream. + * The stream consumer can use that statistics to handle abnormal + * compression ratios, i.e. to prevent ZIP bombs. + * + * @since 1.17 + */ +public interface InputStreamStatistics { + /** + * @return the amount of raw or compressed bytes read by the stream + */ + long getCompressedCount(); + + /** + * @return the amount of decompressed bytes returned by the stream + */ + long getUncompressedCount(); +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/MultiReadOnlySeekableByteChannel.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/MultiReadOnlySeekableByteChannel.java new file mode 100644 index 0000000000..e45137892b --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/compress/utils/MultiReadOnlySeekableByteChannel.java @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.readium.r2.shared.util.zip.compress.utils; + +import org.readium.r2.shared.util.zip.jvm.ClosedChannelException; +import org.readium.r2.shared.util.zip.jvm.NonWritableChannelException; +import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Read-Only Implementation of {@link SeekableByteChannel} that + * concatenates a collection of other {@link SeekableByteChannel}s. + * + * <p>This is a lose port of <a + * href="https://github.com/frugalmechanic/fm-common/blob/master/jvm/src/main/scala/fm/common/MultiReadOnlySeekableByteChannel.scala">MultiReadOnlySeekableByteChannel</a> + * by Tim Underwood.</p> + * + * @since 1.19 + */ +public class MultiReadOnlySeekableByteChannel implements SeekableByteChannel { + + private static final Path[] EMPTY_PATH_ARRAY = {}; + + /** + * Concatenates the given channels. + * + * @param channels the channels to concatenate + * @throws NullPointerException if channels is null + * @return SeekableByteChannel that concatenates all provided channels + */ + public static SeekableByteChannel forSeekableByteChannels(final SeekableByteChannel... channels) { + if (Objects.requireNonNull(channels, "channels must not be null").length == 1) { + return channels[0]; + } + return new MultiReadOnlySeekableByteChannel(Arrays.asList(channels)); + } + + private final List<SeekableByteChannel> channels; + + private long globalPosition; + + private int currentChannelIdx; + + /** + * Concatenates the given channels. + * + * @param channels the channels to concatenate + * @throws NullPointerException if channels is null + */ + public MultiReadOnlySeekableByteChannel(final List<SeekableByteChannel> channels) { + this.channels = Collections.unmodifiableList(new ArrayList<>( + Objects.requireNonNull(channels, "channels must not be null"))); + } + + @Override + public void close() throws IOException { + IOException first = null; + for (final SeekableByteChannel ch : channels) { + try { + ch.close(); + } catch (final IOException ex) { + if (first == null) { + first = ex; + } + } + } + if (first != null) { + throw new IOException("failed to close wrapped channel", first); + } + } + + @Override + public boolean isOpen() { + for (SeekableByteChannel channel: channels) { + if (!channel.isOpen()) { + return false; + } + } + + return true; + } + + /** + * Returns this channel's position. + * + * <p>This method violates the contract of {@link SeekableByteChannel#position()} as it will not throw any exception + * when invoked on a closed channel. Instead it will return the position the channel had when close has been + * called.</p> + */ + @Override + public long position() { + return globalPosition; + } + + @Override + public synchronized SeekableByteChannel position(final long newPosition) throws IOException { + if (newPosition < 0) { + throw new IllegalArgumentException("Negative position: " + newPosition); + } + if (!isOpen()) { + throw new ClosedChannelException(); + } + + globalPosition = newPosition; + + long pos = newPosition; + + for (int i = 0; i < channels.size(); i++) { + final SeekableByteChannel currentChannel = channels.get(i); + final long size = currentChannel.size(); + + final long newChannelPos; + if (pos == -1L) { + // Position is already set for the correct channel, + // the rest of the channels get reset to 0 + newChannelPos = 0; + } else if (pos <= size) { + // This channel is where we want to be + currentChannelIdx = i; + final long tmp = pos; + pos = -1L; // Mark pos as already being set + newChannelPos = tmp; + } else { + // newPosition is past this channel. Set channel + // position to the end and substract channel size from + // pos + pos -= size; + newChannelPos = size; + } + + currentChannel.position(newChannelPos); + } + return this; + } + + /** + * set the position based on the given channel number and relative offset + * + * @param channelNumber the channel number + * @param relativeOffset the relative offset in the corresponding channel + * @return global position of all channels as if they are a single channel + * @throws IOException if positioning fails + */ + public synchronized SeekableByteChannel position(final long channelNumber, final long relativeOffset) throws IOException { + if (!isOpen()) { + throw new ClosedChannelException(); + } + long globalPosition = relativeOffset; + for (int i = 0; i < channelNumber; i++) { + globalPosition += channels.get(i).size(); + } + + return position(globalPosition); + } + + @Override + public synchronized int read(final ByteBuffer dst) throws IOException { + if (!isOpen()) { + throw new ClosedChannelException(); + } + if (!dst.hasRemaining()) { + return 0; + } + + int totalBytesRead = 0; + while (dst.hasRemaining() && currentChannelIdx < channels.size()) { + final SeekableByteChannel currentChannel = channels.get(currentChannelIdx); + final int newBytesRead = currentChannel.read(dst); + if (newBytesRead == -1) { + // EOF for this channel -- advance to next channel idx + currentChannelIdx += 1; + continue; + } + if (currentChannel.position() >= currentChannel.size()) { + // we are at the end of the current channel + currentChannelIdx++; + } + totalBytesRead += newBytesRead; + } + if (totalBytesRead > 0) { + globalPosition += totalBytesRead; + return totalBytesRead; + } + return -1; + } + + @Override + public long size() throws IOException { + if (!isOpen()) { + throw new ClosedChannelException(); + } + long acc = 0; + for (final SeekableByteChannel ch : channels) { + acc += ch.size(); + } + return acc; + } + + /** + * @throws NonWritableChannelException since this implementation is read-only. + */ + @Override + public SeekableByteChannel truncate(final long size) { + throw new NonWritableChannelException(); + } + + /** + * @throws NonWritableChannelException since this implementation is read-only. + */ + @Override + public int write(final ByteBuffer src) { + throw new NonWritableChannelException(); + } + +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/AsynchronousCloseException.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/AsynchronousCloseException.java new file mode 100644 index 0000000000..17ea7d854b --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/AsynchronousCloseException.java @@ -0,0 +1,14 @@ +package org.readium.r2.shared.util.zip.jvm; + +/** + * An {@code AsynchronousCloseException} is thrown when the underlying channel + * for an I/O operation is closed by another thread. + */ +public class AsynchronousCloseException extends ClosedChannelException { + private static final long serialVersionUID = 6891178312432313966L; + /** + * Constructs an {@code AsynchronousCloseException}. + */ + public AsynchronousCloseException() { + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/ByteChannel.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/ByteChannel.java new file mode 100644 index 0000000000..f55229b0ae --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/ByteChannel.java @@ -0,0 +1,13 @@ +package org.readium.r2.shared.util.zip.jvm; +/** + * A ByteChannel is both readable and writable. + * <p> + * The methods for the byte channel are precisely those defined by readable and + * writable byte channels. + * + * @see ReadableByteChannel + * @see WritableByteChannel + */ +public interface ByteChannel extends ReadableByteChannel, WritableByteChannel { + // No methods defined. +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/Channel.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/Channel.java new file mode 100644 index 0000000000..0e6ec5dddf --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/Channel.java @@ -0,0 +1,39 @@ +package org.readium.r2.shared.util.zip.jvm; + +import java.io.Closeable; +import java.io.IOException; + +/** + * A channel is a conduit to I/O services covering such items as files, sockets, + * hardware devices, I/O ports or some software component. + * <p> + * Channels are open upon creation, and can be closed explicitly. Once a channel + * is closed it cannot be re-opened, and any attempts to perform I/O operations + * on the closed channel result in a <code>ClosedChannelException</code>. + * <p> + * Particular implementations or sub-interfaces of {@code Channel} dictate + * whether they are thread-safe or not. + */ +public interface Channel extends Closeable { + /** + * Returns true if this channel is open. + */ + public boolean isOpen(); + /** + * Closes an open channel. If the channel is already closed then this method + * has no effect. If there is a problem with closing the channel then the + * method throws an IOException and the exception contains reasons for the + * failure. + * <p> + * If an attempt is made to perform an operation on a closed channel then a + * {@link ClosedChannelException} will be thrown on that attempt. + * <p> + * If multiple threads attempt to simultaneously close a channel, then only + * one thread will run the closure code, and others will be blocked until + * the first returns. + * + * @throws IOException + * if a problem occurs closing the channel. + */ + public void close() throws IOException; +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/ClosedByInterruptException.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/ClosedByInterruptException.java new file mode 100644 index 0000000000..d15231ba52 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/ClosedByInterruptException.java @@ -0,0 +1,18 @@ +package org.readium.r2.shared.util.zip.jvm; + +/** + * A {@code ClosedByInterruptException} is thrown when a thread is interrupted + * in a blocking I/O operation. + * <p> + * When the thread is interrupted by a call to {@code interrupt()}, it closes + * the channel, sets the interrupt status of the thread to {@code true} and + * throws a {@code ClosedByInterruptException}. + */ +public class ClosedByInterruptException extends AsynchronousCloseException { + private static final long serialVersionUID = -4488191543534286750L; + /** + * Constructs a {@code ClosedByInterruptException}. + */ + public ClosedByInterruptException() { + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/ClosedChannelException.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/ClosedChannelException.java new file mode 100644 index 0000000000..7a57fca4a4 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/ClosedChannelException.java @@ -0,0 +1,15 @@ +package org.readium.r2.shared.util.zip.jvm; + +import java.io.IOException; +/** + * A {@code ClosedChannelException} is thrown when a channel is closed for the + * type of operation attempted. + */ +public class ClosedChannelException extends IOException { + private static final long serialVersionUID = 882777185433553857L; + /** + * Constructs a {@code ClosedChannelException}. + */ + public ClosedChannelException() { + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/NonReadableChannelException.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/NonReadableChannelException.java new file mode 100644 index 0000000000..52576227bf --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/NonReadableChannelException.java @@ -0,0 +1,14 @@ +package org.readium.r2.shared.util.zip.jvm; + +/** + * A {@code NonReadableChannelException} is thrown when attempting to read from + * a channel that is not open for reading. + */ +public class NonReadableChannelException extends IllegalStateException { + private static final long serialVersionUID = -3200915679294993514L; + /** + * Constructs a {@code NonReadableChannelException}. + */ + public NonReadableChannelException() { + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/NonWritableChannelException.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/NonWritableChannelException.java new file mode 100644 index 0000000000..bc3a43bc57 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/NonWritableChannelException.java @@ -0,0 +1,14 @@ +package org.readium.r2.shared.util.zip.jvm; + +/** + * A {@code NonWritableChannelException} is thrown when attempting to write to a + * channel that is not open for writing. + */ +public class NonWritableChannelException extends IllegalStateException { + private static final long serialVersionUID = -7071230488279011621L; + /** + * Constructs a {@code NonWritableChannelException}. + */ + public NonWritableChannelException() { + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/ReadableByteChannel.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/ReadableByteChannel.java new file mode 100644 index 0000000000..476652dd88 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/ReadableByteChannel.java @@ -0,0 +1,48 @@ +package org.readium.r2.shared.util.zip.jvm; + +import java.io.IOException; +import java.nio.ByteBuffer; +/** + * A {@code ReadableByteChannel} is a type of {@link Channel} that can read + * bytes. + * <p> + * Read operations are synchronous on a {@code ReadableByteChannel}, that is, + * if a read is already in progress on the channel then subsequent reads will + * block until the first read completes. It is undefined whether non-read + * operations will block. + */ +public interface ReadableByteChannel extends Channel { + /** + * Reads bytes from the channel into the given buffer. + * <p> + * The maximum number of bytes that will be read is the + * {@link java.nio.Buffer#remaining() remaining} number of bytes in the + * buffer when the method is invoked. The bytes will be read into the buffer + * starting at the buffer's current + * {@link java.nio.Buffer#position() position}. + * <p> + * The call may block if other threads are also attempting to read from the + * same channel. + * <p> + * Upon completion, the buffer's {@code position} is updated to the end of + * the bytes that were read. The buffer's + * {@link java.nio.Buffer#limit() limit} is not changed. + * + * @param buffer + * the byte buffer to receive the bytes. + * @return the number of bytes actually read. + * @throws AsynchronousCloseException + * if another thread closes the channel during the read. + * @throws ClosedByInterruptException + * if another thread interrupts the calling thread while the + * operation is in progress. The interrupt state of the calling + * thread is set and the channel is closed. + * @throws ClosedChannelException + * if the channel is closed. + * @throws IOException + * another I/O error occurs, details are in the message. + * @throws NonReadableChannelException + * if the channel was not opened for reading. + */ + public int read(ByteBuffer buffer) throws IOException; +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/SeekableByteChannel.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/SeekableByteChannel.java new file mode 100644 index 0000000000..64a5ceac4b --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/SeekableByteChannel.java @@ -0,0 +1,139 @@ +package org.readium.r2.shared.util.zip.jvm; + +import java.io.IOException; +import java.nio.ByteBuffer; +/** + * An interface for channels that keep a pointer to a current position within an underlying + * byte-based data source such as a file. + * + * <p>SeekableByteChannels have a pointer into the underlying data source which is referred to as a + * <em>position</em>. The position can be manipulated by moving it within the data source, and the + * current position can be queried. + * + * <p>SeekableByteChannels also have an associated <em>size</em>. The size of the channel is the + * number of bytes that the data source currently contains. The size of the data source can be + * manipulated by adding more bytes to the end or by removing bytes from the end. See + * {@link #truncate}, {@link #position} and {@link #write} for details. The current size can also + * be queried. + * + * @hide Until ready for a public API change + * @since 1.7 + */ +public interface SeekableByteChannel extends ByteChannel { + /** + * Returns the current position as a positive number of bytes from the start of the underlying + * data source. + * + * @throws ClosedChannelException + * if this channel is closed. + * @throws IOException + * if another I/O error occurs. + */ + long position() throws IOException; + /** + * Sets the channel's position to {@code newPosition}. + * + * <p>The argument is the number of bytes counted from the start of the data source. The position + * cannot be set to a value that is negative. The new position can be set beyond the current + * size. If set beyond the current size, attempts to read will return end-of-file. Write + * operations will succeed but they will fill the bytes between the current end of the data + * source + * and the new position with the required number of (unspecified) byte values. + * + * @return the channel. + * @throws IllegalArgumentException + * if the new position is negative. + * @throws ClosedChannelException + * if this channel is closed. + * @throws IOException + * if another I/O error occurs. + */ + SeekableByteChannel position(long newPosition) throws IOException; + /** + * Returns the size of the data source underlying this channel in bytes. + * + * @throws ClosedChannelException + * if this channel is closed. + * @throws IOException + * if an I/O error occurs. + */ + long size() throws IOException; + /** + * Truncates the data source underlying this channel to a given size. Any bytes beyond the given + * size are removed. If there are no bytes beyond the given size then the contents are + * unmodified. + * + * <p>If the position is currently greater than the given size, then it is set to the new size. + * + * @return this channel. + * @throws IllegalArgumentException + * if the requested size is negative. + * @throws ClosedChannelException + * if this channel is closed. + * @throws NonWritableChannelException + * if the channel cannot be written to. + * @throws IOException + * if another I/O error occurs. + */ + SeekableByteChannel truncate(long size) throws IOException; + /** + * Writes bytes from the given byte buffer to this channel. + * + * <p>The bytes are written starting at the channel's current position, and after some number of + * bytes are written (up to the {@link java.nio.Buffer#remaining() remaining} number of bytes in + * the buffer) the channel's position is increased by the number of bytes actually written. + * + * <p>If the channel's position is beyond the current end of the underlying data source, then the + * data source is first extended up to the given position by the required number of unspecified + * byte values. + * + * @param buffer + * the byte buffer containing the bytes to be written. + * @return the number of bytes actually written. + * @throws NonWritableChannelException + * if the channel was not opened for writing. + * @throws ClosedChannelException + * if the channel was already closed. + * @throws AsynchronousCloseException + * if another thread closes the channel during the write. + * @throws ClosedByInterruptException + * if another thread interrupts the calling thread while this operation is in progress. The + * interrupt state of the calling thread is set and the channel is closed. + * @throws IOException + * if another I/O error occurs, details are in the message. + */ + @Override + int write(ByteBuffer buffer) throws IOException; + /** + * Reads bytes from this channel into the given buffer. + * + * <p>If the channels position is beyond the current end of the underlying data source then + * end-of-file (-1) is returned. + * + * <p>The bytes are read starting at the channel's current position, and after some number of + * bytes are read (up to the {@link java.nio.Buffer#remaining() remaining} number of bytes in the + * buffer) the channel's position is increased by the number of bytes actually read. The bytes + * will be read into the buffer starting at the buffer's current + * {@link java.nio.Buffer#position() position}. The buffer's {@link java.nio.Buffer#limit() + * limit} is not changed. + * + * <p>The call may block if other threads are also attempting to read from the same channel. + * + * @param buffer + * the byte buffer to receive the bytes. + * @return the number of bytes actually read, or -1 if the end of the data has been reached + * @throws AsynchronousCloseException + * if another thread closes the channel during the read. + * @throws ClosedByInterruptException + * if another thread interrupts the calling thread while the operation is in progress. The + * interrupt state of the calling thread is set and the channel is closed. + * @throws ClosedChannelException + * if the channel is closed. + * @throws IOException + * another I/O error occurs, details are in the message. + * @throws NonReadableChannelException + * if the channel was not opened for reading. + */ + @Override + int read(ByteBuffer buffer) throws IOException; +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/WritableByteChannel.java b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/WritableByteChannel.java new file mode 100644 index 0000000000..53c07f5679 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/jvm/WritableByteChannel.java @@ -0,0 +1,46 @@ +package org.readium.r2.shared.util.zip.jvm; + +import java.io.IOException; +import java.nio.ByteBuffer; +/** + * A {@code WritableByteChannel} is a type of {@link Channel} that can write + * bytes. + * <p> + * Write operations are synchronous on a {@code WritableByteChannel}, that is, + * if a write is already in progress on the channel then subsequent writes will + * block until the first write completes. It is undefined whether non-write + * operations will block. + */ +public interface WritableByteChannel extends Channel { + /** + * Writes bytes from the given buffer to the channel. + * <p> + * The maximum number of bytes that will be written is the + * <code>remaining()</code> number of bytes in the buffer when the method + * invoked. The bytes will be written from the buffer starting at the + * buffer's <code>position</code>. + * <p> + * The call may block if other threads are also attempting to write on the + * same channel. + * <p> + * Upon completion, the buffer's <code>position()</code> is updated to the + * end of the bytes that were written. The buffer's <code>limit()</code> + * is unmodified. + * + * @param buffer + * the byte buffer containing the bytes to be written. + * @return the number of bytes actually written. + * @throws NonWritableChannelException + * if the channel was not opened for writing. + * @throws ClosedChannelException + * if the channel was already closed. + * @throws AsynchronousCloseException + * if another thread closes the channel during the write. + * @throws ClosedByInterruptException + * if another thread interrupt the calling thread during the + * write. + * @throws IOException + * another IO exception occurs, details are in the message. + */ + public int write(ByteBuffer buffer) throws IOException; +} diff --git a/readium/shared/src/main/res/values/strings.xml b/readium/shared/src/main/res/values/strings.xml deleted file mode 100644 index e838cb7b01..0000000000 --- a/readium/shared/src/main/res/values/strings.xml +++ /dev/null @@ -1,44 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright 2020 Readium Foundation. All rights reserved. - Use of this source code is governed by the BSD-style license - available in the top-level LICENSE file of the project. ---> - -<resources> - <string name="r2.shared.publication.opening_exception.unsupported_format">Format not supported</string> - <string name="r2.shared.publication.opening_exception.not_found">File not found</string> - <string name="r2.shared.publication.opening_exception.parsing_failed">The file is corrupted and can\'t be opened</string> - <string name="r2.shared.publication.opening_exception.forbidden">You are not allowed to open this publication</string> - <string name="r2.shared.publication.opening_exception.unavailable">Not available, please try again later</string> - <string name="r2.shared.publication.opening_exception.incorrect_credentials">Provided credentials were incorrect</string> - - <string name="r2.shared.publication.content_protection.exception.not_supported">This publication cannot be opened because it is protected with %1$s</string> - <string name="r2.shared.publication.content_protection.exception.not_supported.unknown">This publication cannot be opened because it is protected with an unknown DRM</string> - - <string name="r2.shared.resource.exception.bad_request">Invalid request which can\'t be processed</string> - <string name="r2.shared.resource.exception.not_found">Resource not found</string> - <string name="r2.shared.resource.exception.forbidden">You are not allowed to access the resource</string> - <string name="r2.shared.resource.exception.unavailable">The resource is currently unavailable, please try again later</string> - <string name="r2_shared_resource_exception_offline">The Internet connection appears to be offline</string> - <string name="r2.shared.resource.exception.out_of_memory">The resource is too large to be read on this device</string> - <string name="r2.shared.resource.exception.cancelled">The request was cancelled</string> - <string name="r2.shared.resource.exception.other">A service error occurred</string> - - <string name="r2.shared.http.exception.malformed_request">The provided request was not valid</string> - <string name="r2.shared.http.exception.malformed_response">The received response could not be decoded</string> - <string name="r2.shared.http.exception.timeout">Request timed out</string> - <string name="r2.shared.http.exception.bad_request">The provided request was not valid</string> - <string name="r2.shared.http.exception.unauthorized">Authentication required</string> - <string name="r2.shared.http.exception.forbidden">You are not authorized</string> - <string name="r2.shared.http.exception.not_found">Page not found</string> - <string name="r2.shared.http.exception.client_error">A client error occurred</string> - <string name="r2.shared.http.exception.server_error">A server error occurred, please try again later</string> - <string name="r2.shared.http.exception.offline">Your Internet connection appears to be offline</string> - <string name="r2.shared.http.exception.cancelled">The request was cancelled</string> - <string name="r2.shared.http.exception.other">A networking error occurred</string> - - <string name="r2.shared.search.exception.publication_not_searchable">This publication is not searchable</string> - <string name="r2.shared.search.exception.cancelled">The search was cancelled</string> - <string name="r2.shared.search.exception.other">An error occurred while searching</string> -</resources> \ No newline at end of file diff --git a/readium/shared/src/test/java/org/readium/r2/shared/TestUtils.kt b/readium/shared/src/test/java/org/readium/r2/shared/TestUtils.kt index be8667a9e5..e172ba7a7f 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/TestUtils.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/TestUtils.kt @@ -17,10 +17,7 @@ import org.json.JSONObject import org.junit.Assert.assertEquals import org.readium.r2.shared.extensions.toListTest import org.readium.r2.shared.extensions.toMapTest -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.util.use +import org.readium.r2.shared.util.resource.Resource /** * Asserts that two [JSONObject] are equal. @@ -52,12 +49,4 @@ class Fixtures(val path: String? = null) { internal fun Resource.readBlocking(range: LongRange? = null) = runBlocking { read(range) } -internal fun Fetcher.readBlocking(href: String) = runBlocking { get(Link(href = href)).use { it.readBlocking() } } - internal fun Resource.lengthBlocking() = runBlocking { length() } - -internal fun Fetcher.lengthBlocking(href: String) = runBlocking { get(Link(href = href)).use { it.lengthBlocking() } } - -internal fun Resource.linkBlocking() = runBlocking { link() } - -internal fun Fetcher.linkBlocking(href: String) = runBlocking { get(Link(href = href)).use { it.linkBlocking() } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/extensions/URLTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/extensions/URLTest.kt index 2287a3ad89..1c17160783 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/extensions/URLTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/extensions/URLTest.kt @@ -16,13 +16,28 @@ import org.junit.Test class URLTest { @Test fun `remove last component`() { - assertEquals(URL("http://domain.com/two/"), URL("http://domain.com/two/paths").removeLastComponent()) - assertEquals(URL("http://domain.com/two/"), URL("http://domain.com/two/paths/").removeLastComponent()) + assertEquals( + URL("http://domain.com/two/"), + URL("http://domain.com/two/paths").removeLastComponent() + ) + assertEquals( + URL("http://domain.com/two/"), + URL("http://domain.com/two/paths/").removeLastComponent() + ) assertEquals(URL("http://domain.com/"), URL("http://domain.com/path").removeLastComponent()) - assertEquals(URL("http://domain.com/"), URL("http://domain.com/path/").removeLastComponent()) + assertEquals( + URL("http://domain.com/"), + URL("http://domain.com/path/").removeLastComponent() + ) assertEquals(URL("http://domain.com/"), URL("http://domain.com/").removeLastComponent()) assertEquals(URL("http://domain.com"), URL("http://domain.com").removeLastComponent()) - assertEquals(URL("http://domain.com/two/"), URL("http://domain.com/two/paths?a=1&b=2").removeLastComponent()) - assertEquals(URL("http://domain.com/two/"), URL("http://domain.com/two/paths/?a=1b=2").removeLastComponent()) + assertEquals( + URL("http://domain.com/two/"), + URL("http://domain.com/two/paths?a=1&b=2").removeLastComponent() + ) + assertEquals( + URL("http://domain.com/two/"), + URL("http://domain.com/two/paths/?a=1b=2").removeLastComponent() + ) } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/fetcher/ArchiveFetcherTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/fetcher/ArchiveFetcherTest.kt deleted file mode 100644 index 5714daafd5..0000000000 --- a/readium/shared/src/test/java/org/readium/r2/shared/fetcher/ArchiveFetcherTest.kt +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.fetcher - -import android.webkit.MimeTypeMap -import java.nio.charset.StandardCharsets -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotNull -import kotlinx.coroutines.runBlocking -import org.json.JSONObject -import org.junit.Test -import org.junit.runner.RunWith -import org.readium.r2.shared.assertJSONEquals -import org.readium.r2.shared.lengthBlocking -import org.readium.r2.shared.linkBlocking -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.Properties -import org.readium.r2.shared.readBlocking -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows - -@RunWith(RobolectricTestRunner::class) -class ArchiveFetcherTest { - - private val fetcher: Fetcher - - init { - val epub = ArchiveFetcherTest::class.java.getResource("epub.epub") - assertNotNull(epub) - val zipFetcher = runBlocking { ArchiveFetcher.fromPath(epub.path) } - assertNotNull(zipFetcher) - fetcher = zipFetcher - } - - @Test - fun `Link list is correct`() { - Shadows.shadowOf(MimeTypeMap.getSingleton()).apply { - addExtensionMimeTypMapping("css", "text/css") - addExtensionMimeTypMapping("png", "image/png") - addExtensionMimeTypMapping("xml", "text/xml") - } - - fun createLink(href: String, type: String?, entryLength: Long, isCompressed: Boolean): Link { - val props = mutableMapOf<String, Any>( - "archive" to mapOf( - "entryLength" to entryLength, - "isEntryCompressed" to isCompressed - ) - ) - if (isCompressed) { - props["compressedLength"] = entryLength - } - - return Link( - href = href, - type = type, - properties = Properties(props) - ) - } - - assertEquals( - listOf( - createLink("/mimetype", null, 20, false), - createLink("/EPUB/cover.xhtml", "application/xhtml+xml", 259L, true), - createLink("/EPUB/css/epub.css", "text/css", 595L, true), - createLink("/EPUB/css/nav.css", "text/css", 306L, true), - createLink("/EPUB/images/cover.png", "image/png", 35809L, true), - createLink("/EPUB/nav.xhtml", "application/xhtml+xml", 2293L, true), - createLink("/EPUB/package.opf", null, 773L, true), - createLink("/EPUB/s04.xhtml", "application/xhtml+xml", 118269L, true), - createLink("/EPUB/toc.ncx", null, 1697, true), - createLink("/META-INF/container.xml", "text/xml", 176, true) - ), - runBlocking { fetcher.links() } - ) - } - - @Test - fun `Computing length for a missing entry returns NotFound`() { - val resource = fetcher.get(Link(href = "/unknown")) - assertFailsWith<Resource.Exception.NotFound> { resource.lengthBlocking().getOrThrow() } - } - - @Test - fun `Reading a missing entry returns NotFound`() { - val resource = fetcher.get(Link(href = "/unknown")) - assertFailsWith<Resource.Exception.NotFound> { resource.readBlocking().getOrThrow() } - } - - @Test - fun `Fully reading an entry works well`() { - val resource = fetcher.get(Link(href = "/mimetype")) - val result = resource.readBlocking().getOrNull() - assertEquals("application/epub+zip", result?.toString(StandardCharsets.UTF_8)) - } - - @Test - fun `Reading a range of an entry works well`() { - val resource = fetcher.get(Link(href = "/mimetype")) - val result = resource.readBlocking(0..10L).getOrNull() - assertEquals("application", result?.toString(StandardCharsets.UTF_8)) - assertEquals(11, result?.size) - } - - @Test - fun `Out of range indexes are clamped to the available length`() { - val resource = fetcher.get(Link(href = "/mimetype")) - val result = resource.readBlocking(-5..60L).getOrNull() - assertEquals("application/epub+zip", result?.toString(StandardCharsets.UTF_8)) - assertEquals(20, result?.size) - } - - @Test - fun `Decreasing ranges are understood as empty ones`() { - val resource = fetcher.get(Link(href = "/mimetype")) - val result = resource.readBlocking(60..20L).getOrNull() - assertEquals("", result?.toString(StandardCharsets.UTF_8)) - assertEquals(0, result?.size) - } - - @Test - fun `Computing length works well`() { - val resource = fetcher.get(Link(href = "/mimetype")) - val result = resource.lengthBlocking() - assertEquals(20L, result.getOrNull()) - } - - @Test - fun `Computing a directory length returns NotFound`() { - val resource = fetcher.get(Link(href = "/EPUB")) - assertFailsWith<Resource.Exception.NotFound> { resource.lengthBlocking().getOrThrow() } - } - - @Test - fun `Computing the length of a missing file returns NotFound`() { - val resource = fetcher.get(Link(href = "/unknown")) - assertFailsWith<Resource.Exception.NotFound> { resource.lengthBlocking().getOrThrow() } - } - - @Test - fun `Adds compressed length and archive properties to the Link`() = runBlocking { - assertJSONEquals( - JSONObject( - mapOf( - "compressedLength" to 595L, - "archive" to mapOf( - "entryLength" to 595L, - "isEntryCompressed" to true - ) - ) - ), - fetcher.get(Link(href = "/EPUB/css/epub.css")).link().properties.toJSON() - ) - } - - @Test - fun `Original link properties are kept`() { - val resource = fetcher.get(Link(href = "/mimetype", properties = Properties(mapOf("other" to "property")))) - - assertEquals( - Link( - href = "/mimetype", - properties = Properties( - mapOf( - "other" to "property", - "archive" to mapOf("entryLength" to 20L, "isEntryCompressed" to false) - ) - ) - ), - resource.linkBlocking() - ) - } - - /** - * When the HREF contains query parameters, the fetcher should first be able to remove them as - * a fallback. - */ - @Test - fun `Get resource from HREF with query parameters`() = runBlocking { - val resource = fetcher.get(Link(href = "/mimetype?query=param")) - val result = resource.readAsString().getOrNull() - assertEquals("application/epub+zip", result) - } - - /** - * When the HREF contains an anchor, the fetcher should first be able to remove them as - * a fallback. - */ - @Test - fun `Get resource from HREF with anchors`() = runBlocking { - val resource = fetcher.get(Link(href = "/mimetype#anchor")) - val result = resource.readAsString().getOrNull() - assertEquals("application/epub+zip", result) - } -} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/fetcher/FileFetcherTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/fetcher/FileFetcherTest.kt deleted file mode 100644 index 324f610fe7..0000000000 --- a/readium/shared/src/test/java/org/readium/r2/shared/fetcher/FileFetcherTest.kt +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared.fetcher - -import android.webkit.MimeTypeMap -import java.io.File -import java.nio.charset.StandardCharsets -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotNull -import kotlinx.coroutines.runBlocking -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test -import org.junit.runner.RunWith -import org.readium.r2.shared.lengthBlocking -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.readBlocking -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows - -@RunWith(RobolectricTestRunner::class) -class FileFetcherTest { - - private val fetcher: Fetcher - - init { - val text = FileFetcherTest::class.java.getResource("text.txt") - assertNotNull(text) - val directory = FileFetcherTest::class.java.getResource("directory") - assertNotNull(directory) - fetcher = FileFetcher(mapOf("/file_href" to File(text.path), "/dir_href" to File(directory.path))) - } - - @Test - fun `Computing length for a missing file returns NotFound`() { - val resource = fetcher.get(Link(href = "/unknown")) - assertFailsWith<Resource.Exception.NotFound> { resource.lengthBlocking().getOrThrow() } - } - - @Test - fun `Reading a missing file returns NotFound`() { - val resource = fetcher.get(Link(href = "/unknown")) - assertFailsWith<Resource.Exception.NotFound> { resource.readBlocking().getOrThrow() } - } - - @Test - fun `Reading an href in the map works well`() { - val resource = fetcher.get(Link(href = "/file_href")) - val result = resource.readBlocking().getOrNull() - assertEquals("text", result?.toString(StandardCharsets.UTF_8)) - } - - @Test - fun `Reading a file in a directory works well`() { - val resource = fetcher.get(Link(href = "/dir_href/text1.txt")) - val result = resource.readBlocking().getOrNull() - assertEquals("text1", result?.toString(StandardCharsets.UTF_8)) - } - - @Test - fun `Reading a file in a subdirectory works well`() { - val resource = fetcher.get(Link(href = "/dir_href/subdirectory/text2.txt")) - val result = resource.readBlocking().getOrNull() - assertEquals("text2", result?.toString(StandardCharsets.UTF_8)) - } - - @Test - fun `Reading a directory returns NotFound`() { - val resource = fetcher.get(Link(href = "/dir_href/subdirectory")) - assertFailsWith<Resource.Exception.NotFound> { resource.readBlocking().getOrThrow() } - } - - @Test - fun `Reading a file outside the allowed directory returns NotFound`() { - val resource = fetcher.get(Link(href = "/dir_href/../text.txt")) - assertFailsWith<Resource.Exception.NotFound> { resource.readBlocking().getOrThrow() } - } - - @Test - fun `Reading a range works well`() { - val resource = fetcher.get(Link(href = "/file_href")) - val result = resource.readBlocking(0..2L).getOrNull() - assertEquals("tex", result?.toString(StandardCharsets.UTF_8)) - } - - @Test - fun `Reading two ranges with the same resource work well`() { - val resource = fetcher.get(Link(href = "/file_href")) - val result1 = resource.readBlocking(0..1L).getOrNull() - assertEquals("te", result1?.toString(StandardCharsets.UTF_8)) - val result2 = resource.readBlocking(1..3L).getOrNull() - assertEquals("ext", result2?.toString(StandardCharsets.UTF_8)) - } - - @Test - fun `Out of range indexes are clamped to the available length`() { - val resource = fetcher.get(Link(href = "/file_href")) - val result = resource.readBlocking(-5..60L).getOrNull() - assertEquals("text", result?.toString(StandardCharsets.UTF_8)) - assertEquals(4, result?.size) - } - - @Test - @Suppress("EmptyRange") - fun `Decreasing ranges are understood as empty ones`() { - val resource = fetcher.get(Link(href = "/file_href")) - val result = resource.readBlocking(60..20L).getOrNull() - assertEquals("", result?.toString(StandardCharsets.UTF_8)) - assertEquals(0, result?.size) - } - - @Test - fun `Computing length works well`() { - val resource = fetcher.get(Link(href = "/file_href")) - val result = resource.lengthBlocking().getOrNull() - assertEquals(4L, result) - } - - @Test - fun `Computing a directory length returns NotFound`() { - val resource = fetcher.get(Link(href = "/dir_href/subdirectory")) - assertFailsWith<Resource.Exception.NotFound> { resource.lengthBlocking().getOrThrow() } - } - - @Test - fun `Computing the length of a missing file returns NotFound`() { - val resource = fetcher.get(Link(href = "/unknown")) - assertFailsWith<Resource.Exception.NotFound> { resource.lengthBlocking().getOrThrow() } - } - - @Test - fun `Computing links works well`() { - Shadows.shadowOf(MimeTypeMap.getSingleton()).apply { - addExtensionMimeTypMapping("txt", "text/plain") - addExtensionMimeTypMapping("mp3", "audio/mpeg") - } - - assertThat(runBlocking { fetcher.links() }).containsExactlyInAnyOrder( - Link(href = "/dir_href/subdirectory/hello.mp3", type = "audio/mpeg"), - Link(href = "/dir_href/subdirectory/text2.txt", type = "text/plain"), - Link(href = "/dir_href/text1.txt", type = "text/plain"), - Link(href = "/file_href", type = "text/plain") - ) - } -} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/fetcher/ResourceInputStreamTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/fetcher/ResourceInputStreamTest.kt deleted file mode 100644 index 7495ed7b0a..0000000000 --- a/readium/shared/src/test/java/org/readium/r2/shared/fetcher/ResourceInputStreamTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -package org.readium.r2.shared.fetcher - -import java.io.ByteArrayOutputStream -import java.io.File -import kotlin.test.assertNotNull -import kotlin.test.assertTrue -import kotlinx.coroutines.runBlocking -import org.junit.Test - -class ResourceInputStreamTest { - - private val fileContent: ByteArray - private val fetcher: Fetcher - private val bufferSize = 16384 // This is the size used by NanoHTTPd for chunked responses - - init { - val resource = ResourceInputStreamTest::class.java.getResource("epub.epub") - assertNotNull(resource) - fileContent = resource.openStream().readBytes() - val fileFetcher = runBlocking { FileFetcher("/epub.epub", File(resource.path)) } - assertNotNull(fileFetcher) - fetcher = fileFetcher - } - - @Test - fun `stream can be read by chunks`() { - val resourceStream = ResourceInputStream(fetcher.get("/epub.epub")) - val outputStream = ByteArrayOutputStream(fileContent.size) - resourceStream.copyTo(outputStream, bufferSize = bufferSize) - assertTrue(fileContent.contentEquals(outputStream.toByteArray())) - } -} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/AccessibilityTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/AccessibilityTest.kt index d0a294a078..0cc8852a2e 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/AccessibilityTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/AccessibilityTest.kt @@ -67,7 +67,7 @@ class AccessibilityTest { hazards = emptySet(), features = emptySet() ), - Accessibility.fromJSON(JSONObject("{}")), + Accessibility.fromJSON(JSONObject("{}")) ) } @@ -211,7 +211,10 @@ class AccessibilityTest { accessModes = emptySet(), accessModesSufficient = setOf( setOf(Accessibility.PrimaryAccessMode.AUDITORY), - setOf(Accessibility.PrimaryAccessMode.VISUAL, Accessibility.PrimaryAccessMode.TACTILE), + setOf( + Accessibility.PrimaryAccessMode.VISUAL, + Accessibility.PrimaryAccessMode.TACTILE + ), setOf(Accessibility.PrimaryAccessMode.VISUAL) ), features = emptySet(), @@ -298,21 +301,36 @@ class AccessibilityTest { }""" ), Accessibility( - conformsTo = setOf(Accessibility.Profile.EPUB_A11Y_10_WCAG_20_A, Accessibility.Profile("https://profile2")), + conformsTo = setOf( + Accessibility.Profile.EPUB_A11Y_10_WCAG_20_A, + Accessibility.Profile("https://profile2") + ), certification = Accessibility.Certification( certifiedBy = "company1", credential = "credential1", report = "https://report1" ), summary = "Summary", - accessModes = setOf(Accessibility.AccessMode.AUDITORY, Accessibility.AccessMode.CHART_ON_VISUAL), + accessModes = setOf( + Accessibility.AccessMode.AUDITORY, + Accessibility.AccessMode.CHART_ON_VISUAL + ), accessModesSufficient = setOf( setOf(Accessibility.PrimaryAccessMode.AUDITORY), - setOf(Accessibility.PrimaryAccessMode.VISUAL, Accessibility.PrimaryAccessMode.TACTILE), + setOf( + Accessibility.PrimaryAccessMode.VISUAL, + Accessibility.PrimaryAccessMode.TACTILE + ), setOf(Accessibility.PrimaryAccessMode.VISUAL) ), - features = setOf(Accessibility.Feature.READING_ORDER, Accessibility.Feature.ALTERNATIVE_TEXT), - hazards = setOf(Accessibility.Hazard.FLASHING, Accessibility.Hazard.MOTION_SIMULATION) + features = setOf( + Accessibility.Feature.READING_ORDER, + Accessibility.Feature.ALTERNATIVE_TEXT + ), + hazards = setOf( + Accessibility.Hazard.FLASHING, + Accessibility.Hazard.MOTION_SIMULATION + ) ).toJSON() ) } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/ContributorTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/ContributorTest.kt index 436f314906..c6a3b5c252 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/ContributorTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/ContributorTest.kt @@ -45,8 +45,8 @@ class ContributorTest { roles = setOf("bassist"), position = 4.0, links = listOf( - Link(href = "http://link1"), - Link(href = "http://link2") + Link(href = Href("http://link1")!!), + Link(href = Href("http://link2")!!) ) ), Contributor.fromJSON( @@ -217,8 +217,8 @@ class ContributorTest { roles = setOf("bassist"), position = 4.0, links = listOf( - Link(href = "http://link1"), - Link(href = "http://link2") + Link(href = Href("http://link1")!!), + Link(href = Href("http://link2")!!) ) ).toJSON() ) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/HrefTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/HrefTest.kt new file mode 100644 index 0000000000..e35d2bb90b --- /dev/null +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/HrefTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2020 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.publication + +import kotlin.test.assertTrue +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.util.Url +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class HrefTest { + + private val base = Url("http://readium/publication/")!! + + @Test + fun `convert static Href to Url`() { + val url = Url("folder/chapter.xhtml")!! + assertEquals(url, Href(url).resolve()) + assertEquals( + Url("http://readium/publication/folder/chapter.xhtml"), + Href(url).resolve(base) + ) + // Parameters are ignored. + assertEquals(url, Href(url).resolve(parameters = mapOf("a" to "b"))) + } + + @Test + fun `convert templated Href to Url`() { + val template = Href("url{?x,hello,y}name", templated = true)!! + + val parameters = mapOf( + "x" to "aaa", + "hello" to "Hello, world", + "y" to "b", + "foo" to "bar" + ) + + assertEquals( + Url("url?x=&hello=&y=name"), + template.resolve() + ) + + assertEquals( + Url("http://readium/publication/url?x=&hello=&y=name"), + template.resolve(base) + ) + + assertEquals( + Url("url?x=aaa&hello=Hello,%20world&y=bname"), + template.resolve(parameters = parameters) + ) + + assertEquals( + Url("http://readium/publication/url?x=aaa&hello=Hello,%20world&y=bname"), + template.resolve(base, parameters) + ) + } + + @Test + fun `get is templated`() { + assertFalse(Href("url", templated = false)!!.isTemplated) + assertTrue(Href("url", templated = true)!!.isTemplated) + assertTrue(Href("url{?x,hello,y}name", templated = true)!!.isTemplated) + } + + @Test + fun `get template parameters`() { + assertNull(Href("url", templated = false)!!.parameters) + + assertEquals( + listOf<String>(), + Href("url", templated = true)!!.parameters + ) + assertEquals( + listOf("x", "hello", "y"), + Href("url{?x,hello,y}name", templated = true)!!.parameters + ) + } + + @Test + fun getToString() { + assertEquals("folder/chapter.xhtml", Href(Url("folder/chapter.xhtml")!!).toString()) + assertEquals( + "url{?x,hello,y}name", + Href("url{?x,hello,y}name", templated = true)!!.toString() + ) + } + + @Test + fun equality() { + val url1 = Url("folder/chapter1.xhtml")!! + val url2 = Url("folder/chapter2.xhtml")!! + assertEquals(Href(url1), Href(url1)) + assertNotEquals(Href(url1), Href(url2)) + + assertEquals(Href("template1", templated = true), Href("template1", templated = true)!!) + assertNotEquals(Href("template1", templated = true), Href("template2", templated = true)!!) + } +} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/LinkTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/LinkTest.kt index 86b064dde9..dc4eed37f2 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/LinkTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/LinkTest.kt @@ -16,36 +16,16 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.assertJSONEquals import org.readium.r2.shared.toJSON +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class LinkTest { - @Test fun `templateParameters works fine`() { - val href = "/url{?x,hello,y}name{z,y,w}" - assertEquals( - listOf("x", "hello", "y", "z", "w"), - Link(href = href, templated = true).templateParameters - ) - } - - @Test fun `expand works fine`() { - val href = "/url{?x,hello,y}name" - val parameters = mapOf( - "x" to "aaa", - "hello" to "Hello, world", - "y" to "b" - ) - assertEquals( - Link(href = "/url?x=aaa&hello=Hello,%20world&y=bname", templated = false), - Link(href = href, templated = true).expandTemplate(parameters) - ) - } - @Test fun `parse minimal JSON`() { assertEquals( - Link(href = "http://href"), + Link(href = Url("http://href")!!), Link.fromJSON(JSONObject("{'href': 'http://href'}")) ) } @@ -53,9 +33,8 @@ class LinkTest { @Test fun `parse full JSON`() { assertEquals( Link( - href = "http://href", - type = "application/pdf", - templated = true, + href = Href("http://href")!!, + mediaType = MediaType.PDF, title = "Link Title", rels = setOf("publication", "cover"), properties = Properties(otherProperties = mapOf("orientation" to "landscape")), @@ -65,12 +44,12 @@ class LinkTest { duration = 45.6, languages = listOf("fr"), alternates = listOf( - Link(href = "/alternate1"), - Link(href = "/alternate2") + Link(href = Url("/alternate1")!!), + Link(href = Url("/alternate2")!!) ), children = listOf( - Link(href = "http://child1"), - Link(href = "http://child2") + Link(href = Url("http://child1")!!), + Link(href = Url("http://child2")!!) ) ), Link.fromJSON( @@ -78,7 +57,7 @@ class LinkTest { """{ "href": "http://href", "type": "application/pdf", - "templated": true, + "templated": false, "title": "Link Title", "rel": ["publication", "cover"], "properties": { @@ -110,24 +89,29 @@ class LinkTest { @Test fun `parse JSON {rel} as single string`() { assertEquals( Link.fromJSON(JSONObject("{'href': 'a', 'rel': 'publication'}")), - Link(href = "a", rels = setOf("publication")) + Link(href = Url("a")!!, rels = setOf("publication")) ) } @Test fun `parse JSON {templated} defaults to false`() { val link = Link.fromJSON(JSONObject("{'href': 'a'}"))!! - assertFalse(link.templated) + assertFalse(link.href.isTemplated) } @Test fun `parse JSON {templated} as false when null`() { val link = Link.fromJSON(JSONObject("{'href': 'a', 'templated': null}"))!! - assertFalse(link.templated) + assertFalse(link.href.isTemplated) + } + + @Test fun `parse JSON {templated} when true`() { + val link = Link.fromJSON(JSONObject("{'href': 'a', 'templated': true}"))!! + assertTrue(link.href.isTemplated) } @Test fun `parse JSON multiple languages`() { assertEquals( Link.fromJSON(JSONObject("{'href': 'a', 'language': ['fr', 'en']}")), - Link(href = "a", languages = listOf("fr", "en")) + Link(href = Href("a")!!, languages = listOf("fr", "en")) ) } @@ -158,8 +142,8 @@ class LinkTest { @Test fun `parse JSON array`() { assertEquals( listOf( - Link(href = "http://child1"), - Link(href = "http://child2") + Link(href = Url("http://child1")!!), + Link(href = Url("http://child2")!!) ), Link.fromJSONArray( JSONArray( @@ -179,7 +163,7 @@ class LinkTest { @Test fun `parse JSON array ignores invalid links`() { assertEquals( listOf( - Link(href = "http://child2") + Link(href = Url("http://child2")!!) ), Link.fromJSONArray( JSONArray( @@ -195,7 +179,7 @@ class LinkTest { @Test fun `get minimal JSON`() { assertJSONEquals( JSONObject("{'href': 'http://href', 'templated': false}"), - Link(href = "http://href").toJSON() + Link(href = Url("http://href")!!).toJSON() ) } @@ -227,9 +211,8 @@ class LinkTest { }""" ), Link( - href = "http://href", - type = "application/pdf", - templated = true, + href = Href("http://href", templated = true)!!, + mediaType = MediaType.PDF, title = "Link Title", rels = setOf("publication", "cover"), properties = Properties(otherProperties = mapOf("orientation" to "landscape")), @@ -239,12 +222,12 @@ class LinkTest { duration = 45.6, languages = listOf("fr"), alternates = listOf( - Link(href = "/alternate1"), - Link(href = "/alternate2") + Link(href = Url("/alternate1")!!), + Link(href = Url("/alternate2")!!) ), children = listOf( - Link(href = "http://child1"), - Link(href = "http://child2") + Link(href = Url("http://child1")!!), + Link(href = Url("http://child2")!!) ) ).toJSON() ) @@ -259,72 +242,28 @@ class LinkTest { ]""" ), listOf( - Link(href = "http://child1"), - Link(href = "http://child2") + Link(href = Url("http://child1")!!), + Link(href = Url("http://child2")!!) ).toJSON() ) } - @Test fun `get unknown media type`() { - assertEquals(MediaType.BINARY, Link(href = "file").mediaType) - } - @Test fun `get media type from type`() { - assertEquals(MediaType.EPUB, Link(href = "file", type = "application/epub+zip").mediaType) - assertEquals(MediaType.PDF, Link(href = "file", type = "application/pdf").mediaType) - } - - @Test - fun `to URL relative to base URL`() { assertEquals( - "http://host/folder/file.html", - Link("folder/file.html").toUrl("http://host/") + MediaType.EPUB, + Link(href = Url("file")!!, mediaType = MediaType.EPUB).mediaType ) - } - - @Test - fun `to URL relative to base URL with root prefix`() { assertEquals( - "http://host/folder/file.html", - Link("/file.html").toUrl("http://host/folder/") - ) - } - - @Test - fun `to URL relative to null`() { - assertEquals( - "/folder/file.html", - Link("folder/file.html").toUrl(null) - ) - } - - @Test - fun `to URL with invalid HREF`() { - assertNull(Link("").toUrl("http://test.com")) - } - - @Test - fun `to URL with absolute HREF`() { - assertEquals( - "http://test.com/folder/file.html", - Link("http://test.com/folder/file.html").toUrl("http://host/") - ) - } - - @Test - fun `to URL with HREF containing invalid characters`() { - assertEquals( - "http://host/folder/Cory%20Doctorow's/a-fc.jpg", - Link("/Cory Doctorow's/a-fc.jpg").toUrl("http://host/folder/") + MediaType.PDF, + Link(href = Url("file")!!, mediaType = MediaType.PDF).mediaType ) } @Test fun `Make a copy after adding the given {properties}`() { val link = Link( - href = "http://href", - type = "application/pdf", - templated = true, + href = Href("http://href", templated = true)!!, + mediaType = MediaType.PDF, title = "Link Title", rels = setOf("publication", "cover"), properties = Properties(otherProperties = mapOf("orientation" to "landscape")), @@ -334,12 +273,12 @@ class LinkTest { duration = 45.6, languages = listOf("fr"), alternates = listOf( - Link(href = "/alternate1"), - Link(href = "/alternate2") + Link(href = Url("/alternate1")!!), + Link(href = Url("/alternate2")!!) ), children = listOf( - Link(href = "http://child1"), - Link(href = "http://child2") + Link(href = Url("http://child1")!!), + Link(href = Url("http://child2")!!) ) ) @@ -376,15 +315,17 @@ class LinkTest { @Test fun `Find the first index of the {Link} with the given {href} in a list of {Link}`() { - assertNull(listOf(Link(href = "href")).indexOfFirstWithHref("foobar")) + assertNull( + listOf(Link(href = Url("href")!!)).indexOfFirstWithHref(Url("foobar")!!) + ) assertEquals( 1, listOf( - Link(href = "href1"), - Link(href = "href2"), - Link(href = "href2") // duplicated on purpose - ).indexOfFirstWithHref("href2") + Link(href = Url("href1")!!), + Link(href = Url("href2")!!), + Link(href = Url("href2")!!) // duplicated on purpose + ).indexOfFirstWithHref(Url("href2")!!) ) } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/LocalizedStringTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/LocalizedStringTest.kt index 09fa467aa4..6395589b5a 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/LocalizedStringTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/LocalizedStringTest.kt @@ -198,10 +198,11 @@ class LocalizedStringTest { "fr" to "une chaîne" ) ).mapLanguages { (language, translation) -> - if (translation.string == "a string") + if (translation.string == "a string") { "en" - else + } else { language + } } ) } @@ -220,10 +221,11 @@ class LocalizedStringTest { "fr" to "une chaîne" ) ).mapTranslations { (language, translation) -> - if (language == "en") + if (language == "en") { translation.copy(string = "a string") - else + } else { translation + } } ) } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt index d09043efd9..06ddbd0b1d 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt @@ -14,7 +14,10 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.assertJSONEquals +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -22,7 +25,7 @@ class LocatorTest { @Test fun `parse {Locator} minimal JSON`() { assertEquals( - Locator(href = "http://locator", type = "text/html"), + Locator(href = Url("http://locator")!!, mediaType = MediaType.HTML), Locator.fromJSON( JSONObject( """{ @@ -37,8 +40,8 @@ class LocatorTest { @Test fun `parse {Locator} full JSON`() { assertEquals( Locator( - href = "http://locator", - type = "text/html", + href = Url("http://locator")!!, + mediaType = MediaType.HTML, title = "My Locator", locations = Locator.Locations(position = 42), text = Locator.Text(highlight = "Excerpt") @@ -69,6 +72,26 @@ class LocatorTest { assertNull(Locator.fromJSON(JSONObject("{ 'invalid': 'object' }"))) } + @OptIn(DelicateReadiumApi::class) + @Test + fun `parse {Locator} with legacy HREF`() { + val json = JSONObject( + """ + { + "href": "legacy href", + "type": "text/html" + } + """ + ) + + assertNull(Locator.fromJSON(json)) + + assertEquals( + Locator(href = Url("legacy%20href")!!, mediaType = MediaType.HTML), + Locator.fromLegacyJSON(json) + ) + } + @Test fun `get {Locator} minimal JSON`() { assertJSONEquals( JSONObject( @@ -77,7 +100,7 @@ class LocatorTest { "type": "text/html" }""" ), - Locator(href = "http://locator", type = "text/html").toJSON() + Locator(href = Url("http://locator")!!, mediaType = MediaType.HTML).toJSON() ) } @@ -97,8 +120,8 @@ class LocatorTest { }""" ), Locator( - href = "http://locator", - type = "text/html", + href = Url("http://locator")!!, + mediaType = MediaType.HTML, title = "My Locator", locations = Locator.Locations(position = 42), text = Locator.Text(highlight = "Excerpt") @@ -109,8 +132,8 @@ class LocatorTest { @Test fun `copy a {Locator} with different {Locations} sub-properties`() { assertEquals( Locator( - href = "http://locator", - type = "text/html", + href = Url("http://locator")!!, + mediaType = MediaType.HTML, locations = Locator.Locations( fragments = listOf("p=4", "frag34"), progression = 0.74, @@ -120,8 +143,8 @@ class LocatorTest { ) ), Locator( - href = "http://locator", - type = "text/html", + href = Url("http://locator")!!, + mediaType = MediaType.HTML, locations = Locator.Locations(position = 42, progression = 2.0) ).copyWithLocations( fragments = listOf("p=4", "frag34"), @@ -135,13 +158,13 @@ class LocatorTest { @Test fun `copy a {Locator} with reset {Locations} sub-properties`() { assertEquals( Locator( - href = "http://locator", - type = "text/html", + href = Url("http://locator")!!, + mediaType = MediaType.HTML, locations = Locator.Locations() ), Locator( - href = "http://locator", - type = "text/html", + href = Url("http://locator")!!, + mediaType = MediaType.HTML, locations = Locator.Locations(position = 42, progression = 2.0) ).copyWithLocations( fragments = emptyList(), @@ -195,25 +218,64 @@ class LocatorTest { } @Test fun `parse {Locations} ignores {position} smaller than 1`() { - assertEquals(Locator.Locations(position = 1), Locator.Locations.fromJSON(JSONObject("{ 'position': 1 }"))) - assertEquals(Locator.Locations(), Locator.Locations.fromJSON(JSONObject("{ 'position': 0 }"))) - assertEquals(Locator.Locations(), Locator.Locations.fromJSON(JSONObject("{ 'position': -1 }"))) + assertEquals( + Locator.Locations(position = 1), + Locator.Locations.fromJSON(JSONObject("{ 'position': 1 }")) + ) + assertEquals( + Locator.Locations(), + Locator.Locations.fromJSON(JSONObject("{ 'position': 0 }")) + ) + assertEquals( + Locator.Locations(), + Locator.Locations.fromJSON(JSONObject("{ 'position': -1 }")) + ) } @Test fun `parse {Locations} ignores {progression} outside of 0-1 range`() { - assertEquals(Locator.Locations(progression = 0.5), Locator.Locations.fromJSON(JSONObject("{ 'progression': 0.5 }"))) - assertEquals(Locator.Locations(progression = 0.0), Locator.Locations.fromJSON(JSONObject("{ 'progression': 0 }"))) - assertEquals(Locator.Locations(progression = 1.0), Locator.Locations.fromJSON(JSONObject("{ 'progression': 1 }"))) - assertEquals(Locator.Locations(), Locator.Locations.fromJSON(JSONObject("{ 'progression': -0.5 }"))) - assertEquals(Locator.Locations(), Locator.Locations.fromJSON(JSONObject("{ 'progression': 1.2 }"))) + assertEquals( + Locator.Locations(progression = 0.5), + Locator.Locations.fromJSON(JSONObject("{ 'progression': 0.5 }")) + ) + assertEquals( + Locator.Locations(progression = 0.0), + Locator.Locations.fromJSON(JSONObject("{ 'progression': 0 }")) + ) + assertEquals( + Locator.Locations(progression = 1.0), + Locator.Locations.fromJSON(JSONObject("{ 'progression': 1 }")) + ) + assertEquals( + Locator.Locations(), + Locator.Locations.fromJSON(JSONObject("{ 'progression': -0.5 }")) + ) + assertEquals( + Locator.Locations(), + Locator.Locations.fromJSON(JSONObject("{ 'progression': 1.2 }")) + ) } @Test fun `parse {Locations} ignores {totalProgression} outside of 0-1 range`() { - assertEquals(Locator.Locations(totalProgression = 0.5), Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': 0.5 }"))) - assertEquals(Locator.Locations(totalProgression = 0.0), Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': 0 }"))) - assertEquals(Locator.Locations(totalProgression = 1.0), Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': 1 }"))) - assertEquals(Locator.Locations(), Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': -0.5 }"))) - assertEquals(Locator.Locations(), Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': 1.2 }"))) + assertEquals( + Locator.Locations(totalProgression = 0.5), + Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': 0.5 }")) + ) + assertEquals( + Locator.Locations(totalProgression = 0.0), + Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': 0 }")) + ) + assertEquals( + Locator.Locations(totalProgression = 1.0), + Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': 1 }")) + ) + assertEquals( + Locator.Locations(), + Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': -0.5 }")) + ) + assertEquals( + Locator.Locations(), + Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': 1.2 }")) + ) } @Test fun `get {Locations} minimal JSON`() { @@ -297,6 +359,131 @@ class LocatorTest { ).toJSON() ) } + + @Test fun `substring from a range`() { + val text = Locator.Text( + before = "before", + highlight = "highlight", + after = "after" + ) + + assertEquals( + Locator.Text( + before = "before", + highlight = "h", + after = "ighlightafter" + ), + text.substring(0..-1) + ) + + assertEquals( + Locator.Text( + before = "before", + highlight = "h", + after = "ighlightafter" + ), + text.substring(0..0) + ) + + assertEquals( + Locator.Text( + before = "beforehigh", + highlight = "lig", + after = "htafter" + ), + text.substring(4..6) + ) + + assertEquals( + Locator.Text( + before = "before", + highlight = "highlight", + after = "after" + ), + text.substring(0..8) + ) + + assertEquals( + Locator.Text( + before = "beforehighli", + highlight = "ght", + after = "after" + ), + text.substring(6..12) + ) + + assertEquals( + Locator.Text( + before = "beforehighligh", + highlight = "t", + after = "after" + ), + text.substring(8..12) + ) + + assertEquals( + Locator.Text( + before = "beforehighlight", + highlight = "", + after = "after" + ), + text.substring(9..12) + ) + } + + @Test fun `substring from a range with null components`() { + assertEquals( + Locator.Text( + before = "high", + highlight = "lig", + after = "htafter" + ), + Locator.Text( + before = null, + highlight = "highlight", + after = "after" + ).substring(4..6) + ) + + assertEquals( + Locator.Text( + before = "beforehigh", + highlight = "lig", + after = "ht" + ), + Locator.Text( + before = "before", + highlight = "highlight", + after = null + ).substring(4..6) + ) + + assertEquals( + Locator.Text( + before = "before", + highlight = null, + after = "after" + ), + Locator.Text( + before = "before", + highlight = null, + after = "after" + ).substring(4..6) + ) + + assertEquals( + Locator.Text( + before = "before", + highlight = "", + after = "after" + ), + Locator.Text( + before = "before", + highlight = "", + after = "after" + ).substring(4..6) + ) + } } @RunWith(RobolectricTestRunner::class) @@ -325,13 +512,21 @@ class LocatorCollectionTest { ) ), links = listOf( - Link(rels = setOf("self"), href = "/978-1503222687/search?query=apple", type = "application/vnd.readium.locators+json"), - Link(rels = setOf("next"), href = "/978-1503222687/search?query=apple&page=2", type = "application/vnd.readium.locators+json"), + Link( + rels = setOf("self"), + href = Href("/978-1503222687/search?query=apple")!!, + mediaType = MediaType("application/vnd.readium.locators+json")!! + ), + Link( + rels = setOf("next"), + href = Href("/978-1503222687/search?query=apple&page=2")!!, + mediaType = MediaType("application/vnd.readium.locators+json")!! + ) ), locators = listOf( Locator( - href = "/978-1503222687/chap7.html", - type = "application/xhtml+xml", + href = Url("/978-1503222687/chap7.html")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( fragments = listOf(":~:text=riddle,-yet%3F'"), progression = 0.43 @@ -343,8 +538,8 @@ class LocatorCollectionTest { ) ), Locator( - href = "/978-1503222687/chap7.html", - type = "application/xhtml+xml", + href = Url("/978-1503222687/chap7.html")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( fragments = listOf(":~:text=in%20asking-,riddles"), progression = 0.47 @@ -485,13 +680,21 @@ class LocatorCollectionTest { ) ), links = listOf( - Link(rels = setOf("self"), href = "/978-1503222687/search?query=apple", type = "application/vnd.readium.locators+json"), - Link(rels = setOf("next"), href = "/978-1503222687/search?query=apple&page=2", type = "application/vnd.readium.locators+json"), + Link( + rels = setOf("self"), + href = Href("/978-1503222687/search?query=apple")!!, + mediaType = MediaType("application/vnd.readium.locators+json")!! + ), + Link( + rels = setOf("next"), + href = Href("/978-1503222687/search?query=apple&page=2")!!, + mediaType = MediaType("application/vnd.readium.locators+json")!! + ) ), locators = listOf( Locator( - href = "/978-1503222687/chap7.html", - type = "application/xhtml+xml", + href = Url("/978-1503222687/chap7.html")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( fragments = listOf(":~:text=riddle,-yet%3F'"), progression = 0.43 @@ -503,8 +706,8 @@ class LocatorCollectionTest { ) ), Locator( - href = "/978-1503222687/chap7.html", - type = "application/xhtml+xml", + href = Url("/978-1503222687/chap7.html")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( fragments = listOf(":~:text=in%20asking-,riddles"), progression = 0.47 diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/ManifestTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/ManifestTest.kt index f91a3865d4..53604a6733 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/ManifestTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/ManifestTest.kt @@ -15,6 +15,8 @@ import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.assertJSONEquals +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -46,11 +48,20 @@ class ManifestTest { Manifest( context = listOf("https://readium.org/webpub-manifest/context.jsonld"), metadata = Metadata(localizedTitle = LocalizedString("Title")), - links = listOf(Link(href = "/manifest.json", rels = setOf("self"))), - readingOrder = listOf(Link(href = "/chap1.html", type = "text/html")), - resources = listOf(Link(href = "/image.png", type = "image/png")), - tableOfContents = listOf(Link(href = "/cover.html"), Link(href = "/chap1.html")), - subcollections = mapOf("sub" to listOf(PublicationCollection(links = listOf(Link(href = "/sublink"))))) + links = listOf(Link(href = Href("manifest.json")!!, rels = setOf("self"))), + readingOrder = listOf( + Link(href = Href("chap1.html")!!, mediaType = MediaType.HTML) + ), + resources = listOf(Link(href = Href("image.png")!!, mediaType = MediaType.PNG)), + tableOfContents = listOf( + Link(href = Href("cover.html")!!), + Link(href = Href("chap1.html")!!) + ), + subcollections = mapOf( + "sub" to listOf( + PublicationCollection(links = listOf(Link(href = Href("sublink")!!))) + ) + ) ), Manifest.fromJSON( JSONObject( @@ -58,21 +69,21 @@ class ManifestTest { "@context": "https://readium.org/webpub-manifest/context.jsonld", "metadata": {"title": "Title"}, "links": [ - {"href": "/manifest.json", "rel": "self"} + {"href": "manifest.json", "rel": "self"} ], "readingOrder": [ - {"href": "/chap1.html", "type": "text/html"} + {"href": "chap1.html", "type": "text/html"} ], "resources": [ - {"href": "/image.png", "type": "image/png"} + {"href": "image.png", "type": "image/png"} ], "toc": [ - {"href": "/cover.html"}, - {"href": "/chap1.html"} + {"href": "cover.html"}, + {"href": "chap1.html"} ], "sub": { "links": [ - {"href": "/sublink"} + {"href": "sublink"} ] } }""" @@ -87,8 +98,10 @@ class ManifestTest { Manifest( context = listOf("context1", "context2"), metadata = Metadata(localizedTitle = LocalizedString("Title")), - links = listOf(Link(href = "/manifest.json", rels = setOf("self"))), - readingOrder = listOf(Link(href = "/chap1.html", type = "text/html")) + links = listOf(Link(href = Href("manifest.json")!!, rels = setOf("self"))), + readingOrder = listOf( + Link(href = Href("chap1.html")!!, mediaType = MediaType.HTML) + ) ), Manifest.fromJSON( JSONObject( @@ -96,10 +109,10 @@ class ManifestTest { "@context": ["context1", "context2"], "metadata": {"title": "Title"}, "links": [ - {"href": "/manifest.json", "rel": "self"} + {"href": "manifest.json", "rel": "self"} ], "readingOrder": [ - {"href": "/chap1.html", "type": "text/html"} + {"href": "chap1.html", "type": "text/html"} ] }""" ) @@ -114,10 +127,10 @@ class ManifestTest { JSONObject( """{ "links": [ - {"href": "/manifest.json", "rel": "self"} + {"href": "manifest.json", "rel": "self"} ], "readingOrder": [ - {"href": "/chap1.html", "type": "text/html"} + {"href": "chap1.html", "type": "text/html"} ] }""" ) @@ -131,18 +144,20 @@ class ManifestTest { assertEquals( Manifest( metadata = Metadata(localizedTitle = LocalizedString("Title")), - links = listOf(Link(href = "/manifest.json", rels = setOf("self"))), - readingOrder = listOf(Link(href = "/chap1.html", type = "text/html")) + links = listOf(Link(href = Href("manifest.json")!!, rels = setOf("self"))), + readingOrder = listOf( + Link(href = Href("chap1.html")!!, mediaType = MediaType.HTML) + ) ), Manifest.fromJSON( JSONObject( """{ "metadata": {"title": "Title"}, "links": [ - {"href": "/manifest.json", "rel": "self"} + {"href": "manifest.json", "rel": "self"} ], "spine": [ - {"href": "/chap1.html", "type": "text/html"} + {"href": "chap1.html", "type": "text/html"} ] }""" ) @@ -155,19 +170,21 @@ class ManifestTest { assertEquals( Manifest( metadata = Metadata(localizedTitle = LocalizedString("Title")), - links = listOf(Link(href = "/manifest.json", rels = setOf("self"))), - readingOrder = listOf(Link(href = "/chap1.html", type = "text/html")) + links = listOf(Link(href = Href("manifest.json")!!, rels = setOf("self"))), + readingOrder = listOf( + Link(href = Href("chap1.html")!!, mediaType = MediaType.HTML) + ) ), Manifest.fromJSON( JSONObject( """{ "metadata": {"title": "Title"}, "links": [ - {"href": "/manifest.json", "rel": "self"} + {"href": "manifest.json", "rel": "self"} ], "readingOrder": [ - {"href": "/chap1.html", "type": "text/html"}, - {"href": "/chap2.html"} + {"href": "chap1.html", "type": "text/html"}, + {"href": "chap2.html"} ] }""" ) @@ -180,23 +197,25 @@ class ManifestTest { assertEquals( Manifest( metadata = Metadata(localizedTitle = LocalizedString("Title")), - links = listOf(Link(href = "/manifest.json", rels = setOf("self"))), - readingOrder = listOf(Link(href = "/chap1.html", type = "text/html")), - resources = listOf(Link(href = "/withtype", type = "text/html")) + links = listOf(Link(href = Href("manifest.json")!!, rels = setOf("self"))), + readingOrder = listOf( + Link(href = Href("chap1.html")!!, mediaType = MediaType.HTML) + ), + resources = listOf(Link(href = Href("withtype")!!, mediaType = MediaType.HTML)) ), Manifest.fromJSON( JSONObject( """{ "metadata": {"title": "Title"}, "links": [ - {"href": "/manifest.json", "rel": "self"} + {"href": "manifest.json", "rel": "self"} ], "readingOrder": [ - {"href": "/chap1.html", "type": "text/html"} + {"href": "chap1.html", "type": "text/html"} ], "resources": [ - {"href": "/withtype", "type": "text/html"}, - {"href": "/withouttype"} + {"href": "withtype", "type": "text/html"}, + {"href": "withouttype"} ] }""" ) @@ -230,22 +249,22 @@ class ManifestTest { "@context": ["https://readium.org/webpub-manifest/context.jsonld"], "metadata": {"title": {"und": "Title"}, "readingProgression": "auto"}, "links": [ - {"href": "/manifest.json", "rel": ["self"], "templated": false} + {"href": "manifest.json", "rel": ["self"], "templated": false} ], "readingOrder": [ - {"href": "/chap1.html", "type": "text/html", "templated": false} + {"href": "chap1.html", "type": "text/html", "templated": false} ], "resources": [ - {"href": "/image.png", "type": "image/png", "templated": false} + {"href": "image.png", "type": "image/png", "templated": false} ], "toc": [ - {"href": "/cover.html", "templated": false}, - {"href": "/chap1.html", "templated": false} + {"href": "cover.html", "templated": false}, + {"href": "chap1.html", "templated": false} ], "sub": { "metadata": {}, "links": [ - {"href": "/sublink", "templated": false} + {"href": "sublink", "templated": false} ] } }""" @@ -253,155 +272,118 @@ class ManifestTest { Manifest( context = listOf("https://readium.org/webpub-manifest/context.jsonld"), metadata = Metadata(localizedTitle = LocalizedString("Title")), - links = listOf(Link(href = "/manifest.json", rels = setOf("self"))), - readingOrder = listOf(Link(href = "/chap1.html", type = "text/html")), - resources = listOf(Link(href = "/image.png", type = "image/png")), - tableOfContents = listOf(Link(href = "/cover.html"), Link(href = "/chap1.html")), - subcollections = mapOf("sub" to listOf(PublicationCollection(links = listOf(Link(href = "/sublink"))))) - ).toJSON() - ) - } - - @Test - fun `self link is replaced when parsing a package`() { - assertEquals( - Manifest( - metadata = Metadata(localizedTitle = LocalizedString("Title")), - links = listOf(Link(href = "/manifest.json", rels = setOf("alternate"))) - ), - Manifest.fromJSON( - JSONObject( - """{ - "metadata": {"title": "Title"}, - "links": [ - {"href": "/manifest.json", "rel": ["self"], "templated": false} - ] - }""" + links = listOf(Link(href = Href("manifest.json")!!, rels = setOf("self"))), + readingOrder = listOf( + Link(href = Href("chap1.html")!!, mediaType = MediaType.HTML) ), - packaged = true - ) - ) - } - - @Test - fun `self link is kept when parsing a remote manifest`() { - assertEquals( - Manifest( - metadata = Metadata(localizedTitle = LocalizedString("Title")), - links = listOf(Link(href = "/manifest.json", rels = setOf("self"))) - ), - Manifest.fromJSON( - JSONObject( - """{ - "metadata": {"title": "Title"}, - "links": [ - {"href": "/manifest.json", "rel": ["self"]} - ] - }""" + resources = listOf(Link(href = Href("image.png")!!, mediaType = MediaType.PNG)), + tableOfContents = listOf( + Link(href = Href("cover.html")!!), + Link(href = Href("chap1.html")!!) + ), + subcollections = mapOf( + "sub" to listOf( + PublicationCollection(links = listOf(Link(href = Href("sublink")!!))) + ) ) - ) - ) - } - - @Test - fun `href are resolved to root when parsing a package`() { - val json = JSONObject( - """{ - "metadata": {"title": "Title"}, - "links": [ - {"href": "http://example.com/manifest.json", "rel": ["self"], "templated": false} - ], - "readingOrder": [ - {"href": "chap1.html", "type": "text/html", "templated": false} - ] - }""" - ) - - assertEquals( - "/chap1.html", - Manifest.fromJSON(json, packaged = true)?.readingOrder?.first()?.href - ) - } - - @Test - fun `href are resolved to self link when parsing a remote manifest`() { - val json = JSONObject( - """{ - "metadata": {"title": "Title"}, - "links": [ - {"href": "http://example.com/directory/manifest.json", "rel": ["self"], "templated": false} - ], - "readingOrder": [ - {"href": "chap1.html", "type": "text/html", "templated": false} - ] - }""" - ) - - assertEquals( - "http://example.com/directory/chap1.html", - Manifest.fromJSON(json)?.readingOrder?.first()?.href + ).toJSON() ) } @Test fun `get a {Locator} from a minimal {Link}`() { val manifest = Manifest( metadata = Metadata(localizedTitle = LocalizedString()), - readingOrder = listOf(Link(href = "/href", type = "text/html", title = "Resource")) + readingOrder = listOf( + Link(href = Href("href")!!, mediaType = MediaType.HTML, title = "Resource") + ) ) Assert.assertEquals( - Locator(href = "/href", type = "text/html", title = "Resource", locations = Locator.Locations(progression = 0.0)), - manifest.locatorFromLink(Link(href = "/href")) + Locator( + href = Url("href")!!, + mediaType = MediaType.HTML, + title = "Resource", + locations = Locator.Locations(progression = 0.0) + ), + manifest.locatorFromLink(Link(href = Href("href")!!)) ) } @Test fun `get a {Locator} from a link in the reading order, resources or links`() { val manifest = Manifest( metadata = Metadata(localizedTitle = LocalizedString()), - readingOrder = listOf(Link(href = "/href1", type = "text/html")), - resources = listOf(Link(href = "/href2", type = "text/html")), - links = listOf(Link(href = "/href3", type = "text/html")), + readingOrder = listOf(Link(href = Href("href1")!!, mediaType = MediaType.HTML)), + resources = listOf(Link(href = Href("href2")!!, mediaType = MediaType.HTML)), + links = listOf(Link(href = Href("href3")!!, mediaType = MediaType.HTML)) ) Assert.assertEquals( - Locator(href = "/href1", type = "text/html", locations = Locator.Locations(progression = 0.0)), - manifest.locatorFromLink(Link(href = "/href1")) + Locator( + href = Url("href1")!!, + mediaType = MediaType.HTML, + locations = Locator.Locations(progression = 0.0) + ), + manifest.locatorFromLink(Link(href = Href("href1")!!)) ) Assert.assertEquals( - Locator(href = "/href2", type = "text/html", locations = Locator.Locations(progression = 0.0)), - manifest.locatorFromLink(Link(href = "/href2")) + Locator( + href = Url("href2")!!, + mediaType = MediaType.HTML, + locations = Locator.Locations(progression = 0.0) + ), + manifest.locatorFromLink(Link(href = Href("href2")!!)) ) Assert.assertEquals( - Locator(href = "/href3", type = "text/html", locations = Locator.Locations(progression = 0.0)), - manifest.locatorFromLink(Link(href = "/href3")) + Locator( + href = Url("href3")!!, + mediaType = MediaType.HTML, + locations = Locator.Locations(progression = 0.0) + ), + manifest.locatorFromLink(Link(href = Href("href3")!!)) ) } @Test fun `get a {Locator} from a full {Link} with fragment`() { val manifest = Manifest( metadata = Metadata(localizedTitle = LocalizedString()), - readingOrder = listOf(Link(href = "/href", type = "text/html", title = "Resource")) + readingOrder = listOf( + Link(href = Href("href")!!, mediaType = MediaType.HTML, title = "Resource") + ) ) Assert.assertEquals( - Locator(href = "/href", type = "text/html", title = "Resource", locations = Locator.Locations(fragments = listOf("page=42"))), - manifest.locatorFromLink(Link(href = "/href#page=42", type = "text/xml", title = "My link")) + Locator( + href = Url("href")!!, + mediaType = MediaType.HTML, + title = "Resource", + locations = Locator.Locations(fragments = listOf("page=42")) + ), + manifest.locatorFromLink( + Link(href = Href("href#page=42")!!, mediaType = MediaType.XML, title = "My link") + ) ) } @Test fun `get a {Locator} falling back on the {Link} title`() { val manifest = Manifest( metadata = Metadata(localizedTitle = LocalizedString()), - readingOrder = listOf(Link(href = "/href", type = "text/html")) + readingOrder = listOf(Link(href = Href("href")!!, mediaType = MediaType.HTML)) ) Assert.assertEquals( - Locator(href = "/href", type = "text/html", title = "My link", locations = Locator.Locations(fragments = listOf("page=42"))), - manifest.locatorFromLink(Link(href = "/href#page=42", type = "text/xml", title = "My link")) + Locator( + href = Url("href")!!, + mediaType = MediaType.HTML, + title = "My link", + locations = Locator.Locations(fragments = listOf("page=42")) + ), + manifest.locatorFromLink( + Link(href = Href("href#page=42")!!, mediaType = MediaType.XML, title = "My link") + ) ) } @Test fun `get a {Locator} from a {Link} not found in the manifest`() { val manifest = Manifest( metadata = Metadata(localizedTitle = LocalizedString()), - readingOrder = listOf(Link(href = "/href", type = "text/html")) + readingOrder = listOf(Link(href = Href("href")!!, mediaType = MediaType.HTML)) ) - Assert.assertNull(manifest.locatorFromLink(Link(href = "notfound"))) + Assert.assertNull(manifest.locatorFromLink(Link(href = Href("notfound")!!))) } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/MetadataTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/MetadataTest.kt index 7d5d8e7f8d..7729ee16a4 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/MetadataTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/MetadataTest.kt @@ -150,7 +150,7 @@ class MetadataTest { assertEquals( Metadata( conformsTo = setOf(Publication.Profile.DIVINA), - localizedTitle = LocalizedString("Title"), + localizedTitle = LocalizedString("Title") ), Metadata.fromJSON( JSONObject( @@ -180,10 +180,6 @@ class MetadataTest { ) } - @Test fun `parse JSON requires {title}`() { - assertNull(Metadata.fromJSON(JSONObject("{'duration': 4.24}"))) - } - @Test fun `parse JSON {duration} requires positive`() { assertEquals( Metadata(localizedTitle = LocalizedString("t")), @@ -325,15 +321,19 @@ class MetadataTest { } @Test fun `get primary language with no language`() { - assertNull(createMetadata(languages = listOf(), readingProgression = ReadingProgression.AUTO).language) - assertNull(createMetadata(languages = listOf(), readingProgression = ReadingProgression.LTR).language) - assertNull(createMetadata(languages = listOf(), readingProgression = ReadingProgression.RTL).language) + assertNull(createMetadata(languages = listOf(), readingProgression = null).language) + assertNull( + createMetadata(languages = listOf(), readingProgression = ReadingProgression.LTR).language + ) + assertNull( + createMetadata(languages = listOf(), readingProgression = ReadingProgression.RTL).language + ) } @Test fun `get primary language with a single language`() { assertEquals( Language("en"), - createMetadata(languages = listOf("en"), readingProgression = ReadingProgression.AUTO).language + createMetadata(languages = listOf("en"), readingProgression = null).language ) assertEquals( Language("en"), @@ -345,6 +345,10 @@ class MetadataTest { ) } - private fun createMetadata(languages: List<String>, readingProgression: ReadingProgression): Metadata = - Metadata(localizedTitle = LocalizedString("Title"), languages = languages, readingProgression = readingProgression) + private fun createMetadata(languages: List<String>, readingProgression: ReadingProgression?): Metadata = + Metadata( + localizedTitle = LocalizedString("Title"), + languages = languages, + readingProgression = readingProgression + ) } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationCollectionTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationCollectionTest.kt index 8796489503..a4ce6c177d 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationCollectionTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationCollectionTest.kt @@ -23,7 +23,7 @@ class PublicationCollectionTest { @Test fun `parse minimal JSON`() { assertEquals( PublicationCollection( - links = listOf(Link(href = "/link")) + links = listOf(Link(href = Href("/link")!!)) ), PublicationCollection.fromJSON( JSONObject( @@ -40,13 +40,22 @@ class PublicationCollectionTest { assertEquals( PublicationCollection( metadata = mapOf("metadata1" to "value"), - links = listOf(Link(href = "/link")), + links = listOf(Link(href = Href("/link")!!)), subcollections = mapOf( - "sub1" to listOf(PublicationCollection(links = listOf(Link(href = "/sublink")))), - "sub2" to listOf(PublicationCollection(links = listOf(Link(href = "/sublink1"), Link(href = "/sublink2")))), + "sub1" to listOf( + PublicationCollection(links = listOf(Link(href = Href("/sublink")!!))) + ), + "sub2" to listOf( + PublicationCollection( + links = listOf( + Link(href = Href("/sublink1")!!), + Link(href = Href("/sublink2")!!) + ) + ) + ), "sub3" to listOf( - PublicationCollection(links = listOf(Link(href = "/sublink3"))), - PublicationCollection(links = listOf(Link(href = "/sublink4"))) + PublicationCollection(links = listOf(Link(href = Href("/sublink3")!!))), + PublicationCollection(links = listOf(Link(href = Href("/sublink4")!!))) ) ) ), @@ -93,11 +102,20 @@ class PublicationCollectionTest { @Test fun `parse multiple JSON collections`() { assertEquals( mapOf( - "sub1" to listOf(PublicationCollection(links = listOf(Link(href = "/sublink")))), - "sub2" to listOf(PublicationCollection(links = listOf(Link(href = "/sublink1"), Link(href = "/sublink2")))), + "sub1" to listOf( + PublicationCollection(links = listOf(Link(href = Href("/sublink")!!))) + ), + "sub2" to listOf( + PublicationCollection( + links = listOf( + Link(href = Href("/sublink1")!!), + Link(href = Href("/sublink2")!!) + ) + ) + ), "sub3" to listOf( - PublicationCollection(links = listOf(Link(href = "/sublink3"))), - PublicationCollection(links = listOf(Link(href = "/sublink4"))) + PublicationCollection(links = listOf(Link(href = Href("/sublink3")!!))), + PublicationCollection(links = listOf(Link(href = Href("/sublink4")!!))) ) ), PublicationCollection.collectionsFromJSON( @@ -138,7 +156,7 @@ class PublicationCollectionTest { "links": [{"href": "/link", "templated": false}] }""" ), - PublicationCollection(links = listOf(Link(href = "/link"))).toJSON() + PublicationCollection(links = listOf(Link(href = Href("/link")!!))).toJSON() ) } @@ -183,13 +201,22 @@ class PublicationCollectionTest { ), PublicationCollection( metadata = mapOf("metadata1" to "value"), - links = listOf(Link(href = "/link")), + links = listOf(Link(href = Href("/link")!!)), subcollections = mapOf( - "sub1" to listOf(PublicationCollection(links = listOf(Link(href = "/sublink")))), - "sub2" to listOf(PublicationCollection(links = listOf(Link(href = "/sublink1"), Link(href = "/sublink2")))), + "sub1" to listOf( + PublicationCollection(links = listOf(Link(href = Href("/sublink")!!))) + ), + "sub2" to listOf( + PublicationCollection( + links = listOf( + Link(href = Href("/sublink1")!!), + Link(href = Href("/sublink2")!!) + ) + ) + ), "sub3" to listOf( - PublicationCollection(links = listOf(Link(href = "/sublink3"))), - PublicationCollection(links = listOf(Link(href = "/sublink4"))) + PublicationCollection(links = listOf(Link(href = Href("/sublink3")!!))), + PublicationCollection(links = listOf(Link(href = Href("/sublink4")!!))) ) ) ).toJSON() @@ -230,11 +257,20 @@ class PublicationCollectionTest { }""" ), mapOf( - "sub1" to listOf(PublicationCollection(links = listOf(Link(href = "/sublink")))), - "sub2" to listOf(PublicationCollection(links = listOf(Link(href = "/sublink1"), Link(href = "/sublink2")))), + "sub1" to listOf( + PublicationCollection(links = listOf(Link(href = Href("/sublink")!!))) + ), + "sub2" to listOf( + PublicationCollection( + links = listOf( + Link(href = Href("/sublink1")!!), + Link(href = Href("/sublink2")!!) + ) + ) + ), "sub3" to listOf( - PublicationCollection(links = listOf(Link(href = "/sublink3"))), - PublicationCollection(links = listOf(Link(href = "/sublink4"))) + PublicationCollection(links = listOf(Link(href = Href("/sublink3")!!))), + PublicationCollection(links = listOf(Link(href = Href("/sublink4")!!))) ) ).toJSONObject() ) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt index 98defeea8b..cf297e4922 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt @@ -9,22 +9,18 @@ package org.readium.r2.shared.publication -import java.net.URL import kotlinx.coroutines.runBlocking -import org.json.JSONObject import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.readium.r2.shared.Fixtures -import org.readium.r2.shared.fetcher.EmptyFetcher -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.StringResource import org.readium.r2.shared.publication.Publication.Profile import org.readium.r2.shared.publication.services.DefaultLocatorService import org.readium.r2.shared.publication.services.PositionsService import org.readium.r2.shared.publication.services.positions import org.readium.r2.shared.publication.services.positionsByReadingOrder -import org.readium.r2.shared.util.Ref +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.EmptyContainer +import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -34,7 +30,7 @@ class PublicationTest { conformsTo: Set<Profile> = emptySet(), title: String = "Title", language: String = "en", - readingProgression: ReadingProgression = ReadingProgression.AUTO, + readingProgression: ReadingProgression? = null, links: List<Link> = listOf(), readingOrder: List<Link> = emptyList(), resources: List<Link> = emptyList(), @@ -54,30 +50,20 @@ class PublicationTest { servicesBuilder = servicesBuilder ) - @Suppress("DEPRECATION") - @Test fun `get the type computed from the manifest content`() { - val fixtures = Fixtures("format") - fun parseAt(path: String): Publication = - Publication(manifest = Manifest.fromJSON(JSONObject(fixtures.fileAt(path).readText()))!!) - - assertEquals(Publication.TYPE.AUDIO, parseAt("audiobook.json").type) - assertEquals(Publication.TYPE.DiViNa, parseAt("divina.json").type) - assertEquals(Publication.TYPE.WEBPUB, parseAt("webpub.json").type) - assertEquals(Publication.TYPE.WEBPUB, parseAt("opds2-publication.json").type) - } - @Test fun `get the default empty {positions}`() { assertEquals(emptyList<Locator>(), runBlocking { createPublication().positions() }) } @Test fun `get the {positions} computed from the {PositionsService}`() { assertEquals( - listOf(Locator(href = "locator", type = "")), + listOf(Locator(href = Url("locator")!!, mediaType = MediaType.HTML)), createPublication( servicesBuilder = Publication.ServicesBuilder( positions = { object : PositionsService { - override suspend fun positionsByReadingOrder(): List<List<Locator>> = listOf(listOf(Locator(href = "locator", type = ""))) + override suspend fun positionsByReadingOrder(): List<List<Locator>> = listOf( + listOf(Locator(href = Url("locator")!!, mediaType = MediaType.HTML)) + ) } } ) @@ -89,11 +75,11 @@ class PublicationTest { assertEquals( listOf( listOf( - Locator(href = "res1", type = "text/html", title = "Loc A"), - Locator(href = "res1", type = "text/html", title = "Loc B") + Locator(href = Url("res1")!!, mediaType = MediaType.HTML, title = "Loc A"), + Locator(href = Url("res1")!!, mediaType = MediaType.HTML, title = "Loc B") ), listOf( - Locator(href = "res2", type = "text/html", title = "Loc B") + Locator(href = Url("res2")!!, mediaType = MediaType.HTML, title = "Loc B") ) ), createPublication( @@ -102,10 +88,24 @@ class PublicationTest { object : PositionsService { override suspend fun positionsByReadingOrder(): List<List<Locator>> = listOf( listOf( - Locator(href = "res1", type = "text/html", title = "Loc A"), - Locator(href = "res1", type = "text/html", title = "Loc B") + Locator( + href = Url("res1")!!, + mediaType = MediaType.HTML, + title = "Loc A" + ), + Locator( + href = Url("res1")!!, + mediaType = MediaType.HTML, + title = "Loc B" + ) ), - listOf(Locator(href = "res2", type = "text/html", title = "Loc B")) + listOf( + Locator( + href = Url("res2")!!, + mediaType = MediaType.HTML, + title = "Loc B" + ) + ) ) } } @@ -114,34 +114,14 @@ class PublicationTest { ) } - @Test fun `set {self} link`() { - val publication = createPublication() - publication.setSelfLink("http://manifest.json") - - assertEquals( - "http://manifest.json", - publication.linkWithRel("self")?.href - ) - } - - @Test fun `set {self} link replaces existing {self} link`() { - val publication = createPublication( - links = listOf(Link(href = "previous", rels = setOf("self"))) - ) - publication.setSelfLink("http://manifest.json") - - assertEquals( - "http://manifest.json", - publication.linkWithRel("self")?.href - ) - } - @Test fun `get {baseUrl} computes the URL from the {self} link`() { val publication = createPublication( - links = listOf(Link(href = "http://domain.com/path/manifest.json", rels = setOf("self"))) + links = listOf( + Link(href = Href("http://domain.com/path/manifest.json")!!, rels = setOf("self")) + ) ) assertEquals( - URL("http://domain.com/path/"), + Url("http://domain.com/path/manifest.json")!!, publication.baseUrl ) } @@ -150,16 +130,6 @@ class PublicationTest { assertNull(createPublication().baseUrl) } - @Test fun `get {baseUrl} when it's a root`() { - val publication = createPublication( - links = listOf(Link(href = "http://domain.com/manifest.json", rels = setOf("self"))) - ) - assertEquals( - URL("http://domain.com/"), - publication.baseUrl - ) - } - @Test fun `conforms to the given profile`() { // An empty reading order doesn't conform to anything. assertFalse( @@ -170,24 +140,24 @@ class PublicationTest { assertTrue( createPublication( readingOrder = listOf( - Link(href = "c1.mp3", type = "audio/mpeg"), - Link(href = "c2.aac", type = "audio/aac"), + Link(href = Href("c1.mp3")!!, mediaType = MediaType.MP3), + Link(href = Href("c2.aac")!!, mediaType = MediaType.AAC) ) ).conformsTo(Profile.AUDIOBOOK) ) assertTrue( createPublication( readingOrder = listOf( - Link(href = "c1.jpg", type = "image/jpeg"), - Link(href = "c2.png", type = "image/png"), + Link(href = Href("c1.jpg")!!, mediaType = MediaType.JPEG), + Link(href = Href("c2.png")!!, mediaType = MediaType.PNG) ) ).conformsTo(Profile.DIVINA) ) assertTrue( createPublication( readingOrder = listOf( - Link(href = "c1.pdf", type = "application/pdf"), - Link(href = "c2.pdf", type = "application/pdf"), + Link(href = Href("c1.pdf")!!, mediaType = MediaType.PDF), + Link(href = Href("c2.pdf")!!, mediaType = MediaType.PDF) ) ).conformsTo(Profile.PDF) ) @@ -196,16 +166,16 @@ class PublicationTest { assertFalse( createPublication( readingOrder = listOf( - Link(href = "c1.mp3", type = "audio/mpeg"), - Link(href = "c2.jpg", type = "image/jpeg"), + Link(href = Href("c1.mp3")!!, mediaType = MediaType.MP3), + Link(href = Href("c2.jpg")!!, mediaType = MediaType.JPEG) ) ).conformsTo(Profile.AUDIOBOOK) ) assertFalse( createPublication( readingOrder = listOf( - Link(href = "c1.mp3", type = "audio/mpeg"), - Link(href = "c2.jpg", type = "image/jpeg"), + Link(href = Href("c1.mp3")!!, mediaType = MediaType.MP3), + Link(href = Href("c2.jpg")!!, mediaType = MediaType.JPEG) ) ).conformsTo(Profile.DIVINA) ) @@ -214,8 +184,8 @@ class PublicationTest { assertTrue( createPublication( readingOrder = listOf( - Link(href = "c1.xhtml", type = "application/xhtml+xml"), - Link(href = "c2.xhtml", type = "application/xhtml+xml"), + Link(href = Href("c1.xhtml")!!, mediaType = MediaType.XHTML), + Link(href = Href("c2.xhtml")!!, mediaType = MediaType.XHTML) ), conformsTo = setOf(Profile.EPUB) ).conformsTo(Profile.EPUB) @@ -223,8 +193,8 @@ class PublicationTest { assertTrue( createPublication( readingOrder = listOf( - Link(href = "c1.html", type = "text/html"), - Link(href = "c2.html", type = "text/html"), + Link(href = Href("c1.html")!!, mediaType = MediaType.HTML), + Link(href = Href("c2.html")!!, mediaType = MediaType.HTML) ), conformsTo = setOf(Profile.EPUB) ).conformsTo(Profile.EPUB) @@ -232,24 +202,24 @@ class PublicationTest { assertFalse( createPublication( readingOrder = listOf( - Link(href = "c1.xhtml", type = "application/xhtml+xml"), - Link(href = "c2.xhtml", type = "application/xhtml+xml"), + Link(href = Href("c1.xhtml")!!, mediaType = MediaType.XHTML), + Link(href = Href("c2.xhtml")!!, mediaType = MediaType.XHTML) ) ).conformsTo(Profile.EPUB) ) assertFalse( createPublication( readingOrder = listOf( - Link(href = "c1.html", type = "text/html"), - Link(href = "c2.html", type = "text/html"), + Link(href = Href("c1.html")!!, mediaType = MediaType.HTML), + Link(href = Href("c2.html")!!, mediaType = MediaType.HTML) ) ).conformsTo(Profile.EPUB) ) assertFalse( createPublication( readingOrder = listOf( - Link(href = "c1.pdf", type = "application/pdf"), - Link(href = "c2.pdf", type = "application/pdf"), + Link(href = Href("c1.pdf")!!, mediaType = MediaType.PDF), + Link(href = Href("c2.pdf")!!, mediaType = MediaType.PDF) ), conformsTo = setOf(Profile.EPUB) ).conformsTo(Profile.EPUB) @@ -259,8 +229,8 @@ class PublicationTest { assertTrue( createPublication( readingOrder = listOf( - Link(href = "c1.mp3", type = "audio/mpeg"), - Link(href = "c2.aac", type = "audio/aac"), + Link(href = Href("c1.mp3")!!, mediaType = MediaType.MP3), + Link(href = Href("c2.aac")!!, mediaType = MediaType.AAC) ), conformsTo = setOf(Profile.DIVINA) ).conformsTo(Profile.AUDIOBOOK) @@ -268,8 +238,8 @@ class PublicationTest { assertFalse( createPublication( readingOrder = listOf( - Link(href = "c1.mp3", type = "audio/mpeg"), - Link(href = "c2.aac", type = "audio/aac"), + Link(href = Href("c1.mp3")!!, mediaType = MediaType.MP3), + Link(href = Href("c2.aac")!!, mediaType = MediaType.AAC) ), conformsTo = setOf(Profile.DIVINA) ).conformsTo(Profile.DIVINA) @@ -279,20 +249,20 @@ class PublicationTest { val profile = Profile("http://extension") assertTrue( createPublication( - readingOrder = listOf(Link(href = "file")), + readingOrder = listOf(Link(href = Href("file")!!)), conformsTo = setOf(profile) ).conformsTo(profile) ) } @Test fun `find the first {Link} with the given {rel}`() { - val link1 = Link(href = "found", rels = setOf("rel1")) - val link2 = Link(href = "found", rels = setOf("rel2")) - val link3 = Link(href = "found", rels = setOf("rel3")) + val link1 = Link(href = Href("found")!!, rels = setOf("rel1")) + val link2 = Link(href = Href("found")!!, rels = setOf("rel2")) + val link3 = Link(href = Href("found")!!, rels = setOf("rel3")) val publication = createPublication( - links = listOf(Link(href = "other"), link1), - readingOrder = listOf(Link(href = "other"), link2), - resources = listOf(Link(href = "other"), link3) + links = listOf(Link(href = Href("other")!!), link1), + readingOrder = listOf(Link(href = Href("other")!!), link2), + resources = listOf(Link(href = Href("other")!!), link3) ) assertEquals(link1, publication.linkWithRel("rel1")) @@ -307,29 +277,29 @@ class PublicationTest { @Test fun `find all the links with the given {rel}`() { val publication = createPublication( links = listOf( - Link(href = "l1"), - Link(href = "l2", rels = setOf("rel1")) + Link(href = Href("l1")!!), + Link(href = Href("l2")!!, rels = setOf("rel1")) ), readingOrder = listOf( - Link(href = "l3"), - Link(href = "l4", rels = setOf("rel1")) + Link(href = Href("l3")!!), + Link(href = Href("l4")!!, rels = setOf("rel1")) ), resources = listOf( Link( - href = "l5", + href = Href("l5")!!, alternates = listOf( - Link(href = "alternate", rels = setOf("rel1")) + Link(href = Href("alternate")!!, rels = setOf("rel1")) ) ), - Link(href = "l6", rels = setOf("rel1")) + Link(href = Href("l6")!!, rels = setOf("rel1")) ) ) assertEquals( listOf( - Link(href = "l4", rels = setOf("rel1")), - Link(href = "l6", rels = setOf("rel1")), - Link(href = "l2", rels = setOf("rel1")) + Link(href = Href("l4")!!, rels = setOf("rel1")), + Link(href = Href("l6")!!, rels = setOf("rel1")), + Link(href = Href("l2")!!, rels = setOf("rel1")) ), publication.linksWithRel("rel1") ) @@ -340,19 +310,19 @@ class PublicationTest { } @Test fun `find the first {Link} with the given {href}`() { - val link1 = Link(href = "href1") - val link2 = Link(href = "href2") - val link3 = Link(href = "href3") - val link4 = Link(href = "href4") - val link5 = Link(href = "href5") + val link1 = Link(href = Href("href1")!!) + val link2 = Link(href = Href("href2")!!) + val link3 = Link(href = Href("href3")!!) + val link4 = Link(href = Href("href4")!!) + val link5 = Link(href = Href("href5")!!) val publication = createPublication( - links = listOf(Link(href = "other"), link1), + links = listOf(Link(href = Href("other")!!), link1), readingOrder = listOf( Link( - href = "other", + href = Href("other")!!, alternates = listOf( Link( - href = "alt1", + href = Href("alt1")!!, alternates = listOf( link2 ) @@ -363,10 +333,10 @@ class PublicationTest { ), resources = listOf( Link( - href = "other", + href = Href("other")!!, children = listOf( Link( - href = "alt1", + href = Href("alt1")!!, children = listOf( link4 ) @@ -377,97 +347,59 @@ class PublicationTest { ) ) - assertEquals(link1, publication.linkWithHref("href1")) - assertEquals(link2, publication.linkWithHref("href2")) - assertEquals(link3, publication.linkWithHref("href3")) - assertEquals(link4, publication.linkWithHref("href4")) - assertEquals(link5, publication.linkWithHref("href5")) + assertEquals(link1, publication.linkWithHref(Url("href1")!!)) + assertEquals(link2, publication.linkWithHref(Url("href2")!!)) + assertEquals(link3, publication.linkWithHref(Url("href3")!!)) + assertEquals(link4, publication.linkWithHref(Url("href4")!!)) + assertEquals(link5, publication.linkWithHref(Url("href5")!!)) } @Test fun `find the first {Link} with the given {href} without query parameters`() { - val link = Link(href = "http://example.com/index.html") + val link = Link(href = Href("http://example.com/index.html")!!) val publication = createPublication( - readingOrder = listOf(Link(href = "other"), link) + readingOrder = listOf(Link(href = Href("other")!!), link) ) - assertEquals(link, publication.linkWithHref("http://example.com/index.html?title=titre&action=edit")) + assertEquals( + link, + publication.linkWithHref(Url("http://example.com/index.html?title=titre&action=edit")!!) + ) } @Test fun `find the first {Link} with the given {href} without anchor`() { - val link = Link(href = "http://example.com/index.html") + val link = Link(href = Href("http://example.com/index.html")!!) val publication = createPublication( - readingOrder = listOf(Link(href = "other"), link) + readingOrder = listOf(Link(href = Href("other")!!), link) ) - assertEquals(link, publication.linkWithHref("http://example.com/index.html#sec1")) + assertEquals(link, publication.linkWithHref(Url("http://example.com/index.html#sec1")!!)) } @Test fun `find the first {Link} with the given {href} when missing`() { - assertNull(createPublication().linkWithHref("foobar")) - } - - @Test fun `get method passes on href parameters to services`() { - val service = object : Publication.Service { - override fun get(link: Link): Resource? { - assertFalse(link.templated) - assertEquals("param1=a¶m2=b", link.href.substringAfter("?")) - return StringResource(link, "test passed") - } - } - - val link = Link(href = "link?param1=a¶m2=b") - val publication = createPublication( - resources = listOf(link), - servicesBuilder = Publication.ServicesBuilder( - positions = { service } - ) - ) - assertEquals("test passed", runBlocking { publication.get(link).readAsString().getOrNull() }) + assertNull(createPublication().linkWithHref(Url("foobar")!!)) } @Test fun `find the first resource {Link} with the given {href}`() { - val link1 = Link(href = "href1") - val link2 = Link(href = "href2") - val link3 = Link(href = "href3") + val link1 = Link(href = Href("href1")!!) + val link2 = Link(href = Href("href2")!!) + val link3 = Link(href = Href("href3")!!) val publication = createPublication( - links = listOf(Link(href = "other"), link1), - readingOrder = listOf(Link(href = "other"), link2), - resources = listOf(Link(href = "other"), link3) + links = listOf(Link(href = Href("other")!!), link1), + readingOrder = listOf(Link(href = Href("other")!!), link2), + resources = listOf(Link(href = Href("other")!!), link3) ) - assertEquals(link1, publication.linkWithHref("href1")) - assertEquals(link2, publication.linkWithHref("href2")) - assertEquals(link3, publication.linkWithHref("href3")) + assertEquals(link1, publication.linkWithHref(Url("href1")!!)) + assertEquals(link2, publication.linkWithHref(Url("href2")!!)) + assertEquals(link3, publication.linkWithHref(Url("href3")!!)) } @Test fun `find the first resource {Link} with the given {href} when missing`() { - assertNull(createPublication().linkWithHref("foobar")) - } - - @Suppress("DEPRECATION") - @Test fun `find the cover {Link}`() { - val coverLink = Link(href = "cover", rels = setOf("cover")) - val publication = createPublication( - links = listOf(Link(href = "other"), coverLink), - readingOrder = listOf(Link(href = "other")), - resources = listOf(Link(href = "other")) - ) - - assertEquals(coverLink, publication.coverLink) - } - - @Suppress("DEPRECATION") - @Test fun `find the cover {Link} when missing`() { - val publication = createPublication( - links = listOf(Link(href = "other")), - readingOrder = listOf(Link(href = "other")), - resources = listOf(Link(href = "other")) - ) - - assertNull(publication.coverLink) + assertNull(createPublication().linkWithHref(Url("foobar")!!)) } } +@RunWith(RobolectricTestRunner::class) class ServicesBuilderTest { open class FooService : Publication.Service @@ -479,9 +411,8 @@ class ServicesBuilderTest { class BarServiceA : BarService() private val context = Publication.Service.Context( - publication = Ref(), manifest = Manifest(metadata = Metadata(localizedTitle = LocalizedString())), - fetcher = EmptyFetcher(), + container = EmptyContainer(), services = ListPublicationServicesHolder() ) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/ReadingProgressionTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/ReadingProgressionTest.kt index 2900af969a..dcc1c1c963 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/ReadingProgressionTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/ReadingProgressionTest.kt @@ -10,26 +10,13 @@ class ReadingProgressionTest { assertEquals(ReadingProgression.LTR, ReadingProgression("LTR")) assertEquals(ReadingProgression.LTR, ReadingProgression("ltr")) assertEquals(ReadingProgression.RTL, ReadingProgression("rtl")) - assertEquals(ReadingProgression.TTB, ReadingProgression("ttb")) - assertEquals(ReadingProgression.BTT, ReadingProgression("btt")) - assertEquals(ReadingProgression.AUTO, ReadingProgression("auto")) - assertEquals(ReadingProgression.AUTO, ReadingProgression("foobar")) - assertEquals(ReadingProgression.AUTO, ReadingProgression(null)) + assertEquals(null, ReadingProgression("auto")) + assertEquals(null, ReadingProgression("foobar")) + assertEquals(null, ReadingProgression(null)) } @Test fun `get reading progression value`() { assertEquals("ltr", ReadingProgression.LTR.value) assertEquals("rtl", ReadingProgression.RTL.value) - assertEquals("ttb", ReadingProgression.TTB.value) - assertEquals("btt", ReadingProgression.BTT.value) - assertEquals("auto", ReadingProgression.AUTO.value) - } - - @Test fun `is horizontal`() { - assertNull(ReadingProgression.AUTO.isHorizontal) - assertTrue(ReadingProgression.LTR.isHorizontal!!) - assertTrue(ReadingProgression.RTL.isHorizontal!!) - assertFalse(ReadingProgression.TTB.isHorizontal!!) - assertFalse(ReadingProgression.BTT.isHorizontal!!) } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/SubjectTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/SubjectTest.kt index a9e7cb4d79..78fd6c3bff 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/SubjectTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/SubjectTest.kt @@ -44,8 +44,8 @@ class SubjectTest { scheme = "http://scheme", code = "CODE", links = listOf( - Link(href = "pub1"), - Link(href = "pub2") + Link(href = Href("pub1")!!), + Link(href = Href("pub2")!!) ) ), Subject.fromJSON( @@ -181,8 +181,8 @@ class SubjectTest { scheme = "http://scheme", code = "CODE", links = listOf( - Link(href = "pub1"), - Link(href = "pub2") + Link(href = Href("pub1")!!), + Link(href = Href("pub2")!!) ) ).toJSON() ) diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EncryptionParserTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/epub/EncryptionParserTest.kt similarity index 80% rename from readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EncryptionParserTest.kt rename to readium/shared/src/test/java/org/readium/r2/shared/publication/epub/EncryptionParserTest.kt index 4d75a0059a..cae1bba7d5 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EncryptionParserTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/epub/EncryptionParserTest.kt @@ -7,27 +7,28 @@ * LICENSE file present in the project repository where this source code is maintained. */ -package org.readium.r2.streamer.parser.epub +package org.readium.r2.shared.publication.epub import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.entry import org.junit.Test import org.junit.runner.RunWith -import org.readium.r2.shared.parser.xml.XmlParser import org.readium.r2.shared.publication.encryption.Encryption +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.xml.XmlParser import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class EncryptionParserTest { - fun parseEncryption(path: String): Map<String, Encryption> { + fun parseEncryption(path: String): Map<Url, Encryption> { val res = EncryptionParserTest::class.java.getResourceAsStream(path) checkNotNull(res) val document = XmlParser().parse(res) - return EncryptionParser.parse(document) + return EpubEncryptionParser.parse(document) } val lcpChap1 = entry( - "/OEBPS/xhtml/chapter01.xhtml", + Url("OEBPS/xhtml/chapter01.xhtml")!!, Encryption( algorithm = "http://www.w3.org/2001/04/xmlenc#aes256-cbc", compression = "deflate", @@ -38,7 +39,7 @@ class EncryptionParserTest { ) val lcpChap2 = entry( - "/OEBPS/xhtml/chapter02.xhtml", + Url("OEBPS/xhtml/chapter02.xhtml")!!, Encryption( algorithm = "http://www.w3.org/2001/04/xmlenc#aes256-cbc", compression = "none", @@ -50,19 +51,25 @@ class EncryptionParserTest { @Test fun `Check EncryptionParser with namespace prefixes`() { - assertThat(parseEncryption("encryption/encryption-lcp-prefixes.xml")).contains(lcpChap1, lcpChap2) + assertThat(parseEncryption("encryption/encryption-lcp-prefixes.xml")).contains( + lcpChap1, + lcpChap2 + ) } @Test fun `Check EncryptionParser with default namespaces`() { - assertThat(parseEncryption("encryption/encryption-lcp-xmlns.xml")).contains(lcpChap1, lcpChap2) + assertThat(parseEncryption("encryption/encryption-lcp-xmlns.xml")).contains( + lcpChap1, + lcpChap2 + ) } @Test fun `Check EncryptionParser with unknown retrieval method`() { assertThat(parseEncryption("encryption/encryption-unknown-method.xml")).contains( entry( - "/OEBPS/xhtml/chapter.xhtml", + Url("OEBPS/xhtml/chapter.xhtml")!!, Encryption( algorithm = "http://www.w3.org/2001/04/xmlenc#kw-aes128", compression = "deflate", @@ -72,7 +79,7 @@ class EncryptionParserTest { ) ), entry( - "/OEBPS/images/image.jpeg", + Url("OEBPS/images/image.jpeg")!!, Encryption( algorithm = "http://www.w3.org/2001/04/xmlenc#kw-aes128", compression = null, diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/epub/PresentationTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/epub/PresentationTest.kt index eff31a7458..c31dd04e77 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/epub/PresentationTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/epub/PresentationTest.kt @@ -11,10 +11,14 @@ package org.readium.r2.shared.publication.epub import org.junit.Assert.assertEquals import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Properties import org.readium.r2.shared.publication.presentation.Presentation +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class PresentationTest { @Test @@ -58,7 +62,7 @@ class PresentationTest { } private fun createLink(layout: EpubLayout?) = Link( - href = "res", + href = Href("res")!!, properties = Properties( otherProperties = layout?.let { mapOf("layout" to layout.value) } ?: emptyMap() diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/epub/PublicationTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/epub/PublicationTest.kt index 59af76999b..76ef8c5013 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/epub/PublicationTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/epub/PublicationTest.kt @@ -11,8 +11,11 @@ package org.readium.r2.shared.publication.epub import org.junit.Assert.assertEquals import org.junit.Test +import org.junit.runner.RunWith import org.readium.r2.shared.publication.* +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class PublicationTest { private fun createPublication( @@ -25,7 +28,7 @@ class PublicationTest { ) @Test fun `get {pageList}`() { - val links = listOf(Link(href = "/page1.html")) + val links = listOf(Link(href = Href("/page1.html")!!)) assertEquals( links, createPublication( @@ -41,7 +44,7 @@ class PublicationTest { } @Test fun `get {landmarks}`() { - val links = listOf(Link(href = "/landmark.html")) + val links = listOf(Link(href = Href("/landmark.html")!!)) assertEquals( links, createPublication( @@ -57,7 +60,7 @@ class PublicationTest { } @Test fun `get {listOfAudioClips}`() { - val links = listOf(Link(href = "/audio.mp3")) + val links = listOf(Link(href = Href("/audio.mp3")!!)) assertEquals( links, createPublication( @@ -73,7 +76,7 @@ class PublicationTest { } @Test fun `get {listOfIllustrations}`() { - val links = listOf(Link(href = "/image.jpg")) + val links = listOf(Link(href = Href("/image.jpg")!!)) assertEquals( links, createPublication( @@ -89,7 +92,7 @@ class PublicationTest { } @Test fun `get {listOfTables}`() { - val links = listOf(Link(href = "/table.html")) + val links = listOf(Link(href = Href("/table.html")!!)) assertEquals( links, createPublication( @@ -107,7 +110,7 @@ class PublicationTest { } @Test fun `get {listOfVideoClips}`() { - val links = listOf(Link(href = "/video.mov")) + val links = listOf(Link(href = Href("/video.mov")!!)) assertEquals( links, createPublication( diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/opds/PropertiesTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/opds/PropertiesTest.kt index 7ebb65b635..0d32607d0d 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/opds/PropertiesTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/opds/PropertiesTest.kt @@ -14,8 +14,10 @@ import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.opds.* +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Properties +import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -39,7 +41,9 @@ class PropertiesTest { @Test fun `get Properties {price} when available`() { assertEquals( Price(currency = "EUR", value = 4.36), - Properties(otherProperties = mapOf("price" to mapOf("currency" to "EUR", "value" to 4.36))).price + Properties( + otherProperties = mapOf("price" to mapOf("currency" to "EUR", "value" to 4.36)) + ).price ) } @@ -108,14 +112,14 @@ class PropertiesTest { @Test fun `get Properties {authenticate} when available`() { assertEquals( Link( - href = "https://example.com/authentication.json", - type = "application/opds-authentication+json" + href = Href("https://example.com/authentication.json")!!, + mediaType = MediaType("application/opds-authentication+json")!! ), Properties( otherProperties = mapOf( "authenticate" to mapOf( "href" to "https://example.com/authentication.json", - "type" to "application/opds-authentication+json", + "type" to "application/opds-authentication+json" ) ) ).authenticate @@ -127,7 +131,7 @@ class PropertiesTest { Properties( otherProperties = mapOf( "authenticate" to mapOf( - "type" to "application/opds-authentication+json", + "type" to "application/opds-authentication+json" ) ) ).authenticate diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/opds/PublicationTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/opds/PublicationTest.kt index 64a47498d4..2614548695 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/opds/PublicationTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/opds/PublicationTest.kt @@ -11,8 +11,11 @@ package org.readium.r2.shared.publication.opds import org.junit.Assert.assertEquals import org.junit.Test +import org.junit.runner.RunWith import org.readium.r2.shared.publication.* +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class PublicationTest { private fun createPublication( @@ -25,7 +28,7 @@ class PublicationTest { ) @Test fun `get {images}`() { - val links = listOf(Link(href = "/image.png")) + val links = listOf(Link(href = Href("/image.png")!!)) assertEquals( links, createPublication( diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt index 222565778c..5229ece6e4 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt @@ -12,17 +12,18 @@ package org.readium.r2.shared.publication.services import android.graphics.Bitmap import android.graphics.BitmapFactory import android.util.Size -import java.io.File import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith -import org.readium.r2.shared.fetcher.FileFetcher -import org.readium.r2.shared.linkBlocking import org.readium.r2.shared.publication.* -import org.readium.r2.shared.readBlocking +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.file.FileResource +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.SingleResourceContainer +import org.readium.r2.shared.util.toAbsoluteUrl import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -30,7 +31,7 @@ class CoverServiceTest { private val coverBytes: ByteArray private val coverBitmap: Bitmap - private val coverPath: String + private val coverPath: AbsoluteUrl private val coverLink: Link private val publication: Publication @@ -39,8 +40,13 @@ class CoverServiceTest { assertNotNull(cover) coverBytes = cover.readBytes() coverBitmap = BitmapFactory.decodeByteArray(coverBytes, 0, coverBytes.size) - coverPath = cover.path - coverLink = Link(href = coverPath, type = "image/jpeg", width = 598, height = 800) + coverPath = cover.toAbsoluteUrl()!! + coverLink = Link( + href = Href(coverPath), + mediaType = MediaType.JPEG, + width = 598, + height = 800 + ) publication = Publication( manifest = Manifest( @@ -48,27 +54,14 @@ class CoverServiceTest { localizedTitle = LocalizedString("title") ), resources = listOf( - Link(href = coverPath, rels = setOf("cover")) + Link(href = Href(coverPath), rels = setOf("cover")) ) ), - fetcher = FileFetcher(coverPath, File(coverPath)) - ) - } - - @Test - fun `get works fine`() { - val service = InMemoryCoverService(coverBitmap) - val res = service.get(Link("/~readium/cover", rels = setOf("cover"))) - assertNotNull(res) - assertEquals( - Link(href = "/~readium/cover", type = "image/png", width = 598, height = 800, rels = setOf("cover")), - res.linkBlocking() + container = SingleResourceContainer( + coverPath, + FileResource(coverPath.toFile()!!) + ) ) - - val bytes = res.readBlocking().getOrNull() - assertNotNull(bytes) - - assertTrue(BitmapFactory.decodeByteArray(bytes, 0, bytes.size).sameAs(coverBitmap)) } @Test diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/LocatorServiceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/LocatorServiceTest.kt index ed13fb1bc8..0199ed243d 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/LocatorServiceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/LocatorServiceTest.kt @@ -8,13 +8,17 @@ package org.readium.r2.shared.publication.services import kotlin.test.assertEquals import kotlin.test.assertNull -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType +import org.robolectric.RobolectricTestRunner -@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) class LocatorServiceTest { // locate(Locator) checks that the href exists. @@ -22,19 +26,27 @@ class LocatorServiceTest { fun `locate from Locator`() = runTest { val service = createService( readingOrder = listOf( - Link(href = "chap1", type = "application/xml"), - Link(href = "chap2", type = "application/xml"), - Link(href = "chap3", type = "application/xml") + Link(href = Href("chap1")!!, mediaType = MediaType.XML), + Link(href = Href("chap2")!!, mediaType = MediaType.XML), + Link(href = Href("chap3")!!, mediaType = MediaType.XML) ) ) - val locator = Locator(href = "chap2", type = "text/html", text = Locator.Text(highlight = "Highlight")) + val locator = Locator( + href = Url("chap2")!!, + mediaType = MediaType.HTML, + text = Locator.Text(highlight = "Highlight") + ) assertEquals(locator, service.locate(locator)) } @Test fun `locate from Locator with empty reading order`() = runTest { val service = createService(readingOrder = emptyList()) - val locator = Locator(href = "chap2", type = "text/html", text = Locator.Text(highlight = "Highlight")) + val locator = Locator( + href = Url("chap2")!!, + mediaType = MediaType.HTML, + text = Locator.Text(highlight = "Highlight") + ) assertNull(service.locate(locator)) } @@ -42,11 +54,15 @@ class LocatorServiceTest { fun `locate from Locator not found`() = runTest { val service = createService( readingOrder = listOf( - Link(href = "chap1", type = "application/xml"), - Link(href = "chap3", type = "application/xml") + Link(href = Href("chap1")!!, mediaType = MediaType.XML), + Link(href = Href("chap3")!!, mediaType = MediaType.XML) ) ) - val locator = Locator(href = "chap2", type = "text/html", text = Locator.Text(highlight = "Highlight")) + val locator = Locator( + href = Url("chap2")!!, + mediaType = MediaType.HTML, + text = Locator.Text(highlight = "Highlight") + ) assertNull(service.locate(locator)) } @@ -56,8 +72,8 @@ class LocatorServiceTest { assertEquals( Locator( - href = "chap1", - type = "text/html", + href = Url("chap1")!!, + mediaType = MediaType.HTML, locations = Locator.Locations( progression = 0.0, totalProgression = 0.0, @@ -69,8 +85,8 @@ class LocatorServiceTest { assertEquals( Locator( - href = "chap3", - type = "text/html", + href = Url("chap3")!!, + mediaType = MediaType.HTML, title = "Chapter 3", locations = Locator.Locations( progression = 0.0, @@ -86,8 +102,8 @@ class LocatorServiceTest { assertEquals( Locator( - href = "chap4", - type = "text/html", + href = Url("chap4")!!, + mediaType = MediaType.HTML, locations = Locator.Locations( progression = (0.4 - chap4FirstTotalProg) / (chap5FirstTotalProg - chap4FirstTotalProg), totalProgression = 0.4, @@ -99,8 +115,8 @@ class LocatorServiceTest { assertEquals( Locator( - href = "chap4", - type = "text/html", + href = Url("chap4")!!, + mediaType = MediaType.HTML, locations = Locator.Locations( progression = (0.55 - chap4FirstTotalProg) / (chap5FirstTotalProg - chap4FirstTotalProg), totalProgression = 0.55, @@ -112,8 +128,8 @@ class LocatorServiceTest { assertEquals( Locator( - href = "chap5", - type = "text/html", + href = Url("chap5")!!, + mediaType = MediaType.HTML, locations = Locator.Locations( progression = (0.9 - chap5FirstTotalProg) / (1.0 - chap5FirstTotalProg), totalProgression = 0.9, @@ -125,8 +141,8 @@ class LocatorServiceTest { assertEquals( Locator( - href = "chap5", - type = "text/html", + href = Url("chap5")!!, + mediaType = MediaType.HTML, locations = Locator.Locations( progression = 1.0, totalProgression = 1.0, @@ -161,8 +177,8 @@ class LocatorServiceTest { private var positionsFixtures = listOf( listOf( Locator( - href = "chap1", - type = "text/html", + href = Url("chap1")!!, + mediaType = MediaType.HTML, locations = Locator.Locations( progression = 0.0, position = 1, @@ -172,8 +188,8 @@ class LocatorServiceTest { ), listOf( Locator( - href = "chap2", - type = "application/xml", + href = Url("chap2")!!, + mediaType = MediaType.XML, locations = Locator.Locations( progression = 0.0, position = 2, @@ -183,8 +199,8 @@ class LocatorServiceTest { ), listOf( Locator( - href = "chap3", - type = "text/html", + href = Url("chap3")!!, + mediaType = MediaType.HTML, title = "Chapter 3", locations = Locator.Locations( progression = 0.0, @@ -195,8 +211,8 @@ class LocatorServiceTest { ), listOf( Locator( - href = "chap4", - type = "text/html", + href = Url("chap4")!!, + mediaType = MediaType.HTML, locations = Locator.Locations( progression = 0.0, position = 4, @@ -204,8 +220,8 @@ class LocatorServiceTest { ) ), Locator( - href = "chap4", - type = "text/html", + href = Url("chap4")!!, + mediaType = MediaType.HTML, locations = Locator.Locations( progression = 0.5, position = 5, @@ -215,8 +231,8 @@ class LocatorServiceTest { ), listOf( Locator( - href = "chap5", - type = "text/html", + href = Url("chap5")!!, + mediaType = MediaType.HTML, locations = Locator.Locations( progression = 0.0, position = 6, @@ -224,8 +240,8 @@ class LocatorServiceTest { ) ), Locator( - href = "chap5", - type = "text/html", + href = Url("chap5")!!, + mediaType = MediaType.HTML, locations = Locator.Locations( progression = 1.0 / 3.0, position = 7, @@ -233,8 +249,8 @@ class LocatorServiceTest { ) ), Locator( - href = "chap5", - type = "text/html", + href = Url("chap5")!!, + mediaType = MediaType.HTML, locations = Locator.Locations( progression = 2.0 / 3.0, position = 8, diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/PositionsServiceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/PositionsServiceTest.kt index e07fae86ce..419fa090e4 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/PositionsServiceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/PositionsServiceTest.kt @@ -11,85 +11,20 @@ package org.readium.r2.shared.publication.services import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking -import org.json.JSONObject import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith -import org.readium.r2.shared.extensions.mapNotNull -import org.readium.r2.shared.extensions.optNullableInt +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class PositionsServiceTest { - @Test - fun `get works fine`() { - val positions = listOf( - listOf( - Locator( - href = "res", - type = "application/xml", - locations = Locator.Locations( - position = 1, - totalProgression = 0.0 - ) - ) - ), - listOf( - Locator( - href = "chap1", - type = "image/png", - locations = Locator.Locations( - position = 2, - totalProgression = 1.0 / 4.0 - ) - ) - ), - listOf( - Locator( - href = "chap2", - type = "image/png", - title = "Chapter 2", - locations = Locator.Locations( - position = 3, - totalProgression = 3.0 / 4.0 - ) - ), - Locator( - href = "chap2", - type = "image/png", - title = "Chapter 2.5", - locations = Locator.Locations( - position = 4, - totalProgression = 3.0 / 4.0 - ) - ) - ) - ) - - val service = object : PositionsService { - override suspend fun positionsByReadingOrder(): List<List<Locator>> = positions - } - - val json = service.get(Link("/~readium/positions")) - ?.let { runBlocking { it.readAsString() } } - ?.getOrNull() - ?.let { JSONObject(it) } - val total = json - ?.optNullableInt("total") - val locators = json - ?.optJSONArray("positions") - ?.mapNotNull { locator -> - (locator as? JSONObject)?.let { Locator.fromJSON(it) } - } - - assertEquals(positions.flatten().size, total) - assertEquals(positions.flatten(), locators) - } - @Test fun `helper for ServicesBuilder works fine`() { val factory = { _: Publication.Service.Context -> @@ -104,11 +39,15 @@ class PositionsServiceTest { } } +@RunWith(RobolectricTestRunner::class) class PerResourcePositionsServiceTest { @Test fun `Positions from an empty {readingOrder}`() { - val service = PerResourcePositionsService(readingOrder = emptyList(), fallbackMediaType = "") + val service = PerResourcePositionsService( + readingOrder = emptyList(), + fallbackMediaType = MediaType.BINARY + ) Assert.assertEquals(0, runBlocking { service.positions().size }) } @@ -116,15 +55,15 @@ class PerResourcePositionsServiceTest { @Test fun `Positions from a {readingOrder} with one resource`() { val service = PerResourcePositionsService( - readingOrder = listOf(Link(href = "res", type = "image/png")), - fallbackMediaType = "" + readingOrder = listOf(Link(href = Href("res")!!, mediaType = MediaType.PNG)), + fallbackMediaType = MediaType.BINARY ) Assert.assertEquals( listOf( Locator( - href = "res", - type = "image/png", + href = Url("res")!!, + mediaType = MediaType.PNG, locations = Locator.Locations( position = 1, totalProgression = 0.0 @@ -139,34 +78,34 @@ class PerResourcePositionsServiceTest { fun `Positions from a {readingOrder} with a few resources`() { val service = PerResourcePositionsService( readingOrder = listOf( - Link(href = "res"), - Link(href = "chap1", type = "image/png"), - Link(href = "chap2", type = "image/png", title = "Chapter 2") + Link(href = Href("res")!!), + Link(href = Href("chap1")!!, mediaType = MediaType.PNG), + Link(href = Href("chap2")!!, mediaType = MediaType.PNG, title = "Chapter 2") ), - fallbackMediaType = "" + fallbackMediaType = MediaType.BINARY ) Assert.assertEquals( listOf( Locator( - href = "res", - type = "", + href = Url("res")!!, + mediaType = MediaType.BINARY, locations = Locator.Locations( position = 1, totalProgression = 0.0 ) ), Locator( - href = "chap1", - type = "image/png", + href = Url("chap1")!!, + mediaType = MediaType.PNG, locations = Locator.Locations( position = 2, totalProgression = 1.0 / 3.0 ) ), Locator( - href = "chap2", - type = "image/png", + href = Url("chap2")!!, + mediaType = MediaType.PNG, title = "Chapter 2", locations = Locator.Locations( position = 3, @@ -182,16 +121,16 @@ class PerResourcePositionsServiceTest { fun `{type} fallbacks on the given media type`() { val services = PerResourcePositionsService( readingOrder = listOf( - Link(href = "res") + Link(href = Href("res")!!) ), - fallbackMediaType = "image/*" + fallbackMediaType = MediaType("image/*")!! ) Assert.assertEquals( listOf( Locator( - href = "res", - type = "image/*", + href = Url("res")!!, + mediaType = MediaType("image/*")!!, locations = Locator.Locations( position = 1, totalProgression = 0.0 diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIteratorTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIteratorTest.kt index 0912130f79..c57b962e96 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIteratorTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIteratorTest.kt @@ -8,7 +8,7 @@ import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.fetcher.StringResource +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.services.content.Content @@ -18,14 +18,16 @@ import org.readium.r2.shared.publication.services.content.Content.AttributeKey.C import org.readium.r2.shared.publication.services.content.Content.TextElement import org.readium.r2.shared.publication.services.content.Content.TextElement.Segment import org.readium.r2.shared.util.Language +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.StringResource import org.robolectric.RobolectricTestRunner @OptIn(ExperimentalReadiumApi::class) @RunWith(RobolectricTestRunner::class) class HtmlResourceContentIteratorTest { - private val link = Link(href = "/dir/res.xhtml", type = "application/xhtml+xml") - private val locator = Locator(href = "/dir/res.xhtml", type = "application/xhtml+xml") + private val locator = Locator(href = Url("/dir/res.xhtml")!!, mediaType = MediaType.XHTML) private val html = """ <?xml version="1.0" encoding="UTF-8"?> @@ -51,7 +53,8 @@ class HtmlResourceContentIteratorTest { private val elements: List<Content.Element> = listOf( TextElement( locator = locator( - selector = "#pgepubid00498 > div.center", + progression = 0.0, + selector = "html > body > section > div.center", before = null, highlight = "171" ), @@ -59,7 +62,8 @@ class HtmlResourceContentIteratorTest { segments = listOf( Segment( locator = locator( - selector = "#pgepubid00498 > div.center", + progression = 0.0, + selector = "html > body > section > div.center", before = null, highlight = "171" ), @@ -70,7 +74,8 @@ class HtmlResourceContentIteratorTest { ), TextElement( locator = locator( - selector = "#pgepubid00498 > h3", + progression = 0.2, + selector = "html > body > section > h3", before = "171", highlight = "INTRODUCTORY" ), @@ -78,18 +83,20 @@ class HtmlResourceContentIteratorTest { segments = listOf( Segment( locator = locator( - selector = "#pgepubid00498 > h3", + progression = 0.2, + selector = "html > body > section > h3", before = "171", highlight = "INTRODUCTORY" ), text = "INTRODUCTORY", attributes = listOf(Attribute(LANGUAGE, Language("en"))) - ), + ) ) ), TextElement( locator = locator( - selector = "#pgepubid00498 > p:nth-child(3)", + progression = 0.4, + selector = "html > body > section > p:nth-child(3)", before = "171INTRODUCTORY", highlight = "The difficulties of classification are very apparent here, and once more it must be noted that illustrative and practical purposes rather than logical ones are served by the arrangement adopted. The modern fanciful story is here placed next to the real folk story instead of after all the groups of folk products. The Hebrew stories at the beginning belong quite as well, perhaps even better, in Section V, while the stories at the end of Section VI shade off into the more modern types of short tales." ), @@ -97,18 +104,20 @@ class HtmlResourceContentIteratorTest { segments = listOf( Segment( locator = locator( - selector = "#pgepubid00498 > p:nth-child(3)", + progression = 0.4, + selector = "html > body > section > p:nth-child(3)", before = "171INTRODUCTORY", highlight = "The difficulties of classification are very apparent here, and once more it must be noted that illustrative and practical purposes rather than logical ones are served by the arrangement adopted. The modern fanciful story is here placed next to the real folk story instead of after all the groups of folk products. The Hebrew stories at the beginning belong quite as well, perhaps even better, in Section V, while the stories at the end of Section VI shade off into the more modern types of short tales." ), text = "The difficulties of classification are very apparent here, and once more it must be noted that illustrative and practical purposes rather than logical ones are served by the arrangement adopted. The modern fanciful story is here placed next to the real folk story instead of after all the groups of folk products. The Hebrew stories at the beginning belong quite as well, perhaps even better, in Section V, while the stories at the end of Section VI shade off into the more modern types of short tales.", - attributes = listOf(Attribute(LANGUAGE, Language("en"))), - ), + attributes = listOf(Attribute(LANGUAGE, Language("en"))) + ) ) ), TextElement( locator = locator( - selector = "#pgepubid00498 > p:nth-child(4)", + progression = 0.6, + selector = "html > body > section > p:nth-child(4)", before = "ade off into the more modern types of short tales.", highlight = "The child's natural literature. The world has lost certain secrets as the price of an advancing civilization." ), @@ -116,18 +125,20 @@ class HtmlResourceContentIteratorTest { segments = listOf( Segment( locator = locator( - selector = "#pgepubid00498 > p:nth-child(4)", + progression = 0.6, + selector = "html > body > section > p:nth-child(4)", before = "ade off into the more modern types of short tales.", highlight = "The child's natural literature. The world has lost certain secrets as the price of an advancing civilization." ), text = "The child's natural literature. The world has lost certain secrets as the price of an advancing civilization.", attributes = listOf(Attribute(LANGUAGE, Language("en"))) - ), + ) ) ), TextElement( locator = locator( - selector = "#pgepubid00498 > p:nth-child(5)", + progression = 0.8, + selector = "html > body > section > p:nth-child(5)", before = "secrets as the price of an advancing civilization.", highlight = "Without discussing the limits of the culture-epoch theory of human development as a complete guide in education, it is clear that the young child passes through a period when his mind looks out upon the world in a manner analogous to that of the folk as expressed in their literature." ), @@ -135,18 +146,20 @@ class HtmlResourceContentIteratorTest { segments = listOf( Segment( locator = locator( - selector = "#pgepubid00498 > p:nth-child(5)", + progression = 0.8, + selector = "html > body > section > p:nth-child(5)", before = "secrets as the price of an advancing civilization.", highlight = "Without discussing the limits of the culture-epoch theory of human development as a complete guide in education, it is clear that the young child passes through a period when his mind looks out upon the world in a manner analogous to that of the folk as expressed in their literature." ), text = "Without discussing the limits of the culture-epoch theory of human development as a complete guide in education, it is clear that the young child passes through a period when his mind looks out upon the world in a manner analogous to that of the folk as expressed in their literature.", attributes = listOf(Attribute(LANGUAGE, Language("en"))) - ), + ) ) ) ) private fun locator( + progression: Double? = null, selector: String? = null, before: String? = null, highlight: String? = null, @@ -154,6 +167,7 @@ class HtmlResourceContentIteratorTest { ): Locator = locator.copy( locations = Locator.Locations( + progression = progression, otherLocations = buildMap { selector?.let { put("cssSelector", it) } } @@ -161,8 +175,16 @@ class HtmlResourceContentIteratorTest { text = Locator.Text(before = before, highlight = highlight, after = after) ) - private fun iterator(html: String, startLocator: Locator = locator): HtmlResourceContentIterator = - HtmlResourceContentIterator(StringResource(link, html), startLocator) + private fun iterator( + html: String, + startLocator: Locator = locator, + totalProgressionRange: ClosedRange<Double>? = null + ): HtmlResourceContentIterator = + HtmlResourceContentIterator( + StringResource(html), + totalProgressionRange = totalProgressionRange, + startLocator + ) private suspend fun HtmlResourceContentIterator.elements(): List<Content.Element> = buildList { @@ -249,13 +271,13 @@ class HtmlResourceContentIteratorTest { @Test fun `starting from a CSS selector`() = runTest { - val iter = iterator(html, locator(selector = "#pgepubid00498 > p:nth-child(3)")) + val iter = iterator(html, locator(selector = "html > body > section > p:nth-child(3)")) assertEquals(elements.subList(2, elements.size), iter.elements()) } @Test fun `calling previous() when starting from a CSS selector`() = runTest { - val iter = iterator(html, locator(selector = "#pgepubid00498 > p:nth-child(3)")) + val iter = iterator(html, locator(selector = "html > body > section > p:nth-child(3)")) assertTrue(iter.hasPrevious()) assertEquals(elements[1], iter.previous()) } @@ -277,6 +299,7 @@ class HtmlResourceContentIteratorTest { assertEquals( TextElement( locator = locator( + progression = 0.5, selector = "html > body > p:nth-child(2)", before = "oin sur la chaussée, aussi loin qu’on pouvait voir", highlight = "Lui, notre colonel, savait peut-être pourquoi ces deux gens-là tiraient [...] On buvait de la bière sucrée." @@ -285,9 +308,10 @@ class HtmlResourceContentIteratorTest { segments = listOf( Segment( locator = locator( + progression = 0.5, selector = "html > body > p:nth-child(2)", before = "oin sur la chaussée, aussi loin qu’on pouvait voir", - highlight = "Lui, notre colonel, savait peut-être pourquoi ces deux gens-là tiraient [...] On buvait de la bière sucrée.", + highlight = "Lui, notre colonel, savait peut-être pourquoi ces deux gens-là tiraient [...] On buvait de la bière sucrée." ), text = "Lui, notre colonel, savait peut-être pourquoi ces deux gens-là tiraient [...] On buvait de la bière sucrée.", attributes = listOf(Attribute(LANGUAGE, Language("fr"))) @@ -316,6 +340,7 @@ class HtmlResourceContentIteratorTest { assertEquals( TextElement( locator = locator( + progression = 0.5, selector = "html > body > p:nth-child(2)", before = "oin sur la chaussée, aussi loin qu’on pouvait voir", highlight = "Lui, notre colonel, savait peut-être pourquoi ces deux gens-là tiraient [...] On buvait de la bière sucrée." @@ -324,9 +349,10 @@ class HtmlResourceContentIteratorTest { segments = listOf( Segment( locator = locator( + progression = 0.5, selector = "html > body > p:nth-child(2)", before = "oin sur la chaussée, aussi loin qu’on pouvait voir", - highlight = "Lui, notre colonel, savait peut-être pourquoi ces deux gens-là tiraient [...] On buvait de la bière sucrée.", + highlight = "Lui, notre colonel, savait peut-être pourquoi ces deux gens-là tiraient [...] On buvait de la bière sucrée." ), text = "Lui, notre colonel, savait peut-être pourquoi ces deux gens-là tiraient [...] On buvait de la bière sucrée.", attributes = listOf(Attribute(LANGUAGE, Language("fr"))) @@ -353,17 +379,19 @@ class HtmlResourceContentIteratorTest { listOf( Content.ImageElement( locator = locator( + progression = 0.0, selector = "html > body > img:nth-child(1)" ), - embeddedLink = Link(href = "/dir/image.png"), + embeddedLink = Link(href = Href("/dir/image.png")!!), caption = null, attributes = emptyList() ), Content.ImageElement( locator = locator( + progression = 0.5, selector = "html > body > img:nth-child(2)" ), - embeddedLink = Link(href = "/cover.jpg"), + embeddedLink = Link(href = Href("/cover.jpg")!!), caption = null, attributes = listOf(Attribute(ACCESSIBILITY_LABEL, "Accessibility description")) ) @@ -380,7 +408,7 @@ class HtmlResourceContentIteratorTest { <body> <audio src="audio.mp3" /> <audio> - <source src="audio.mp3" type="audio/mp3" /> + <source src="audio.mp3" type="audio/mpeg" /> <source src="audio.ogg" type="audio/ogg" /> </audio> </body> @@ -391,20 +419,22 @@ class HtmlResourceContentIteratorTest { listOf( Content.AudioElement( locator = locator( + progression = 0.0, selector = "html > body > audio:nth-child(1)" ), - embeddedLink = Link(href = "/dir/audio.mp3"), + embeddedLink = Link(href = Href("/dir/audio.mp3")!!), attributes = emptyList() ), Content.AudioElement( locator = locator( + progression = 0.5, selector = "html > body > audio:nth-child(2)" ), embeddedLink = Link( - href = "/dir/audio.mp3", - type = "audio/mp3", + href = Href("/dir/audio.mp3")!!, + mediaType = MediaType.MP3, alternates = listOf( - Link(href = "/dir/audio.ogg", type = "audio/ogg") + Link(href = Href("/dir/audio.ogg")!!, mediaType = MediaType.OGG) ) ), attributes = emptyList() @@ -433,20 +463,25 @@ class HtmlResourceContentIteratorTest { listOf( Content.VideoElement( locator = locator( + progression = 0.0, selector = "html > body > video:nth-child(1)" ), - embeddedLink = Link(href = "/dir/video.mp4"), + embeddedLink = Link(href = Href("/dir/video.mp4")!!), attributes = emptyList() ), Content.VideoElement( locator = locator( + progression = 0.5, selector = "html > body > video:nth-child(2)" ), embeddedLink = Link( - href = "/dir/video.mp4", - type = "video/mp4", + href = Href("/dir/video.mp4")!!, + mediaType = MediaType("video/mp4")!!, alternates = listOf( - Link(href = "/dir/video.m4v", type = "video/x-m4v") + Link( + href = Href("/dir/video.m4v")!!, + mediaType = MediaType("video/x-m4v")!! + ) ) ), attributes = emptyList() @@ -479,6 +514,7 @@ class HtmlResourceContentIteratorTest { listOf( TextElement( locator = locator( + progression = 0.0, selector = "#c06-li-0001", highlight = "Let's start at the top—the source of ideas." ), @@ -486,17 +522,19 @@ class HtmlResourceContentIteratorTest { segments = listOf( Segment( locator = locator( + progression = 0.0, selector = "#c06-li-0001", highlight = "Let's start at the top—the source of ideas." ), text = "Let's start at the top—the source of ideas.", attributes = emptyList() - ), + ) ), attributes = emptyList() ), TextElement( locator = locator( + progression = 1 / 3.0, selector = "#c06-para-0019", before = " top—the source of ideas.\n ", highlight = "While almost everyone today claims to be Agile, what I've just described is very much a waterfall process." @@ -505,6 +543,7 @@ class HtmlResourceContentIteratorTest { segments = listOf( Segment( locator = locator( + progression = 1 / 3.0, selector = "#c06-para-0019", before = " top—the source of ideas.\n ", highlight = "While almost everyone today claims to be Agile, what I've just described is very much a waterfall process." @@ -517,7 +556,8 @@ class HtmlResourceContentIteratorTest { ), TextElement( locator = locator( - selector = "#c06-para-0019", + progression = 2 / 3.0, + selector = "html > body > ol.decimal > li > aside", before = "e just described is very much a waterfall process.\n \n ", highlight = "Trailing text" ), @@ -525,7 +565,8 @@ class HtmlResourceContentIteratorTest { segments = listOf( Segment( locator = locator( - selector = "#c06-para-0019", + progression = 2 / 3.0, + selector = "html > body > ol.decimal > li > aside", before = "e just described is very much a waterfall process.\n ", highlight = "Trailing text" ), @@ -539,4 +580,109 @@ class HtmlResourceContentIteratorTest { iterator(html).elements() ) } + + @Test + fun `iterating over text nodes located around a nested block element`() = runTest { + val html = """ + <?xml version="1.0" encoding="UTF-8"?> + <html xmlns="http://www.w3.org/1999/xhtml"> + <body> + <div id="a">begin a <div id="b">in b</div> end a</div> + <div id="c">in c</div> + </body> + </html> + """ + + assertEquals( + listOf( + TextElement( + locator = locator( + progression = 0.0, + selector = "#a", + highlight = "begin a" + ), + role = TextElement.Role.Body, + segments = listOf( + Segment( + locator = locator( + progression = 0.0, + selector = "#a", + highlight = "begin a" + ), + text = "begin a", + attributes = emptyList() + ) + ), + attributes = emptyList() + ), + TextElement( + locator = locator( + progression = 0.25, + selector = "#b", + before = "begin a ", + highlight = "in b" + ), + role = TextElement.Role.Body, + segments = listOf( + Segment( + locator = locator( + progression = 0.25, + selector = "#b", + before = "begin a ", + highlight = "in b" + ), + text = "in b", + attributes = emptyList() + ) + ), + attributes = emptyList() + ), + TextElement( + locator = locator( + progression = 0.5, + selector = "#a", + before = "begin a in b ", + highlight = "end a" + ), + role = TextElement.Role.Body, + segments = listOf( + Segment( + locator = locator( + progression = 0.5, + selector = "#a", + before = "begin a in b ", + highlight = "end a" + ), + text = "end a", + attributes = emptyList() + ) + ), + attributes = emptyList() + ), + TextElement( + locator = locator( + progression = 0.75, + selector = "#c", + before = "begin a in b end a", + highlight = "in c" + ), + role = TextElement.Role.Body, + segments = listOf( + Segment( + locator = locator( + progression = 0.75, + selector = "#c", + before = "begin a in b end a", + highlight = "in c" + ), + text = "in c", + attributes = emptyList() + ) + ), + attributes = emptyList() + ) + ), + iterator(html).elements() + ) + } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/URITemplateTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/URITemplateTest.kt index 43bd26f63f..cf105dc253 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/URITemplateTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/URITemplateTest.kt @@ -56,7 +56,9 @@ class URITemplateTest { assertEquals( "https://lsd-test.edrlab.org/licenses/39ef1ff2-cda2-4219-a26a-d504fbb24c17/renew?end=2020-11-12T16:02:00.000%2B01:00&id=38dfd7ba-a80b-4253-a047-e6aa9c21d6f0&name=Pixel%203a", - URITemplate("https://lsd-test.edrlab.org/licenses/39ef1ff2-cda2-4219-a26a-d504fbb24c17/renew{?end,id,name}") + URITemplate( + "https://lsd-test.edrlab.org/licenses/39ef1ff2-cda2-4219-a26a-d504fbb24c17/renew{?end,id,name}" + ) .expand( mapOf( "id" to "38dfd7ba-a80b-4253-a047-e6aa9c21d6f0", diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/UrlTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/UrlTest.kt new file mode 100644 index 0000000000..d887abc74b --- /dev/null +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/UrlTest.kt @@ -0,0 +1,396 @@ +package org.readium.r2.shared.util + +import android.net.Uri +import java.io.File +import java.net.URI +import java.net.URL +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.DelicateReadiumApi +import org.readium.r2.shared.util.Url.Query +import org.readium.r2.shared.util.Url.QueryParameter +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class UrlTest { + + @Test + fun createFromInvalidUrl() { + assertNull(Url("")) + assertNull(Url(" ")) + assertNull(Url("invalid character")) + + assertNull(AbsoluteUrl(" ")) + assertNull(AbsoluteUrl("invalid character")) + + assertNull(RelativeUrl(" ")) + assertNull(RelativeUrl("invalid character")) + } + + @Test + fun createFromRelativePath() { + assertEquals(RelativeUrl(Uri.parse("/foo/bar")), Url("/foo/bar")) + assertEquals(RelativeUrl(Uri.parse("foo/bar")), Url("foo/bar")) + assertEquals(RelativeUrl(Uri.parse("../bar")), Url("../bar")) + + // Special characters valid in a path. + assertEquals("$&+,/=@", RelativeUrl("$&+,/=@")?.path) + + // Used in the EPUB parser + val url = Url("#") as? RelativeUrl + assertNotNull(url) + assertEquals(null, url.path) + assertEquals(null, url.fragment) + } + + @OptIn(DelicateReadiumApi::class) + @Test + fun createFromLegacyHref() { + testLegacy<RelativeUrl>("dir/chapter.xhtml", "dir/chapter.xhtml") + // Starting slash is removed. + testLegacy<RelativeUrl>("/dir/chapter.xhtml", "dir/chapter.xhtml") + // Special characters are percent-encoded. + testLegacy<RelativeUrl>("/dir/per%cent.xhtml", "dir/per%25cent.xhtml") + testLegacy<RelativeUrl>("/barré.xhtml", "barr%C3%A9.xhtml") + testLegacy<RelativeUrl>("/spa ce.xhtml", "spa%20ce.xhtml") + // We assume that a relative path is percent-decoded. + testLegacy<RelativeUrl>("/spa%20ce.xhtml", "spa%2520ce.xhtml") + // Some special characters are authorized in a path. + testLegacy<RelativeUrl>("/$&+,/=@", "$&+,/=@") + // Valid absolute URL are left untouched. + testLegacy<AbsoluteUrl>( + "http://domain.com/a%20book?page=3", + "http://domain.com/a%20book?page=3" + ) + // Invalid absolute URL. + assertNull(Url.fromLegacyHref("http://domain.com/a book")) + } + + @OptIn(DelicateReadiumApi::class) + private inline fun <reified T : Url> testLegacy(href: String, expected: String) { + val url = Url.fromLegacyHref(href) + assertNotNull(url) + assertIs<T>(url) + assertEquals(expected, url.toString()) + } + + @Test + fun createFromFragmentOnly() { + assertEquals(RelativeUrl(Uri.parse("#fragment")), Url("#fragment")) + } + + @Test + fun createFromQueryOnly() { + assertEquals(RelativeUrl(Uri.parse("?query=param")), Url("?query=param")) + } + + @Test + fun createFromAbsoluteUrl() { + assertEquals( + AbsoluteUrl(Uri.parse("http://example.com/foo")), + Url("http://example.com/foo") + ) + assertEquals(AbsoluteUrl(Uri.parse("file:///foo/bar")), Url("file:///foo/bar")) + } + + @Test + fun getToString() { + assertEquals("foo/bar?query#fragment", Url("foo/bar?query#fragment")?.toString()) + assertEquals( + "http://example.com/foo/bar?query#fragment", + Url("http://example.com/foo/bar?query#fragment")?.toString() + ) + assertEquals( + "file:///foo/bar?query#fragment", + Url("file:///foo/bar?query#fragment")?.toString() + ) + } + + @Test + fun getPath() { + assertEquals("foo/bar", RelativeUrl("foo/bar?query#fragment")?.path) + assertEquals("/foo/bar/", AbsoluteUrl("http://example.com/foo/bar/")?.path) + assertEquals("/foo/bar", AbsoluteUrl("http://example.com/foo/bar?query#fragment")?.path) + assertEquals("/foo/bar/", AbsoluteUrl("file:///foo/bar/")?.path) + assertEquals("/foo/bar", AbsoluteUrl("file:///foo/bar?query#fragment")?.path) + } + + @Test + fun getPathFromEmptyRelativeUrl() { + assertNull(RelativeUrl("#fragment")!!.path) + } + + @Test + fun pathIsPercentDecoded() { + assertEquals("foo/%bar quz", Url("foo/%25bar%20quz")?.path) + assertEquals("/foo/%bar quz", Url("http://example.com/foo/%25bar%20quz")?.path) + } + + @Test + fun getFilename() { + assertEquals("bar", Url("foo/bar?query#fragment")?.filename) + assertEquals(null, Url("foo/bar/?query#fragment")?.filename) + assertEquals("bar", Url("http://example.com/foo/bar?query#fragment")?.filename) + assertEquals(null, Url("http://example.com/foo/bar/")?.filename) + assertEquals("bar", Url("file:///foo/bar?query#fragment")?.filename) + assertEquals(null, Url("file:///foo/bar/")?.filename) + } + + @Test + fun filenameIsPercentDecoded() { + assertEquals("%bar quz", Url("foo/%25bar%20quz")?.filename) + assertEquals("%bar quz", Url("http://example.com/foo/%25bar%20quz")?.filename) + } + + @Test + fun getExtension() { + assertEquals("txt", Url("foo/bar.txt?query#fragment")?.extension?.value) + assertEquals(null, Url("foo/bar?query#fragment")?.extension) + assertEquals(null, Url("foo/bar/?query#fragment")?.extension) + assertEquals("txt", Url("http://example.com/foo/bar.txt?query#fragment")?.extension?.value) + assertEquals(null, Url("http://example.com/foo/bar?query#fragment")?.extension) + assertEquals(null, Url("http://example.com/foo/bar/")?.extension) + assertEquals("txt", Url("file:///foo/bar.txt?query#fragment")?.extension?.value) + assertEquals(null, Url("file:///foo/bar?query#fragment")?.extension) + assertEquals(null, Url("file:///foo/bar/")?.extension) + } + + @Test + fun extensionIsPercentDecoded() { + assertEquals("%bar", Url("foo.%25bar")?.extension?.value) + assertEquals("%bar", Url("http://example.com/foo.%25bar")?.extension?.value) + } + + @Test + fun getQueryParameters() { + assertEquals(Query(emptyList()), Url("http://domain.com/path")!!.query) + assertEquals( + Query(listOf(QueryParameter(name = "query", value = "param"))), + Url("http://domain.com/path?query=param#anchor")!!.query + ) + assertEquals( + Query( + listOf( + QueryParameter(name = "query", value = "param"), + QueryParameter(name = "fruit", value = "banana"), + QueryParameter(name = "query", value = "other"), + QueryParameter(name = "empty", value = null) + ) + ), + Url("http://domain.com/path?query=param&fruit=banana&query=other&empty")!!.query + ) + } + + @Test + fun getScheme() { + assertEquals(Url.Scheme("content"), (Url("content:///foo/bar") as? AbsoluteUrl)?.scheme) + assertEquals(Url.Scheme("content"), (Url("CONTENT:///foo/bar") as? AbsoluteUrl)?.scheme) + assertEquals(Url.Scheme("file"), (Url("file:///foo/bar") as? AbsoluteUrl)?.scheme) + assertEquals(Url.Scheme("http"), (Url("http://example.com/foo") as? AbsoluteUrl)?.scheme) + assertEquals(Url.Scheme("https"), (Url("https://example.com/foo") as? AbsoluteUrl)?.scheme) + } + + @Test + fun testScheme() { + assertEquals(true, (Url("content:///foo/bar") as? AbsoluteUrl)?.isContent) + assertEquals(false, (Url("content:///foo/bar") as? AbsoluteUrl)?.isHttp) + + assertEquals(true, (Url("file:///foo/bar") as? AbsoluteUrl)?.isFile) + assertEquals(false, (Url("file:///foo/bar") as? AbsoluteUrl)?.isContent) + + assertEquals(true, (Url("http://example.com/foo") as? AbsoluteUrl)?.isHttp) + assertEquals(true, (Url("https://example.com/foo") as? AbsoluteUrl)?.isHttp) + assertEquals(false, (Url("http://example.com/foo") as? AbsoluteUrl)?.isFile) + } + + @Test + fun resolveHttpUrl() { + var base = Url("http://example.com/foo/bar")!! + assertEquals(Url("http://example.com/foo/quz/baz")!!, base.resolve(Url("quz/baz")!!)) + assertEquals(Url("http://example.com/quz/baz")!!, base.resolve(Url("../quz/baz")!!)) + assertEquals(Url("http://example.com/quz/baz")!!, base.resolve(Url("/quz/baz")!!)) + assertEquals(Url("http://example.com/foo/bar#fragment")!!, base.resolve(Url("#fragment")!!)) + assertEquals(Url("file:///foo/bar")!!, base.resolve(Url("file:///foo/bar")!!)) + + // With trailing slash + base = Url("http://example.com/foo/bar/")!! + assertEquals(Url("http://example.com/foo/bar/quz/baz")!!, base.resolve(Url("quz/baz")!!)) + assertEquals(Url("http://example.com/foo/quz/baz")!!, base.resolve(Url("../quz/baz")!!)) + } + + @Test + fun resolveFileUrl() { + var base = Url("file:///root/foo/bar")!! + assertEquals(Url("file:///root/foo/quz")!!, base.resolve(Url("quz")!!)) + assertEquals(Url("file:///root/foo/quz/baz")!!, base.resolve(Url("quz/baz")!!)) + assertEquals(Url("file:///root/quz")!!, base.resolve(Url("../quz")!!)) + assertEquals(Url("file:///quz/baz")!!, base.resolve(Url("/quz/baz")!!)) + assertEquals( + Url("http://example.com/foo/bar")!!, + base.resolve(Url("http://example.com/foo/bar")!!) + ) + + // With trailing slash + base = Url("file:///root/foo/bar/")!! + assertEquals(Url("file:///root/foo/bar/quz/baz")!!, base.resolve(Url("quz/baz")!!)) + assertEquals(Url("file:///root/foo/quz")!!, base.resolve(Url("../quz")!!)) + } + + @Test + fun resolveTwoRelativeUrls() { + var base = Url("foo/bar")!! + assertEquals(Url("foo/quz/baz")!!, base.resolve(Url("quz/baz")!!)) + assertEquals(Url("quz/baz")!!, base.resolve(Url("../quz/baz")!!)) + assertEquals(Url("/quz/baz")!!, base.resolve(Url("/quz/baz")!!)) + assertEquals(Url("foo/bar#fragment")!!, base.resolve(Url("#fragment")!!)) + assertEquals( + Url("http://example.com/foo/bar")!!, + base.resolve(Url("http://example.com/foo/bar")!!) + ) + + // With trailing slash + base = Url("foo/bar/")!! + assertEquals(Url("foo/bar/quz/baz")!!, base.resolve(Url("quz/baz")!!)) + assertEquals(Url("foo/quz/baz")!!, base.resolve(Url("../quz/baz")!!)) + + // With starting slash + base = Url("/foo/bar")!! + assertEquals(Url("/foo/quz/baz")!!, base.resolve(Url("quz/baz")!!)) + assertEquals(Url("/quz/baz")!!, base.resolve(Url("/quz/baz")!!)) + } + + @Test + fun relativizeHttpUrl() { + var base = Url("http://example.com/foo")!! + assertEquals(Url("quz/baz")!!, base.relativize(Url("http://example.com/foo/quz/baz")!!)) + assertEquals(Url("#fragment")!!, base.relativize(Url("http://example.com/foo#fragment")!!)) + assertEquals(Url("#fragment")!!, base.relativize(Url("http://example.com/foo/#fragment")!!)) + assertEquals(Url("file:///foo/bar")!!, base.relativize(Url("file:///foo/bar")!!)) + + // With trailing slash + base = Url("http://example.com/foo/")!! + assertEquals(Url("quz/baz")!!, base.relativize(Url("http://example.com/foo/quz/baz")!!)) + } + + @Test + fun relativizeFileUrl() { + var base = Url("file:///root/foo")!! + assertEquals(Url("quz/baz")!!, base.relativize(Url("file:///root/foo/quz/baz")!!)) + assertEquals( + Url("http://example.com/foo/bar")!!, + base.relativize(Url("http://example.com/foo/bar")!!) + ) + + // With trailing slash + base = Url("file:///root/foo/")!! + assertEquals(Url("quz/baz")!!, base.relativize(Url("file:///root/foo/quz/baz")!!)) + } + + @Test + fun relativizeTwoRelativeUrls() { + var base = Url("foo")!! + assertEquals(Url("quz/baz")!!, base.relativize(Url("foo/quz/baz")!!)) + assertEquals(Url("quz/baz")!!, base.relativize(Url("quz/baz")!!)) + assertEquals(Url("/quz/baz")!!, base.relativize(Url("/quz/baz")!!)) + assertEquals(Url("#fragment")!!, base.relativize(Url("foo#fragment")!!)) + assertEquals(Url("#fragment")!!, base.relativize(Url("foo/#fragment")!!)) + assertEquals( + Url("http://example.com/foo/bar")!!, + base.relativize(Url("http://example.com/foo/bar")!!) + ) + + // With trailing slash + base = Url("foo/")!! + assertEquals(Url("quz/baz")!!, base.relativize(Url("foo/quz/baz")!!)) + + // With starting slash + base = Url("/foo")!! + assertEquals(Url("quz/baz")!!, base.relativize(Url("/foo/quz/baz")!!)) + assertEquals(Url("/quz/baz")!!, base.relativize(Url("/quz/baz")!!)) + } + + @Test + fun fromFile() { + assertEquals(AbsoluteUrl(Uri.parse("file:///tmp/test.txt")), File("/tmp/test.txt").toUrl()) + } + + @Test + fun toFile() { + assertEquals( + File("/tmp/test.txt"), + (Url("file:///tmp/test.txt") as? AbsoluteUrl)?.toFile() + ) + } + + @Test + fun fromURI() { + assertEquals(RelativeUrl(Uri.parse("foo/bar")), URI("foo/bar").toUrl()) + assertEquals(RelativeUrl(Uri.parse("/foo/bar")), URI("/foo/bar").toUrl()) + assertEquals( + AbsoluteUrl(Uri.parse("http://example.com/foo/bar")), + URI("http://example.com/foo/bar").toUrl() + ) + assertEquals( + AbsoluteUrl(Uri.parse("file:///tmp/test.txt")), + URI("file:///tmp/test.txt").toUrl() + ) + assertEquals( + AbsoluteUrl(Uri.parse("file:///tmp/test.txt")), + URI("file:/tmp/test.txt").toUrl() + ) + } + + @Test + fun fromURL() { + assertEquals( + AbsoluteUrl(Uri.parse("http://example.com/foo/bar")), + URL("http://example.com/foo/bar").toUrl() + ) + assertEquals( + AbsoluteUrl(Uri.parse("file:///tmp/test.txt")), + URL("file:///tmp/test.txt").toUrl() + ) + assertEquals( + AbsoluteUrl(Uri.parse("file:///tmp/test.txt")), + URL("file:/tmp/test.txt").toUrl() + ) + } + + @Test + fun getFirstParameterNamedX() { + val params = Query( + listOf( + QueryParameter(name = "query", value = "param"), + QueryParameter(name = "fruit", value = "banana"), + QueryParameter(name = "query", value = "other"), + QueryParameter(name = "empty", value = null) + ) + ) + + assertEquals(params.firstNamedOrNull("query"), "param") + assertEquals(params.firstNamedOrNull("fruit"), "banana") + assertNull(params.firstNamedOrNull("empty")) + assertNull(params.firstNamedOrNull("not-found")) + } + + @Test + fun getAllParametersNamedX() { + val params = Query( + listOf( + QueryParameter(name = "query", value = "param"), + QueryParameter(name = "fruit", value = "banana"), + QueryParameter(name = "query", value = "other"), + QueryParameter(name = "empty", value = null) + ) + ) + + assertEquals(params.allNamed("query"), listOf("param", "other")) + assertEquals(params.allNamed("fruit"), listOf("banana")) + assertEquals(params.allNamed("empty"), emptyList<String>()) + assertEquals(params.allNamed("not-found"), emptyList<String>()) + } +} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/archive/ArchiveTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/archive/ArchiveTest.kt deleted file mode 100644 index 8ebbe189da..0000000000 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/archive/ArchiveTest.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared.util.archive - -import java.io.File -import java.nio.charset.StandardCharsets -import kotlin.test.assertEquals -import kotlin.test.assertFails -import kotlin.test.assertNotNull -import kotlinx.coroutines.runBlocking -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -class ArchiveTest(val archive: Archive) { - - companion object { - - @Parameterized.Parameters - @JvmStatic - fun archives(): List<Archive> { - val epubZip = ArchiveTest::class.java.getResource("epub.epub") - assertNotNull(epubZip) - val zipArchive = runBlocking { DefaultArchiveFactory().open(File(epubZip.path), password = null) } - assertNotNull(zipArchive) - - val epubExploded = ArchiveTest::class.java.getResource("epub") - assertNotNull(epubExploded) - val explodedArchive = runBlocking { DefaultArchiveFactory().open(File(epubExploded.path), password = null) } - assertNotNull(explodedArchive) - - return listOf(zipArchive, explodedArchive) - } - } - - @Test - fun `Entry list is correct`() { - assertThat(runBlocking { archive.entries().map { it.path } }) - .contains( - "mimetype", - "EPUB/cover.xhtml", - "EPUB/css/epub.css", - "EPUB/css/nav.css", - "EPUB/images/cover.png", - "EPUB/nav.xhtml", - "EPUB/package.opf", - "EPUB/s04.xhtml", - "EPUB/toc.ncx", - "META-INF/container.xml" - ) - } - - @Test - fun `Attempting to get a missing entry throws`() { - assertFails { runBlocking { archive.entry("unknown") } } - } - - @Test - fun `Fully reading an entry works well`() { - val bytes = runBlocking { archive.entry("mimetype").read() } - assertEquals("application/epub+zip", bytes.toString(StandardCharsets.UTF_8)) - } - - @Test - fun `Reading a range of an entry works well`() { - val bytes = runBlocking { archive.entry("mimetype").read(0..10L) } - assertEquals("application", bytes.toString(StandardCharsets.UTF_8)) - assertEquals(11, bytes.size) - } - - @Test - fun `Out of range indexes are clamped to the available length`() { - val bytes = runBlocking { archive.entry("mimetype").read(-5..60L) } - assertEquals("application/epub+zip", bytes.toString(StandardCharsets.UTF_8)) - assertEquals(20, bytes.size) - } - - @Test - fun `Decreasing ranges are understood as empty ones`() { - val bytes = runBlocking { archive.entry("mimetype").read(60..20L) } - assertEquals("", bytes.toString(StandardCharsets.UTF_8)) - assertEquals(0, bytes.size) - } - - @Test - fun `Computing size works well`() { - val size = runBlocking { archive.entry("mimetype").length } - assertEquals(20L, size) - } -} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/asset/AssetSnifferTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/asset/AssetSnifferTest.kt new file mode 100644 index 0000000000..1f0ca9a491 --- /dev/null +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/asset/AssetSnifferTest.kt @@ -0,0 +1,843 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.asset + +import java.io.File +import kotlin.test.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.Fixtures +import org.readium.r2.shared.util.Either +import org.readium.r2.shared.util.FileExtension +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.checkSuccess +import org.readium.r2.shared.util.data.EmptyContainer +import org.readium.r2.shared.util.file.FileResource +import org.readium.r2.shared.util.format.AvifSpecification +import org.readium.r2.shared.util.format.BmpSpecification +import org.readium.r2.shared.util.format.EpubSpecification +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatHints +import org.readium.r2.shared.util.format.FormatSpecification +import org.readium.r2.shared.util.format.GifSpecification +import org.readium.r2.shared.util.format.HtmlSpecification +import org.readium.r2.shared.util.format.InformalAudiobookSpecification +import org.readium.r2.shared.util.format.InformalComicSpecification +import org.readium.r2.shared.util.format.JpegSpecification +import org.readium.r2.shared.util.format.JsonSpecification +import org.readium.r2.shared.util.format.JxlSpecification +import org.readium.r2.shared.util.format.LcpLicenseSpecification +import org.readium.r2.shared.util.format.LcpSpecification +import org.readium.r2.shared.util.format.LpfSpecification +import org.readium.r2.shared.util.format.Opds1CatalogSpecification +import org.readium.r2.shared.util.format.Opds1EntrySpecification +import org.readium.r2.shared.util.format.Opds2CatalogSpecification +import org.readium.r2.shared.util.format.Opds2PublicationSpecification +import org.readium.r2.shared.util.format.OpdsAuthenticationSpecification +import org.readium.r2.shared.util.format.PdfSpecification +import org.readium.r2.shared.util.format.PngSpecification +import org.readium.r2.shared.util.format.ProblemDetailsSpecification +import org.readium.r2.shared.util.format.RarSpecification +import org.readium.r2.shared.util.format.RpfSpecification +import org.readium.r2.shared.util.format.RwpmSpecification +import org.readium.r2.shared.util.format.TiffSpecification +import org.readium.r2.shared.util.format.W3cPubManifestSpecification +import org.readium.r2.shared.util.format.WebpSpecification +import org.readium.r2.shared.util.format.XmlSpecification +import org.readium.r2.shared.util.format.ZipSpecification +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.StringResource +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class AssetSnifferTest { + + private val fixtures = Fixtures("util/asset") + + private val sniffer = AssetSniffer() + + private suspend fun AssetSniffer.sniffHints(formatHints: FormatHints): Try<Format, AssetSniffer.SniffError> = + sniff(hints = formatHints, source = Either.Right(EmptyContainer())) + .map { it.format } + + private suspend fun AssetSniffer.sniff(file: File, hints: FormatHints = FormatHints()): Try<Format, AssetSniffer.SniffError> = + sniff(FileResource(file), hints) + + private suspend fun AssetSniffer.sniff(resource: Resource, hints: FormatHints = FormatHints()): Try<Format, AssetSniffer.SniffError> = + sniff(hints = hints, source = Either.Left(resource)) + .map { it.format } + + private suspend fun AssetSniffer.sniffFileExtension(extension: String?): Try<Format, AssetSniffer.SniffError> = + sniffHints(FormatHints(fileExtension = extension?.let { FileExtension((it)) })) + + private suspend fun AssetSniffer.sniffMediaType(mediaType: String?): Try<Format, AssetSniffer.SniffError> = + sniffHints(FormatHints(mediaType = mediaType?.let { MediaType(it) })) + + private val epubFormat = + Format( + specification = FormatSpecification(ZipSpecification, EpubSpecification), + mediaType = MediaType.EPUB, + fileExtension = FileExtension("epub") + ) + + private val audiobookFormat = + Format( + specification = FormatSpecification(ZipSpecification, RpfSpecification), + mediaType = MediaType.READIUM_AUDIOBOOK, + fileExtension = FileExtension("audiobook") + ) + + private val audiobookManifestFormat = + Format( + specification = FormatSpecification(JsonSpecification, RwpmSpecification), + mediaType = MediaType.READIUM_AUDIOBOOK_MANIFEST, + fileExtension = FileExtension("json") + ) + + @Test + fun `sniff ignores extension case`() = runBlocking { + assertEquals( + epubFormat, + sniffer.sniffFileExtension("EPUB").checkSuccess() + ) + } + + @Test + fun `sniff ignores media type case`() = runBlocking { + assertEquals( + epubFormat, + sniffer.sniffMediaType("APPLICATION/EPUB+ZIP").checkSuccess() + ) + } + + @Test + fun `sniff ignores media type extra parameters`() = runBlocking { + assertEquals( + epubFormat, + sniffer.sniffMediaType("application/epub+zip;param=value").checkSuccess() + ) + } + + @Test + fun `sniff from metadata`() = runBlocking { + assertEquals( + sniffer.sniffFileExtension(null).failureOrNull(), + AssetSniffer.SniffError.NotRecognized + ) + assertEquals( + audiobookFormat, + sniffer.sniffFileExtension("audiobook").checkSuccess() + ) + assertEquals( + sniffer.sniffMediaType(null).failureOrNull(), + AssetSniffer.SniffError.NotRecognized + ) + assertEquals( + audiobookFormat, + sniffer.sniffMediaType("application/audiobook+zip").checkSuccess() + ) + assertEquals( + audiobookFormat, + sniffer.sniffHints( + FormatHints( + mediaTypes = listOf("application/audiobook+zip"), + fileExtensions = listOf("audiobook") + ) + ).checkSuccess() + ) + } + + @Test + fun `sniff from bytes`() = runBlocking { + assertEquals( + audiobookManifestFormat, + sniffer.sniff(fixtures.fileAt("audiobook.json")).checkSuccess() + ) + } + + @Test + fun `sniff unknown format`() = runBlocking { + assertEquals( + AssetSniffer.SniffError.NotRecognized, + sniffer.sniffMediaType(mediaType = "invalid").failureOrNull() + ) + assertEquals( + AssetSniffer.SniffError.NotRecognized, + sniffer.sniff(fixtures.fileAt("unknown")).failureOrNull() + ) + } + + @Test + fun `sniff audiobook`() = runBlocking { + assertEquals( + audiobookFormat, + sniffer.sniffFileExtension("audiobook").checkSuccess() + ) + assertEquals( + audiobookFormat, + sniffer.sniffMediaType("application/audiobook+zip").checkSuccess() + ) + assertEquals( + audiobookFormat, + sniffer.sniff(fixtures.fileAt("audiobook-package.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff audiobook manifest`() = runBlocking { + assertEquals( + audiobookManifestFormat, + sniffer.sniffMediaType("application/audiobook+json").checkSuccess() + ) + assertEquals( + audiobookManifestFormat, + sniffer.sniff(fixtures.fileAt("audiobook.json")).checkSuccess() + ) + assertEquals( + audiobookManifestFormat, + sniffer.sniff(fixtures.fileAt("audiobook-wrongtype.json")).checkSuccess() + ) + } + + @Test + fun `sniff BMP`() = runBlocking { + val format = Format( + specification = FormatSpecification(BmpSpecification), + mediaType = MediaType.BMP, + fileExtension = FileExtension("bmp") + ) + + assertEquals(format, sniffer.sniffFileExtension("bmp").checkSuccess()) + assertEquals(format, sniffer.sniffFileExtension("dib").checkSuccess()) + assertEquals(format, sniffer.sniffMediaType("image/bmp").checkSuccess()) + assertEquals(format, sniffer.sniffMediaType("image/x-bmp").checkSuccess()) + } + + @Test + fun `sniff CBZ`() = runBlocking { + val cbzFormat = Format( + specification = FormatSpecification(ZipSpecification, InformalComicSpecification), + mediaType = MediaType.CBZ, + fileExtension = FileExtension("cbz") + ) + + val cbrFormat = Format( + specification = FormatSpecification(RarSpecification, InformalComicSpecification), + mediaType = MediaType.CBR, + fileExtension = FileExtension("cbr") + ) + + assertEquals( + cbzFormat, + sniffer.sniffFileExtension("cbz").checkSuccess() + ) + assertEquals( + cbzFormat, + sniffer.sniffMediaType("application/vnd.comicbook+zip").checkSuccess() + ) + assertEquals( + cbzFormat, + sniffer.sniffMediaType("application/x-cbz").checkSuccess() + ) + assertEquals( + cbrFormat, + sniffer.sniffMediaType("application/x-cbr").checkSuccess() + ) + assertEquals( + cbzFormat, + sniffer.sniff(fixtures.fileAt("cbz.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff DiViNa`() = runBlocking { + val format = Format( + specification = FormatSpecification(ZipSpecification, RpfSpecification), + mediaType = MediaType.DIVINA, + fileExtension = FileExtension("divina") + ) + + assertEquals( + format, + sniffer.sniffFileExtension("divina").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("application/divina+zip").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("divina-package.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff DiViNa manifest`() = runBlocking { + val format = Format( + specification = FormatSpecification(JsonSpecification, RwpmSpecification), + mediaType = MediaType.DIVINA_MANIFEST, + fileExtension = FileExtension("json") + ) + + assertEquals( + format, + sniffer.sniffMediaType("application/divina+json").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("divina.json")).checkSuccess() + ) + } + + @Test + fun `sniff EPUB`() = runBlocking { + assertEquals( + epubFormat, + sniffer.sniffFileExtension("epub").checkSuccess() + ) + assertEquals( + epubFormat, + sniffer.sniffMediaType("application/epub+zip").checkSuccess() + ) + assertEquals( + epubFormat, + sniffer.sniff(fixtures.fileAt("epub.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff AVIF`() = runBlocking { + val format = Format( + specification = FormatSpecification(AvifSpecification), + mediaType = MediaType.AVIF, + fileExtension = FileExtension("avif") + ) + + assertEquals(format, sniffer.sniffFileExtension("avif").checkSuccess()) + assertEquals(format, sniffer.sniffMediaType("image/avif").checkSuccess()) + } + + @Test + fun `sniff GIF`() = runBlocking { + val format = Format( + specification = FormatSpecification(GifSpecification), + mediaType = MediaType.GIF, + fileExtension = FileExtension("gif") + ) + + assertEquals(format, sniffer.sniffFileExtension("gif").checkSuccess()) + assertEquals(format, sniffer.sniffMediaType("image/gif").checkSuccess()) + } + + @Test + fun `sniff HTML`() = runBlocking { + val format = Format( + specification = FormatSpecification(HtmlSpecification), + mediaType = MediaType.HTML, + fileExtension = FileExtension("html") + ) + assertEquals( + format, + sniffer.sniffFileExtension("htm").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffFileExtension("html").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("text/html").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("html.unknown")).checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("html-doctype-case.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff XHTML`() = runBlocking { + val format = Format( + specification = FormatSpecification(XmlSpecification, HtmlSpecification), + mediaType = MediaType.XHTML, + fileExtension = FileExtension("xhtml") + ) + + assertEquals( + format, + sniffer.sniffFileExtension("xht").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffFileExtension("xhtml").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("application/xhtml+xml").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("xhtml.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff JPEG`() = runBlocking { + val format = Format( + specification = FormatSpecification(JpegSpecification), + mediaType = MediaType.JPEG, + fileExtension = FileExtension("jpg") + ) + + assertEquals(format, sniffer.sniffFileExtension("jpg").checkSuccess()) + assertEquals(format, sniffer.sniffFileExtension("jpeg").checkSuccess()) + assertEquals(format, sniffer.sniffFileExtension("jpe").checkSuccess()) + assertEquals(format, sniffer.sniffFileExtension("jif").checkSuccess()) + assertEquals(format, sniffer.sniffFileExtension("jfif").checkSuccess()) + assertEquals(format, sniffer.sniffFileExtension("jfi").checkSuccess()) + assertEquals(format, sniffer.sniffMediaType("image/jpeg").checkSuccess()) + } + + @Test + fun `sniff JXL`() = runBlocking { + val format = Format( + specification = FormatSpecification(JxlSpecification), + mediaType = MediaType.JXL, + fileExtension = FileExtension("jxl") + ) + + assertEquals(format, sniffer.sniffFileExtension("jxl").checkSuccess()) + assertEquals(format, sniffer.sniffMediaType("image/jxl").checkSuccess()) + } + + @Test + fun `sniff RAR`() = runBlocking { + val format = Format( + specification = FormatSpecification(RarSpecification), + mediaType = MediaType.RAR, + fileExtension = FileExtension("rar") + ) + + assertEquals( + format, + sniffer.sniffFileExtension("rar").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("application/vnd.rar").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("application/x-rar").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("application/x-rar-compressed").checkSuccess() + ) + } + + @Test + fun `sniff OPDS 1 feed`() = runBlocking { + val format = Format( + specification = FormatSpecification(XmlSpecification, Opds1CatalogSpecification), + mediaType = MediaType.OPDS1, + fileExtension = FileExtension("xml") + ) + + assertEquals( + format, + sniffer.sniffMediaType("application/atom+xml;profile=opds-catalog").checkSuccess() + ) + assertEquals( + format.copy(mediaType = MediaType.OPDS1_NAVIGATION_FEED), + sniffer.sniffMediaType("application/atom+xml;profile=opds-catalog;kind=navigation").checkSuccess() + ) + assertEquals( + format.copy(mediaType = MediaType.OPDS1_ACQUISITION_FEED), + sniffer.sniffMediaType("application/atom+xml;profile=opds-catalog;kind=acquisition").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("opds1-feed.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff OPDS 1 entry`() = runBlocking { + val format = Format( + specification = FormatSpecification(XmlSpecification, Opds1EntrySpecification), + mediaType = MediaType.OPDS1_ENTRY, + fileExtension = FileExtension("xml") + ) + + assertEquals( + format, + sniffer.sniffMediaType("application/atom+xml;type=entry;profile=opds-catalog").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("opds1-entry.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff OPDS 2 feed`() = runBlocking { + val format = Format( + specification = FormatSpecification(JsonSpecification, Opds2CatalogSpecification), + mediaType = MediaType.OPDS2, + fileExtension = FileExtension("json") + ) + + assertEquals( + format, + sniffer.sniffMediaType("application/opds+json").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("opds2-feed.json")).checkSuccess() + ) + } + + @Test + fun `sniff OPDS 2 publication`() = runBlocking { + val format = Format( + specification = FormatSpecification(JsonSpecification, Opds2PublicationSpecification), + mediaType = MediaType.OPDS2_PUBLICATION, + fileExtension = FileExtension("json") + ) + + assertEquals( + format, + sniffer.sniffMediaType("application/opds-publication+json").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("opds2-publication.json")).checkSuccess() + ) + } + + @Test + fun `sniff OPDS authentication document`() = runBlocking { + val format = Format( + specification = FormatSpecification(JsonSpecification, OpdsAuthenticationSpecification), + mediaType = MediaType.OPDS_AUTHENTICATION, + fileExtension = FileExtension("json") + ) + + assertEquals( + format, + sniffer.sniffMediaType("application/opds-authentication+json").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("application/vnd.opds.authentication.v1.0+json").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("opds-authentication.json")).checkSuccess() + ) + } + + @Test + fun `sniff LCP protected audiobook`() = runBlocking { + val format = Format( + specification = FormatSpecification( + ZipSpecification, + RpfSpecification, + LcpSpecification + ), + mediaType = MediaType.LCP_PROTECTED_AUDIOBOOK, + fileExtension = FileExtension("lcpa") + ) + + assertEquals( + format, + sniffer.sniffFileExtension("lcpa").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("application/audiobook+lcp").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("audiobook-lcp.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff LCP protected PDF`() = runBlocking { + val format = Format( + specification = FormatSpecification( + ZipSpecification, + RpfSpecification, + LcpSpecification + ), + mediaType = MediaType.LCP_PROTECTED_PDF, + fileExtension = FileExtension("lcpdf") + ) + + assertEquals( + format, + sniffer.sniffFileExtension("lcpdf").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("application/pdf+lcp").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("pdf-lcp.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff LCP license document`() = runBlocking { + val format = Format( + specification = FormatSpecification(JsonSpecification, LcpLicenseSpecification), + mediaType = MediaType.LCP_LICENSE_DOCUMENT, + fileExtension = FileExtension("lcpl") + ) + + assertEquals( + format, + sniffer.sniffFileExtension("lcpl").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("application/vnd.readium.lcp.license.v1.0+json").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("lcpl.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff LPF`() = runBlocking { + val format = Format( + specification = FormatSpecification(ZipSpecification, LpfSpecification), + mediaType = MediaType.LPF, + fileExtension = FileExtension("lpf") + ) + + /*assertEquals( + format, + sniffer.sniffFileExtension("lpf").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("application/lpf+zip").checkSuccess() + )*/ + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("lpf.unknown")).checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("lpf-index-html.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff PDF`() = runBlocking { + val format = Format( + specification = FormatSpecification(PdfSpecification), + mediaType = MediaType.PDF, + fileExtension = FileExtension("pdf") + ) + + assertEquals( + format, + sniffer.sniffFileExtension("pdf").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("application/pdf").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("pdf.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff PNG`() = runBlocking { + val format = Format( + specification = FormatSpecification(PngSpecification), + mediaType = MediaType.PNG, + fileExtension = FileExtension("png") + ) + + assertEquals(format, sniffer.sniffFileExtension("png").checkSuccess()) + assertEquals(format, sniffer.sniffMediaType("image/png").checkSuccess()) + } + + @Test + fun `sniff TIFF`() = runBlocking { + val format = Format( + specification = FormatSpecification(TiffSpecification), + mediaType = MediaType.TIFF, + fileExtension = FileExtension("tiff") + ) + + assertEquals(format, sniffer.sniffFileExtension("tiff").checkSuccess()) + assertEquals(format, sniffer.sniffFileExtension("tif").checkSuccess()) + assertEquals(format, sniffer.sniffMediaType("image/tiff").checkSuccess()) + assertEquals( + format, + sniffer.sniffMediaType("image/tiff-fx").checkSuccess() + ) + } + + @Test + fun `sniff WebP`() = runBlocking { + val format = Format( + specification = FormatSpecification(WebpSpecification), + mediaType = MediaType.WEBP, + fileExtension = FileExtension("webp") + ) + + assertEquals(format, sniffer.sniffFileExtension("webp").checkSuccess()) + assertEquals(format, sniffer.sniffMediaType("image/webp").checkSuccess()) + } + + @Test + fun `sniff WebPub`() = runBlocking { + val format = Format( + specification = FormatSpecification(ZipSpecification, RpfSpecification), + mediaType = MediaType.READIUM_WEBPUB, + fileExtension = FileExtension("webpub") + ) + + assertEquals( + format, + sniffer.sniffFileExtension("webpub").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("application/webpub+zip").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("webpub-package.unknown")).checkSuccess() + ) + } + + @Test + fun `Sniff LCP protected Readium package`() = runBlocking { + val format = Format( + specification = FormatSpecification( + ZipSpecification, + RpfSpecification, + LcpSpecification + ), + mediaType = MediaType.READIUM_WEBPUB, + fileExtension = FileExtension("webpub") + ) + + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("webpub-lcp.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff WebPub manifest`() = runBlocking { + val format = Format( + specification = FormatSpecification(JsonSpecification, RwpmSpecification), + mediaType = MediaType.READIUM_WEBPUB_MANIFEST, + fileExtension = FileExtension("json") + ) + + assertEquals( + format, + sniffer.sniffMediaType("application/webpub+json").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("webpub.json")).checkSuccess() + ) + } + + @Test + fun `sniff W3C WPUB manifest`() = runBlocking { + val format = Format( + specification = FormatSpecification(JsonSpecification, W3cPubManifestSpecification), + mediaType = MediaType.W3C_WPUB_MANIFEST, + fileExtension = FileExtension("json") + ) + + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("w3c-wpub.json")).checkSuccess() + ) + } + + @Test + fun `sniff ZAB`() = runBlocking { + val format = Format( + specification = FormatSpecification(ZipSpecification, InformalAudiobookSpecification), + mediaType = MediaType.ZAB, + fileExtension = FileExtension("zab") + ) + + assertEquals( + format, + sniffer.sniffFileExtension("zab").checkSuccess() + ) + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("zab.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff JSON`() = runBlocking { + val format = Format( + specification = FormatSpecification(JsonSpecification), + mediaType = MediaType.JSON, + fileExtension = FileExtension("json") + ) + + assertEquals( + format, + sniffer.sniff(fixtures.fileAt("any.json")).checkSuccess() + ) + } + + @Test + fun `sniff JSON problem details`() = runBlocking { + val format = Format( + specification = FormatSpecification(JsonSpecification, ProblemDetailsSpecification), + mediaType = MediaType.JSON_PROBLEM_DETAILS, + fileExtension = FileExtension("json") + ) + + assertEquals( + format, + sniffer.sniffMediaType("application/problem+json").checkSuccess() + ) + assertEquals( + format, + sniffer.sniffMediaType("application/problem+json; charset=utf-8").checkSuccess() + ) + + // The sniffing of a JSON document should not take precedence over the JSON problem details. + assertEquals( + format, + sniffer.sniff( + resource = StringResource("""{"title": "Message"}"""), + hints = FormatHints(mediaType = MediaType("application/problem+json")!!) + ).checkSuccess() + ) + } +} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/format/DefaultSniffersTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/format/DefaultSniffersTest.kt new file mode 100644 index 0000000000..311f969596 --- /dev/null +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/format/DefaultSniffersTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.format + +import kotlin.test.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.util.FileExtension +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.checkSuccess +import org.readium.r2.shared.util.mediatype.MediaType +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DefaultSniffersTest { + + private val epubFormat = Format( + specification = FormatSpecification(ZipSpecification, EpubSpecification), + mediaType = MediaType.EPUB, + fileExtension = FileExtension("epub") + ) + + @Test + fun `EpubDrmSniffer doesn't recognize EPUB with empty encryption xml`() = runBlocking { + assertEquals( + epubFormat, + EpubDrmSniffer.sniffContainer( + format = epubFormat, + container = TestContainer( + Url("META-INF/encryption.xml")!! to """<?xml version='1.0' encoding='utf-8'?><encryption xmlns="urn:oasis:names:tc:opendocument:xmlns:container" xmlns:enc="http://www.w3.org/2001/04/xmlenc#"></encryption>""" + ) + ).checkSuccess() + ) + } + + @Test + fun `Sniff Adobe ADEPT`() = runBlocking { + assertEquals( + epubFormat.copy(specification = epubFormat.specification + AdeptSpecification), + EpubDrmSniffer.sniffContainer( + format = epubFormat, + container = TestContainer( + Url("META-INF/encryption.xml")!! to """<?xml version="1.0"?><encryption xmlns="urn:oasis:names:tc:opendocument:xmlns:container"> + <EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#"> + <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"></EncryptionMethod> + <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"> + <resource xmlns="http://ns.adobe.com/adept">urn:uuid:2c43729c-b985-4531-8e86-ae75ce5e5da9</resource> + </KeyInfo> + <CipherData> + <CipherReference URI="OEBPS/stylesheet.css"></CipherReference> + </CipherData> + </EncryptedData> + </encryption>""" + ) + ).checkSuccess() + ) + } + + @Test + fun `Sniff Adobe ADEPT from rights xml`() = runBlocking { + assertEquals( + epubFormat.copy(specification = epubFormat.specification + AdeptSpecification), + EpubDrmSniffer.sniffContainer( + format = epubFormat, + container = TestContainer( + Url("META-INF/encryption.xml")!! to """<?xml version='1.0' encoding='utf-8'?><encryption xmlns="urn:oasis:names:tc:opendocument:xmlns:container" xmlns:enc="http://www.w3.org/2001/04/xmlenc#"></encryption>""", + Url("META-INF/rights.xml")!! to """<?xml version="1.0"?><adept:rights xmlns:adept="http://ns.adobe.com/adept"></adept:rights>""" + ) + ).checkSuccess() + ) + } + + @Test + fun `Sniff LCP protected EPUB`() = runBlocking { + assertEquals( + epubFormat.copy(specification = epubFormat.specification + LcpSpecification), + EpubDrmSniffer.sniffContainer( + format = epubFormat, + container = TestContainer(Url("META-INF/license.lcpl")!! to "{}") + ).checkSuccess() + ) + } + + @Test + fun `Sniff LCP protected EPUB missing the license`() = runBlocking { + assertEquals( + epubFormat.copy(specification = epubFormat.specification + LcpSpecification), + EpubDrmSniffer.sniffContainer( + format = epubFormat, + container = TestContainer( + Url("META-INF/encryption.xml")!! to """<?xml version="1.0" encoding="UTF-8"?> +<encryption xmlns="urn:oasis:names:tc:opendocument:xmlns:container"> + <EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#"> + <EncryptionMethod xmlns="http://www.w3.org/2001/04/xmlenc#" Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc"></EncryptionMethod> + <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"> + <RetrievalMethod xmlns="http://www.w3.org/2000/09/xmldsig#" URI="license.lcpl#/encryption/content_key" Type="http://readium.org/2014/01/lcp#EncryptedContentKey"></RetrievalMethod> + </KeyInfo> + <CipherData xmlns="http://www.w3.org/2001/04/xmlenc#"> + <CipherReference xmlns="http://www.w3.org/2001/04/xmlenc#" URI="OPS/chapter_001.xhtml"></CipherReference> + </CipherData> + <EncryptionProperties xmlns="http://www.w3.org/2001/04/xmlenc#"> + <EncryptionProperty xmlns="http://www.w3.org/2001/04/xmlenc#"> + <Compression xmlns="http://www.idpf.org/2016/encryption#compression" Method="8" OriginalLength="13877"></Compression> + </EncryptionProperty> + </EncryptionProperties> + </EncryptedData> +</encryption>""" + ) + ).checkSuccess() + ) + } +} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/format/TestContainer.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/format/TestContainer.kt new file mode 100644 index 0000000000..5f6b02b8b3 --- /dev/null +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/format/TestContainer.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.format + +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.StringResource + +class TestContainer( + private val resources: Map<Url, String> = emptyMap() +) : Container<Resource> { + + companion object { + + operator fun invoke(vararg entry: Pair<Url, String>): TestContainer = + TestContainer(entry.toMap()) + } + + override val entries: Set<Url> = + resources.keys + + override fun get(url: Url): Resource? = + resources[url]?.let { StringResource(it) } + + override suspend fun close() {} +} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/http/ProblemDetailsTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/http/ProblemDetailsTest.kt index 99f3e28ecf..4c3cfcf824 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/http/ProblemDetailsTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/http/ProblemDetailsTest.kt @@ -18,7 +18,10 @@ class ProblemDetailsTest { """ ) - assertEquals(ProblemDetails(title = "You do not have enough credit."), ProblemDetails.fromJSON(json)) + assertEquals( + ProblemDetails(title = "You do not have enough credit."), + ProblemDetails.fromJSON(json) + ) } @Test diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeTest.kt index fcc7315892..4e4a1f8417 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeTest.kt @@ -1,22 +1,21 @@ package org.readium.r2.shared.util.mediatype import kotlin.test.* -import kotlinx.coroutines.runBlocking import org.junit.Test class MediaTypeTest { @Test fun `returns null for invalid types`() { - assertNull(MediaType.parse("application")) - assertNull(MediaType.parse("application/atom+xml/extra")) + assertNull(MediaType("application")) + assertNull(MediaType("application/atom+xml/extra")) } @Test fun `to string`() { assertEquals( "application/atom+xml;profile=opds-catalog", - MediaType.parse("application/atom+xml;profile=opds-catalog")?.toString() + MediaType("application/atom+xml;profile=opds-catalog")?.toString() ) } @@ -24,16 +23,16 @@ class MediaTypeTest { fun `to string is normalized`() { assertEquals( "application/atom+xml;a=0;profile=OPDS-CATALOG", - MediaType.parse("APPLICATION/ATOM+XML;PROFILE=OPDS-CATALOG ; a=0")?.toString() + MediaType("APPLICATION/ATOM+XML;PROFILE=OPDS-CATALOG ; a=0")?.toString() ) // Parameters are sorted by name assertEquals( "application/atom+xml;a=0;b=1", - MediaType.parse("application/atom+xml;a=0;b=1")?.toString() + MediaType("application/atom+xml;a=0;b=1")?.toString() ) assertEquals( "application/atom+xml;a=0;b=1", - MediaType.parse("application/atom+xml;b=1;a=0")?.toString() + MediaType("application/atom+xml;b=1;a=0")?.toString() ) } @@ -41,18 +40,18 @@ class MediaTypeTest { fun `get type`() { assertEquals( "application", - MediaType.parse("application/atom+xml;profile=opds-catalog")?.type + MediaType("application/atom+xml;profile=opds-catalog")?.type ) - assertEquals("*", MediaType.parse("*/jpeg")?.type) + assertEquals("*", MediaType("*/jpeg")?.type) } @Test fun `get subtype`() { assertEquals( "atom+xml", - MediaType.parse("application/atom+xml;profile=opds-catalog")?.subtype + MediaType("application/atom+xml;profile=opds-catalog")?.subtype ) - assertEquals("*", MediaType.parse("image/*")?.subtype) + assertEquals("*", MediaType("image/*")?.subtype) } @Test @@ -62,13 +61,13 @@ class MediaTypeTest { "type" to "entry", "profile" to "opds-catalog" ), - MediaType.parse("application/atom+xml;type=entry;profile=opds-catalog")?.parameters + MediaType("application/atom+xml;type=entry;profile=opds-catalog")?.parameters ) } @Test fun `get empty parameters`() { - assertTrue(MediaType.parse("application/atom+xml")!!.parameters.isEmpty()) + assertTrue(MediaType("application/atom+xml")!!.parameters.isEmpty()) } @Test @@ -78,28 +77,30 @@ class MediaTypeTest { "type" to "entry", "profile" to "opds-catalog" ), - MediaType.parse("application/atom+xml ; type=entry ; profile=opds-catalog ")?.parameters + MediaType( + "application/atom+xml ; type=entry ; profile=opds-catalog " + )?.parameters ) } @Test fun `get structured syntax suffix`() { - assertNull(MediaType.parse("foo/bar")?.structuredSyntaxSuffix) - assertNull(MediaType.parse("application/zip")?.structuredSyntaxSuffix) - assertEquals("+zip", MediaType.parse("application/epub+zip")?.structuredSyntaxSuffix) - assertEquals("+zip", MediaType.parse("foo/bar+json+zip")?.structuredSyntaxSuffix) + assertNull(MediaType("foo/bar")?.structuredSyntaxSuffix) + assertNull(MediaType("application/zip")?.structuredSyntaxSuffix) + assertEquals("+zip", MediaType("application/epub+zip")?.structuredSyntaxSuffix) + assertEquals("+zip", MediaType("foo/bar+json+zip")?.structuredSyntaxSuffix) } @Test fun `get charset`() { - assertNull(MediaType.parse("text/html")?.charset) - assertEquals(Charsets.UTF_8, MediaType.parse("text/html;charset=utf-8")?.charset) - assertEquals(Charsets.UTF_16, MediaType.parse("text/html;charset=utf-16")?.charset) + assertNull(MediaType("text/html")?.charset) + assertEquals(Charsets.UTF_8, MediaType("text/html;charset=utf-8")?.charset) + assertEquals(Charsets.UTF_16, MediaType("text/html;charset=utf-16")?.charset) } @Test fun `type, subtype and parameter names are lowercased`() { - val mediaType = MediaType.parse("APPLICATION/ATOM+XML;PROFILE=OPDS-CATALOG") + val mediaType = MediaType("APPLICATION/ATOM+XML;PROFILE=OPDS-CATALOG") assertEquals("application", mediaType?.type) assertEquals("atom+xml", mediaType?.subtype) assertEquals(mapOf("profile" to "OPDS-CATALOG"), mediaType?.parameters) @@ -107,283 +108,303 @@ class MediaTypeTest { @Test fun `charset value is uppercased`() { - assertEquals("UTF-8", MediaType.parse("text/html;charset=utf-8")?.parameters?.get("charset")) + assertEquals( + "UTF-8", + MediaType("text/html;charset=utf-8")?.parameters?.get("charset") + ) } @Test fun `charset value is canonicalized`() { - assertEquals("US-ASCII", MediaType.parse("text/html;charset=ascii")?.parameters?.get("charset")) - assertEquals("UNKNOWN", MediaType.parse("text/html;charset=unknown")?.parameters?.get("charset")) - } - - @Test - fun `canonicalize media type`() = runBlocking { - assertEquals(MediaType.parse("text/html", fileExtension = "html")!!, MediaType.parse("text/html;charset=utf-8")!!.canonicalMediaType()) - assertEquals(MediaType.parse("application/atom+xml;profile=opds-catalog")!!, MediaType.parse("application/atom+xml;profile=opds-catalog;charset=utf-8")!!.canonicalMediaType()) - assertEquals(MediaType.parse("application/unknown;charset=utf-8")!!, MediaType.parse("application/unknown;charset=utf-8")!!.canonicalMediaType()) + assertEquals( + "US-ASCII", + MediaType("text/html;charset=ascii")?.parameters?.get("charset") + ) + assertEquals( + "UNKNOWN", + MediaType("text/html;charset=unknown")?.parameters?.get("charset") + ) } @Test fun equality() { - assertEquals(MediaType.parse("application/atom+xml")!!, MediaType.parse("application/atom+xml")!!) - assertEquals(MediaType.parse("application/atom+xml;profile=opds-catalog")!!, MediaType.parse("application/atom+xml;profile=opds-catalog")!!) - assertNotEquals(MediaType.parse("application/atom+xml")!!, MediaType.parse("application/atom")!!) - assertNotEquals(MediaType.parse("application/atom+xml")!!, MediaType.parse("text/atom+xml")!!) - assertNotEquals(MediaType.parse("application/atom+xml;profile=opds-catalog")!!, MediaType.parse("application/atom+xml")!!) + assertEquals( + MediaType("application/atom+xml")!!, + MediaType("application/atom+xml")!! + ) + assertEquals( + MediaType("application/atom+xml;profile=opds-catalog")!!, + MediaType("application/atom+xml;profile=opds-catalog")!! + ) + assertNotEquals( + MediaType("application/atom+xml")!!, + MediaType("application/atom")!! + ) + assertNotEquals( + MediaType("application/atom+xml")!!, + MediaType("text/atom+xml")!! + ) + assertNotEquals( + MediaType("application/atom+xml;profile=opds-catalog")!!, + MediaType("application/atom+xml")!! + ) } @Test fun `equality ignores case of type, subtype and parameter names`() { assertEquals( - MediaType.parse("application/atom+xml;profile=opds-catalog")!!, - MediaType.parse("APPLICATION/ATOM+XML;PROFILE=opds-catalog")!! + MediaType("application/atom+xml;profile=opds-catalog")!!, + MediaType("APPLICATION/ATOM+XML;PROFILE=opds-catalog")!! ) assertNotEquals( - MediaType.parse("application/atom+xml;profile=opds-catalog")!!, - MediaType.parse("APPLICATION/ATOM+XML;PROFILE=OPDS-CATALOG")!! + MediaType("application/atom+xml;profile=opds-catalog")!!, + MediaType("APPLICATION/ATOM+XML;PROFILE=OPDS-CATALOG")!! ) } @Test fun `equality ignores parameters order`() { assertEquals( - MediaType.parse("application/atom+xml;type=entry;profile=opds-catalog")!!, - MediaType.parse("application/atom+xml;profile=opds-catalog;type=entry")!! + MediaType("application/atom+xml;type=entry;profile=opds-catalog")!!, + MediaType("application/atom+xml;profile=opds-catalog;type=entry")!! ) } @Test fun `equality ignores charset case`() { assertEquals( - MediaType.parse("application/atom+xml;charset=utf-8")!!, - MediaType.parse("application/atom+xml;charset=UTF-8")!! + MediaType("application/atom+xml;charset=utf-8")!!, + MediaType("application/atom+xml;charset=UTF-8")!! ) } @Test fun `contains equal media type`() { assertTrue( - MediaType.parse("text/html;charset=utf-8") - !!.contains(MediaType.parse("text/html;charset=utf-8")) + MediaType("text/html;charset=utf-8")!!.contains( + MediaType("text/html;charset=utf-8") + ) ) } @Test fun `contains must match parameters`() { assertFalse( - MediaType.parse("text/html;charset=utf-8") - !!.contains(MediaType.parse("text/html;charset=ascii")) + MediaType("text/html;charset=utf-8")!!.contains( + MediaType("text/html;charset=ascii") + ) ) assertFalse( - MediaType.parse("text/html;charset=utf-8") - !!.contains(MediaType.parse("text/html")) + MediaType("text/html;charset=utf-8")!!.contains(MediaType("text/html")) ) } @Test fun `contains ignores parameters order`() { assertTrue( - MediaType.parse("text/html;charset=utf-8;type=entry") - !!.contains(MediaType.parse("text/html;type=entry;charset=utf-8")) + MediaType("text/html;charset=utf-8;type=entry")!!.contains( + MediaType("text/html;type=entry;charset=utf-8") + ) ) } @Test fun `contains ignore extra parameters`() { assertTrue( - MediaType.parse("text/html") - !!.contains(MediaType.parse("text/html;charset=utf-8")) + MediaType("text/html")!!.contains(MediaType("text/html;charset=utf-8")) ) } @Test fun `contains supports wildcards`() { assertTrue( - MediaType.parse("*/*") - !!.contains(MediaType.parse("text/html;charset=utf-8")) + MediaType("*/*")!!.contains(MediaType("text/html;charset=utf-8")) ) assertTrue( - MediaType.parse("text/*") - !!.contains(MediaType.parse("text/html;charset=utf-8")) + MediaType("text/*")!!.contains(MediaType("text/html;charset=utf-8")) ) assertFalse( - MediaType.parse("text/*") - !!.contains(MediaType.parse("application/zip")) + MediaType("text/*")!!.contains(MediaType("application/zip")) ) } @Test fun `contains from string`() { assertTrue( - MediaType.parse("text/html;charset=utf-8") - !!.contains("text/html;charset=utf-8") + MediaType("text/html;charset=utf-8")!!.contains("text/html;charset=utf-8") ) } @Test fun `matches equal media type`() { assertTrue( - MediaType.parse("text/html;charset=utf-8") - !!.matches(MediaType.parse("text/html;charset=utf-8")) + MediaType("text/html;charset=utf-8")!!.matches( + MediaType("text/html;charset=utf-8") + ) ) } @Test fun `matches must match parameters`() { assertFalse( - MediaType.parse("text/html;charset=ascii") - !!.matches(MediaType.parse("text/html;charset=utf-8")) + MediaType("text/html;charset=ascii")!!.matches( + MediaType("text/html;charset=utf-8") + ) ) } @Test fun `matches ignores parameters order`() { assertTrue( - MediaType.parse("text/html;charset=utf-8;type=entry") - !!.matches(MediaType.parse("text/html;type=entry;charset=utf-8")) + MediaType("text/html;charset=utf-8;type=entry")!!.matches( + MediaType("text/html;type=entry;charset=utf-8") + ) ) } @Test fun `matches ignores extra parameters`() { assertTrue( - MediaType.parse("text/html;charset=utf-8") - !!.matches(MediaType.parse("text/html;charset=utf-8;extra=param")) + MediaType("text/html;charset=utf-8")!!.matches( + MediaType("text/html;charset=utf-8;extra=param") + ) ) assertTrue( - MediaType.parse("text/html;charset=utf-8;extra=param") - !!.matches(MediaType.parse("text/html;charset=utf-8")) + MediaType("text/html;charset=utf-8;extra=param")!!.matches( + MediaType("text/html;charset=utf-8") + ) ) } @Test fun `matches supports wildcards`() { - assertTrue(MediaType.parse("text/html;charset=utf-8")!!.matches(MediaType.parse("*/*"))) - assertTrue(MediaType.parse("text/html;charset=utf-8")!!.matches(MediaType.parse("text/*"))) - assertFalse(MediaType.parse("application/zip")!!.matches(MediaType.parse("text/*"))) - assertTrue(MediaType.parse("*/*")!!.matches(MediaType.parse("text/html;charset=utf-8"))) - assertTrue(MediaType.parse("text/*")!!.matches(MediaType.parse("text/html;charset=utf-8"))) - assertFalse(MediaType.parse("text/*")!!.matches(MediaType.parse("application/zip"))) + assertTrue(MediaType("text/html;charset=utf-8")!!.matches(MediaType("*/*"))) + assertTrue(MediaType("text/html;charset=utf-8")!!.matches(MediaType("text/*"))) + assertFalse(MediaType("application/zip")!!.matches(MediaType("text/*"))) + assertTrue(MediaType("*/*")!!.matches(MediaType("text/html;charset=utf-8"))) + assertTrue(MediaType("text/*")!!.matches(MediaType("text/html;charset=utf-8"))) + assertFalse(MediaType("text/*")!!.matches(MediaType("application/zip"))) } @Test fun `matches from string`() { assertTrue( - MediaType.parse("text/html;charset=utf-8") - !!.matches("text/html;charset=utf-8") + MediaType("text/html;charset=utf-8")!!.matches("text/html;charset=utf-8") ) } @Test fun `matches any media type`() { assertTrue( - MediaType.parse("text/html") - !!.matchesAny(MediaType.parse("application/zip")!!, MediaType.parse("text/html;charset=utf-8")!!) + MediaType("text/html")!!.matchesAny( + MediaType("application/zip")!!, + MediaType("text/html;charset=utf-8")!! + ) ) assertFalse( - MediaType.parse("text/html") - !!.matchesAny(MediaType.parse("application/zip")!!, MediaType.parse("text/plain;charset=utf-8")!!) + MediaType("text/html")!!.matchesAny( + MediaType("application/zip")!!, + MediaType("text/plain;charset=utf-8")!! + ) ) assertTrue( - MediaType.parse("text/html") - !!.matchesAny("application/zip", "text/html;charset=utf-8") + MediaType("text/html")!!.matchesAny("application/zip", "text/html;charset=utf-8") ) assertFalse( - MediaType.parse("text/html") - !!.matchesAny("application/zip", "text/plain;charset=utf-8") + MediaType("text/html")!!.matchesAny("application/zip", "text/plain;charset=utf-8") ) } @Test fun `is ZIP`() { - assertFalse(MediaType.parse("text/plain")!!.isZip) - assertTrue(MediaType.parse("application/zip")!!.isZip) - assertTrue(MediaType.parse("application/zip;charset=utf-8")!!.isZip) - assertTrue(MediaType.parse("application/epub+zip")!!.isZip) + assertFalse(MediaType("text/plain")!!.isZip) + assertTrue(MediaType("application/zip")!!.isZip) + assertTrue(MediaType("application/zip;charset=utf-8")!!.isZip) + assertTrue(MediaType("application/epub+zip")!!.isZip) // These media types must be explicitly matched since they don't have any ZIP hint - assertTrue(MediaType.parse("application/audiobook+lcp")!!.isZip) - assertTrue(MediaType.parse("application/pdf+lcp")!!.isZip) + assertTrue(MediaType("application/audiobook+lcp")!!.isZip) + assertTrue(MediaType("application/pdf+lcp")!!.isZip) } @Test fun `is JSON`() { - assertFalse(MediaType.parse("text/plain")!!.isJson) - assertTrue(MediaType.parse("application/json")!!.isJson) - assertTrue(MediaType.parse("application/json;charset=utf-8")!!.isJson) - assertTrue(MediaType.parse("application/opds+json")!!.isJson) + assertFalse(MediaType("text/plain")!!.isJson) + assertTrue(MediaType("application/json")!!.isJson) + assertTrue(MediaType("application/json;charset=utf-8")!!.isJson) + assertTrue(MediaType("application/opds+json")!!.isJson) } @Test fun `is OPDS`() { - assertFalse(MediaType.parse("text/html")!!.isOpds) - assertTrue(MediaType.parse("application/atom+xml;profile=opds-catalog")!!.isOpds) - assertTrue(MediaType.parse("application/atom+xml;type=entry;profile=opds-catalog")!!.isOpds) - assertTrue(MediaType.parse("application/opds+json")!!.isOpds) - assertTrue(MediaType.parse("application/opds-publication+json")!!.isOpds) - assertTrue(MediaType.parse("application/opds+json;charset=utf-8")!!.isOpds) - assertTrue(MediaType.parse("application/opds-authentication+json")!!.isOpds) + assertFalse(MediaType("text/html")!!.isOpds) + assertTrue(MediaType("application/atom+xml;profile=opds-catalog")!!.isOpds) + assertTrue(MediaType("application/atom+xml;type=entry;profile=opds-catalog")!!.isOpds) + assertTrue(MediaType("application/opds+json")!!.isOpds) + assertTrue(MediaType("application/opds-publication+json")!!.isOpds) + assertTrue(MediaType("application/opds+json;charset=utf-8")!!.isOpds) + assertTrue(MediaType("application/opds-authentication+json")!!.isOpds) } @Test fun `is HTML`() { - assertFalse(MediaType.parse("application/opds+json")!!.isHtml) - assertTrue(MediaType.parse("text/html")!!.isHtml) - assertTrue(MediaType.parse("application/xhtml+xml")!!.isHtml) - assertTrue(MediaType.parse("text/html;charset=utf-8")!!.isHtml) + assertFalse(MediaType("application/opds+json")!!.isHtml) + assertTrue(MediaType("text/html")!!.isHtml) + assertTrue(MediaType("application/xhtml+xml")!!.isHtml) + assertTrue(MediaType("text/html;charset=utf-8")!!.isHtml) } @Test fun `is bitmap`() { - assertFalse(MediaType.parse("text/html")!!.isBitmap) - assertTrue(MediaType.parse("image/bmp")!!.isBitmap) - assertTrue(MediaType.parse("image/gif")!!.isBitmap) - assertTrue(MediaType.parse("image/jpeg")!!.isBitmap) - assertTrue(MediaType.parse("image/png")!!.isBitmap) - assertTrue(MediaType.parse("image/tiff")!!.isBitmap) - assertTrue(MediaType.parse("image/tiff")!!.isBitmap) - assertTrue(MediaType.parse("image/tiff;charset=utf-8")!!.isBitmap) + assertFalse(MediaType("text/html")!!.isBitmap) + assertTrue(MediaType("image/bmp")!!.isBitmap) + assertTrue(MediaType("image/gif")!!.isBitmap) + assertTrue(MediaType("image/jpeg")!!.isBitmap) + assertTrue(MediaType("image/png")!!.isBitmap) + assertTrue(MediaType("image/tiff")!!.isBitmap) + assertTrue(MediaType("image/tiff")!!.isBitmap) + assertTrue(MediaType("image/tiff;charset=utf-8")!!.isBitmap) } @Test fun `is audio`() { - assertFalse(MediaType.parse("text/html")!!.isAudio) - assertTrue(MediaType.parse("audio/unknown")!!.isAudio) - assertTrue(MediaType.parse("audio/mpeg;param=value")!!.isAudio) + assertFalse(MediaType("text/html")!!.isAudio) + assertTrue(MediaType("audio/unknown")!!.isAudio) + assertTrue(MediaType("audio/mpeg;param=value")!!.isAudio) } @Test fun `is video`() { - assertFalse(MediaType.parse("text/html")!!.isVideo) - assertTrue(MediaType.parse("video/unknown")!!.isVideo) - assertTrue(MediaType.parse("video/mpeg;param=value")!!.isVideo) + assertFalse(MediaType("text/html")!!.isVideo) + assertTrue(MediaType("video/unknown")!!.isVideo) + assertTrue(MediaType("video/mpeg;param=value")!!.isVideo) } @Test fun `is RWPM`() { - assertFalse(MediaType.parse("text/html")!!.isRwpm) - assertTrue(MediaType.parse("application/audiobook+json")!!.isRwpm) - assertTrue(MediaType.parse("application/divina+json")!!.isRwpm) - assertTrue(MediaType.parse("application/webpub+json")!!.isRwpm) - assertTrue(MediaType.parse("application/webpub+json;charset=utf-8")!!.isRwpm) + assertFalse(MediaType("text/html")!!.isRwpm) + assertTrue(MediaType("application/audiobook+json")!!.isRwpm) + assertTrue(MediaType("application/divina+json")!!.isRwpm) + assertTrue(MediaType("application/webpub+json")!!.isRwpm) + assertTrue(MediaType("application/webpub+json;charset=utf-8")!!.isRwpm) } @Test fun `is publication`() { - assertFalse(MediaType.parse("text/html")!!.isPublication) - assertTrue(MediaType.parse("application/audiobook+zip")!!.isPublication) - assertTrue(MediaType.parse("application/audiobook+json")!!.isPublication) - assertTrue(MediaType.parse("application/audiobook+lcp")!!.isPublication) - assertTrue(MediaType.parse("application/audiobook+json;charset=utf-8")!!.isPublication) - assertTrue(MediaType.parse("application/divina+zip")!!.isPublication) - assertTrue(MediaType.parse("application/divina+json")!!.isPublication) - assertTrue(MediaType.parse("application/webpub+zip")!!.isPublication) - assertTrue(MediaType.parse("application/webpub+json")!!.isPublication) - assertTrue(MediaType.parse("application/vnd.comicbook+zip")!!.isPublication) - assertTrue(MediaType.parse("application/epub+zip")!!.isPublication) - assertTrue(MediaType.parse("application/lpf+zip")!!.isPublication) - assertTrue(MediaType.parse("application/pdf")!!.isPublication) - assertTrue(MediaType.parse("application/pdf+lcp")!!.isPublication) - assertTrue(MediaType.parse("application/x.readium.w3c.wpub+json")!!.isPublication) - assertTrue(MediaType.parse("application/x.readium.zab+zip")!!.isPublication) + assertFalse(MediaType("text/html")!!.isPublication) + assertTrue(MediaType("application/audiobook+zip")!!.isPublication) + assertTrue(MediaType("application/audiobook+json")!!.isPublication) + assertTrue(MediaType("application/audiobook+lcp")!!.isPublication) + assertTrue(MediaType("application/audiobook+json;charset=utf-8")!!.isPublication) + assertTrue(MediaType("application/divina+zip")!!.isPublication) + assertTrue(MediaType("application/divina+json")!!.isPublication) + assertTrue(MediaType("application/webpub+zip")!!.isPublication) + assertTrue(MediaType("application/webpub+json")!!.isPublication) + assertTrue(MediaType("application/vnd.comicbook+zip")!!.isPublication) + assertTrue(MediaType("application/epub+zip")!!.isPublication) + assertTrue(MediaType("application/lpf+zip")!!.isPublication) + assertTrue(MediaType("application/pdf")!!.isPublication) + assertTrue(MediaType("application/pdf+lcp")!!.isPublication) + assertTrue(MediaType("application/x.readium.w3c.wpub+json")!!.isPublication) + assertTrue(MediaType("application/x.readium.zab+zip")!!.isPublication) } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/SnifferTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/SnifferTest.kt deleted file mode 100644 index f58c0c4a78..0000000000 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/SnifferTest.kt +++ /dev/null @@ -1,315 +0,0 @@ -package org.readium.r2.shared.util.mediatype - -import android.webkit.MimeTypeMap -import kotlin.test.assertEquals -import kotlin.test.assertNull -import kotlinx.coroutines.runBlocking -import org.junit.Test -import org.junit.runner.RunWith -import org.readium.r2.shared.Fixtures -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows.shadowOf - -@RunWith(RobolectricTestRunner::class) -class SnifferTest { - - val fixtures = Fixtures("format") - - @Test - fun `sniff ignores extension case`() = runBlocking { - assertEquals(MediaType.EPUB, MediaType.of(fileExtension = "EPUB")) - } - - @Test - fun `sniff ignores media type case`() = runBlocking { - assertEquals(MediaType.EPUB, MediaType.of(mediaType = "APPLICATION/EPUB+ZIP")) - } - - @Test - fun `sniff ignores media type extra parameters`() = runBlocking { - assertEquals(MediaType.EPUB, MediaType.of(mediaType = "application/epub+zip;param=value")) - } - - @Test - fun `sniff from metadata`() = runBlocking { - assertNull(MediaType.of(fileExtension = null)) - assertEquals(MediaType.READIUM_AUDIOBOOK, MediaType.of(fileExtension = "audiobook")) - assertNull(MediaType.of(mediaType = null)) - assertEquals(MediaType.READIUM_AUDIOBOOK, MediaType.of(mediaType = "application/audiobook+zip")) - assertEquals(MediaType.READIUM_AUDIOBOOK, MediaType.of(mediaType = "application/audiobook+zip")) - assertEquals(MediaType.READIUM_AUDIOBOOK, MediaType.of(mediaType = "application/audiobook+zip", fileExtension = "audiobook")) - assertEquals(MediaType.READIUM_AUDIOBOOK, MediaType.of(mediaTypes = listOf("application/audiobook+zip"), fileExtensions = listOf("audiobook"))) - } - - @Test - fun `sniff from a file`() = runBlocking { - assertEquals(MediaType.READIUM_AUDIOBOOK_MANIFEST, MediaType.ofFile(fixtures.fileAt("audiobook.json"))) - } - - @Test - fun `sniff from bytes`() = runBlocking { - assertEquals(MediaType.READIUM_AUDIOBOOK_MANIFEST, MediaType.ofBytes({ fixtures.fileAt("audiobook.json").readBytes() })) - } - - @Test - fun `sniff unknown format`() = runBlocking { - assertNull(MediaType.of(mediaType = "invalid")) - assertNull(MediaType.ofFile(fixtures.fileAt("unknown"))) - } - - @Test - fun `sniff falls back on parsing the given media type if it's valid`() = runBlocking { - val expected = MediaType.parse("fruit/grapes")!! - assertEquals(expected, MediaType.of(mediaType = "fruit/grapes")) - assertEquals(expected, MediaType.of(mediaType = "fruit/grapes")) - assertEquals(expected, MediaType.of(mediaTypes = listOf("invalid", "fruit/grapes"), fileExtensions = emptyList())) - assertEquals(expected, MediaType.of(mediaTypes = listOf("fruit/grapes", "vegetable/brocoli"), fileExtensions = emptyList())) - } - - @Test - fun `sniff audiobook`() = runBlocking { - assertEquals(MediaType.READIUM_AUDIOBOOK, MediaType.of(fileExtension = "audiobook")) - assertEquals(MediaType.READIUM_AUDIOBOOK, MediaType.of(mediaType = "application/audiobook+zip")) - assertEquals(MediaType.READIUM_AUDIOBOOK, MediaType.ofFile(fixtures.fileAt("audiobook-package.unknown"))) - } - - @Test - fun `sniff audiobook manifest`() = runBlocking { - assertEquals(MediaType.READIUM_AUDIOBOOK_MANIFEST, MediaType.of(mediaType = "application/audiobook+json")) - assertEquals(MediaType.READIUM_AUDIOBOOK_MANIFEST, MediaType.ofFile(fixtures.fileAt("audiobook.json"))) - assertEquals(MediaType.READIUM_AUDIOBOOK_MANIFEST, MediaType.ofFile(fixtures.fileAt("audiobook-wrongtype.json"))) - } - - @Test - fun `sniff BMP`() = runBlocking { - assertEquals(MediaType.BMP, MediaType.of(fileExtension = "bmp")) - assertEquals(MediaType.BMP, MediaType.of(fileExtension = "dib")) - assertEquals(MediaType.BMP, MediaType.of(mediaType = "image/bmp")) - assertEquals(MediaType.BMP, MediaType.of(mediaType = "image/x-bmp")) - } - - @Test - fun `sniff CBZ`() = runBlocking { - assertEquals(MediaType.CBZ, MediaType.of(fileExtension = "cbz")) - assertEquals(MediaType.CBZ, MediaType.of(mediaType = "application/vnd.comicbook+zip")) - assertEquals(MediaType.CBZ, MediaType.of(mediaType = "application/x-cbz")) - assertEquals(MediaType.CBZ, MediaType.of(mediaType = "application/x-cbr")) - assertEquals(MediaType.CBZ, MediaType.ofFile(fixtures.fileAt("cbz.unknown"))) - } - - @Test - fun `sniff DiViNa`() = runBlocking { - assertEquals(MediaType.DIVINA, MediaType.of(fileExtension = "divina")) - assertEquals(MediaType.DIVINA, MediaType.of(mediaType = "application/divina+zip")) - assertEquals(MediaType.DIVINA, MediaType.ofFile(fixtures.fileAt("divina-package.unknown"))) - } - - @Test - fun `sniff DiViNa manifest`() = runBlocking { - assertEquals(MediaType.DIVINA_MANIFEST, MediaType.of(mediaType = "application/divina+json")) - assertEquals(MediaType.DIVINA_MANIFEST, MediaType.ofFile(fixtures.fileAt("divina.json"))) - } - - @Test - fun `sniff EPUB`() = runBlocking { - assertEquals(MediaType.EPUB, MediaType.of(fileExtension = "epub")) - assertEquals(MediaType.EPUB, MediaType.of(mediaType = "application/epub+zip")) - assertEquals(MediaType.EPUB, MediaType.ofFile(fixtures.fileAt("epub.unknown"))) - } - - @Test - fun `sniff AVIF`() = runBlocking { - assertEquals(MediaType.AVIF, MediaType.of(fileExtension = "avif")) - assertEquals(MediaType.AVIF, MediaType.of(mediaType = "image/avif")) - } - - @Test - fun `sniff GIF`() = runBlocking { - assertEquals(MediaType.GIF, MediaType.of(fileExtension = "gif")) - assertEquals(MediaType.GIF, MediaType.of(mediaType = "image/gif")) - } - - @Test - fun `sniff HTML`() = runBlocking { - assertEquals(MediaType.HTML, MediaType.of(fileExtension = "htm")) - assertEquals(MediaType.HTML, MediaType.of(fileExtension = "html")) - assertEquals(MediaType.HTML, MediaType.of(mediaType = "text/html")) - assertEquals(MediaType.HTML, MediaType.ofFile(fixtures.fileAt("html.unknown"))) - assertEquals(MediaType.HTML, MediaType.ofFile(fixtures.fileAt("html-doctype-case.unknown"))) - } - - @Test - fun `sniff XHTML`() = runBlocking { - assertEquals(MediaType.XHTML, MediaType.of(fileExtension = "xht")) - assertEquals(MediaType.XHTML, MediaType.of(fileExtension = "xhtml")) - assertEquals(MediaType.XHTML, MediaType.of(mediaType = "application/xhtml+xml")) - assertEquals(MediaType.XHTML, MediaType.ofFile(fixtures.fileAt("xhtml.unknown"))) - } - - @Test - fun `sniff JPEG`() = runBlocking { - assertEquals(MediaType.JPEG, MediaType.of(fileExtension = "jpg")) - assertEquals(MediaType.JPEG, MediaType.of(fileExtension = "jpeg")) - assertEquals(MediaType.JPEG, MediaType.of(fileExtension = "jpe")) - assertEquals(MediaType.JPEG, MediaType.of(fileExtension = "jif")) - assertEquals(MediaType.JPEG, MediaType.of(fileExtension = "jfif")) - assertEquals(MediaType.JPEG, MediaType.of(fileExtension = "jfi")) - assertEquals(MediaType.JPEG, MediaType.of(mediaType = "image/jpeg")) - } - - @Test - fun `sniff JXL`() = runBlocking { - assertEquals(MediaType.JXL, MediaType.of(fileExtension = "jxl")) - assertEquals(MediaType.JXL, MediaType.of(mediaType = "image/jxl")) - } - - @Test - fun `sniff OPDS 1 feed`() = runBlocking { - assertEquals(MediaType.OPDS1, MediaType.of(mediaType = "application/atom+xml;profile=opds-catalog")) - assertEquals(MediaType.OPDS1, MediaType.ofFile(fixtures.fileAt("opds1-feed.unknown"))) - } - - @Test - fun `sniff OPDS 1 entry`() = runBlocking { - assertEquals(MediaType.OPDS1_ENTRY, MediaType.of(mediaType = "application/atom+xml;type=entry;profile=opds-catalog")) - assertEquals(MediaType.OPDS1_ENTRY, MediaType.ofFile(fixtures.fileAt("opds1-entry.unknown"))) - } - - @Test - fun `sniff OPDS 2 feed`() = runBlocking { - assertEquals(MediaType.OPDS2, MediaType.of(mediaType = "application/opds+json")) - assertEquals(MediaType.OPDS2, MediaType.ofFile(fixtures.fileAt("opds2-feed.json"))) - } - - @Test - fun `sniff OPDS 2 publication`() = runBlocking { - assertEquals(MediaType.OPDS2_PUBLICATION, MediaType.of(mediaType = "application/opds-publication+json")) - assertEquals(MediaType.OPDS2_PUBLICATION, MediaType.ofFile(fixtures.fileAt("opds2-publication.json"))) - } - - @Test - fun `sniff OPDS authentication document`() = runBlocking { - assertEquals(MediaType.OPDS_AUTHENTICATION, MediaType.of(mediaType = "application/opds-authentication+json")) - assertEquals(MediaType.OPDS_AUTHENTICATION, MediaType.of(mediaType = "application/vnd.opds.authentication.v1.0+json")) - assertEquals(MediaType.OPDS_AUTHENTICATION, MediaType.ofFile(fixtures.fileAt("opds-authentication.json"))) - } - - @Test - fun `sniff LCP protected audiobook`() = runBlocking { - assertEquals(MediaType.LCP_PROTECTED_AUDIOBOOK, MediaType.of(fileExtension = "lcpa")) - assertEquals(MediaType.LCP_PROTECTED_AUDIOBOOK, MediaType.of(mediaType = "application/audiobook+lcp")) - assertEquals(MediaType.LCP_PROTECTED_AUDIOBOOK, MediaType.ofFile(fixtures.fileAt("audiobook-lcp.unknown"))) - } - - @Test - fun `sniff LCP protected PDF`() = runBlocking { - assertEquals(MediaType.LCP_PROTECTED_PDF, MediaType.of(fileExtension = "lcpdf")) - assertEquals(MediaType.LCP_PROTECTED_PDF, MediaType.of(mediaType = "application/pdf+lcp")) - assertEquals(MediaType.LCP_PROTECTED_PDF, MediaType.ofFile(fixtures.fileAt("pdf-lcp.unknown"))) - } - - @Test - fun `sniff LCP license document`() = runBlocking { - assertEquals(MediaType.LCP_LICENSE_DOCUMENT, MediaType.of(fileExtension = "lcpl")) - assertEquals(MediaType.LCP_LICENSE_DOCUMENT, MediaType.of(mediaType = "application/vnd.readium.lcp.license.v1.0+json")) - assertEquals(MediaType.LCP_LICENSE_DOCUMENT, MediaType.ofFile(fixtures.fileAt("lcpl.unknown"))) - } - - @Test - fun `sniff LPF`() = runBlocking { - assertEquals(MediaType.LPF, MediaType.of(fileExtension = "lpf")) - assertEquals(MediaType.LPF, MediaType.of(mediaType = "application/lpf+zip")) - assertEquals(MediaType.LPF, MediaType.ofFile(fixtures.fileAt("lpf.unknown"))) - assertEquals(MediaType.LPF, MediaType.ofFile(fixtures.fileAt("lpf-index-html.unknown"))) - } - - @Test - fun `sniff PDF`() = runBlocking { - assertEquals(MediaType.PDF, MediaType.of(fileExtension = "pdf")) - assertEquals(MediaType.PDF, MediaType.of(mediaType = "application/pdf")) - assertEquals(MediaType.PDF, MediaType.ofFile(fixtures.fileAt("pdf.unknown"))) - } - - @Test - fun `sniff PNG`() = runBlocking { - assertEquals(MediaType.PNG, MediaType.of(fileExtension = "png")) - assertEquals(MediaType.PNG, MediaType.of(mediaType = "image/png")) - } - - @Test - fun `sniff TIFF`() = runBlocking { - assertEquals(MediaType.TIFF, MediaType.of(fileExtension = "tiff")) - assertEquals(MediaType.TIFF, MediaType.of(fileExtension = "tif")) - assertEquals(MediaType.TIFF, MediaType.of(mediaType = "image/tiff")) - assertEquals(MediaType.TIFF, MediaType.of(mediaType = "image/tiff-fx")) - } - - @Test - fun `sniff WebP`() = runBlocking { - assertEquals(MediaType.WEBP, MediaType.of(fileExtension = "webp")) - assertEquals(MediaType.WEBP, MediaType.of(mediaType = "image/webp")) - } - - @Test - fun `sniff WebPub`() = runBlocking { - assertEquals(MediaType.READIUM_WEBPUB, MediaType.of(fileExtension = "webpub")) - assertEquals(MediaType.READIUM_WEBPUB, MediaType.of(mediaType = "application/webpub+zip")) - assertEquals(MediaType.READIUM_WEBPUB, MediaType.ofFile(fixtures.fileAt("webpub-package.unknown"))) - } - - @Test - fun `sniff WebPub manifest`() = runBlocking { - assertEquals(MediaType.READIUM_WEBPUB_MANIFEST, MediaType.of(mediaType = "application/webpub+json")) - assertEquals(MediaType.READIUM_WEBPUB_MANIFEST, MediaType.ofFile(fixtures.fileAt("webpub.json"))) - } - - @Test - fun `sniff W3C WPUB manifest`() = runBlocking { - assertEquals(MediaType.W3C_WPUB_MANIFEST, MediaType.ofFile(fixtures.fileAt("w3c-wpub.json"))) - } - - @Test - fun `sniff ZAB`() = runBlocking { - assertEquals(MediaType.ZAB, MediaType.of(fileExtension = "zab")) - assertEquals(MediaType.ZAB, MediaType.ofFile(fixtures.fileAt("zab.unknown"))) - } - - @Test - fun `sniff JSON`() = runBlocking { - assertEquals(MediaType.JSON, MediaType.of(mediaType = "application/json")) - assertEquals(MediaType.JSON, MediaType.of(mediaType = "application/json; charset=utf-8")) - assertEquals(MediaType.JSON, MediaType.ofFile(fixtures.fileAt("any.json"))) - } - - @Test - fun `sniff JSON problem details`() = runBlocking { - assertEquals(MediaType.JSON_PROBLEM_DETAILS, MediaType.of(mediaType = "application/problem+json")) - assertEquals(MediaType.JSON_PROBLEM_DETAILS, MediaType.of(mediaType = "application/problem+json; charset=utf-8")) - - // The sniffing of a JSON document should not take precedence over the JSON problem details. - assertEquals(MediaType.JSON_PROBLEM_DETAILS, MediaType.ofBytes({ """{"title": "Message"}""".toByteArray() }, mediaType = "application/problem+json")) - } - - @Test - fun `sniff system media types`() = runBlocking { - shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") - val xlsx = MediaType.parse( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - name = "XLSX", - fileExtension = "xlsx" - )!! - assertEquals(xlsx, MediaType.of(mediaTypes = emptyList(), fileExtensions = listOf("foobar", "xlsx"))) - assertEquals(xlsx, MediaType.of(mediaTypes = listOf("applicaton/foobar", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), fileExtensions = emptyList())) - } - - @Test - fun `sniff system media types from bytes`() = runBlocking { - shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("png", "image/png") - val png = MediaType.parse( - "image/png", - name = "PNG", - fileExtension = "png" - )!! - assertEquals(png, MediaType.ofFile(fixtures.fileAt("png.unknown"))) - } -} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/fetcher/BufferingResourceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt similarity index 83% rename from readium/shared/src/test/java/org/readium/r2/shared/fetcher/BufferingResourceTest.kt rename to readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt index 0b5e4f1c40..171a34bc32 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/fetcher/BufferingResourceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt @@ -1,28 +1,32 @@ -package org.readium.r2.shared.fetcher +package org.readium.r2.shared.util.resource import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.fail import kotlinx.coroutines.runBlocking import org.junit.Test +import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures -import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.util.checkSuccess +import org.readium.r2.shared.util.file.FileResource +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class BufferingResourceTest { @Test fun `get file`() { - assertEquals(sut().file, file) + assertEquals(file, sut().sourceUrl?.toFile()) } @Test - fun `get link`() = runBlocking { - assertEquals(sut().link(), link) + fun `get properties`() = runBlocking { + assertEquals(resource.properties().checkSuccess(), sut().properties().checkSuccess()) } @Test fun `get length`() = runBlocking { - assertEquals(sut().length().getOrNull(), 161291) + assertEquals(161291, sut().length().getOrNull()) } @Test @@ -117,12 +121,11 @@ class BufferingResourceTest { } } - private val link = Link(href = "file") - private val file = Fixtures("fetcher").fileAt("epub.epub") + private val file = Fixtures("util/resource").fileAt("epub.epub") private val data = file.readBytes() - private val resource = FileFetcher.FileResource(link, file) + private val resource = FileResource(file) - private fun sut(bufferSize: Long = 1024): BufferingResource = + private fun sut(bufferSize: Int = 1024): BufferingResource = BufferingResource(resource, bufferSize = bufferSize) private fun testRead(sut: BufferingResource, range: LongRange? = null) { diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt new file mode 100644 index 0000000000..46b0871458 --- /dev/null +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt @@ -0,0 +1,130 @@ +/* + * Module: r2-shared-kotlin + * Developers: Quentin Gliosca + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.shared.util.resource + +import android.webkit.MimeTypeMap +import java.nio.charset.StandardCharsets +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.lengthBlocking +import org.readium.r2.shared.readBlocking +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.checkSuccess +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.file.DirectoryContainer +import org.readium.r2.shared.util.toAbsoluteUrl +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows + +@RunWith(RobolectricTestRunner::class) +class DirectoryContainerTest { + + private val directory = assertNotNull( + DirectoryContainerTest::class.java.getResource("directory")?.toAbsoluteUrl()?.toFile() + ) + + private fun sut(): Container<Resource> = runBlocking { + assertNotNull( + DirectoryContainer(directory).checkSuccess() + ) + } + + @Test + fun `Reading a missing file returns null`() { + assertNull(sut()[Url("unknown")!!]) + } + + @Test + fun `Reading a file at the root works well`() { + val resource = assertNotNull(sut()[Url("text1.txt")!!]) + val result = resource.readBlocking().getOrNull() + assertEquals("text1", result?.toString(StandardCharsets.UTF_8)) + } + + @Test + fun `Reading a file in a subdirectory works well`() { + val resource = assertNotNull(sut()[Url("subdirectory/text2.txt")!!]) + val result = resource.readBlocking().getOrNull() + assertEquals("text2", result?.toString(StandardCharsets.UTF_8)) + } + + @Test + fun `Reading a directory returns null`() { + assertNull(sut()[Url("subdirectory")!!]) + } + + @Test + fun `Reading a file outside the allowed directory returns null`() { + assertNull(sut()[Url("../epub.epub")!!]) + } + + @Test + fun `Reading a range works well`() { + val resource = assertNotNull(sut()[Url("text1.txt")!!]) + val result = assertNotNull(resource.readBlocking(0..2L).getOrNull()) + assertEquals("tex", result.toString(StandardCharsets.UTF_8)) + } + + @Test + fun `Reading two ranges with the same resource work well`() { + val resource = assertNotNull(sut()[Url("text1.txt")!!]) + val result1 = resource.readBlocking(0..1L).getOrNull() + assertEquals("te", result1?.toString(StandardCharsets.UTF_8)) + val result2 = resource.readBlocking(1..3L).getOrNull() + assertEquals("ext", result2?.toString(StandardCharsets.UTF_8)) + } + + @Test + fun `Out of range indexes are clamped to the available length`() { + val resource = assertNotNull(sut()[Url("text1.txt")!!]) + val result = resource.readBlocking(-5..60L).getOrNull() + assertEquals("text1", result?.toString(StandardCharsets.UTF_8)) + assertEquals(5, result?.size) + } + + @Test + @Suppress("EmptyRange") + fun `Decreasing ranges are understood as empty ones`() { + val resource = assertNotNull(sut()[Url("text1.txt")!!]) + val result = resource.readBlocking(60..20L).getOrNull() + assertEquals("", result?.toString(StandardCharsets.UTF_8)) + assertEquals(0, result?.size) + } + + @Test + fun `Computing length works well`() { + val resource = assertNotNull(sut().get(Url("text1.txt")!!)) + val result = resource.lengthBlocking().getOrNull() + assertEquals(5L, result) + } + + @Test + fun `Computing entries works well`() { + runBlocking { + // FIXME: Test media types + Shadows.shadowOf(MimeTypeMap.getSingleton()).apply { + addExtensionMimeTypeMapping("txt", "text/plain") + addExtensionMimeTypeMapping("mp3", "audio/mpeg") + } + + val entries = sut().entries + assertThat(entries).contains( + Url("subdirectory/hello.mp3")!!, + Url("subdirectory/text2.txt")!!, + Url("text1.txt")!! + ) + } + } +} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/archive/PropertiesTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt similarity index 84% rename from readium/shared/src/test/java/org/readium/r2/shared/publication/archive/PropertiesTest.kt rename to readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt index 5c112baa5e..befffd96b7 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/archive/PropertiesTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt @@ -1,4 +1,4 @@ -package org.readium.r2.shared.publication.archive +package org.readium.r2.shared.util.resource import org.json.JSONObject import org.junit.Assert.assertEquals @@ -6,7 +6,8 @@ import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.assertJSONEquals -import org.readium.r2.shared.publication.Properties +import org.readium.r2.shared.util.archive.ArchiveProperties +import org.readium.r2.shared.util.archive.archive import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -14,14 +15,14 @@ class PropertiesTest { @Test fun `get no archive`() { - assertNull(Properties().archive) + assertNull(Resource.Properties().archive) } @Test fun `get full archive`() { assertEquals( ArchiveProperties(entryLength = 8273, isEntryCompressed = true), - Properties( + Resource.Properties( mapOf( "archive" to mapOf( "entryLength" to 8273, @@ -35,7 +36,7 @@ class PropertiesTest { @Test fun `get invalid archive`() { assertNull( - Properties( + Resource.Properties( mapOf( "archive" to mapOf( "foo" to "bar" @@ -48,7 +49,7 @@ class PropertiesTest { @Test fun `get incomplete archive`() { assertNull( - Properties( + Resource.Properties( mapOf( "archive" to mapOf( "isEntryCompressed" to true @@ -58,7 +59,7 @@ class PropertiesTest { ) assertNull( - Properties( + Resource.Properties( mapOf( "archive" to mapOf( "entryLength" to 8273 diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ReadableInputStreamAdapterTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ReadableInputStreamAdapterTest.kt new file mode 100644 index 0000000000..a0387e31b8 --- /dev/null +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ReadableInputStreamAdapterTest.kt @@ -0,0 +1,30 @@ +package org.readium.r2.shared.util.resource + +import java.io.ByteArrayOutputStream +import java.io.File +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.util.data.asInputStream +import org.readium.r2.shared.util.file.FileResource +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ReadableInputStreamAdapterTest { + + private val file = File( + assertNotNull(ReadableInputStreamAdapterTest::class.java.getResource("epub.epub")?.path) + ) + private val fileContent: ByteArray = file.readBytes() + private val bufferSize = 16384 // This is the size used by NanoHTTPd for chunked responses + + @Test + fun `stream can be read by chunks`() { + val resource = FileResource(file) + val resourceStream = resource.asInputStream() + val outputStream = ByteArrayOutputStream(fileContent.size) + resourceStream.copyTo(outputStream, bufferSize = bufferSize) + assertTrue(fileContent.contentEquals(outputStream.toByteArray())) + } +} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt new file mode 100644 index 0000000000..6f92ea2b71 --- /dev/null +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -0,0 +1,152 @@ +/* + * Module: r2-shared-kotlin + * Developers: Quentin Gliosca + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.shared.util.resource + +import java.io.File +import java.nio.charset.StandardCharsets +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.util.FileExtension +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.checkSuccess +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.file.DirectoryContainer +import org.readium.r2.shared.util.format.EpubSpecification +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatSpecification +import org.readium.r2.shared.util.format.ZipSpecification +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.use +import org.readium.r2.shared.util.zip.FileZipArchiveProvider +import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider +import org.robolectric.ParameterizedRobolectricTestRunner + +@RunWith(ParameterizedRobolectricTestRunner::class) +class ZipContainerTest(val sut: suspend () -> Container<Resource>) { + + companion object { + + @ParameterizedRobolectricTestRunner.Parameters + @JvmStatic + fun archives(): List<suspend () -> Container<Resource>> { + val epubZip = ZipContainerTest::class.java.getResource("epub.epub") + assertNotNull(epubZip) + val format = Format( + specification = FormatSpecification(ZipSpecification, EpubSpecification), + mediaType = MediaType.EPUB, + fileExtension = FileExtension("epub") + ) + + val zipArchive = suspend { + assertNotNull( + FileZipArchiveProvider() + .open(format, File(epubZip.path)) + .getOrNull() + ) + } + + val apacheZipArchive = suspend { + StreamingZipArchiveProvider() + .openFile(File(epubZip.path)) + } + + val epubExploded = ZipContainerTest::class.java.getResource("epub") + assertNotNull(epubExploded) + val explodedArchive = suspend { + assertNotNull( + DirectoryContainer(File(epubExploded.path)).checkSuccess() + ) + } + assertNotNull(explodedArchive) + + return listOf(zipArchive, apacheZipArchive, explodedArchive) + } + } + + @Test + fun `Entry list is correct`(): Unit = runBlocking { + sut().use { container -> + assertThat(container.entries) + .contains( + Url("mimetype")!!, + Url("EPUB/cover.xhtml")!!, + Url("EPUB/css/epub.css")!!, + Url("EPUB/css/nav.css")!!, + Url("EPUB/images/cover.png")!!, + Url("EPUB/nav.xhtml")!!, + Url("EPUB/package.opf")!!, + Url("EPUB/s04.xhtml")!!, + Url("EPUB/toc.ncx")!!, + Url("META-INF/container.xml")!! + ) + } + } + + @Test + fun `Attempting to read a missing entry throws`(): Unit = runBlocking { + sut().use { container -> + assertNull(container[Url("unknown")!!]) + } + } + + @Test + fun `Fully reading an entry works well`(): Unit = runBlocking { + sut().use { container -> + val resource = assertNotNull(container[Url("mimetype")!!]) + val bytes = resource.read().checkSuccess() + assertEquals("application/epub+zip", bytes.toString(StandardCharsets.UTF_8)) + } + } + + @Test + fun `Reading a range of an entry works well`(): Unit = runBlocking { + sut().use { container -> + val resource = assertNotNull(container[Url("mimetype")!!]) + val bytes = resource.read(0..10L).checkSuccess() + assertEquals("application", bytes.toString(StandardCharsets.UTF_8)) + assertEquals(11, bytes.size) + } + } + + @Test + fun `Out of range indexes are clamped to the available length`(): Unit = runBlocking { + sut().use { container -> + val resource = assertNotNull(container[Url("mimetype")!!]) + val bytes = resource.read(-5..60L).checkSuccess() + assertEquals("application/epub+zip", bytes.toString(StandardCharsets.UTF_8)) + assertEquals(20, bytes.size) + } + } + + @Suppress("EmptyRange") + @Test + fun `Decreasing ranges are understood as empty ones`(): Unit = runBlocking { + sut().use { container -> + val resource = assertNotNull(container[Url("mimetype")!!]) + val bytes = resource.read(60..20L).checkSuccess() + assertEquals("", bytes.toString(StandardCharsets.UTF_8)) + assertEquals(0, bytes.size) + } + } + + @Test + fun `Computing size works well`(): Unit = runBlocking { + sut().use { container -> + val resource = assertNotNull(container[Url("mimetype")!!]) + val size = resource.length().checkSuccess() + assertEquals(20L, size) + } + } +} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/tokenizer/IcuTextTokenizerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/tokenizer/IcuTextTokenizerTest.kt index 19e695ec04..3739de9c72 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/tokenizer/IcuTextTokenizerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/tokenizer/IcuTextTokenizerTest.kt @@ -23,7 +23,10 @@ class IcuTextTokenizerTest { @Test fun tokenizeEmptyContent() = runBlocking { - val tokenizer = IcuTextTokenizer(language = Language(Locale.ENGLISH), unit = TextUnit.Sentence) + val tokenizer = IcuTextTokenizer( + language = Language(Locale.ENGLISH), + unit = TextUnit.Sentence + ) assertEquals(emptyList(), tokenizer.tokenize("")) } @@ -40,7 +43,10 @@ class IcuTextTokenizerTest { @Test fun tokenizeBySentences() = runBlocking { - val tokenizer = IcuTextTokenizer(language = Language(Locale.ENGLISH), unit = TextUnit.Sentence) + val tokenizer = IcuTextTokenizer( + language = Language(Locale.ENGLISH), + unit = TextUnit.Sentence + ) val source = """ Alice said, looking above: "and what is the use of a book?". So she was considering (as well as she could), whether making a daisy-chain would be worth the trouble In the end, she went ahead. @@ -49,7 +55,7 @@ class IcuTextTokenizerTest { listOf( "Alice said, looking above: \"and what is the use of a book?\".", "So she was considering (as well as she could), whether making a daisy-chain would be worth the trouble", - "In the end, she went ahead.", + "In the end, she went ahead." ), tokenizer.tokenize(source) .map { source.substring(it) } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/tokenizer/NaiveTextTokenizerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/tokenizer/NaiveTextTokenizerTest.kt index cf8f17d01e..e6dd15d00e 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/tokenizer/NaiveTextTokenizerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/tokenizer/NaiveTextTokenizerTest.kt @@ -46,7 +46,7 @@ class NaiveTextTokenizerTest { assertContentEquals( listOf( "Alice said, looking above: \"and what is the use of a book?\".", - "So she was considering (as well as she could), whether making a daisy-chain would be worth the trouble\nIn the end, she went ahead.", + "So she was considering (as well as she could), whether making a daisy-chain would be worth the trouble\nIn the end, she went ahead." ), tokenizer.tokenize(source) .map { source.substring(it) } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/parser/xml/XmlParserTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/xml/XmlParserTest.kt similarity index 97% rename from readium/shared/src/test/java/org/readium/r2/shared/parser/xml/XmlParserTest.kt rename to readium/shared/src/test/java/org/readium/r2/shared/util/xml/XmlParserTest.kt index 54e5a0c9c1..5d336813ca 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/parser/xml/XmlParserTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/xml/XmlParserTest.kt @@ -7,7 +7,7 @@ * LICENSE file present in the project repository where this source code is maintained. */ -package org.readium.r2.shared.parser.xml +package org.readium.r2.shared.util.xml import java.io.ByteArrayInputStream import javax.xml.XMLConstants @@ -67,7 +67,13 @@ class XmlParserTest { "meta", "", "fr", - mapOf("" to mapOf("refines" to "#title", "property" to "alternate-script", "xml:lang" to "fr")), + mapOf( + "" to mapOf( + "refines" to "#title", + "property" to "alternate-script", + "xml:lang" to "fr" + ) + ), listOf(TextNode("Moby Dick")) ) val expectedCreator = ElementNode( diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/fetcher/text.txt b/readium/shared/src/test/resources/org/readium/r2/shared/fetcher/text.txt deleted file mode 100644 index f3a34851d4..0000000000 --- a/readium/shared/src/test/resources/org/readium/r2/shared/fetcher/text.txt +++ /dev/null @@ -1 +0,0 @@ -text \ No newline at end of file diff --git a/readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/encryption/encryption-lcp-prefixes.xml b/readium/shared/src/test/resources/org/readium/r2/shared/publication/epub/encryption/encryption-lcp-prefixes.xml similarity index 100% rename from readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/encryption/encryption-lcp-prefixes.xml rename to readium/shared/src/test/resources/org/readium/r2/shared/publication/epub/encryption/encryption-lcp-prefixes.xml diff --git a/readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/encryption/encryption-lcp-xmlns.xml b/readium/shared/src/test/resources/org/readium/r2/shared/publication/epub/encryption/encryption-lcp-xmlns.xml similarity index 100% rename from readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/encryption/encryption-lcp-xmlns.xml rename to readium/shared/src/test/resources/org/readium/r2/shared/publication/epub/encryption/encryption-lcp-xmlns.xml diff --git a/readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/encryption/encryption-unknown-method.xml b/readium/shared/src/test/resources/org/readium/r2/shared/publication/epub/encryption/encryption-unknown-method.xml similarity index 100% rename from readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/encryption/encryption-unknown-method.xml rename to readium/shared/src/test/resources/org/readium/r2/shared/publication/epub/encryption/encryption-unknown-method.xml diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub.epub b/readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub.epub deleted file mode 100644 index 9f037a1b6e..0000000000 Binary files a/readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub.epub and /dev/null differ diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/any.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/any.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/any.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/any.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/audiobook-lcp.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook-lcp.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/audiobook-lcp.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook-lcp.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/audiobook-package.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook-package.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/audiobook-package.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook-package.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/audiobook-wrongtype.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook-wrongtype.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/audiobook-wrongtype.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook-wrongtype.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/audiobook.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/audiobook.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/cbz.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/cbz.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/cbz.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/cbz.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/divina-package.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/divina-package.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/divina-package.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/divina-package.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/divina.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/divina.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/divina.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/divina.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/epub.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/epub.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/epub.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/epub.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/html-doctype-case.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/html-doctype-case.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/html-doctype-case.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/html-doctype-case.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/html.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/html.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/html.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/html.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/lcpl.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/lcpl.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/lcpl.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/lcpl.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/lpf-index-html.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/lpf-index-html.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/lpf-index-html.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/lpf-index-html.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/lpf.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/lpf.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/lpf.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/lpf.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/opds-authentication.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds-authentication.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/opds-authentication.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds-authentication.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/opds1-entry.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds1-entry.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/opds1-entry.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds1-entry.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/opds1-feed.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds1-feed.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/opds1-feed.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds1-feed.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/opds2-feed.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds2-feed.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/opds2-feed.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds2-feed.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/opds2-publication.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds2-publication.json similarity index 94% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/opds2-publication.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds2-publication.json index bb0b7b0191..e45578910d 100644 --- a/readium/shared/src/test/resources/org/readium/r2/shared/format/opds2-publication.json +++ b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds2-publication.json @@ -8,7 +8,7 @@ "modified": "2015-09-29T17:00:00Z" }, "links": [ - {"rel": "self", "href": "http://example.org/manifest.json", "type": "application/webpub+json"}, + {"rel": "self", "href": "http://example.org/manifest.json", "type": "application/opds-publication+json"}, { "href": "/buy", "rel": "http://opds-spec.org/acquisition/buy", diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/pdf-lcp.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/pdf-lcp.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/pdf-lcp.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/pdf-lcp.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/pdf.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/pdf.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/pdf.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/pdf.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/png.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/png.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/png.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/png.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/unknown.zip b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/unknown.zip similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/unknown.zip rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/unknown.zip diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/w3c-wpub.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/w3c-wpub.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/w3c-wpub.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/w3c-wpub.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/webpub-lcp.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/webpub-lcp.unknown new file mode 100644 index 0000000000..5bd9674499 Binary files /dev/null and b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/webpub-lcp.unknown differ diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/webpub-package.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/webpub-package.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/webpub-package.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/webpub-package.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/webpub.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/webpub.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/webpub.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/webpub.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/xhtml.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/xhtml.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/xhtml.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/xhtml.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/format/zab.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/zab.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/format/zab.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/zab.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/fetcher/directory/subdirectory/hello.mp3 b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/directory/subdirectory/hello.mp3 similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/fetcher/directory/subdirectory/hello.mp3 rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/directory/subdirectory/hello.mp3 diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/fetcher/directory/subdirectory/text2.txt b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/directory/subdirectory/text2.txt similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/fetcher/directory/subdirectory/text2.txt rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/directory/subdirectory/text2.txt diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/fetcher/directory/text1.txt b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/directory/text1.txt similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/fetcher/directory/text1.txt rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/directory/text1.txt diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/fetcher/epub.epub b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub.epub similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/fetcher/epub.epub rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub.epub diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/cover.xhtml b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/cover.xhtml similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/cover.xhtml rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/cover.xhtml diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/css/epub.css b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/css/epub.css similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/css/epub.css rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/css/epub.css diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/css/nav.css b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/css/nav.css similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/css/nav.css rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/css/nav.css diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/images/cover.png b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/images/cover.png similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/images/cover.png rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/images/cover.png diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/nav.xhtml b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/nav.xhtml similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/nav.xhtml rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/nav.xhtml diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/package.opf b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/package.opf similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/package.opf rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/package.opf diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/s04.xhtml b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/s04.xhtml similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/s04.xhtml rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/s04.xhtml diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/toc.ncx b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/toc.ncx similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/EPUB/toc.ncx rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/EPUB/toc.ncx diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/META-INF/container.xml b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/META-INF/container.xml similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/META-INF/container.xml rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/META-INF/container.xml diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/mimetype b/readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/mimetype similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/archive/epub/mimetype rename to readium/shared/src/test/resources/org/readium/r2/shared/util/resource/epub/mimetype diff --git a/readium/streamer/build.gradle.kts b/readium/streamer/build.gradle.kts index 0d30a46089..bec32e78df 100644 --- a/readium/streamer/build.gradle.kts +++ b/readium/streamer/build.gradle.kts @@ -11,15 +11,17 @@ plugins { } android { - compileSdk = 33 + resourcePrefix = "readium_" + + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } testOptions { unitTests.isIncludeAndroidResources = true @@ -40,6 +42,10 @@ android { namespace = "org.readium.r2.streamer" } +kotlin { + explicitApi() +} + rootProject.ext["publish.artifactId"] = "readium-streamer" apply(from = "$rootDir/scripts/publish-module.gradle") @@ -64,6 +70,7 @@ dependencies { // Tests testImplementation(libs.junit) + testImplementation(libs.kotlin.junit) androidTestImplementation(libs.androidx.ext.junit) androidTestImplementation(libs.androidx.expresso.core) diff --git a/readium/streamer/src/main/AndroidManifest.xml b/readium/streamer/src/main/AndroidManifest.xml index c2b1d5cfd6..eb475b4918 100644 --- a/readium/streamer/src/main/AndroidManifest.xml +++ b/readium/streamer/src/main/AndroidManifest.xml @@ -1,10 +1,7 @@ <!-- - ~ Module: r2-streamer-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <manifest /> diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationOpener.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationOpener.kt new file mode 100644 index 0000000000..dd3e4b0c26 --- /dev/null +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationOpener.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.streamer + +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.protection.ContentProtection +import org.readium.r2.shared.publication.protection.FallbackContentProtection +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.logging.WarningLogger +import org.readium.r2.streamer.parser.PublicationParser + +/** + * Opens a [Publication] from an [Asset]. + * + * @param publicationParser Parses the content of a publication [Asset]. + * @param contentProtections Opens DRM-protected publications. + * @param onCreatePublication Called on every parsed [Publication.Builder]. It can be used to modify + * the manifest, the root container or the list of service factories of a [Publication]. + */ +public class PublicationOpener( + private val publicationParser: PublicationParser, + contentProtections: List<ContentProtection> = emptyList(), + private val onCreatePublication: Publication.Builder.() -> Unit = {} +) { + public sealed class OpenError( + override val message: String, + override val cause: Error? + ) : Error { + + public class Reading( + override val cause: ReadError + ) : OpenError("An error occurred while trying to read asset.", cause) + + public class FormatNotSupported( + override val cause: Error? = null + ) : OpenError("Asset is not supported.", cause) + } + + private val contentProtections: List<ContentProtection> = + contentProtections + FallbackContentProtection() + + /** + * Opens a [Publication] from the given asset. + * + * If you are opening the publication to render it in a Navigator, you must set [allowUserInteraction] + * to true to prompt the user for its credentials when the publication is protected. However, + * set it to false if you just want to import the [Publication] without reading its content, to + * avoid prompting the user. + * + * The [warnings] logger can be used to observe non-fatal parsing warnings, caused by + * publication authoring mistakes. This can be useful to warn users of potential rendering + * issues. + * + * @param asset Digital medium (e.g. a file) used to access the publication. + * @param credentials Credentials that Content Protections can use to attempt to unlock a + * publication, for example a password. + * @param allowUserInteraction Indicates whether the user can be prompted, for example for its + * credentials. + * @param onCreatePublication Transformation which will be applied on the Publication Builder. + * It can be used to modify the manifest, the root container or the list of service + * factories of the [Publication]. + * @param warnings Logger used to broadcast non-fatal parsing warnings. + * @return A [Publication] or an [OpenError] in case of failure. + */ + public suspend fun open( + asset: Asset, + credentials: String? = null, + allowUserInteraction: Boolean, + onCreatePublication: Publication.Builder.() -> Unit = {}, + warnings: WarningLogger? = null + ): Try<Publication, OpenError> { + var protectionOnCreatePublication: Publication.Builder.() -> Unit = {} + + var transformedAsset: Asset = asset + + for (protection in contentProtections) { + val openResult = protection + .open(asset, credentials, allowUserInteraction) + .getOrElse { + when (it) { + is ContentProtection.OpenError.Reading -> + return Try.failure(OpenError.Reading(it.cause)) + is ContentProtection.OpenError.AssetNotSupported -> + null + } + } + + if (openResult != null) { + transformedAsset = openResult.asset + protectionOnCreatePublication = openResult.onCreatePublication + break + } + } + + val builder = publicationParser.parse(transformedAsset, warnings) + .getOrElse { return Try.failure(wrapParserException(it)) } + + builder.apply { + protectionOnCreatePublication() + this.onCreatePublication() + onCreatePublication() + } + + val publication = builder.build() + return Try.success(publication) + } + + private fun wrapParserException(e: PublicationParser.ParseError): OpenError = + when (e) { + is PublicationParser.ParseError.FormatNotSupported -> + OpenError.FormatNotSupported(DebugError("Cannot find a parser for this asset.")) + is PublicationParser.ParseError.Reading -> + OpenError.Reading(e.cause) + } +} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationParser.kt deleted file mode 100644 index 3a82ea8fd0..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationParser.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.streamer - -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.asset.PublicationAsset -import org.readium.r2.shared.util.logging.WarningLogger - -/** - * Parses a Publication from an asset. - */ -interface PublicationParser { - - /** - * Constructs a [Publication.Builder] to build a [Publication] from a publication asset. - * - * @param asset Digital medium (e.g. a file) used to access the publication. - * @param fetcher Initial leaf fetcher which should be used to read the publication's resources. - * This can be used to: - * - support content protection technologies - * - parse exploded archives or in archiving formats unknown to the parser, e.g. RAR - * If the asset is not an archive, it will be reachable at the HREF /<asset.name>, - * e.g. with a PDF. - * @param warnings Used to report non-fatal parsing warnings, such as publication authoring - * mistakes. This is useful to warn users of potential rendering issues or help authors - * debug their publications. - */ - suspend fun parse(asset: PublicationAsset, fetcher: Fetcher, warnings: WarningLogger? = null): Publication.Builder? -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/Streamer.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/Streamer.kt index 3c135b2a1d..5232921c1c 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/Streamer.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/Streamer.kt @@ -1,184 +1,14 @@ /* - * Module: r2-streamer-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. */ package org.readium.r2.streamer -import android.content.Context -import org.readium.r2.shared.PdfSupport -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.publication.ContentProtection -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.asset.PublicationAsset -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.archive.ArchiveFactory -import org.readium.r2.shared.util.archive.DefaultArchiveFactory -import org.readium.r2.shared.util.http.DefaultHttpClient -import org.readium.r2.shared.util.logging.WarningLogger -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.pdf.PdfDocumentFactory -import org.readium.r2.streamer.parser.FallbackContentProtection -import org.readium.r2.streamer.parser.audio.AudioParser -import org.readium.r2.streamer.parser.epub.EpubParser -import org.readium.r2.streamer.parser.epub.setLayoutStyle -import org.readium.r2.streamer.parser.image.ImageParser -import org.readium.r2.streamer.parser.pdf.PdfParser -import org.readium.r2.streamer.parser.readium.ReadiumWebPubParser - -internal typealias PublicationTry<SuccessT> = Try<SuccessT, Publication.OpeningException> - -/** - * Opens a Publication using a list of parsers. - * - * The [Streamer] is configured to use Readium's default parsers, which you can bypass using - * [ignoreDefaultParsers]. However, you can provide additional [parsers] which will take precedence - * over the default ones. This can also be used to provide an alternative configuration of a - * default parser. - * - * @param context Application context. - * @param parsers Parsers used to open a publication, in addition to the default parsers. - * @param ignoreDefaultParsers When true, only parsers provided in parsers will be used. - * @param archiveFactory Opens an archive (e.g. ZIP, RAR), optionally protected by credentials. - * @param pdfFactory Parses a PDF document, optionally protected by password. - * @param httpClient Service performing HTTP requests. - * @param onCreatePublication Called on every parsed [Publication.Builder]. It can be used to modify - * the [Manifest], the root [Fetcher] or the list of service factories of a [Publication]. - */ -@OptIn(PdfSupport::class) -class Streamer constructor( - context: Context, - parsers: List<PublicationParser> = emptyList(), - ignoreDefaultParsers: Boolean = false, - contentProtections: List<ContentProtection> = emptyList(), - private val archiveFactory: ArchiveFactory = DefaultArchiveFactory(), - private val pdfFactory: PdfDocumentFactory<*>? = null, - private val httpClient: DefaultHttpClient = DefaultHttpClient(), - private val onCreatePublication: Publication.Builder.() -> Unit = {} -) { - private val context = context.applicationContext - - private val contentProtections: List<ContentProtection> = - contentProtections + listOf(FallbackContentProtection()) - - /** - * Parses a [Publication] from the given asset. - * - * If you are opening the publication to render it in a Navigator, you must set [allowUserInteraction] - * to true to prompt the user for its credentials when the publication is protected. However, - * set it to false if you just want to import the [Publication] without reading its content, to - * avoid prompting the user. - * - * When using Content Protections, you can use [sender] to provide a free object which can be - * used to give some context. For example, it could be the source Activity or Fragment which - * would be used to present a credentials dialog. - * - * The [warnings] logger can be used to observe non-fatal parsing warnings, caused by - * publication authoring mistakes. This can be useful to warn users of potential rendering - * issues. - * - * @param asset Digital medium (e.g. a file) used to access the publication. - * @param credentials Credentials that Content Protections can use to attempt to unlock a - * publication, for example a password. - * @param allowUserInteraction Indicates whether the user can be prompted, for example for its - * credentials. - * @param sender Free object that can be used by reading apps to give some UX context when - * presenting dialogs. - * @param onCreatePublication Transformation which will be applied on the Publication Builder. - * It can be used to modify the [Manifest], the root [Fetcher] or the list of service - * factories of the [Publication]. - * @param warnings Logger used to broadcast non-fatal parsing warnings. - * @return Null if the asset was not recognized by any parser, or a - * [Publication.OpeningException] in case of failure. - */ - suspend fun open( - asset: PublicationAsset, - credentials: String? = null, - allowUserInteraction: Boolean, - sender: Any? = null, - onCreatePublication: Publication.Builder.() -> Unit = {}, - warnings: WarningLogger? = null - ): PublicationTry<Publication> = try { - - @Suppress("NAME_SHADOWING") - var asset = asset - var fetcher = asset.createFetcher(PublicationAsset.Dependencies(archiveFactory = archiveFactory), credentials = credentials) - .getOrThrow() - - val protectedAsset = contentProtections - .lazyMapFirstNotNullOrNull { - it.open(asset, fetcher, credentials, allowUserInteraction, sender) - } - ?.getOrThrow() - - if (protectedAsset != null) { - asset = protectedAsset.asset - fetcher = protectedAsset.fetcher - } - - val builder = parsers - .lazyMapFirstNotNullOrNull { - try { - it.parse(asset, fetcher, warnings) - } catch (e: Exception) { - throw Publication.OpeningException.ParsingFailed(e) - } - } ?: throw Publication.OpeningException.UnsupportedFormat(Exception("Cannot find a parser for this asset")) - - // Transform from the Content Protection. - protectedAsset?.let { builder.apply(it.onCreatePublication) } - // Transform provided by the reading app during the construction of the Streamer. - builder.apply(this.onCreatePublication) - // Transform provided by the reading app in `Streamer.open()`. - builder.apply(onCreatePublication) - - val publication = builder.build() - .apply { addLegacyProperties(asset.mediaType()) } - - Try.success(publication) - } catch (e: Publication.OpeningException) { - Try.failure(e) - } - - private val defaultParsers: List<PublicationParser> by lazy { - listOfNotNull( - EpubParser(), - pdfFactory?.let { PdfParser(context, it) }, - ReadiumWebPubParser(context, pdfFactory, httpClient), - ImageParser(), - AudioParser() - ) - } - - private val parsers: List<PublicationParser> = parsers + - if (!ignoreDefaultParsers) defaultParsers else emptyList() - - @Suppress("UNCHECKED_CAST") - private suspend fun <T, R> List<T>.lazyMapFirstNotNullOrNull(transform: suspend (T) -> R): R? { - for (it in this) { - return transform(it) ?: continue - } - return null - } - - private fun Publication.addLegacyProperties(mediaType: MediaType?) { - @Suppress("DEPRECATION") - type = mediaType.toPublicationType() - - if (mediaType == MediaType.EPUB) - setLayoutStyle() - } -} - -internal fun MediaType?.toPublicationType(): Publication.TYPE = - when (this) { - MediaType.READIUM_AUDIOBOOK, MediaType.READIUM_AUDIOBOOK_MANIFEST, MediaType.LCP_PROTECTED_AUDIOBOOK -> Publication.TYPE.AUDIO - MediaType.DIVINA, MediaType.DIVINA_MANIFEST -> Publication.TYPE.DiViNa - MediaType.CBZ -> Publication.TYPE.CBZ - MediaType.EPUB -> Publication.TYPE.EPUB - else -> Publication.TYPE.WEBPUB - } +@Deprecated( + "Use a `PublicationOpener` instead. See the migration guide.", + ReplaceWith("PublicationOpener"), + level = DeprecationLevel.ERROR +) +public class Streamer diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/container/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/container/Container.kt index 2402644281..81da13752e 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/container/Container.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/container/Container.kt @@ -10,8 +10,7 @@ package org.readium.r2.streamer.container import java.io.InputStream -import org.readium.r2.shared.RootFile -import org.readium.r2.shared.drm.DRM +import java.lang.Error /** * Container of a publication @@ -27,23 +26,64 @@ import org.readium.r2.shared.drm.DRM * * @func dataInputStream : return the InputStream of content */ -interface Container { - var rootFile: RootFile - var drm: DRM? - - @Deprecated("Use [publication.get()] to access publication content.") - fun data(relativePath: String): ByteArray - @Deprecated("Use [publication.get()] to access publication content.") - fun dataLength(relativePath: String): Long - @Deprecated("Use [publication.get()] to access publication content.") - fun dataInputStream(relativePath: String): InputStream +@Deprecated( + "Use [publication.get()] to access publication content.", + level = DeprecationLevel.ERROR +) +public interface Container { + @Deprecated( + "Use [publication.get()] to access publication content.", + level = DeprecationLevel.ERROR + ) + public fun data(relativePath: String): ByteArray + + @Deprecated( + "Use [publication.get()] to access publication content.", + level = DeprecationLevel.ERROR + ) + public fun dataLength(relativePath: String): Long + + @Deprecated( + "Use [publication.get()] to access publication content.", + level = DeprecationLevel.ERROR + ) + public fun dataInputStream(relativePath: String): InputStream } -sealed class ContainerError : Exception() { - object streamInitFailed : ContainerError() - object fileNotFound : ContainerError() - object fileError : ContainerError() - data class missingFile(val path: String) : ContainerError() - data class xmlParse(val underlyingError: Error) : ContainerError() - data class missingLink(val title: String?) : ContainerError() +public sealed class ContainerError : Exception() { + @Deprecated( + "Use [publication.get()] to access publication content.", + level = DeprecationLevel.ERROR + ) + public object streamInitFailed : ContainerError() + + @Deprecated( + "Use [publication.get()] to access publication content.", + level = DeprecationLevel.ERROR + ) + public object fileNotFound : ContainerError() + + @Deprecated( + "Use [publication.get()] to access publication content.", + level = DeprecationLevel.ERROR + ) + public object fileError : ContainerError() + + @Deprecated( + "Use [publication.get()] to access publication content.", + level = DeprecationLevel.ERROR + ) + public data class missingFile(public val path: String) : ContainerError() + + @Deprecated( + "Use [publication.get()] to access publication content.", + level = DeprecationLevel.ERROR + ) + public data class xmlParse(public val underlyingError: Error) : ContainerError() + + @Deprecated( + "Use [publication.get()] to access publication content.", + level = DeprecationLevel.ERROR + ) + public data class missingLink(public val title: String?) : ContainerError() } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/container/PublicationContainer.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/container/PublicationContainer.kt deleted file mode 100644 index d89b51ef5e..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/container/PublicationContainer.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Mickaël Menu - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.container - -import java.io.InputStream -import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.RootFile -import org.readium.r2.shared.drm.DRM -import org.readium.r2.shared.extensions.tryOr -import org.readium.r2.shared.fetcher.ResourceInputStream -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.mediatype.MediaType - -/** - * Temporary solution to migrate to [Publication.get] while ensuring backward compatibility with - * [Container]. - */ -internal class PublicationContainer( - private val publication: Publication, - path: String, - mediaType: MediaType, - override var drm: DRM? = null -) : Container { - - override var rootFile = RootFile(rootPath = path, mimetype = mediaType.toString()) - - @Deprecated("Use [publication.get()] to access publication content.") - override fun data(relativePath: String): ByteArray = runBlocking { - publication.get(relativePath).read().getOrThrow() - } - - @Deprecated("Use [publication.get()] to access publication content.") - override fun dataLength(relativePath: String): Long = runBlocking { - tryOr(0) { - publication.get(relativePath).length().getOrThrow() - } - } - - @Deprecated("Use [publication.get()] to access publication content.") - override fun dataInputStream(relativePath: String): InputStream = - ResourceInputStream(publication.get(relativePath)).buffered() - - private fun Publication.get(href: String) = get(Link(href)) -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt new file mode 100644 index 0000000000..bf079a4814 --- /dev/null +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt @@ -0,0 +1,84 @@ +/* + * Module: r2-streamer-kotlin + * Developers: Mickaël Menu, Quentin Gliosca + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.streamer.extensions + +import java.io.File +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.appendToFilename +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.ResourceAsset +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.SingleResourceContainer +import org.readium.r2.shared.util.use + +internal fun Iterable<Url>.guessTitle(): String? { + val firstEntry = firstOrNull() ?: return null + val commonFirstComponent = pathCommonFirstComponent() ?: return null + + if (commonFirstComponent.name == firstEntry.path) { + return null + } + + return commonFirstComponent.name +} + +/** Returns a [File] to the directory containing all paths, if there is such a directory. */ +internal fun Iterable<Url>.pathCommonFirstComponent(): File? = + mapNotNull { it.path?.substringBefore("/") } + .distinct() + .takeIf { it.size == 1 } + ?.firstOrNull() + ?.let { File(it) } + +internal fun ResourceAsset.toContainer(): Container<Resource> { + // Historically, the reading order of a standalone file contained a single link with the + // HREF "/$assetName". This was fragile if the asset named changed, or was different on + // other devices. To avoid this, we now use a single link with the HREF + // "publication.extension". + val extension = format + .fileExtension + + return SingleResourceContainer( + Url(extension.appendToFilename("publication"))!!, + resource + ) +} + +internal suspend fun AssetRetriever.sniffContainerEntries( + container: Container<Resource>, + filter: (Url) -> Boolean +): Try<Map<Url, Format>, ReadError> = + container + .filter(filter) + .fold(Try.success(emptyMap())) { acc: Try<Map<Url, Format>, ReadError>, url -> + when (acc) { + is Try.Failure -> + acc + + is Try.Success -> + container[url]!!.use { resource -> + sniffFormat(resource).fold( + onSuccess = { + Try.success(acc.value + (url to it)) + }, + onFailure = { + when (it) { + is AssetRetriever.RetrieveError.FormatNotSupported -> acc + is AssetRetriever.RetrieveError.Reading -> Try.failure(it.cause) + } + } + ) + } + } + } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Fetcher.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Fetcher.kt deleted file mode 100644 index ca38ad03b4..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Fetcher.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Mickaël Menu, Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.extensions - -import java.io.File -import org.json.JSONObject -import org.readium.r2.shared.extensions.tryOrNull -import org.readium.r2.shared.fetcher.ArchiveFetcher -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.fetcher.FileFetcher -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.parser.xml.ElementNode -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.util.archive.ArchiveFactory -import org.readium.r2.shared.util.archive.DefaultArchiveFactory -import org.readium.r2.shared.util.use - -/** Returns the resource data at the given [Link]'s HREF, or throws a [Resource.Exception] */ -@Throws(Resource.Exception::class) -internal suspend fun Fetcher.readBytes(link: Link): ByteArray = - get(link).use { it.read().getOrThrow() } - -/** Returns the resource data at the given [href], or throws a [Resource.Exception] */ -@Throws(Resource.Exception::class) -internal suspend fun Fetcher.readBytes(href: String): ByteArray = - get(href).use { it.read().getOrThrow() } - -/** Returns the resource data as an XML Document at the given [href], or null. */ -internal suspend fun Fetcher.readAsXmlOrNull(href: String): ElementNode? = - get(href).use { it.readAsXml().getOrNull() } - -/** Returns the resource data as a JSON object at the given [href], or null. */ -internal suspend fun Fetcher.readAsJsonOrNull(href: String): JSONObject? = - get(href).use { it.readAsJson().getOrNull() } - -/** Creates a [Fetcher] from either an archive file, or an exploded directory. **/ -internal suspend fun Fetcher.Companion.fromArchiveOrDirectory( - path: String, - archiveFactory: ArchiveFactory = DefaultArchiveFactory() -): Fetcher? { - val file = File(path) - val isDirectory = tryOrNull { file.isDirectory } ?: return null - - return if (isDirectory) { - FileFetcher(href = "/", file = file) - } else { - ArchiveFetcher.fromPath(path, archiveFactory) - } -} - -internal suspend fun Fetcher.guessTitle(): String? { - val firstLink = links().firstOrNull() ?: return null - val commonFirstComponent = links().hrefCommonFirstComponent() ?: return null - - if (commonFirstComponent.name == firstLink.href.removePrefix("/")) - return null - - return commonFirstComponent.name -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/File.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/File.kt index babebeebe8..d452095b98 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/File.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/File.kt @@ -10,13 +10,6 @@ package org.readium.r2.streamer.extensions import java.io.File -import java.util.* - -internal val File.lowercasedExtension: String - get() = extension.lowercase(Locale.getDefault()) - -internal val File.isHiddenOrThumbs: Boolean - get() = name.let { it.startsWith(".") || it == "Thumbs.db" } /** * Returns a [File] to the first component of the [File]'s path, diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt deleted file mode 100644 index ec8db2eb94..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.extensions - -import java.io.File -import org.readium.r2.shared.publication.Link - -/** Returns a [File] to the directory containing all links, if there is such a directory. */ -internal fun List<Link>.hrefCommonFirstComponent(): File? = - map { it.href.removePrefix("/").substringBefore("/") } - .distinct() - .takeIf { it.size == 1 } - ?.firstOrNull() - ?.let { File(it) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Url.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Url.kt new file mode 100644 index 0000000000..a4e3eae502 --- /dev/null +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Url.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.streamer.extensions + +import org.readium.r2.shared.util.Url + +internal val Url.isHiddenOrThumbs: Boolean + get() = filename?.let { it.startsWith(".") || it == "Thumbs.db" } ?: false diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/fetcher/Fetcher.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/fetcher/Fetcher.kt index 359e40a728..275cfaf7d0 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/fetcher/Fetcher.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/fetcher/Fetcher.kt @@ -11,21 +11,20 @@ package org.readium.r2.streamer.fetcher import java.io.InputStream import org.readium.r2.shared.publication.Publication -import org.readium.r2.streamer.container.Container -import org.readium.r2.streamer.server.Resources @Suppress("UNUSED_PARAMETER", "unused") -@Deprecated("Use [publication.get(link)] to access publication content.", level = DeprecationLevel.ERROR) -class Fetcher( - var publication: Publication, - var container: Container, - private val userPropertiesPath: String?, - customResources: Resources? = null +@Deprecated( + "Use [publication.get(link)] to access publication content.", + level = DeprecationLevel.ERROR +) +public class Fetcher( + public var publication: Publication, + private val userPropertiesPath: String? ) { - fun data(path: String): ByteArray? = throw NotImplementedError() + public fun data(path: String): ByteArray? = throw NotImplementedError() - fun dataStream(path: String): InputStream = throw NotImplementedError() + public fun dataStream(path: String): InputStream = throw NotImplementedError() - fun dataLength(path: String): Long = throw NotImplementedError() + public fun dataLength(path: String): Long = throw NotImplementedError() } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/fetcher/HtmlInjector.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/fetcher/HtmlInjector.kt deleted file mode 100644 index 23c67e74cf..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/fetcher/HtmlInjector.kt +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Aferdita Muriqi, Clément Baumannn, Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -@file:Suppress("DEPRECATION") - -package org.readium.r2.streamer.fetcher - -import java.io.File -import org.json.JSONArray -import org.json.JSONObject -import org.readium.r2.shared.Injectable -import org.readium.r2.shared.ReadiumCSSName -import org.readium.r2.shared.fetcher.LazyResource -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.ResourceTry -import org.readium.r2.shared.fetcher.TransformingResource -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.epub.EpubLayout -import org.readium.r2.shared.publication.epub.layoutOf -import org.readium.r2.shared.publication.presentation.presentation -import org.readium.r2.shared.publication.services.isProtected -import org.readium.r2.streamer.parser.epub.ReadiumCssLayout -import org.readium.r2.streamer.server.Resources -import timber.log.Timber - -internal class HtmlInjector( - val publication: Publication, - val userPropertiesPath: String?, - val customResources: Resources? = null -) { - - fun transform(resource: Resource): Resource = LazyResource { - - val link = resource.link() - if (link.mediaType.isHtml) - inject(resource) - else - resource - } - - private suspend fun inject(resource: Resource): Resource = object : TransformingResource(resource) { - - override suspend fun transform(data: ResourceTry<ByteArray>): ResourceTry<ByteArray> = - data.map { - val trimmedText = it.toString(link().mediaType.charset ?: Charsets.UTF_8).trim() - val res = if (publication.metadata.presentation.layoutOf(link()) == EpubLayout.REFLOWABLE) - injectReflowableHtml(trimmedText) - else - injectFixedLayoutHtml(trimmedText) - res.toByteArray() - } - } - - private fun injectReflowableHtml(content: String): String { - var resourceHtml = content - // Inject links to css and js files - val head = regexForOpeningHTMLTag("head").find(resourceHtml, 0) - if (head == null) { - Timber.e("No <head> tag found in this resource") - return resourceHtml - } - var beginHeadIndex = head.range.last + 1 - var endHeadIndex = resourceHtml.indexOf("</head>", 0, true) - if (endHeadIndex == -1) - return content - - val layout = ReadiumCssLayout(publication.metadata) - - val endIncludes = mutableListOf<String>() - val beginIncludes = mutableListOf<String>() - beginIncludes.add(getHtmlLink("/assets/readium-css/${layout.readiumCSSPath}ReadiumCSS-before.css")) - - // Fix Readium CSS issue with the positioning of <audio> elements. - // https://github.com/readium/readium-css/issues/94 - // https://github.com/readium/r2-navigator-kotlin/issues/193 - beginIncludes.add( - """ - <style> - audio[controls] { - width: revert; - height: revert; - } - </style> - """.trimIndent() - ) - - // Fix broken pagination when a book contains `overflow-x: hidden`. - // https://github.com/readium/kotlin-toolkit/issues/292 - // Inspired by https://github.com/readium/readium-css/issues/119#issuecomment-1302348238 - beginIncludes.add( - """ - <style> - :root[style], :root { overflow: visible !important; } - :root[style] > body, :root > body { overflow: visible !important; } - </style> - """.trimMargin() - ) - - endIncludes.add(getHtmlLink("/assets/readium-css/${layout.readiumCSSPath}ReadiumCSS-after.css")) - endIncludes.add(getHtmlScript("/assets/scripts/readium-reflowable.js")) - - customResources?.let { - // Inject all custom resourses - for ((key, value) in it.resources) { - if (value is Pair<*, *>) { - if (Injectable(value.second as String) == Injectable.Script) { - endIncludes.add(getHtmlScript("/${Injectable.Script.rawValue}/$key")) - } else if (Injectable(value.second as String) == Injectable.Style) { - endIncludes.add(getHtmlLink("/${Injectable.Style.rawValue}/$key")) - } - } - } - } - - for (element in beginIncludes) { - resourceHtml = StringBuilder(resourceHtml).insert(beginHeadIndex, element).toString() - beginHeadIndex += element.length - endHeadIndex += element.length - } - for (element in endIncludes) { - resourceHtml = StringBuilder(resourceHtml).insert(endHeadIndex, element).toString() - endHeadIndex += element.length - } - resourceHtml = StringBuilder(resourceHtml).insert(endHeadIndex, getHtmlFont(fontFamily = "OpenDyslexic", href = "/assets/fonts/OpenDyslexic-Regular.otf")).toString() - resourceHtml = StringBuilder(resourceHtml).insert(endHeadIndex, "<style>@import url('https://fonts.googleapis.com/css?family=PT+Serif|Roboto|Source+Sans+Pro|Vollkorn');</style>\n").toString() - - // Disable the text selection if the publication is protected. - // FIXME: This is a hack until proper LCP copy is implemented, see https://github.com/readium/r2-testapp-kotlin/issues/266 - if (publication.isProtected) { - resourceHtml = StringBuilder(resourceHtml).insert( - endHeadIndex, - """ - <style> - *:not(input):not(textarea) { - user-select: none; - -webkit-user-select: none; - } - </style> - """ - ).toString() - } - - // Inject userProperties - getProperties(publication.userSettingsUIPreset)?.let { propertyPair -> - val html = regexForOpeningHTMLTag("html").find(resourceHtml, 0) - html?.let { - val match = Regex("""(style=("([^"]*)"[ >]))|(style='([^']*)'[ >])""").find(html.value, 0) - if (match != null) { - val beginStyle = match.range.first + 7 - var newHtml = html.value - newHtml = StringBuilder(newHtml).insert(beginStyle, "${buildStringProperties(propertyPair)} ").toString() - resourceHtml = StringBuilder(resourceHtml).replace(regexForOpeningHTMLTag("html"), newHtml) - } else { - val beginHtmlIndex = resourceHtml.indexOf("<html", 0, true) + 5 - resourceHtml = StringBuilder(resourceHtml).insert(beginHtmlIndex, " style=\"${buildStringProperties(propertyPair)}\"").toString() - } - } ?: run { - val beginHtmlIndex = resourceHtml.indexOf("<html", 0, true) + 5 - resourceHtml = StringBuilder(resourceHtml).insert(beginHtmlIndex, " style=\"${buildStringProperties(propertyPair)}\"").toString() - } - } - - resourceHtml = applyDirectionAttribute(resourceHtml, publication) - - return resourceHtml - } - - private fun applyDirectionAttribute(resourceHtml: String, publication: Publication): String { - var resourceHtml1 = resourceHtml - fun addRTLDir(tagName: String, html: String): String { - return regexForOpeningHTMLTag(tagName).find(html, 0)?.let { result -> - Regex("""dir=""").find(result.value, 0)?.let { - html - } ?: run { - val beginHtmlIndex = html.indexOf("<$tagName", 0, true) + 5 - StringBuilder(html).insert(beginHtmlIndex, " dir=\"rtl\"").toString() - } - } ?: run { - html - } - } - - if (publication.cssStyle == "rtl") { - resourceHtml1 = addRTLDir("html", resourceHtml1) - resourceHtml1 = addRTLDir("body", resourceHtml1) - } - - return resourceHtml1 - } - - private fun regexForOpeningHTMLTag(name: String): Regex = - Regex("""<$name.*?>""", setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)) - - private fun injectFixedLayoutHtml(content: String): String { - var resourceHtml = content - val endHeadIndex = resourceHtml.indexOf("</head>", 0, true) - if (endHeadIndex == -1) - return content - val includes = mutableListOf<String>() - includes.add(getHtmlScript("/assets/scripts/readium-fixed.js")) - for (element in includes) { - resourceHtml = StringBuilder(resourceHtml).insert(endHeadIndex, element).toString() - } - return resourceHtml - } - - private fun getHtmlFont(fontFamily: String, href: String): String { - val prefix = "<style type=\"text/css\"> @font-face{font-family: \"$fontFamily\"; src:url(\"" - val suffix = "\") format('truetype');}</style>\n" - return prefix + href + suffix - } - - private fun getHtmlLink(resourceName: String): String { - val prefix = "<link rel=\"stylesheet\" type=\"text/css\" href=\"" - val suffix = "\"/>\n" - return prefix + resourceName + suffix - } - - private fun getHtmlScript(resourceName: String): String { - val prefix = "<script type=\"text/javascript\" src=\"" - val suffix = "\"></script>\n" - - return prefix + resourceName + suffix - } - - private fun getProperties(preset: MutableMap<ReadiumCSSName, Boolean>): MutableMap<String, String>? { - - // userProperties is a JSON string containing the css userProperties - var userPropertiesString: String? = null - userPropertiesPath?.let { - userPropertiesString = String() - val file = File(it) - if (file.isFile && file.canRead()) { - for (i in file.readLines()) { - userPropertiesString += i - } - } - } - - return userPropertiesString?.let { - // Parsing of the String into a JSONArray of JSONObject with each "name" and "value" of the css properties - // Making that JSONArray a MutableMap<String, String> to make easier the access of data - return@let try { - val propertiesArray = JSONArray(userPropertiesString) - val properties: MutableMap<String, String> = mutableMapOf() - for (i in 0 until propertiesArray.length()) { - val value = JSONObject(propertiesArray.getString(i)) - var isInPreset = false - - for (property in preset) { - if (property.key.ref == value.getString("name")) { - isInPreset = true - val presetPair = Pair(property.key, preset[property.key]) - val presetValue = applyPreset(presetPair) - properties[presetValue.getString("name")] = presetValue.getString("value") - } - } - - if (!isInPreset) { - properties[value.getString("name")] = value.getString("value") - } - } - properties - } catch (e: Exception) { - null - } - } - } - - private fun applyPreset(preset: Pair<ReadiumCSSName, Boolean?>): JSONObject { - val readiumCSSProperty = JSONObject() - - readiumCSSProperty.put("name", preset.first.ref) - - when (preset.first) { - ReadiumCSSName.hyphens -> { - readiumCSSProperty.put("value", "") - } - ReadiumCSSName.fontOverride -> { - readiumCSSProperty.put("value", "readium-font-off") - } - ReadiumCSSName.appearance -> { - readiumCSSProperty.put("value", "readium-default-on") - } - ReadiumCSSName.publisherDefault -> { - readiumCSSProperty.put("value", "") - } - ReadiumCSSName.columnCount -> { - readiumCSSProperty.put("value", "auto") - } - ReadiumCSSName.pageMargins -> { - readiumCSSProperty.put("value", "0.5") - } - ReadiumCSSName.lineHeight -> { - readiumCSSProperty.put("value", "1.0") - } - ReadiumCSSName.ligatures -> { - readiumCSSProperty.put("value", "") - } - ReadiumCSSName.fontFamily -> { - readiumCSSProperty.put("value", "Original") - } - ReadiumCSSName.fontSize -> { - readiumCSSProperty.put("value", "100%") - } - ReadiumCSSName.wordSpacing -> { - readiumCSSProperty.put("value", "0.0rem") - } - ReadiumCSSName.letterSpacing -> { - readiumCSSProperty.put("value", "0.0em") - } - ReadiumCSSName.textAlignment -> { - readiumCSSProperty.put("value", "justify") - } - ReadiumCSSName.paraIndent -> { - readiumCSSProperty.put("value", "") - } - ReadiumCSSName.scroll -> { - if (preset.second!!) { - readiumCSSProperty.put("value", "readium-scroll-on") - } else { - readiumCSSProperty.put("value", "readium-scroll-off") - } - } - } - - return readiumCSSProperty - } - - private fun buildStringProperties(list: MutableMap<String, String>): String { - var string = "" - for (property in list) { - string = string + " " + property.key + ": " + property.value + ";" - } - return string - } -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/fetcher/LcpDecryptor.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/fetcher/LcpDecryptor.kt deleted file mode 100644 index 96de6fce88..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/fetcher/LcpDecryptor.kt +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Mickaël Menu - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.fetcher - -import java.io.IOException -import org.readium.r2.shared.drm.DRM -import org.readium.r2.shared.drm.DRMLicense -import org.readium.r2.shared.extensions.inflate -import org.readium.r2.shared.fetcher.* -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.encryption.encryption -import org.readium.r2.shared.util.Try - -/** - * Decrypts a resource protected with LCP. - */ -internal class LcpDecryptor(val drm: DRM) { - - fun transform(resource: Resource): Resource = LazyResource { - // Checks if the resource is encrypted and whether the encryption schemes of the resource - // and the DRM license are the same. - val license = drm.license - val link = resource.link() - val encryption = link.properties.encryption - if (license == null || encryption == null || encryption.scheme != drm.scheme.rawValue) - return@LazyResource resource - - when { - link.isDeflated || !link.isCbcEncrypted -> FullLcpResource(resource, license) - else -> CbcLcpResource(resource, license) - } - } - - /** - * A LCP resource that is read, decrypted and cached fully before reading requested ranges. - * - * Can be used when it's impossible to map a read range (byte range request) to the encrypted - * resource, for example when the resource is deflated before encryption. - */ - private class FullLcpResource( - resource: Resource, - private val license: DRMLicense - ) : TransformingResource(resource) { - - override suspend fun transform(data: ResourceTry<ByteArray>): ResourceTry<ByteArray> = - license.decryptFully(data, resource.link().isDeflated) - - override suspend fun length(): ResourceTry<Long> = - resource.link().properties.encryption?.originalLength - ?.let { Try.success(it) } - ?: super.length() - } - - /** - * A LCP resource used to read content encrypted with the CBC algorithm. - * - * Supports random access for byte range requests, but the resource MUST NOT be deflated. - */ - private class CbcLcpResource( - private val resource: Resource, - private val license: DRMLicense - ) : Resource { - - override suspend fun link(): Link = resource.link() - - /** Plain text size. */ - override suspend fun length(): ResourceTry<Long> = - resource.length().flatMapCatching { length -> - if (length < 2 * AES_BLOCK_SIZE) { - throw Exception("Invalid CBC-encrypted stream") - } - - val readOffset = length - (2 * AES_BLOCK_SIZE) - resource.read(readOffset..length) - .mapCatching { bytes -> - val decryptedBytes = license.decipher(bytes) - ?: throw Exception("Can't decrypt trailing size of CBC-encrypted stream") - - return@mapCatching length - - AES_BLOCK_SIZE - // Minus IV or previous block - (AES_BLOCK_SIZE - decryptedBytes.size) % AES_BLOCK_SIZE // Minus padding part - } - } - - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> { - return if (range == null) { - license.decryptFully(resource.read(), isDeflated = false) - } else { - resource.length().flatMapCatching { length -> - val blockPosition = range.first % AES_BLOCK_SIZE - - // For beginning of the cipher text, IV used for XOR. - // For cipher text in the middle, previous block used for XOR. - val readPosition = range.first - blockPosition - - // Count blocks to read. - // First block for IV or previous block to perform XOR. - var blocksCount: Long = 1 - var bytesInFirstBlock = (AES_BLOCK_SIZE - blockPosition) % AES_BLOCK_SIZE - if (length < bytesInFirstBlock) { - bytesInFirstBlock = 0 - } - if (bytesInFirstBlock > 0) { - blocksCount += 1 - } - - blocksCount += (length - bytesInFirstBlock) / AES_BLOCK_SIZE - if ((length - bytesInFirstBlock) % AES_BLOCK_SIZE != 0L) { - blocksCount += 1 - } - - val readSize = blocksCount * AES_BLOCK_SIZE - resource.read(readPosition..(readPosition + readSize)) - .mapCatching { - var bytes = license.decipher(it) - ?: throw IOException("Can't decrypt the content at: ${link().href}") - - if (bytes.size > length) { - bytes = bytes.copyOfRange(0, length.toInt()) - } - - bytes - } - } - } - } - - override suspend fun close() = resource.close() - - companion object { - private const val AES_BLOCK_SIZE = 16 // bytes - } - } -} - -private fun DRMLicense.decryptFully(data: ResourceTry<ByteArray>, isDeflated: Boolean): ResourceTry<ByteArray> = - data.mapCatching { - // Decrypts the resource. - var bytes = decipher(it) - ?.takeIf { b -> b.isNotEmpty() } - ?: throw Exception("Failed to decrypt the resource") - - // Removes the padding. - val padding = bytes.last().toInt() - bytes = bytes.copyOfRange(0, bytes.size - padding) - - // If the ressource was compressed using deflate, inflates it. - if (isDeflated) { - bytes = bytes.inflate(nowrap = true) - } - - bytes - } - -private val Link.isDeflated: Boolean get() = - properties.encryption?.compression?.lowercase(java.util.Locale.ROOT) == "deflate" - -private val Link.isCbcEncrypted: Boolean get() = - properties.encryption?.algorithm == "http://www.w3.org/2001/04/xmlenc#aes256-cbc" diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/FallbackContentProtection.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/FallbackContentProtection.kt deleted file mode 100644 index 9ee1481285..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/FallbackContentProtection.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2021 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.streamer.parser - -import org.readium.r2.shared.UserException -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.publication.ContentProtection -import org.readium.r2.shared.publication.ContentProtection.Scheme -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.asset.PublicationAsset -import org.readium.r2.shared.publication.services.ContentProtectionService -import org.readium.r2.shared.publication.services.contentProtectionServiceFactory -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.streamer.extensions.readAsJsonOrNull -import org.readium.r2.streamer.extensions.readAsXmlOrNull -import org.readium.r2.streamer.parser.epub.EncryptionParser -import org.readium.r2.streamer.parser.epub.Namespaces - -/** - * [ContentProtection] implementation used as a fallback by the Streamer to detect known DRM - * schemes (e.g. LCP or ADEPT), if they are not supported by the app. - */ -internal class FallbackContentProtection : ContentProtection { - - class Service(override val scheme: Scheme?) : ContentProtectionService { - - override val isRestricted: Boolean = true - override val credentials: String? = null - override val rights = ContentProtectionService.UserRights.AllRestricted - override val error: UserException = ContentProtection.Exception.SchemeNotSupported(scheme) - - companion object { - - fun createFactory(scheme: Scheme?): (Publication.Service.Context) -> Service = - { Service(scheme) } - } - } - - override suspend fun open( - asset: PublicationAsset, - fetcher: Fetcher, - credentials: String?, - allowUserInteraction: Boolean, - sender: Any? - ): Try<ContentProtection.ProtectedAsset, Publication.OpeningException>? { - val scheme: Scheme = sniffScheme(fetcher, asset.mediaType()) - ?: return null - - val protectedFile = ContentProtection.ProtectedAsset( - asset = asset, - fetcher = fetcher, - onCreatePublication = { - servicesBuilder.contentProtectionServiceFactory = Service.createFactory(scheme) - } - ) - - return Try.success(protectedFile) - } - - internal suspend fun sniffScheme(fetcher: Fetcher, mediaType: MediaType): Scheme? = - when { - fetcher.readAsJsonOrNull("/license.lcpl") != null -> - Scheme.Lcp - - mediaType.matches(MediaType.EPUB) -> { - val rightsXml = fetcher.readAsXmlOrNull("/META-INF/rights.xml") - val encryptionXml = fetcher.readAsXmlOrNull("/META-INF/encryption.xml") - val encryption = encryptionXml?.let { EncryptionParser.parse(it) } - - when { - ( - fetcher.readAsJsonOrNull("/META-INF/license.lcpl") != null || - encryption?.any { it.value.scheme == "http://readium.org/2014/01/lcp" } == true - ) -> Scheme.Lcp - - ( - encryptionXml != null && ( - rightsXml?.namespace == "http://ns.adobe.com/adept" || - encryptionXml - .get("EncryptedData", Namespaces.ENC) - .flatMap { it.get("KeyInfo", Namespaces.SIG) } - .flatMap { it.get("resource", "http://ns.adobe.com/adept") } - .isNotEmpty() - ) - ) -> Scheme.Adept - - // A file with only obfuscated fonts might still have an `encryption.xml` file. - // To make sure that we don't lock a readable publication, we ignore unknown - // encryption.xml schemes. - else -> null - } - } - - else -> null - } -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt index d118a578fc..026bd23397 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt @@ -1,21 +1,105 @@ /* - * Module: r2-streamer-kotlin - * Developers: Quentin Gliosca, Aferdita Muriqi, Clément Baumann - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. + * Copyright 2020 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. */ package org.readium.r2.streamer.parser -import java.io.File +import android.content.Context import org.readium.r2.shared.publication.Publication -import org.readium.r2.streamer.container.Container +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.logging.WarningLogger +import org.readium.r2.shared.util.pdf.PdfDocumentFactory +import org.readium.r2.streamer.parser.audio.AudioParser +import org.readium.r2.streamer.parser.epub.EpubParser +import org.readium.r2.streamer.parser.image.ImageParser +import org.readium.r2.streamer.parser.pdf.PdfParser +import org.readium.r2.streamer.parser.readium.ReadiumWebPubParser + +/** + * Parses a [Publication] from an [Asset]. + */ +public interface PublicationParser { + + /** + * Constructs a [Publication.Builder] to build a [Publication] from a publication asset. + * + * @param asset Publication asset. + * @param warnings Used to report non-fatal parsing warnings, such as publication authoring + * mistakes. This is useful to warn users of potential rendering issues or help authors + * debug their publications. + */ + public suspend fun parse( + asset: Asset, + warnings: WarningLogger? = null + ): Try<Publication.Builder, ParseError> + + public sealed class ParseError( + public override val message: String, + public override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { + + public class FormatNotSupported : + ParseError("Asset format not supported.", null) -data class PubBox(var publication: Publication, var container: Container) + public class Reading(override val cause: org.readium.r2.shared.util.data.ReadError) : + ParseError("An error occurred while trying to read asset.", cause) + } +} + +/** + * Default implementation of [PublicationParser] handling all the publication formats supported by + * Readium. + * + * @param additionalParsers Parsers used to open a publication, in addition to the default parsers. They take precedence over the default ones. + * @param httpClient Service performing HTTP requests. + * @param pdfFactory Parses a PDF document, optionally protected by password. + * @param assetRetriever Opens assets in case of indirection. + */ +public class DefaultPublicationParser( + context: Context, + private val httpClient: HttpClient, + assetRetriever: AssetRetriever, + pdfFactory: PdfDocumentFactory<*>?, + additionalParsers: List<PublicationParser> = emptyList() +) : PublicationParser by CompositePublicationParser( + additionalParsers + listOfNotNull( + EpubParser(), + pdfFactory?.let { PdfParser(context, it) }, + ReadiumWebPubParser(context, httpClient, pdfFactory), + ImageParser(assetRetriever), + AudioParser(assetRetriever) + ) +) + +/** + * A composite [PublicationParser] which tries several parsers until it finds one which supports + * the asset. + */ +public class CompositePublicationParser( + private val parsers: List<PublicationParser> +) : PublicationParser { -interface PublicationParser { + public constructor(vararg parsers: PublicationParser) : + this(parsers.toList()) - fun parse(fileAtPath: String, fallbackTitle: String = File(fileAtPath).name): PubBox? + override suspend fun parse( + asset: Asset, + warnings: WarningLogger? + ): Try<Publication.Builder, PublicationParser.ParseError> { + for (parser in parsers) { + val result = parser.parse(asset, warnings) + if ( + result is Try.Success || + result is Try.Failure && result.value !is PublicationParser.ParseError.FormatNotSupported + ) { + return result + } + } + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) + } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioBookParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioBookParser.kt deleted file mode 100644 index 030f1faf11..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioBookParser.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi, Irteza Sheikh - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.parser.audio - -import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.publication.Manifest -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.streamer.container.ContainerError -import org.readium.r2.streamer.container.PublicationContainer -import org.readium.r2.streamer.extensions.fromArchiveOrDirectory -import org.readium.r2.streamer.extensions.readAsJsonOrNull -import org.readium.r2.streamer.parser.PubBox -import org.readium.r2.streamer.parser.PublicationParser - -class AudioBookConstant { - companion object { - @Deprecated("Use [MediaType.AUDIOBOOK.toString()] instead", replaceWith = ReplaceWith("MediaType.AUDIOBOOK.toString()")) - val mimetype get() = MediaType.READIUM_AUDIOBOOK.toString() - } -} - -/** - * AudiobookParser : Handle any Audiobook Package file. Opening, listing files - * get name of the resource, creating the Publication - * for rendering - */ -class AudioBookParser : PublicationParser { - - /** - * This functions parse a manifest.json and build PubBox object from it - */ - override fun parse(fileAtPath: String, fallbackTitle: String): PubBox? = runBlocking { - _parse(fileAtPath) - } - - private suspend fun _parse(fileAtPath: String): PubBox? { - val fetcher = Fetcher.fromArchiveOrDirectory(fileAtPath) - ?: throw ContainerError.missingFile(fileAtPath) - - val manifest = fetcher.readAsJsonOrNull("manifest.json") - ?.let { Manifest.fromJSON(it, packaged = true) } - ?: return null - - val publication = Publication( - manifest = manifest - ).apply { - @Suppress("DEPRECATION") - type = Publication.TYPE.AUDIO - } - - val container = PublicationContainer( - publication = publication, - path = fileAtPath, - mediaType = MediaType.READIUM_AUDIOBOOK - ) - - return PubBox(publication, container) - } -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioLocatorService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioLocatorService.kt index 2251486b21..ea16f436b1 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioLocatorService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioLocatorService.kt @@ -14,7 +14,7 @@ import org.readium.r2.shared.publication.services.LocatorService import org.readium.r2.shared.util.mediatype.MediaType /** Locator service for audio publications. */ -class AudioLocatorService(private val readingOrder: List<Link>) : LocatorService { +public class AudioLocatorService(private val readingOrder: List<Link>) : LocatorService { /** Duration per reading order index. */ private val durations: List<Double> = @@ -50,13 +50,16 @@ class AudioLocatorService(private val readingOrder: List<Link>) : LocatorService val positionInResource = positionInPublication - resourcePosition return Locator( - href = link.href, - type = link.type ?: MediaType.BINARY.toString(), + href = link.url(), + mediaType = link.mediaType ?: MediaType.BINARY, locations = Locator.Locations( fragments = listOf("t=${positionInResource.toInt()}"), progression = link.duration?.let { duration -> - if (duration == 0.0) 0.0 - else positionInResource / duration + if (duration == 0.0) { + 0.0 + } else { + positionInResource / duration + } }, totalProgression = totalProgression ) @@ -86,9 +89,9 @@ class AudioLocatorService(private val readingOrder: List<Link>) : LocatorService return null } - companion object { + public companion object { - fun createFactory(): (Publication.Service.Context) -> AudioLocatorService = { + public fun createFactory(): (Publication.Service.Context) -> AudioLocatorService = { AudioLocatorService(readingOrder = it.manifest.readingOrder) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index 8d39de6c63..d8563479b6 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -1,24 +1,45 @@ /* - * Module: r2-streamer-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. */ package org.readium.r2.streamer.parser.audio -import java.io.File -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.publication.* -import org.readium.r2.shared.publication.asset.PublicationAsset +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.publication.LocalizedString +import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.publication.Metadata +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.format.AacSpecification +import org.readium.r2.shared.util.format.AiffSpecification +import org.readium.r2.shared.util.format.FlacSpecification +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.InformalAudiobookSpecification +import org.readium.r2.shared.util.format.Mp3Specification +import org.readium.r2.shared.util.format.Mp4Specification +import org.readium.r2.shared.util.format.OggSpecification +import org.readium.r2.shared.util.format.OpusSpecification +import org.readium.r2.shared.util.format.Specification +import org.readium.r2.shared.util.format.WavSpecification +import org.readium.r2.shared.util.format.WebmSpecification +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.streamer.PublicationParser +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs -import org.readium.r2.streamer.extensions.lowercasedExtension +import org.readium.r2.streamer.extensions.sniffContainerEntries +import org.readium.r2.streamer.extensions.toContainer +import org.readium.r2.streamer.parser.PublicationParser /** * Parses an audiobook Publication from an unstructured archive format containing audio files, @@ -26,57 +47,108 @@ import org.readium.r2.streamer.extensions.lowercasedExtension * * It can also work for a standalone audio file. */ -class AudioParser : PublicationParser { +public class AudioParser( + private val assetSniffer: AssetRetriever +) : PublicationParser { + + override suspend fun parse( + asset: Asset, + warnings: WarningLogger? + ): Try<Publication.Builder, PublicationParser.ParseError> = + when (asset) { + is ResourceAsset -> parseResourceAsset(asset) + is ContainerAsset -> parseContainerAsset(asset) + } + + private fun parseResourceAsset( + asset: ResourceAsset + ): Try<Publication.Builder, PublicationParser.ParseError> { + if (!asset.format.conformsToAny(audioSpecifications)) { + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) + } - override suspend fun parse(asset: PublicationAsset, fetcher: Fetcher, warnings: WarningLogger?): Publication.Builder? { + val container = + asset.toContainer() - if (!accepts(asset, fetcher)) - return null + val readingOrderWithFormat = + listOfNotNull(container.first() to asset.format) + + return finalizeParsing(container, readingOrderWithFormat, null) + } - val readingOrder = fetcher.links() - .filter { link -> with(File(link.href)) { lowercasedExtension in audioExtensions && !isHiddenOrThumbs } } - .sortedBy(Link::href) - .toMutableList() + private suspend fun parseContainerAsset( + asset: ContainerAsset + ): Try<Publication.Builder, PublicationParser.ParseError> { + if (!asset.format.conformsTo(InformalAudiobookSpecification)) { + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) + } - if (readingOrder.isEmpty()) - throw Exception("No audio file found in the publication.") + val entryFormats: Map<Url, Format> = assetSniffer + .sniffContainerEntries(asset.container) { !it.isHiddenOrThumbs } + .getOrElse { return Try.failure(PublicationParser.ParseError.Reading(it)) } - val title = fetcher.guessTitle() ?: asset.name + val readingOrderWithFormat = + asset.container + .mapNotNull { url -> entryFormats[url]?.let { url to it } } + .filter { (_, format) -> format.specification.specifications.any { it in audioSpecifications } } + .sortedBy { it.first.toString() } + + if (readingOrderWithFormat.isEmpty()) { + return Try.failure( + PublicationParser.ParseError.Reading( + ReadError.Decoding( + DebugError("No audio file found in the publication.") + ) + ) + ) + } + + val title = asset + .container + .entries + .guessTitle() + + return finalizeParsing(asset.container, readingOrderWithFormat, title) + } + + private fun finalizeParsing( + container: Container<Resource>, + readingOrderWithFormat: List<Pair<Url, Format>>, + title: String? + ): Try<Publication.Builder, PublicationParser.ParseError> { + val readingOrder = readingOrderWithFormat.map { (url, format) -> + Link(href = url, mediaType = format.mediaType) + } val manifest = Manifest( metadata = Metadata( conformsTo = setOf(Publication.Profile.AUDIOBOOK), - localizedTitle = LocalizedString(title) + localizedTitle = title?.let { LocalizedString(it) } ), readingOrder = readingOrder ) - return Publication.Builder( + val publicationBuilder = Publication.Builder( manifest = manifest, - fetcher = fetcher, + container = container, servicesBuilder = Publication.ServicesBuilder( locator = AudioLocatorService.createFactory() ) ) - } - - private suspend fun accepts(asset: PublicationAsset, fetcher: Fetcher): Boolean { - if (asset.mediaType() == MediaType.ZAB) - return true - - val allowedExtensions = audioExtensions + - listOf("asx", "bio", "m3u", "m3u8", "pla", "pls", "smil", "txt", "vlc", "wpl", "xspf", "zpl") - - if (fetcher.links().filterNot { File(it.href).isHiddenOrThumbs } - .all { File(it.href).lowercasedExtension in allowedExtensions } - ) - return true - return false + return Try.success(publicationBuilder) } - private val audioExtensions = listOf( - "aac", "aiff", "alac", "flac", "m4a", "m4b", "mp3", - "ogg", "oga", "mogg", "opus", "wav", "webm" - ) + private val audioSpecifications: Set<Specification> = + setOf( + AacSpecification, + AiffSpecification, + FlacSpecification, + Mp4Specification, + Mp3Specification, + OggSpecification, + OpusSpecification, + WavSpecification, + WebmSpecification + ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/cbz/CBZParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/cbz/CBZParser.kt deleted file mode 100644 index 8b254c96f1..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/cbz/CBZParser.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi, Clément Baumann - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.parser.cbz - -import java.io.File -import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.publication.LocalizedString -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.asset.FileAsset -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.streamer.container.ContainerError -import org.readium.r2.streamer.container.PublicationContainer -import org.readium.r2.streamer.extensions.fromArchiveOrDirectory -import org.readium.r2.streamer.parser.PubBox -import org.readium.r2.streamer.parser.PublicationParser -import org.readium.r2.streamer.parser.image.ImageParser - -@Deprecated("Use [MediaType] instead") -class CBZConstant { - companion object { - @Deprecated("Use [MediaType.CBZ.toString()] instead", replaceWith = ReplaceWith("MediaType.CBZ.toString()")) - val mimetypeCBZ get() = MediaType.CBZ.toString() - @Deprecated("RAR archives are not supported in Readium, don't use this constant", level = DeprecationLevel.ERROR) - const val mimetypeCBR = "application/x-cbr" - @Deprecated("Use [MediaType.JPEG.toString()] instead", replaceWith = ReplaceWith("MediaType.JPEG.toString()")) - val mimetypeJPEG get() = MediaType.JPEG.toString() - @Deprecated("Use [MediaType.PNG.toString()] instead", replaceWith = ReplaceWith("MediaType.PNG.toString()")) - val mimetypePNG = MediaType.PNG.toString() - } -} - -/** - * CBZParser : Handle any CBZ file. Opening, listing files - * get name of the resource, creating the Publication - * for rendering - */ -class CBZParser : PublicationParser { - - private val imageParser = ImageParser() - - override fun parse(fileAtPath: String, fallbackTitle: String): PubBox? = runBlocking { - makePubBox(fileAtPath, fallbackTitle) - } - - private suspend fun makePubBox(fileAtPath: String, fallbackTitle: String): PubBox? { - - val file = File(fileAtPath) - - val fetcher = Fetcher.fromArchiveOrDirectory(fileAtPath) - ?: throw ContainerError.missingFile(fileAtPath) - - val publication = imageParser.parse(FileAsset(file), fetcher) - ?.apply { - val title = LocalizedString(fallbackTitle) - val metadata = manifest.metadata.copy(localizedTitle = title) - manifest = manifest.copy(metadata = metadata) - } - ?.build() - ?.apply { - @Suppress("DEPRECATION") - type = Publication.TYPE.CBZ - } - ?: return null - - val container = PublicationContainer( - publication = publication, - path = file.canonicalPath, - mediaType = MediaType.CBZ - ) - - return PubBox(publication, container) - } -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/divina/DiViNaParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/divina/DiViNaParser.kt deleted file mode 100644 index 489f80153f..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/divina/DiViNaParser.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.parser.divina - -import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.publication.Manifest -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.services.PerResourcePositionsService -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.streamer.container.ContainerError -import org.readium.r2.streamer.container.PublicationContainer -import org.readium.r2.streamer.extensions.fromArchiveOrDirectory -import org.readium.r2.streamer.extensions.readAsJsonOrNull -import org.readium.r2.streamer.parser.PubBox -import org.readium.r2.streamer.parser.PublicationParser - -class DiViNaConstant { - companion object { - @Deprecated("Use [MediaType.DIVINA_MANIFEST.toString()] instead", replaceWith = ReplaceWith("MediaType.DIVINA_MANIFEST.toString()"), level = DeprecationLevel.ERROR) - val mimetype get() = MediaType.DIVINA_MANIFEST.toString() - } -} - -/** - * DiViNaParser : Handle any DiViNa file. Opening, listing files - * get name of the resource, creating the Publication - * for rendering - */ -class DiViNaParser : PublicationParser { - override fun parse(fileAtPath: String, fallbackTitle: String): PubBox? = runBlocking { - _parse(fileAtPath) - } - - private suspend fun _parse(fileAtPath: String): PubBox? { - val fetcher = Fetcher.fromArchiveOrDirectory(fileAtPath) - ?: throw ContainerError.missingFile(fileAtPath) - - val manifest = fetcher.readAsJsonOrNull("manifest.json") - .let { Manifest.fromJSON(it) } - ?: return null - - val publication = Publication( - manifest = manifest, - servicesBuilder = Publication.ServicesBuilder( - positions = PerResourcePositionsService.createFactory(fallbackMediaType = "image/*") - ) - ).apply { - @Suppress("DEPRECATION") - type = Publication.TYPE.DiViNa - } - - val container = PublicationContainer( - publication = publication, - path = fileAtPath, - mediaType = MediaType.DIVINA - ) - return PubBox(publication, container) - } -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/AccessibilityAdapter.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/AccessibilityAdapter.kt index 9c6cc49b82..565e688811 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/AccessibilityAdapter.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/AccessibilityAdapter.kt @@ -61,10 +61,14 @@ internal class AccessibilityAdapter { private fun conformedToProfileOrNull(item: MetadataItem): Accessibility.Profile? = if (item is MetadataItem.Meta && item.property == Vocabularies.DCTERMS + "conformsTo") { accessibilityProfileFromString(item.value) - } else if (item is MetadataItem.Link && item.rels.contains(Vocabularies.DCTERMS + "conformsTo")) { - accessibilityProfileFromString(item.href) - } else + } else if (item is MetadataItem.Link && item.rels.contains( + Vocabularies.DCTERMS + "conformsTo" + ) + ) { + accessibilityProfileFromString(item.href.toString()) + } else { null + } private fun adaptAccessModeSufficient(items: List<MetadataItem>): Pair<Set<Set<Accessibility.PrimaryAccessMode>>, List<MetadataItem>> = items .takeAllWithProperty(Vocabularies.SCHEMA + "accessModeSufficient") @@ -105,7 +109,7 @@ internal class AccessibilityAdapter { remainingItems .takeFirstWithRel(Vocabularies.A11Y + "certifierReport") .let { remainingItems = it.second; it.first } - ?.let { certification = certification.copy(report = it.href) } + ?.let { certification = certification.copy(report = it.href.toString()) } } return if (remainingItems.size == items.size) { @@ -127,7 +131,7 @@ internal class AccessibilityAdapter { return Accessibility.Certification( certifiedBy = value, credential = credential, - report = report + report = report?.toString() ) } @@ -143,7 +147,7 @@ internal class AccessibilityAdapter { "http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-a", "http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a", "https://idpf.org/epub/a11y/accessibility-20170105.html#wcag-a", - "https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a", + "https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a" ) private fun isWCAG_20_AA(value: String) = value in setOf( @@ -151,7 +155,7 @@ internal class AccessibilityAdapter { "http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa", "http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa", "https://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa", - "https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa", + "https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa" ) private fun isWCAG_20_AAA(value: String) = value in setOf( @@ -159,6 +163,6 @@ internal class AccessibilityAdapter { "http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa", "http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa", "https://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa", - "https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa", + "https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa" ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ClockValueParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ClockValueParser.kt index 3484ab7b0a..e5d0516a06 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ClockValueParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ClockValueParser.kt @@ -29,8 +29,8 @@ internal object ClockValueParser { private fun parseClockvalue(value: String): Double? { val parts = value.split(":").map { it.toDoubleOrNull() ?: return null } - val min_sec = parts.last() + parts[parts.size - 2] * 60 - return if (parts.size > 2) min_sec + parts[parts.size - 3] * 3600 else min_sec + val minSec = parts.last() + parts[parts.size - 2] * 60 + return if (parts.size > 2) minSec + parts[parts.size - 3] * 3600 else minSec } private fun parseTimecount(value: Double, metric: String): Double? = diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt index 43060ca40b..c0dc70b29b 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt @@ -9,33 +9,44 @@ package org.readium.r2.streamer.parser.epub import com.mcxiaoke.koi.HASH import com.mcxiaoke.koi.ext.toHexBytes import kotlin.experimental.xor -import org.readium.r2.shared.fetcher.LazyResource -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.ResourceTry -import org.readium.r2.shared.fetcher.TransformingResource -import org.readium.r2.shared.publication.encryption.encryption +import org.readium.r2.shared.publication.encryption.Encryption +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadTry +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.TransformingResource +import org.readium.r2.shared.util.resource.flatMap /** * Deobfuscates fonts according to https://www.w3.org/TR/epub-33/#sec-font-obfuscation */ -internal class EpubDeobfuscator(private val pubId: String) { +internal class EpubDeobfuscator( + private val pubId: String, + private val encryptionData: Map<Url, Encryption> +) { - fun transform(resource: Resource): Resource = LazyResource { - val algorithm = resource.link().properties.encryption?.algorithm - if (algorithm != null && algorithm2length.containsKey(algorithm)) { - DeobfuscatingResource(resource, algorithm) - } else { - resource + @Suppress("Unused_parameter") + fun transform(url: Url, resource: Resource): Resource = + resource.flatMap { + val algorithm = resource.sourceUrl + ?.let { encryptionData[it] } + ?.algorithm + if (algorithm != null && algorithm2length.containsKey(algorithm)) { + DeobfuscatingResource(resource, algorithm) + } else { + resource + } } - } - inner class DeobfuscatingResource(resource: Resource, private val algorithm: String) : TransformingResource(resource) { + inner class DeobfuscatingResource( + private val resource: Resource, + private val algorithm: String + ) : TransformingResource(resource) { // The obfuscation doesn't change the length of the resource. - override suspend fun length(): ResourceTry<Long> = + override suspend fun length(): ReadTry<Long> = resource.length() - override suspend fun transform(data: ResourceTry<ByteArray>): ResourceTry<ByteArray> = + override suspend fun transform(data: ReadTry<ByteArray>): ReadTry<ByteArray> = data.map { bytes -> val obfuscationLength: Int = algorithm2length[algorithm] ?: return@map bytes @@ -45,7 +56,11 @@ internal class EpubDeobfuscator(private val pubId: String) { else -> HASH.sha1(pubId).toHexBytes() } - deobfuscate(bytes = bytes, obfuscationKey = obfuscationKey, obfuscationLength = obfuscationLength) + deobfuscate( + bytes = bytes, + obfuscationKey = obfuscationKey, + obfuscationLength = obfuscationLength + ) bytes } } @@ -56,7 +71,6 @@ internal class EpubDeobfuscator(private val pubId: String) { ) private fun deobfuscate(bytes: ByteArray, obfuscationKey: ByteArray, obfuscationLength: Int) { - @Suppress("NAME_SHADOWING") val toDeobfuscate = 0 until obfuscationLength for (i in toDeobfuscate) bytes[i] = bytes[i].xor(obfuscationKey[i % obfuscationKey.size]) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index ef38fcf073..fe54dae184 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -4,80 +4,38 @@ * available in the top-level LICENSE file of the project. */ -@file:Suppress("DEPRECATION") - package org.readium.r2.streamer.parser.epub -import java.io.File -import kotlinx.coroutines.runBlocking import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.ReadiumCSSName -import org.readium.r2.shared.Search -import org.readium.r2.shared.drm.DRM -import org.readium.r2.shared.extensions.addPrefix -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.fetcher.TransformingFetcher import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.asset.FileAsset -import org.readium.r2.shared.publication.asset.PublicationAsset import org.readium.r2.shared.publication.encryption.Encryption +import org.readium.r2.shared.publication.epub.EpubEncryptionParser import org.readium.r2.shared.publication.services.content.DefaultContentService import org.readium.r2.shared.publication.services.content.iterators.HtmlResourceContentIterator import org.readium.r2.shared.publication.services.search.StringSearchService -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.DecodeError +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.data.decodeXml +import org.readium.r2.shared.util.data.readDecodeOrElse +import org.readium.r2.shared.util.data.readDecodeOrNull +import org.readium.r2.shared.util.format.EpubSpecification +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.TransformingContainer import org.readium.r2.shared.util.use -import org.readium.r2.streamer.PublicationParser -import org.readium.r2.streamer.container.Container -import org.readium.r2.streamer.container.ContainerError -import org.readium.r2.streamer.container.PublicationContainer -import org.readium.r2.streamer.extensions.fromArchiveOrDirectory -import org.readium.r2.streamer.extensions.readAsXmlOrNull -import org.readium.r2.streamer.fetcher.LcpDecryptor -import org.readium.r2.streamer.parser.PubBox - -@Suppress("DEPRECATION") -object EPUBConstant { - - @Deprecated("Use [MediaType.EPUB.toString()] instead", replaceWith = ReplaceWith("MediaType.EPUB.toString()")) - val mimetype: String get() = MediaType.EPUB.toString() - - internal val ltrPreset: MutableMap<ReadiumCSSName, Boolean> = mutableMapOf( - ReadiumCSSName.ref("hyphens") to false, - ReadiumCSSName.ref("ligatures") to false - ) - - internal val rtlPreset: MutableMap<ReadiumCSSName, Boolean> = mutableMapOf( - ReadiumCSSName.ref("hyphens") to false, - ReadiumCSSName.ref("wordSpacing") to false, - ReadiumCSSName.ref("letterSpacing") to false, - ReadiumCSSName.ref("ligatures") to true - ) - - internal val cjkHorizontalPreset: MutableMap<ReadiumCSSName, Boolean> = mutableMapOf( - ReadiumCSSName.ref("textAlignment") to false, - ReadiumCSSName.ref("hyphens") to false, - ReadiumCSSName.ref("paraIndent") to false, - ReadiumCSSName.ref("wordSpacing") to false, - ReadiumCSSName.ref("letterSpacing") to false - ) - - internal val cjkVerticalPreset: MutableMap<ReadiumCSSName, Boolean> = mutableMapOf( - ReadiumCSSName.ref("scroll") to true, - ReadiumCSSName.ref("columnCount") to false, - ReadiumCSSName.ref("textAlignment") to false, - ReadiumCSSName.ref("hyphens") to false, - ReadiumCSSName.ref("paraIndent") to false, - ReadiumCSSName.ref("wordSpacing") to false, - ReadiumCSSName.ref("letterSpacing") to false - ) - - val forceScrollPreset: MutableMap<ReadiumCSSName, Boolean> = mutableMapOf( - ReadiumCSSName.ref("scroll") to true - ) -} +import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.streamer.parser.PublicationParser +import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref /** * Parses a Publication from an EPUB publication. @@ -85,125 +43,132 @@ object EPUBConstant { * @param reflowablePositionsStrategy Strategy used to calculate the number of positions in a * reflowable resource. */ -class EpubParser( +@OptIn(ExperimentalReadiumApi::class) +public class EpubParser( private val reflowablePositionsStrategy: EpubPositionsService.ReflowableStrategy = EpubPositionsService.ReflowableStrategy.recommended -) : PublicationParser, org.readium.r2.streamer.parser.PublicationParser { - - override suspend fun parse(asset: PublicationAsset, fetcher: Fetcher, warnings: WarningLogger?): Publication.Builder? = - _parse(asset, fetcher, asset.name) - - @OptIn(Search::class, ExperimentalReadiumApi::class) - suspend fun _parse(asset: PublicationAsset, fetcher: Fetcher, fallbackTitle: String): Publication.Builder? { - - if (asset.mediaType() != MediaType.EPUB) - return null +) : PublicationParser { + + override suspend fun parse( + asset: Asset, + warnings: WarningLogger? + ): Try<Publication.Builder, PublicationParser.ParseError> { + if (asset !is ContainerAsset || !asset.format.conformsTo(EpubSpecification)) { + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) + } - val opfPath = getRootFilePath(fetcher).addPrefix("/") - val opfXmlDocument = fetcher.get(opfPath).readAsXml().getOrThrow() + val opfPath = getRootFilePath(asset.container) + .getOrElse { return Try.failure(it) } + val opfResource = asset.container[opfPath] + ?: return Try.failure( + PublicationParser.ParseError.Reading( + ReadError.Decoding( + DebugError("Missing OPF file.") + ) + ) + ) + val opfXmlDocument = opfResource.use { resource -> + resource.readDecodeOrElse( + decode = { it.decodeXml() }, + recover = { return Try.failure(PublicationParser.ParseError.Reading(it)) } + ) + } val packageDocument = PackageDocument.parse(opfXmlDocument, opfPath) - ?: throw Exception("Invalid OPF file.") + ?: return Try.failure( + PublicationParser.ParseError.Reading( + ReadError.Decoding( + DebugError("Invalid OPF file.") + ) + ) + ) + + val encryptionData = parseEncryptionData(asset.container) val manifest = ManifestAdapter( - fallbackTitle = fallbackTitle, packageDocument = packageDocument, - navigationData = parseNavigationData(packageDocument, fetcher), - encryptionData = parseEncryptionData(fetcher), - displayOptions = parseDisplayOptions(fetcher) + navigationData = parseNavigationData(packageDocument, asset.container), + encryptionData = encryptionData, + displayOptions = parseDisplayOptions(asset.container) ).adapt() - @Suppress("NAME_SHADOWING") - var fetcher = fetcher - manifest.metadata.identifier?.let { - fetcher = TransformingFetcher(fetcher, EpubDeobfuscator(it)::transform) + var container = asset.container + manifest.metadata.identifier?.let { id -> + val deobfuscator = EpubDeobfuscator(id, encryptionData) + container = TransformingContainer(container, deobfuscator::transform) } - return Publication.Builder( + val builder = Publication.Builder( manifest = manifest, - fetcher = fetcher, + container = container, servicesBuilder = Publication.ServicesBuilder( positions = EpubPositionsService.createFactory(reflowablePositionsStrategy), search = StringSearchService.createDefaultFactory(), content = DefaultContentService.createFactory( - listOf( - HtmlResourceContentIterator.createFactory() + resourceContentIteratorFactories = listOf( + HtmlResourceContentIterator.Factory() ) - ), + ) ) ) - } - - override fun parse( - fileAtPath: String, - fallbackTitle: String - ): PubBox? = runBlocking { - val file = File(fileAtPath) - val asset = FileAsset(file) - - var fetcher = Fetcher.fromArchiveOrDirectory(fileAtPath) - ?: throw ContainerError.missingFile(fileAtPath) - - val drm = if (fetcher.isProtectedWithLcp()) DRM(DRM.Brand.lcp) else null - if (drm?.brand == DRM.Brand.lcp) { - fetcher = TransformingFetcher(fetcher, LcpDecryptor(drm)::transform) - } - - val builder = try { - _parse(asset, fetcher, fallbackTitle) - } catch (e: Exception) { - return@runBlocking null - } ?: return@runBlocking null - - val publication = builder.build() - .apply { - @Suppress("DEPRECATION") - type = Publication.TYPE.EPUB - - // This might need to be moved as it's not really about parsing the EPUB but it - // sets values needed (in UserSettings & ContentFilter) - setLayoutStyle() - } + return Try.success(builder) + } - val container = PublicationContainer( - publication = publication, - path = file.canonicalPath, - mediaType = MediaType.EPUB, - drm = drm - ).apply { - rootFile.rootFilePath = getRootFilePath(fetcher) - } + private suspend fun getRootFilePath(container: Container<Resource>): Try<Url, PublicationParser.ParseError> { + val containerXmlUrl = Url("META-INF/container.xml")!! - PubBox(publication, container) - } + val containerXmlResource = container[containerXmlUrl] + ?: return Try.failure( + PublicationParser.ParseError.Reading( + ReadError.Decoding("container.xml not found.") + ) + ) - private suspend fun getRootFilePath(fetcher: Fetcher): String = - fetcher.readAsXmlOrNull("/META-INF/container.xml") - ?.getFirst("rootfiles", Namespaces.OPC) + return containerXmlResource + .readDecodeOrElse( + decode = { it.decodeXml() }, + recover = { return Try.failure(PublicationParser.ParseError.Reading(it)) } + ) + .getFirst("rootfiles", Namespaces.OPC) ?.getFirst("rootfile", Namespaces.OPC) ?.getAttr("full-path") - ?: throw Exception("Unable to find an OPF file.") + ?.let { Url.fromEpubHref(it) } + ?.let { Try.success(it) } + ?: Try.failure( + PublicationParser.ParseError.Reading( + ReadError.Decoding("Cannot successfully parse OPF.") + ) + ) + } - private suspend fun parseEncryptionData(fetcher: Fetcher): Map<String, Encryption> = - fetcher.readAsXmlOrNull("/META-INF/encryption.xml") - ?.let { EncryptionParser.parse(it) } + private suspend fun parseEncryptionData(container: Container<Resource>): Map<Url, Encryption> = + container.readDecodeXmlOrNull(path = "META-INF/encryption.xml") + ?.let { EpubEncryptionParser.parse(it) } ?: emptyMap() - private suspend fun parseNavigationData(packageDocument: PackageDocument, fetcher: Fetcher): Map<String, List<Link>> = - parseNavigationDocument(packageDocument, fetcher) - ?: parseNcx(packageDocument, fetcher) + private suspend fun parseNavigationData( + packageDocument: PackageDocument, + container: Container<Resource> + ): Map<String, List<Link>> = + parseNavigationDocument(packageDocument, container) + ?: parseNcx(packageDocument, container) ?: emptyMap() - private suspend fun parseNavigationDocument(packageDocument: PackageDocument, fetcher: Fetcher): Map<String, List<Link>>? = + private suspend fun parseNavigationDocument( + packageDocument: PackageDocument, + container: Container<Resource> + ): Map<String, List<Link>>? = packageDocument.manifest .firstOrNull { it.properties.contains(Vocabularies.ITEM + "nav") } ?.let { navItem -> - val navPath = Href(navItem.href, baseHref = packageDocument.path).string - fetcher.readAsXmlOrNull(navPath) - ?.let { NavigationDocumentParser.parse(it, navPath) } + container.readDecodeXmlOrNull(navItem.href) + ?.let { NavigationDocumentParser.parse(it, navItem.href) } } ?.takeUnless { it.isEmpty() } - private suspend fun parseNcx(packageDocument: PackageDocument, fetcher: Fetcher): Map<String, List<Link>>? { + private suspend fun parseNcx( + packageDocument: PackageDocument, + container: Container<Resource> + ): Map<String, List<Link>>? { val ncxItem = if (packageDocument.spine.toc != null) { packageDocument.manifest.firstOrNull { it.id == packageDocument.spine.toc } @@ -212,17 +177,17 @@ class EpubParser( } return ncxItem - ?.let { - val ncxPath = Href(ncxItem.href, baseHref = packageDocument.path).string - fetcher.readAsXmlOrNull(ncxPath)?.let { NcxParser.parse(it, ncxPath) } + ?.let { item -> + container.readDecodeXmlOrNull(item.href) + ?.let { NcxParser.parse(it, item.href) } } ?.takeUnless { it.isEmpty() } } - private suspend fun parseDisplayOptions(fetcher: Fetcher): Map<String, String> { + private suspend fun parseDisplayOptions(container: Container<Resource>): Map<String, String> { val displayOptionsXml = - fetcher.readAsXmlOrNull("/META-INF/com.apple.ibooks.display-options.xml") - ?: fetcher.readAsXmlOrNull("/META-INF/com.kobobooks.display-options.xml") + container.readDecodeXmlOrNull("META-INF/com.apple.ibooks.display-options.xml") + ?: container.readDecodeXmlOrNull("META-INF/com.kobobooks.display-options.xml") return displayOptionsXml?.getFirst("platform", "") ?.get("option", "") @@ -234,26 +199,27 @@ class EpubParser( ?.toMap().orEmpty() } - @Deprecated("This is done automatically in [parse], you can remove the call to [fillEncryption]", ReplaceWith("")) - @Suppress("Unused_parameter") - fun fillEncryption(container: Container, publication: Publication, drm: DRM?): Pair<Container, Publication> { - return Pair(container, publication) - } -} - -@Suppress("DEPRECATION") -internal fun Publication.setLayoutStyle() { - val layout = ReadiumCssLayout(metadata) + public suspend inline fun<R> Readable.readDecodeOrElse( + url: Url, + decode: (value: ByteArray) -> Try<R, DecodeError>, + recover: (ReadError) -> R + ): R = + readDecodeOrElse(decode, recover) { + recover( + ReadError.Decoding( + DebugError("Couldn't decode resource at $url", it.cause) + ) + ) + } - cssStyle = layout.cssId + private suspend inline fun Container<Readable>.readDecodeXmlOrNull( + path: String + ): ElementNode? = + Url.fromDecodedPath(path)?.let { url -> readDecodeXmlOrNull(url) } - userSettingsUIPreset = when (layout) { - ReadiumCssLayout.RTL -> EPUBConstant.rtlPreset - ReadiumCssLayout.LTR -> EPUBConstant.ltrPreset - ReadiumCssLayout.CJK_VERTICAL -> EPUBConstant.cjkVerticalPreset - ReadiumCssLayout.CJK_HORIZONTAL -> EPUBConstant.cjkHorizontalPreset - } + /** Returns the resource data as an XML Document at the given [url], or null. */ + private suspend inline fun Container<Readable>.readDecodeXmlOrNull( + url: Url + ): ElementNode? = + readDecodeOrNull(url) { it.decodeXml() } } - -private suspend fun Fetcher.isProtectedWithLcp(): Boolean = - get("/META-INF/license.lcpl").use { it.length().isSuccess } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt index 91fdaa6cdd..3fb4c54b8a 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt @@ -7,22 +7,24 @@ package org.readium.r2.streamer.parser.epub import kotlin.math.ceil -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.fetcher.Resource import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.archive.archive import org.readium.r2.shared.publication.encryption.encryption import org.readium.r2.shared.publication.epub.EpubLayout import org.readium.r2.shared.publication.epub.layoutOf import org.readium.r2.shared.publication.presentation.Presentation import org.readium.r2.shared.publication.presentation.presentation import org.readium.r2.shared.publication.services.PositionsService +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.archive.archive +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use /** - * Positions Service for an EPUB from its [readingOrder] and [fetcher]. + * Positions Service for an EPUB from its [readingOrder] and [container]. * * The [presentation] is used to apply different calculation strategy if the resource has a * reflowable or fixed layout. @@ -30,23 +32,25 @@ import org.readium.r2.shared.util.use * https://github.com/readium/architecture/blob/master/models/locators/best-practices/format.md#epub * https://github.com/readium/architecture/issues/101 */ -class EpubPositionsService( +public class EpubPositionsService( private val readingOrder: List<Link>, private val presentation: Presentation, - private val fetcher: Fetcher, + private val container: Container<Resource>, private val reflowableStrategy: ReflowableStrategy ) : PositionsService { - companion object { + public companion object { - fun createFactory(reflowableStrategy: ReflowableStrategy = ReflowableStrategy.recommended): ( + public fun createFactory( + reflowableStrategy: ReflowableStrategy = ReflowableStrategy.recommended + ): ( Publication.Service.Context ) -> EpubPositionsService = { context -> EpubPositionsService( readingOrder = context.manifest.readingOrder, presentation = context.manifest.metadata.presentation, - fetcher = context.fetcher, + container = context.container, reflowableStrategy = reflowableStrategy ) } @@ -57,17 +61,17 @@ class EpubPositionsService( * * Note that a fixed-layout resource always has a single position. */ - sealed class ReflowableStrategy { + public sealed class ReflowableStrategy { /** Returns the number of positions in the given [resource] according to the strategy. */ - abstract suspend fun positionCount(resource: Resource): Int + public abstract suspend fun positionCount(link: Link, resource: Resource): Int /** * Use the original length of each resource (before compression and encryption) and split it * by the given [pageLength]. */ - data class OriginalLength(val pageLength: Int) : ReflowableStrategy() { - override suspend fun positionCount(resource: Resource): Int { - val length = resource.link().properties.encryption?.originalLength + public data class OriginalLength(val pageLength: Int) : ReflowableStrategy() { + override suspend fun positionCount(link: Link, resource: Resource): Int { + val length = link.properties.encryption?.originalLength ?: resource.length().getOrNull() ?: 0 return ceil(length.toDouble() / pageLength.toDouble()).toInt() @@ -79,9 +83,9 @@ class EpubPositionsService( * Use the archive entry length (whether it is compressed or stored) and split it by the * given [pageLength]. */ - data class ArchiveEntryLength(val pageLength: Int) : ReflowableStrategy() { - override suspend fun positionCount(resource: Resource): Int { - val length = resource.link().properties.archive?.entryLength + public data class ArchiveEntryLength(val pageLength: Int) : ReflowableStrategy() { + override suspend fun positionCount(link: Link, resource: Resource): Int { + val length = resource.properties().getOrNull()?.archive?.entryLength ?: resource.length().getOrNull() ?: 0 return ceil(length.toDouble() / pageLength.toDouble()).toInt() @@ -89,20 +93,21 @@ class EpubPositionsService( } } - companion object { + public companion object { /** * Recommended historical strategy: archive entry length split by 1024 bytes pages. * * This strategy is used by Adobe RMSDK as well. * See https://github.com/readium/architecture/issues/123 */ - val recommended = ArchiveEntryLength(pageLength = 1024) + public val recommended: ReflowableStrategy = ArchiveEntryLength(pageLength = 1024) } } override suspend fun positionsByReadingOrder(): List<List<Locator>> { - if (!::_positions.isInitialized) + if (!::_positions.isInitialized) { _positions = computePositions() + } return _positions } @@ -116,7 +121,9 @@ class EpubPositionsService( if (presentation.layoutOf(link) == EpubLayout.FIXED) { createFixed(link, lastPositionOfPreviousResource) } else { - createReflowable(link, lastPositionOfPreviousResource, fetcher) + container.get(link.url()) + ?.use { createReflowable(link, lastPositionOfPreviousResource, it) } + ?: emptyList() } positions.lastOrNull()?.locations?.position?.let { @@ -127,7 +134,7 @@ class EpubPositionsService( } // Calculates [totalProgression]. - val totalPageCount = positions.map { it.size }.sum() + val totalPageCount = positions.sumOf { it.size } positions = positions.map { item -> item.map { locator -> val position = locator.locations.position @@ -144,35 +151,48 @@ class EpubPositionsService( return positions } - private fun createFixed(link: Link, startPosition: Int) = listOf( - createLocator( - link, - progression = 0.0, - position = startPosition + 1 + private fun createFixed(link: Link, startPosition: Int): List<Locator> = + listOf( + createLocator( + href = link.url(), + type = link.mediaType, + title = link.title, + progression = 0.0, + position = startPosition + 1 + ) ) - ) - private suspend fun createReflowable(link: Link, startPosition: Int, fetcher: Fetcher): List<Locator> { - val positionCount = fetcher.get(link).use { resource -> - reflowableStrategy.positionCount(resource) - } + private suspend fun createReflowable(link: Link, startPosition: Int, resource: Resource): List<Locator> { + val href = link.url() + + val positionCount = + reflowableStrategy.positionCount(link, resource) - return (1..positionCount).map { position -> + return (1..positionCount).mapNotNull { position -> createLocator( - link, + href = href, + type = link.mediaType, + title = link.title, progression = (position - 1) / positionCount.toDouble(), position = startPosition + position ) } } - private fun createLocator(link: Link, progression: Double, position: Int) = Locator( - href = link.href, - type = link.type ?: "text/html", - title = link.title, - locations = Locator.Locations( - progression = progression, - position = position + private fun createLocator( + href: Url, + type: MediaType?, + title: String?, + progression: Double, + position: Int + ): Locator = + Locator( + href = href, + mediaType = type ?: MediaType.XHTML, + title = title, + locations = Locator.Locations( + progression = progression, + position = position + ) ) - ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ManifestAdapter.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ManifestAdapter.kt index 8e94f84b87..fe1fe9012c 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ManifestAdapter.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ManifestAdapter.kt @@ -10,6 +10,7 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.PublicationCollection import org.readium.r2.shared.publication.encryption.Encryption +import org.readium.r2.shared.util.Url /** * Creates a [Manifest] model from an EPUB package's document. @@ -18,10 +19,9 @@ import org.readium.r2.shared.publication.encryption.Encryption * See https://github.com/readium/architecture/blob/master/streamer/parser/metadata.md#epub-2x-9 */ internal class ManifestAdapter( - private val fallbackTitle: String, private val packageDocument: PackageDocument, private val navigationData: Map<String, List<Link>> = emptyMap(), - private val encryptionData: Map<String, Encryption> = emptyMap(), + private val encryptionData: Map<Url, Encryption> = emptyMap(), private val displayOptions: Map<String, String> = emptyMap() ) { private val epubVersion = packageDocument.epubVersion @@ -31,7 +31,6 @@ internal class ManifestAdapter( // Compute metadata val metadata = MetadataAdapter( epubVersion, - fallbackTitle, packageDocument.uniqueIdentifierId, spine.direction, displayOptions @@ -39,12 +38,11 @@ internal class ManifestAdapter( // Compute links val (readingOrder, resources) = ResourceAdapter( - epubVersion, packageDocument.spine, packageDocument.manifest, encryptionData, metadata.coverId, - metadata.durationById, + metadata.durationById ).adapt() // Compute toc and otherCollections diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataAdapter.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataAdapter.kt index da18d18c2c..df199727aa 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataAdapter.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataAdapter.kt @@ -14,9 +14,8 @@ import org.readium.r2.shared.publication.presentation.Presentation internal class MetadataAdapter( private val epubVersion: Double, - private val fallbackTitle: String, private val uniqueIdentifierId: String?, - private val readingProgression: ReadingProgression, + private val readingProgression: ReadingProgression?, private val displayOptions: Map<String, String> ) { data class Result( @@ -65,7 +64,7 @@ internal class MetadataAdapter( ?.value val (localizedTitle, localizedSortAs, localizedSubtitle) = globalItemsHolder - .adapt(TitleAdapter(fallbackTitle)::adapt) + .adapt(TitleAdapter()::adapt) val (belongsToCollections, belongsToSeries) = globalItemsHolder .adapt(CollectionAdapter()::adapt) @@ -149,14 +148,16 @@ private class LinksAdapter { private fun mapLink(link: MetadataItem.Link): Link { val contains: MutableList<String> = mutableListOf() if (link.rels.contains(Vocabularies.LINK + "record")) { - if (link.properties.contains(Vocabularies.LINK + "onix")) + if (link.properties.contains(Vocabularies.LINK + "onix")) { contains.add("onix") - if (link.properties.contains(Vocabularies.LINK + "xmp")) + } + if (link.properties.contains(Vocabularies.LINK + "xmp")) { contains.add("xmp") + } } return Link( href = link.href, - type = link.mediaType, + mediaType = link.mediaType, rels = link.rels, properties = Properties(mapOf("contains" to contains)) ) @@ -187,10 +188,10 @@ private class LanguageAdapter { .mapFirst { it.map(MetadataItem.Meta::value) } } -private class TitleAdapter(private val fallbackTitle: String) { +private class TitleAdapter() { data class Result( - val localizedTitle: LocalizedString, + val localizedTitle: LocalizedString?, val localizedSortAs: LocalizedString?, val localizedSubtitle: LocalizedString? ) @@ -205,7 +206,6 @@ private class TitleAdapter(private val fallbackTitle: String) { val mainTitleItem = mainTitleWithItem?.second val localizedTitle = mainTitle?.value - ?: LocalizedString(fallbackTitle) val localizedSortAs = mainTitle?.fileAs ?: items.firstWithProperty("calibre:title_sort") ?.let { LocalizedString(it.value) } @@ -221,7 +221,11 @@ private class TitleAdapter(private val fallbackTitle: String) { .removeFirstOrNull { it == mainTitleItem }.second .removeFirstOrNull { it == subtitleItem }.second - return Result(localizedTitle, localizedSortAs, localizedSubtitle) to remainingItems + return Result( + localizedTitle = localizedTitle, + localizedSortAs = localizedSortAs, + localizedSubtitle = localizedSubtitle + ) to remainingItems } } @@ -294,8 +298,11 @@ private fun MetadataItem.Meta.toContributor(): Pair<String?, Contributor> { } val contributor = Contributor( - localizedString, localizedSortAs = localizedSortAs, - roles = roles, identifier = identifier, position = groupPosition + localizedString, + localizedSortAs = localizedSortAs, + roles = roles, + identifier = identifier, + position = groupPosition ) return Pair(type, contributor) @@ -357,15 +364,15 @@ private class OtherMetadataAdapter { } private fun MetadataItem.Meta.toMap(): Any = - if (children.isEmpty()) + if (children.isEmpty()) { value - else { + } else { val mappedMetaChildren = children .filterIsInstance(MetadataItem.Meta::class.java) .associate { Pair(it.property, it.toMap()) } val mappedLinkChildren = children .filterIsInstance(MetadataItem.Link::class.java) - .flatMap { link -> link.rels.map { rel -> Pair(rel, link.href) } } + .flatMap { link -> link.rels.map { rel -> Pair(rel, link.url()) } } .toMap() mappedMetaChildren + mappedLinkChildren + Pair("@value", value) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataParser.kt index bfddcbd072..dd00ed2680 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataParser.kt @@ -6,91 +6,140 @@ package org.readium.r2.streamer.parser.epub -import org.readium.r2.shared.parser.xml.ElementNode -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.publication.Href +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref internal class MetadataParser( - private val epubVersion: Double, private val prefixMap: Map<String, String> ) { - fun parse(document: ElementNode, filePath: String): List<MetadataItem>? { + fun parse(document: ElementNode, filePath: Url): List<MetadataItem>? { val metadata = document.getFirst("metadata", Namespaces.OPF) ?: return null val items = parseElements(metadata, filePath) return resolveItemsHierarchy(items) } - private fun parseElements(metadataElement: ElementNode, filePath: String): List<MetadataItem> = - metadataElement.getAll().mapNotNull { e -> - when { - e.namespace == Namespaces.DC -> - parseDcElement(e) - e.namespace == Namespaces.OPF && e.name == "meta" -> - parseMetaElement(e) - e.namespace == Namespaces.OPF && e.name == "link" -> - parseLinkElement(e, filePath) - else -> null + private fun parseElements(metadataElement: ElementNode, filePath: Url): List<MetadataItem> { + val oldMetas: MutableList<ElementNode> = mutableListOf() + val newMetas: MutableList<ElementNode> = mutableListOf() + val links: MutableList<ElementNode> = mutableListOf() + val dcItems: MutableList<ElementNode> = mutableListOf() + + metadataElement + .getAll() + .forEach { e -> + when { + e.namespace == Namespaces.DC -> { + dcItems.add(e) + } + e.namespace == Namespaces.OPF && e.name == "meta" -> { + if (e.getAttr("property") == null) { + oldMetas.add(e) + } else { + newMetas.add(e) + } + } + e.namespace == Namespaces.OPF && e.name == "link" -> { + links.add(e) + } + else -> {} + } } - } - private fun parseLinkElement(element: ElementNode, filePath: String): MetadataItem.Link? { - val href = element.getAttr("href") ?: return null + val parsedNewMetas = newMetas + .mapNotNull { parseNewMetaElement(it) } + + val propertiesFromGlobalNewMetas = parsedNewMetas + .filter { it.refines == null } + .map { it.property } + .toSet() + + val parsedOldMetas = oldMetas + .mapNotNull { parseOldMetaElement(it) } + // Ignore EPUB2 fallbacks in EPUB3 + .filter { it.property !in propertiesFromGlobalNewMetas } + + return parsedNewMetas + parsedOldMetas + + dcItems.mapNotNull { parseDcElement(it) } + + links.mapNotNull { parseLinkElement(it, filePath) } + } + + private fun parseLinkElement(element: ElementNode, filePath: Url): MetadataItem.Link? { + val href = element.getAttr("href")?.let { Url.fromEpubHref(it) } ?: return null val relAttr = element.getAttr("rel").orEmpty() val rel = parseProperties(relAttr).map { resolveProperty(it, prefixMap, DEFAULT_VOCAB.LINK) } val propAttr = element.getAttr("properties").orEmpty() - val properties = parseProperties(propAttr).map { resolveProperty(it, prefixMap, DEFAULT_VOCAB.LINK) } + val properties = parseProperties(propAttr).map { + resolveProperty( + it, + prefixMap, + DEFAULT_VOCAB.LINK + ) + } val mediaType = element.getAttr("media-type") val refines = element.getAttr("refines")?.removePrefix("#") return MetadataItem.Link( id = element.id, refines = refines, - href = Href(href, baseHref = filePath).string, + href = Href(filePath.resolve(href)), rels = rel.toSet(), - mediaType = mediaType, + mediaType = mediaType?.let { MediaType(it) }, properties = properties ) } - private fun parseMetaElement(element: ElementNode): MetadataItem.Meta? { - return if (element.getAttr("property") == null) { - val name = element.getAttr("name")?.trim()?.ifEmpty { null } - ?: return null - val content = element.getAttr("content")?.trim()?.ifEmpty { null } - ?: return null - val resolvedName = resolveProperty(name, prefixMap) - MetadataItem.Meta( - id = element.id, - refines = null, - property = resolvedName, - value = content, - lang = element.lang - ) - } else { - val propName = element.getAttr("property")?.trim()?.ifEmpty { null } - ?: return null - val propValue = element.text?.trim()?.ifEmpty { null } - ?: return null - val resolvedProp = resolveProperty(propName, prefixMap, DEFAULT_VOCAB.META) - val resolvedScheme = - element.getAttr("scheme")?.trim()?.ifEmpty { null }?.let { resolveProperty(it, prefixMap) } - val refines = element.getAttr("refines")?.removePrefix("#") - MetadataItem.Meta( - id = element.id, - refines = refines, - property = resolvedProp, - value = propValue, - lang = element.lang, - scheme = resolvedScheme - ) - } + private fun parseNewMetaElement(element: ElementNode): MetadataItem.Meta? { + val propName = element.getAttr("property")?.trim()?.ifEmpty { null } + ?: return null + val propValue = element.text?.trim()?.ifEmpty { null } + ?: return null + val resolvedProp = resolveProperty(propName, prefixMap, DEFAULT_VOCAB.META) + val resolvedScheme = + element.getAttr("scheme")?.trim()?.ifEmpty { null }?.let { + resolveProperty( + it, + prefixMap + ) + } + val refines = element.getAttr("refines")?.removePrefix("#") + return MetadataItem.Meta( + id = element.id, + refines = refines, + property = resolvedProp, + value = propValue, + lang = element.lang, + scheme = resolvedScheme + ) + } + + private fun parseOldMetaElement(element: ElementNode): MetadataItem.Meta? { + val name = element.getAttr("name")?.trim()?.ifEmpty { null } + ?: return null + val content = element.getAttr("content")?.trim()?.ifEmpty { null } + ?: return null + val resolvedName = resolveProperty(name, prefixMap) + return MetadataItem.Meta( + id = element.id, + refines = null, + property = resolvedName, + value = content, + lang = element.lang + ) } private fun parseDcElement(element: ElementNode): MetadataItem.Meta? { val propValue = element.text?.trim()?.ifEmpty { null } ?: return null val propName = Vocabularies.DCTERMS + element.name return when (element.name) { - "creator", "contributor", "publisher" -> contributorWithLegacyAttr(element, propName, propValue) + "creator", "contributor", "publisher" -> contributorWithLegacyAttr( + element, + propName, + propValue + ) "date" -> dateWithLegacyAttr(element, propName, propValue) else -> MetadataItem.Meta( id = element.id, @@ -141,6 +190,7 @@ internal class MetadataParser( private fun resolveItemsHierarchy(items: List<MetadataItem>): List<MetadataItem> { val metadataIds = items.mapNotNull { it.id } val rootExpr = items.filter { it.refines == null || it.refines !in metadataIds } + @Suppress("Unchecked_cast") val exprByRefines = items.groupBy(MetadataItem::refines) as Map<String, List<MetadataItem.Meta>> return rootExpr.map { computeMetadataItem(it, exprByRefines, emptySet()) } @@ -173,11 +223,13 @@ internal sealed class MetadataItem { override val id: String?, override val refines: String?, override val children: List<MetadataItem> = emptyList(), - val href: String, + val href: Href, val rels: Set<String>, - val mediaType: String?, - val properties: List<String> = emptyList(), - ) : MetadataItem() + val mediaType: MediaType?, + val properties: List<String> = emptyList() + ) : MetadataItem() { + fun url(): Url = href.resolve() + } data class Meta( override val id: String?, @@ -186,6 +238,6 @@ internal sealed class MetadataItem { val property: String, val value: String, val lang: String, - val scheme: String? = null, + val scheme: String? = null ) : MetadataItem() } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt index 53fc3f5b10..aa1e51054d 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt @@ -6,19 +6,26 @@ package org.readium.r2.streamer.parser.epub -import org.readium.r2.shared.parser.xml.ElementNode import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref internal object NavigationDocumentParser { - fun parse(document: ElementNode, filePath: String): Map<String, List<Link>> { + fun parse(document: ElementNode, filePath: Url): Map<String, List<Link>> { val docPrefixes = document.getAttrNs("prefix", Namespaces.OPS) ?.let { parsePrefixes(it) }.orEmpty() val prefixMap = CONTENT_RESERVED_PREFIXES + docPrefixes // prefix element overrides reserved prefixes val body = document.getFirst("body", Namespaces.XHTML) ?: return emptyMap() - val navs = body.collect("nav", Namespaces.XHTML).mapNotNull { parseNavElement(it, filePath, prefixMap) } + val navs = body.collect("nav", Namespaces.XHTML).mapNotNull { + parseNavElement( + it, + filePath, + prefixMap + ) + } val navMap = navs.flatMap { nav -> nav.first.map { type -> Pair(type, nav.second) } }.toMap() @@ -30,26 +37,43 @@ internal object NavigationDocumentParser { private fun parseNavElement( nav: ElementNode, - filePath: String, + filePath: Url, prefixMap: Map<String, String> ): Pair<List<String>, List<Link>>? { val typeAttr = nav.getAttrNs("type", Namespaces.OPS) ?: return null - val types = parseProperties(typeAttr).mapNotNull { resolveProperty(it, prefixMap, DEFAULT_VOCAB.TYPE) } + val types = parseProperties(typeAttr).map { + resolveProperty( + it, + prefixMap, + DEFAULT_VOCAB.TYPE + ) + } val links = nav.getFirst("ol", Namespaces.XHTML)?.let { parseOlElement(it, filePath) } return if (types.isNotEmpty() && !links.isNullOrEmpty()) Pair(types, links) else null } - private fun parseOlElement(element: ElementNode, filePath: String): List<Link> = + private fun parseOlElement(element: ElementNode, filePath: Url): List<Link> = element.get("li", Namespaces.XHTML).mapNotNull { parseLiElement(it, filePath) } - private fun parseLiElement(element: ElementNode, filePath: String): Link? { + private fun parseLiElement(element: ElementNode, filePath: Url): Link? { val first = element.getAll().firstOrNull() ?: return null // should be <a>, <span>, or <ol> - val title = if (first.name == "ol") "" else first.collectText().replace("\\s+".toRegex(), " ").trim() - val rawHref = first.getAttr("href") - val href = if (first.name == "a" && !rawHref.isNullOrBlank()) Href(rawHref, baseHref = filePath).string else "#" + val title = if (first.name == "ol") { + "" + } else { + first.collectText().replace( + "\\s+".toRegex(), + " " + ).trim() + } + val rawHref = first.getAttr("href")?.let { Url.fromEpubHref(it) } + val href = if (first.name == "a" && rawHref != null) { + filePath.resolve(rawHref) + } else { + Url("#")!! + } val children = element.getFirst("ol", Namespaces.XHTML)?.let { parseOlElement(it, filePath) }.orEmpty() - return if (children.isEmpty() && (href == "#" || title == "")) { + return if (children.isEmpty() && (href.toString() == "#" || title == "")) { null } else { Link( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NcxParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NcxParser.kt index 5e76b46381..a0db885df4 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NcxParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NcxParser.kt @@ -6,13 +6,14 @@ package org.readium.r2.streamer.parser.epub -import org.readium.r2.shared.parser.xml.ElementNode import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref internal object NcxParser { - fun parse(document: ElementNode, filePath: String): Map<String, List<Link>> { + fun parse(document: ElementNode, filePath: Url): Map<String, List<Link>> { val toc = document.getFirst("navMap", Namespaces.NCX) ?.let { parseNavMapElement(it, filePath) }?.let { Pair("toc", it) } val pageList = document.getFirst("pageList", Namespaces.NCX) @@ -20,33 +21,47 @@ internal object NcxParser { return listOfNotNull(toc, pageList).toMap() } - private fun parseNavMapElement(element: ElementNode, filePath: String): List<Link> = + private fun parseNavMapElement(element: ElementNode, filePath: Url): List<Link> = element.get("navPoint", Namespaces.NCX).mapNotNull { parseNavPointElement(it, filePath) } - private fun parsePageListElement(element: ElementNode, filePath: String): List<Link> = + private fun parsePageListElement(element: ElementNode, filePath: Url): List<Link> = element.get("pageTarget", Namespaces.NCX).mapNotNull { val href = extractHref(it, filePath) val title = extractTitle(it) - if (href.isNullOrBlank() || title.isNullOrBlank()) + if (href == null || title.isNullOrBlank()) { null - else Link(title = title, href = href) + } else { + Link(title = title, href = href) + } } - private fun parseNavPointElement(element: ElementNode, filePath: String): Link? { + private fun parseNavPointElement(element: ElementNode, filePath: Url): Link? { val title = extractTitle(element) val href = extractHref(element, filePath) - val children = element.get("navPoint", Namespaces.NCX).mapNotNull { parseNavPointElement(it, filePath) } - return if (children.isEmpty() && (href == null || title == null)) + val children = element.get("navPoint", Namespaces.NCX).mapNotNull { + parseNavPointElement( + it, + filePath + ) + } + return if (children.isEmpty() && (href == null || title == null)) { null - else - Link(title = title, href = href ?: "#", children = children) + } else { + Link( + title = title, + href = href ?: Url("#")!!, + children = children + ) + } } private fun extractTitle(element: ElementNode) = element.getFirst("navLabel", Namespaces.NCX)?.getFirst("text", Namespaces.NCX) ?.text?.replace("\\s+".toRegex(), " ")?.trim()?.ifBlank { null } - private fun extractHref(element: ElementNode, filePath: String) = + private fun extractHref(element: ElementNode, filePath: Url) = element.getFirst("content", Namespaces.NCX)?.getAttr("src") - ?.ifBlank { null }?.let { Href(it, baseHref = filePath).string } + ?.ifBlank { null } + ?.let { Url.fromEpubHref(it) } + ?.let { filePath.resolve(it) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt index 84b1bdfd2d..0e3afb4dd7 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt @@ -6,12 +6,13 @@ package org.readium.r2.streamer.parser.epub -import org.readium.r2.shared.parser.xml.ElementNode import org.readium.r2.shared.publication.ReadingProgression -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref internal data class PackageDocument( - val path: String, + val path: Url, val epubVersion: Double, val uniqueIdentifierId: String?, val metadata: List<MetadataItem>, @@ -20,11 +21,11 @@ internal data class PackageDocument( ) { companion object { - fun parse(document: ElementNode, filePath: String): PackageDocument? { + fun parse(document: ElementNode, filePath: Url): PackageDocument? { val packagePrefixes = document.getAttr("prefix")?.let { parsePrefixes(it) }.orEmpty() val prefixMap = PACKAGE_RESERVED_PREFIXES + packagePrefixes // prefix element overrides reserved prefixes val epubVersion = document.getAttr("version")?.toDoubleOrNull() ?: 1.2 - val metadata = MetadataParser(epubVersion, prefixMap).parse(document, filePath) + val metadata = MetadataParser(prefixMap).parse(document, filePath) ?: return null val manifestElement = document.getFirst("manifest", Namespaces.OPF) ?: return null @@ -45,7 +46,7 @@ internal data class PackageDocument( } internal data class Item( - val href: String, + val href: Url, val id: String?, val fallback: String?, val mediaOverlay: String?, @@ -53,11 +54,19 @@ internal data class Item( val properties: List<String> ) { companion object { - fun parse(element: ElementNode, filePath: String, prefixMap: Map<String, String>): Item? { - val href = element.getAttr("href")?.let { Href(it, baseHref = filePath).string } + fun parse(element: ElementNode, filePath: Url, prefixMap: Map<String, String>): Item? { + val href = element.getAttr("href") + ?.let { Url.fromEpubHref(it) } + ?.let { filePath.resolve(it) } ?: return null val propAttr = element.getAttr("properties").orEmpty() - val properties = parseProperties(propAttr).mapNotNull { resolveProperty(it, prefixMap, DEFAULT_VOCAB.ITEM) } + val properties = parseProperties(propAttr).map { + resolveProperty( + it, + prefixMap, + DEFAULT_VOCAB.ITEM + ) + } return Item( href = href, id = element.id, @@ -72,16 +81,21 @@ internal data class Item( internal data class Spine( val itemrefs: List<Itemref>, - val direction: ReadingProgression, + val direction: ReadingProgression?, val toc: String? = null ) { companion object { fun parse(element: ElementNode, prefixMap: Map<String, String>, epubVersion: Double): Spine { - val itemrefs = element.get("itemref", Namespaces.OPF).mapNotNull { Itemref.parse(it, prefixMap) } + val itemrefs = element.get("itemref", Namespaces.OPF).mapNotNull { + Itemref.parse( + it, + prefixMap + ) + } val pageProgressionDirection = when (element.getAttr("page-progression-direction")) { "rtl" -> ReadingProgression.RTL "ltr" -> ReadingProgression.LTR - else -> ReadingProgression.AUTO // null or "default" + else -> null // null or "default" } val ncx = if (epubVersion < 3.0) element.getAttr("toc") else null return Spine(itemrefs, pageProgressionDirection, ncx) @@ -100,7 +114,7 @@ internal data class Itemref( val notLinear = element.getAttr("linear") == "no" val propAttr = element.getAttr("properties").orEmpty() val properties = parseProperties(propAttr) - .mapNotNull { resolveProperty(it, prefixMap, DEFAULT_VOCAB.ITEMREF) } + .map { resolveProperty(it, prefixMap, DEFAULT_VOCAB.ITEMREF) } return Itemref(idref, !notLinear, properties) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PresentationAdapter.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PresentationAdapter.kt index ba43a4f7ce..accdb8a286 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PresentationAdapter.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PresentationAdapter.kt @@ -31,13 +31,16 @@ internal class PresentationAdapter( val layoutProp = if (epubVersion < 3.0) { - if (displayOptions["fixed-layout"] == "true") + if (displayOptions["fixed-layout"] == "true") { "pre-paginated" - else + } else { "reflowable" - } else itemsHolder - .adapt { it.takeFirstWithProperty(Vocabularies.RENDITION + "layout") } - ?.value + } + } else { + itemsHolder + .adapt { it.takeFirstWithProperty(Vocabularies.RENDITION + "layout") } + ?.value + } val (overflow, continuous) = when (flowProp) { "paginated" -> Pair(Presentation.Overflow.PAGINATED, false) @@ -65,8 +68,11 @@ internal class PresentationAdapter( } val presentation = Presentation( - overflow = overflow, continuous = continuous, - layout = layout, orientation = orientation, spread = spread + overflow = overflow, + continuous = continuous, + layout = layout, + orientation = orientation, + spread = spread ) return presentation to itemsHolder.remainingItems diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PropertyDataType.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PropertyDataType.kt index 20ec765144..1fcf8ca4d2 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PropertyDataType.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PropertyDataType.kt @@ -40,8 +40,9 @@ internal fun resolveProperty( defaultVocab.iri + splitted[0] } else if (splitted.size == 2 && prefixMap[splitted[0]] != null) { prefixMap[splitted[0]] + splitted[1] - } else + } else { property + } } internal fun parsePrefixes(prefixes: String): Map<String, String> = diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ReadiumCssLayout.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ReadiumCssLayout.kt deleted file mode 100644 index 1269827764..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ReadiumCssLayout.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.streamer.parser.epub - -import org.readium.r2.shared.publication.Metadata -import org.readium.r2.shared.publication.ReadingProgression - -internal enum class ReadiumCssLayout(val cssId: String) { - // Right to left - RTL("rtl"), - // Left to right - LTR("ltr"), - // Asian language, vertically laid out - CJK_VERTICAL("cjk-vertical"), - // Asian language, horizontally laid out - CJK_HORIZONTAL("cjk-horizontal"); - - val readiumCSSPath: String get() = when (this) { - LTR -> "" - RTL -> "rtl/" - CJK_VERTICAL -> "cjk-vertical/" - CJK_HORIZONTAL -> "cjk-horizontal/" - } - - companion object { - - operator fun invoke(metadata: Metadata): ReadiumCssLayout = - @Suppress("Deprecation") - invoke(languages = metadata.languages, readingProgression = metadata.effectiveReadingProgression) - - /** - * Determines the [ReadiumCssLayout] for the given BCP 47 language codes and - * [readingProgression]. - * Defaults to [LTR]. - */ - operator fun invoke(languages: List<String>, readingProgression: ReadingProgression): ReadiumCssLayout { - val isCjk: Boolean = - if (languages.size == 1) { - val language = languages[0].split("-")[0] // Remove region - listOf("zh", "ja", "ko").contains(language) - } else { - false - } - - return when (readingProgression) { - ReadingProgression.RTL, ReadingProgression.BTT -> - if (isCjk) CJK_VERTICAL - else RTL - - ReadingProgression.LTR, ReadingProgression.TTB, ReadingProgression.AUTO -> - if (isCjk) CJK_HORIZONTAL - else LTR - } - } - } -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ResourceAdapter.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ResourceAdapter.kt index 85dda67f48..53efbea449 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ResourceAdapter.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ResourceAdapter.kt @@ -7,15 +7,17 @@ package org.readium.r2.streamer.parser.epub import org.readium.r2.shared.extensions.toMap +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Properties import org.readium.r2.shared.publication.encryption.Encryption +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType internal class ResourceAdapter( - private val epubVersion: Double, private val spine: Spine, private val manifest: List<Item>, - private val encryptionData: Map<String, Encryption>, + private val encryptionData: Map<Url, Encryption>, private val coverId: String?, private val durationById: Map<String, Double?> ) { @@ -34,21 +36,27 @@ internal class ResourceAdapter( fun adapt(): Links { val readingOrderIds = spine.itemrefs.filter { it.linear }.map { it.idref } - val readingOrder = readingOrderIds.mapNotNull { id -> itemById[id]?.let { item -> computeLink(item) } } + val readingOrder = readingOrderIds.mapNotNull { id -> + itemById[id]?.let { item -> + computeLink( + item + ) + } + } val readingOrderAllIds = computeIdsWithFallbacks(readingOrderIds) val resourceItems = manifest.filterNot { it.id in readingOrderAllIds } val resources = resourceItems.map { computeLink(it) } return Links(readingOrder, resources) } - /** Recursively find the ids of the fallback items in [items] */ + /** Recursively find the ids contained in fallback chains of items with [ids]. */ private fun computeIdsWithFallbacks(ids: List<String>): Set<String> { val fallbackIds: MutableSet<String> = mutableSetOf() ids.forEach { fallbackIds.addAll(computeFallbackChain(it)) } return fallbackIds } - /** Compute the ids contained in the fallback chain of [item] */ + /** Compute the ids contained in the fallback chain of item with [id]. */ private fun computeFallbackChain(id: String): Set<String> { // The termination has already been checked while computing links val ids: MutableSet<String> = mutableSetOf() @@ -63,8 +71,8 @@ internal class ResourceAdapter( val (rels, properties) = computePropertiesAndRels(item, itemrefByIdref[item.id]) return Link( - href = item.href, - type = item.mediaType, + href = Href(item.href), + mediaType = item.mediaType?.let { MediaType(it) }, duration = durationById[item.id], rels = rels, properties = properties, @@ -98,13 +106,15 @@ internal class ResourceAdapter( /** Compute alternate links for [item], checking for an infinite recursion */ private fun computeAlternates(item: Item, fallbackChain: Set<String>): List<Link> { - val fallback = item.fallback?.let { id -> - if (id in fallbackChain) null else + if (id in fallbackChain) { + null + } else { itemById[id]?.let { val updatedChain = if (item.id != null) fallbackChain + item.id else fallbackChain computeLink(it, updatedChain) } + } } val mediaOverlays = item.mediaOverlay?.let { id -> itemById[id]?.let { diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/SmilParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/SmilParser.kt index bbd1a4e5ce..460d8b8007 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/SmilParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/SmilParser.kt @@ -8,8 +8,9 @@ package org.readium.r2.streamer.parser.epub import org.readium.r2.shared.MediaOverlayNode import org.readium.r2.shared.MediaOverlays -import org.readium.r2.shared.parser.xml.ElementNode -import org.readium.r2.shared.util.Href +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref internal object SmilParser { /* According to https://www.w3.org/publishing/epub3/epub-mediaoverlays.html#sec-overlays-content-conf @@ -19,18 +20,19 @@ internal object SmilParser { one EPUB Content Document by means of its attribute epub:textref */ - fun parse(document: ElementNode, filePath: String): MediaOverlays? { + fun parse(document: ElementNode, filePath: Url): MediaOverlays? { val body = document.getFirst("body", Namespaces.SMIL) ?: return null return parseSeq(body, filePath)?.let { MediaOverlays(it) } } - private fun parseSeq(node: ElementNode, filePath: String): List<MediaOverlayNode>? { + private fun parseSeq(node: ElementNode, filePath: Url): List<MediaOverlayNode>? { val children: MutableList<MediaOverlayNode> = mutableListOf() for (child in node.getAll()) { - if (child.name == "par" && child.namespace == Namespaces.SMIL) + if (child.name == "par" && child.namespace == Namespaces.SMIL) { parsePar(child, filePath)?.let { children.add(it) } - else if (child.name == "seq" && child.namespace == Namespaces.SMIL) + } else if (child.name == "seq" && child.namespace == Namespaces.SMIL) { parseSeq(child, filePath)?.let { children.addAll(it) } + } } /* No wrapping media overlay can be created unless: @@ -38,31 +40,43 @@ internal object SmilParser { - the seq element has an textref attribute (this is mandatory according to the EPUB spec) */ val textref = node.getAttrNs("textref", Namespaces.OPS) + ?.let { Url.fromEpubHref(it) } val audioFiles = children.mapNotNull(MediaOverlayNode::audioFile) return if (textref != null && audioFiles.distinct().size == 1) { // hierarchy - val normalizedTextref = Href(textref, baseHref = filePath).string + val normalizedTextref = filePath.resolve(textref) listOf(mediaOverlayFromChildren(normalizedTextref, children)) - } else children + } else { + children + } } - private fun parsePar(node: ElementNode, filePath: String): MediaOverlayNode? { - val text = node.getFirst("text", Namespaces.SMIL)?.getAttr("src") ?: return null - val audio = node.getFirst("audio", Namespaces.SMIL)?.let { audioNode -> - val src = audioNode.getAttr("src") - val begin = audioNode.getAttr("clipBegin")?.let { ClockValueParser.parse(it) } ?: "" - val end = audioNode.getAttr("clipEnd")?.let { ClockValueParser.parse(it) } ?: "" - "$src#t=$begin,$end" - } - return MediaOverlayNode(Href(text, baseHref = filePath).string, Href(audio ?: "", baseHref = filePath).string) + private fun parsePar(node: ElementNode, filePath: Url): MediaOverlayNode? { + val text = node.getFirst("text", Namespaces.SMIL) + ?.getAttr("src") + ?.let { Url.fromEpubHref(it) } + ?: return null + val audio = node.getFirst("audio", Namespaces.SMIL) + ?.let { audioNode -> + val src = audioNode.getAttr("src") + val begin = audioNode.getAttr("clipBegin")?.let { ClockValueParser.parse(it) } ?: "" + val end = audioNode.getAttr("clipEnd")?.let { ClockValueParser.parse(it) } ?: "" + "$src#t=$begin,$end" + } + ?.let { Url.fromEpubHref(it) } + + return MediaOverlayNode( + filePath.resolve(text), + audio?.let { filePath.resolve(audio) } + ) } - private fun mediaOverlayFromChildren(text: String, children: List<MediaOverlayNode>): MediaOverlayNode { + private fun mediaOverlayFromChildren(text: Url, children: List<MediaOverlayNode>): MediaOverlayNode { require(children.isNotEmpty() && children.mapNotNull { it.audioFile }.distinct().size <= 1) val audioChildren = children.mapNotNull { if (it.audioFile != null) it else null } val file = audioChildren.first().audioFile val start = audioChildren.first().clip.start ?: "" val end = audioChildren.last().clip.end ?: "" - val audio = "$file#t=$start,$end" + val audio = Url.fromEpubHref("$file#t=$start,$end") return MediaOverlayNode(text, audio, children, listOf("section")) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/extensions/UrlExt.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/extensions/UrlExt.kt new file mode 100644 index 0000000000..4280496e76 --- /dev/null +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/extensions/UrlExt.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.streamer.parser.epub.extensions + +import org.readium.r2.shared.util.Url + +/** + * According to the EPUB specification, the HREFs in the EPUB package must be valid URLs (so + * percent-encoded). Unfortunately, many EPUBs don't follow this rule, and use invalid HREFs such + * as `my chapter.html` or `/dir/my chapter.html`. + * + * As a workaround, we assume the HREFs are valid percent-encoded URLs, and fallback to decoded paths + * if we can't parse the URL. + */ +internal fun Url.Companion.fromEpubHref(href: String): Url? = + Url(href) ?: fromDecodedPath(href) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index d23ff85ea0..9a31fcc2b7 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -1,25 +1,46 @@ /* - * Module: r2-streamer-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. */ package org.readium.r2.streamer.parser.image -import java.io.File -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.publication.* -import org.readium.r2.shared.publication.asset.PublicationAsset +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.publication.LocalizedString +import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.publication.Metadata +import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.PerResourcePositionsService +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.format.AvifSpecification +import org.readium.r2.shared.util.format.BmpSpecification +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.GifSpecification +import org.readium.r2.shared.util.format.InformalComicSpecification +import org.readium.r2.shared.util.format.JpegSpecification +import org.readium.r2.shared.util.format.JxlSpecification +import org.readium.r2.shared.util.format.PngSpecification +import org.readium.r2.shared.util.format.Specification +import org.readium.r2.shared.util.format.TiffSpecification +import org.readium.r2.shared.util.format.WebpSpecification +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.streamer.PublicationParser +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs -import org.readium.r2.streamer.extensions.lowercasedExtension +import org.readium.r2.streamer.extensions.sniffContainerEntries +import org.readium.r2.streamer.extensions.toContainer +import org.readium.r2.streamer.parser.PublicationParser /** * Parses an image–based Publication from an unstructured archive format containing bitmap files, @@ -27,26 +48,78 @@ import org.readium.r2.streamer.extensions.lowercasedExtension * * It can also work for a standalone bitmap file. */ -class ImageParser : PublicationParser { +public class ImageParser( + private val assetRetriever: AssetRetriever +) : PublicationParser { override suspend fun parse( - asset: PublicationAsset, - fetcher: Fetcher, + asset: Asset, warnings: WarningLogger? - ): Publication.Builder? { + ): Try<Publication.Builder, PublicationParser.ParseError> = + when (asset) { + is ResourceAsset -> parseResourceAsset(asset) + is ContainerAsset -> parseContainerAsset(asset) + } + + private fun parseResourceAsset( + asset: ResourceAsset + ): Try<Publication.Builder, PublicationParser.ParseError> { + if (!asset.format.conformsToAny(bitmapSpecifications)) { + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) + } + + val container = + asset.toContainer() + + val readingOrderWithFormat = + listOfNotNull(container.first() to asset.format) + + return finalizeParsing(container, readingOrderWithFormat, null) + } + + private suspend fun parseContainerAsset( + asset: ContainerAsset + ): Try<Publication.Builder, PublicationParser.ParseError> { + if (!asset.format.conformsTo(InformalComicSpecification)) { + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) + } + + val entryFormats: Map<Url, Format> = assetRetriever + .sniffContainerEntries(asset.container) { !it.isHiddenOrThumbs } + .getOrElse { return Try.failure(PublicationParser.ParseError.Reading(it)) } - if (!accepts(asset, fetcher)) - return null + val readingOrderWithFormat = + asset.container + .mapNotNull { url -> entryFormats[url]?.let { url to it } } + .filter { (_, format) -> format.specification.specifications.any { it in bitmapSpecifications } } + .sortedBy { it.first.toString() } - val readingOrder = fetcher.links() - .filter { !File(it.href).isHiddenOrThumbs && it.mediaType.isBitmap } - .sortedBy(Link::href) - .toMutableList() + if (readingOrderWithFormat.isEmpty()) { + return Try.failure( + PublicationParser.ParseError.Reading( + ReadError.Decoding( + DebugError("No bitmap found in the publication.") + ) + ) + ) + } + + val title = asset + .container + .entries + .guessTitle() - if (readingOrder.isEmpty()) - throw Exception("No bitmap found in the publication.") + return finalizeParsing(asset.container, readingOrderWithFormat, title) + } - val title = fetcher.guessTitle() ?: asset.name + private fun finalizeParsing( + container: Container<Resource>, + readingOrderWithFormat: List<Pair<Url, Format>>, + title: String? + ): Try<Publication.Builder, PublicationParser.ParseError> { + val readingOrder = readingOrderWithFormat.map { (url, format) -> + Link(href = url, mediaType = format.mediaType) + }.toMutableList() // First valid resource is the cover. readingOrder[0] = readingOrder[0].copy(rels = setOf("cover")) @@ -54,32 +127,33 @@ class ImageParser : PublicationParser { val manifest = Manifest( metadata = Metadata( conformsTo = setOf(Publication.Profile.DIVINA), - localizedTitle = LocalizedString(title) + localizedTitle = title?.let { LocalizedString(it) } ), readingOrder = readingOrder ) - return Publication.Builder( + val publicationBuilder = Publication.Builder( manifest = manifest, - fetcher = fetcher, + container = container, servicesBuilder = Publication.ServicesBuilder( - positions = PerResourcePositionsService.createFactory(fallbackMediaType = "image/*") + positions = PerResourcePositionsService.createFactory( + fallbackMediaType = MediaType("image/*")!! + ) ) ) - } - private suspend fun accepts(asset: PublicationAsset, fetcher: Fetcher): Boolean { - if (asset.mediaType() == MediaType.CBZ) - return true - - val allowedExtensions = listOf("acbf", "txt", "xml") + return Try.success(publicationBuilder) + } - if (fetcher.links() - .filterNot { File(it.href).isHiddenOrThumbs } - .all { it.mediaType.isBitmap || File(it.href).lowercasedExtension in allowedExtensions } + private val bitmapSpecifications: Set<Specification> = + setOf( + AvifSpecification, + BmpSpecification, + GifSpecification, + JpegSpecification, + JxlSpecification, + PngSpecification, + TiffSpecification, + WebpSpecification ) - return true - - return false - } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index 69121026d4..1a4382dc2c 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -1,68 +1,67 @@ /* - * Module: r2-shared-kotlin - * Developers: Mickaël Menu - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. */ package org.readium.r2.streamer.parser.pdf import android.content.Context -import java.io.File -import kotlinx.coroutines.runBlocking import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.PdfSupport -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.fetcher.FileFetcher import org.readium.r2.shared.publication.* -import org.readium.r2.shared.publication.asset.FileAsset -import org.readium.r2.shared.publication.asset.PublicationAsset import org.readium.r2.shared.publication.services.InMemoryCacheService import org.readium.r2.shared.publication.services.InMemoryCoverService +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.ResourceAsset +import org.readium.r2.shared.util.format.PdfSpecification +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.pdf.PdfDocumentFactory import org.readium.r2.shared.util.pdf.toLinks -import org.readium.r2.streamer.PublicationParser -import org.readium.r2.streamer.container.PublicationContainer -import org.readium.r2.streamer.parser.PubBox +import org.readium.r2.streamer.extensions.toContainer +import org.readium.r2.streamer.parser.PublicationParser /** * Parses a PDF file into a Readium [Publication]. */ -@PdfSupport @OptIn(ExperimentalReadiumApi::class) -class PdfParser( +public class PdfParser( context: Context, private val pdfFactory: PdfDocumentFactory<*> -) : PublicationParser, org.readium.r2.streamer.parser.PublicationParser { +) : PublicationParser { private val context = context.applicationContext - override suspend fun parse(asset: PublicationAsset, fetcher: Fetcher, warnings: WarningLogger?): Publication.Builder? = - _parse(asset, fetcher, asset.name) + override suspend fun parse( + asset: Asset, + warnings: WarningLogger? + ): Try<Publication.Builder, PublicationParser.ParseError> { + if (asset !is ResourceAsset || !asset.format.conformsTo(PdfSpecification)) { + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) + } - suspend fun _parse(asset: PublicationAsset, fetcher: Fetcher, fallbackTitle: String): Publication.Builder? { - if (asset.mediaType() != MediaType.PDF) - return null + val container = asset + .toContainer() - val fileHref = fetcher.links().firstOrNull { it.mediaType == MediaType.PDF }?.href - ?: throw Exception("Unable to find PDF file.") - val document = pdfFactory.open(fetcher.get(fileHref), password = null) - val tableOfContents = document.outline.toLinks(fileHref) + val url = container.entries + .first() + + val document = pdfFactory.open(container[url]!!, password = null) + .getOrElse { return Try.failure(PublicationParser.ParseError.Reading(it)) } + val tableOfContents = document.outline.toLinks(url) val manifest = Manifest( metadata = Metadata( identifier = document.identifier, conformsTo = setOf(Publication.Profile.PDF), - localizedTitle = LocalizedString(document.title?.ifBlank { null } ?: fallbackTitle), + localizedTitle = document.title?.ifBlank { null }?.let { LocalizedString(it) }, authors = listOfNotNull(document.author).map { Contributor(name = it) }, readingProgression = document.readingProgression, - numberOfPages = document.pageCount, + numberOfPages = document.pageCount ), - readingOrder = listOf(Link(href = fileHref, type = MediaType.PDF.toString())), + readingOrder = listOf(Link(href = url, mediaType = MediaType.PDF)), tableOfContents = tableOfContents ) @@ -72,27 +71,8 @@ class PdfParser( cover = document.cover(context)?.let { InMemoryCoverService.createFactory(it) } ) - return Publication.Builder(manifest, fetcher, servicesBuilder) - } - - override fun parse(fileAtPath: String, fallbackTitle: String): PubBox? = runBlocking { - - val file = File(fileAtPath) - val asset = FileAsset(file) - val baseFetcher = FileFetcher(href = "/${file.name}", file = file) - val builder = try { - _parse(asset, baseFetcher, fallbackTitle) - } catch (e: Exception) { - return@runBlocking null - } ?: return@runBlocking null - - val publication = builder.build() - val container = PublicationContainer( - publication = publication, - path = file.canonicalPath, - mediaType = MediaType.PDF - ) + val publicationBuilder = Publication.Builder(manifest, container, servicesBuilder) - PubBox(publication, container) + return Try.success(publicationBuilder) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfPositionsService.kt index e9d028ce99..85415516cb 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfPositionsService.kt @@ -38,12 +38,14 @@ internal class PdfPositionsService( return@lazy listOf(emptyList<Locator>()) } + val href = link.url() + return@lazy listOf( (1..pageCount).map { position -> val progression = (position - 1) / pageCount.toDouble() Locator( - href = link.href, - type = link.type ?: MediaType.PDF.toString(), + href = href, + mediaType = link.mediaType ?: MediaType.PDF, locations = Locator.Locations( fragments = listOf("page=$position"), progression = progression, diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt index 602c832e78..72f773540e 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt @@ -10,30 +10,32 @@ package org.readium.r2.streamer.parser.readium import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.PdfSupport import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.PositionsService +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory import org.readium.r2.shared.util.pdf.cachedIn +import org.readium.r2.shared.util.toDebugDescription import timber.log.Timber /** - * Creates the [positions] for an LCP protected PDF [Publication] from its [readingOrder] and - * [fetcher]. + * Creates the [positions] for an LCP protected PDF [Publication] from its reading order and + * container. */ -@OptIn(PdfSupport::class, ExperimentalReadiumApi::class) +@OptIn(ExperimentalReadiumApi::class) internal class LcpdfPositionsService( private val pdfFactory: PdfDocumentFactory<*>, - private val context: Publication.Service.Context, + private val context: Publication.Service.Context ) : PositionsService { override suspend fun positionsByReadingOrder(): List<List<Locator>> { - if (!::_positions.isInitialized) + if (!::_positions.isInitialized) { _positions = computePositions() + } return _positions } @@ -54,7 +56,12 @@ internal class LcpdfPositionsService( var lastPositionOfPreviousResource = 0 return resources.map { (pageCount, link) -> - val positions = createPositionsOf(link, pageCount = pageCount, totalPageCount = totalPageCount, startPosition = lastPositionOfPreviousResource) + val positions = createPositionsOf( + link, + pageCount = pageCount, + totalPageCount = totalPageCount, + startPosition = lastPositionOfPreviousResource + ) lastPositionOfPreviousResource += pageCount positions } @@ -70,13 +77,15 @@ internal class LcpdfPositionsService( return emptyList() } + val href = link.url() + // FIXME: Use the [tableOfContents] to generate the titles return (1..pageCount).map { position -> val progression = (position - 1) / pageCount.toDouble() val totalProgression = (startPosition + position - 1) / totalPageCount.toDouble() Locator( - href = link.href, - type = link.type ?: MediaType.PDF.toString(), + href = href, + mediaType = link.mediaType ?: MediaType.PDF, locations = Locator.Locations( fragments = listOf("page=$position"), progression = progression, @@ -87,15 +96,18 @@ internal class LcpdfPositionsService( } } - private suspend fun openPdfAt(link: Link): PdfDocument? = - try { - pdfFactory - .cachedIn(context.services) - .open(context.fetcher.get(link), password = null) - } catch (e: Exception) { - Timber.e(e) - null - } + private suspend fun openPdfAt(link: Link): PdfDocument? { + val resource = context.container.get(link.url()) + ?: return null + + return pdfFactory + .cachedIn(context.services) + .open(resource, password = null) + .getOrElse { + Timber.e(it.toDebugDescription()) + null + } + } companion object { diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index f7864ae0dc..eb2c7d1541 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -1,162 +1,174 @@ /* - * Module: r2-streamer-kotlin - * Developers: Mickaël Menu - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. */ package org.readium.r2.streamer.parser.readium import android.content.Context -import java.io.File -import java.io.FileNotFoundException -import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.PdfSupport -import org.readium.r2.shared.drm.DRM -import org.readium.r2.shared.fetcher.* -import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.asset.FileAsset -import org.readium.r2.shared.publication.asset.PublicationAsset -import org.readium.r2.shared.publication.services.* +import org.readium.r2.shared.publication.services.InMemoryCacheService +import org.readium.r2.shared.publication.services.PerResourcePositionsService +import org.readium.r2.shared.publication.services.WebPositionsService +import org.readium.r2.shared.publication.services.cacheServiceFactory +import org.readium.r2.shared.publication.services.locatorServiceFactory +import org.readium.r2.shared.publication.services.positionsServiceFactory +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset +import org.readium.r2.shared.util.data.CompositeContainer +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.decodeRwpm +import org.readium.r2.shared.util.data.readDecodeOrElse +import org.readium.r2.shared.util.format.FormatSpecification +import org.readium.r2.shared.util.format.LcpSpecification +import org.readium.r2.shared.util.format.RpfSpecification +import org.readium.r2.shared.util.format.RwpmSpecification import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpContainer import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.pdf.PdfDocumentFactory -import org.readium.r2.shared.util.use -import org.readium.r2.streamer.PublicationParser -import org.readium.r2.streamer.container.ContainerError -import org.readium.r2.streamer.container.PublicationContainer -import org.readium.r2.streamer.extensions.readAsJsonOrNull -import org.readium.r2.streamer.fetcher.LcpDecryptor -import org.readium.r2.streamer.parser.PubBox +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.SingleResourceContainer +import org.readium.r2.streamer.parser.PublicationParser import org.readium.r2.streamer.parser.audio.AudioLocatorService -import org.readium.r2.streamer.toPublicationType +import timber.log.Timber /** * Parses any Readium Web Publication package or manifest, e.g. WebPub, Audiobook, DiViNa, LCPDF... */ -@OptIn(PdfSupport::class) -class ReadiumWebPubParser( +public class ReadiumWebPubParser( private val context: Context? = null, - private val pdfFactory: PdfDocumentFactory<*>?, private val httpClient: HttpClient, -) : PublicationParser, org.readium.r2.streamer.parser.PublicationParser { + private val pdfFactory: PdfDocumentFactory<*>? +) : PublicationParser { override suspend fun parse( - asset: PublicationAsset, - fetcher: Fetcher, + asset: Asset, warnings: WarningLogger? - ): Publication.Builder? { - val mediaType = asset.mediaType() - - if (!mediaType.isReadiumWebPubProfile) - return null - - val isPackage = !mediaType.isRwpm - - val manifestJson = - if (isPackage) { - fetcher.readAsJsonOrNull("/manifest.json") - } else { - // For a single manifest file, reads the first (and only) file in the fetcher. - fetcher.links().firstOrNull() - ?.let { fetcher.readAsJsonOrNull(it.href) } - } - ?: throw Exception("Manifest not found") - - val manifest = Manifest.fromJSON(manifestJson, packaged = isPackage) - ?: throw Exception("Failed to parse the RWPM Manifest") - - @Suppress("NAME_SHADOWING") - var fetcher = fetcher + ): Try<Publication.Builder, PublicationParser.ParseError> = when (asset) { + is ResourceAsset -> parseResourceAsset(asset.resource, asset.format.specification) + is ContainerAsset -> parseContainerAsset(asset.container, asset.format.specification) + } - // For a manifest, we discard the [fetcher] provided by the Streamer, because it was only - // used to read the manifest file. We use an [HttpFetcher] instead to serve the remote - // resources. - if (!isPackage) { - val baseUrl = manifest.linkWithRel("self")?.let { File(it.href).parent } - fetcher = HttpFetcher(httpClient, baseUrl) + private suspend fun parseContainerAsset( + container: Container<Resource>, + formatSpecification: FormatSpecification + ): Try<Publication.Builder, PublicationParser.ParseError> { + if (!formatSpecification.conformsTo(RpfSpecification)) { + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } + val manifestResource = container[Url("manifest.json")!!] + ?: return Try.failure( + PublicationParser.ParseError.Reading( + ReadError.Decoding( + DebugError("Missing manifest.") + ) + ) + ) + + val manifest = manifestResource + .readDecodeOrElse( + decode = { it.decodeRwpm() }, + recover = { return Try.failure(PublicationParser.ParseError.Reading(it)) } + ) + // Checks the requirements from the LCPDF specification. // https://readium.org/lcp-specs/notes/lcp-for-pdf.html val readingOrder = manifest.readingOrder - if (asset.mediaType() == MediaType.LCP_PROTECTED_PDF && (readingOrder.isEmpty() || !readingOrder.all { it.mediaType.matches(MediaType.PDF) })) { - throw Exception("Invalid LCP Protected PDF.") + if (manifest.conformsTo(Publication.Profile.PDF) && formatSpecification.conformsTo( + LcpSpecification + ) && + (readingOrder.isEmpty() || !readingOrder.all { MediaType.PDF.matches(it.mediaType) }) + ) { + return Try.failure( + PublicationParser.ParseError.Reading( + ReadError.Decoding("Invalid LCP Protected PDF.") + ) + ) } val servicesBuilder = Publication.ServicesBuilder().apply { cacheServiceFactory = InMemoryCacheService.createFactory(context) - when (asset.mediaType()) { - MediaType.LCP_PROTECTED_PDF -> - positionsServiceFactory = pdfFactory?.let { LcpdfPositionsService.create(it) } - - MediaType.DIVINA_MANIFEST, MediaType.DIVINA -> - positionsServiceFactory = PerResourcePositionsService.createFactory("image/*") + positionsServiceFactory = when { + manifest.conformsTo(Publication.Profile.PDF) && formatSpecification.conformsTo( + LcpSpecification + ) -> + pdfFactory?.let { LcpdfPositionsService.create(it) } + manifest.conformsTo(Publication.Profile.DIVINA) -> + PerResourcePositionsService.createFactory(MediaType("image/*")!!) + else -> + WebPositionsService.createFactory(httpClient) + } - MediaType.READIUM_AUDIOBOOK, MediaType.READIUM_AUDIOBOOK_MANIFEST, MediaType.LCP_PROTECTED_AUDIOBOOK -> - locatorServiceFactory = AudioLocatorService.createFactory() + locatorServiceFactory = when { + manifest.conformsTo(Publication.Profile.AUDIOBOOK) -> + AudioLocatorService.createFactory() + else -> + null } } - return Publication.Builder(manifest, fetcher, servicesBuilder) + val publicationBuilder = Publication.Builder(manifest, container, servicesBuilder) + return Try.success(publicationBuilder) } - override fun parse(fileAtPath: String, fallbackTitle: String): PubBox? = runBlocking { - - val file = File(fileAtPath) - val asset = FileAsset(file) - val mediaType = asset.mediaType() - var baseFetcher = try { - ArchiveFetcher.fromPath(file.path) ?: FileFetcher(href = "/${file.name}", file = file) - } catch (e: SecurityException) { - return@runBlocking null - } catch (e: FileNotFoundException) { - throw ContainerError.missingFile(fileAtPath) - } - - val drm = if (baseFetcher.isProtectedWithLcp()) DRM(DRM.Brand.lcp) else null - if (drm?.brand == DRM.Brand.lcp) { - baseFetcher = TransformingFetcher(baseFetcher, LcpDecryptor(drm)::transform) + private suspend fun parseResourceAsset( + resource: Resource, + formatSpecification: FormatSpecification + ): Try<Publication.Builder, PublicationParser.ParseError> { + if (!formatSpecification.conformsTo(RwpmSpecification)) { + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } - val builder = try { - parse(asset, baseFetcher) - } catch (e: Exception) { - return@runBlocking null - } ?: return@runBlocking null - - val publication = builder.build() - .apply { - @Suppress("DEPRECATION") - type = mediaType.toPublicationType() + val manifest = resource + .readDecodeOrElse( + decode = { it.decodeRwpm() }, + recover = { return Try.failure(PublicationParser.ParseError.Reading(it)) } + ) + + val baseUrl = manifest.linkWithRel("self")?.href?.resolve() + if (baseUrl == null) { + Timber.w("No self link found in the manifest at ${resource.sourceUrl}") + } else { + if (baseUrl !is AbsoluteUrl) { + return Try.failure( + PublicationParser.ParseError.Reading( + ReadError.Decoding("Self link is not absolute.") + ) + ) } - - val container = PublicationContainer( - publication = publication, - path = file.canonicalPath, - mediaType = mediaType, - drm = drm - ).apply { - if (!mediaType.isRwpm) { - rootFile.rootFilePath = "manifest.json" + if (!baseUrl.isHttp) { + return Try.failure( + PublicationParser.ParseError.Reading( + ReadError.Decoding("Self link doesn't use the HTTP(S) scheme.") + ) + ) } } - PubBox(publication, container) - } -} + val resources = (manifest.readingOrder + manifest.resources) + .map { it.url() } + .toSet() -private suspend fun Fetcher.isProtectedWithLcp(): Boolean = - get("license.lcpl").use { it.length().isSuccess } + val container = + CompositeContainer( + SingleResourceContainer( + Url("manifest.json")!!, + resource + ), + HttpContainer(baseUrl, resources, httpClient) + ) -/** Returns whether this media type is of a Readium Web Publication profile. */ -private val MediaType.isReadiumWebPubProfile: Boolean get() = matchesAny( - MediaType.READIUM_WEBPUB, MediaType.READIUM_WEBPUB_MANIFEST, - MediaType.READIUM_AUDIOBOOK, MediaType.READIUM_AUDIOBOOK_MANIFEST, MediaType.LCP_PROTECTED_AUDIOBOOK, - MediaType.DIVINA, MediaType.DIVINA_MANIFEST, MediaType.LCP_PROTECTED_PDF -) + return parseContainerAsset(container, FormatSpecification(RpfSpecification)) + } +} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/server/Assets.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/server/Assets.kt deleted file mode 100644 index 2eb1056f12..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/server/Assets.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Mickaël Menu - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server - -import android.content.res.AssetManager -import android.net.Uri -import java.io.File -import java.io.InputStream -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.readium.r2.shared.extensions.isParentOf -import org.readium.r2.shared.util.mediatype.MediaType - -/** - * Files to be served from the application's assets. - * - * @param basePath Base path (ignoring host) from where the files are served. - * @param fallbackMediaType Media type which will be used for responses when it can't be determined - * from the served file. - */ -internal class Assets( - private val assetManager: AssetManager, - private val basePath: String, - private val fallbackMediaType: MediaType = MediaType.BINARY -) { - private val assets: MutableList<Pair<String, File>> = mutableListOf() - - fun add(href: String, path: String) { - // Inserts at the beginning to take precedence over already registered assets. - assets.add(0, Pair(href, File("/$path").canonicalFile)) - } - - suspend fun find(uri: Uri): ServedAsset? { - val path = uri.path?.removePrefix(basePath) ?: return null - - for ((href, file) in assets) { - if (path.startsWith(href)) { - val requestedFile = File(file, path.removePrefix(href)).canonicalFile - // Makes sure that the requested file is `file` or one of its descendant. - if (file == requestedFile || file.isParentOf(requestedFile)) { - val mediaType = MediaType.of(fileExtension = requestedFile.extension) ?: fallbackMediaType - return withContext(Dispatchers.IO) { - ServedAsset(assetManager.open(requestedFile.path.removePrefix("/")), mediaType) - } - } - } - } - - return null - } - - data class ServedAsset( - val stream: InputStream, - val mediaType: MediaType - ) -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/server/Files.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/server/Files.kt deleted file mode 100644 index 17d9719dda..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/server/Files.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi, Clément Baumann - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server - -import android.net.Uri -import java.io.File -import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.extensions.isParentOf -import org.readium.r2.shared.util.mediatype.MediaType - -/** - * Files to be served from the file system. - * - * @param basePath Base path (ignoring host) from where the files are served. - * @param fallbackMediaType Media type which will be used for responses when it can't be determined - * from the served file. - */ -internal class Files( - private val basePath: String, - private val fallbackMediaType: MediaType = MediaType.BINARY -) { - private val files: MutableMap<String, File> = mutableMapOf() - - operator fun set(href: String, file: File) { - files[href] = file.canonicalFile - } - - operator fun get(key: String): File? = files[key] - - fun find(uri: Uri): ServedFile? { - val path = uri.path?.removePrefix(basePath) ?: return null - - for ((href, file) in files) { - if (path.startsWith(href)) { - val requestedFile = File(file, path.removePrefix(href)).canonicalFile - // Makes sure that the requested file is `file` or one of its descendant. - if (file.isParentOf(requestedFile)) { - return ServedFile(requestedFile, fallbackMediaType) - } - } - } - - return null - } - - data class ServedFile( - val file: File, - private val fallbackMediaType: MediaType - ) { - val mediaType: MediaType get() = runBlocking { MediaType.ofFile(file) ?: fallbackMediaType } - } -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/server/Globals.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/server/Globals.kt index 20cd8a6452..0ca9f8bc2b 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/server/Globals.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/server/Globals.kt @@ -12,5 +12,5 @@ package org.readium.r2.streamer.server /** * Created by aferditamuriqi on 10/3/17. */ -@Deprecated("Use Publication::localBaseUrlOf() instead") -const val BASE_URL = "http://127.0.0.1" +@Deprecated("Use Publication::localBaseUrlOf() instead", level = DeprecationLevel.ERROR) +public const val BASE_URL: String = "http://127.0.0.1" diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/server/Resources.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/server/Resources.kt index 66b4f2c3ae..545dd158f6 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/server/Resources.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/server/Resources.kt @@ -9,22 +9,8 @@ package org.readium.r2.streamer.server -import org.readium.r2.shared.Injectable - -class Resources { - val resources: MutableMap<String, Any> = mutableMapOf() - - fun add(key: String, body: String, injectable: Injectable? = null) { - injectable?.let { - resources[key] = Pair(body, injectable.rawValue) - } ?: run { - resources[key] = body - } - } - - fun get(key: String): String? = - when (val resource = resources[key]) { - is Pair<*, *> -> resource.first as? String - else -> resource as? String - } -} +@Deprecated( + "The HTTP server is not needed anymore (see migration guide)", + level = DeprecationLevel.ERROR +) +public class Resources diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/server/Server.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/server/Server.kt index fae1ad676f..2d507bd8ea 100755 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/server/Server.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/server/Server.kt @@ -10,175 +10,26 @@ package org.readium.r2.streamer.server import android.content.Context -import android.content.res.AssetManager -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.net.URL -import java.net.URLDecoder -import java.util.* import org.nanohttpd.router.RouterNanoHTTPD -import org.readium.r2.shared.Injectable -import org.readium.r2.shared.publication.Publication -import org.readium.r2.streamer.BuildConfig.DEBUG -import org.readium.r2.streamer.container.Container -import org.readium.r2.streamer.server.handler.AssetHandler -import org.readium.r2.streamer.server.handler.FileHandler -import org.readium.r2.streamer.server.handler.ManifestHandler -import org.readium.r2.streamer.server.handler.PublicationResourceHandler -import org.readium.r2.streamer.server.handler.ResourceHandler -import timber.log.Timber -@Deprecated("The HTTP server is not needed anymore (see migration guide)") -class Server( +@Suppress("Unused_parameter") +@Deprecated( + "The HTTP server is not needed anymore (see migration guide)", + level = DeprecationLevel.ERROR +) +public class Server( port: Int, context: Context, enableReadiumNavigatorSupport: Boolean = true -) : AbstractServer(port, context.applicationContext, enableReadiumNavigatorSupport) - -abstract class AbstractServer( - private var port: Int, - private val context: Context, - private val enableReadiumNavigatorSupport: Boolean = true, -) : RouterNanoHTTPD("127.0.0.1", port) { - - private val MANIFEST_HANDLE = "/manifest" - private val JSON_MANIFEST_HANDLE = "/manifest.json" - private val MANIFEST_ITEM_HANDLE = "/(.*)" - private val MEDIA_OVERLAY_HANDLE = "/media-overlay" - private val CSS_HANDLE = "/" + Injectable.Style.rawValue + "/(.*)" - private val JS_HANDLE = "/" + Injectable.Script.rawValue + "/(.*)" - private val FONT_HANDLE = "/" + Injectable.Font.rawValue + "/(.*)" - private val ASSETS_HANDLE = "/assets/(.*)" - private var containsMediaOverlay = false - - private val resources = Resources() - private val customResources = Resources() - private val assets = Assets(context.assets, basePath = "/assets/") - private val fonts = Files(basePath = "/${Injectable.Style}/") - - init { - assets.add(href = "readium-css", path = "readium/readium-css") - assets.add(href = "scripts", path = "readium/scripts") - assets.add(href = "fonts", path = "readium/fonts") - } - - private fun addResource( - name: String, - body: String, - custom: Boolean = false, - injectable: Injectable? = null - ) { - if (custom) { - customResources.add(name, body, injectable) - } - resources.add(name, body) - } - - private fun addFont(name: String, inputStream: InputStream, context: Context) { - val dir = File(context.filesDir.path + "/" + Injectable.Font.rawValue + "/") - dir.mkdirs() - inputStream.toFile(context.filesDir.path + "/" + Injectable.Font.rawValue + "/" + name) - val file = File(context.filesDir.path + "/" + Injectable.Font.rawValue + "/" + name) - fonts[name] = file - } - - fun loadCustomResource(inputStream: InputStream, fileName: String, injectable: Injectable) { - try { - addResource(fileName, Scanner(inputStream, "utf-8").useDelimiter("\\A").next(), true, injectable) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - } - - fun loadCustomFont(inputStream: InputStream, context: Context, fileName: String) { - try { - addFont(fileName, inputStream, context) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - } - - fun addPublication(publication: Publication, userPropertiesFile: File? = null): URL? { - return addPublication(publication, null, "/${UUID.randomUUID()}", userPropertiesFile?.path) - } - - private fun addPublication( - publication: Publication, - container: Container?, - filename: String, - userPropertiesPath: String? - ): URL? { - if (container?.rootFile?.rootFilePath?.isEmpty() == true) { - return null - } - @Suppress("DEPRECATION") val baseUrl = URL(Publication.localBaseUrlOf(filename = filename, port = port)) - val fetcher = ServingFetcher( - publication, - enableReadiumNavigatorSupport, - userPropertiesPath, - customResources - ) - - // NanoHTTPD expects percent-decoded routes. - val basePath = - try { URLDecoder.decode(baseUrl.path, "UTF-8") } catch (e: Exception) { baseUrl.path } - - setRoute(basePath + JSON_MANIFEST_HANDLE, ManifestHandler::class.java, fetcher) - setRoute(basePath + MANIFEST_HANDLE, ManifestHandler::class.java, fetcher) - setRoute(basePath + MANIFEST_ITEM_HANDLE, PublicationResourceHandler::class.java, fetcher) - setRoute(ASSETS_HANDLE, AssetHandler::class.java, assets) - setRoute(JS_HANDLE, ResourceHandler::class.java, resources) - setRoute(CSS_HANDLE, ResourceHandler::class.java, resources) - setRoute(FONT_HANDLE, FileHandler::class.java, fonts) - - return baseUrl - } - - @Deprecated("Use the easier-to-use addPublication()", replaceWith = ReplaceWith("this.addPublication(publication, userPropertiesFile = File(userPropertiesPath))"), level = DeprecationLevel.ERROR) - fun addEpub(publication: Publication, fileName: String, userPropertiesPath: String?) { - addPublication(publication, null, filename = fileName, userPropertiesPath = userPropertiesPath) - } - - @Deprecated("Use the easier-to-use addPublication()", replaceWith = ReplaceWith("this.addPublication(publication, userPropertiesFile = File(userPropertiesPath))"), level = DeprecationLevel.ERROR) - fun addEpub( - publication: Publication, - container: Container?, - fileName: String, - userPropertiesPath: String? - ) { - addPublication(publication, container, filename = fileName, userPropertiesPath = userPropertiesPath) - } - - private fun setRoute(url: String, handler: Class<*>, vararg initParameter: Any) { - try { removeRoute(url) } catch (e: Exception) {} - addRoute(url, handler, *initParameter) - } - - // FIXME: To review once the media-overlays will be supported in the Publication model -// private fun addLinks(publication: Publication, filePath: String) { -// containsMediaOverlay = false -// for (link in publication.otherLinks) { -// if (link.rel.contains("media-overlay")) { -// containsMediaOverlay = true -// link.href = link.href?.replace("port", "127.0.0.1:$listeningPort$filePath") -// } -// } -// } - - private fun InputStream.toFile(path: String) { - use { input -> - File(path).outputStream().use { input.copyTo(it) } - } - } - - @Suppress("UNUSED_PARAMETER") - @Deprecated("This is not needed anymore") - fun loadReadiumCSSResources(assets: AssetManager) {} - @Suppress("UNUSED_PARAMETER") - @Deprecated("This is not needed anymore") - fun loadR2ScriptResources(assets: AssetManager) {} - @Suppress("UNUSED_PARAMETER") - @Deprecated("This is not needed anymore") - fun loadR2FontResources(assets: AssetManager, context: Context) {} -} +) + +@Suppress("Unused_parameter") +@Deprecated( + "The HTTP server is not needed anymore (see migration guide)", + level = DeprecationLevel.ERROR +) +public abstract class AbstractServer( + port: Int, + context: Context, + enableReadiumNavigatorSupport: Boolean = true +) : RouterNanoHTTPD("127.0.0.1", port) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/server/ServingFetcher.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/server/ServingFetcher.kt deleted file mode 100644 index 9fc7d640c8..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/server/ServingFetcher.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi, Clément Baumannn, Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server - -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.Publication -import org.readium.r2.streamer.fetcher.HtmlInjector - -internal class ServingFetcher( - val publication: Publication, - private val enableReadiumNavigatorSupport: Boolean, - userPropertiesPath: String?, - customResources: Resources? = null, -) : Fetcher { - - private val htmlInjector: HtmlInjector by lazy { - HtmlInjector( - publication, - userPropertiesPath, - customResources - ) - } - - override suspend fun links(): List<Link> = emptyList() - - override fun get(link: Link): Resource { - val resource = publication.get(link) - return if (enableReadiumNavigatorSupport) - transformResourceForReadiumNavigator(resource) - else - resource - } - - private fun transformResourceForReadiumNavigator(resource: Resource): Resource { - return if (publication.conformsTo(Publication.Profile.EPUB)) - htmlInjector.transform(resource) - else - resource - } - - override fun get(href: String): Resource { - val link = publication.linkWithHref(href) - ?.copy(href = href) // query parameters must be kept - ?: Link(href = href) - - return get(link) - } - - override suspend fun close() {} -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/AssetHandler.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/AssetHandler.kt deleted file mode 100644 index 2611e97346..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/AssetHandler.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Mickaël Menu - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server.handler - -import android.net.Uri -import kotlinx.coroutines.runBlocking -import org.nanohttpd.protocols.http.response.Response -import org.nanohttpd.router.RouterNanoHTTPD -import org.readium.r2.streamer.server.Assets - -/** - * Serves files from the local file system. - * - * The NanoHTTPD init parameter must be an instance of `Assets`. - */ -internal class AssetHandler : BaseHandler() { - - override fun handle( - resource: RouterNanoHTTPD.UriResource, - uri: Uri, - parameters: Map<String, String>? - ): Response { - val assets = resource.initParameter(Assets::class.java) - val asset = runBlocking { assets.find(uri) } ?: return notFoundResponse - return createResponse(mediaType = asset.mediaType, body = asset.stream) - } -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/BaseHandler.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/BaseHandler.kt deleted file mode 100644 index e667caf497..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/BaseHandler.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Mickaël Menu - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server.handler - -import android.net.Uri -import java.io.FileNotFoundException -import java.io.InputStream -import org.nanohttpd.protocols.http.IHTTPSession -import org.nanohttpd.protocols.http.response.IStatus -import org.nanohttpd.protocols.http.response.Response -import org.nanohttpd.protocols.http.response.Status -import org.nanohttpd.router.RouterNanoHTTPD -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.streamer.BuildConfig -import timber.log.Timber - -internal abstract class BaseHandler : RouterNanoHTTPD.DefaultHandler() { - - override fun getMimeType(): String? = null - override fun getText(): String = "" - override fun getStatus(): IStatus = Status.OK - - override fun get( - uriResource: RouterNanoHTTPD.UriResource?, - urlParams: Map<String, String>?, - session: IHTTPSession? - ): Response { - uriResource ?: return notFoundResponse - session ?: return notFoundResponse - - if (BuildConfig.DEBUG) Timber.v("Method: ${session.method}, URL: ${session.uri}") - - return try { - val uri = Uri.parse(session.uri) - handle(resource = uriResource, uri = uri, parameters = urlParams) - } catch (e: FileNotFoundException) { - if (BuildConfig.DEBUG) Timber.e("Server handler error: %s", e.toString()) - notFoundResponse - } catch (e: Exception) { - if (BuildConfig.DEBUG) Timber.e("Server handler error: %s", e.toString()) - createErrorResponse(Status.INTERNAL_ERROR) - } - } - - abstract fun handle( - resource: RouterNanoHTTPD.UriResource, - uri: Uri, - parameters: Map<String, String>? - ): Response - - fun createResponse(mediaType: MediaType, body: String): Response = - createResponse(mediaType, body.toByteArray()) - - fun createResponse(mediaType: MediaType, body: ByteArray): Response = - Response.newFixedLengthResponse(Status.OK, mediaType.toString(), body).apply { - addHeader("Accept-Ranges", "bytes") - } - - fun createResponse(mediaType: MediaType, body: InputStream): Response = - Response.newChunkedResponse(Status.OK, mediaType.toString(), body).apply { - addHeader("Accept-Ranges", "bytes") - } - - fun createErrorResponse(status: Status) = - Response.newFixedLengthResponse(status, "text/html", "") - - val notFoundResponse: Response get() = createErrorResponse(Status.NOT_FOUND) -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/FileHandler.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/FileHandler.kt deleted file mode 100644 index 40f309ddb5..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/FileHandler.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Mickaël Menu - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server.handler - -import android.net.Uri -import org.nanohttpd.protocols.http.response.Response -import org.nanohttpd.router.RouterNanoHTTPD -import org.readium.r2.streamer.server.Files - -/** - * Serves files from the local file system. - * - * The NanoHTTPD init parameter must be an instance of `Files`. - */ -internal class FileHandler : BaseHandler() { - - override fun handle( - resource: RouterNanoHTTPD.UriResource, - uri: Uri, - parameters: Map<String, String>? - ): Response { - val files = resource.initParameter(Files::class.java) - val file = files.find(uri) ?: return notFoundResponse - return createResponse(mediaType = file.mediaType, body = file.file.inputStream()) - } -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/ManifestHandler.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/ManifestHandler.kt deleted file mode 100644 index 98c69e7264..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/ManifestHandler.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi, Clément Baumann, Mickaël Menu - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server.handler - -import android.net.Uri -import org.nanohttpd.protocols.http.response.Response -import org.nanohttpd.router.RouterNanoHTTPD -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.streamer.server.ServingFetcher - -internal class ManifestHandler : BaseHandler() { - - override fun handle( - resource: RouterNanoHTTPD.UriResource, - uri: Uri, - parameters: Map<String, String>? - ): Response { - val fetcher = resource.initParameter(ServingFetcher::class.java) - return createResponse(mediaType = MediaType.READIUM_WEBPUB_MANIFEST, body = fetcher.publication.jsonManifest) - } -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/PublicationResourceHandler.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/PublicationResourceHandler.kt deleted file mode 100644 index b97d63ce8a..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/PublicationResourceHandler.kt +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi, Clément Baumann - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server.handler - -import java.io.InputStream -import kotlinx.coroutines.runBlocking -import org.nanohttpd.protocols.http.IHTTPSession -import org.nanohttpd.protocols.http.NanoHTTPD.MIME_PLAINTEXT -import org.nanohttpd.protocols.http.response.IStatus -import org.nanohttpd.protocols.http.response.Response -import org.nanohttpd.protocols.http.response.Response.newChunkedResponse -import org.nanohttpd.protocols.http.response.Response.newFixedLengthResponse -import org.nanohttpd.protocols.http.response.Status -import org.nanohttpd.router.RouterNanoHTTPD -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.ResourceInputStream -import org.readium.r2.streamer.BuildConfig.DEBUG -import org.readium.r2.streamer.server.ServingFetcher -import timber.log.Timber - -class PublicationResourceHandler : RouterNanoHTTPD.DefaultHandler() { - - override fun getMimeType(): String? { - return null - } - - override fun getText(): String { - return ResponseStatus.FAILURE_RESPONSE - } - - override fun getStatus(): IStatus { - return Status.OK - } - - override fun get( - uriResource: RouterNanoHTTPD.UriResource, - urlParams: Map<String, String>, - session: IHTTPSession - ): Response = runBlocking { - - if (DEBUG) Timber.v("Method: ${session.method}, Uri: ${session.uri}") - val fetcher = uriResource.initParameter(ServingFetcher::class.java) - - val href = getHref(session) - val resource = fetcher.get(href) - - try { - serveResponse(session, resource) - .apply { - // Disable HTTP caching for publication resources, because it poses a security - // threat for protected publications. - addHeader("Cache-Control", "no-cache, no-store, must-revalidate") - addHeader("Pragma", "no-cache") - addHeader("Expires", "0") - } - } catch (e: Resource.Exception) { - Timber.e(e) - responseFromFailure(e) - .also { resource.close() } - } catch (e: Exception) { - if (DEBUG) Timber.e(e) - newFixedLengthResponse(Status.INTERNAL_ERROR, mimeType, ResponseStatus.FAILURE_RESPONSE) - .also { resource.close() } - } - } - - private suspend fun serveResponse(session: IHTTPSession, resource: Resource): Response { - // Depending on the nature of the Response, either resource is closed here, - // or the responsibility of closing it is forwarded to the created ResourceInputStream. - // In the latter case, NanoHTTPd will close it at the end of the transmission. - - var rangeRequest: String? = session.headers["range"] - val mimeType = resource.link().mediaType.toString() - - // Calculate etag - val etag = Integer.toHexString(resource.hashCode()) // FIXME: Is this working? - - // Support skipping: - var startFrom: Long = 0 - var endAt: Long = -1 - if (rangeRequest != null) { - if (rangeRequest.startsWith("bytes=")) { - rangeRequest = rangeRequest.substring("bytes=".length) - val minus = rangeRequest.indexOf('-') - try { - if (minus > 0) { - startFrom = java.lang.Long.parseLong(rangeRequest.substring(0, minus)) - endAt = java.lang.Long.parseLong(rangeRequest.substring(minus + 1)) - } - } catch (ignored: NumberFormatException) { - } - } - } - - val dataLength = resource.length().getOrThrow() - - // Change return code and add Content-Range header when skipping is requested - return if (rangeRequest != null && startFrom >= 0) { - if (endAt < 0) { - endAt = dataLength - 1 - } - - if (startFrom >= dataLength) { - createResponse(Status.RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "", dataLength) - .apply { - addHeader("Content-Range", "bytes 0-0/$dataLength") - addHeader("ETag", etag) - }.also { - resource.close() - } - } else { - val responseStream = ResourceInputStream(resource, range = startFrom..endAt) - createResponse(Status.PARTIAL_CONTENT, mimeType, responseStream, dataLength) - .apply { - addHeader("Content-Range", "bytes $startFrom-$endAt/$dataLength") - addHeader("ETag", etag) - } - } - } else { - if (etag == session.headers["if-none-match"]) - createResponse(Status.NOT_MODIFIED, mimeType, "", dataLength) - .also { - resource.close() - } - else { - createResponse(Status.OK, mimeType, ResourceInputStream(resource), dataLength) - .apply { - addHeader("ETag", etag) - } - } - } - } - - private fun createResponse( - status: Status, - mimeType: String, - data: InputStream, - dataLength: Long - ): Response { - val response = newChunkedResponse(status, mimeType, data.buffered()) - response.addHeader("Accept-Ranges", "bytes") - response.addHeader("Content-Length", dataLength.toString()) - return response - } - - private fun createResponse(status: Status, mimeType: String, message: String, dataLength: Long): Response { - val response = newFixedLengthResponse(status, mimeType, message) - response.addHeader("Accept-Ranges", "bytes") - response.addHeader("Content-Length", dataLength.toString()) - return response - } - - private fun getHref(session: IHTTPSession): String { - val path = session.uri - val offset = path.indexOf("/", 0) - val startIndex = path.indexOf("/", offset + 1) - val filePath = path.substring(startIndex) - - return if (session.queryParameterString.isNullOrBlank()) - filePath - else - "$filePath?${session.queryParameterString}" - } - - private fun responseFromFailure(error: Resource.Exception): Response { - val status = when (error) { - is Resource.Exception.NotFound -> Status.NOT_FOUND - is Resource.Exception.Forbidden -> Status.FORBIDDEN - is Resource.Exception.Unavailable, is Resource.Exception.Offline -> Status.SERVICE_UNAVAILABLE - is Resource.Exception.BadRequest -> Status.BAD_REQUEST - is Resource.Exception.Cancelled, is Resource.Exception.OutOfMemory, is Resource.Exception.Other -> Status.INTERNAL_ERROR - } - return newFixedLengthResponse(status, mimeType, ResponseStatus.FAILURE_RESPONSE) - } -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/ResourceHandler.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/ResourceHandler.kt deleted file mode 100644 index be4cb33b56..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/ResourceHandler.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Mickaël Menu - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server.handler - -import android.net.Uri -import kotlinx.coroutines.runBlocking -import org.nanohttpd.protocols.http.response.Response -import org.nanohttpd.router.RouterNanoHTTPD -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.streamer.server.Resources - -/** - * Serves in-memory resources. - * - * The NanoHTTPD init parameter must be an instance of `Resources`. - */ -internal class ResourceHandler : BaseHandler() { - - override fun handle( - resource: RouterNanoHTTPD.UriResource, - uri: Uri, - parameters: Map<String, String>? - ): Response { - val resources = resource.initParameter(Resources::class.java) - val href = uri.path?.substringAfterLast("/") ?: return notFoundResponse - val body = resources.get(href) ?: return notFoundResponse - val mediaType = runBlocking { MediaType.of(fileExtension = href.substringAfterLast(".", "")) } - return createResponse(mediaType = mediaType ?: MediaType.BINARY, body = body) - } -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/ResponseStatus.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/ResponseStatus.kt deleted file mode 100644 index 6a9b641d10..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/server/handler/ResponseStatus.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi, Clément Baumann - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server.handler - -object ResponseStatus { - const val SUCCESS_RESPONSE = "{\"success\":true}" - const val FAILURE_RESPONSE = "{\"success\":false}" -} diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/TestUtils.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/TestUtils.kt index cacdcc0f3f..535863857c 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/TestUtils.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/TestUtils.kt @@ -10,12 +10,12 @@ package org.readium.r2.streamer import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.fetcher.Resource import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.asset.PublicationAsset +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.streamer.parser.PublicationParser internal fun Resource.readBlocking(range: LongRange? = null) = runBlocking { read(range) } -internal fun PublicationParser.parseBlocking(asset: PublicationAsset, fetcher: Fetcher): - Publication.Builder? = runBlocking { parse(asset, fetcher) } +internal fun PublicationParser.parseBlocking(asset: Asset): + Publication.Builder? = runBlocking { parse(asset).getOrNull() } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/ContainerTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/ContainerTest.kt new file mode 100644 index 0000000000..5f31104f27 --- /dev/null +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/ContainerTest.kt @@ -0,0 +1,57 @@ +/* + * Module: r2-streamer-kotlin + * Developers: Quentin Gliosca + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.streamer.extensions + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.util.Url +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ContainerTest { + + @Test + fun `pathCommonFirstComponent is null when files are in the root`() { + assertNull( + listOf(Url("im1.jpg")!!, Url("im2.jpg")!!, Url("toc.xml")!!) + .pathCommonFirstComponent() + ) + } + + @Test + fun `pathCommonFirstComponent is null when files are in different directories`() { + assertNull( + listOf(Url("dir1/im1.jpg")!!, Url("dir2/im2.jpg")!!, Url("toc.xml")!!) + .pathCommonFirstComponent() + ) + } + + @Test + fun `pathCommonFirstComponent is correct when there is only one file in the root`() { + assertEquals( + "im1.jpg", + listOf(Url("im1.jpg")!!).pathCommonFirstComponent()?.name + ) + } + + @Test + fun `pathCommonFirstComponent is correct when all files are in the same directory`() { + assertEquals( + "root", + listOf( + Url("root/im1.jpg")!!, + Url("root/im2.jpg")!!, + Url("root/xml/toc.xml")!! + ).pathCommonFirstComponent()?.name + ) + } +} diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/LinkTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/LinkTest.kt deleted file mode 100644 index 747bb57bd8..0000000000 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/LinkTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Quentin Gliosca - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.extensions - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Test -import org.readium.r2.shared.publication.Link - -class LinkTest { - - @Test - fun `hrefCommonFirstComponent is null when files are in the root`() { - assertNull( - listOf(Link("/im1.jpg"), Link("/im2.jpg"), Link("/toc.xml")) - .hrefCommonFirstComponent() - ) - } - - @Test - fun `hrefCommonFirstComponent is null when files are in different directories`() { - assertNull( - listOf(Link("/dir1/im1.jpg"), Link("/dir2/im2.jpg"), Link("/toc.xml")) - .hrefCommonFirstComponent() - ) - } - - @Test - fun `hrefCommonFirstComponent is correct when there is only one file in the root`() { - assertEquals( - "im1.jpg", - listOf(Link("/im1.jpg")).hrefCommonFirstComponent()?.name - ) - } - - @Test - fun `hrefCommonFirstComponent is correct when all files are in the same directory`() { - assertEquals( - "root", - listOf( - Link("/root/im1.jpg"), - Link("/root/im2.jpg"), - Link("/root/xml/toc.xml") - ).hrefCommonFirstComponent()?.name - ) - } -} diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/fetcher/HtmlInjectorTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/fetcher/HtmlInjectorTest.kt deleted file mode 100644 index f1ed8da4c8..0000000000 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/fetcher/HtmlInjectorTest.kt +++ /dev/null @@ -1,172 +0,0 @@ -package org.readium.r2.streamer.fetcher - -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Test -import org.readium.r2.shared.fetcher.StringResource -import org.readium.r2.shared.publication.* - -class HtmlInjectorTest { - - @Test - fun `Inject a reflowable with a simple HEAD`() { - assertEquals( - """ - <?xml version="1.0" encoding="utf-8"?> - <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> - <html xmlns="http://www.w3.org/1999/xhtml"> - <head><link rel="stylesheet" type="text/css" href="/assets/readium-css/ReadiumCSS-before.css"/> - <style> - audio[controls] { - width: revert; - height: revert; - } - </style> <style> - :root[style], :root { overflow: visible !important; } - :root[style] > body, :root > body { overflow: visible !important; } - </style> - <title>Publication</title> - <link rel="stylesheet" href="style.css" type="text/css"/> - <link rel="stylesheet" type="text/css" href="/assets/readium-css/ReadiumCSS-after.css"/> - <script type="text/javascript" src="/assets/scripts/readium-reflowable.js"></script> - <style>@import url('https://fonts.googleapis.com/css?family=PT+Serif|Roboto|Source+Sans+Pro|Vollkorn');</style> - <style type="text/css"> @font-face{font-family: "OpenDyslexic"; src:url("/assets/fonts/OpenDyslexic-Regular.otf") format('truetype');}</style> - </head> - <body></body> - </html> - """.trimIndent(), - transform( - """ - <?xml version="1.0" encoding="utf-8"?> - <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> - <html xmlns="http://www.w3.org/1999/xhtml"> - <head> - <title>Publication</title> - <link rel="stylesheet" href="style.css" type="text/css"/> - </head> - <body></body> - </html> - """.trimIndent() - ) - ) - } - - @Test - fun `Inject a reflowable with a HEAD with attributes`() { - assertEquals( - """ - <?xml version="1.0" encoding="utf-8"?> - <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> - <html xmlns="http://www.w3.org/1999/xhtml"> - <head xmlns:xlink="http://www.w3.org/1999/xlink"><link rel="stylesheet" type="text/css" href="/assets/readium-css/ReadiumCSS-before.css"/> - <style> - audio[controls] { - width: revert; - height: revert; - } - </style> <style> - :root[style], :root { overflow: visible !important; } - :root[style] > body, :root > body { overflow: visible !important; } - </style> - <title>Publication</title> - <link rel="stylesheet" href="style.css" type="text/css"/> - <link rel="stylesheet" type="text/css" href="/assets/readium-css/ReadiumCSS-after.css"/> - <script type="text/javascript" src="/assets/scripts/readium-reflowable.js"></script> - <style>@import url('https://fonts.googleapis.com/css?family=PT+Serif|Roboto|Source+Sans+Pro|Vollkorn');</style> - <style type="text/css"> @font-face{font-family: "OpenDyslexic"; src:url("/assets/fonts/OpenDyslexic-Regular.otf") format('truetype');}</style> - </head> - <body></body> - </html> - """.trimIndent(), - transform( - """ - <?xml version="1.0" encoding="utf-8"?> - <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> - <html xmlns="http://www.w3.org/1999/xhtml"> - <head xmlns:xlink="http://www.w3.org/1999/xlink"> - <title>Publication</title> - <link rel="stylesheet" href="style.css" type="text/css"/> - </head> - <body></body> - </html> - """.trimIndent() - ) - ) - } - - @Test - fun `Inject a reflowable with HEAD with attributes on a single line`() { - assertEquals( - """ - <?xml version="1.0" encoding="utf-8"?> - <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head xmlns:xlink="http://www.w3.org/1999/xlink"><link rel="stylesheet" type="text/css" href="/assets/readium-css/ReadiumCSS-before.css"/> - <style> - audio[controls] { - width: revert; - height: revert; - } - </style> <style> - :root[style], :root { overflow: visible !important; } - :root[style] > body, :root > body { overflow: visible !important; } - </style><title>Publication</title><link rel="stylesheet" href="style.css" type="text/css"/><link rel="stylesheet" type="text/css" href="/assets/readium-css/ReadiumCSS-after.css"/> - <script type="text/javascript" src="/assets/scripts/readium-reflowable.js"></script> - <style>@import url('https://fonts.googleapis.com/css?family=PT+Serif|Roboto|Source+Sans+Pro|Vollkorn');</style> - <style type="text/css"> @font-face{font-family: "OpenDyslexic"; src:url("/assets/fonts/OpenDyslexic-Regular.otf") format('truetype');}</style> - </head><body></body></html> - """.trimIndent(), - transform( - """ - <?xml version="1.0" encoding="utf-8"?> - <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head xmlns:xlink="http://www.w3.org/1999/xlink"><title>Publication</title><link rel="stylesheet" href="style.css" type="text/css"/></head><body></body></html> - """.trimIndent() - ) - ) - } - - @Test - fun `Inject a reflowable with uppercase HEAD with attributes on several lines`() { - assertEquals( - """ - <?xml version="1.0" encoding="utf-8"?> - <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><HEAD - xmlns:xlink="http://www.w3.org/1999/xlink" - ><link rel="stylesheet" type="text/css" href="/assets/readium-css/ReadiumCSS-before.css"/> - <style> - audio[controls] { - width: revert; - height: revert; - } - </style> <style> - :root[style], :root { overflow: visible !important; } - :root[style] > body, :root > body { overflow: visible !important; } - </style><title>Publication</title><link rel="stylesheet" href="style.css" type="text/css"/><link rel="stylesheet" type="text/css" href="/assets/readium-css/ReadiumCSS-after.css"/> - <script type="text/javascript" src="/assets/scripts/readium-reflowable.js"></script> - <style>@import url('https://fonts.googleapis.com/css?family=PT+Serif|Roboto|Source+Sans+Pro|Vollkorn');</style> - <style type="text/css"> @font-face{font-family: "OpenDyslexic"; src:url("/assets/fonts/OpenDyslexic-Regular.otf") format('truetype');}</style> - </HEAD><body></body></html> - """.trimIndent(), - transform( - """ - <?xml version="1.0" encoding="utf-8"?> - <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><HEAD - xmlns:xlink="http://www.w3.org/1999/xlink" - ><title>Publication</title><link rel="stylesheet" href="style.css" type="text/css"/></HEAD><body></body></html> - """.trimIndent() - ) - ) - } - - private fun transform(content: String): String = runBlocking { - val sut = HtmlInjector( - publication = Publication(manifest = Manifest(metadata = Metadata(localizedTitle = LocalizedString("")))), - userPropertiesPath = null - ) - - val link = Link(href = "", type = "application/xhtml+xml") - - sut - .transform(StringResource(link, content)) - .readAsString() - .getOrThrow() - } -} diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/FallbackContentProtectionTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/FallbackContentProtectionTest.kt deleted file mode 100644 index d3d0aa8395..0000000000 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/FallbackContentProtectionTest.kt +++ /dev/null @@ -1,146 +0,0 @@ -package org.readium.r2.streamer.parser - -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Test -import org.junit.runner.RunWith -import org.readium.r2.shared.fetcher.FailureResource -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.StringResource -import org.readium.r2.shared.publication.ContentProtection.Scheme -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.util.mediatype.MediaType -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class FallbackContentProtectionTest { - - @Test - fun `Sniff no content protection`() { - assertNull(sniff(mediaType = MediaType.EPUB, resources = emptyMap())) - } - - @Test - fun `Sniff EPUB with empty encryption xml`() { - assertNull( - sniff( - mediaType = MediaType.EPUB, - resources = mapOf( - "/META-INF/encryption.xml" to """<?xml version='1.0' encoding='utf-8'?><encryption xmlns="urn:oasis:names:tc:opendocument:xmlns:container" xmlns:enc="http://www.w3.org/2001/04/xmlenc#"></encryption>""" - ) - ) - ) - } - - @Test - fun `Sniff LCP protected package`() { - assertEquals( - Scheme.Lcp, - sniff( - mediaType = MediaType.ZIP, - resources = mapOf( - "/license.lcpl" to "{}" - ) - ) - ) - } - - @Test - fun `Sniff LCP protected EPUB`() { - assertEquals( - Scheme.Lcp, - sniff( - mediaType = MediaType.EPUB, - resources = mapOf( - "/META-INF/license.lcpl" to "{}" - ) - ) - ) - } - - @Test - fun `Sniff LCP protected EPUB missing the license`() { - assertEquals( - Scheme.Lcp, - sniff( - mediaType = MediaType.EPUB, - resources = mapOf( - "/META-INF/encryption.xml" to """<?xml version="1.0" encoding="UTF-8"?> -<encryption xmlns="urn:oasis:names:tc:opendocument:xmlns:container"> - <EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#"> - <EncryptionMethod xmlns="http://www.w3.org/2001/04/xmlenc#" Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc"></EncryptionMethod> - <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"> - <RetrievalMethod xmlns="http://www.w3.org/2000/09/xmldsig#" URI="license.lcpl#/encryption/content_key" Type="http://readium.org/2014/01/lcp#EncryptedContentKey"></RetrievalMethod> - </KeyInfo> - <CipherData xmlns="http://www.w3.org/2001/04/xmlenc#"> - <CipherReference xmlns="http://www.w3.org/2001/04/xmlenc#" URI="OPS/chapter_001.xhtml"></CipherReference> - </CipherData> - <EncryptionProperties xmlns="http://www.w3.org/2001/04/xmlenc#"> - <EncryptionProperty xmlns="http://www.w3.org/2001/04/xmlenc#"> - <Compression xmlns="http://www.idpf.org/2016/encryption#compression" Method="8" OriginalLength="13877"></Compression> - </EncryptionProperty> - </EncryptionProperties> - </EncryptedData> -</encryption>""" - ) - ) - ) - } - - @Test - fun `Sniff Adobe ADEPT`() { - assertEquals( - Scheme.Adept, - sniff( - mediaType = MediaType.EPUB, - resources = mapOf( - "/META-INF/encryption.xml" to """<?xml version="1.0"?><encryption xmlns="urn:oasis:names:tc:opendocument:xmlns:container"> - <EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#"> - <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"></EncryptionMethod> - <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"> - <resource xmlns="http://ns.adobe.com/adept">urn:uuid:2c43729c-b985-4531-8e86-ae75ce5e5da9</resource> - </KeyInfo> - <CipherData> - <CipherReference URI="OEBPS/stylesheet.css"></CipherReference> - </CipherData> - </EncryptedData> - </encryption>""" - ) - ) - ) - } - - @Test - fun `Sniff Adobe ADEPT from rights xml`() { - assertEquals( - Scheme.Adept, - sniff( - mediaType = MediaType.EPUB, - resources = mapOf( - "/META-INF/encryption.xml" to """<?xml version='1.0' encoding='utf-8'?><encryption xmlns="urn:oasis:names:tc:opendocument:xmlns:container" xmlns:enc="http://www.w3.org/2001/04/xmlenc#"></encryption>""", - "/META-INF/rights.xml" to """<?xml version="1.0"?><adept:rights xmlns:adept="http://ns.adobe.com/adept"></adept:rights>""" - ) - ) - ) - } - - private fun sniff(mediaType: MediaType, resources: Map<String, String>): Scheme? = runBlocking { - FallbackContentProtection().sniffScheme( - fetcher = TestFetcher(resources), - mediaType = mediaType - ) - } -} - -class TestFetcher(private val resources: Map<String, String> = emptyMap()) : Fetcher { - - override suspend fun links(): List<Link> = resources.map { Link(href = it.key) } - - override fun get(link: Link): Resource = - resources[link.href]?.let { StringResource(link, it) } - ?: FailureResource(link, Resource.Exception.NotFound()) - - override suspend fun close() {} -} diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/audio/AudioLocatorServiceTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/audio/AudioLocatorServiceTest.kt index cc085ab295..1131667b2a 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/audio/AudioLocatorServiceTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/audio/AudioLocatorServiceTest.kt @@ -10,21 +10,31 @@ import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class AudioLocatorServiceTest { @Test fun `locate(Locator) matching reading order HREF`() = runBlocking { val service = AudioLocatorService( listOf( - Link("l1"), - Link("l2") + Link(Href("l1")!!), + Link(Href("l2")!!) ) ) - val locator = Locator("l1", type = "audio/mpeg", locations = Locator.Locations(totalProgression = 0.53)) + val locator = Locator( + Url("l1")!!, + mediaType = MediaType.MP3, + locations = Locator.Locations(totalProgression = 0.53) + ) assertEquals(locator, service.locate(locator)) } @@ -32,12 +42,16 @@ class AudioLocatorServiceTest { fun `locate(Locator) returns null if no match`() = runBlocking { val service = AudioLocatorService( listOf( - Link("l1"), - Link("l2") + Link(Href("l1")!!), + Link(Href("l2")!!) ) ) - val locator = Locator("l3", type = "audio/mpeg", locations = Locator.Locations(totalProgression = 0.53)) + val locator = Locator( + Url("l3")!!, + mediaType = MediaType.MP3, + locations = Locator.Locations(totalProgression = 0.53) + ) assertNull(service.locate(locator)) } @@ -45,45 +59,66 @@ class AudioLocatorServiceTest { fun `locate(Locator) uses totalProgression`() = runBlocking { val service = AudioLocatorService( listOf( - Link("l1", type = "audio/mpeg", duration = 100.0), - Link("l2", type = "audio/mpeg", duration = 100.0) + Link(Href("l1")!!, mediaType = MediaType.MP3, duration = 100.0), + Link(Href("l2")!!, mediaType = MediaType.MP3, duration = 100.0) ) ) assertEquals( Locator( - "l1", type = "audio/mpeg", + Url("l1")!!, + mediaType = MediaType.MP3, locations = Locator.Locations( fragments = listOf("t=98"), progression = 98 / 100.0, totalProgression = 0.49 ) ), - service.locate(Locator("wrong", type = "audio/mpeg", locations = Locator.Locations(totalProgression = 0.49))) + service.locate( + Locator( + Url("wrong")!!, + mediaType = MediaType.MP3, + locations = Locator.Locations(totalProgression = 0.49) + ) + ) ) assertEquals( Locator( - "l2", type = "audio/mpeg", + Url("l2")!!, + mediaType = MediaType.MP3, locations = Locator.Locations( fragments = listOf("t=0"), progression = 0.0, totalProgression = 0.5 ) ), - service.locate(Locator("wrong", type = "audio/mpeg", locations = Locator.Locations(totalProgression = 0.5))) + service.locate( + Locator( + Url("wrong")!!, + mediaType = MediaType.MP3, + locations = Locator.Locations(totalProgression = 0.5) + ) + ) ) assertEquals( Locator( - "l2", type = "audio/mpeg", + Url("l2")!!, + mediaType = MediaType.MP3, locations = Locator.Locations( fragments = listOf("t=2"), progression = 0.02, totalProgression = 0.51 ) ), - service.locate(Locator("wrong", type = "audio/mpeg", locations = Locator.Locations(totalProgression = 0.51))) + service.locate( + Locator( + Url("wrong")!!, + mediaType = MediaType.MP3, + locations = Locator.Locations(totalProgression = 0.51) + ) + ) ) } @@ -91,15 +126,15 @@ class AudioLocatorServiceTest { fun `locate(Locator) using totalProgression keeps title and text`() = runBlocking { val service = AudioLocatorService( listOf( - Link("l1", type = "audio/mpeg", duration = 100.0), - Link("l2", type = "audio/mpeg", duration = 100.0) + Link(Href("l1")!!, mediaType = MediaType.MP3, duration = 100.0), + Link(Href("l2")!!, mediaType = MediaType.MP3, duration = 100.0) ) ) assertEquals( Locator( - "l1", - type = "audio/mpeg", + Url("l1")!!, + mediaType = MediaType.MP3, title = "Title", locations = Locator.Locations( fragments = listOf("t=80"), @@ -110,8 +145,8 @@ class AudioLocatorServiceTest { ), service.locate( Locator( - "wrong", - type = "wrong-type", + Url("wrong")!!, + mediaType = MediaType.BINARY, title = "Title", locations = Locator.Locations( fragments = listOf("ignored"), @@ -130,14 +165,15 @@ class AudioLocatorServiceTest { fun `locate progression`() = runBlocking { val service = AudioLocatorService( listOf( - Link("l1", type = "audio/mpeg", duration = 100.0), - Link("l2", type = "audio/mpeg", duration = 100.0) + Link(Href("l1")!!, mediaType = MediaType.MP3, duration = 100.0), + Link(Href("l2")!!, mediaType = MediaType.MP3, duration = 100.0) ) ) assertEquals( Locator( - "l1", type = "audio/mpeg", + Url("l1")!!, + mediaType = MediaType.MP3, locations = Locator.Locations( fragments = listOf("t=0"), progression = 0.0, @@ -149,7 +185,8 @@ class AudioLocatorServiceTest { assertEquals( Locator( - "l1", type = "audio/mpeg", + Url("l1")!!, + mediaType = MediaType.MP3, locations = Locator.Locations( fragments = listOf("t=98"), progression = 98 / 100.0, @@ -161,7 +198,8 @@ class AudioLocatorServiceTest { assertEquals( Locator( - "l2", type = "audio/mpeg", + Url("l2")!!, + mediaType = MediaType.MP3, locations = Locator.Locations( fragments = listOf("t=0"), progression = 0.0, @@ -173,7 +211,8 @@ class AudioLocatorServiceTest { assertEquals( Locator( - "l2", type = "audio/mpeg", + Url("l2")!!, + mediaType = MediaType.MP3, locations = Locator.Locations( fragments = listOf("t=2"), progression = 0.02, @@ -185,7 +224,8 @@ class AudioLocatorServiceTest { assertEquals( Locator( - "l2", type = "audio/mpeg", + Url("l2")!!, + mediaType = MediaType.MP3, locations = Locator.Locations( fragments = listOf("t=100"), progression = 1.0, @@ -200,8 +240,8 @@ class AudioLocatorServiceTest { fun `locate invalid progression`() = runBlocking { val service = AudioLocatorService( listOf( - Link("l1", type = "audio/mpeg", duration = 100.0), - Link("l2", type = "audio/mpeg", duration = 100.0) + Link(Href("l1")!!, mediaType = MediaType.MP3, duration = 100.0), + Link(Href("l2")!!, mediaType = MediaType.MP3, duration = 100.0) ) ) diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt index 2d1d115207..4e4c2c8227 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt @@ -10,18 +10,16 @@ package org.readium.r2.streamer.parser.epub import java.io.File +import kotlin.test.* import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat -import org.junit.Assert.assertNotNull import org.junit.Test import org.junit.runner.RunWith -import org.readium.r2.shared.extensions.toMap -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.fetcher.FileFetcher -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.Properties import org.readium.r2.shared.publication.encryption.Encryption +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.checkSuccess +import org.readium.r2.shared.util.file.DirectoryContainer +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.streamer.readBlocking import org.robolectric.RobolectricTestRunner @@ -29,46 +27,42 @@ import org.robolectric.RobolectricTestRunner class EpubDeobfuscatorTest { private val identifier = "urn:uuid:36d5078e-ff7d-468e-a5f3-f47c14b91f2f" - private val transformer = EpubDeobfuscator(identifier) - private val fetcher: Fetcher - private val font: ByteArray - init { - val deobfuscationDir = EpubDeobfuscatorTest::class.java - .getResource("deobfuscation/cut-cut.woff") + private val deobfuscationDir = requireNotNull( + EpubDeobfuscatorTest::class.java + .getResource("deobfuscation") ?.path - ?.let { File(it).parentFile } - assertNotNull(deobfuscationDir) - fetcher = FileFetcher("/deobfuscation", deobfuscationDir!!) + ?.let { File(it) } + ) - val fontResult = fetcher.get(Link(href = "/deobfuscation/cut-cut.woff")).readBlocking() - assert(fontResult.isSuccess) - font = fontResult.getOrThrow() + private val container = runBlocking { + DirectoryContainer(deobfuscationDir).checkSuccess() } - private fun deobfuscate(href: String, algorithm: String?): Resource { - val encryption = algorithm?.let { - Encryption( - algorithm = algorithm - ).toJSON().toMap() - } - val properties = encryption?.let { - mapOf("encrypted" to it) - }.orEmpty() + private val font = requireNotNull(container[Url("cut-cut.woff")!!]) + .readBlocking() + .checkSuccess() + + private fun deobfuscate(url: Url, resource: Resource, algorithm: String?): Resource { + val encryptionData = + if (resource.sourceUrl != null && algorithm != null) { + mapOf(resource.sourceUrl as Url to Encryption(algorithm = algorithm)) + } else { + emptyMap() + } + + val deobfuscator = EpubDeobfuscator(identifier, encryptionData) - val obfuscatedRes = fetcher.get( - Link( - href = href, - properties = Properties(properties) - ) - ) - return transformer.transform(obfuscatedRes) + return deobfuscator.transform(url, resource) } @Test fun testIdpfDeobfuscation() { + val url = Url("cut-cut.obf.woff")!! + val resource = assertNotNull(container[url]) val deobfuscatedRes = deobfuscate( - "/deobfuscation/cut-cut.obf.woff", + url, + resource, "http://www.idpf.org/2008/embedding" ).readBlocking().getOrNull() assertThat(deobfuscatedRes).isEqualTo(font) @@ -77,18 +71,24 @@ class EpubDeobfuscatorTest { @Test fun testIdpfDeobfuscationWithRange() { runBlocking { + val url = Url("cut-cut.obf.woff")!! + val resource = assertNotNull(container[url]) val deobfuscatedRes = deobfuscate( - "/deobfuscation/cut-cut.obf.woff", + url, + resource, "http://www.idpf.org/2008/embedding" - ).read(20L until 40L).getOrThrow() + ).read(20L until 40L).checkSuccess() assertThat(deobfuscatedRes).isEqualTo(font.copyOfRange(20, 40)) } } @Test fun testAdobeDeobfuscation() { + val url = Url("cut-cut.adb.woff")!! + val resource = assertNotNull(container[url]) val deobfuscatedRes = deobfuscate( - "/deobfuscation/cut-cut.adb.woff", + url, + resource, "http://ns.adobe.com/pdf/enc#RC" ).readBlocking().getOrNull() assertThat(deobfuscatedRes).isEqualTo(font) @@ -96,8 +96,11 @@ class EpubDeobfuscatorTest { @Test fun `a resource is passed through when the link doesn't contain encryption data`() { + val url = Url("cut-cut.woff")!! + val resource = assertNotNull(container[url]) val deobfuscatedRes = deobfuscate( - "/deobfuscation/cut-cut.woff", + url, + resource, null ).readBlocking().getOrNull() assertThat(deobfuscatedRes).isEqualTo(font) @@ -105,8 +108,11 @@ class EpubDeobfuscatorTest { @Test fun `a resource is passed through when the algorithm is unknown`() { + val url = Url("cut-cut.woff")!! + val resource = assertNotNull(container[url]) val deobfuscatedRes = deobfuscate( - "/deobfuscation/cut-cut.woff", + url, + resource, "unknown algorithm" ).readBlocking().getOrNull() assertThat(deobfuscatedRes).isEqualTo(font) diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt index 0ab294b9fc..719db9b7ce 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt @@ -13,15 +13,20 @@ import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.readium.r2.shared.fetcher.Fetcher -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.fetcher.ResourceTry import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Properties import org.readium.r2.shared.publication.epub.EpubLayout import org.readium.r2.shared.publication.presentation.Presentation +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.archive.ArchiveProperties +import org.readium.r2.shared.util.archive.archive +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadTry +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -38,15 +43,15 @@ class EpubPositionsServiceTest { fun `Positions from a {readingOrder} with one resource`() { val service = createService( readingOrder = listOf( - Pair(1L, Link(href = "res", type = "application/xml")) + ReadingOrderItem(href = Url("res")!!, length = 1, type = MediaType.XML) ) ) assertEquals( listOf( Locator( - href = "res", - type = "application/xml", + href = Url("res")!!, + mediaType = MediaType.XML, locations = Locator.Locations( progression = 0.0, position = 1, @@ -62,17 +67,17 @@ class EpubPositionsServiceTest { fun `Positions from a {readingOrder} with a few resources`() { val service = createService( readingOrder = listOf( - Pair(1L, Link(href = "res")), - Pair(2L, Link(href = "chap1", type = "application/xml")), - Pair(2L, Link(href = "chap2", type = "text/html", title = "Chapter 2")) + ReadingOrderItem(Url("res")!!, length = 1), + ReadingOrderItem(Url("chap1")!!, length = 2, MediaType.XML), + ReadingOrderItem(Url("chap2")!!, length = 2, MediaType.XHTML, title = "Chapter 2") ) ) assertEquals( listOf( Locator( - href = "res", - type = "text/html", + href = Url("res")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 1, @@ -80,8 +85,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap1", - type = "application/xml", + href = Url("chap1")!!, + mediaType = MediaType.XML, locations = Locator.Locations( progression = 0.0, position = 2, @@ -89,8 +94,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap2", - type = "text/html", + href = Url("chap2")!!, + mediaType = MediaType.XHTML, title = "Chapter 2", locations = Locator.Locations( progression = 0.0, @@ -107,16 +112,16 @@ class EpubPositionsServiceTest { fun `{type} fallbacks on text-html`() { val service = createService( readingOrder = listOf( - Pair(1L, Link(href = "chap1", properties = createProperties(layout = EpubLayout.REFLOWABLE))), - Pair(1L, Link(href = "chap2", properties = createProperties(layout = EpubLayout.FIXED))) + ReadingOrderItem(Url("chap1")!!, length = 1, layout = EpubLayout.REFLOWABLE), + ReadingOrderItem(Url("chap2")!!, length = 1, layout = EpubLayout.FIXED) ) ) assertEquals( listOf( Locator( - href = "chap1", - type = "text/html", + href = Url("chap1")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 1, @@ -124,8 +129,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap2", - type = "text/html", + href = Url("chap2")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 2, @@ -142,17 +147,22 @@ class EpubPositionsServiceTest { val service = createService( layout = EpubLayout.FIXED, readingOrder = listOf( - Pair(10000L, Link(href = "res")), - Pair(20000L, Link(href = "chap1", type = "application/xml")), - Pair(40000L, Link(href = "chap2", type = "text/html", title = "Chapter 2")) + ReadingOrderItem(Url("res")!!, length = 10000), + ReadingOrderItem(Url("chap1")!!, length = 20000, MediaType.XML), + ReadingOrderItem( + Url("chap2")!!, + length = 40000, + MediaType.XHTML, + title = "Chapter 2" + ) ) ) assertEquals( listOf( Locator( - href = "res", - type = "text/html", + href = Url("res")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 1, @@ -160,8 +170,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap1", - type = "application/xml", + href = Url("chap1")!!, + mediaType = MediaType.XML, locations = Locator.Locations( progression = 0.0, position = 2, @@ -169,8 +179,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap2", - type = "text/html", + href = Url("chap2")!!, + mediaType = MediaType.XHTML, title = "Chapter 2", locations = Locator.Locations( progression = 0.0, @@ -188,20 +198,22 @@ class EpubPositionsServiceTest { val service = createService( layout = EpubLayout.REFLOWABLE, readingOrder = listOf( - Pair(0L, Link(href = "chap1")), - Pair(49L, Link(href = "chap2", type = "application/xml")), - Pair(50L, Link(href = "chap3", type = "text/html", title = "Chapter 3")), - Pair(51L, Link(href = "chap4")), - Pair(120L, Link(href = "chap5")) + ReadingOrderItem(Url("chap1")!!, length = 0), + ReadingOrderItem(Url("chap2")!!, length = 49, MediaType.XML), + ReadingOrderItem(Url("chap3")!!, length = 50, MediaType.XHTML, title = "Chapter 3"), + ReadingOrderItem(Url("chap4")!!, length = 51), + ReadingOrderItem(Url("chap5")!!, length = 120) ), - reflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength(pageLength = 50) + reflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength( + pageLength = 50 + ) ) assertEquals( listOf( Locator( - href = "chap1", - type = "text/html", + href = Url("chap1")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 1, @@ -209,8 +221,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap2", - type = "application/xml", + href = Url("chap2")!!, + mediaType = MediaType.XML, locations = Locator.Locations( progression = 0.0, position = 2, @@ -218,8 +230,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap3", - type = "text/html", + href = Url("chap3")!!, + mediaType = MediaType.XHTML, title = "Chapter 3", locations = Locator.Locations( progression = 0.0, @@ -228,8 +240,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap4", - type = "text/html", + href = Url("chap4")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 4, @@ -237,8 +249,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap4", - type = "text/html", + href = Url("chap4")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.5, position = 5, @@ -246,8 +258,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap5", - type = "text/html", + href = Url("chap5")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 6, @@ -255,8 +267,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap5", - type = "text/html", + href = Url("chap5")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 1.0 / 3.0, position = 7, @@ -264,8 +276,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap5", - type = "text/html", + href = Url("chap5")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 2.0 / 3.0, position = 8, @@ -283,16 +295,18 @@ class EpubPositionsServiceTest { val service = createService( layout = null, readingOrder = listOf( - Pair(60L, Link(href = "chap1")) + ReadingOrderItem(Url("chap1")!!, length = 60) ), - reflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength(pageLength = 50) + reflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength( + pageLength = 50 + ) ) assertEquals( listOf( Locator( - href = "chap1", - type = "text/html", + href = Url("chap1")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 1, @@ -300,8 +314,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap1", - type = "text/html", + href = Url("chap1")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.5, position = 2, @@ -318,18 +332,20 @@ class EpubPositionsServiceTest { val service = createService( layout = EpubLayout.FIXED, readingOrder = listOf( - Pair(20000L, Link(href = "chap1")), - Pair(60L, Link(href = "chap2", properties = createProperties(layout = EpubLayout.REFLOWABLE))), - Pair(20000L, Link(href = "chap3", properties = createProperties(layout = EpubLayout.FIXED))) + ReadingOrderItem(Url("chap1")!!, length = 20000), + ReadingOrderItem(Url("chap2")!!, length = 60, layout = EpubLayout.REFLOWABLE), + ReadingOrderItem(Url("chap3")!!, length = 20000, layout = EpubLayout.FIXED) ), - reflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength(pageLength = 50) + reflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength( + pageLength = 50 + ) ) assertEquals( listOf( Locator( - href = "chap1", - type = "text/html", + href = Url("chap1")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 1, @@ -337,8 +353,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap2", - type = "text/html", + href = Url("chap2")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 2, @@ -346,8 +362,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap2", - type = "text/html", + href = Url("chap2")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.5, position = 3, @@ -355,8 +371,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap3", - type = "text/html", + href = Url("chap3")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 4, @@ -373,29 +389,31 @@ class EpubPositionsServiceTest { val service = createService( layout = EpubLayout.REFLOWABLE, readingOrder = listOf( - Pair(60L, Link(href = "chap1", properties = createProperties(archiveEntryLength = 20L))), - Pair(60L, Link(href = "chap2")) + ReadingOrderItem(Url("chap1")!!, length = 60, archiveEntryLength = 20L), + ReadingOrderItem(Url("chap2")!!, length = 60) ), - reflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength(pageLength = 50) + reflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength( + pageLength = 50 + ) ) assertEquals( listOf( listOf( Locator( - href = "chap1", - type = "text/html", + href = Url("chap1")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 1, totalProgression = 0.0 ) - ), + ) ), listOf( Locator( - href = "chap2", - type = "text/html", + href = Url("chap2")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 2, @@ -403,8 +421,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap2", - type = "text/html", + href = Url("chap2")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.5, position = 3, @@ -422,17 +440,19 @@ class EpubPositionsServiceTest { val service = createService( layout = EpubLayout.REFLOWABLE, readingOrder = listOf( - Pair(60L, Link(href = "chap1", properties = createProperties(originalLength = 20L))), - Pair(60L, Link(href = "chap2")) + ReadingOrderItem(Url("chap1")!!, length = 60, originalLength = 20L), + ReadingOrderItem(Url("chap2")!!, length = 60) ), - reflowableStrategy = EpubPositionsService.ReflowableStrategy.OriginalLength(pageLength = 50) + reflowableStrategy = EpubPositionsService.ReflowableStrategy.OriginalLength( + pageLength = 50 + ) ) assertEquals( listOf( Locator( - href = "chap1", - type = "text/html", + href = Url("chap1")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 1, @@ -440,8 +460,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap2", - type = "text/html", + href = Url("chap2")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.0, position = 2, @@ -449,8 +469,8 @@ class EpubPositionsServiceTest { ) ), Locator( - href = "chap2", - type = "text/html", + href = Url("chap2")!!, + mediaType = MediaType.XHTML, locations = Locator.Locations( progression = 0.5, position = 3, @@ -464,27 +484,36 @@ class EpubPositionsServiceTest { private fun createService( layout: EpubLayout? = null, - readingOrder: List<Pair<Long, Link>>, - reflowableStrategy: EpubPositionsService.ReflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength(pageLength = 50) + readingOrder: List<ReadingOrderItem>, + reflowableStrategy: EpubPositionsService.ReflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength( + pageLength = 50 + ) ) = EpubPositionsService( - readingOrder = readingOrder.map { it.second }, - fetcher = object : Fetcher { + readingOrder = readingOrder.map { it.link }, + container = object : Container<Resource> { + + private fun find(relativePath: Url): ReadingOrderItem? = + readingOrder.find { it.link.url() == relativePath } + + override val entries: Set<Url> = readingOrder.map { it.href }.toSet() - private fun findResource(relativePath: String): Pair<Long, Link>? = - readingOrder.find { it.second.href == relativePath } + override fun get(url: Url): Resource { + val item = requireNotNull(find(url)) - override suspend fun links(): List<Link> = emptyList() + return object : Resource { - override fun get(link: Link): Resource = object : Resource { - override suspend fun link(): Link = link + override val sourceUrl: AbsoluteUrl? = null - override suspend fun length() = findResource(link.href) - ?.let { Try.success(it.first) } - ?: Try.failure(Resource.Exception.NotFound()) + override suspend fun properties(): ReadTry<Resource.Properties> = + Try.success(item.resourceProperties) - override suspend fun read(range: LongRange?): ResourceTry<ByteArray> = Try.success(ByteArray(0)) + override suspend fun length() = Try.success(item.length) - override suspend fun close() {} + override suspend fun read(range: LongRange?): ReadTry<ByteArray> = + Try.success(ByteArray(0)) + + override suspend fun close() {} + } } override suspend fun close() {} @@ -493,27 +522,44 @@ class EpubPositionsServiceTest { reflowableStrategy = reflowableStrategy ) - private fun createProperties( - layout: EpubLayout? = null, - archiveEntryLength: Long? = null, - originalLength: Long? = null - ): Properties { - val properties = mutableMapOf<String, Any>() - if (layout != null) { - properties["layout"] = layout.value - } - if (originalLength != null) { - properties["encrypted"] = mapOf( - "algorithm" to "algo", - "originalLength" to originalLength - ) - } - if (archiveEntryLength != null) { - properties["archive"] = mapOf( - "entryLength" to archiveEntryLength, - "isEntryCompressed" to true + class ReadingOrderItem( + val href: Url, + val length: Long, + val type: MediaType? = null, + val title: String? = null, + val archiveEntryLength: Long? = null, + val originalLength: Long? = null, + val layout: EpubLayout? = null + ) { + val link: Link = Link( + href = href, + mediaType = type, + title = title, + properties = Properties( + buildMap { + if (layout != null) { + put("layout", layout.value) + } + if (originalLength != null) { + put( + "encrypted", + mapOf( + "algorithm" to "algo", + "originalLength" to originalLength + ) + ) + } + } ) + ) + + val resourceProperties: Resource.Properties = Resource.Properties { + if (archiveEntryLength != null) { + archive = ArchiveProperties( + entryLength = archiveEntryLength, + isEntryCompressed = true + ) + } } - return Properties(otherProperties = properties) } } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/MetadataTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/MetadataTest.kt index 2f18533cf5..c3729a4fb6 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/MetadataTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/MetadataTest.kt @@ -20,6 +20,7 @@ import org.readium.r2.shared.publication.Link as SharedLink import org.readium.r2.shared.publication.epub.EpubLayout import org.readium.r2.shared.publication.presentation.Presentation import org.readium.r2.shared.publication.presentation.presentation +import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -50,7 +51,10 @@ class ContributorParsingTest { @Test fun `Unknown roles are ignored`() { - val contributor = Contributor(localizedName = LocalizedString("Contributor 2"), roles = setOf("unknown")) + val contributor = Contributor( + localizedName = LocalizedString("Contributor 2"), + roles = setOf("unknown") + ) assertThat(epub2Metadata.contributors).contains(contributor) assertThat(epub3Metadata.contributors).contains(contributor) } @@ -224,7 +228,9 @@ class TitleTest { @Test fun `The selected subtitle has the lowest display-seq property (epub3 only)`() { val metadata = parsePackageDocument("package/title-multiple-subtitles.opf").metadata - assertThat(metadata.localizedSubtitle).isEqualTo(LocalizedString.fromStrings(mapOf(null to "Subtitle 2"))) + assertThat(metadata.localizedSubtitle).isEqualTo( + LocalizedString.fromStrings(mapOf(null to "Subtitle 2")) + ) } } @@ -332,8 +338,8 @@ class MetadataMiscTest { @Test fun `Cover link is rightly identified`() { val expected = SharedLink( - href = "/OEBPS/cover.jpg", - type = "image/jpeg", + href = Href("OEBPS/cover.jpg")!!, + mediaType = MediaType.JPEG, rels = setOf("cover") ) assertThat(parsePackageDocument("package/cover-epub2.opf").resources.firstWithRel("cover")) @@ -356,9 +362,8 @@ class MetadataMiscTest { entry( Vocabularies.DCTERMS + "source", listOf( - "Feedbooks", mapOf("@value" to "Web", "http://my.url/#scheme" to "http"), - "Internet" + "Feedbooks" ) ), entry( @@ -416,7 +421,10 @@ class CollectionTest { @Test fun `Series with position are rightly computed`() { val expected = - Collection(localizedName = LocalizedString.fromStrings(mapOf("en" to "Series B")), position = 1.5) + Collection( + localizedName = LocalizedString.fromStrings(mapOf("en" to "Series B")), + position = 1.5 + ) assertThat(epub2Metadata.belongsToSeries).contains(expected) assertThat(epub3Metadata.belongsToSeries).contains(expected) } @@ -434,8 +442,12 @@ class AccessibilityTest { } @Test fun `conformsTo contains WCAG profiles and only them`() { - assertThat(epub2Metadata.accessibility?.conformsTo).containsExactlyInAnyOrder(Accessibility.Profile.EPUB_A11Y_10_WCAG_20_A) - assertThat(epub3Metadata.accessibility?.conformsTo).containsExactlyInAnyOrder(Accessibility.Profile.EPUB_A11Y_10_WCAG_20_A) + assertThat(epub2Metadata.accessibility?.conformsTo).containsExactlyInAnyOrder( + Accessibility.Profile.EPUB_A11Y_10_WCAG_20_A + ) + assertThat(epub3Metadata.accessibility?.conformsTo).containsExactlyInAnyOrder( + Accessibility.Profile.EPUB_A11Y_10_WCAG_20_A + ) } @Test fun `certification is rightly parsed`() { @@ -450,32 +462,53 @@ class AccessibilityTest { @Test fun `features are rightly parsed`() { assertThat(epub2Metadata.accessibility?.features) - .containsExactlyInAnyOrder(Accessibility.Feature.ALTERNATIVE_TEXT, Accessibility.Feature.STRUCTURAL_NAVIGATION) + .containsExactlyInAnyOrder( + Accessibility.Feature.ALTERNATIVE_TEXT, + Accessibility.Feature.STRUCTURAL_NAVIGATION + ) } @Test fun `hazards are rightly parsed`() { assertThat(epub2Metadata.accessibility?.hazards) - .containsExactlyInAnyOrder(Accessibility.Hazard.MOTION_SIMULATION, Accessibility.Hazard.NO_SOUND_HAZARD) + .containsExactlyInAnyOrder( + Accessibility.Hazard.MOTION_SIMULATION, + Accessibility.Hazard.NO_SOUND_HAZARD + ) assertThat(epub3Metadata.accessibility?.hazards) - .containsExactlyInAnyOrder(Accessibility.Hazard.MOTION_SIMULATION, Accessibility.Hazard.NO_SOUND_HAZARD) + .containsExactlyInAnyOrder( + Accessibility.Hazard.MOTION_SIMULATION, + Accessibility.Hazard.NO_SOUND_HAZARD + ) } @Test fun `accessModes are rightly parsed`() { assertThat(epub2Metadata.accessibility?.accessModes) - .containsExactlyInAnyOrder(Accessibility.AccessMode.VISUAL, Accessibility.AccessMode.TEXTUAL) + .containsExactlyInAnyOrder( + Accessibility.AccessMode.VISUAL, + Accessibility.AccessMode.TEXTUAL + ) assertThat(epub3Metadata.accessibility?.accessModes) - .containsExactlyInAnyOrder(Accessibility.AccessMode.VISUAL, Accessibility.AccessMode.TEXTUAL) + .containsExactlyInAnyOrder( + Accessibility.AccessMode.VISUAL, + Accessibility.AccessMode.TEXTUAL + ) } @Test fun `accessModesSufficient are rightly parsed`() { assertThat(epub2Metadata.accessibility?.accessModesSufficient) .containsExactlyInAnyOrder( - setOf(Accessibility.PrimaryAccessMode.VISUAL, Accessibility.PrimaryAccessMode.TEXTUAL), + setOf( + Accessibility.PrimaryAccessMode.VISUAL, + Accessibility.PrimaryAccessMode.TEXTUAL + ), setOf(Accessibility.PrimaryAccessMode.TEXTUAL) ) assertThat(epub3Metadata.accessibility?.accessModesSufficient) .containsExactlyInAnyOrder( - setOf(Accessibility.PrimaryAccessMode.VISUAL, Accessibility.PrimaryAccessMode.TEXTUAL), + setOf( + Accessibility.PrimaryAccessMode.VISUAL, + Accessibility.PrimaryAccessMode.TEXTUAL + ), setOf(Accessibility.PrimaryAccessMode.TEXTUAL) ) } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParserTest.kt index 78abbc2bd5..ee468858af 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParserTest.kt @@ -13,8 +13,10 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Assert.assertNotNull import org.junit.Test import org.junit.runner.RunWith -import org.readium.r2.shared.parser.xml.XmlParser +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.xml.XmlParser import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -23,7 +25,10 @@ class NavigationDocumentParserTest { val res = NavigationDocumentParser::class.java.getResourceAsStream(path) checkNotNull(res) val document = XmlParser().parse(res) - val navigationDocument = NavigationDocumentParser.parse(document, "/OEBPS/xhtml/nav.xhtml") + val navigationDocument = NavigationDocumentParser.parse( + document, + Url("OEBPS/xhtml/nav.xhtml")!! + ) assertNotNull(navigationDocument) return navigationDocument } @@ -37,61 +42,75 @@ class NavigationDocumentParserTest { @Test fun `nav can be a non-direct descendant of body`() { assertThat(navSection["toc"]).containsExactly( - Link(title = "Chapter 1", href = "/OEBPS/xhtml/chapter1.xhtml") + Link(title = "Chapter 1", href = Href("OEBPS/xhtml/chapter1.xhtml")!!) ) } @Test fun `Newlines are trimmed from title`() { assertThat(navTitles["toc"]).contains( - Link(title = "A link with new lines splitting the text", href = "/OEBPS/xhtml/chapter1.xhtml") + Link( + title = "A link with new lines splitting the text", + href = Href("OEBPS/xhtml/chapter1.xhtml")!! + ) ) } @Test fun `Spaces are trimmed from title`() { assertThat(navTitles["toc"]).contains( - Link(title = "A link with ignorable spaces", href = "/OEBPS/xhtml/chapter2.xhtml") + Link( + title = "A link with ignorable spaces", + href = Href("OEBPS/xhtml/chapter2.xhtml")!! + ) ) } @Test fun `Nested HTML elements are allowed in titles`() { assertThat(navTitles["toc"]).contains( - Link(title = "A link with nested HTML elements", href = "/OEBPS/xhtml/chapter3.xhtml") + Link( + title = "A link with nested HTML elements", + href = Href("OEBPS/xhtml/chapter3.xhtml")!! + ) ) } @Test fun `Entries with a zero-length title and no children are ignored`() { assertThat(navTitles["toc"]).doesNotContain( - Link(title = "", href = "/OEBPS/xhtml/chapter4.xhtml") + Link(title = "", href = Href("OEBPS/xhtml/chapter4.xhtml")!!) ) } @Test fun `Unlinked entries without children are ignored`() { assertThat(navTitles["toc"]).doesNotContain( - Link(title = "An unlinked element without children must be ignored", href = "#") + Link( + title = "An unlinked element without children must be ignored", + href = Href("#")!! + ) ) } @Test fun `Hierarchical items are allowed`() { assertThat(navChildren["toc"]).containsExactly( - Link(title = "Introduction", href = "/OEBPS/xhtml/introduction.xhtml"), + Link(title = "Introduction", href = Href("OEBPS/xhtml/introduction.xhtml")!!), Link( - title = "Part I", href = "#", + title = "Part I", + href = Href("#")!!, children = listOf( - Link(title = "Chapter 1", href = "/OEBPS/xhtml/part1/chapter1.xhtml"), - Link(title = "Chapter 2", href = "/OEBPS/xhtml/part1/chapter2.xhtml") + Link(title = "Chapter 1", href = Href("OEBPS/xhtml/part1/chapter1.xhtml")!!), + Link(title = "Chapter 2", href = Href("OEBPS/xhtml/part1/chapter2.xhtml")!!) ) ), Link( - title = "Part II", href = "/OEBPS/xhtml/part2/chapter1.xhtml", + title = "Part II", + href = Href("OEBPS/xhtml/part2/chapter1.xhtml")!!, children = listOf( - Link(title = "Chapter 1", href = "/OEBPS/xhtml/part2/chapter1.xhtml"), - Link(title = "Chapter 2", href = "/OEBPS/xhtml/part2/chapter2.xhtml") + Link(title = "Chapter 1", href = Href("OEBPS/xhtml/part2/chapter1.xhtml")!!), + Link(title = "Chapter 2", href = Href("OEBPS/xhtml/part2/chapter2.xhtml")!!) ) ) ) @@ -105,24 +124,24 @@ class NavigationDocumentParserTest { @Test fun `toc is rightly parsed`() { assertThat(navComplex["toc"]).containsExactly( - Link(title = "Chapter 1", href = "/OEBPS/xhtml/chapter1.xhtml"), - Link(title = "Chapter 2", href = "/OEBPS/xhtml/chapter2.xhtml") + Link(title = "Chapter 1", href = Href("OEBPS/xhtml/chapter1.xhtml")!!), + Link(title = "Chapter 2", href = Href("OEBPS/xhtml/chapter2.xhtml")!!) ) } @Test fun `landmarks are rightly parsed`() { assertThat(navComplex["landmarks"]).containsExactly( - Link(title = "Table of Contents", href = "/OEBPS/xhtml/nav.xhtml#toc"), - Link(title = "Begin Reading", href = "/OEBPS/xhtml/chapter1.xhtml") + Link(title = "Table of Contents", href = Href("OEBPS/xhtml/nav.xhtml#toc")!!), + Link(title = "Begin Reading", href = Href("OEBPS/xhtml/chapter1.xhtml")!!) ) } @Test fun `page-list is rightly parsed`() { assertThat(navComplex["page-list"]).containsExactly( - Link(title = "1", href = "/OEBPS/xhtml/chapter1.xhtml#page1"), - Link(title = "2", href = "/OEBPS/xhtml/chapter1.xhtml#page2") + Link(title = "1", href = Href("OEBPS/xhtml/chapter1.xhtml#page1")!!), + Link(title = "2", href = Href("OEBPS/xhtml/chapter1.xhtml#page2")!!) ) } } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/NcxParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/NcxParserTest.kt index 25ae8c8fef..d090a33f63 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/NcxParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/NcxParserTest.kt @@ -13,8 +13,10 @@ import org.assertj.core.api.Assertions import org.junit.Assert.assertNotNull import org.junit.Test import org.junit.runner.RunWith -import org.readium.r2.shared.parser.xml.XmlParser +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.xml.XmlParser import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -23,7 +25,7 @@ class NcxParserTest { val res = NcxParser::class.java.getResourceAsStream(path) checkNotNull(res) val document = XmlParser().parse(res) - val ncx = NcxParser.parse(document, "OEBPS/ncx.ncx") + val ncx = NcxParser.parse(document, Url("OEBPS/ncx.ncx")!!) assertNotNull(ncx) return ncx } @@ -36,47 +38,58 @@ class NcxParserTest { @Test fun `Newlines are trimmed from title`() { Assertions.assertThat(ncxTitles["toc"]).contains( - Link(title = "A link with new lines splitting the text", href = "/OEBPS/xhtml/chapter1.xhtml") + Link( + title = "A link with new lines splitting the text", + href = Href("OEBPS/xhtml/chapter1.xhtml")!! + ) ) } @Test fun `Spaces are trimmed from title`() { Assertions.assertThat(ncxTitles["toc"]).contains( - Link(title = "A link with ignorable spaces", href = "/OEBPS/xhtml/chapter2.xhtml") + Link( + title = "A link with ignorable spaces", + href = Href("OEBPS/xhtml/chapter2.xhtml")!! + ) ) } @Test fun `Entries with a zero-length title and no children are ignored`() { Assertions.assertThat(ncxTitles["toc"]).doesNotContain( - Link(title = "", href = "/OEBPS/xhtml/chapter3.xhtml") + Link(title = "", href = Href("OEBPS/xhtml/chapter3.xhtml")!!) ) } @Test fun `Unlinked entries without children are ignored`() { Assertions.assertThat(ncxTitles["toc"]).doesNotContain( - Link(title = "An unlinked element without children must be ignored", href = "#") + Link( + title = "An unlinked element without children must be ignored", + href = Href("#")!! + ) ) } @Test fun `Hierarchical items are allowed`() { Assertions.assertThat(ncxChildren["toc"]).containsExactly( - Link(title = "Introduction", href = "/OEBPS/xhtml/introduction.xhtml"), + Link(title = "Introduction", href = Href("OEBPS/xhtml/introduction.xhtml")!!), Link( - title = "Part I", href = "#", + title = "Part I", + href = Href("#")!!, children = listOf( - Link(title = "Chapter 1", href = "/OEBPS/xhtml/part1/chapter1.xhtml"), - Link(title = "Chapter 2", href = "/OEBPS/xhtml/part1/chapter2.xhtml") + Link(title = "Chapter 1", href = Href("OEBPS/xhtml/part1/chapter1.xhtml")!!), + Link(title = "Chapter 2", href = Href("OEBPS/xhtml/part1/chapter2.xhtml")!!) ) ), Link( - title = "Part II", href = "/OEBPS/xhtml/part2/chapter1.xhtml", + title = "Part II", + href = Href("OEBPS/xhtml/part2/chapter1.xhtml")!!, children = listOf( - Link(title = "Chapter 1", href = "/OEBPS/xhtml/part2/chapter1.xhtml"), - Link(title = "Chapter 2", href = "/OEBPS/xhtml/part2/chapter2.xhtml") + Link(title = "Chapter 1", href = Href("OEBPS/xhtml/part2/chapter1.xhtml")!!), + Link(title = "Chapter 2", href = Href("OEBPS/xhtml/part2/chapter2.xhtml")!!) ) ) ) @@ -90,16 +103,16 @@ class NcxParserTest { @Test fun `toc is rightly parsed`() { Assertions.assertThat(ncxComplex["toc"]).containsExactly( - Link(title = "Chapter 1", href = "/OEBPS/xhtml/chapter1.xhtml"), - Link(title = "Chapter 2", href = "/OEBPS/xhtml/chapter2.xhtml") + Link(title = "Chapter 1", href = Href("OEBPS/xhtml/chapter1.xhtml")!!), + Link(title = "Chapter 2", href = Href("OEBPS/xhtml/chapter2.xhtml")!!) ) } @Test fun `page list is rightly parsed`() { Assertions.assertThat(ncxComplex["page-list"]).containsExactly( - Link(title = "1", href = "/OEBPS/xhtml/chapter1.xhtml#page1"), - Link(title = "2", href = "/OEBPS/xhtml/chapter1.xhtml#page2") + Link(title = "1", href = Href("OEBPS/xhtml/chapter1.xhtml#page1")!!), + Link(title = "2", href = Href("OEBPS/xhtml/chapter1.xhtml#page2")!!) ) } } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt index 2dc783a088..3c5eeef5b8 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt @@ -12,7 +12,7 @@ package org.readium.r2.streamer.parser.epub import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.junit.runner.RunWith -import org.readium.r2.shared.parser.xml.XmlParser +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.ReadingProgression @@ -20,13 +20,16 @@ import org.readium.r2.shared.publication.epub.EpubLayout import org.readium.r2.shared.publication.epub.contains import org.readium.r2.shared.publication.epub.layout import org.readium.r2.shared.publication.presentation.* +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.xml.XmlParser import org.robolectric.RobolectricTestRunner fun parsePackageDocument(path: String): Manifest { val pub = PackageDocument::class.java.getResourceAsStream(path) ?.let { XmlParser().parse(it) } - ?.let { PackageDocument.parse(it, "OEBPS/content.opf") } - ?.let { ManifestAdapter("fallback title", it) } + ?.let { PackageDocument.parse(it, Url("OEBPS/content.opf")!!) } + ?.let { ManifestAdapter(it) } ?.adapt() checkNotNull(pub) return pub @@ -39,13 +42,15 @@ class ReadingProgressionTest { @Test fun `No page progression direction is mapped to default`() { assertThat(parsePackageDocument("package/progression-none.opf").metadata.readingProgression) - .isEqualTo(ReadingProgression.AUTO) + .isEqualTo(null) } @Test fun `Default page progression direction is rightly parsed`() { - assertThat(parsePackageDocument("package/progression-default.opf").metadata.readingProgression) - .isEqualTo(ReadingProgression.AUTO) + assertThat( + parsePackageDocument("package/progression-default.opf").metadata.readingProgression + ) + .isEqualTo(null) } @Test @@ -69,7 +74,9 @@ class LinkPropertyTest { fun `contains is rightly filled`() { with(propertiesPub) { assertThat(readingOrder[0].properties.contains).containsExactlyInAnyOrder("mathml") - assertThat(readingOrder[1].properties.contains).containsExactlyInAnyOrder("remote-resources") + assertThat(readingOrder[1].properties.contains).containsExactlyInAnyOrder( + "remote-resources" + ) assertThat(readingOrder[2].properties.contains).containsExactlyInAnyOrder("js", "svg") assertThat(readingOrder[3].properties.contains).isEmpty() assertThat(readingOrder[4].properties.contains).isEmpty() @@ -93,24 +100,36 @@ class LinkPropertyTest { with(propertiesPub) { assertThat(readingOrder[0].properties.layout).isEqualTo(EpubLayout.FIXED) assertThat(readingOrder[0].properties.overflow).isEqualTo(Presentation.Overflow.AUTO) - assertThat(readingOrder[0].properties.orientation).isEqualTo(Presentation.Orientation.AUTO) + assertThat(readingOrder[0].properties.orientation).isEqualTo( + Presentation.Orientation.AUTO + ) assertThat(readingOrder[0].properties.page).isEqualTo(Presentation.Page.RIGHT) assertThat(readingOrder[0].properties.spread).isNull() assertThat(readingOrder[1].properties.layout).isEqualTo(EpubLayout.REFLOWABLE) - assertThat(readingOrder[1].properties.overflow).isEqualTo(Presentation.Overflow.PAGINATED) - assertThat(readingOrder[1].properties.orientation).isEqualTo(Presentation.Orientation.LANDSCAPE) + assertThat(readingOrder[1].properties.overflow).isEqualTo( + Presentation.Overflow.PAGINATED + ) + assertThat(readingOrder[1].properties.orientation).isEqualTo( + Presentation.Orientation.LANDSCAPE + ) assertThat(readingOrder[1].properties.page).isEqualTo(Presentation.Page.LEFT) assertThat(readingOrder[0].properties.spread).isNull() assertThat(readingOrder[2].properties.layout).isNull() - assertThat(readingOrder[2].properties.overflow).isEqualTo(Presentation.Overflow.SCROLLED) - assertThat(readingOrder[2].properties.orientation).isEqualTo(Presentation.Orientation.PORTRAIT) + assertThat(readingOrder[2].properties.overflow).isEqualTo( + Presentation.Overflow.SCROLLED + ) + assertThat(readingOrder[2].properties.orientation).isEqualTo( + Presentation.Orientation.PORTRAIT + ) assertThat(readingOrder[2].properties.page).isEqualTo(Presentation.Page.CENTER) assertThat(readingOrder[2].properties.spread).isNull() assertThat(readingOrder[3].properties.layout).isNull() - assertThat(readingOrder[3].properties.overflow).isEqualTo(Presentation.Overflow.SCROLLED) + assertThat(readingOrder[3].properties.overflow).isEqualTo( + Presentation.Overflow.SCROLLED + ) assertThat(readingOrder[3].properties.orientation).isNull() assertThat(readingOrder[3].properties.page).isNull() assertThat(readingOrder[3].properties.spread).isEqualTo(Presentation.Spread.AUTO) @@ -126,12 +145,12 @@ class LinkTest { fun `readingOrder is rightly computed`() { assertThat(resourcesPub.readingOrder).containsExactly( Link( - href = "/titlepage.xhtml", - type = "application/xhtml+xml" + href = Href("titlepage.xhtml")!!, + mediaType = MediaType.XHTML ), Link( - href = "/OEBPS/chapter01.xhtml", - type = "application/xhtml+xml" + href = Href("OEBPS/chapter01.xhtml")!!, + mediaType = MediaType.XHTML ) ) } @@ -140,42 +159,42 @@ class LinkTest { fun `resources are rightly computed`() { assertThat(resourcesPub.resources).containsExactlyInAnyOrder( Link( - href = "/OEBPS/fonts/MinionPro.otf", - type = "application/vnd.ms-opentype" + href = Href("OEBPS/fonts/MinionPro.otf")!!, + mediaType = MediaType("application/vnd.ms-opentype")!! ), Link( - href = "/OEBPS/nav.xhtml", - type = "application/xhtml+xml", + href = Href("OEBPS/nav.xhtml")!!, + mediaType = MediaType.XHTML, rels = setOf("contents") ), Link( - href = "/style.css", - type = "text/css" + href = Href("style.css")!!, + mediaType = MediaType.CSS ), Link( - href = "/OEBPS/chapter01.smil", - type = "application/smil+xml" + href = Href("OEBPS/chapter01.smil")!!, + mediaType = MediaType.SMIL ), Link( - href = "/OEBPS/chapter02.smil", - type = "application/smil+xml", + href = Href("OEBPS/chapter02.smil")!!, + mediaType = MediaType.SMIL, duration = 1949.0 ), Link( - href = "/OEBPS/images/alice01a.png", - type = "image/png", + href = Href("OEBPS/images/alice01a.png")!!, + mediaType = MediaType.PNG, rels = setOf("cover") ), Link( - href = "/OEBPS/images/alice02a.gif", - type = "image/gif" + href = Href("OEBPS/images/alice02a.gif")!!, + mediaType = MediaType.GIF ), Link( - href = "/OEBPS/chapter02.xhtml", - type = "application/xhtml+xml" + href = Href("OEBPS/chapter02.xhtml")!!, + mediaType = MediaType.XHTML ), Link( - href = "/OEBPS/nomediatype.txt" + href = Href("OEBPS/nomediatype.txt")!! ) ) } @@ -186,16 +205,16 @@ class LinkMiscTest { fun `Fallbacks are mapped to alternates`() { assertThat(parsePackageDocument("package/fallbacks.opf")).isEqualTo( Link( - href = "/OEBPS/chap1_docbook.xml", - type = "application/docbook+xml", + href = Href("OEBPS/chap1_docbook.xml")!!, + mediaType = MediaType("application/docbook+xml")!!, alternates = listOf( Link( - href = "/OEBPS/chap1.xml", - type = "application/z3998-auth+xml", + href = Href("OEBPS/chap1.xml")!!, + mediaType = MediaType("application/z3998-auth+xml")!!, alternates = listOf( Link( - href = "/OEBPS/chap1.xhtml", - type = "application/xhtml+xml" + href = Href("OEBPS/chap1.xhtml")!!, + mediaType = MediaType.XHTML ) ) ) diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PropertyDataTypeTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PropertyDataTypeTest.kt index 8adcb94072..86f90ce72d 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PropertyDataTypeTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PropertyDataTypeTest.kt @@ -25,7 +25,9 @@ class ParsePrefixesTest { @Test fun `Space between prefixes and iris can be ommited`() { - val prefixes = parsePrefixes("foaf: http://xmlns.com/foaf/spec/ dbp:http://dbpedia.org/ontology/") + val prefixes = parsePrefixes( + "foaf: http://xmlns.com/foaf/spec/ dbp:http://dbpedia.org/ontology/" + ) assertThat(prefixes).contains( entry("foaf", "http://xmlns.com/foaf/spec/"), entry("dbp", "http://dbpedia.org/ontology/") @@ -35,7 +37,9 @@ class ParsePrefixesTest { @Test fun `Multiple prefixes are rightly parsed`() { - val prefixes = parsePrefixes("foaf: http://xmlns.com/foaf/spec/ dbp: http://dbpedia.org/ontology/") + val prefixes = parsePrefixes( + "foaf: http://xmlns.com/foaf/spec/ dbp: http://dbpedia.org/ontology/" + ) assertThat(prefixes).contains( entry("foaf", "http://xmlns.com/foaf/spec/"), entry("dbp", "http://dbpedia.org/ontology/") diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index 342492ec3e..6fee0b8388 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -15,75 +15,113 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Test -import org.readium.r2.shared.fetcher.Fetcher +import org.junit.runner.RunWith import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.asset.FileAsset -import org.readium.r2.shared.publication.asset.PublicationAsset import org.readium.r2.shared.publication.firstWithRel -import org.readium.r2.shared.util.archive.DefaultArchiveFactory +import org.readium.r2.shared.util.FileExtension +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset +import org.readium.r2.shared.util.checkSuccess +import org.readium.r2.shared.util.file.FileResource +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatSpecification +import org.readium.r2.shared.util.format.InformalComicSpecification +import org.readium.r2.shared.util.format.JpegSpecification +import org.readium.r2.shared.util.format.ZipSpecification +import org.readium.r2.shared.util.http.DefaultHttpClient +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.zip.ZipArchiveOpener import org.readium.r2.streamer.parseBlocking +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +@RunWith(RobolectricTestRunner::class) class ImageParserTest { - private val parser = ImageParser() + private val archiveOpener = ZipArchiveOpener() - private val cbzAsset = assetForResource("futuristic_tales.cbz") - private val cbzFetcher = fetcherForAsset(cbzAsset) + private val contentResolver = RuntimeEnvironment.getApplication().contentResolver - private val jpgAsset = assetForResource("futuristic_tales.jpg") - private val jpgFetcher = fetcherForAsset(jpgAsset) + private val assetSniffer = AssetRetriever(contentResolver, DefaultHttpClient()) - private fun assetForResource(resource: String): PublicationAsset { - val path = ImageParserTest::class.java.getResource(resource)?.path - assertNotNull(path) - return FileAsset(File(path!!)) + private val parser = ImageParser(assetSniffer) + + private val cbzAsset = runBlocking { + val file = fileForResource("futuristic_tales.cbz") + val resource = FileResource(file) + val format = Format( + specification = FormatSpecification(ZipSpecification, InformalComicSpecification), + mediaType = MediaType.CBZ, + fileExtension = FileExtension("cbz") + ) + val archive = archiveOpener.open(format, resource).checkSuccess() + ContainerAsset(format, archive.container) } - private fun fetcherForAsset(asset: PublicationAsset): Fetcher = runBlocking { - asset.createFetcher(PublicationAsset.Dependencies(DefaultArchiveFactory()), credentials = null).getOrThrow() + private val jpgAsset = runBlocking { + val file = fileForResource("futuristic_tales.jpg") + val resource = FileResource(file) + val format = Format( + specification = FormatSpecification(JpegSpecification), + mediaType = MediaType.JPEG, + fileExtension = FileExtension("jpg") + ) + ResourceAsset(format, resource) + } + + private fun fileForResource(resource: String): File { + val path = ImageParserTest::class.java.getResource(resource)?.path + return File(requireNotNull(path)) } @Test fun `CBZ is accepted`() { - assertNotNull(parser.parseBlocking(cbzAsset, cbzFetcher)) + assertNotNull(parser.parseBlocking(cbzAsset)) } @Test fun `JPG is accepted`() { - assertNotNull(parser.parseBlocking(jpgAsset, jpgFetcher)) + assertNotNull(parser.parseBlocking(jpgAsset)) } @Test fun `conformsTo contains the Divina profile`() { - assertEquals(setOf(Publication.Profile.DIVINA), parser.parseBlocking(cbzAsset, cbzFetcher)?.manifest?.metadata?.conformsTo) + val manifest = parser.parseBlocking(cbzAsset)?.manifest + assertEquals(setOf(Publication.Profile.DIVINA), manifest?.metadata?.conformsTo) } @Test fun `readingOrder is sorted alphabetically`() { - val builder = parser.parseBlocking(cbzAsset, cbzFetcher) + val builder = parser.parseBlocking(cbzAsset) assertNotNull(builder) + val base = Url.fromDecodedPath("Cory Doctorow's Futuristic Tales of the Here and Now/")!! val readingOrder = builder!!.manifest.readingOrder - .map { it.href.removePrefix("/Cory Doctorow's Futuristic Tales of the Here and Now") } + .map { base.relativize(it.url()).toString() } assertThat(readingOrder) - .containsExactly("/a-fc.jpg", "/x-002.jpg", "/x-003.jpg", "/x-004.jpg") + .containsExactly("a-fc.jpg", "x-002.jpg", "x-003.jpg", "x-004.jpg") } @Test fun `the cover is the first item in the readingOrder`() { - val builder = parser.parseBlocking(cbzAsset, cbzFetcher) + val builder = parser.parseBlocking(cbzAsset) assertNotNull(builder) with(builder!!.manifest.readingOrder) { assertEquals( - "/Cory Doctorow's Futuristic Tales of the Here and Now/a-fc.jpg", - firstWithRel("cover")?.href + Url.fromDecodedPath("Cory Doctorow's Futuristic Tales of the Here and Now/a-fc.jpg"), + firstWithRel("cover")?.url() ) } } @Test fun `title is based on archive's root directory when any`() { - val builder = parser.parseBlocking(cbzAsset, cbzFetcher) + val builder = parser.parseBlocking(cbzAsset) assertNotNull(builder) - assertEquals("Cory Doctorow's Futuristic Tales of the Here and Now", builder!!.manifest.metadata.title) + assertEquals( + "Cory Doctorow's Futuristic Tales of the Here and Now", + builder!!.manifest.metadata.title + ) } } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/pdf/PdfPositionsServiceTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/pdf/PdfPositionsServiceTest.kt index 4935bff016..ce9ac826fb 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/pdf/PdfPositionsServiceTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/pdf/PdfPositionsServiceTest.kt @@ -11,9 +11,15 @@ package org.readium.r2.streamer.parser.pdf import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class PdfPositionsServiceTest { @Test @@ -31,8 +37,8 @@ class PdfPositionsServiceTest { listOf( listOf( Locator( - href = "/publication.pdf", - type = "application/pdf", + href = Url("publication.pdf")!!, + mediaType = MediaType.PDF, locations = Locator.Locations( fragments = listOf("page=1"), progression = 0.0, @@ -56,8 +62,8 @@ class PdfPositionsServiceTest { listOf( listOf( Locator( - href = "/publication.pdf", - type = "application/pdf", + href = Url("publication.pdf")!!, + mediaType = MediaType.PDF, locations = Locator.Locations( fragments = listOf("page=1"), progression = 0.0, @@ -66,8 +72,8 @@ class PdfPositionsServiceTest { ) ), Locator( - href = "/publication.pdf", - type = "application/pdf", + href = Url("publication.pdf")!!, + mediaType = MediaType.PDF, locations = Locator.Locations( fragments = listOf("page=2"), progression = 1.0 / 3.0, @@ -76,8 +82,8 @@ class PdfPositionsServiceTest { ) ), Locator( - href = "/publication.pdf", - type = "application/pdf", + href = Url("publication.pdf")!!, + mediaType = MediaType.PDF, locations = Locator.Locations( fragments = listOf("page=3"), progression = 2.0 / 3.0, @@ -92,7 +98,7 @@ class PdfPositionsServiceTest { } private fun createService( - link: Link = Link(href = "/publication.pdf"), + link: Link = Link(href = Href("publication.pdf")!!), pageCount: Int ) = PdfPositionsService( link = link, diff --git a/settings.gradle.kts b/settings.gradle.kts index c1b9f5d53a..7fa3925861 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,13 +18,16 @@ pluginManagement { // it to integrate Readium in submodules. // See https://github.com/readium/kotlin-toolkit/pull/97 plugins { - id("com.android.application") version ("7.3.1") - id("com.android.library") version ("7.3.1") - id("io.github.gradle-nexus.publish-plugin") version ("1.1.0") - id("org.jetbrains.dokka") version ("1.7.20") - id("org.jetbrains.kotlin.android") version ("1.7.20") - id("org.jetbrains.kotlin.plugin.serialization") version ("1.7.10") - id("org.jlleitschuh.gradle.ktlint") version ("11.0.0") + id("com.android.application") version ("8.2.1") + id("com.android.library") version ("8.2.1") + id("io.github.gradle-nexus.publish-plugin") version ("1.3.0") + id("org.jetbrains.dokka") version ("1.9.10") + id("org.jetbrains.kotlin.android") version ("1.9.22") + id("org.jetbrains.kotlin.plugin.serialization") version ("1.9.22") + id("org.jlleitschuh.gradle.ktlint") version ("11.5.1") + // Make sure to align with the Kotlin version. + // See https://github.com/google/ksp/releases + id("com.google.devtools.ksp") version ("1.9.22-1.0.16") } } dependencyResolutionManagement { @@ -41,20 +44,20 @@ dependencyResolutionManagement { rootProject.name = "Readium" -include(":readium:adapters:pdfium:pdfium-document") -project(":readium:adapters:pdfium:pdfium-document") +include(":readium:adapters:pdfium:document") +project(":readium:adapters:pdfium:document") .name = "readium-adapter-pdfium-document" -include(":readium:adapters:pdfium:pdfium-navigator") -project(":readium:adapters:pdfium:pdfium-navigator") +include(":readium:adapters:pdfium:navigator") +project(":readium:adapters:pdfium:navigator") .name = "readium-adapter-pdfium-navigator" -include(":readium:adapters:pspdfkit:pspdfkit-document") -project(":readium:adapters:pspdfkit:pspdfkit-document") +include(":readium:adapters:pspdfkit:document") +project(":readium:adapters:pspdfkit:document") .name = "readium-adapter-pspdfkit-document" -include(":readium:adapters:pspdfkit:pspdfkit-navigator") -project(":readium:adapters:pspdfkit:pspdfkit-navigator") +include(":readium:adapters:pspdfkit:navigator") +project(":readium:adapters:pspdfkit:navigator") .name = "readium-adapter-pspdfkit-navigator" include(":readium:lcp") @@ -65,10 +68,26 @@ include(":readium:navigator") project(":readium:navigator") .name = "readium-navigator" +include(":readium:navigators:media:common") +project(":readium:navigators:media:common") + .name = "readium-navigator-media-common" + +include(":readium:navigators:media:audio") +project(":readium:navigators:media:audio") + .name = "readium-navigator-media-audio" + +include(":readium:navigators:media:tts") +project(":readium:navigators:media:tts") + .name = "readium-navigator-media-tts" + include(":readium:navigator-media2") project(":readium:navigator-media2") .name = "readium-navigator-media2" +include(":readium:adapters:exoplayer:audio") +project(":readium:adapters:exoplayer:audio") + .name = "readium-adapter-exoplayer-audio" + include(":readium:opds") project(":readium:opds") .name = "readium-opds" diff --git a/test-app/build.gradle.kts b/test-app/build.gradle.kts index ede0e9659b..c90b6c5e69 100644 --- a/test-app/build.gradle.kts +++ b/test-app/build.gradle.kts @@ -7,15 +7,15 @@ plugins { id("com.android.application") kotlin("android") - kotlin("kapt") + id("com.google.devtools.ksp") kotlin("plugin.parcelize") } android { - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 21 - targetSdk = 33 + targetSdk = 34 applicationId = "org.readium.r2reader" @@ -28,11 +28,11 @@ android { ndk.abiFilters.add("x86_64") } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn" } composeOptions { @@ -41,6 +41,7 @@ android { buildFeatures { viewBinding = true compose = true + buildConfig = true } buildTypes { getByName("release") { @@ -48,7 +49,7 @@ android { proguardFiles(getDefaultProguardFile("proguard-android.txt")) } } - packagingOptions { + packaging { resources.excludes.add("META-INF/*") } @@ -69,12 +70,18 @@ dependencies { implementation(project(":readium:readium-shared")) implementation(project(":readium:readium-streamer")) implementation(project(":readium:readium-navigator")) + implementation(project(":readium:navigators:media:readium-navigator-media-audio")) + implementation(project(":readium:navigators:media:readium-navigator-media-tts")) + // Only required if you want to support audiobooks using ExoPlayer. + implementation(project(":readium:adapters:exoplayer")) implementation(project(":readium:readium-navigator-media2")) implementation(project(":readium:readium-opds")) implementation(project(":readium:readium-lcp")) // Only required if you want to support PDF files using PDFium. implementation(project(":readium:adapters:pdfium")) + implementation(libs.accompanist.themeadapter.material) + implementation(libs.androidx.compose.activity) implementation(libs.androidx.activity.ktx) implementation(libs.androidx.appcompat) @@ -102,12 +109,11 @@ dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.jsoup) - implementation(libs.bundles.media2) implementation(libs.bundles.media3) // Room database implementation(libs.bundles.room) - kapt(libs.androidx.room.compiler) + ksp(libs.androidx.room.compiler) // Tests testImplementation(libs.junit) diff --git a/test-app/src/main/AndroidManifest.xml b/test-app/src/main/AndroidManifest.xml index 129d540698..c3ef1ad4fb 100644 --- a/test-app/src/main/AndroidManifest.xml +++ b/test-app/src/main/AndroidManifest.xml @@ -1,13 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> @@ -15,21 +11,27 @@ <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> + <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> + <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> + + <queries> + <intent> + <action android:name="android.intent.action.TTS_SERVICE" /> + </intent> + </queries> <!-- android:networkSecurityConfig is required for r2-lcp-kotlin and r2-navigator-kotlin --> <application android:name=".Application" - android:allowBackup="false" android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" + android:label="Readium" android:supportsRtl="true" android:theme="@style/AppTheme" android:networkSecurityConfig="@xml/network_security_config" - tools:replace="android:allowBackup" tools:targetApi="n"> <activity android:name=".MainActivity" android:clearTaskOnLaunch="true" - android:configChanges="orientation|screenSize" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> @@ -39,7 +41,7 @@ </activity> <activity - android:name=".utils.R2DispatcherActivity" + android:name=".utils.ImportActivity" android:launchMode="singleInstance" android:noHistory="true" android:theme="@android:style/Theme.NoDisplay" @@ -189,7 +191,7 @@ android:name=".reader.ReaderActivity" android:label="Reader" /> - <service android:name=".reader.tts.TtsService" + <service android:name=".reader.MediaService" android:enabled="true" android:exported="true" android:foregroundServiceType="mediaPlayback" @@ -197,17 +199,8 @@ <intent-filter> <action android:name="androidx.media3.session.MediaSessionService"/> - <action android:name="android.media.session.MediaSessionService" /> - </intent-filter> - </service> - - <service android:name=".reader.MediaService" - android:enabled="true" - android:exported="true" - tools:ignore="ExportedService"> - - <intent-filter> <action android:name="androidx.media2.session.MediaSessionService"/> + <action android:name="android.media.session.MediaSessionService" /> </intent-filter> </service> diff --git a/test-app/src/main/assets/readium/error.xhtml b/test-app/src/main/assets/readium/error.xhtml deleted file mode 100644 index 66d7f90ca8..0000000000 --- a/test-app/src/main/assets/readium/error.xhtml +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" - "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> - -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> -<head> - <title>${error}</title> -</head> -<body style="text-align: center;"> -<h1 style="font-size: 5em;">${error}</h1> -<h2><pre style="white-space: pre-wrap;">${href}</pre></h2> -</body> -</html> diff --git a/test-app/src/main/java/org/readium/r2/testapp/Application.kt b/test-app/src/main/java/org/readium/r2/testapp/Application.kt index accdf9c510..be3dca948a 100755 --- a/test-app/src/main/java/org/readium/r2/testapp/Application.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Application.kt @@ -6,17 +6,29 @@ package org.readium.r2.testapp -import android.content.* +import android.content.Context +import android.os.Build +import android.os.StrictMode import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore import com.google.android.material.color.DynamicColors import java.io.File -import java.util.* -import kotlinx.coroutines.* +import java.util.Properties +import java.util.concurrent.Executors +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope import org.readium.r2.testapp.BuildConfig.DEBUG -import org.readium.r2.testapp.bookshelf.BookRepository -import org.readium.r2.testapp.db.BookDatabase +import org.readium.r2.testapp.data.BookRepository +import org.readium.r2.testapp.data.DownloadRepository +import org.readium.r2.testapp.data.db.AppDatabase +import org.readium.r2.testapp.data.model.Download +import org.readium.r2.testapp.domain.Bookshelf +import org.readium.r2.testapp.domain.CoverStorage +import org.readium.r2.testapp.domain.LcpPublicationRetriever +import org.readium.r2.testapp.domain.LocalPublicationRetriever +import org.readium.r2.testapp.domain.OpdsPublicationRetriever +import org.readium.r2.testapp.domain.PublicationRetriever import org.readium.r2.testapp.reader.ReaderRepository import timber.log.Timber @@ -30,6 +42,9 @@ class Application : android.app.Application() { lateinit var bookRepository: BookRepository private set + lateinit var bookshelf: Bookshelf + private set + lateinit var readerRepository: ReaderRepository private set @@ -40,28 +55,66 @@ class Application : android.app.Application() { by preferencesDataStore(name = "navigator-preferences") override fun onCreate() { + if (DEBUG) { +// enableStrictMode() + Timber.plant(Timber.DebugTree()) + } + super.onCreate() + DynamicColors.applyToActivitiesIfAvailable(this) - if (DEBUG) Timber.plant(Timber.DebugTree()) readium = Readium(this) storageDir = computeStorageDir() - /* - * Initializing repositories - */ - bookRepository = - BookDatabase.getDatabase(this).booksDao() - .let { dao -> - BookRepository( - applicationContext, - dao, - storageDir, - readium.lcpService, - readium.streamer + val database = AppDatabase.getDatabase(this) + + bookRepository = BookRepository(database.booksDao()) + + bookshelf = + Bookshelf( + bookRepository, + CoverStorage(storageDir, httpClient = readium.httpClient), + readium.publicationOpener, + readium.assetRetriever, + createPublicationRetriever = { listener -> + PublicationRetriever( + listener = listener, + createLocalPublicationRetriever = { localListener -> + LocalPublicationRetriever( + listener = localListener, + context = applicationContext, + storageDir = storageDir, + assetRetriever = readium.assetRetriever, + createLcpPublicationRetriever = { lcpListener -> + readium.lcpService.getOrNull()?.publicationRetriever() + ?.let { retriever -> + LcpPublicationRetriever( + listener = lcpListener, + downloadRepository = DownloadRepository( + Download.Type.LCP, + database.downloadsDao() + ), + lcpPublicationRetriever = retriever + ) + } + } + ) + }, + createOpdsPublicationRetriever = { opdsListener -> + OpdsPublicationRetriever( + listener = opdsListener, + downloadManager = readium.downloadManager, + downloadRepository = DownloadRepository( + Download.Type.OPDS, + database.downloadsDao() + ) + ) + } ) } + ) readerRepository = ReaderRepository( this@Application, @@ -79,8 +132,41 @@ class Application : android.app.Application() { properties.getProperty("useExternalFileDir", "false")!!.toBoolean() return File( - if (useExternalFileDir) getExternalFilesDir(null)?.path + "/" - else filesDir?.path + "/" + if (useExternalFileDir) { + getExternalFilesDir(null)?.path + "/" + } else { + filesDir?.path + "/" + } + ) + } + + /** + * Strict mode will log violation of VM and threading policy. + * Use it to make sure the app doesn't do too much work on the main thread. + */ + private fun enableStrictMode() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + return + } + + val executor = Executors.newSingleThreadExecutor() + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyListener(executor) { violation -> + Timber.e(violation, "Thread policy violation") + } +// .penaltyDeath() + .build() + ) + StrictMode.setVmPolicy( + StrictMode.VmPolicy.Builder() + .detectAll() + .penaltyListener(executor) { violation -> + Timber.e(violation, "VM policy violation") + } +// .penaltyDeath() + .build() ) } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt b/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt index 4f01d3c307..1127436189 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt @@ -15,21 +15,17 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.google.android.material.bottomnavigation.BottomNavigationView -import org.readium.r2.testapp.bookshelf.BookshelfViewModel +import com.google.android.material.snackbar.Snackbar class MainActivity : AppCompatActivity() { private lateinit var navController: NavController - private val viewModel: BookshelfViewModel by viewModels() + private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - intent.data?.let { - viewModel.addPublicationFromUri(it) - } - val navView: BottomNavigationView = findViewById(R.id.nav_view) val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment @@ -37,14 +33,33 @@ class MainActivity : AppCompatActivity() { val appBarConfiguration = AppBarConfiguration( setOf( - R.id.navigation_bookshelf, R.id.navigation_catalog_list, R.id.navigation_about + R.id.navigation_bookshelf, + R.id.navigation_catalog_list, + R.id.navigation_about ) ) setupActionBarWithNavController(navController, appBarConfiguration) navView.setupWithNavController(navController) + + viewModel.channel.receive(this) { handleEvent(it) } } override fun onSupportNavigateUp(): Boolean { return navController.navigateUp() || super.onSupportNavigateUp() } + + private fun handleEvent(event: MainViewModel.Event) { + when (event) { + is MainViewModel.Event.ImportPublicationSuccess -> + Snackbar.make( + findViewById(android.R.id.content), + getString(R.string.import_publication_success), + Snackbar.LENGTH_LONG + ).show() + + is MainViewModel.Event.ImportPublicationError -> { + event.error.toUserError().show(this) + } + } + } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/MainViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/MainViewModel.kt new file mode 100644 index 0000000000..199879229c --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/MainViewModel.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import org.readium.r2.testapp.domain.Bookshelf +import org.readium.r2.testapp.domain.ImportError +import org.readium.r2.testapp.utils.EventChannel + +class MainViewModel( + application: Application +) : AndroidViewModel(application) { + + private val app = + getApplication<org.readium.r2.testapp.Application>() + + val channel: EventChannel<Event> = + EventChannel(Channel(Channel.UNLIMITED), viewModelScope) + + init { + app.bookshelf.channel.receiveAsFlow() + .onEach { sendImportFeedback(it) } + .launchIn(viewModelScope) + } + + private fun sendImportFeedback(event: Bookshelf.Event) { + when (event) { + is Bookshelf.Event.ImportPublicationError -> { + channel.send(Event.ImportPublicationError(event.error)) + } + Bookshelf.Event.ImportPublicationSuccess -> { + channel.send(Event.ImportPublicationSuccess) + } + } + } + + sealed class Event { + + object ImportPublicationSuccess : + Event() + + class ImportPublicationError( + val error: ImportError + ) : Event() + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 0abdd0406b..5e1d029a4b 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -7,37 +7,76 @@ package org.readium.r2.testapp import android.content.Context -import org.readium.adapters.pdfium.document.PdfiumDocumentFactory +import android.view.View +import org.readium.adapter.pdfium.document.PdfiumDocumentFactory +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpService +import org.readium.r2.lcp.auth.LcpDialogAuthentication import org.readium.r2.navigator.preferences.FontFamily import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try -import org.readium.r2.streamer.Streamer +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager +import org.readium.r2.shared.util.http.DefaultHttpClient +import org.readium.r2.streamer.PublicationOpener +import org.readium.r2.streamer.parser.DefaultPublicationParser /** * Holds the shared Readium objects and services used by the app. */ class Readium(context: Context) { + val httpClient = + DefaultHttpClient() + + val assetRetriever = + AssetRetriever(context.contentResolver, httpClient) + + val downloadManager = + AndroidDownloadManager( + context = context, + destStorage = AndroidDownloadManager.Storage.App + ) + /** * The LCP service decrypts LCP-protected publication and acquire publications from a * license file. */ - val lcpService = LcpService(context) - ?.let { Try.success(it) } - ?: Try.failure(Exception("liblcp is missing on the classpath")) + val lcpService = LcpService( + context, + assetRetriever, + downloadManager + )?.let { Try.success(it) } + ?: Try.failure(LcpError.Unknown(DebugError("liblcp is missing on the classpath"))) + + private val lcpDialogAuthentication = LcpDialogAuthentication() + + private val contentProtections = listOfNotNull( + lcpService.getOrNull()?.contentProtection(lcpDialogAuthentication) + ) /** - * The Streamer is used to open and parse publications. + * The PublicationFactory is used to open publications. */ - val streamer = Streamer( - context, - contentProtections = listOfNotNull( - lcpService.getOrNull()?.contentProtection() + val publicationOpener = PublicationOpener( + publicationParser = DefaultPublicationParser( + context, + assetRetriever = assetRetriever, + httpClient = httpClient, + // Only required if you want to support PDF files using the PDFium adapter. + pdfFactory = PdfiumDocumentFactory(context) ), - // Only required if you want to support PDF files using the PDFium adapter. - pdfFactory = PdfiumDocumentFactory(context) + contentProtections = contentProtections ) + + fun onLcpDialogAuthenticationParentAttached(view: View) { + lcpDialogAuthentication.onParentViewAttachedToWindow(view) + } + + fun onLcpDialogAuthenticationParentDetached() { + lcpDialogAuthentication.onParentViewDetachedFromWindow() + } } @OptIn(ExperimentalReadiumApi::class) diff --git a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookRepository.kt deleted file mode 100644 index b9c64e07f6..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookRepository.kt +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2021 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.testapp.bookshelf - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.net.Uri -import androidx.annotation.ColorInt -import androidx.lifecycle.LiveData -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.net.HttpURLConnection -import java.net.URL -import java.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.withContext -import org.joda.time.DateTime -import org.readium.r2.lcp.LcpService -import org.readium.r2.shared.extensions.mediaType -import org.readium.r2.shared.extensions.tryOrNull -import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.asset.FileAsset -import org.readium.r2.shared.publication.indexOfFirstWithHref -import org.readium.r2.shared.publication.services.cover -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.flatMap -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.streamer.Streamer -import org.readium.r2.testapp.db.BooksDao -import org.readium.r2.testapp.domain.model.Book -import org.readium.r2.testapp.domain.model.Bookmark -import org.readium.r2.testapp.domain.model.Highlight -import org.readium.r2.testapp.utils.extensions.authorName -import org.readium.r2.testapp.utils.extensions.copyToTempFile -import org.readium.r2.testapp.utils.extensions.moveTo -import timber.log.Timber - -class BookRepository( - private val context: Context, - private val booksDao: BooksDao, - private val storageDir: File, - private val lcpService: Try<LcpService, Exception>, - private val streamer: Streamer -) { - - fun books(): LiveData<List<Book>> = booksDao.getAllBooks() - - suspend fun get(id: Long) = booksDao.get(id) - - suspend fun saveProgression(locator: Locator, bookId: Long) = - booksDao.saveProgression(locator.toJSON().toString(), bookId) - - suspend fun insertBookmark(bookId: Long, publication: Publication, locator: Locator): Long { - val resource = publication.readingOrder.indexOfFirstWithHref(locator.href)!! - val bookmark = Bookmark( - creation = DateTime().toDate().time, - bookId = bookId, - publicationId = publication.metadata.identifier ?: publication.metadata.title, - resourceIndex = resource.toLong(), - resourceHref = locator.href, - resourceType = locator.type, - resourceTitle = locator.title.orEmpty(), - location = locator.locations.toJSON().toString(), - locatorText = Locator.Text().toJSON().toString() - ) - - return booksDao.insertBookmark(bookmark) - } - - fun bookmarksForBook(bookId: Long): LiveData<List<Bookmark>> = - booksDao.getBookmarksForBook(bookId) - - suspend fun deleteBookmark(bookmarkId: Long) = booksDao.deleteBookmark(bookmarkId) - - suspend fun highlightById(id: Long): Highlight? = - booksDao.getHighlightById(id) - - fun highlightsForBook(bookId: Long): Flow<List<Highlight>> = - booksDao.getHighlightsForBook(bookId) - - suspend fun addHighlight( - bookId: Long, - style: Highlight.Style, - @ColorInt tint: Int, - locator: Locator, - annotation: String - ): Long = - booksDao.insertHighlight(Highlight(bookId, style, tint, locator, annotation)) - - suspend fun deleteHighlight(id: Long) = booksDao.deleteHighlight(id) - - suspend fun updateHighlightAnnotation(id: Long, annotation: String) { - booksDao.updateHighlightAnnotation(id, annotation) - } - - suspend fun updateHighlightStyle(id: Long, style: Highlight.Style, @ColorInt tint: Int) { - booksDao.updateHighlightStyle(id, style, tint) - } - - private suspend fun insertBookIntoDatabase( - href: String, - mediaType: MediaType, - publication: Publication - ): Long { - val book = Book( - creation = DateTime().toDate().time, - title = publication.metadata.title, - author = publication.metadata.authorName, - href = href, - identifier = publication.metadata.identifier ?: "", - type = mediaType.toString(), - progression = "{}" - ) - return booksDao.insertBook(book) - } - - private suspend fun deleteBookFromDatabase(id: Long) = - booksDao.deleteBook(id) - - sealed class ImportException( - message: String? = null, - cause: Throwable? = null - ) : Exception(message, cause) { - - class LcpAcquisitionFailed( - cause: Throwable - ) : ImportException(cause = cause) - - object IOException : ImportException() - - object ImportDatabaseFailed : ImportException() - - class UnableToOpenPublication( - val exception: Publication.OpeningException - ) : ImportException(cause = exception) - } - - suspend fun addBook( - contentUri: Uri - ): Try<Unit, ImportException> = - contentUri.copyToTempFile(context, storageDir) - .mapFailure { ImportException.IOException } - .map { addBook(it) } - - suspend fun addBook( - tempFile: File, - coverUrl: String? = null - ): Try<Unit, ImportException> { - val sourceMediaType = tempFile.mediaType() - val publicationAsset: FileAsset = - if (sourceMediaType != MediaType.LCP_LICENSE_DOCUMENT) - FileAsset(tempFile, sourceMediaType) - else { - lcpService - .flatMap { it.acquirePublication(tempFile) } - .fold( - { - val mediaType = - MediaType.of(fileExtension = File(it.suggestedFilename).extension) - FileAsset(it.localFile, mediaType) - }, - { - tryOrNull { tempFile.delete() } - return Try.failure(ImportException.LcpAcquisitionFailed(it)) - } - ) - } - - val mediaType = publicationAsset.mediaType() - val fileName = "${UUID.randomUUID()}.${mediaType.fileExtension}" - val libraryAsset = FileAsset(File(storageDir, fileName), mediaType) - - try { - publicationAsset.file.moveTo(libraryAsset.file) - } catch (e: Exception) { - Timber.d(e) - tryOrNull { publicationAsset.file.delete() } - return Try.failure(ImportException.IOException) - } - - streamer.open(libraryAsset, allowUserInteraction = false) - .onSuccess { publication -> - val id = insertBookIntoDatabase( - libraryAsset.file.path, - libraryAsset.mediaType(), - publication - ) - if (id == -1L) - return Try.failure(ImportException.ImportDatabaseFailed) - - val cover: Bitmap? = coverUrl - ?.let { getBitmapFromURL(it) } - ?: publication.cover() - storeCoverImage(cover, id.toString()) - Try.success(Unit) - } - .onFailure { - tryOrNull { libraryAsset.file.delete() } - Timber.d(it) - return Try.failure(ImportException.UnableToOpenPublication(it)) - } - - return Try.success(Unit) - } - - private suspend fun storeCoverImage(cover: Bitmap?, imageName: String) = - withContext(Dispatchers.IO) { - // TODO Figure out where to store these cover images - val coverImageDir = File(storageDir, "covers/") - if (!coverImageDir.exists()) { - coverImageDir.mkdirs() - } - val coverImageFile = File(storageDir, "covers/$imageName.png") - - val resized = cover?.let { Bitmap.createScaledBitmap(it, 120, 200, true) } - val fos = FileOutputStream(coverImageFile) - resized?.compress(Bitmap.CompressFormat.PNG, 80, fos) - fos.flush() - fos.close() - } - - private suspend fun getBitmapFromURL(src: String): Bitmap? = - withContext(Dispatchers.IO) { - try { - val url = URL(src) - val connection = url.openConnection() as HttpURLConnection - connection.doInput = true - connection.connect() - val input = connection.inputStream - BitmapFactory.decodeStream(input) - } catch (e: IOException) { - e.printStackTrace() - null - } - } - - suspend fun deleteBook(book: Book) { - book.id?.let { deleteBookFromDatabase(it) } - tryOrNull { File(book.href).delete() } - tryOrNull { File(storageDir, "covers/${book.id}.png").delete() } - } -} diff --git a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfAdapter.kt b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfAdapter.kt index 651e35405f..4d1016a3be 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfAdapter.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfAdapter.kt @@ -14,8 +14,8 @@ import androidx.recyclerview.widget.RecyclerView import com.squareup.picasso.Picasso import java.io.File import org.readium.r2.testapp.R +import org.readium.r2.testapp.data.model.Book import org.readium.r2.testapp.databinding.ItemRecycleBookBinding -import org.readium.r2.testapp.domain.model.Book import org.readium.r2.testapp.utils.singleClick class BookshelfAdapter( @@ -29,13 +29,14 @@ class BookshelfAdapter( ): ViewHolder { return ViewHolder( ItemRecycleBookBinding.inflate( - LayoutInflater.from(parent.context), parent, false + LayoutInflater.from(parent.context), + parent, + false ) ) } override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { - val book = getItem(position) viewHolder.bind(book) @@ -46,10 +47,8 @@ class BookshelfAdapter( fun bind(book: Book) { binding.bookshelfTitleText.text = book.title - val coverImageFile = - File("${binding.root.context?.filesDir?.path}/covers/${book.id}.png") Picasso.get() - .load(coverImageFile) + .load(File(book.cover)) .placeholder(R.drawable.cover) .into(binding.bookshelfCoverImage) binding.root.singleClick { diff --git a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt index d40da78c98..32a2c01b38 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt @@ -6,10 +6,10 @@ package org.readium.r2.testapp.bookshelf +import android.content.Intent import android.graphics.Rect import android.net.Uri import android.os.Bundle -import android.text.TextUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -17,25 +17,40 @@ import android.webkit.URLUtil import android.widget.EditText import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.testapp.Application import org.readium.r2.testapp.R +import org.readium.r2.testapp.data.model.Book import org.readium.r2.testapp.databinding.FragmentBookshelfBinding -import org.readium.r2.testapp.domain.model.Book import org.readium.r2.testapp.opds.GridAutoFitLayoutManager import org.readium.r2.testapp.reader.ReaderActivityContract import org.readium.r2.testapp.utils.viewLifecycle class BookshelfFragment : Fragment() { + private inner class OnViewAttachedListener : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(view: View) { + app.readium.onLcpDialogAuthenticationParentAttached(view) + } + + override fun onViewDetachedFromWindow(view: View) { + app.readium.onLcpDialogAuthenticationParentDetached() + } + } + private val bookshelfViewModel: BookshelfViewModel by activityViewModels() private lateinit var bookshelfAdapter: BookshelfAdapter - private lateinit var documentPickerLauncher: ActivityResultLauncher<String> + private lateinit var appStoragePickerLauncher: ActivityResultLauncher<String> + private lateinit var sharedStoragePickerLauncher: ActivityResultLauncher<Array<String>> private var binding: FragmentBookshelfBinding by viewLifecycle() + private var onViewAttachedListener: OnViewAttachedListener = OnViewAttachedListener() + + private val app: Application + get() = requireContext().applicationContext as Application override fun onCreateView( inflater: LayoutInflater, @@ -49,18 +64,32 @@ class BookshelfFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + view.addOnAttachStateChangeListener(onViewAttachedListener) + bookshelfViewModel.channel.receive(viewLifecycleOwner) { handleEvent(it) } bookshelfAdapter = BookshelfAdapter( - onBookClick = { book -> book.id?.let { bookshelfViewModel.openPublication(it, requireActivity()) } }, + onBookClick = { book -> + book.id?.let { + bookshelfViewModel.openPublication(it) + } + }, onBookLongClick = { book -> confirmDeleteBook(book) } ) - documentPickerLauncher = + appStoragePickerLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> uri?.let { - binding.bookshelfProgressBar.visibility = View.VISIBLE - bookshelfViewModel.addPublicationFromUri(it) + bookshelfViewModel.importPublicationFromStorage(it) + } + } + + sharedStoragePickerLauncher = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> + uri?.let { + val takeFlags: Int = Intent.FLAG_GRANT_WRITE_URI_PERMISSION + requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags) + bookshelfViewModel.addPublicationFromStorage(it) } } @@ -79,7 +108,6 @@ class BookshelfFragment : Fragment() { bookshelfAdapter.submitList(it) } - // FIXME embedded dialogs like this are ugly binding.bookshelfAddBookFab.setOnClickListener { var selected = 0 MaterialAlertDialogBuilder(requireContext()) @@ -88,32 +116,10 @@ class BookshelfFragment : Fragment() { dialog.cancel() } .setPositiveButton(getString(R.string.ok)) { _, _ -> - if (selected == 0) { - documentPickerLauncher.launch("*/*") - } else { - val urlEditText = EditText(requireContext()) - val urlDialog = MaterialAlertDialogBuilder(requireContext()) - .setTitle(getString(R.string.add_book)) - .setMessage(R.string.enter_url) - .setView(urlEditText) - .setNegativeButton(R.string.cancel) { dialog, _ -> - dialog.cancel() - } - .setPositiveButton(getString(R.string.ok), null) - .show() - urlDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { - if (TextUtils.isEmpty(urlEditText.text)) { - urlEditText.error = getString(R.string.invalid_url) - } else if (!URLUtil.isValidUrl(urlEditText.text.toString())) { - urlEditText.error = getString(R.string.invalid_url) - } else { - val url = urlEditText.text.toString() - val uri = Uri.parse(url) - binding.bookshelfProgressBar.visibility = View.VISIBLE - bookshelfViewModel.addPublicationFromUri(uri) - urlDialog.dismiss() - } - } + when (selected) { + 0 -> appStoragePickerLauncher.launch("*/*") + 1 -> sharedStoragePickerLauncher.launch(arrayOf("*/*")) + else -> askForRemoteUrl() } } .setSingleChoiceItems(R.array.documentSelectorArray, 0) { _, which -> @@ -123,35 +129,40 @@ class BookshelfFragment : Fragment() { } } - private fun handleEvent(event: BookshelfViewModel.Event) { - val message = - when (event) { - is BookshelfViewModel.Event.ImportPublicationSuccess -> - getString(R.string.import_publication_success) - - is BookshelfViewModel.Event.ImportPublicationError -> { - event.errorMessage + private fun askForRemoteUrl() { + val urlEditText = EditText(requireContext()) + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.add_book)) + .setMessage(R.string.enter_url) + .setView(urlEditText) + .setNegativeButton(R.string.cancel) { dialog, _ -> + dialog.cancel() + } + .setPositiveButton(getString(R.string.ok)) { _, _ -> + val url = AbsoluteUrl(urlEditText.text.toString()) + if (url == null || !URLUtil.isValidUrl(urlEditText.text.toString())) { + urlEditText.error = getString(R.string.invalid_url) + return@setPositiveButton } - is BookshelfViewModel.Event.OpenPublicationError -> { - val detail = event.errorMessage - ?: "Unable to open publication. An unexpected error occurred." - "Error: $detail" - } + bookshelfViewModel.addPublicationFromWeb(url) + } + .show() + } - is BookshelfViewModel.Event.LaunchReader -> { - val intent = ReaderActivityContract().createIntent(requireContext(), event.arguments) - startActivity(intent) - null - } + private fun handleEvent(event: BookshelfViewModel.Event) { + when (event) { + is BookshelfViewModel.Event.OpenPublicationError -> { + event.error.toUserError().show(requireActivity()) + } + + is BookshelfViewModel.Event.LaunchReader -> { + val intent = ReaderActivityContract().createIntent( + requireContext(), + event.arguments + ) + startActivity(intent) } - binding.bookshelfProgressBar.visibility = View.GONE - message?.let { - Snackbar.make( - requireView(), - it, - Snackbar.LENGTH_LONG - ).show() } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfViewModel.kt index a793a6bc89..875e58c3d9 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfViewModel.kt @@ -6,120 +6,64 @@ package org.readium.r2.testapp.bookshelf -import android.app.Activity import android.app.Application -import android.content.Context import android.net.Uri import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.readium.r2.shared.UserException -import org.readium.r2.testapp.BuildConfig -import org.readium.r2.testapp.R -import org.readium.r2.testapp.domain.model.Book +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.toUrl +import org.readium.r2.testapp.data.model.Book +import org.readium.r2.testapp.reader.OpeningError import org.readium.r2.testapp.reader.ReaderActivityContract -import org.readium.r2.testapp.reader.ReaderRepository import org.readium.r2.testapp.utils.EventChannel -import org.readium.r2.testapp.utils.extensions.copyToTempFile class BookshelfViewModel(application: Application) : AndroidViewModel(application) { private val app get() = getApplication<org.readium.r2.testapp.Application>() - private val preferences = - application.getSharedPreferences("org.readium.r2.settings", Context.MODE_PRIVATE) - val channel = EventChannel(Channel<Event>(Channel.BUFFERED), viewModelScope) val books = app.bookRepository.books() - init { - copySamplesFromAssetsToStorage() + fun deletePublication(book: Book) = + viewModelScope.launch { + app.bookshelf.deleteBook(book) + } + + fun importPublicationFromStorage(uri: Uri) { + app.bookshelf.importPublicationFromStorage(uri) } - private fun copySamplesFromAssetsToStorage() = viewModelScope.launch(Dispatchers.IO) { - withContext(Dispatchers.IO) { - if (!preferences.contains("samples")) { - val dir = app.storageDir - if (!dir.exists()) { - dir.mkdirs() - } - val samples = app.assets.list("Samples")?.filterNotNull().orEmpty() - for (element in samples) { - val file = - app.assets.open("Samples/$element").copyToTempFile(app.storageDir) - if (file != null) - app.bookRepository.addBook(file) - else if (BuildConfig.DEBUG) - error("Unable to load sample into the library") - } - preferences.edit().putBoolean("samples", true).apply() - } - } + fun addPublicationFromStorage(uri: Uri) { + app.bookshelf.addPublicationFromStorage(uri.toUrl()!! as AbsoluteUrl) } - fun deletePublication(book: Book) = - viewModelScope.launch { - app.bookRepository.deleteBook(book) - } + fun addPublicationFromWeb(url: AbsoluteUrl) { + app.bookshelf.addPublicationFromWeb(url) + } - fun addPublicationFromUri(uri: Uri) = + fun openPublication( + bookId: Long + ) { viewModelScope.launch { - app.bookRepository - .addBook(uri) - .onFailure { exception -> - val errorMessage = when (exception) { - is BookRepository.ImportException.UnableToOpenPublication -> - exception.exception.getUserMessage(app) - BookRepository.ImportException.ImportDatabaseFailed -> - app.getString(R.string.unable_add_pub_database) - is BookRepository.ImportException.LcpAcquisitionFailed -> - "Error: " + exception.message - BookRepository.ImportException.IOException -> - app.getString(R.string.unexpected_io_exception) - } - channel.send(Event.ImportPublicationError(errorMessage)) + app.readerRepository + .open(bookId) + .onFailure { + channel.send(Event.OpenPublicationError(it)) } .onSuccess { - channel.send(Event.ImportPublicationSuccess) + val arguments = ReaderActivityContract.Arguments(bookId) + channel.send(Event.LaunchReader(arguments)) } } - - fun openPublication( - bookId: Long, - activity: Activity - ) = viewModelScope.launch { - app.readerRepository - .open(bookId, activity) - .onFailure { exception -> - if (exception is ReaderRepository.CancellationException) - return@launch - - val message = when (exception) { - is UserException -> exception.getUserMessage(app) - else -> exception.message - } - channel.send(Event.OpenPublicationError(message)) - } - .onSuccess { - val arguments = ReaderActivityContract.Arguments(bookId) - channel.send(Event.LaunchReader(arguments)) - } } sealed class Event { - object ImportPublicationSuccess : Event() - - class ImportPublicationError( - val errorMessage: String - ) : Event() - class OpenPublicationError( - val errorMessage: String? + val error: OpeningError ) : Event() class LaunchReader( diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListAdapter.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListAdapter.kt index f35ec8cc82..93251e2369 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListAdapter.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListAdapter.kt @@ -14,8 +14,8 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import org.readium.r2.testapp.R +import org.readium.r2.testapp.data.model.Catalog import org.readium.r2.testapp.databinding.ItemRecycleButtonBinding -import org.readium.r2.testapp.domain.model.Catalog class CatalogFeedListAdapter(private val onLongClick: (Catalog) -> Unit) : ListAdapter<Catalog, CatalogFeedListAdapter.ViewHolder>(CatalogListDiff()) { @@ -30,7 +30,6 @@ class CatalogFeedListAdapter(private val onLongClick: (Catalog) -> Unit) : } override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { - val catalog = getItem(position) viewHolder.bind(catalog) diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListFragment.kt index 9dbfeebffd..bc298b3e36 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListFragment.kt @@ -23,8 +23,8 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import org.readium.r2.testapp.R +import org.readium.r2.testapp.data.model.Catalog import org.readium.r2.testapp.databinding.FragmentCatalogFeedListBinding -import org.readium.r2.testapp.domain.model.Catalog import org.readium.r2.testapp.utils.viewLifecycle class CatalogFeedListFragment : Fragment() { @@ -70,7 +70,6 @@ class CatalogFeedListFragment : Fragment() { val VERSION_KEY = "OPDS_CATALOG_VERSION" if (preferences.getInt(VERSION_KEY, 0) < version) { - preferences.edit().putInt(VERSION_KEY, version).apply() val oPDS2Catalog = Catalog( @@ -120,7 +119,9 @@ class CatalogFeedListFragment : Fragment() { private fun handleEvent(event: CatalogFeedListViewModel.Event) { val message = when (event) { - is CatalogFeedListViewModel.Event.FeedListEvent.CatalogParseFailed -> getString(R.string.catalog_parse_error) + is CatalogFeedListViewModel.Event.FeedListEvent.CatalogParseFailed -> getString( + R.string.catalog_parse_error + ) } Snackbar.make( requireView(), diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt index 6e0ecf15a2..e503660a35 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt @@ -9,24 +9,27 @@ package org.readium.r2.testapp.catalogs import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope -import java.net.URL import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import org.json.JSONObject import org.readium.r2.opds.OPDS1Parser import org.readium.r2.opds.OPDS2Parser import org.readium.r2.shared.opds.ParseData +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.fetchWithDecoder -import org.readium.r2.testapp.db.BookDatabase -import org.readium.r2.testapp.domain.model.Catalog +import org.readium.r2.testapp.data.CatalogRepository +import org.readium.r2.testapp.data.db.AppDatabase +import org.readium.r2.testapp.data.model.Catalog import org.readium.r2.testapp.utils.EventChannel class CatalogFeedListViewModel(application: Application) : AndroidViewModel(application) { - private val catalogDao = BookDatabase.getDatabase(application).catalogDao() + private val httpClient = getApplication<org.readium.r2.testapp.Application>().readium.httpClient + private val catalogDao = AppDatabase.getDatabase(application).catalogDao() private val repository = CatalogRepository(catalogDao) val eventChannel = EventChannel(Channel<Event>(Channel.BUFFERED), viewModelScope) @@ -41,7 +44,7 @@ class CatalogFeedListViewModel(application: Application) : AndroidViewModel(appl } fun parseCatalog(url: String, title: String) = viewModelScope.launch { - val parseData = parseURL(URL(url)) + val parseData = parseURL(url) parseData.onSuccess { data -> val catalog = Catalog( title = title, @@ -55,8 +58,11 @@ class CatalogFeedListViewModel(application: Application) : AndroidViewModel(appl } } - private suspend fun parseURL(url: URL): Try<ParseData, Exception> { - return DefaultHttpClient().fetchWithDecoder(HttpRequest(url.toString())) { + private suspend fun parseURL(urlString: String): Try<ParseData, Error> { + val url = AbsoluteUrl(urlString) + ?: return Try.failure(DebugError("Invalid URL")) + + return httpClient.fetchWithDecoder(HttpRequest(url)) { val result = it.body if (isJson(result)) { OPDS2Parser.parse(result, url) diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFragment.kt index a4b88809ca..c0bd323727 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFragment.kt @@ -9,11 +9,17 @@ package org.readium.r2.testapp.catalogs import android.os.Bundle import android.view.LayoutInflater import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.core.os.BundleCompat import androidx.core.os.bundleOf +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle import androidx.navigation.Navigation import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar @@ -22,20 +28,20 @@ import org.readium.r2.testapp.MainActivity import org.readium.r2.testapp.R import org.readium.r2.testapp.bookshelf.BookshelfFragment import org.readium.r2.testapp.catalogs.CatalogFeedListAdapter.Companion.CATALOGFEED +import org.readium.r2.testapp.data.model.Catalog import org.readium.r2.testapp.databinding.FragmentCatalogBinding -import org.readium.r2.testapp.domain.model.Catalog import org.readium.r2.testapp.opds.GridAutoFitLayoutManager import org.readium.r2.testapp.utils.viewLifecycle class CatalogFragment : Fragment() { - private val catalogViewModel: CatalogViewModel by viewModels() + private val catalogViewModel: CatalogViewModel by activityViewModels() private lateinit var publicationAdapter: PublicationAdapter private lateinit var groupAdapter: GroupAdapter private lateinit var navigationAdapter: NavigationAdapter private lateinit var catalog: Catalog private var showFacetMenu = false - private lateinit var facets: MutableList<Facet> + private lateinit var facets: List<Facet> private var binding: FragmentCatalogBinding by viewLifecycle() override fun onCreateView( @@ -43,19 +49,18 @@ class CatalogFragment : Fragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { + catalogViewModel.channel.receive(this) { handleEvent(it) } - catalogViewModel.eventChannel.receive(this) { handleEvent(it) } - catalog = arguments?.get(CATALOGFEED) as Catalog + catalog = arguments?.let { BundleCompat.getParcelable(it, CATALOGFEED, Catalog::class.java) }!! binding = FragmentCatalogBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - publicationAdapter = PublicationAdapter() + publicationAdapter = PublicationAdapter(catalogViewModel::publication::set) navigationAdapter = NavigationAdapter(catalog.type) - groupAdapter = GroupAdapter(catalog.type) - setHasOptionsMenu(true) + groupAdapter = GroupAdapter(catalog.type, catalogViewModel::publication::set) binding.catalogNavigationList.apply { layoutManager = LinearLayoutManager(requireContext()) @@ -84,63 +89,70 @@ class CatalogFragment : Fragment() { (activity as MainActivity).supportActionBar?.title = catalog.title - // TODO this feels hacky, I don't want to parse the file if it has not changed - if (catalogViewModel.parseData.value == null) { - binding.catalogProgressBar.visibility = View.VISIBLE - catalogViewModel.parseCatalog(catalog) - } - catalogViewModel.parseData.observe(viewLifecycleOwner, { result -> - - facets = result.feed?.facets ?: mutableListOf() - - if (facets.size > 0) { - showFacetMenu = true - } - requireActivity().invalidateOptionsMenu() - - navigationAdapter.submitList(result.feed!!.navigation) - publicationAdapter.submitList(result.feed!!.publications) - groupAdapter.submitList(result.feed!!.groups) + catalogViewModel.parseCatalog(catalog) + binding.catalogProgressBar.visibility = View.VISIBLE + + val menuHost: MenuHost = requireActivity() + + menuHost.addMenuProvider( + object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menu.clear() + if (showFacetMenu) { + facets.let { + for (i in facets.indices) { + val submenu = menu.addSubMenu(facets[i].title) + for (link in facets[i].links) { + val item = submenu.add(link.title) + item.setOnMenuItemClickListener { + val catalog1 = Catalog( + title = link.title!!, + href = link.href.toString(), + type = catalog.type + ) + val bundle = bundleOf(CATALOGFEED to catalog1) + Navigation.findNavController(requireView()) + .navigate(R.id.action_navigation_catalog_self, bundle) + true + } + } + } + } + } + } - binding.catalogProgressBar.visibility = View.GONE - }) + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return false + } + }, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) } - private fun handleEvent(event: CatalogViewModel.Event.FeedEvent) { - val message = - when (event) { - is CatalogViewModel.Event.FeedEvent.CatalogParseFailed -> getString(R.string.failed_parsing_catalog) + private fun handleEvent(event: CatalogViewModel.Event) { + when (event) { + is CatalogViewModel.Event.CatalogParseFailed -> { + Snackbar.make( + requireView(), + getString(R.string.failed_parsing_catalog), + Snackbar.LENGTH_LONG + ).show() } - binding.catalogProgressBar.visibility = View.GONE - Snackbar.make( - requireView(), - message, - Snackbar.LENGTH_LONG - ).show() - } - override fun onPrepareOptionsMenu(menu: Menu) { - menu.clear() - if (showFacetMenu) { - facets.let { - for (i in facets.indices) { - val submenu = menu.addSubMenu(facets[i].title) - for (link in facets[i].links) { - val item = submenu.add(link.title) - item.setOnMenuItemClickListener { - val catalog1 = Catalog( - title = link.title!!, - href = link.href, - type = catalog.type - ) - val bundle = bundleOf(CATALOGFEED to catalog1) - Navigation.findNavController(requireView()) - .navigate(R.id.action_navigation_catalog_self, bundle) - true - } - } + is CatalogViewModel.Event.CatalogParseSuccess -> { + facets = event.result.feed?.facets ?: emptyList() + + if (facets.size > 0) { + showFacetMenu = true } + requireActivity().invalidateOptionsMenu() + + navigationAdapter.submitList(event.result.feed!!.navigation) + publicationAdapter.submitList(event.result.feed!!.publications) + groupAdapter.submitList(event.result.feed!!.groups) } } + binding.catalogProgressBar.visibility = View.GONE } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogViewModel.kt index d231a42f06..3128c1dd17 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogViewModel.kt @@ -8,100 +8,57 @@ package org.readium.r2.testapp.catalogs import android.app.Application import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import java.io.File -import java.net.MalformedURLException -import java.net.URL -import java.util.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import org.readium.r2.opds.OPDS1Parser import org.readium.r2.opds.OPDS2Parser import org.readium.r2.shared.opds.ParseData import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.opds.images +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.http.HttpRequest -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.testapp.domain.model.Catalog +import org.readium.r2.testapp.data.model.Catalog import org.readium.r2.testapp.utils.EventChannel -import org.readium.r2.testapp.utils.extensions.downloadTo import timber.log.Timber class CatalogViewModel(application: Application) : AndroidViewModel(application) { - private val app get() = - getApplication<org.readium.r2.testapp.Application>() + val channel = EventChannel(Channel<Event>(Channel.BUFFERED), viewModelScope) - val detailChannel = EventChannel(Channel<Event.DetailEvent>(Channel.BUFFERED), viewModelScope) - val eventChannel = EventChannel(Channel<Event.FeedEvent>(Channel.BUFFERED), viewModelScope) - val parseData = MutableLiveData<ParseData>() + lateinit var publication: Publication + private val app = getApplication<org.readium.r2.testapp.Application>() fun parseCatalog(catalog: Catalog) = viewModelScope.launch { var parseRequest: Try<ParseData, Exception>? = null - catalog.href.let { - val request = HttpRequest(it) - try { - parseRequest = if (catalog.type == 1) { - OPDS1Parser.parseRequest(request) - } else { - OPDS2Parser.parseRequest(request) + catalog.href.let { href -> + AbsoluteUrl(href) + ?.let { HttpRequest(it) } + ?.let { request -> + parseRequest = if (catalog.type == 1) { + OPDS1Parser.parseRequest(request, app.readium.httpClient) + } else { + OPDS2Parser.parseRequest(request, app.readium.httpClient) + } } - } catch (e: MalformedURLException) { - eventChannel.send(Event.FeedEvent.CatalogParseFailed) - } } parseRequest?.onSuccess { - parseData.postValue(it) + channel.send(Event.CatalogParseSuccess(it)) } parseRequest?.onFailure { Timber.e(it) - eventChannel.send(Event.FeedEvent.CatalogParseFailed) + channel.send(Event.CatalogParseFailed) } } fun downloadPublication(publication: Publication) = viewModelScope.launch { - val filename = UUID.randomUUID().toString() - val dest = File(app.storageDir, filename) - - getDownloadURL(publication) - .flatMap { url -> - url.downloadTo(dest) - }.flatMap { - val opdsCover = publication.images.firstOrNull()?.href - app.bookRepository.addBook(dest, opdsCover) - }.onSuccess { - detailChannel.send(Event.DetailEvent.ImportPublicationSuccess) - }.onFailure { - detailChannel.send(Event.DetailEvent.ImportPublicationFailed) - } + app.bookshelf.importPublicationFromOpds(publication) } - private fun getDownloadURL(publication: Publication): Try<URL, Exception> = - publication.links - .firstOrNull { it.mediaType.isPublication || it.mediaType == MediaType.LCP_LICENSE_DOCUMENT } - ?.let { - try { - Try.success(URL(it.href)) - } catch (e: Exception) { - Try.failure(e) - } - } ?: Try.failure(Exception("No supported link to acquire publication.")) - sealed class Event { - sealed class FeedEvent : Event() { - - object CatalogParseFailed : FeedEvent() - } - - sealed class DetailEvent : Event() { + object CatalogParseFailed : Event() - object ImportPublicationSuccess : DetailEvent() - - object ImportPublicationFailed : DetailEvent() - } + class CatalogParseSuccess(val result: ParseData) : Event() } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/GroupAdapter.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/GroupAdapter.kt index 4e0f3fc378..ad57d97f05 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/GroupAdapter.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/GroupAdapter.kt @@ -16,11 +16,15 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import org.readium.r2.shared.opds.Group +import org.readium.r2.shared.publication.Publication import org.readium.r2.testapp.R +import org.readium.r2.testapp.data.model.Catalog import org.readium.r2.testapp.databinding.ItemGroupViewBinding -import org.readium.r2.testapp.domain.model.Catalog -class GroupAdapter(val type: Int) : +class GroupAdapter( + val type: Int, + private val setModelPublication: (Publication) -> Unit +) : ListAdapter<Group, GroupAdapter.ViewHolder>(GroupDiff()) { override fun onCreateViewHolder( @@ -29,7 +33,9 @@ class GroupAdapter(val type: Int) : ): ViewHolder { return ViewHolder( ItemGroupViewBinding.inflate( - LayoutInflater.from(parent.context), parent, false + LayoutInflater.from(parent.context), + parent, + false ) ) } @@ -49,7 +55,7 @@ class GroupAdapter(val type: Int) : binding.groupViewGroupPublications.itemRecycleMoreButton.visibility = View.VISIBLE binding.groupViewGroupPublications.itemRecycleMoreButton.setOnClickListener { val catalog1 = Catalog( - href = group.links.first().href, + href = group.links.first().href.toString(), title = group.title, type = type ) @@ -62,7 +68,7 @@ class GroupAdapter(val type: Int) : layoutManager = LinearLayoutManager(binding.root.context) (layoutManager as LinearLayoutManager).orientation = LinearLayoutManager.HORIZONTAL - adapter = PublicationAdapter().apply { + adapter = PublicationAdapter(setModelPublication).apply { submitList(group.publications) } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/NavigationAdapter.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/NavigationAdapter.kt index 1b84181d6d..1b0ef32280 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/NavigationAdapter.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/NavigationAdapter.kt @@ -15,8 +15,8 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import org.readium.r2.shared.publication.Link import org.readium.r2.testapp.R +import org.readium.r2.testapp.data.model.Catalog import org.readium.r2.testapp.databinding.ItemRecycleButtonBinding -import org.readium.r2.testapp.domain.model.Catalog class NavigationAdapter(val type: Int) : ListAdapter<Link, NavigationAdapter.ViewHolder>(LinkDiff()) { @@ -27,7 +27,9 @@ class NavigationAdapter(val type: Int) : ): ViewHolder { return ViewHolder( ItemRecycleButtonBinding.inflate( - LayoutInflater.from(parent.context), parent, false + LayoutInflater.from(parent.context), + parent, + false ) ) } @@ -45,7 +47,7 @@ class NavigationAdapter(val type: Int) : binding.catalogListButton.text = link.title binding.catalogListButton.setOnClickListener { val catalog1 = Catalog( - href = link.href, + href = link.href.toString(), title = link.title!!, type = type ) diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/PublicationAdapter.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/PublicationAdapter.kt index 929b32f097..27e58b224b 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/PublicationAdapter.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/PublicationAdapter.kt @@ -6,7 +6,6 @@ package org.readium.r2.testapp.catalogs -import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.navigation.Navigation @@ -14,13 +13,14 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.squareup.picasso.Picasso -import org.readium.r2.shared.extensions.putPublication import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.opds.images import org.readium.r2.testapp.R import org.readium.r2.testapp.databinding.ItemRecycleCatalogBinding -class PublicationAdapter : +class PublicationAdapter( + private val setModelPublication: (Publication) -> Unit +) : ListAdapter<Publication, PublicationAdapter.ViewHolder>(PublicationListDiff()) { override fun onCreateViewHolder( @@ -33,7 +33,6 @@ class PublicationAdapter : } override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { - val publication = getItem(position) viewHolder.bind(publication) @@ -46,21 +45,21 @@ class PublicationAdapter : binding.catalogListTitleText.text = publication.metadata.title publication.linkWithRel("http://opds-spec.org/image/thumbnail")?.let { link -> - Picasso.get().load(link.href) + Picasso.get().load(link.href.toString()) .into(binding.catalogListCoverImage) } ?: run { if (publication.images.isNotEmpty()) { Picasso.get() - .load(publication.images.first().href).into(binding.catalogListCoverImage) + .load(publication.images.first().href.toString()).into( + binding.catalogListCoverImage + ) } } binding.root.setOnClickListener { - val bundle = Bundle().apply { - putPublication(publication) - } + setModelPublication(publication) Navigation.findNavController(it) - .navigate(R.id.action_navigation_catalog_to_navigation_catalog_detail, bundle) + .navigate(R.id.action_navigation_catalog_to_navigation_catalog_detail) } } } @@ -78,7 +77,7 @@ class PublicationAdapter : oldItem: Publication, newItem: Publication ): Boolean { - return oldItem.jsonManifest == newItem.jsonManifest + return oldItem.manifest == newItem.manifest } } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/PublicationDetailFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/PublicationDetailFragment.kt index b27c0250ce..f3c9ec9194 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/PublicationDetailFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/PublicationDetailFragment.kt @@ -11,20 +11,17 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import com.google.android.material.snackbar.Snackbar +import androidx.fragment.app.activityViewModels import com.squareup.picasso.Picasso -import org.readium.r2.shared.extensions.getPublicationOrNull import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.opds.images import org.readium.r2.testapp.MainActivity -import org.readium.r2.testapp.R import org.readium.r2.testapp.databinding.FragmentPublicationDetailBinding class PublicationDetailFragment : Fragment() { private var publication: Publication? = null - private val catalogViewModel: CatalogViewModel by viewModels() + private val catalogViewModel: CatalogViewModel by activityViewModels() private var _binding: FragmentPublicationDetailBinding? = null private val binding get() = _binding!! @@ -35,10 +32,11 @@ class PublicationDetailFragment : Fragment() { savedInstanceState: Bundle? ): View { _binding = FragmentPublicationDetailBinding.inflate( - inflater, container, false + inflater, + container, + false ) - catalogViewModel.detailChannel.receive(this) { handleEvent(it) } - publication = arguments?.getPublicationOrNull() + publication = catalogViewModel.publication return binding.root } @@ -47,7 +45,7 @@ class PublicationDetailFragment : Fragment() { (activity as MainActivity).supportActionBar?.title = publication?.metadata?.title publication?.images?.firstOrNull() - ?.let { Picasso.get().load(it.href) } + ?.let { Picasso.get().load(it.href.toString()) } ?.into(binding.catalogDetailCoverImage) binding.catalogDetailDescriptionText.text = publication?.metadata?.description @@ -55,25 +53,10 @@ class PublicationDetailFragment : Fragment() { binding.catalogDetailDownloadButton.setOnClickListener { publication?.let { it1 -> - binding.catalogDetailProgressBar.visibility = View.VISIBLE catalogViewModel.downloadPublication( it1 ) } } } - - private fun handleEvent(event: CatalogViewModel.Event.DetailEvent) { - val message = - when (event) { - is CatalogViewModel.Event.DetailEvent.ImportPublicationSuccess -> getString(R.string.import_publication_success) - is CatalogViewModel.Event.DetailEvent.ImportPublicationFailed -> getString(R.string.unable_add_pub_database) - } - binding.catalogDetailProgressBar.visibility = View.GONE - Snackbar.make( - requireView(), - message, - Snackbar.LENGTH_LONG - ).show() - } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt new file mode 100644 index 0000000000..14d0bed426 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2021 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.data + +import androidx.annotation.ColorInt +import androidx.lifecycle.LiveData +import java.io.File +import kotlinx.coroutines.flow.Flow +import org.joda.time.DateTime +import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.indexOfFirstWithHref +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.testapp.data.db.BooksDao +import org.readium.r2.testapp.data.model.Book +import org.readium.r2.testapp.data.model.Bookmark +import org.readium.r2.testapp.data.model.Highlight +import org.readium.r2.testapp.utils.extensions.readium.authorName + +class BookRepository( + private val booksDao: BooksDao +) { + fun books(): LiveData<List<Book>> = booksDao.getAllBooks() + + suspend fun get(id: Long) = booksDao.get(id) + + suspend fun saveProgression(locator: Locator, bookId: Long) = + booksDao.saveProgression(locator.toJSON().toString(), bookId) + + suspend fun insertBookmark(bookId: Long, publication: Publication, locator: Locator): Long { + val resource = publication.readingOrder.indexOfFirstWithHref(locator.href)!! + val bookmark = Bookmark( + creation = DateTime().toDate().time, + bookId = bookId, + resourceIndex = resource.toLong(), + resourceHref = locator.href.toString(), + resourceType = locator.mediaType.toString(), + resourceTitle = locator.title.orEmpty(), + location = locator.locations.toJSON().toString(), + locatorText = Locator.Text().toJSON().toString() + ) + + return booksDao.insertBookmark(bookmark) + } + + fun bookmarksForBook(bookId: Long): LiveData<List<Bookmark>> = + booksDao.getBookmarksForBook(bookId) + + suspend fun deleteBookmark(bookmarkId: Long) = booksDao.deleteBookmark(bookmarkId) + + suspend fun highlightById(id: Long): Highlight? = + booksDao.getHighlightById(id) + + fun highlightsForBook(bookId: Long): Flow<List<Highlight>> = + booksDao.getHighlightsForBook(bookId) + + suspend fun addHighlight( + bookId: Long, + style: Highlight.Style, + @ColorInt tint: Int, + locator: Locator, + annotation: String + ): Long = + booksDao.insertHighlight(Highlight(bookId, style, tint, locator, annotation)) + + suspend fun deleteHighlight(id: Long) = booksDao.deleteHighlight(id) + + suspend fun updateHighlightAnnotation(id: Long, annotation: String) { + booksDao.updateHighlightAnnotation(id, annotation) + } + + suspend fun updateHighlightStyle(id: Long, style: Highlight.Style, @ColorInt tint: Int) { + booksDao.updateHighlightStyle(id, style, tint) + } + + suspend fun insertBook( + url: Url, + mediaType: MediaType, + publication: Publication, + cover: File + ): Long { + val book = Book( + creation = DateTime().toDate().time, + title = publication.metadata.title ?: url.filename, + author = publication.metadata.authorName, + href = url.toString(), + identifier = publication.metadata.identifier ?: "", + mediaType = mediaType, + progression = "{}", + cover = cover.path + ) + return booksDao.insertBook(book) + } + + suspend fun deleteBook(id: Long) = + booksDao.deleteBook(id) +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/data/CatalogRepository.kt similarity index 80% rename from test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogRepository.kt rename to test-app/src/main/java/org/readium/r2/testapp/data/CatalogRepository.kt index c029699336..4f44f4a71c 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/CatalogRepository.kt @@ -4,11 +4,11 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.testapp.catalogs +package org.readium.r2.testapp.data import androidx.lifecycle.LiveData -import org.readium.r2.testapp.db.CatalogDao -import org.readium.r2.testapp.domain.model.Catalog +import org.readium.r2.testapp.data.db.CatalogDao +import org.readium.r2.testapp.data.model.Catalog class CatalogRepository(private val catalogDao: CatalogDao) { diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/DownloadRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/data/DownloadRepository.kt new file mode 100644 index 0000000000..a65c774d00 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/data/DownloadRepository.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.data + +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.testapp.data.db.DownloadsDao +import org.readium.r2.testapp.data.model.Download + +class DownloadRepository( + private val type: Download.Type, + private val downloadsDao: DownloadsDao +) { + + suspend fun all(): List<Download> = + downloadsDao.getDownloads(type) + + suspend fun insert( + id: String, + cover: AbsoluteUrl? + ) { + downloadsDao.insert( + Download(id = id, type = type, cover = cover?.toString()) + ) + } + + suspend fun remove( + id: String + ) { + downloadsDao.delete(id, type) + } + + suspend fun getCover(id: String): AbsoluteUrl? = + downloadsDao.get(id, type)?.cover?.let { AbsoluteUrl(it) } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/db/BookDatabase.kt b/test-app/src/main/java/org/readium/r2/testapp/data/db/AppDatabase.kt similarity index 59% rename from test-app/src/main/java/org/readium/r2/testapp/db/BookDatabase.kt rename to test-app/src/main/java/org/readium/r2/testapp/data/db/AppDatabase.kt index d4562bad0c..4f19e64f87 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/db/BookDatabase.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/db/AppDatabase.kt @@ -4,32 +4,41 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.testapp.db +package org.readium.r2.testapp.data.db import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters -import org.readium.r2.testapp.domain.model.* +import org.readium.r2.testapp.data.model.* +import org.readium.r2.testapp.data.model.Book +import org.readium.r2.testapp.data.model.Bookmark +import org.readium.r2.testapp.data.model.Catalog +import org.readium.r2.testapp.data.model.Highlight @Database( - entities = [Book::class, Bookmark::class, Highlight::class, Catalog::class], + entities = [Book::class, Bookmark::class, Highlight::class, Catalog::class, Download::class], version = 1, exportSchema = false ) -@TypeConverters(HighlightConverters::class) -abstract class BookDatabase : RoomDatabase() { +@TypeConverters( + HighlightConverters::class, + Download.Type.Converter::class +) +abstract class AppDatabase : RoomDatabase() { abstract fun booksDao(): BooksDao abstract fun catalogDao(): CatalogDao + abstract fun downloadsDao(): DownloadsDao + companion object { @Volatile - private var INSTANCE: BookDatabase? = null + private var INSTANCE: AppDatabase? = null - fun getDatabase(context: Context): BookDatabase { + fun getDatabase(context: Context): AppDatabase { val tempInstance = INSTANCE if (tempInstance != null) { return tempInstance @@ -37,8 +46,8 @@ abstract class BookDatabase : RoomDatabase() { synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, - BookDatabase::class.java, - "books_database" + AppDatabase::class.java, + "database" ).build() INSTANCE = instance return instance diff --git a/test-app/src/main/java/org/readium/r2/testapp/db/BooksDao.kt b/test-app/src/main/java/org/readium/r2/testapp/data/db/BooksDao.kt similarity index 81% rename from test-app/src/main/java/org/readium/r2/testapp/db/BooksDao.kt rename to test-app/src/main/java/org/readium/r2/testapp/data/db/BooksDao.kt index c9ed6da5aa..af31e8a779 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/db/BooksDao.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/db/BooksDao.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.testapp.db +package org.readium.r2.testapp.data.db import androidx.annotation.ColorInt import androidx.lifecycle.LiveData @@ -13,9 +13,9 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import kotlinx.coroutines.flow.Flow -import org.readium.r2.testapp.domain.model.Book -import org.readium.r2.testapp.domain.model.Bookmark -import org.readium.r2.testapp.domain.model.Highlight +import org.readium.r2.testapp.data.model.Book +import org.readium.r2.testapp.data.model.Bookmark +import org.readium.r2.testapp.data.model.Highlight @Dao interface BooksDao { @@ -59,7 +59,9 @@ interface BooksDao { /** * Retrieve all highlights for a specific book */ - @Query("SELECT * FROM ${Highlight.TABLE_NAME} WHERE ${Highlight.BOOK_ID} = :bookId ORDER BY ${Highlight.TOTAL_PROGRESSION} ASC") + @Query( + "SELECT * FROM ${Highlight.TABLE_NAME} WHERE ${Highlight.BOOK_ID} = :bookId ORDER BY ${Highlight.TOTAL_PROGRESSION} ASC" + ) fun getHighlightsForBook(bookId: Long): Flow<List<Highlight>> /** @@ -87,13 +89,17 @@ interface BooksDao { /** * Updates a highlight's annotation. */ - @Query("UPDATE ${Highlight.TABLE_NAME} SET ${Highlight.ANNOTATION} = :annotation WHERE ${Highlight.ID} = :id") + @Query( + "UPDATE ${Highlight.TABLE_NAME} SET ${Highlight.ANNOTATION} = :annotation WHERE ${Highlight.ID} = :id" + ) suspend fun updateHighlightAnnotation(id: Long, annotation: String) /** * Updates a highlight's tint and style. */ - @Query("UPDATE ${Highlight.TABLE_NAME} SET ${Highlight.TINT} = :tint, ${Highlight.STYLE} = :style WHERE ${Highlight.ID} = :id") + @Query( + "UPDATE ${Highlight.TABLE_NAME} SET ${Highlight.TINT} = :tint, ${Highlight.STYLE} = :style WHERE ${Highlight.ID} = :id" + ) suspend fun updateHighlightStyle(id: Long, style: Highlight.Style, @ColorInt tint: Int) /** @@ -113,6 +119,8 @@ interface BooksDao { * @param locator Location of the book * @param id The book to update */ - @Query("UPDATE " + Book.TABLE_NAME + " SET " + Book.PROGRESSION + " = :locator WHERE " + Book.ID + "= :id") + @Query( + "UPDATE " + Book.TABLE_NAME + " SET " + Book.PROGRESSION + " = :locator WHERE " + Book.ID + "= :id" + ) suspend fun saveProgression(locator: String, id: Long) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/db/CatalogDao.kt b/test-app/src/main/java/org/readium/r2/testapp/data/db/CatalogDao.kt similarity index 83% rename from test-app/src/main/java/org/readium/r2/testapp/db/CatalogDao.kt rename to test-app/src/main/java/org/readium/r2/testapp/data/db/CatalogDao.kt index e27a705848..1d4051e3c7 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/db/CatalogDao.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/db/CatalogDao.kt @@ -4,14 +4,14 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.testapp.db +package org.readium.r2.testapp.data.db import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import org.readium.r2.testapp.domain.model.Catalog +import org.readium.r2.testapp.data.model.Catalog @Dao interface CatalogDao { @@ -28,7 +28,9 @@ interface CatalogDao { * Retrieve list of Catalog models based on Catalog model * @return List of Catalog models as LiveData */ - @Query("SELECT * FROM " + Catalog.TABLE_NAME + " WHERE " + Catalog.TITLE + " = :title AND " + Catalog.HREF + " = :href AND " + Catalog.TYPE + " = :type") + @Query( + "SELECT * FROM " + Catalog.TABLE_NAME + " WHERE " + Catalog.TITLE + " = :title AND " + Catalog.HREF + " = :href AND " + Catalog.TYPE + " = :type" + ) fun getCatalogModels(title: String, href: String, type: Int): LiveData<List<Catalog>> /** diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/db/DownloadsDao.kt b/test-app/src/main/java/org/readium/r2/testapp/data/db/DownloadsDao.kt new file mode 100644 index 0000000000..c8ed9aba9e --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/data/db/DownloadsDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.data.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import org.readium.r2.testapp.data.model.Download + +@Dao +interface DownloadsDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(download: Download) + + @Query( + "DELETE FROM " + Download.TABLE_NAME + + " WHERE " + Download.ID + " = :id AND " + Download.TYPE + " = :type" + ) + suspend fun delete(id: String, type: Download.Type) + + @Query( + "SELECT * FROM " + Download.TABLE_NAME + + " WHERE " + Download.ID + " = :id AND " + Download.TYPE + " = :type" + ) + suspend fun get(id: String, type: Download.Type): Download? + + @Query( + "SELECT * FROM " + Download.TABLE_NAME + + " WHERE " + Download.TYPE + " = :type" + ) + suspend fun getDownloads(type: Download.Type): List<Download> +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/model/Book.kt b/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt similarity index 53% rename from test-app/src/main/java/org/readium/r2/testapp/domain/model/Book.kt rename to test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt index 309693610d..2f9d32eaa4 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/model/Book.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt @@ -4,15 +4,12 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.testapp.domain.model +package org.readium.r2.testapp.data.model -import android.net.Uri -import android.os.Build import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import java.net.URI -import java.nio.file.Paths +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.mediatype.MediaType @Entity(tableName = Book.TABLE_NAME) @@ -25,43 +22,45 @@ data class Book( @ColumnInfo(name = HREF) val href: String, @ColumnInfo(name = TITLE) - val title: String, + val title: String?, @ColumnInfo(name = AUTHOR) val author: String? = null, @ColumnInfo(name = IDENTIFIER) val identifier: String, @ColumnInfo(name = PROGRESSION) val progression: String? = null, - @ColumnInfo(name = TYPE) - val type: String + @ColumnInfo(name = MEDIA_TYPE) + val rawMediaType: String, + @ColumnInfo(name = COVER) + val cover: String ) { - val fileName: String? - get() { - val url = URI(href) - if (!url.scheme.isNullOrEmpty() && url.isAbsolute) { - val uri = Uri.parse(href) - return uri.lastPathSegment - } - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val path = Paths.get(href) - path.fileName.toString() - } else { - val uri = Uri.parse(href) - uri.lastPathSegment - } - } + constructor( + id: Long? = null, + creation: Long? = null, + href: String, + title: String?, + author: String? = null, + identifier: String, + progression: String? = null, + mediaType: MediaType, + cover: String + ) : this( + id = id, + creation = creation, + href = href, + title = title, + author = author, + identifier = identifier, + progression = progression, + rawMediaType = mediaType.toString(), + cover = cover + ) - val url: URI? - get() { - val url = URI(href) - if (url.isAbsolute && url.scheme.isNullOrEmpty()) { - return null - } - return url - } + val url: AbsoluteUrl get() = AbsoluteUrl(href)!! - suspend fun mediaType(): MediaType? = MediaType.of(type) + val mediaType: MediaType get() = + MediaType(rawMediaType)!! companion object { @@ -73,6 +72,7 @@ data class Book( const val AUTHOR = "author" const val IDENTIFIER = "identifier" const val PROGRESSION = "progression" - const val TYPE = "type" + const val MEDIA_TYPE = "media_type" + const val COVER = "cover" } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/model/Bookmark.kt b/test-app/src/main/java/org/readium/r2/testapp/data/model/Bookmark.kt similarity index 65% rename from test-app/src/main/java/org/readium/r2/testapp/domain/model/Bookmark.kt rename to test-app/src/main/java/org/readium/r2/testapp/data/model/Bookmark.kt index e9cd2ae496..4a8b27646b 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/model/Bookmark.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/model/Bookmark.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.testapp.domain.model +package org.readium.r2.testapp.data.model import androidx.room.ColumnInfo import androidx.room.Entity @@ -12,12 +12,14 @@ import androidx.room.Index import androidx.room.PrimaryKey import org.json.JSONObject import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType @Entity( tableName = Bookmark.TABLE_NAME, indices = [ Index( - value = ["BOOK_ID", "LOCATION"], + value = [Bookmark.BOOK_ID, Bookmark.LOCATION], unique = true ) ] @@ -30,8 +32,6 @@ data class Bookmark( var creation: Long? = null, @ColumnInfo(name = BOOK_ID) val bookId: Long, - @ColumnInfo(name = PUBLICATION_ID) - val publicationId: String, @ColumnInfo(name = RESOURCE_INDEX) val resourceIndex: Long, @ColumnInfo(name = RESOURCE_HREF) @@ -48,8 +48,8 @@ data class Bookmark( val locator get() = Locator( - href = resourceHref, - type = resourceType, + href = Url(resourceHref)!!, + mediaType = MediaType(resourceType) ?: MediaType.BINARY, title = resourceTitle, locations = Locator.Locations.fromJSON(JSONObject(location)), text = Locator.Text.fromJSON(JSONObject(locatorText)) @@ -57,16 +57,15 @@ data class Bookmark( companion object { - const val TABLE_NAME = "BOOKMARKS" - const val ID = "ID" - const val CREATION_DATE = "CREATION_DATE" - const val BOOK_ID = "BOOK_ID" - const val PUBLICATION_ID = "PUBLICATION_ID" - const val RESOURCE_INDEX = "RESOURCE_INDEX" - const val RESOURCE_HREF = "RESOURCE_HREF" - const val RESOURCE_TYPE = "RESOURCE_TYPE" - const val RESOURCE_TITLE = "RESOURCE_TITLE" - const val LOCATION = "LOCATION" - const val LOCATOR_TEXT = "LOCATOR_TEXT" + const val TABLE_NAME = "bookmarks" + const val ID = "id" + const val CREATION_DATE = "creation_date" + const val BOOK_ID = "book_id" + const val RESOURCE_INDEX = "resource_index" + const val RESOURCE_HREF = "resource_href" + const val RESOURCE_TYPE = "resource_type" + const val RESOURCE_TITLE = "resource_title" + const val LOCATION = "location" + const val LOCATOR_TEXT = "locator_text" } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/model/Catalog.kt b/test-app/src/main/java/org/readium/r2/testapp/data/model/Catalog.kt similarity index 76% rename from test-app/src/main/java/org/readium/r2/testapp/domain/model/Catalog.kt rename to test-app/src/main/java/org/readium/r2/testapp/data/model/Catalog.kt index 2eb3073a0a..86ad9bbba6 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/model/Catalog.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/model/Catalog.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.testapp.domain.model +package org.readium.r2.testapp.data.model import android.os.Parcelable import androidx.room.ColumnInfo @@ -27,10 +27,10 @@ data class Catalog( ) : Parcelable { companion object { - const val TABLE_NAME = "CATALOG" - const val ID = "ID" - const val TITLE = "TITLE" - const val HREF = "HREF" - const val TYPE = "TYPE" + const val TABLE_NAME = "catalogs" + const val ID = "id" + const val TITLE = "title" + const val HREF = "href" + const val TYPE = "type" } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/model/Download.kt b/test-app/src/main/java/org/readium/r2/testapp/data/model/Download.kt new file mode 100644 index 0000000000..ad2e6296ea --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/data/model/Download.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.data.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.TypeConverter + +/** + * Represents an on-going publication download, either from an OPDS catalog or an LCP acquisition. + * + * The download [id] is unique relative to its [type] (OPDS or LCP). + */ +@Entity(tableName = Download.TABLE_NAME, primaryKeys = [Download.ID, Download.TYPE]) +data class Download( + @ColumnInfo(name = TYPE) + val type: Type, + @ColumnInfo(name = ID) + val id: String, + @ColumnInfo(name = COVER) + val cover: String? = null, + @ColumnInfo(name = CREATION_DATE, defaultValue = "CURRENT_TIMESTAMP") + val creation: Long? = null +) { + enum class Type(val value: String) { + OPDS("opds"), LCP("lcp"); + + class Converter { + private val values = values().associateBy(Type::value) + + @TypeConverter + fun fromString(value: String?): Type = values[value]!! + + @TypeConverter + fun toString(type: Type): String = type.value + } + } + + companion object { + const val TABLE_NAME = "downloads" + const val CREATION_DATE = "creation_date" + const val ID = "id" + const val TYPE = "type" + const val COVER = "cover" + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/model/Highlight.kt b/test-app/src/main/java/org/readium/r2/testapp/data/model/Highlight.kt similarity index 84% rename from test-app/src/main/java/org/readium/r2/testapp/domain/model/Highlight.kt rename to test-app/src/main/java/org/readium/r2/testapp/data/model/Highlight.kt index 6df45b7d9c..87474ee719 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/model/Highlight.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/model/Highlight.kt @@ -4,12 +4,14 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.testapp.domain.model +package org.readium.r2.testapp.data.model import androidx.annotation.ColorInt import androidx.room.* import org.json.JSONObject import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType /** * @param id Primary key, auto-incremented @@ -27,7 +29,12 @@ import org.readium.r2.shared.publication.Locator @Entity( tableName = "highlights", foreignKeys = [ - ForeignKey(entity = Book::class, parentColumns = [Book.ID], childColumns = [Highlight.BOOK_ID], onDelete = ForeignKey.CASCADE) + ForeignKey( + entity = Book::class, + parentColumns = [Book.ID], + childColumns = [Highlight.BOOK_ID], + onDelete = ForeignKey.CASCADE + ) ], indices = [Index(value = [Highlight.BOOK_ID])] ) @@ -42,7 +49,8 @@ data class Highlight( @ColumnInfo(name = STYLE) var style: Style, @ColumnInfo(name = TINT, defaultValue = "0") - @ColorInt var tint: Int, + @ColorInt + var tint: Int, @ColumnInfo(name = HREF) var href: String, @ColumnInfo(name = TYPE) @@ -56,7 +64,7 @@ data class Highlight( @ColumnInfo(name = TEXT, defaultValue = "{}") var text: Locator.Text = Locator.Text(), @ColumnInfo(name = ANNOTATION, defaultValue = "") - var annotation: String = "", + var annotation: String = "" ) { constructor( @@ -70,8 +78,8 @@ data class Highlight( bookId = bookId, style = style, tint = tint, - href = locator.href, - type = locator.type, + href = locator.href.toString(), + type = locator.mediaType.toString(), title = locator.title, totalProgression = locator.locations.totalProgression ?: 0.0, locations = locator.locations, @@ -80,11 +88,11 @@ data class Highlight( ) val locator: Locator get() = Locator( - href = href, - type = type, + href = Url(href)!!, + mediaType = MediaType(type) ?: MediaType.BINARY, title = title, locations = locations, - text = text, + text = text ) enum class Style(val value: String) { @@ -121,16 +129,23 @@ data class Highlight( class HighlightConverters { @TypeConverter fun styleFromString(value: String?): Highlight.Style = Highlight.Style.getOrDefault(value) + @TypeConverter fun styleToString(style: Highlight.Style): String = style.value @TypeConverter - fun textFromString(value: String?): Locator.Text = Locator.Text.fromJSON(value?.let { JSONObject(it) }) + fun textFromString(value: String?): Locator.Text = Locator.Text.fromJSON( + value?.let { JSONObject(it) } + ) + @TypeConverter fun textToString(text: Locator.Text): String = text.toJSON().toString() @TypeConverter - fun locationsFromString(value: String?): Locator.Locations = Locator.Locations.fromJSON(value?.let { JSONObject(it) }) + fun locationsFromString(value: String?): Locator.Locations = Locator.Locations.fromJSON( + value?.let { JSONObject(it) } + ) + @TypeConverter fun locationsToString(text: Locator.Locations): String = text.toJSON().toString() } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt new file mode 100644 index 0000000000..39246187e5 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.domain + +import android.net.Uri +import java.io.File +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.file.FileSystemError +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.toUrl +import org.readium.r2.streamer.PublicationOpener +import org.readium.r2.testapp.data.BookRepository +import org.readium.r2.testapp.data.model.Book +import org.readium.r2.testapp.utils.extensions.formatPercentage +import org.readium.r2.testapp.utils.tryOrLog +import timber.log.Timber + +/** + * The [Bookshelf] supports two different processes: + * - directly _adding_ the url to a remote asset or an asset from shared storage to the database + * - _importing_ an asset, that is downloading or copying the publication the asset points to to the app storage + * before adding it to the database + */ +class Bookshelf( + private val bookRepository: BookRepository, + private val coverStorage: CoverStorage, + private val publicationOpener: PublicationOpener, + private val assetRetriever: AssetRetriever, + createPublicationRetriever: (PublicationRetriever.Listener) -> PublicationRetriever +) { + val channel: Channel<Event> = + Channel(Channel.UNLIMITED) + + private val publicationRetriever: PublicationRetriever + + init { + publicationRetriever = createPublicationRetriever(PublicationRetrieverListener()) + } + + sealed class Event { + data object ImportPublicationSuccess : + Event() + + class ImportPublicationError( + val error: ImportError + ) : Event() + } + + private val coroutineScope: CoroutineScope = + MainScope() + + private inner class PublicationRetrieverListener : PublicationRetriever.Listener { + override fun onSuccess(publication: File, coverUrl: AbsoluteUrl?) { + coroutineScope.launch { + val url = publication.toUrl() + addBookFeedback(url, coverUrl) + } + } + + override fun onProgressed(progress: Double) { + Timber.e("Downloaded ${progress.formatPercentage()}") + } + + override fun onError(error: ImportError) { + coroutineScope.launch { + channel.send(Event.ImportPublicationError(error)) + } + } + } + + fun importPublicationFromStorage( + uri: Uri + ) { + publicationRetriever.retrieveFromStorage(uri) + } + + fun importPublicationFromOpds( + publication: Publication + ) { + publicationRetriever.retrieveFromOpds(publication) + } + + fun addPublicationFromWeb( + url: AbsoluteUrl + ) { + coroutineScope.launch { + addBookFeedback(url) + } + } + + fun addPublicationFromStorage( + url: AbsoluteUrl + ) { + coroutineScope.launch { + addBookFeedback(url) + } + } + + private suspend fun addBookFeedback( + url: AbsoluteUrl, + coverUrl: AbsoluteUrl? = null + ) { + addBook(url, coverUrl) + .onSuccess { channel.send(Event.ImportPublicationSuccess) } + .onFailure { channel.send(Event.ImportPublicationError(it)) } + } + + private suspend fun addBook( + url: AbsoluteUrl, + coverUrl: AbsoluteUrl? = null + ): Try<Unit, ImportError> { + val asset = + assetRetriever.retrieve(url) + .getOrElse { + return Try.failure( + ImportError.Publication(PublicationError(it)) + ) + } + + publicationOpener.open( + asset, + allowUserInteraction = false + ).onSuccess { publication -> + val coverFile = + coverStorage.storeCover(publication, coverUrl) + .getOrElse { + return Try.failure( + ImportError.FileSystem( + FileSystemError.IO(it) + ) + ) + } + + val id = bookRepository.insertBook( + url, + asset.format.mediaType, + publication, + coverFile + ) + if (id == -1L) { + coverFile.delete() + return Try.failure( + ImportError.Database( + DebugError("Could not insert book into database.") + ) + ) + } + } + .onFailure { + Timber.e("Cannot open publication: $it.") + return Try.failure( + ImportError.Publication(PublicationError(it)) + ) + } + + return Try.success(Unit) + } + + suspend fun deleteBook(book: Book) { + val id = book.id!! + bookRepository.deleteBook(id) + tryOrLog { book.url.toFile()?.delete() } + tryOrLog { File(book.cover).delete() } + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt new file mode 100644 index 0000000000..27c95de8e0 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt @@ -0,0 +1,70 @@ +package org.readium.r2.testapp.domain + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import java.io.File +import java.io.FileOutputStream +import java.util.UUID +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.services.cover +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.http.HttpRequest +import org.readium.r2.shared.util.http.fetchWithDecoder +import org.readium.r2.testapp.utils.tryOrLog + +class CoverStorage( + appStorageDir: File, + private val httpClient: HttpClient +) { + + private val coverDir: File = + File(appStorageDir, "covers/") + .apply { if (!exists()) mkdirs() } + + suspend fun storeCover(publication: Publication, overrideUrl: AbsoluteUrl?): Try<File, Exception> { + val coverBitmap: Bitmap? = overrideUrl?.fetchBitmap() + ?: publication.cover() + return try { + Try.success(storeCover(coverBitmap)) + } catch (e: Exception) { + Try.failure(e) + } + } + + private suspend fun AbsoluteUrl.fetchBitmap(): Bitmap? = + tryOrLog { + when { + isFile -> toFile()?.toBitmap() + isHttp -> httpClient.fetchBitmap(HttpRequest(this)).getOrNull() + else -> null + } + } + + private suspend fun File.toBitmap(): Bitmap? = + withContext(Dispatchers.IO) { + tryOrLog { + BitmapFactory.decodeFile(path) + } + } + + private suspend fun HttpClient.fetchBitmap(request: HttpRequest): Try<Bitmap, HttpError> = + fetchWithDecoder(request) { response -> + BitmapFactory.decodeByteArray(response.body, 0, response.body.size) + } + + private suspend fun storeCover(cover: Bitmap?): File = + withContext(Dispatchers.IO) { + val coverImageFile = File(coverDir, "${UUID.randomUUID()}.png") + val resized = cover?.let { Bitmap.createScaledBitmap(it, 120, 200, true) } + val fos = FileOutputStream(coverImageFile) + resized?.compress(Bitmap.CompressFormat.PNG, 80, fos) + fos.flush() + fos.close() + coverImageFile + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt new file mode 100644 index 0000000000..2f01d68ce7 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.domain + +import org.readium.r2.lcp.LcpError +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.file.FileSystemError +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError + +sealed class ImportError( + override val cause: Error? +) : Error { + + override val message: String = + "Import failed" + + object MissingLcpSupport : + ImportError(DebugError("Lcp support is missing.")) + + class LcpAcquisitionFailed( + override val cause: LcpError + ) : ImportError(cause) + + class Publication( + override val cause: PublicationError + ) : ImportError(cause) + + class FileSystem( + override val cause: FileSystemError + ) : ImportError(cause) + + class DownloadFailed( + override val cause: DownloadManager.DownloadError + ) : ImportError(cause) + + class Opds(override val cause: Error) : + ImportError(cause) + + class Database(override val cause: Error) : + ImportError(cause) + + fun toUserError(): UserError = when (this) { + is MissingLcpSupport -> UserError(R.string.missing_lcp_support, cause = this) + is Database -> UserError(R.string.import_publication_unable_add_pub_database, cause = this) + is DownloadFailed -> UserError(R.string.import_publication_download_failed, cause = this) + is LcpAcquisitionFailed -> cause.toUserError() + is Opds -> UserError(R.string.import_publication_no_acquisition, cause = this) + is Publication -> cause.toUserError() + is FileSystem -> cause.toUserError() + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/LcpUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/LcpUserError.kt new file mode 100644 index 0000000000..6d4796fe41 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/LcpUserError.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.domain + +import org.readium.r2.lcp.LcpError +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError + +fun LcpError.toUserError(): UserError = when (this) { + LcpError.LicenseInteractionNotAvailable -> + UserError(R.string.lcp_error_license_interaction_not_available, cause = this) + LcpError.LicenseProfileNotSupported -> + UserError(R.string.lcp_error_license_profile_not_supported, cause = this) + LcpError.CrlFetching -> + UserError(R.string.lcp_error_crl_fetching, cause = this) + is LcpError.Network -> + UserError(R.string.lcp_error_network, cause = this) + + is LcpError.Runtime -> + UserError(R.string.lcp_error_runtime, cause = this) + is LcpError.Unknown -> + UserError(R.string.lcp_error_unknown, cause = this) + + is LcpError.Container -> + when (this) { + is LcpError.Container.FileNotFound -> + UserError(R.string.lcp_error_container_file_not_found, cause = this) + LcpError.Container.OpenFailed -> + UserError(R.string.lcp_error_container_open_failed, cause = this) + is LcpError.Container.ReadFailed -> + UserError(R.string.lcp_error_container_read_failed, cause = this) + is LcpError.Container.WriteFailed -> + UserError(R.string.lcp_error_container_write_failed, cause = this) + } + + is LcpError.Decryption -> + when (this) { + LcpError.Decryption.ContentDecryptError -> + UserError(R.string.lcp_error_decryption_content_decrypt_error, cause = this) + LcpError.Decryption.ContentKeyDecryptError -> + UserError(R.string.lcp_error_decryption_content_key_decrypt_error, cause = this) + } + + is LcpError.LicenseIntegrity -> + when (this) { + LcpError.LicenseIntegrity.CertificateRevoked -> + UserError(R.string.lcp_error_license_integrity_certificate_revoked, cause = this) + LcpError.LicenseIntegrity.InvalidCertificateSignature -> + UserError( + R.string.lcp_error_license_integrity_invalid_certificate_signature, + cause = this + ) + LcpError.LicenseIntegrity.InvalidLicenseSignature -> + UserError( + R.string.lcp_error_license_integrity_invalid_license_signature, + cause = this + ) + LcpError.LicenseIntegrity.InvalidLicenseSignatureDate -> + UserError( + R.string.lcp_error_license_integrity_invalid_license_signature_date, + cause = this + ) + LcpError.LicenseIntegrity.InvalidUserKeyCheck -> + UserError(R.string.lcp_error_license_integrity_invalid_user_key_check, cause = this) + } + + is LcpError.LicenseStatus -> + when (this) { + is LcpError.LicenseStatus.Cancelled -> + UserError(R.string.lcp_error_license_status_cancelled, date, cause = this) + is LcpError.LicenseStatus.Expired -> + UserError(R.string.lcp_error_license_status_expired, end, cause = this) + is LcpError.LicenseStatus.NotStarted -> + UserError(R.string.lcp_error_license_status_not_started, start, cause = this) + is LcpError.LicenseStatus.Returned -> + UserError(R.string.lcp_error_license_status_returned, date, cause = this) + is LcpError.LicenseStatus.Revoked -> + UserError( + R.plurals.lcp_error_license_status_revoked, + devicesCount, + date, + devicesCount, + cause = this + ) + } + + is LcpError.Parsing -> + when (this) { + LcpError.Parsing.LicenseDocument -> + UserError(R.string.lcp_error_parsing_license_document, cause = this) + LcpError.Parsing.MalformedJSON -> + UserError(R.string.lcp_error_parsing_malformed_json, cause = this) + LcpError.Parsing.StatusDocument -> + UserError(R.string.lcp_error_parsing_license_document, cause = this) + else -> + UserError(R.string.lcp_error_parsing, cause = this) + } + + is LcpError.Renew -> + when (this) { + is LcpError.Renew.InvalidRenewalPeriod -> + UserError(R.string.lcp_error_renew_invalid_renewal_period, cause = this) + LcpError.Renew.RenewFailed -> + UserError(R.string.lcp_error_renew_renew_failed, cause = this) + LcpError.Renew.UnexpectedServerError -> + UserError(R.string.lcp_error_renew_unexpected_server_error, cause = this) + } + + is LcpError.Return -> + when (this) { + LcpError.Return.AlreadyReturnedOrExpired -> + UserError(R.string.lcp_error_return_already_returned_or_expired, cause = this) + LcpError.Return.ReturnFailed -> + UserError(R.string.lcp_error_return_return_failed, cause = this) + LcpError.Return.UnexpectedServerError -> + UserError(R.string.lcp_error_return_unexpected_server_error, cause = this) + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt new file mode 100644 index 0000000000..d5eabcaac8 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.domain + +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.streamer.PublicationOpener +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError + +sealed class PublicationError( + override val message: String, + override val cause: Error? = null +) : Error { + + class ReadError(override val cause: org.readium.r2.shared.util.data.ReadError) : + PublicationError(cause.message, cause.cause) + + class UnsupportedScheme(cause: Error) : + PublicationError(cause.message, cause.cause) + + class FormatNotSupported(cause: Error) : + PublicationError(cause.message, cause.cause) + + class InvalidPublication(cause: Error) : + PublicationError(cause.message, cause.cause) + + class Unexpected(cause: Error) : + PublicationError(cause.message, cause.cause) + + fun toUserError(): UserError = + when (this) { + is InvalidPublication -> + UserError(R.string.publication_error_invalid_publication, cause = this) + is Unexpected -> + UserError(R.string.publication_error_unexpected, cause = this) + is FormatNotSupported -> + UserError(R.string.publication_error_unsupported_asset, cause = this) + is UnsupportedScheme -> + UserError(R.string.publication_error_scheme_not_supported, cause = this) + is ReadError -> + cause.toUserError() + } + + companion object { + + operator fun invoke(error: AssetRetriever.RetrieveUrlError): PublicationError = + when (error) { + is AssetRetriever.RetrieveUrlError.Reading -> + ReadError(error.cause) + is AssetRetriever.RetrieveUrlError.FormatNotSupported -> + FormatNotSupported(error) + is AssetRetriever.RetrieveUrlError.SchemeNotSupported -> + UnsupportedScheme(error) + } + + operator fun invoke(error: AssetRetriever.RetrieveError): PublicationError = + when (error) { + is AssetRetriever.RetrieveError.Reading -> + ReadError(error.cause) + is AssetRetriever.RetrieveError.FormatNotSupported -> + FormatNotSupported(error) + } + + operator fun invoke(error: PublicationOpener.OpenError): PublicationError = + when (error) { + is PublicationOpener.OpenError.Reading -> + ReadError(error.cause) + is PublicationOpener.OpenError.FormatNotSupported -> + FormatNotSupported(error) + } + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt new file mode 100644 index 0000000000..45b84ca90c --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -0,0 +1,419 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.domain + +import android.content.Context +import android.net.Uri +import java.io.File +import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import org.readium.r2.lcp.LcpError +import org.readium.r2.lcp.LcpPublicationRetriever as ReadiumLcpPublicationRetriever +import org.readium.r2.lcp.LcpService +import org.readium.r2.lcp.license.model.LicenseDocument +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.opds.images +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.ResourceAsset +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.file.FileSystemError +import org.readium.r2.shared.util.format.LcpLicenseSpecification +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.testapp.data.DownloadRepository +import org.readium.r2.testapp.utils.extensions.copyToTempFile +import org.readium.r2.testapp.utils.extensions.moveTo +import org.readium.r2.testapp.utils.tryOrNull +import timber.log.Timber + +/** + * Retrieves a publication from a remote or local source and import it into the bookshelf storage. + * + * If the source file is a LCP license document, the protected publication will be downloaded. + */ +class PublicationRetriever( + private val listener: Listener, + createLocalPublicationRetriever: (Listener) -> LocalPublicationRetriever, + createOpdsPublicationRetriever: (Listener) -> OpdsPublicationRetriever +) { + + private val localPublicationRetriever: LocalPublicationRetriever + private val opdsPublicationRetriever: OpdsPublicationRetriever + + interface Listener { + + fun onSuccess(publication: File, coverUrl: AbsoluteUrl?) + fun onProgressed(progress: Double) + fun onError(error: ImportError) + } + + init { + localPublicationRetriever = createLocalPublicationRetriever(object : Listener { + override fun onSuccess(publication: File, coverUrl: AbsoluteUrl?) { + listener.onSuccess(publication, coverUrl) + } + + override fun onProgressed(progress: Double) { + listener.onProgressed(progress) + } + + override fun onError(error: ImportError) { + listener.onError(error) + } + }) + + opdsPublicationRetriever = createOpdsPublicationRetriever(object : Listener { + override fun onSuccess(publication: File, coverUrl: AbsoluteUrl?) { + localPublicationRetriever.retrieve(publication, coverUrl) + } + + override fun onProgressed(progress: Double) { + listener.onProgressed(progress) + } + + override fun onError(error: ImportError) { + listener.onError(error) + } + }) + } + + fun retrieveFromStorage(uri: Uri) { + localPublicationRetriever.retrieve(uri) + } + + fun retrieveFromOpds(publication: Publication) { + opdsPublicationRetriever.retrieve(publication) + } +} + +/** + * Retrieves a publication from a file (publication or LCP license document) stored on the device. + */ +class LocalPublicationRetriever( + private val listener: PublicationRetriever.Listener, + private val context: Context, + private val storageDir: File, + private val assetRetriever: AssetRetriever, + createLcpPublicationRetriever: (PublicationRetriever.Listener) -> LcpPublicationRetriever? +) { + + private val lcpPublicationRetriever: LcpPublicationRetriever? + + private val coroutineScope: CoroutineScope = + MainScope() + + init { + lcpPublicationRetriever = createLcpPublicationRetriever(LcpListener()) + } + + /** + * Retrieves the publication from the given local [uri]. + */ + fun retrieve(uri: Uri) { + coroutineScope.launch { + val tempFile = uri.copyToTempFile(context, storageDir) + .getOrElse { + listener.onError( + ImportError.FileSystem(FileSystemError.IO(it)) + ) + return@launch + } + retrieveFromStorage(tempFile) + } + } + + /** + * Retrieves the publication stored at the given [tempFile]. + */ + fun retrieve( + tempFile: File, + coverUrl: AbsoluteUrl? = null + ) { + coroutineScope.launch { + retrieveFromStorage(tempFile, coverUrl) + } + } + + private suspend fun retrieveFromStorage( + tempFile: File, + coverUrl: AbsoluteUrl? = null + ) { + val sourceAsset = assetRetriever.retrieve(tempFile) + .getOrElse { + listener.onError( + ImportError.Publication(PublicationError(it)) + ) + return + } + + if ( + sourceAsset is ResourceAsset && + sourceAsset.format.conformsTo(LcpLicenseSpecification) + ) { + if (lcpPublicationRetriever == null) { + listener.onError(ImportError.MissingLcpSupport) + } else { + lcpPublicationRetriever.retrieve(sourceAsset, tempFile, coverUrl) + } + return + } + + val fileExtension = sourceAsset.format.fileExtension + val fileName = "${UUID.randomUUID()}.${fileExtension.value}" + val libraryFile = File(storageDir, fileName) + + try { + tempFile.moveTo(libraryFile) + } catch (e: Exception) { + Timber.d(e) + tryOrNull { libraryFile.delete() } + listener.onError( + ImportError.Publication( + PublicationError.ReadError( + ReadError.Access(FileSystemError.IO(e)) + ) + ) + ) + return + } + + listener.onSuccess(libraryFile, coverUrl) + } + + private inner class LcpListener : PublicationRetriever.Listener { + override fun onSuccess(publication: File, coverUrl: AbsoluteUrl?) { + coroutineScope.launch { + retrieve(publication, coverUrl) + } + } + + override fun onProgressed(progress: Double) { + listener.onProgressed(progress) + } + + override fun onError(error: ImportError) { + listener.onError(error) + } + } +} + +/** + * Retrieves a publication from an OPDS entry. + */ +class OpdsPublicationRetriever( + private val listener: PublicationRetriever.Listener, + private val downloadManager: DownloadManager, + private val downloadRepository: DownloadRepository +) { + + private val coroutineScope: CoroutineScope = + MainScope() + + init { + coroutineScope.launch { + for (download in downloadRepository.all()) { + downloadManager.register( + DownloadManager.RequestId(download.id), + downloadListener + ) + } + } + } + + /** + * Retrieves the file of the given OPDS [publication]. + */ + fun retrieve(publication: Publication) { + coroutineScope.launch { + val publicationUrl = publication.acquisitionUrl() + .getOrElse { + listener.onError(ImportError.Opds(it)) + return@launch + } + + val coverUrl = publication.images.firstOrNull() + ?.let { publication.url(it) } + + val requestId = downloadManager.submit( + request = DownloadManager.Request( + publicationUrl, + headers = emptyMap() + ), + listener = downloadListener + ) + downloadRepository.insert( + id = requestId.value, + cover = coverUrl as? AbsoluteUrl + ) + } + } + + private fun Publication.acquisitionUrl(): Try<AbsoluteUrl, Error> { + val acquisitionUrl = links + .filter { it.mediaType?.isPublication == true || it.mediaType == MediaType.LCP_LICENSE_DOCUMENT } + .firstNotNullOfOrNull { it.url() as? AbsoluteUrl } + ?: return Try.failure(DebugError("No supported link to acquire publication.")) + + return Try.success(acquisitionUrl) + } + + private val downloadListener: DownloadListener = + DownloadListener() + + private inner class DownloadListener : DownloadManager.Listener { + override fun onDownloadCompleted( + requestId: DownloadManager.RequestId, + download: DownloadManager.Download + ) { + coroutineScope.launch { + val coverUrl = downloadRepository.getCover(requestId.value) + downloadRepository.remove(requestId.value) + listener.onSuccess(download.file, coverUrl) + } + } + + override fun onDownloadProgressed( + requestId: DownloadManager.RequestId, + downloaded: Long, + expected: Long? + ) { + coroutineScope.launch { + val progression = expected?.let { downloaded.toDouble() / expected } ?: return@launch + listener.onProgressed(progression) + } + } + + override fun onDownloadFailed( + requestId: DownloadManager.RequestId, + error: DownloadManager.DownloadError + ) { + coroutineScope.launch { + downloadRepository.remove(requestId.value) + listener.onError(ImportError.DownloadFailed(error)) + } + } + + override fun onDownloadCancelled(requestId: DownloadManager.RequestId) { + coroutineScope.launch { + Timber.v("Download ${requestId.value} has been cancelled.") + downloadRepository.remove(requestId.value) + } + } + } +} + +/** + * Retrieves a publication from an LCP license document. + */ +class LcpPublicationRetriever( + private val listener: PublicationRetriever.Listener, + private val downloadRepository: DownloadRepository, + private val lcpPublicationRetriever: ReadiumLcpPublicationRetriever +) { + + private val coroutineScope: CoroutineScope = + MainScope() + + init { + coroutineScope.launch { + for (download in downloadRepository.all()) { + lcpPublicationRetriever.register( + ReadiumLcpPublicationRetriever.RequestId(download.id), + lcpRetrieverListener + ) + } + } + } + + /** + * Retrieves a publication protected with the given license. + */ + fun retrieve( + licenceAsset: ResourceAsset, + licenceFile: File, + coverUrl: AbsoluteUrl? + ) { + coroutineScope.launch { + val license = licenceAsset.resource.read() + .getOrElse { + listener.onError(ImportError.Publication(PublicationError.ReadError(it))) + return@launch + } + .let { + LicenseDocument.fromBytes(it) + .getOrElse { error -> + listener.onError( + ImportError.LcpAcquisitionFailed(error) + ) + return@launch + } + } + + tryOrNull { licenceFile.delete() } + + val requestId = lcpPublicationRetriever.retrieve( + license, + lcpRetrieverListener + ) + + downloadRepository.insert(requestId.value, coverUrl) + } + } + + private val lcpRetrieverListener: LcpRetrieverListener = + LcpRetrieverListener() + + private inner class LcpRetrieverListener : ReadiumLcpPublicationRetriever.Listener { + override fun onAcquisitionCompleted( + requestId: ReadiumLcpPublicationRetriever.RequestId, + acquiredPublication: LcpService.AcquiredPublication + ) { + coroutineScope.launch { + val coverUrl = downloadRepository.getCover(requestId.value) + downloadRepository.remove(requestId.value) + listener.onSuccess(acquiredPublication.localFile, coverUrl) + } + } + + override fun onAcquisitionProgressed( + requestId: ReadiumLcpPublicationRetriever.RequestId, + downloaded: Long, + expected: Long? + ) { + coroutineScope.launch { + val progression = expected?.let { downloaded.toDouble() / expected } ?: return@launch + listener.onProgressed(progression) + } + } + + override fun onAcquisitionFailed( + requestId: ReadiumLcpPublicationRetriever.RequestId, + error: LcpError + ) { + coroutineScope.launch { + downloadRepository.remove(requestId.value) + listener.onError( + ImportError.LcpAcquisitionFailed(error) + ) + } + } + + override fun onAcquisitionCancelled(requestId: ReadiumLcpPublicationRetriever.RequestId) { + coroutineScope.launch { + Timber.v("Acquisition ${requestId.value} has been cancelled.") + downloadRepository.remove(requestId.value) + } + } + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt new file mode 100644 index 0000000000..f0b4c8a7fe --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.domain + +import org.readium.r2.shared.util.content.ContentResolverError +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.file.FileSystemError +import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.http.HttpStatus +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError + +fun ReadError.toUserError(): UserError = when (this) { + is ReadError.Access -> + when (val cause = this.cause) { + is HttpError -> cause.toUserError() + is FileSystemError -> cause.toUserError() + is ContentResolverError -> cause.toUserError() + else -> UserError(R.string.error_unexpected, cause = this) + } + + is ReadError.Decoding -> UserError(R.string.publication_error_invalid_publication, cause = this) + is ReadError.OutOfMemory -> UserError(R.string.publication_error_out_of_memory, cause = this) + is ReadError.UnsupportedOperation -> UserError( + R.string.publication_error_unexpected, + cause = this + ) +} + +fun HttpError.toUserError(): UserError = when (this) { + is HttpError.IO -> UserError(R.string.publication_error_network_unexpected, cause = this) + is HttpError.MalformedResponse -> UserError( + R.string.publication_error_network_unexpected, + cause = this + ) + is HttpError.Redirection -> UserError( + R.string.publication_error_network_unexpected, + cause = this + ) + is HttpError.Timeout -> UserError(R.string.publication_error_network_timeout, cause = this) + is HttpError.Unreachable -> UserError( + R.string.publication_error_network_unreachable, + cause = this + ) + is HttpError.SslHandshake -> UserError( + R.string.publication_error_network_ssl_handshake, + cause = this + ) + is HttpError.ErrorResponse -> when (status) { + HttpStatus.Forbidden -> UserError( + R.string.publication_error_network_forbidden, + cause = this + ) + HttpStatus.NotFound -> UserError(R.string.publication_error_network_not_found, cause = this) + else -> UserError(R.string.publication_error_network_unexpected, cause = this) + } +} + +fun FileSystemError.toUserError(): UserError = when (this) { + is FileSystemError.Forbidden -> UserError( + R.string.publication_error_filesystem_unexpected, + cause = this + ) + is FileSystemError.IO -> UserError( + R.string.publication_error_filesystem_unexpected, + cause = this + ) + is FileSystemError.InsufficientSpace -> UserError( + R.string.publication_error_filesystem_insufficient_space, + cause = this + ) + is FileSystemError.FileNotFound -> UserError( + R.string.publication_error_filesystem_not_found, + cause = this + ) +} + +fun ContentResolverError.toUserError(): UserError = when (this) { + is ContentResolverError.FileNotFound -> UserError( + R.string.publication_error_filesystem_not_found, + cause = this + ) + is ContentResolverError.IO -> UserError( + R.string.publication_error_filesystem_unexpected, + cause = this + ) + is ContentResolverError.NotAvailable -> UserError(R.string.error_unexpected, cause = this) +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt index 883dda0ae2..b042ad256e 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt @@ -15,19 +15,16 @@ import androidx.fragment.app.setFragmentResult import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar import java.util.* import kotlinx.coroutines.launch import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat import org.readium.r2.lcp.MaterialRenewListener import org.readium.r2.lcp.lcpLicense -import org.readium.r2.shared.UserException import org.readium.r2.testapp.R import org.readium.r2.testapp.databinding.FragmentDrmManagementBinding import org.readium.r2.testapp.reader.ReaderViewModel import org.readium.r2.testapp.utils.viewLifecycle -import timber.log.Timber class DrmManagementFragment : Fragment() { @@ -73,10 +70,11 @@ class DrmManagementFragment : Fragment() { binding.drmValueCopiesLeft.text = model.copiesLeft val datesVisibility = - if (model.start != null && model.end != null && model.start != model.end) + if (model.start != null && model.end != null && model.start != model.end) { View.VISIBLE - else + } else { View.GONE + } binding.drmStart.visibility = datesVisibility binding.drmValueStart.text = model.start.toFormattedString() @@ -103,9 +101,8 @@ class DrmManagementFragment : Fragment() { model.renewLoan(this@DrmManagementFragment) .onSuccess { newDate -> binding.drmValueEnd.text = newDate.toFormattedString() - }.onFailure { exception -> - exception.toastUserMessage(requireView()) } + .onFailure { handle(it) } } } @@ -121,22 +118,17 @@ class DrmManagementFragment : Fragment() { .onSuccess { val result = DrmManagementContract.createResult(hasReturned = true) setFragmentResult(DrmManagementContract.REQUEST_KEY, result) - }.onFailure { exception -> - exception.toastUserMessage(requireView()) } + .onFailure { handle(it) } } } .show() } + + private fun handle(error: DrmManagementViewModel.DrmError) { + error.toUserError().show(requireActivity()) + } } private fun Date?.toFormattedString() = DateTime(this).toString(DateTimeFormat.shortDateTime()).orEmpty() - -// FIXME: the toast is drawn behind the navigation bar -private fun Exception.toastUserMessage(view: View) { - if (this is UserException) - Snackbar.make(view, getUserMessage(view.context), Snackbar.LENGTH_LONG).show() - - Timber.d(this) -} diff --git a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt index bc14687812..551f2682d1 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt @@ -8,11 +8,19 @@ package org.readium.r2.testapp.drm import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel -import java.util.* +import java.util.Date +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try +import org.readium.r2.testapp.utils.UserError abstract class DrmManagementViewModel : ViewModel() { + interface DrmError { + + val error: Error + fun toUserError(): UserError + } + abstract val type: String open val state: String? = null @@ -33,11 +41,9 @@ abstract class DrmManagementViewModel : ViewModel() { open val canRenewLoan: Boolean = false - open suspend fun renewLoan(fragment: Fragment): Try<Date?, Exception> = - Try.failure(Exception("Renewing a loan is not supported")) + abstract suspend fun renewLoan(fragment: Fragment): Try<Date?, DrmError> open val canReturnPublication: Boolean = false - open suspend fun returnPublication(): Try<Unit, Exception> = - Try.failure(Exception("Returning a publication is not supported")) + abstract suspend fun returnPublication(): Try<Unit, DrmError> } diff --git a/test-app/src/main/java/org/readium/r2/testapp/drm/LcpManagementViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/drm/LcpManagementViewModel.kt index 8dd921dd27..e5939baa9a 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/drm/LcpManagementViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/drm/LcpManagementViewModel.kt @@ -9,37 +9,51 @@ package org.readium.r2.testapp.drm import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import java.util.* +import java.util.Date +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpLicense import org.readium.r2.shared.util.Try +import org.readium.r2.testapp.domain.toUserError +import org.readium.r2.testapp.utils.UserError class LcpManagementViewModel( private val lcpLicense: LcpLicense, - private val renewListener: LcpLicense.RenewListener, + private val renewListener: LcpLicense.RenewListener ) : DrmManagementViewModel() { class Factory( private val lcpLicense: LcpLicense, - private val renewListener: LcpLicense.RenewListener, + private val renewListener: LcpLicense.RenewListener ) : ViewModelProvider.NewInstanceFactory() { override fun <T : ViewModel> create(modelClass: Class<T>): T = - modelClass.getDeclaredConstructor(LcpLicense::class.java, LcpLicense.RenewListener::class.java) + modelClass.getDeclaredConstructor( + LcpLicense::class.java, + LcpLicense.RenewListener::class.java + ) .newInstance(lcpLicense, renewListener) } + class LcpDrmError( + override val error: LcpError + ) : DrmError { + + override fun toUserError(): UserError = + error.toUserError() + } + override val type: String = "LCP" override val state: String? - get() = lcpLicense.status?.status?.rawValue + get() = lcpLicense.status?.status?.value - override val provider: String? + override val provider: String get() = lcpLicense.license.provider - override val issued: Date? + override val issued: Date get() = lcpLicense.license.issued - override val updated: Date? + override val updated: Date get() = lcpLicense.license.updated override val start: Date? @@ -49,25 +63,27 @@ class LcpManagementViewModel( get() = lcpLicense.license.rights.end override val copiesLeft: String = - lcpLicense.charactersToCopyLeft + lcpLicense.charactersToCopyLeft.value ?.let { "$it characters" } ?: super.copiesLeft override val printsLeft: String = - lcpLicense.pagesToPrintLeft + lcpLicense.pagesToPrintLeft.value ?.let { "$it pages" } ?: super.printsLeft override val canRenewLoan: Boolean get() = lcpLicense.canRenewLoan - override suspend fun renewLoan(fragment: Fragment): Try<Date?, Exception> { + override suspend fun renewLoan(fragment: Fragment): Try<Date?, LcpDrmError> { return lcpLicense.renewLoan(renewListener) + .mapFailure { LcpDrmError(it) } } override val canReturnPublication: Boolean get() = lcpLicense.canReturnPublication - override suspend fun returnPublication(): Try<Unit, Exception> = + override suspend fun returnPublication(): Try<Unit, LcpDrmError> = lcpLicense.returnPublication() + .mapFailure { LcpDrmError(it) } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/opds/GridAutoFitLayoutManager.kt b/test-app/src/main/java/org/readium/r2/testapp/opds/GridAutoFitLayoutManager.kt index 20f512453b..d437f0742d 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/opds/GridAutoFitLayoutManager.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/opds/GridAutoFitLayoutManager.kt @@ -27,7 +27,12 @@ class GridAutoFitLayoutManager : GridLayoutManager { setColumnWidth(checkedColumnWidth(context, columnWidth)) } /* Initially set spanCount to 1, will be changed automatically later. */ - constructor(context: Context, columnWidth: Int, orientation: Int, reverseLayout: Boolean) : super(context, 1, orientation, reverseLayout) { + constructor(context: Context, columnWidth: Int, orientation: Int, reverseLayout: Boolean) : super( + context, + 1, + orientation, + reverseLayout + ) { setColumnWidth(checkedColumnWidth(context, columnWidth)) } /* Initially set spanCount to 1, will be changed automatically later. */ @@ -35,12 +40,14 @@ class GridAutoFitLayoutManager : GridLayoutManager { var width = columnWidth width = if (width <= 0) { TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, sColumnWidth.toFloat(), + TypedValue.COMPLEX_UNIT_DIP, + sColumnWidth.toFloat(), context.resources.displayMetrics ).toInt() } else { TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, width.toFloat(), + TypedValue.COMPLEX_UNIT_DIP, + width.toFloat(), context.resources.displayMetrics ).toInt() } diff --git a/test-app/src/main/java/org/readium/r2/testapp/outline/BookmarksFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/outline/BookmarksFragment.kt index 5dd4bc6cf8..d11ce37b93 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/outline/BookmarksFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/outline/BookmarksFragment.kt @@ -22,12 +22,13 @@ import kotlin.math.roundToInt import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Url import org.readium.r2.testapp.R +import org.readium.r2.testapp.data.model.Bookmark import org.readium.r2.testapp.databinding.FragmentListviewBinding import org.readium.r2.testapp.databinding.ItemRecycleBookmarkBinding -import org.readium.r2.testapp.domain.model.Bookmark import org.readium.r2.testapp.reader.ReaderViewModel -import org.readium.r2.testapp.utils.extensions.outlineTitle +import org.readium.r2.testapp.utils.extensions.readium.outlineTitle import org.readium.r2.testapp.utils.viewLifecycle class BookmarksFragment : Fragment() { @@ -58,13 +59,20 @@ class BookmarksFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - bookmarkAdapter = BookmarkAdapter(publication, onBookmarkDeleteRequested = { bookmark -> viewModel.deleteBookmark(bookmark.id!!) }, onBookmarkSelectedRequested = { bookmark -> onBookmarkSelected(bookmark) }) + bookmarkAdapter = BookmarkAdapter( + publication, + onBookmarkDeleteRequested = { bookmark -> viewModel.deleteBookmark(bookmark.id!!) }, + onBookmarkSelectedRequested = { bookmark -> onBookmarkSelected(bookmark) } + ) binding.listView.apply { layoutManager = LinearLayoutManager(requireContext()) adapter = bookmarkAdapter } - val comparator: Comparator<Bookmark> = compareBy({ it.resourceIndex }, { it.locator.locations.progression }) + val comparator: Comparator<Bookmark> = compareBy( + { it.resourceIndex }, + { it.locator.locations.progression } + ) viewModel.getBookmarks().observe(viewLifecycleOwner) { val bookmarks = it.sortedWith(comparator) bookmarkAdapter.submitList(bookmarks) @@ -92,7 +100,9 @@ class BookmarkAdapter( ): ViewHolder { return ViewHolder( ItemRecycleBookmarkBinding.inflate( - LayoutInflater.from(parent.context), parent, false + LayoutInflater.from(parent.context), + parent, + false ) ) } @@ -104,10 +114,12 @@ class BookmarkAdapter( holder.bind(item) } - inner class ViewHolder(val binding: ItemRecycleBookmarkBinding) : RecyclerView.ViewHolder(binding.root) { + inner class ViewHolder(val binding: ItemRecycleBookmarkBinding) : RecyclerView.ViewHolder( + binding.root + ) { fun bind(bookmark: Bookmark) { - val title = getBookSpineItem(bookmark.resourceHref) + val title = getBookSpineItem(Url(bookmark.resourceHref)!!) ?: "*Title Missing*" binding.bookmarkChapter.text = title @@ -120,7 +132,6 @@ class BookmarkAdapter( binding.bookmarkTimestamp.text = formattedDate binding.overflow.setOnClickListener { - val popupMenu = PopupMenu(binding.overflow.context, binding.overflow) popupMenu.menuInflater.inflate(R.menu.menu_bookmark, popupMenu.menu) popupMenu.show() @@ -139,14 +150,14 @@ class BookmarkAdapter( } } - private fun getBookSpineItem(href: String): String? { + private fun getBookSpineItem(href: Url): String? { for (link in publication.tableOfContents) { - if (link.href == href) { + if (link.url() == href) { return link.outlineTitle } } for (link in publication.readingOrder) { - if (link.href == href) { + if (link.url() == href) { return link.outlineTitle } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/outline/HighlightsFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/outline/HighlightsFragment.kt index 1137d0eb16..f354c09853 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/outline/HighlightsFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/outline/HighlightsFragment.kt @@ -25,9 +25,9 @@ import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat import org.readium.r2.shared.publication.Publication import org.readium.r2.testapp.R +import org.readium.r2.testapp.data.model.Highlight import org.readium.r2.testapp.databinding.FragmentListviewBinding import org.readium.r2.testapp.databinding.ItemRecycleHighlightBinding -import org.readium.r2.testapp.domain.model.Highlight import org.readium.r2.testapp.reader.ReaderViewModel import org.readium.r2.testapp.utils.viewLifecycle @@ -59,7 +59,11 @@ class HighlightsFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - highlightAdapter = HighlightAdapter(publication, onDeleteHighlightRequested = { highlight -> viewModel.deleteHighlight(highlight.id) }, onHighlightSelectedRequested = { highlight -> onHighlightSelected(highlight) }) + highlightAdapter = HighlightAdapter( + publication, + onDeleteHighlightRequested = { highlight -> viewModel.deleteHighlight(highlight.id) }, + onHighlightSelectedRequested = { highlight -> onHighlightSelected(highlight) } + ) binding.listView.apply { layoutManager = LinearLayoutManager(requireContext()) adapter = highlightAdapter @@ -91,7 +95,9 @@ class HighlightAdapter( ): ViewHolder { return ViewHolder( ItemRecycleHighlightBinding.inflate( - LayoutInflater.from(parent.context), parent, false + LayoutInflater.from(parent.context), + parent, + false ) ) } @@ -103,19 +109,25 @@ class HighlightAdapter( holder.bind(item) } - inner class ViewHolder(val binding: ItemRecycleHighlightBinding) : RecyclerView.ViewHolder(binding.root) { + inner class ViewHolder(val binding: ItemRecycleHighlightBinding) : RecyclerView.ViewHolder( + binding.root + ) { fun bind(highlight: Highlight) { binding.highlightChapter.text = highlight.title binding.highlightText.text = highlight.locator.text.highlight binding.annotation.text = highlight.annotation - val formattedDate = DateTime(highlight.creation).toString(DateTimeFormat.shortDateTime()) + val formattedDate = DateTime(highlight.creation).toString( + DateTimeFormat.shortDateTime() + ) binding.highlightTimeStamp.text = formattedDate binding.highlightOverflow.setOnClickListener { - - val popupMenu = PopupMenu(binding.highlightOverflow.context, binding.highlightOverflow) + val popupMenu = PopupMenu( + binding.highlightOverflow.context, + binding.highlightOverflow + ) popupMenu.menuInflater.inflate(R.menu.menu_bookmark, popupMenu.menu) popupMenu.show() diff --git a/test-app/src/main/java/org/readium/r2/testapp/outline/NavigationFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/outline/NavigationFragment.kt index 8626204fd8..fb8836549c 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/outline/NavigationFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/outline/NavigationFragment.kt @@ -11,6 +11,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout +import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import androidx.lifecycle.ViewModelProvider @@ -20,7 +21,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.testapp.databinding.FragmentListviewBinding import org.readium.r2.testapp.databinding.ItemRecycleNavigationBinding import org.readium.r2.testapp.reader.ReaderViewModel -import org.readium.r2.testapp.utils.extensions.outlineTitle +import org.readium.r2.testapp.utils.extensions.readium.outlineTitle import org.readium.r2.testapp.utils.viewLifecycle /* @@ -41,7 +42,9 @@ class NavigationFragment : Fragment() { publication = it.publication } - links = requireNotNull(requireArguments().getParcelableArrayList(LINKS_ARG)) + links = requireNotNull( + BundleCompat.getParcelableArrayList(requireArguments(), LINKS_ARG, Link::class.java) + ) } override fun onCreateView( @@ -96,7 +99,10 @@ class NavigationFragment : Fragment() { fun newInstance(links: List<Link>) = NavigationFragment().apply { arguments = Bundle().apply { - putParcelableArrayList(LINKS_ARG, if (links is ArrayList<Link>) links else ArrayList(links)) + putParcelableArrayList( + LINKS_ARG, + if (links is ArrayList<Link>) links else ArrayList(links) + ) } } } @@ -111,7 +117,9 @@ class NavigationAdapter(private val onLinkSelected: (Link) -> Unit) : ): ViewHolder { return ViewHolder( ItemRecycleNavigationBinding.inflate( - LayoutInflater.from(parent.context), parent, false + LayoutInflater.from(parent.context), + parent, + false ) ) } @@ -123,11 +131,16 @@ class NavigationAdapter(private val onLinkSelected: (Link) -> Unit) : holder.bind(item) } - inner class ViewHolder(val binding: ItemRecycleNavigationBinding) : RecyclerView.ViewHolder(binding.root) { + inner class ViewHolder(val binding: ItemRecycleNavigationBinding) : RecyclerView.ViewHolder( + binding.root + ) { fun bind(item: Pair<Int, Link>) { binding.navigationTextView.text = item.second.outlineTitle - binding.indentation.layoutParams = LinearLayout.LayoutParams(item.first * 50, ViewGroup.LayoutParams.MATCH_PARENT) + binding.indentation.layoutParams = LinearLayout.LayoutParams( + item.first * 50, + ViewGroup.LayoutParams.MATCH_PARENT + ) binding.root.setOnClickListener { onLinkSelected(item.second) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/outline/OutlineContract.kt b/test-app/src/main/java/org/readium/r2/testapp/outline/OutlineContract.kt index 55f23bc6a7..fb46d2d5e1 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/outline/OutlineContract.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/outline/OutlineContract.kt @@ -7,6 +7,7 @@ package org.readium.r2.testapp.outline import android.os.Bundle +import androidx.core.os.BundleCompat import org.readium.r2.shared.publication.Locator object OutlineContract { @@ -21,7 +22,9 @@ object OutlineContract { Bundle().apply { putParcelable(DESTINATION_KEY, locator) } fun parseResult(result: Bundle): Result { - val destination = requireNotNull(result.getParcelable<Locator>(DESTINATION_KEY)) + val destination = requireNotNull( + BundleCompat.getParcelable(result, DESTINATION_KEY, Locator::class.java) + ) return Result(destination) } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/outline/OutlineFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/outline/OutlineFragment.kt index 3de2c3d22d..aa5e1f8bc5 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/outline/OutlineFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/outline/OutlineFragment.kt @@ -60,12 +60,22 @@ class OutlineFragment : Fragment() { super.onViewCreated(view, savedInstanceState) val outlines: List<Outline> = when { - publication.conformsTo(Publication.Profile.EPUB) -> listOf(Outline.Contents, Outline.Bookmarks, Outline.Highlights, Outline.PageList, Outline.Landmarks) + publication.conformsTo(Publication.Profile.EPUB) -> listOf( + Outline.Contents, + Outline.Bookmarks, + Outline.Highlights, + Outline.PageList, + Outline.Landmarks + ) else -> listOf(Outline.Contents, Outline.Bookmarks) } binding.outlinePager.adapter = OutlineFragmentStateAdapter(this, publication, outlines) - TabLayoutMediator(binding.outlineTabLayout, binding.outlinePager) { tab, idx -> tab.setText(outlines[idx].label) }.attach() + TabLayoutMediator(binding.outlineTabLayout, binding.outlinePager) { tab, idx -> + tab.setText( + outlines[idx].label + ) + }.attach() } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/AudioReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/AudioReaderFragment.kt index a9c0010823..c22b40414b 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/AudioReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/AudioReaderFragment.kt @@ -20,25 +20,28 @@ import androidx.lifecycle.viewModelScope import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit -import kotlin.time.ExperimentalTime -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.readium.navigator.media2.ExperimentalMedia2 -import org.readium.navigator.media2.MediaNavigator +import org.readium.adapter.exoplayer.audio.ExoPlayerPreferences +import org.readium.adapter.exoplayer.audio.ExoPlayerSettings +import org.readium.navigator.media.common.MediaNavigator +import org.readium.navigator.media.common.TimeBasedMediaNavigator +import org.readium.r2.navigator.preferences.Configurable +import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.services.cover import org.readium.r2.testapp.R import org.readium.r2.testapp.databinding.FragmentAudiobookBinding +import org.readium.r2.testapp.reader.preferences.UserPreferencesViewModel import org.readium.r2.testapp.utils.viewLifecycle import timber.log.Timber -@OptIn(ExperimentalMedia2::class, ExperimentalTime::class, ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalReadiumApi::class) class AudioReaderFragment : BaseReaderFragment(), SeekBar.OnSeekBarChangeListener { - override lateinit var navigator: MediaNavigator + override lateinit var navigator: TimeBasedMediaNavigator<*, *, *> - private lateinit var displayedPlayback: MediaNavigator.Playback private var binding: FragmentAudiobookBinding by viewLifecycle() private var seekingItem: Int? = null @@ -64,6 +67,14 @@ class AudioReaderFragment : BaseReaderFragment(), SeekBar.OnSeekBarChangeListene override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + @Suppress("Unchecked_cast") + (navigator as? Configurable<ExoPlayerSettings, ExoPlayerPreferences>) + ?.let { navigator -> + @Suppress("Unchecked_cast") + (model.settings as UserPreferencesViewModel<ExoPlayerSettings, ExoPlayerPreferences>) + .bind(navigator, viewLifecycleOwner) + } + binding.publicationTitle.text = model.publication.metadata.title viewLifecycleOwner.lifecycleScope.launch { @@ -72,19 +83,16 @@ class AudioReaderFragment : BaseReaderFragment(), SeekBar.OnSeekBarChangeListene } } - displayedPlayback = navigator.playback.value - - viewLifecycleOwner.lifecycleScope.launch { - navigator.playback.collectLatest { playback -> - onPlaybackChanged(playback) - } - } + navigator.playback + .onEach { onPlaybackChanged(it) } + .launchIn(viewLifecycleOwner.lifecycleScope) } - private fun onPlaybackChanged(playback: MediaNavigator.Playback) { + private fun onPlaybackChanged( + playback: TimeBasedMediaNavigator.Playback + ) { Timber.v("onPlaybackChanged $playback") - this.displayedPlayback = playback - if (playback.state == MediaNavigator.Playback.State.Error) { + if (playback.state is MediaNavigator.State.Failure) { onPlayerError() return } @@ -94,22 +102,27 @@ class AudioReaderFragment : BaseReaderFragment(), SeekBar.OnSeekBarChangeListene binding.timelineDuration.isEnabled = true binding.timelinePosition.isEnabled = true binding.playPause.setImageResource( - if (playback.state == MediaNavigator.Playback.State.Playing) + if (playback.playWhenReady) { R.drawable.ic_baseline_pause_24 - else + } else { R.drawable.ic_baseline_play_arrow_24 + } ) + if (seekingItem == null) { - updateTimeline(playback.resource, playback.buffer.position) + updateTimeline(playback) } } - private fun updateTimeline(resource: MediaNavigator.Playback.Resource, buffered: Duration) { - binding.timelineBar.max = resource.duration?.inWholeSeconds?.toInt() ?: 0 - binding.timelineDuration.text = resource.duration?.formatElapsedTime() - binding.timelineBar.progress = resource.position.inWholeSeconds.toInt() - binding.timelinePosition.text = resource.position.formatElapsedTime() - binding.timelineBar.secondaryProgress = buffered.inWholeSeconds.toInt() + private fun updateTimeline( + playback: TimeBasedMediaNavigator.Playback + ) { + val currentItem = navigator.readingOrder.items[playback.index] + binding.timelineBar.max = currentItem.duration?.inWholeSeconds?.toInt() ?: 0 + binding.timelineDuration.text = currentItem.duration?.formatElapsedTime() + binding.timelineBar.progress = playback.offset.inWholeSeconds.toInt() + binding.timelinePosition.text = playback.offset.formatElapsedTime() + binding.timelineBar.secondaryProgress = playback.buffered?.inWholeSeconds?.toInt() ?: 0 } private fun Duration.formatElapsedTime(): String = @@ -139,7 +152,7 @@ class AudioReaderFragment : BaseReaderFragment(), SeekBar.OnSeekBarChangeListene @Suppress("UNUSED_PARAMETER") private fun forbidUserSeeking(view: View, event: MotionEvent): Boolean = - this.displayedPlayback.state == MediaNavigator.Playback.State.Finished + navigator.playback.value.state is MediaNavigator.State.Ended override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { if (fromUser) { @@ -149,44 +162,35 @@ class AudioReaderFragment : BaseReaderFragment(), SeekBar.OnSeekBarChangeListene override fun onStartTrackingTouch(seekBar: SeekBar) { Timber.d("onStartTrackingTouch") - seekingItem = this.displayedPlayback.resource.index + seekingItem = navigator.playback.value.index } override fun onStopTrackingTouch(seekBar: SeekBar) { Timber.d("onStopTrackingTouch") - seekingItem?.let { index -> - lifecycleScope.launch { - navigator.seek(index, seekBar.progress.seconds) - // Some timeline updates might have been missed during seeking. - val playbackNow = navigator.playback.value - updateTimeline(playbackNow.resource, playbackNow.buffer.position) - seekingItem = null - } - } + navigator.skipTo(checkNotNull(seekingItem), seekBar.progress.seconds) + seekingItem = null } private fun onPlayPause(@Suppress("UNUSED_PARAMETER") view: View) { - return when (displayedPlayback.state) { - MediaNavigator.Playback.State.Playing -> { + return when (navigator.playback.value.state) { + is MediaNavigator.State.Ready, is MediaNavigator.State.Buffering -> { model.viewModelScope.launch { - navigator.pause() - } - Unit - } - MediaNavigator.Playback.State.Paused -> { - model.viewModelScope.launch { - navigator.play() + if (navigator.playback.value.playWhenReady) { + navigator.pause() + } else { + navigator.play() + } } Unit } - MediaNavigator.Playback.State.Finished -> { + is MediaNavigator.State.Ended -> { model.viewModelScope.launch { - navigator.seek(0, Duration.ZERO) + navigator.skipTo(0, Duration.ZERO) navigator.play() } Unit } - MediaNavigator.Playback.State.Error -> { + is MediaNavigator.State.Failure -> { // Do nothing. } } @@ -194,13 +198,13 @@ class AudioReaderFragment : BaseReaderFragment(), SeekBar.OnSeekBarChangeListene private fun onSkipForward(@Suppress("UNUSED_PARAMETER") view: View) { model.viewModelScope.launch { - navigator.goForward() + navigator.skipForward() } } private fun onSkipBackward(@Suppress("UNUSED_PARAMETER") view: View) { model.viewModelScope.launch { - navigator.goBackward() + navigator.skipBackward() } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/BaseReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/BaseReaderFragment.kt index 3318834dba..75db24de60 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/BaseReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/BaseReaderFragment.kt @@ -10,18 +10,21 @@ import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem +import android.view.View import android.widget.Toast +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import org.readium.r2.lcp.lcpLicense import org.readium.r2.navigator.Navigator import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.testapp.R import org.readium.r2.testapp.reader.preferences.UserPreferencesBottomSheetDialogFragment +import org.readium.r2.testapp.utils.UserError /* * Base reader fragment class @@ -37,7 +40,6 @@ abstract class BaseReaderFragment : Fragment() { protected abstract val navigator: Navigator override fun onCreate(savedInstanceState: Bundle?) { - setHasOptionsMenu(true) super.onCreate(savedInstanceState) model.fragmentChannel.receive(this) { event -> @@ -47,55 +49,74 @@ abstract class BaseReaderFragment : Fragment() { when (event) { is ReaderViewModel.FeedbackEvent.BookmarkFailed -> toast(R.string.bookmark_exists) - is ReaderViewModel.FeedbackEvent.BookmarkSuccessfullyAdded -> toast(R.string.bookmark_added) + is ReaderViewModel.FeedbackEvent.BookmarkSuccessfullyAdded -> toast( + R.string.bookmark_added + ) } } } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val menuHost: MenuHost = requireActivity() + + menuHost.addMenuProvider( + object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_reader, menu) + + menu.findItem(R.id.settings).isVisible = + navigator is Configurable<*, *> + + menu.findItem(R.id.drm).isVisible = + model.publication.lcpLicense != null + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + when (menuItem.itemId) { + R.id.toc -> { + model.activityChannel.send( + ReaderViewModel.ActivityCommand.OpenOutlineRequested + ) + return true + } + R.id.bookmark -> { + model.insertBookmark(navigator.currentLocator.value) + return true + } + R.id.settings -> { + val settingsModel = checkNotNull(model.settings) + UserPreferencesBottomSheetDialogFragment(settingsModel, "User Settings") + .show(childFragmentManager, "Settings") + return true + } + R.id.drm -> { + model.activityChannel.send( + ReaderViewModel.ActivityCommand.OpenDrmManagementRequested + ) + return true + } + } + return false + } + }, + viewLifecycleOwner + ) + } + override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) setMenuVisibility(!hidden) requireActivity().invalidateOptionsMenu() } - override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.menu_reader, menu) - - menu.findItem(R.id.settings).isVisible = - navigator is Configurable<*, *> - - menu.findItem(R.id.drm).isVisible = - model.publication.lcpLicense != null - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.toc -> { - model.activityChannel.send(ReaderViewModel.Event.OpenOutlineRequested) - } - R.id.bookmark -> { - model.insertBookmark(navigator.currentLocator.value) - } - R.id.settings -> { - val settingsModel = checkNotNull(model.settings) - UserPreferencesBottomSheetDialogFragment(settingsModel, "User Settings") - .show(childFragmentManager, "Settings") - } - R.id.drm -> { - model.activityChannel.send(ReaderViewModel.Event.OpenDrmManagementRequested) - } - else -> return super.onOptionsItemSelected(item) - } - - return true - } - open fun go(locator: Locator, animated: Boolean) { navigator.go(locator, animated) } - protected fun showError(error: UserException) { - val context = context ?: return - Toast.makeText(context, error.getUserMessage(context), Toast.LENGTH_LONG).show() + protected fun showError(error: UserError) { + val activity = activity ?: return + error.show(activity) } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/EpubReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/EpubReaderFragment.kt index d87d3d4796..8c203d33a3 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/EpubReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/EpubReaderFragment.kt @@ -14,10 +14,16 @@ import android.view.inputmethod.InputMethodManager import android.widget.ImageView import androidx.annotation.ColorInt import androidx.appcompat.widget.SearchView +import androidx.core.os.BundleCompat +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.fragment.app.FragmentResultListener import androidx.fragment.app.commit import androidx.fragment.app.commitNow +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.launch import org.readium.r2.navigator.DecorableNavigator import org.readium.r2.navigator.Decoration import org.readium.r2.navigator.ExperimentalDecorator @@ -35,7 +41,7 @@ import org.readium.r2.testapp.reader.preferences.UserPreferencesViewModel import org.readium.r2.testapp.search.SearchFragment @OptIn(ExperimentalReadiumApi::class, ExperimentalDecorator::class) -class EpubReaderFragment : VisualReaderFragment(), EpubNavigatorFragment.Listener { +class EpubReaderFragment : VisualReaderFragment() { override lateinit var navigator: EpubNavigatorFragment @@ -62,8 +68,8 @@ class EpubReaderFragment : VisualReaderFragment(), EpubNavigatorFragment.Listene childFragmentManager.fragmentFactory = readerData.navigatorFactory.createFragmentFactory( initialLocator = readerData.initialLocation, - listener = this, initialPreferences = readerData.preferencesManager.preferences.value, + listener = model, configuration = EpubNavigatorFragment.Configuration { // To customize the text selection menu. selectionActionModeCallback = customSelectionActionModeCallback @@ -104,14 +110,16 @@ class EpubReaderFragment : VisualReaderFragment(), EpubNavigatorFragment.Listene this, FragmentResultListener { _, result -> menuSearch.collapseActionView() - result.getParcelable<Locator>(SearchFragment::class.java.name)?.let { + BundleCompat.getParcelable( + result, + SearchFragment::class.java.name, + Locator::class.java + )?.let { navigator.go(it) } } ) - setHasOptionsMenu(true) - super.onCreate(savedInstanceState) } @@ -121,14 +129,18 @@ class EpubReaderFragment : VisualReaderFragment(), EpubNavigatorFragment.Listene savedInstanceState: Bundle? ): View { val view = super.onCreateView(inflater, container, savedInstanceState) - val navigatorFragmentTag = getString(R.string.epub_navigator_tag) if (savedInstanceState == null) { childFragmentManager.commitNow { - add(R.id.fragment_reader_container, EpubNavigatorFragment::class.java, Bundle(), navigatorFragmentTag) + add( + R.id.fragment_reader_container, + EpubNavigatorFragment::class.java, + Bundle(), + NAVIGATOR_FRAGMENT_TAG + ) } } - navigator = childFragmentManager.findFragmentByTag(navigatorFragmentTag) as EpubNavigatorFragment + navigator = childFragmentManager.findFragmentByTag(NAVIGATOR_FRAGMENT_TAG) as EpubNavigatorFragment return view } @@ -140,10 +152,42 @@ class EpubReaderFragment : VisualReaderFragment(), EpubNavigatorFragment.Listene (model.settings as UserPreferencesViewModel<EpubSettings, EpubPreferences>) .bind(navigator, viewLifecycleOwner) - viewLifecycleOwner.lifecycleScope.launchWhenStarted { - // Display page number labels if the book contains a `page-list` navigation document. - (navigator as? DecorableNavigator)?.applyPageNumberDecorations() + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + // Display page number labels if the book contains a `page-list` navigation document. + (navigator as? DecorableNavigator)?.applyPageNumberDecorations() + } } + + val menuHost: MenuHost = requireActivity() + + menuHost.addMenuProvider( + object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuSearch = menu.findItem(R.id.search).apply { + isVisible = true + menuSearchView = actionView as SearchView + } + + connectSearch() + if (!isSearchViewIconified) menuSearch.expandActionView() + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + when (menuItem.itemId) { + R.id.search -> { + return true + } + android.R.id.home -> { + menuSearch.collapseActionView() + return true + } + } + return false + } + }, + viewLifecycleOwner + ) } /** @@ -161,25 +205,13 @@ class EpubReaderFragment : VisualReaderFragment(), EpubNavigatorFragment.Listene Decoration( id = "page-$index", locator = locator, - style = DecorationStylePageNumber(label = label), + style = DecorationStylePageNumber(label = label) ) } applyDecorations(decorations, "pageNumbers") } - override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) { - super.onCreateOptionsMenu(menu, menuInflater) - - menuSearch = menu.findItem(R.id.search).apply { - isVisible = true - menuSearchView = actionView as SearchView - } - - connectSearch() - if (!isSearchViewIconified) menuSearch.expandActionView() - } - override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putBoolean(IS_SEARCH_VIEW_ICONIFIED, isSearchViewIconified) @@ -220,33 +252,27 @@ class EpubReaderFragment : VisualReaderFragment(), EpubNavigatorFragment.Listene } }) - menuSearchView.findViewById<ImageView>(R.id.search_close_btn).setOnClickListener { + menuSearchView.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn).setOnClickListener { menuSearchView.requestFocus() model.cancelSearch() menuSearchView.setQuery("", false) (activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager)?.showSoftInput( - this.view, InputMethodManager.SHOW_FORCED + this.view, + 0 ) } } - override fun onOptionsItemSelected(item: MenuItem): Boolean = - when (item.itemId) { - R.id.search -> { - super.onOptionsItemSelected(item) - } - android.R.id.home -> { - menuSearch.collapseActionView() - true - } - else -> super.onOptionsItemSelected(item) - } - private fun showSearchFragment() { childFragmentManager.commit { childFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG)?.let { remove(it) } - add(R.id.fragment_reader_container, SearchFragment::class.java, Bundle(), SEARCH_FRAGMENT_TAG) + add( + R.id.fragment_reader_container, + SearchFragment::class.java, + Bundle(), + SEARCH_FRAGMENT_TAG + ) hide(navigator) addToBackStack(SEARCH_FRAGMENT_TAG) } @@ -254,6 +280,7 @@ class EpubReaderFragment : VisualReaderFragment(), EpubNavigatorFragment.Listene companion object { private const val SEARCH_FRAGMENT_TAG = "search" + private const val NAVIGATOR_FRAGMENT_TAG = "navigator" private const val IS_SEARCH_VIEW_ICONIFIED = "isSearchViewIconified" } } @@ -269,7 +296,7 @@ class EpubReaderFragment : VisualReaderFragment(), EpubNavigatorFragment.Listene @OptIn(ExperimentalDecorator::class) private fun annotationMarkTemplate(@ColorInt defaultTint: Int = Color.YELLOW): HtmlDecorationTemplate { val className = "testapp-annotation-mark" - val iconUrl = EpubNavigatorFragment.assetUrl("annotation-icon.svg") + val iconUrl = checkNotNull(EpubNavigatorFragment.assetUrl("annotation-icon.svg")) return HtmlDecorationTemplate( layout = HtmlDecorationTemplate.Layout.BOUNDS, width = HtmlDecorationTemplate.Width.PAGE, diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/FullscreenReaderActivityDelegate.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/FullscreenReaderActivityDelegate.kt index b8c1bc8d5c..62d648b1d3 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/FullscreenReaderActivityDelegate.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/FullscreenReaderActivityDelegate.kt @@ -22,11 +22,10 @@ import org.readium.r2.testapp.utils.showSystemUi class FullscreenReaderActivityDelegate( private val activity: AppCompatActivity, private val readerFragment: VisualReaderFragment, - private val binding: ActivityReaderBinding, + private val binding: ActivityReaderBinding ) : DefaultLifecycleObserver { override fun onCreate(owner: LifecycleOwner) { - // Without this, activity_reader_container receives the insets only once, // although we need a call every time the reader is hidden activity.window.decorView.setOnApplyWindowInsetsListener { view, insets -> @@ -49,19 +48,21 @@ class FullscreenReaderActivityDelegate( } private fun updateSystemUiVisibility() { - if (readerFragment.isHidden) + if (readerFragment.isHidden) { activity.showSystemUi() - else + } else { readerFragment.updateSystemUiVisibility() + } // Seems to be required to adjust padding when transitioning from the outlines to the screen reader binding.activityContainer.requestApplyInsets() } private fun updateSystemUiPadding(container: View, insets: WindowInsets) { - if (readerFragment.isHidden) + if (readerFragment.isHidden) { container.padSystemUi(insets, activity) - else + } else { container.clearPadding() + } } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ImageReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ImageReaderFragment.kt index ad7fdbb21e..15e811a9bb 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ImageReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ImageReaderFragment.kt @@ -15,7 +15,7 @@ import org.readium.r2.navigator.Navigator import org.readium.r2.navigator.image.ImageNavigatorFragment import org.readium.r2.testapp.R -class ImageReaderFragment : VisualReaderFragment(), ImageNavigatorFragment.Listener { +class ImageReaderFragment : VisualReaderFragment() { override lateinit var navigator: Navigator @@ -31,7 +31,11 @@ class ImageReaderFragment : VisualReaderFragment(), ImageNavigatorFragment.Liste } childFragmentManager.fragmentFactory = - ImageNavigatorFragment.createFactory(publication, readerData.initialLocation, this) + ImageNavigatorFragment.createFactory( + publication, + readerData.initialLocation, + model + ) super.onCreate(savedInstanceState) } @@ -44,7 +48,12 @@ class ImageReaderFragment : VisualReaderFragment(), ImageNavigatorFragment.Liste val view = super.onCreateView(inflater, container, savedInstanceState) if (savedInstanceState == null) { childFragmentManager.commitNow { - add(R.id.fragment_reader_container, ImageNavigatorFragment::class.java, Bundle(), NAVIGATOR_FRAGMENT_TAG) + add( + R.id.fragment_reader_container, + ImageNavigatorFragment::class.java, + Bundle(), + NAVIGATOR_FRAGMENT_TAG + ) } } navigator = childFragmentManager.findFragmentByTag(NAVIGATOR_FRAGMENT_TAG)!! as Navigator diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/MediaService.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/MediaService.kt index 8e420630b4..369b2dd6e9 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/MediaService.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/MediaService.kt @@ -13,21 +13,31 @@ import android.content.Intent import android.content.ServiceConnection import android.os.Build import android.os.IBinder -import androidx.lifecycle.lifecycleScope -import androidx.media2.session.MediaSession -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.sample -import org.readium.navigator.media2.ExperimentalMedia2 -import org.readium.navigator.media2.MediaNavigator -import org.readium.r2.testapp.utils.LifecycleMedia2SessionService +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.ServiceCompat +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSessionService +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.readium.navigator.media.common.Media3Adapter +import org.readium.navigator.media.common.MediaNavigator +import org.readium.r2.shared.ExperimentalReadiumApi import timber.log.Timber -@OptIn(ExperimentalMedia2::class) -class MediaService : LifecycleMedia2SessionService() { +@OptIn(ExperimentalReadiumApi::class) +typealias AnyMediaNavigator = MediaNavigator<*, *, *> + +@OptIn(ExperimentalReadiumApi::class) +@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +class MediaService : MediaSessionService() { + + class Session( + val bookId: Long, + val navigator: AnyMediaNavigator, + val mediaSession: MediaSession + ) { + val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + } /** * The service interface to be used by the app. @@ -37,38 +47,54 @@ class MediaService : LifecycleMedia2SessionService() { private val app: org.readium.r2.testapp.Application get() = application as org.readium.r2.testapp.Application - private var saveLocationJob: Job? = null - - private var mediaNavigator: MediaNavigator? = null + private val sessionMutable: MutableStateFlow<Session?> = + MutableStateFlow(null) - var mediaSession: MediaSession? = null + val session: StateFlow<Session?> = + sessionMutable.asStateFlow() - fun closeNavigator() { - stopForeground(true) - mediaSession?.close() - mediaSession = null - saveLocationJob?.cancel() - saveLocationJob = null - mediaNavigator?.close() - mediaNavigator?.publication?.close() - mediaNavigator = null + fun closeSession() { + Timber.d("closeSession") + session.value?.let { session -> + session.mediaSession.release() + session.coroutineScope.cancel() + session.navigator.close() + sessionMutable.value = null + } } @OptIn(FlowPreview::class) - fun bindNavigator(navigator: MediaNavigator, bookId: Long) { + fun <N> openSession( + navigator: N, + bookId: Long + ) where N : AnyMediaNavigator, N : Media3Adapter { + Timber.d("openSession") val activityIntent = createSessionActivityIntent() - mediaNavigator = navigator - mediaSession = navigator.session(applicationContext, activityIntent) - .also { addSession(it) } + val mediaSession = MediaSession.Builder(applicationContext, navigator.asMedia3Player()) + .setSessionActivity(activityIntent) + .setId(bookId.toString()) + .build() + + addSession(mediaSession) + + val session = Session( + bookId, + navigator, + mediaSession + ) + + sessionMutable.value = session /* * Launch a job for saving progression even when playback is going on in the background * with no ReaderActivity opened. */ - saveLocationJob = navigator.currentLocator + navigator.currentLocator .sample(3000) - .onEach { locator -> app.bookRepository.saveProgression(locator, bookId) } - .launchIn(lifecycleScope) + .onEach { locator -> + Timber.d("Saving TTS progression $locator") + app.bookRepository.saveProgression(locator, bookId) + }.launchIn(session.coroutineScope) } private fun createSessionActivityIntent(): PendingIntent { @@ -78,24 +104,28 @@ class MediaService : LifecycleMedia2SessionService() { flags = flags or PendingIntent.FLAG_IMMUTABLE } - val intent = application.packageManager.getLaunchIntentForPackage(application.packageName) + val intent = application.packageManager.getLaunchIntentForPackage( + application.packageName + ) + return PendingIntent.getActivity(applicationContext, 0, intent, flags) } + + fun stop() { + closeSession() + ServiceCompat.stopForeground(this@MediaService, ServiceCompat.STOP_FOREGROUND_REMOVE) + this@MediaService.stopSelf() + } } private val binder by lazy { Binder() } - override fun onCreate() { - super.onCreate() - Timber.d("MediaService created.") - } - - override fun onBind(intent: Intent): IBinder? { + override fun onBind(intent: Intent?): IBinder? { Timber.d("onBind called with $intent") - return if (intent.action == SERVICE_INTERFACE) { + return if (intent?.action == SERVICE_INTERFACE) { super.onBind(intent) // Readium-aware client. Timber.d("Returning custom binder.") @@ -108,20 +138,24 @@ class MediaService : LifecycleMedia2SessionService() { } override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { - return binder.mediaSession - } - - override fun onDestroy() { - super.onDestroy() - Timber.d("MediaService destroyed.") + return binder.session.value?.mediaSession } override fun onTaskRemoved(rootIntent: Intent) { super.onTaskRemoved(rootIntent) Timber.d("Task removed. Stopping session and service.") - // Close the navigator to allow the service to be stopped. - binder.closeNavigator() - stopSelf() + // Close the session to allow the service to be stopped. + binder.closeSession() + binder.stop() + } + + override fun onDestroy() { + Timber.d("Destroying MediaService.") + binder.closeSession() + // Ensure one more time that all notifications are gone and, + // hopefully, pending intents cancelled. + NotificationManagerCompat.from(this).cancelAll() + super.onDestroy() } companion object { @@ -133,7 +167,12 @@ class MediaService : LifecycleMedia2SessionService() { application.startService(intent) } - suspend fun bind(application: Application): MediaService.Binder { + fun stop(application: Application) { + val intent = intent(application) + application.stopService(intent) + } + + suspend fun bind(application: Application): Binder { val mediaServiceBinder: CompletableDeferred<Binder> = CompletableDeferred() @@ -141,21 +180,23 @@ class MediaService : LifecycleMedia2SessionService() { override fun onServiceConnected(name: ComponentName?, service: IBinder) { Timber.d("MediaService bound.") - mediaServiceBinder.complete(service as MediaService.Binder) + mediaServiceBinder.complete(service as Binder) } override fun onServiceDisconnected(name: ComponentName) { - Timber.e("MediaService disconnected.") - - // Should not happen, do nothing. + Timber.d("MediaService disconnected.") } override fun onNullBinding(name: ComponentName) { + if (mediaServiceBinder.isCompleted) { + // This happens when the service has successfully connected and later + // stopped and disconnected. + return + } val errorMessage = "Failed to bind to MediaService." Timber.e(errorMessage) val exception = IllegalStateException(errorMessage) mediaServiceBinder.completeExceptionally(exception) - // Should not happen, do nothing. } } @@ -165,11 +206,6 @@ class MediaService : LifecycleMedia2SessionService() { return mediaServiceBinder.await() } - fun stop(application: Application) { - val intent = intent(application) - application.stopService(intent) - } - private fun intent(application: Application) = Intent(SERVICE_INTERFACE) // MediaSessionService.onBind requires the intent to have a non-null action diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/MediaServiceFacade.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/MediaServiceFacade.kt new file mode 100644 index 0000000000..be4ce39a2a --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/MediaServiceFacade.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.reader + +import android.app.Application +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.readium.navigator.media.common.Media3Adapter +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.testapp.utils.CoroutineQueue + +/** + * Enables to try to close a session without starting the [MediaService] if it is not started. + */ +@OptIn(ExperimentalReadiumApi::class) +class MediaServiceFacade( + private val application: Application +) { + private val coroutineScope: CoroutineScope = + MainScope() + + private val coroutineQueue: CoroutineQueue = + CoroutineQueue() + + private var binder: MediaService.Binder? = + null + + private var bindingJob: Job? = + null + + private val sessionMutable: MutableStateFlow<MediaService.Session?> = + MutableStateFlow(null) + + val session: StateFlow<MediaService.Session?> = + sessionMutable.asStateFlow() + + /** + * Throws an IllegalStateException if binding to the MediaService fails. + */ + suspend fun <N> openSession( + bookId: Long, + navigator: N + ) where N : AnyMediaNavigator, N : Media3Adapter { + coroutineQueue.await { + MediaService.start(application) + binder = try { + MediaService.bind(application) + } catch (e: Exception) { + // Failed to bind to the service. + MediaService.stop(application) + throw e + } + + bindingJob = binder!!.session + .onEach { sessionMutable.value = it } + .launchIn(coroutineScope) + binder!!.openSession(navigator, bookId) + } + } + + fun closeSession() { + coroutineQueue.launch { + bindingJob?.cancelAndJoin() + binder?.closeSession() + binder?.stop() + sessionMutable.value = null + binder = null + } + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt new file mode 100644 index 0000000000..7620848ab2 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.reader + +import org.readium.r2.shared.util.Error +import org.readium.r2.testapp.R +import org.readium.r2.testapp.domain.toUserError +import org.readium.r2.testapp.utils.UserError + +sealed class OpeningError( + override val cause: Error? +) : Error { + + override val message: String = + "Could not open publication" + + class PublicationError( + override val cause: org.readium.r2.testapp.domain.PublicationError + ) : OpeningError(cause) + + class RestrictedPublication( + cause: Error + ) : OpeningError(cause) + + class CannotRender(cause: Error) : + OpeningError(cause) + + class AudioEngineInitialization( + cause: Error + ) : OpeningError(cause) + + fun toUserError(): UserError = + when (this) { + is AudioEngineInitialization -> + UserError(R.string.opening_publication_audio_engine_initialization, cause = this) + is PublicationError -> + cause.toUserError() + is RestrictedPublication -> + UserError(R.string.publication_error_restricted, cause = this) + is CannotRender -> + UserError(R.string.opening_publication_cannot_render, cause = this) + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/PdfReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/PdfReaderFragment.kt index 7605704b69..db7356fa63 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/PdfReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/PdfReaderFragment.kt @@ -10,22 +10,20 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast import androidx.fragment.app.commitNow -import org.readium.adapters.pdfium.navigator.PdfiumEngineProvider -import org.readium.adapters.pdfium.navigator.PdfiumPreferences -import org.readium.adapters.pdfium.navigator.PdfiumSettings +import org.readium.adapter.pdfium.navigator.PdfiumEngineProvider +import org.readium.adapter.pdfium.navigator.PdfiumNavigatorFragment +import org.readium.adapter.pdfium.navigator.PdfiumPreferences +import org.readium.adapter.pdfium.navigator.PdfiumSettings import org.readium.r2.navigator.pdf.PdfNavigatorFragment import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.fetcher.Resource -import org.readium.r2.shared.publication.Link import org.readium.r2.testapp.R import org.readium.r2.testapp.reader.preferences.UserPreferencesViewModel @OptIn(ExperimentalReadiumApi::class) -class PdfReaderFragment : VisualReaderFragment(), PdfNavigatorFragment.Listener { +class PdfReaderFragment : VisualReaderFragment() { - override lateinit var navigator: PdfNavigatorFragment<PdfiumSettings, PdfiumPreferences> + override lateinit var navigator: PdfiumNavigatorFragment override fun onCreate(savedInstanceState: Bundle?) { val readerData = model.readerInitData as? PdfReaderInitData ?: run { @@ -44,7 +42,7 @@ class PdfReaderFragment : VisualReaderFragment(), PdfNavigatorFragment.Listener readerData.navigatorFactory.createFragmentFactory( initialLocator = readerData.initialLocation, initialPreferences = readerData.preferencesManager.preferences.value, - listener = this, + listener = model ) super.onCreate(savedInstanceState) @@ -58,12 +56,18 @@ class PdfReaderFragment : VisualReaderFragment(), PdfNavigatorFragment.Listener val view = super.onCreateView(inflater, container, savedInstanceState) if (savedInstanceState == null) { childFragmentManager.commitNow { - replace(R.id.fragment_reader_container, PdfNavigatorFragment::class.java, Bundle(), NAVIGATOR_FRAGMENT_TAG) + replace( + R.id.fragment_reader_container, + PdfNavigatorFragment::class.java, + Bundle(), + NAVIGATOR_FRAGMENT_TAG + ) } } + @Suppress("Unchecked_cast") navigator = childFragmentManager.findFragmentByTag(NAVIGATOR_FRAGMENT_TAG)!! - as PdfNavigatorFragment<PdfiumSettings, PdfiumPreferences> + as PdfiumNavigatorFragment return view } @@ -75,17 +79,6 @@ class PdfReaderFragment : VisualReaderFragment(), PdfNavigatorFragment.Listener .bind(navigator, viewLifecycleOwner) } - override fun onResourceLoadFailed(link: Link, error: Resource.Exception) { - val message = when (error) { - is Resource.Exception.OutOfMemory -> "The PDF is too large to be rendered on this device" - else -> "Failed to render this PDF" - } - Toast.makeText(requireActivity(), message, Toast.LENGTH_LONG).show() - - // There's nothing we can do to recover, so we quit the Activity. - requireActivity().finish() - } - companion object { const val NAVIGATOR_FRAGMENT_TAG = "navigator" diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivity.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivity.kt index e3b1465a38..4b69de6a79 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivity.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivity.kt @@ -10,7 +10,6 @@ import android.os.Build import android.os.Bundle import android.view.MenuItem import android.view.WindowManager -import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment @@ -18,9 +17,8 @@ import androidx.fragment.app.FragmentResultListener import androidx.fragment.app.commit import androidx.fragment.app.commitNow import androidx.lifecycle.ViewModelProvider -import org.readium.navigator.media2.ExperimentalMedia2 -import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.util.toUri import org.readium.r2.testapp.Application import org.readium.r2.testapp.R import org.readium.r2.testapp.databinding.ActivityReaderBinding @@ -28,6 +26,7 @@ import org.readium.r2.testapp.drm.DrmManagementContract import org.readium.r2.testapp.drm.DrmManagementFragment import org.readium.r2.testapp.outline.OutlineContract import org.readium.r2.testapp.outline.OutlineFragment +import org.readium.r2.testapp.utils.launchWebBrowser /* * An activity to read a publication @@ -38,10 +37,11 @@ open class ReaderActivity : AppCompatActivity() { private val model: ReaderViewModel by viewModels() - override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory { - val arguments = ReaderActivityContract.parseIntent(this) - return ReaderViewModel.createFactory(application as Application, arguments) - } + override val defaultViewModelProviderFactory: ViewModelProvider.Factory + get() = ReaderViewModel.createFactory( + application as Application, + ReaderActivityContract.parseIntent(this) + ) private lateinit var binding: ActivityReaderBinding private lateinit var readerFragment: BaseReaderFragment @@ -82,8 +82,9 @@ open class ReaderActivity : AppCompatActivity() { DrmManagementContract.REQUEST_KEY, this, FragmentResultListener { _, result -> - if (DrmManagementContract.parseResult(result).hasReturned) + if (DrmManagementContract.parseResult(result).hasReturned) { finish() + } } ) @@ -97,7 +98,6 @@ open class ReaderActivity : AppCompatActivity() { } } - @OptIn(ExperimentalMedia2::class) private fun createReaderFragment(readerData: ReaderInitData): BaseReaderFragment? { val readerClass: Class<out Fragment>? = when (readerData) { is EpubReaderInitData -> EpubReaderFragment::class.java @@ -143,22 +143,27 @@ open class ReaderActivity : AppCompatActivity() { super.finish() } - private fun handleReaderFragmentEvent(event: ReaderViewModel.Event) { - when (event) { - is ReaderViewModel.Event.OpenOutlineRequested -> showOutlineFragment() - is ReaderViewModel.Event.OpenDrmManagementRequested -> showDrmManagementFragment() - is ReaderViewModel.Event.Failure -> showError(event.error) - else -> {} + private fun handleReaderFragmentEvent(command: ReaderViewModel.ActivityCommand) { + when (command) { + is ReaderViewModel.ActivityCommand.OpenOutlineRequested -> + showOutlineFragment() + is ReaderViewModel.ActivityCommand.OpenDrmManagementRequested -> + showDrmManagementFragment() + is ReaderViewModel.ActivityCommand.OpenExternalLink -> + launchWebBrowser(this, command.url.toUri()) + is ReaderViewModel.ActivityCommand.ToastError -> + command.error.show(this) } } - private fun showError(error: UserException) { - Toast.makeText(this, error.getUserMessage(this), Toast.LENGTH_LONG).show() - } - private fun showOutlineFragment() { supportFragmentManager.commit { - add(R.id.activity_container, OutlineFragment::class.java, Bundle(), OUTLINE_FRAGMENT_TAG) + add( + R.id.activity_container, + OutlineFragment::class.java, + Bundle(), + OUTLINE_FRAGMENT_TAG + ) hide(readerFragment) addToBackStack(null) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivityContract.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivityContract.kt index 8a0a4d16f9..c0862ef8c7 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivityContract.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivityContract.kt @@ -30,8 +30,9 @@ class ReaderActivityContract : } override fun parseResult(resultCode: Int, intent: Intent?): Arguments? { - if (intent == null) + if (intent == null) { return null + } val extras = requireNotNull(intent.extras) return parseExtras(extras) diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderInitData.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderInitData.kt index cf042237f6..8897f5633b 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderInitData.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderInitData.kt @@ -8,20 +8,18 @@ package org.readium.r2.testapp.reader -import org.readium.adapters.pdfium.navigator.PdfiumPreferences -import org.readium.adapters.pdfium.navigator.PdfiumPreferencesEditor -import org.readium.adapters.pdfium.navigator.PdfiumSettings -import org.readium.navigator.media2.ExperimentalMedia2 -import org.readium.navigator.media2.MediaNavigator +import org.readium.adapter.exoplayer.audio.ExoPlayerNavigator +import org.readium.adapter.exoplayer.audio.ExoPlayerNavigatorFactory +import org.readium.adapter.exoplayer.audio.ExoPlayerPreferences +import org.readium.adapter.pdfium.navigator.PdfiumNavigatorFactory +import org.readium.adapter.pdfium.navigator.PdfiumPreferences +import org.readium.navigator.media.tts.AndroidTtsNavigatorFactory +import org.readium.navigator.media.tts.android.AndroidTtsPreferences import org.readium.r2.navigator.epub.EpubNavigatorFactory import org.readium.r2.navigator.epub.EpubPreferences -import org.readium.r2.navigator.media3.tts.AndroidTtsNavigatorFactory -import org.readium.r2.navigator.media3.tts.android.AndroidTtsPreferences -import org.readium.r2.navigator.pdf.PdfNavigatorFactory import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.* import org.readium.r2.testapp.reader.preferences.PreferencesManager -import org.readium.r2.testapp.reader.tts.TtsServiceFacade sealed class ReaderInitData { abstract val bookId: Long @@ -31,15 +29,15 @@ sealed class ReaderInitData { sealed class VisualReaderInitData( override val bookId: Long, override val publication: Publication, - var initialLocation: Locator?, - val ttsInitData: TtsInitData?, + val initialLocation: Locator?, + val ttsInitData: TtsInitData? ) : ReaderInitData() class ImageReaderInitData( bookId: Long, publication: Publication, initialLocation: Locator?, - ttsInitData: TtsInitData?, + ttsInitData: TtsInitData? ) : VisualReaderInitData(bookId, publication, initialLocation, ttsInitData) class EpubReaderInitData( @@ -48,7 +46,7 @@ class EpubReaderInitData( initialLocation: Locator?, val preferencesManager: PreferencesManager<EpubPreferences>, val navigatorFactory: EpubNavigatorFactory, - ttsInitData: TtsInitData?, + ttsInitData: TtsInitData? ) : VisualReaderInitData(bookId, publication, initialLocation, ttsInitData) class PdfReaderInitData( @@ -56,32 +54,30 @@ class PdfReaderInitData( publication: Publication, initialLocation: Locator?, val preferencesManager: PreferencesManager<PdfiumPreferences>, - val navigatorFactory: PdfNavigatorFactory<PdfiumSettings, PdfiumPreferences, PdfiumPreferencesEditor>, - ttsInitData: TtsInitData?, + val navigatorFactory: PdfiumNavigatorFactory, + ttsInitData: TtsInitData? ) : VisualReaderInitData(bookId, publication, initialLocation, ttsInitData) -@OptIn(ExperimentalMedia2::class) +class TtsInitData( + val mediaServiceFacade: MediaServiceFacade, + val navigatorFactory: AndroidTtsNavigatorFactory, + val preferencesManager: PreferencesManager<AndroidTtsPreferences> +) + class MediaReaderInitData( override val bookId: Long, override val publication: Publication, - val mediaNavigator: MediaNavigator, - val sessionBinder: MediaService.Binder - // val preferencesManager: PreferencesManager<ExoPlayerPreferences>, - // val navigatorFactory: PlayerNavigatorFactory<ExoPlayerSettings, ExoPlayerPreferences, ExoPlayerPreferencesEditor> + val mediaNavigator: ExoPlayerNavigator, + val preferencesManager: PreferencesManager<ExoPlayerPreferences>, + val navigatorFactory: ExoPlayerNavigatorFactory ) : ReaderInitData() class DummyReaderInitData( - override val bookId: Long, + override val bookId: Long ) : ReaderInitData() { override val publication: Publication = Publication( Manifest( - metadata = Metadata(identifier = "dummy", localizedTitle = LocalizedString("")) + metadata = Metadata(identifier = "dummy") ) ) } - -class TtsInitData( - val ttsServiceFacade: TtsServiceFacade, - val ttsNavigatorFactory: AndroidTtsNavigatorFactory, - val preferencesManager: PreferencesManager<AndroidTtsPreferences>, -) diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt index db187d7d25..c10784fa2b 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt @@ -6,32 +6,33 @@ package org.readium.r2.testapp.reader -import android.app.Activity import android.app.Application import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences as JetpackPreferences -import java.io.File import org.json.JSONObject -import org.readium.adapters.pdfium.navigator.PdfiumEngineProvider -import org.readium.navigator.media2.ExperimentalMedia2 -import org.readium.navigator.media2.MediaNavigator +import org.readium.adapter.exoplayer.audio.ExoPlayerEngineProvider +import org.readium.adapter.pdfium.navigator.PdfiumEngineProvider +import org.readium.navigator.media.audio.AudioNavigatorFactory +import org.readium.navigator.media.tts.TtsNavigatorFactory import org.readium.r2.navigator.epub.EpubNavigatorFactory -import org.readium.r2.navigator.media3.tts.TtsNavigatorFactory import org.readium.r2.navigator.pdf.PdfNavigatorFactory import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.asset.FileAsset +import org.readium.r2.shared.publication.allAreHtml import org.readium.r2.shared.publication.services.isRestricted import org.readium.r2.shared.publication.services.protectionError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrElse import org.readium.r2.testapp.Readium -import org.readium.r2.testapp.bookshelf.BookRepository +import org.readium.r2.testapp.data.BookRepository +import org.readium.r2.testapp.domain.PublicationError import org.readium.r2.testapp.reader.preferences.AndroidTtsPreferencesManagerFactory import org.readium.r2.testapp.reader.preferences.EpubPreferencesManagerFactory +import org.readium.r2.testapp.reader.preferences.ExoPlayerPreferencesManagerFactory import org.readium.r2.testapp.reader.preferences.PdfiumPreferencesManagerFactory -import org.readium.r2.testapp.reader.tts.TtsServiceFacade +import org.readium.r2.testapp.utils.CoroutineQueue import timber.log.Timber /** @@ -46,193 +47,214 @@ class ReaderRepository( private val application: Application, private val readium: Readium, private val bookRepository: BookRepository, - private val preferencesDataStore: DataStore<JetpackPreferences>, + private val preferencesDataStore: DataStore<JetpackPreferences> ) { - object CancellationException : Exception() + + private val coroutineQueue: CoroutineQueue = + CoroutineQueue() private val repository: MutableMap<Long, ReaderInitData> = mutableMapOf() - private val ttsServiceFacade: TtsServiceFacade = - TtsServiceFacade(application) + private val mediaServiceFacade: MediaServiceFacade = + MediaServiceFacade(application) operator fun get(bookId: Long): ReaderInitData? = repository[bookId] - suspend fun open(bookId: Long, activity: Activity): Try<Unit, Exception> { - return try { - openThrowing(bookId, activity) - Try.success(Unit) - } catch (e: Exception) { - Try.failure(e) - } - } + suspend fun open(bookId: Long): Try<Unit, OpeningError> = + coroutineQueue.await { doOpen(bookId) } - private suspend fun openThrowing(bookId: Long, activity: Activity) { + private suspend fun doOpen(bookId: Long): Try<Unit, OpeningError> { if (bookId in repository.keys) { - return + return Try.success(Unit) } - val book = bookRepository.get(bookId) - ?: throw Exception("Cannot find book in database.") - - val file = File(book.href) - require(file.exists()) - val asset = FileAsset(file) + val book = checkNotNull(bookRepository.get(bookId)) { "Cannot find book in database." } + + val asset = readium.assetRetriever.retrieve( + book.url, + book.mediaType + ).getOrElse { + return Try.failure( + OpeningError.PublicationError( + PublicationError(it) + ) + ) + } - val publication = readium.streamer.open(asset, allowUserInteraction = true, sender = activity) - .getOrThrow() + val publication = readium.publicationOpener.open( + asset, + allowUserInteraction = true + ).getOrElse { + return Try.failure( + OpeningError.PublicationError( + PublicationError(it) + ) + ) + } // The publication is protected with a DRM and not unlocked. if (publication.isRestricted) { - throw publication.protectionError - ?: CancellationException + return Try.failure( + OpeningError.RestrictedPublication( + publication.protectionError + ?: DebugError("Publication is restricted.") + ) + ) } - val initialLocator = book.progression?.let { Locator.fromJSON(JSONObject(it)) } + val initialLocator = book.progression + ?.let { Locator.fromJSON(JSONObject(it)) } val readerInitData = when { publication.conformsTo(Publication.Profile.AUDIOBOOK) -> openAudio(bookId, publication, initialLocator) - publication.conformsTo(Publication.Profile.EPUB) -> + publication.conformsTo(Publication.Profile.EPUB) || publication.readingOrder.allAreHtml -> openEpub(bookId, publication, initialLocator) publication.conformsTo(Publication.Profile.PDF) -> openPdf(bookId, publication, initialLocator) publication.conformsTo(Publication.Profile.DIVINA) -> openImage(bookId, publication, initialLocator) else -> - throw Exception("Publication is not supported.") + Try.failure( + OpeningError.CannotRender( + DebugError("No navigator supports this publication.") + ) + ) } - repository[bookId] = readerInitData + return readerInitData.map { repository[bookId] = it } } - @OptIn(ExperimentalMedia2::class) private suspend fun openAudio( bookId: Long, publication: Publication, initialLocator: Locator? - ): MediaReaderInitData { - - val navigator = MediaNavigator.create( - application, - publication, - initialLocator - ).getOrElse { throw Exception("Cannot open audiobook.") } - - MediaService.start(application) - val mediaBinder = MediaService.bind(application) - mediaBinder.bindNavigator(navigator, bookId) - return MediaReaderInitData(bookId, publication, navigator, mediaBinder) - } - - /* private suspend fun openAudio( - bookId: Long, - publication: Publication, - initialLocator: Locator? - ): MediaReaderInitData { - + ): Try<MediaReaderInitData, OpeningError> { val preferencesManager = ExoPlayerPreferencesManagerFactory(preferencesDataStore) .createPreferenceManager(bookId) - val mediaEngine = ExoPlayerEngineProvider(application) val initialPreferences = preferencesManager.preferences.value - val actualInitialLocator = initialLocator - ?: publication.locatorFromLink(publication.readingOrder[0])!! - val navigatorFactory = PlayerNavigatorFactory( + val navigatorFactory = AudioNavigatorFactory( publication, - mediaEngine, - DefaultMetadataProvider(), - initialPreferences, - actualInitialLocator, + ExoPlayerEngineProvider(application) + ) ?: return Try.failure( + OpeningError.CannotRender( + DebugError("Cannot create audio navigator factory.") + ) ) - val navigator = navigatorFactory.getMediaNavigator() - .getOrElse { throw Exception("Cannot open audiobook.") } + val navigator = navigatorFactory.createNavigator( + initialLocator, + initialPreferences + ).getOrElse { + return Try.failure( + when (it) { + is AudioNavigatorFactory.Error.EngineInitialization -> + OpeningError.AudioEngineInitialization(it) + is AudioNavigatorFactory.Error.UnsupportedPublication -> + OpeningError.CannotRender(it) + } + ) + } - val navigator = MediaNavigator.create( - application, + mediaServiceFacade.openSession(bookId, navigator) + val initData = MediaReaderInitData( + bookId, publication, - initialLocator - ).getOrElse { throw Exception("Cannot open audiobook.") } - - mediaBinder.bindNavigator(navigator, bookId) - return MediaReaderInitData(bookId, publication,, preferencesManager, navigatorFactory) - } */ + navigator, + preferencesManager, + navigatorFactory + ) + return Try.success(initData) + } private suspend fun openEpub( bookId: Long, publication: Publication, initialLocator: Locator? - ): EpubReaderInitData { - + ): Try<EpubReaderInitData, OpeningError> { val preferencesManager = EpubPreferencesManagerFactory(preferencesDataStore) .createPreferenceManager(bookId) val navigatorFactory = EpubNavigatorFactory(publication) val ttsInitData = getTtsInitData(bookId, publication) - return EpubReaderInitData( - bookId, publication, initialLocator, - preferencesManager, navigatorFactory, ttsInitData + val initData = EpubReaderInitData( + bookId, + publication, + initialLocator, + preferencesManager, + navigatorFactory, + ttsInitData ) + return Try.success(initData) } private suspend fun openPdf( bookId: Long, publication: Publication, initialLocator: Locator? - ): PdfReaderInitData { - + ): Try<PdfReaderInitData, OpeningError> { val preferencesManager = PdfiumPreferencesManagerFactory(preferencesDataStore) .createPreferenceManager(bookId) val pdfEngine = PdfiumEngineProvider() val navigatorFactory = PdfNavigatorFactory(publication, pdfEngine) val ttsInitData = getTtsInitData(bookId, publication) - return PdfReaderInitData( - bookId, publication, initialLocator, - preferencesManager, navigatorFactory, + val initData = PdfReaderInitData( + bookId, + publication, + initialLocator, + preferencesManager, + navigatorFactory, ttsInitData ) + return Try.success(initData) } private suspend fun openImage( bookId: Long, publication: Publication, initialLocator: Locator? - ): ImageReaderInitData { - return ImageReaderInitData( + ): Try<ImageReaderInitData, OpeningError> { + val initData = ImageReaderInitData( bookId = bookId, publication = publication, initialLocation = initialLocator, ttsInitData = getTtsInitData(bookId, publication) ) + return Try.success(initData) } private suspend fun getTtsInitData( bookId: Long, - publication: Publication, + publication: Publication ): TtsInitData? { val preferencesManager = AndroidTtsPreferencesManagerFactory(preferencesDataStore) .createPreferenceManager(bookId) - val navigatorFactory = TtsNavigatorFactory(application, publication) ?: return null - return TtsInitData(ttsServiceFacade, navigatorFactory, preferencesManager) + val navigatorFactory = TtsNavigatorFactory( + application, + publication + ) ?: return null + return TtsInitData(mediaServiceFacade, navigatorFactory, preferencesManager) } - suspend fun close(bookId: Long) { - Timber.d("Closing Publication") - when (val initData = repository.remove(bookId)) { - is MediaReaderInitData -> { - initData.sessionBinder.closeNavigator() - MediaService.stop(application) - initData.publication.close() - } - is VisualReaderInitData -> { - initData.ttsInitData?.ttsServiceFacade?.closeSession() - initData.publication.close() - } - null, is DummyReaderInitData -> { - // Do nothing + fun close(bookId: Long) { + coroutineQueue.launch { + Timber.v("Closing Publication $bookId.") + when (val initData = repository.remove(bookId)) { + is MediaReaderInitData -> { + mediaServiceFacade.closeSession() + initData.publication.close() + } + is VisualReaderInitData -> { + mediaServiceFacade.closeSession() + initData.publication.close() + } + null, is DummyReaderInitData -> { + // Do nothing + } } } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt index a20d3f1152..b4c9d659e3 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt @@ -19,32 +19,46 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import org.readium.r2.navigator.Decoration import org.readium.r2.navigator.ExperimentalDecorator +import org.readium.r2.navigator.epub.EpubNavigatorFragment +import org.readium.r2.navigator.image.ImageNavigatorFragment +import org.readium.r2.navigator.pdf.PdfNavigatorFragment import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.Search -import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.LocatorCollection import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.search.SearchIterator import org.readium.r2.shared.publication.services.search.SearchTry import org.readium.r2.shared.publication.services.search.search +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.testapp.Application -import org.readium.r2.testapp.bookshelf.BookRepository -import org.readium.r2.testapp.domain.model.Highlight +import org.readium.r2.testapp.R +import org.readium.r2.testapp.data.BookRepository +import org.readium.r2.testapp.data.model.Highlight +import org.readium.r2.testapp.domain.toUserError import org.readium.r2.testapp.reader.preferences.UserPreferencesViewModel import org.readium.r2.testapp.reader.tts.TtsViewModel import org.readium.r2.testapp.search.SearchPagingSource import org.readium.r2.testapp.utils.EventChannel +import org.readium.r2.testapp.utils.UserError import org.readium.r2.testapp.utils.createViewModelFactory import timber.log.Timber -@OptIn(Search::class, ExperimentalDecorator::class, ExperimentalCoroutinesApi::class) +@OptIn( + ExperimentalDecorator::class, + ExperimentalCoroutinesApi::class, + ExperimentalReadiumApi::class +) class ReaderViewModel( private val bookId: Long, private val readerRepository: ReaderRepository, - private val bookRepository: BookRepository, -) : ViewModel() { + private val bookRepository: BookRepository +) : ViewModel(), + EpubNavigatorFragment.Listener, + ImageNavigatorFragment.Listener, + PdfNavigatorFragment.Listener { val readerInitData = try { @@ -57,12 +71,15 @@ class ReaderViewModel( val publication: Publication = readerInitData.publication - val activityChannel: EventChannel<Event> = + val activityChannel: EventChannel<ActivityCommand> = EventChannel(Channel(Channel.BUFFERED), viewModelScope) val fragmentChannel: EventChannel<FeedbackEvent> = EventChannel(Channel(Channel.BUFFERED), viewModelScope) + val searchChannel: EventChannel<SearchCommand> = + EventChannel(Channel(Channel.BUFFERED), viewModelScope) + val tts: TtsViewModel? = TtsViewModel( viewModelScope = viewModelScope, readerInitData = readerInitData @@ -74,10 +91,7 @@ class ReaderViewModel( ) fun close() { - viewModelScope.launch { - tts?.stop() - readerRepository.close(bookId) - } + readerRepository.close(bookId) } fun saveProgression(locator: Locator) = viewModelScope.launch { @@ -146,15 +160,21 @@ class ReaderViewModel( createDecoration( idSuffix = "highlight", style = when (style) { - Highlight.Style.HIGHLIGHT -> Decoration.Style.Highlight(tint = tint, isActive = isActive) - Highlight.Style.UNDERLINE -> Decoration.Style.Underline(tint = tint, isActive = isActive) + Highlight.Style.HIGHLIGHT -> Decoration.Style.Highlight( + tint = tint, + isActive = isActive + ) + Highlight.Style.UNDERLINE -> Decoration.Style.Underline( + tint = tint, + isActive = isActive + ) } ), // Additional page margin icon decoration, if the highlight has an associated note. annotation.takeIf { it.isNotEmpty() }?.let { createDecoration( idSuffix = "annotation", - style = DecorationStyleAnnotationMark(tint = tint), + style = DecorationStyleAnnotationMark(tint = tint) ) } ) @@ -191,10 +211,16 @@ class ReaderViewModel( lastSearchQuery = query _searchLocators.value = emptyList() searchIterator = publication.search(query) - .onFailure { activityChannel.send(Event.Failure(it)) } - .getOrNull() + ?: run { + activityChannel.send( + ActivityCommand.ToastError( + UserError(R.string.search_error_not_searchable, cause = null) + ) + ) + null + } pagingSourceFactory.invalidate() - activityChannel.send(Event.StartNewSearch) + searchChannel.send(SearchCommand.StartNewSearch) } fun cancelSearch() = viewModelScope.launch { @@ -233,6 +259,21 @@ class ReaderViewModel( SearchPagingSource(listener = PagingSourceListener()) } + // Navigator.Listener + + override fun onResourceLoadFailed(href: Url, error: ReadError) { + activityChannel.send( + ActivityCommand.ToastError(error.toUserError()) + ) + } + + // HyperlinkNavigator.Listener + override fun onExternalLinkActivated(url: AbsoluteUrl) { + activityChannel.send(ActivityCommand.OpenExternalLink(url)) + } + + // Search + inner class PagingSourceListener : SearchPagingSource.Listener { override suspend fun next(): SearchTry<LocatorCollection?> { val iterator = searchIterator ?: return Try.success(null) @@ -248,11 +289,11 @@ class ReaderViewModel( // Events - sealed class Event { - object OpenOutlineRequested : Event() - object OpenDrmManagementRequested : Event() - object StartNewSearch : Event() - class Failure(val error: UserException) : Event() + sealed class ActivityCommand { + object OpenOutlineRequested : ActivityCommand() + object OpenDrmManagementRequested : ActivityCommand() + class OpenExternalLink(val url: AbsoluteUrl) : ActivityCommand() + class ToastError(val error: UserError) : ActivityCommand() } sealed class FeedbackEvent { @@ -260,10 +301,18 @@ class ReaderViewModel( object BookmarkFailed : FeedbackEvent() } + sealed class SearchCommand { + object StartNewSearch : SearchCommand() + } + companion object { fun createFactory(application: Application, arguments: ReaderActivityContract.Arguments) = createViewModelFactory { - ReaderViewModel(arguments.bookId, application.readerRepository, application.bookRepository) + ReaderViewModel( + arguments.bookId, + application.readerRepository, + application.bookRepository + ) } } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt index b88905de3c..7b30482096 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt @@ -9,7 +9,6 @@ package org.readium.r2.testapp.reader import android.app.AlertDialog import android.content.Context import android.graphics.Color -import android.graphics.PointF import android.graphics.RectF import android.os.Bundle import android.view.* @@ -31,6 +30,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -40,16 +41,18 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize +import org.readium.navigator.media.tts.android.AndroidTtsEngine import org.readium.r2.navigator.* -import org.readium.r2.navigator.media3.tts.android.AndroidTtsEngine +import org.readium.r2.navigator.input.InputListener +import org.readium.r2.navigator.input.TapEvent import org.readium.r2.navigator.util.BaseActionModeCallback -import org.readium.r2.navigator.util.EdgeTapNavigation +import org.readium.r2.navigator.util.DirectionalNavigationAdapter import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.util.Language import org.readium.r2.testapp.R +import org.readium.r2.testapp.data.model.Highlight import org.readium.r2.testapp.databinding.FragmentReaderBinding -import org.readium.r2.testapp.domain.model.Highlight import org.readium.r2.testapp.reader.preferences.UserPreferencesBottomSheetDialogFragment import org.readium.r2.testapp.reader.tts.TtsControls import org.readium.r2.testapp.reader.tts.TtsViewModel @@ -63,7 +66,7 @@ import org.readium.r2.testapp.utils.extensions.throttleLatest * Provides common menu items and saves last location on stop. */ @OptIn(ExperimentalDecorator::class, ExperimentalReadiumApi::class) -abstract class VisualReaderFragment : BaseReaderFragment(), VisualNavigator.Listener { +abstract class VisualReaderFragment : BaseReaderFragment() { protected var binding: FragmentReaderBinding by viewLifecycle() @@ -83,18 +86,25 @@ abstract class VisualReaderFragment : BaseReaderFragment(), VisualNavigator.List */ private var disableTouches by mutableStateOf(false) - /** - * When true, the fragment won't save progression. - * This is useful in the case where the TTS is on and a service is saving progression - * in background. - */ - private var preventProgressionSaving: Boolean = false - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) navigatorFragment = navigator as Fragment + (navigator as OverflowableNavigator).apply { + // This will automatically turn pages when tapping the screen edges or arrow keys. + addInputListener(DirectionalNavigationAdapter(this)) + } + + (navigator as VisualNavigator).apply { + addInputListener(object : InputListener { + override fun onTap(event: TapEvent): Boolean { + requireActivity().toggleSystemUi() + return true + } + }) + } + setupObservers() childFragmentManager.addOnBackStackChangedListener { @@ -126,6 +136,27 @@ abstract class VisualReaderFragment : BaseReaderFragment(), VisualNavigator.List content = { Overlay() } ) } + + val menuHost: MenuHost = requireActivity() + + menuHost.addMenuProvider( + object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menu.findItem(R.id.tts).isVisible = (model.tts != null) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + when (menuItem.itemId) { + R.id.tts -> { + checkNotNull(model.tts).start(navigator) + return true + } + } + return false + } + }, + viewLifecycleOwner + ) } @Composable @@ -146,13 +177,9 @@ abstract class VisualReaderFragment : BaseReaderFragment(), VisualNavigator.List private fun setupObservers() { viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { navigator.currentLocator - .onEach { - if (!preventProgressionSaving) { - model.saveProgression(it) - } - } + .onEach { model.saveProgression(it) } .launchIn(this) setupHighlights(this) @@ -184,13 +211,15 @@ abstract class VisualReaderFragment : BaseReaderFragment(), VisualNavigator.List * Setup text-to-speech observers, if available. */ private suspend fun setupTts(scope: CoroutineScope) { + val activity = requireActivity() + model.tts?.apply { events .onEach { event -> when (event) { - is TtsViewModel.Event.OnError -> - showError(event.error) - + is TtsViewModel.Event.OnError -> { + showError(event.error.toUserError()) + } is TtsViewModel.Event.OnMissingVoiceData -> confirmAndInstallTtsVoice(event.language) } @@ -231,12 +260,6 @@ abstract class VisualReaderFragment : BaseReaderFragment(), VisualNavigator.List } .launchIn(scope) } - - showControls - .onEach { showControls -> - preventProgressionSaving = showControls - } - .launchIn(scope) } } @@ -245,11 +268,14 @@ abstract class VisualReaderFragment : BaseReaderFragment(), VisualNavigator.List */ private suspend fun confirmAndInstallTtsVoice(language: Language) { val activity = activity ?: return - val tts = model.tts ?: return + model.tts ?: return if ( activity.confirmDialog( - getString(R.string.tts_error_language_support_incomplete, language.locale.displayLanguage) + getString( + R.string.tts_error_language_support_incomplete, + language.locale.displayLanguage + ) ) ) { AndroidTtsEngine.requestInstallVoice(activity) @@ -272,19 +298,6 @@ abstract class VisualReaderFragment : BaseReaderFragment(), VisualNavigator.List requireActivity().invalidateOptionsMenu() } - override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) { - super.onCreateOptionsMenu(menu, menuInflater) - menu.findItem(R.id.tts).isVisible = (model.tts != null) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.tts -> checkNotNull(model.tts).start(navigator) - else -> return super.onOptionsItemSelected(item) - } - return true - } - // DecorableNavigator.Listener private val decorationListener by lazy { DecorationListener() } @@ -308,8 +321,11 @@ abstract class VisualReaderFragment : BaseReaderFragment(), VisualNavigator.List val isUnderline = (decoration.style is Decoration.Style.Underline) showHighlightPopup( rect, - style = if (isUnderline) Highlight.Style.UNDERLINE - else Highlight.Style.HIGHLIGHT, + style = if (isUnderline) { + Highlight.Style.UNDERLINE + } else { + Highlight.Style.HIGHLIGHT + }, highlightId = id ) } @@ -330,7 +346,7 @@ abstract class VisualReaderFragment : BaseReaderFragment(), VisualNavigator.List R.id.green to Color.rgb(173, 247, 123), R.id.blue to Color.rgb(124, 198, 247), R.id.yellow to Color.rgb(249, 239, 125), - R.id.purple to Color.rgb(182, 153, 255), + R.id.purple to Color.rgb(182, 153, 255) ) val customSelectionActionModeCallback: ActionMode.Callback by lazy { SelectionActionModeCallback() } @@ -359,76 +375,81 @@ abstract class VisualReaderFragment : BaseReaderFragment(), VisualNavigator.List } } - private fun showHighlightPopupWithStyle(style: Highlight.Style) = viewLifecycleOwner.lifecycleScope.launchWhenResumed { - // Get the rect of the current selection to know where to position the highlight - // popup. - (navigator as? SelectableNavigator)?.currentSelection()?.rect?.let { selectionRect -> - showHighlightPopup(selectionRect, style) + private fun showHighlightPopupWithStyle(style: Highlight.Style) = + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + // Get the rect of the current selection to know where to position the highlight + // popup. + (navigator as? SelectableNavigator)?.currentSelection()?.rect?.let { selectionRect -> + showHighlightPopup(selectionRect, style) + } + } } - } private fun showHighlightPopup(rect: RectF, style: Highlight.Style, highlightId: Long? = null) = - viewLifecycleOwner.lifecycleScope.launchWhenResumed { - if (popupWindow?.isShowing == true) return@launchWhenResumed + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + if (popupWindow?.isShowing == true) return@repeatOnLifecycle - model.activeHighlightId.value = highlightId + model.activeHighlightId.value = highlightId - val isReverse = (rect.top > 60) - val popupView = layoutInflater.inflate( - if (isReverse) R.layout.view_action_mode_reverse else R.layout.view_action_mode, - null, - false - ) - popupView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) - ) + val isReverse = (rect.top > 60) + val popupView = layoutInflater.inflate( + if (isReverse) R.layout.view_action_mode_reverse else R.layout.view_action_mode, + null, + false + ) + popupView.measure( + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + ) - popupWindow = PopupWindow( - popupView, - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ).apply { - isFocusable = true - setOnDismissListener { - model.activeHighlightId.value = null + popupWindow = PopupWindow( + popupView, + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + isFocusable = true + setOnDismissListener { + model.activeHighlightId.value = null + } } - } - val x = rect.left - val y = if (isReverse) rect.top else rect.bottom + rect.height() + val x = rect.left + val y = if (isReverse) rect.top else rect.bottom + rect.height() - popupWindow?.showAtLocation(popupView, Gravity.NO_GRAVITY, x.toInt(), y.toInt()) + popupWindow?.showAtLocation(popupView, Gravity.NO_GRAVITY, x.toInt(), y.toInt()) - val highlight = highlightId?.let { model.highlightById(it) } - popupView.run { - findViewById<View>(R.id.notch).run { - setX(rect.left * 2) - } + val highlight = highlightId?.let { model.highlightById(it) } + popupView.run { + findViewById<View>(R.id.notch).run { + setX(rect.left * 2) + } - fun selectTint(view: View) { - val tint = highlightTints[view.id] ?: return - selectHighlightTint(highlightId, style, tint) - } + fun selectTint(view: View) { + val tint = highlightTints[view.id] ?: return + selectHighlightTint(highlightId, style, tint) + } - findViewById<View>(R.id.red).setOnClickListener(::selectTint) - findViewById<View>(R.id.green).setOnClickListener(::selectTint) - findViewById<View>(R.id.blue).setOnClickListener(::selectTint) - findViewById<View>(R.id.yellow).setOnClickListener(::selectTint) - findViewById<View>(R.id.purple).setOnClickListener(::selectTint) + findViewById<View>(R.id.red).setOnClickListener(::selectTint) + findViewById<View>(R.id.green).setOnClickListener(::selectTint) + findViewById<View>(R.id.blue).setOnClickListener(::selectTint) + findViewById<View>(R.id.yellow).setOnClickListener(::selectTint) + findViewById<View>(R.id.purple).setOnClickListener(::selectTint) - findViewById<View>(R.id.annotation).setOnClickListener { - popupWindow?.dismiss() - showAnnotationPopup(highlightId) - } - findViewById<View>(R.id.del).run { - visibility = if (highlight != null) View.VISIBLE else View.GONE - setOnClickListener { - highlightId?.let { - model.deleteHighlight(highlightId) - } + findViewById<View>(R.id.annotation).setOnClickListener { popupWindow?.dismiss() - mode?.finish() + showAnnotationPopup(highlightId) + } + findViewById<View>(R.id.del).run { + visibility = if (highlight != null) View.VISIBLE else View.GONE + setOnClickListener { + highlightId?.let { + model.deleteHighlight(highlightId) + } + popupWindow?.dismiss() + mode?.finish() + } } } } @@ -439,76 +460,97 @@ abstract class VisualReaderFragment : BaseReaderFragment(), VisualNavigator.List style: Highlight.Style, @ColorInt tint: Int ) = - viewLifecycleOwner.lifecycleScope.launchWhenResumed { - if (highlightId != null) { - model.updateHighlightStyle(highlightId, style, tint) - } else { - (navigator as? SelectableNavigator)?.let { navigator -> - navigator.currentSelection()?.let { selection -> - model.addHighlight(locator = selection.locator, style = style, tint = tint) + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + if (highlightId != null) { + model.updateHighlightStyle(highlightId, style, tint) + } else { + (navigator as? SelectableNavigator)?.let { navigator -> + navigator.currentSelection()?.let { selection -> + model.addHighlight( + locator = selection.locator, + style = style, + tint = tint + ) + } + navigator.clearSelection() } - navigator.clearSelection() } - } - popupWindow?.dismiss() - mode?.finish() - } - - private fun showAnnotationPopup(highlightId: Long? = null) = viewLifecycleOwner.lifecycleScope.launchWhenResumed { - val activity = activity ?: return@launchWhenResumed - val view = layoutInflater.inflate(R.layout.popup_note, null, false) - val note = view.findViewById<EditText>(R.id.note) - val alert = AlertDialog.Builder(activity) - .setView(view) - .create() - - fun dismiss() { - alert.dismiss() - mode?.finish() - (activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) - .hideSoftInputFromWindow(note.applicationWindowToken, InputMethodManager.HIDE_NOT_ALWAYS) + popupWindow?.dismiss() + mode?.finish() + } } - with(view) { - val highlight = highlightId?.let { model.highlightById(it) } - if (highlight != null) { - note.setText(highlight.annotation) - findViewById<View>(R.id.sidemark).setBackgroundColor(highlight.tint) - findViewById<TextView>(R.id.select_text).text = highlight.locator.text.highlight - - findViewById<TextView>(R.id.positive).setOnClickListener { - val text = note.text.toString() - model.updateHighlightAnnotation(highlight.id, annotation = text) - dismiss() + private fun showAnnotationPopup(highlightId: Long? = null) = + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + val activity = activity ?: return@repeatOnLifecycle + val view = layoutInflater.inflate(R.layout.popup_note, null, false) + val note = view.findViewById<EditText>(R.id.note) + val alert = AlertDialog.Builder(activity) + .setView(view) + .create() + + fun dismiss() { + alert.dismiss() + mode?.finish() + (activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) + .hideSoftInputFromWindow( + note.applicationWindowToken, + InputMethodManager.HIDE_NOT_ALWAYS + ) } - } else { - val tint = highlightTints.values.random() - findViewById<View>(R.id.sidemark).setBackgroundColor(tint) - val navigator = navigator as? SelectableNavigator ?: return@launchWhenResumed - val selection = navigator.currentSelection() ?: return@launchWhenResumed - navigator.clearSelection() - findViewById<TextView>(R.id.select_text).text = selection.locator.text.highlight - - findViewById<TextView>(R.id.positive).setOnClickListener { - model.addHighlight(locator = selection.locator, style = Highlight.Style.HIGHLIGHT, tint = tint, annotation = note.text.toString()) - dismiss() + + with(view) { + val highlight = highlightId?.let { model.highlightById(it) } + if (highlight != null) { + note.setText(highlight.annotation) + findViewById<View>(R.id.sidemark).setBackgroundColor(highlight.tint) + findViewById<TextView>(R.id.select_text).text = + highlight.locator.text.highlight + + findViewById<TextView>(R.id.positive).setOnClickListener { + val text = note.text.toString() + model.updateHighlightAnnotation(highlight.id, annotation = text) + dismiss() + } + } else { + val tint = highlightTints.values.random() + findViewById<View>(R.id.sidemark).setBackgroundColor(tint) + val navigator = + navigator as? SelectableNavigator ?: return@repeatOnLifecycle + val selection = navigator.currentSelection() ?: return@repeatOnLifecycle + navigator.clearSelection() + findViewById<TextView>(R.id.select_text).text = + selection.locator.text.highlight + + findViewById<TextView>(R.id.positive).setOnClickListener { + model.addHighlight( + locator = selection.locator, + style = Highlight.Style.HIGHLIGHT, + tint = tint, + annotation = note.text.toString() + ) + dismiss() + } + } + + findViewById<TextView>(R.id.negative).setOnClickListener { + dismiss() + } } - } - findViewById<TextView>(R.id.negative).setOnClickListener { - dismiss() + alert.show() } } - alert.show() - } - fun updateSystemUiVisibility() { - if (navigatorFragment.isHidden) + if (navigatorFragment.isHidden) { requireActivity().showSystemUi() - else + } else { requireActivity().hideSystemUi() + } requireView().requestApplyInsets() } @@ -520,23 +562,6 @@ abstract class VisualReaderFragment : BaseReaderFragment(), VisualNavigator.List container.clearPadding() } } - - // VisualNavigator.Listener - - override fun onTap(point: PointF): Boolean { - val navigated = edgeTapNavigation.onTap(point, requireView()) - - if (!navigated) { - requireActivity().toggleSystemUi() - } - return true - } - - private val edgeTapNavigation by lazy { - EdgeTapNavigation( - navigator = navigator as VisualNavigator - ) - } } /** diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/preferences/PreferencesManagers.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/preferences/PreferencesManagers.kt index 9cd54fb689..623045fbc9 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/preferences/PreferencesManagers.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/preferences/PreferencesManagers.kt @@ -19,34 +19,32 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map -import org.readium.adapters.pdfium.navigator.PdfiumPreferences -import org.readium.adapters.pdfium.navigator.PdfiumPreferencesSerializer -import org.readium.adapters.pdfium.navigator.PdfiumPublicationPreferencesFilter -import org.readium.adapters.pdfium.navigator.PdfiumSharedPreferencesFilter +import org.readium.adapter.exoplayer.audio.ExoPlayerPreferences +import org.readium.adapter.exoplayer.audio.ExoPlayerPreferencesSerializer +import org.readium.adapter.pdfium.navigator.PdfiumPreferences +import org.readium.adapter.pdfium.navigator.PdfiumPreferencesSerializer +import org.readium.adapter.pdfium.navigator.PdfiumPublicationPreferencesFilter +import org.readium.adapter.pdfium.navigator.PdfiumSharedPreferencesFilter +import org.readium.navigator.media.tts.android.AndroidTtsPreferences +import org.readium.navigator.media.tts.android.AndroidTtsPreferencesSerializer +import org.readium.navigator.media.tts.android.AndroidTtsPublicationPreferencesFilter +import org.readium.navigator.media.tts.android.AndroidTtsSharedPreferencesFilter import org.readium.r2.navigator.epub.EpubPreferences import org.readium.r2.navigator.epub.EpubPreferencesSerializer import org.readium.r2.navigator.epub.EpubPublicationPreferencesFilter import org.readium.r2.navigator.epub.EpubSharedPreferencesFilter -import org.readium.r2.navigator.media3.exoplayer.ExoPlayerPreferences -import org.readium.r2.navigator.media3.exoplayer.ExoPlayerPreferencesSerializer -import org.readium.r2.navigator.media3.exoplayer.ExoPlayerPublicationPreferencesFilter -import org.readium.r2.navigator.media3.exoplayer.ExoPlayerSharedPreferencesFilter -import org.readium.r2.navigator.media3.tts.android.AndroidTtsPreferences -import org.readium.r2.navigator.media3.tts.android.AndroidTtsPreferencesSerializer -import org.readium.r2.navigator.media3.tts.android.AndroidTtsPublicationPreferencesFilter -import org.readium.r2.navigator.media3.tts.android.AndroidTtsSharedPreferencesFilter import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.navigator.preferences.PreferencesFilter import org.readium.r2.navigator.preferences.PreferencesSerializer import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.testapp.utils.extensions.stateInFirst +import org.readium.r2.testapp.utils.tryOrNull class PreferencesManager<P : Configurable.Preferences<P>> internal constructor( val preferences: StateFlow<P>, @Suppress("Unused") // Keep the scope alive until the PreferencesManager is garbage collected private val coroutineScope: CoroutineScope, - private val editPreferences: suspend (P) -> Unit, + private val editPreferences: suspend (P) -> Unit ) { suspend fun setPreferences(preferences: P) { @@ -118,7 +116,7 @@ sealed class PreferencesManagerFactory<P : Configurable.Preferences<P>>( } class EpubPreferencesManagerFactory( - dataStore: DataStore<Preferences>, + dataStore: DataStore<Preferences> ) : PreferencesManagerFactory<EpubPreferences>( dataStore = dataStore, klass = EpubPreferences::class, @@ -129,7 +127,7 @@ class EpubPreferencesManagerFactory( ) class PdfiumPreferencesManagerFactory( - dataStore: DataStore<Preferences>, + dataStore: DataStore<Preferences> ) : PreferencesManagerFactory<PdfiumPreferences>( dataStore = dataStore, klass = PdfiumPreferences::class, @@ -144,8 +142,8 @@ class ExoPlayerPreferencesManagerFactory( ) : PreferencesManagerFactory<ExoPlayerPreferences>( dataStore = dataStore, klass = ExoPlayerPreferences::class, - sharedPreferencesFilter = ExoPlayerSharedPreferencesFilter, - publicationPreferencesFilter = ExoPlayerPublicationPreferencesFilter, + sharedPreferencesFilter = { preferences -> preferences }, + publicationPreferencesFilter = { ExoPlayerPreferences() }, preferencesSerializer = ExoPlayerPreferencesSerializer(), emptyPreferences = ExoPlayerPreferences() ) diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/preferences/UserPreferences.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/preferences/UserPreferences.kt index 88af6ccda2..3f784264ba 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/preferences/UserPreferences.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/preferences/UserPreferences.kt @@ -15,13 +15,13 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import org.readium.adapters.pdfium.navigator.PdfiumPreferencesEditor +import org.readium.adapter.exoplayer.audio.ExoPlayerPreferencesEditor +import org.readium.adapter.pdfium.navigator.PdfiumPreferencesEditor +import org.readium.navigator.media.tts.android.AndroidTtsEngine import org.readium.r2.navigator.epub.EpubPreferencesEditor -import org.readium.r2.navigator.media3.tts.android.AndroidTtsEngine import org.readium.r2.navigator.preferences.* import org.readium.r2.navigator.preferences.TextAlign as ReadiumTextAlign import org.readium.r2.shared.ExperimentalReadiumApi @@ -123,7 +123,7 @@ private fun <P : Configurable.Preferences<P>, E : PreferencesEditor<P>> UserPref theme = editor.theme, typeScale = editor.typeScale, verticalText = editor.verticalText, - wordSpacing = editor.wordSpacing, + wordSpacing = editor.wordSpacing ) EpubLayout.FIXED -> FixedLayoutUserPreferences( @@ -131,53 +131,66 @@ private fun <P : Configurable.Preferences<P>, E : PreferencesEditor<P>> UserPref backgroundColor = editor.backgroundColor, language = editor.language, readingProgression = editor.readingProgression, - spread = editor.spread, + spread = editor.spread ) } is TtsPreferencesEditor -> - TtsUserPreferences( + MediaUserPreferences( commit = commit, language = editor.language, voice = editor.voice, speed = editor.speed, pitch = editor.pitch ) + is ExoPlayerPreferencesEditor -> + MediaUserPreferences( + commit = commit, + speed = editor.speed, + pitch = editor.pitch + ) } } } @Composable -private fun ColumnScope.TtsUserPreferences( +private fun MediaUserPreferences( commit: () -> Unit, - language: Preference<Language?>, - voice: EnumPreference<AndroidTtsEngine.Voice.Id?>, - speed: RangePreference<Double>, - pitch: RangePreference<Double> + language: Preference<Language?>? = null, + voice: EnumPreference<AndroidTtsEngine.Voice.Id?>? = null, + speed: RangePreference<Double>? = null, + pitch: RangePreference<Double>? = null ) { Column { - StepperItem( - title = stringResource(R.string.speed_rate), - preference = speed, - commit = commit - ) - StepperItem( - title = stringResource(R.string.pitch_rate), - preference = pitch, - commit = commit - ) - LanguageItem( - preference = language, - commit = commit - ) + if (speed != null) { + StepperItem( + title = stringResource(R.string.speed_rate), + preference = speed, + commit = commit + ) + } - val context = LocalContext.current + if (pitch != null) { + StepperItem( + title = stringResource(R.string.pitch_rate), + preference = pitch, + commit = commit + ) + } + if (language != null) { + LanguageItem( + preference = language, + commit = commit + ) + } - MenuItem( - title = stringResource(R.string.tts_voice), - preference = voice, - formatValue = { it?.value ?: context.getString(R.string.defaultValue) }, - commit = commit - ) + if (voice != null) { + MenuItem( + title = stringResource(R.string.tts_voice), + preference = voice, + formatValue = { it?.value ?: "Default" }, + commit = commit + ) + } } } @@ -185,7 +198,7 @@ private fun ColumnScope.TtsUserPreferences( * User settings for a publication with a fixed layout, such as fixed-layout EPUB, PDF or comic book. */ @Composable -private fun ColumnScope.FixedLayoutUserPreferences( +private fun FixedLayoutUserPreferences( commit: () -> Unit, language: Preference<Language?>? = null, readingProgression: EnumPreference<ReadingProgression>? = null, @@ -198,12 +211,6 @@ private fun ColumnScope.FixedLayoutUserPreferences( pageSpacing: RangePreference<Double>? = null ) { if (language != null || readingProgression != null) { - fun reset() { - language?.clear() - readingProgression?.clear() - commit() - } - if (language != null) { LanguageItem( preference = language, @@ -258,7 +265,7 @@ private fun ColumnScope.FixedLayoutUserPreferences( ButtonGroupItem( title = "Spread", preference = spread, - commit = commit, + commit = commit ) { value -> when (value) { Spread.AUTO -> "Auto" @@ -305,7 +312,7 @@ private fun ColumnScope.FixedLayoutUserPreferences( * a reflowable EPUB, HTML document or PDF with reflow mode enabled. */ @Composable -private fun ColumnScope.ReflowableUserPreferences( +private fun ReflowableUserPreferences( commit: () -> Unit, backgroundColor: Preference<Color>? = null, columnCount: EnumPreference<ColumnCount>? = null, @@ -330,16 +337,9 @@ private fun ColumnScope.ReflowableUserPreferences( theme: EnumPreference<Theme>? = null, typeScale: RangePreference<Double>? = null, verticalText: Preference<Boolean>? = null, - wordSpacing: RangePreference<Double>? = null, + wordSpacing: RangePreference<Double>? = null ) { if (language != null || readingProgression != null || verticalText != null) { - fun reset() { - language?.clear() - readingProgression?.clear() - verticalText?.clear() - commit() - } - if (language != null) { LanguageItem( preference = language, @@ -368,7 +368,6 @@ private fun ColumnScope.ReflowableUserPreferences( } if (scroll != null || columnCount != null || pageMargins != null) { - if (scroll != null) { SwitchItem( title = "Scroll", @@ -381,7 +380,7 @@ private fun ColumnScope.ReflowableUserPreferences( ButtonGroupItem( title = "Columns", preference = columnCount, - commit = commit, + commit = commit ) { value -> when (value) { ColumnCount.AUTO -> "Auto" @@ -403,7 +402,6 @@ private fun ColumnScope.ReflowableUserPreferences( } if (theme != null || textColor != null || imageFilter != null) { - if (theme != null) { ButtonGroupItem( title = "Theme", @@ -505,7 +503,7 @@ private fun ColumnScope.ReflowableUserPreferences( SwitchItem( title = "Publisher styles", preference = publisherStyles, - commit = commit, + commit = commit ) if (!(publisherStyles.value ?: publisherStyles.effectiveValue)) { @@ -603,7 +601,7 @@ private fun Divider() { private fun PresetsMenuButton( presets: List<Preset>, clear: () -> Unit, - commit: () -> Unit, + commit: () -> Unit ) { if (presets.isEmpty()) return diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/preferences/UserPreferencesViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/preferences/UserPreferencesViewModel.kt index 991639b320..f8c4d2130f 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/preferences/UserPreferencesViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/preferences/UserPreferencesViewModel.kt @@ -14,17 +14,16 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.readium.adapters.pdfium.navigator.PdfiumPreferences -import org.readium.adapters.pdfium.navigator.PdfiumSettings +import org.readium.adapter.exoplayer.audio.ExoPlayerPreferences +import org.readium.adapter.exoplayer.audio.ExoPlayerSettings +import org.readium.adapter.pdfium.navigator.PdfiumPreferences +import org.readium.adapter.pdfium.navigator.PdfiumSettings import org.readium.r2.navigator.epub.EpubPreferences import org.readium.r2.navigator.epub.EpubSettings import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.navigator.preferences.PreferencesEditor import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.testapp.reader.EpubReaderInitData -import org.readium.r2.testapp.reader.PdfReaderInitData -import org.readium.r2.testapp.reader.ReaderInitData -import org.readium.r2.testapp.reader.ReaderViewModel +import org.readium.r2.testapp.reader.* import org.readium.r2.testapp.utils.extensions.mapStateIn /** @@ -36,8 +35,8 @@ import org.readium.r2.testapp.utils.extensions.mapStateIn */ @OptIn(ExperimentalReadiumApi::class) class UserPreferencesViewModel<S : Configurable.Settings, P : Configurable.Preferences<P>>( - private val bookId: Long, private val viewModelScope: CoroutineScope, + private val bookId: Long, private val preferencesManager: PreferencesManager<P>, private val createPreferencesEditor: (P) -> PreferencesEditor<P> ) { @@ -68,16 +67,28 @@ class UserPreferencesViewModel<S : Configurable.Settings, P : Configurable.Prefe when (readerInitData) { is EpubReaderInitData -> with(readerInitData) { UserPreferencesViewModel<EpubSettings, EpubPreferences>( - bookId, viewModelScope, preferencesManager, + viewModelScope, + bookId, + preferencesManager, createPreferencesEditor = navigatorFactory::createPreferencesEditor ) } is PdfReaderInitData -> with(readerInitData) { UserPreferencesViewModel<PdfiumSettings, PdfiumPreferences>( - bookId, viewModelScope, preferencesManager, + viewModelScope, + bookId, + preferencesManager, createPreferencesEditor = navigatorFactory::createPreferencesEditor ) } + is MediaReaderInitData -> with(readerInitData) { + UserPreferencesViewModel<ExoPlayerSettings, ExoPlayerPreferences>( + viewModelScope, + bookId, + preferencesManager, + createPreferencesEditor = navigatorFactory::createAudioPreferencesEditor + ) + } else -> null } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsControls.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsControls.kt index a5459d8ba0..141360df69 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsControls.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsControls.kt @@ -4,8 +4,6 @@ * available in the top-level LICENSE file of the project. */ -@file:OptIn(ExperimentalReadiumApi::class) - package org.readium.r2.testapp.reader.tts import androidx.compose.foundation.layout.Arrangement @@ -23,7 +21,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.testapp.R import org.readium.r2.testapp.utils.extensions.asStateWhenStarted @@ -60,39 +57,45 @@ fun TtsControls( onPrevious: () -> Unit, onNext: () -> Unit, onPreferences: () -> Unit, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { Card( modifier = modifier ) { Row( horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { val largeButtonModifier = Modifier.size(40.dp) IconButton(onClick = onPrevious) { Icon( imageVector = Icons.Default.SkipPrevious, - contentDescription = stringResource(R.string.tts_previous), + contentDescription = stringResource(R.string.tts_previous) ) } IconButton( - onClick = onPlayPause, + onClick = onPlayPause ) { Icon( - imageVector = if (playing) Icons.Default.Pause - else Icons.Default.PlayArrow, + imageVector = if (playing) { + Icons.Default.Pause + } else { + Icons.Default.PlayArrow + }, contentDescription = stringResource( - if (playing) R.string.tts_pause - else R.string.tts_play + if (playing) { + R.string.tts_pause + } else { + R.string.tts_play + } ), modifier = Modifier.then(largeButtonModifier) ) } IconButton( - onClick = onStop, + onClick = onStop ) { Icon( imageVector = Icons.Default.Stop, diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsError.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsError.kt new file mode 100644 index 0000000000..e5da980ec6 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsError.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.reader.tts + +import org.readium.navigator.media.tts.TtsNavigator +import org.readium.navigator.media.tts.TtsNavigatorFactory +import org.readium.navigator.media.tts.android.AndroidTtsEngine +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError + +@OptIn(ExperimentalReadiumApi::class) +sealed class TtsError( + override val message: String, + override val cause: Error? = null +) : Error { + + class ContentError(override val cause: TtsNavigator.Error.ContentError) : + TtsError(cause.message, cause.cause) + + sealed class EngineError(override val cause: AndroidTtsEngine.Error) : + TtsError(cause.message, cause.cause) { + + class Network(override val cause: AndroidTtsEngine.Error.Network) : + EngineError(cause) + + class Other(override val cause: AndroidTtsEngine.Error) : + EngineError(cause) + } + + class ServiceError(val exception: Exception) : + TtsError("Could not open session.", ThrowableError(exception)) + + class Initialization(override val cause: TtsNavigatorFactory.Error) : + TtsError(cause.message, cause) + + fun toUserError(): UserError = when (this) { + is ContentError -> UserError(R.string.tts_error_other, cause = this) + is EngineError.Network -> UserError(R.string.tts_error_network, cause = this) + is EngineError.Other -> UserError(R.string.tts_error_other, cause = this) + is Initialization -> UserError(R.string.tts_error_initialization, cause = this) + is ServiceError -> UserError(R.string.error_unexpected, cause = this) + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsPreferencesEditor.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsPreferencesEditor.kt index 303743b977..705156b286 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsPreferencesEditor.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsPreferencesEditor.kt @@ -6,9 +6,9 @@ package org.readium.r2.testapp.reader.tts -import org.readium.r2.navigator.media3.tts.android.AndroidTtsEngine -import org.readium.r2.navigator.media3.tts.android.AndroidTtsPreferences -import org.readium.r2.navigator.media3.tts.android.AndroidTtsPreferencesEditor +import org.readium.navigator.media.tts.android.AndroidTtsEngine +import org.readium.navigator.media.tts.android.AndroidTtsPreferences +import org.readium.navigator.media.tts.android.AndroidTtsPreferencesEditor import org.readium.r2.navigator.preferences.* import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.Language @@ -41,7 +41,6 @@ class TtsPreferencesEditor( * TTS default language and to ignore regions. */ val voice: EnumPreference<AndroidTtsEngine.Voice.Id?> = run { - // Recomposition will be triggered higher if the value changes. val currentLanguage = language.effectiveValue?.removeRegion() editor.voices.map( diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsService.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsService.kt deleted file mode 100644 index b759bfc0e0..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsService.kt +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.testapp.reader.tts - -import android.app.Application -import android.app.PendingIntent -import android.content.ComponentName -import android.content.Intent -import android.content.ServiceConnection -import android.os.Build -import android.os.IBinder -import androidx.core.content.ContextCompat -import androidx.media3.session.MediaSession -import androidx.media3.session.MediaSessionService -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import org.readium.r2.navigator.media3.tts.AndroidTtsNavigator -import org.readium.r2.shared.ExperimentalReadiumApi -import timber.log.Timber - -@OptIn(ExperimentalReadiumApi::class) -@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) -class TtsService : MediaSessionService() { - - class Session( - val bookId: Long, - val navigator: AndroidTtsNavigator, - val mediaSession: MediaSession, - ) { - val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - } - - /** - * The service interface to be used by the app. - */ - inner class Binder : android.os.Binder() { - - private val app: org.readium.r2.testapp.Application - get() = application as org.readium.r2.testapp.Application - - private val sessionMutable: MutableStateFlow<Session?> = - MutableStateFlow(null) - - val session: StateFlow<Session?> = - sessionMutable.asStateFlow() - - fun closeSession() { - stopForeground(true) - session.value?.mediaSession?.release() - session.value?.navigator?.close() - session.value?.coroutineScope?.cancel() - sessionMutable.value = null - } - - @OptIn(FlowPreview::class) - fun openSession( - navigator: AndroidTtsNavigator, - bookId: Long - ) { - val activityIntent = createSessionActivityIntent() - val mediaSession = MediaSession.Builder(applicationContext, navigator.asPlayer()) - .setSessionActivity(activityIntent) - .setId(bookId.toString()) - .build() - - addSession(mediaSession) - - val session = Session( - bookId, - navigator, - mediaSession - ) - - sessionMutable.value = session - - /* - * Launch a job for saving progression even when playback is going on in the background - * with no ReaderActivity opened. - */ - navigator.currentLocator - .sample(3000) - .onEach { locator -> - Timber.d("Saving TTS progression $locator") - app.bookRepository.saveProgression(locator, bookId) - }.launchIn(session.coroutineScope) - } - - private fun createSessionActivityIntent(): PendingIntent { - // This intent will be triggered when the notification is clicked. - var flags = PendingIntent.FLAG_UPDATE_CURRENT - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - flags = flags or PendingIntent.FLAG_IMMUTABLE - } - - val intent = application.packageManager.getLaunchIntentForPackage(application.packageName) - - return PendingIntent.getActivity(applicationContext, 0, intent, flags) - } - } - - private val binder by lazy { - Binder() - } - - override fun onBind(intent: Intent?): IBinder? { - Timber.d("onBind called with $intent") - - return if (intent?.action == SERVICE_INTERFACE) { - super.onBind(intent) - // Readium-aware client. - Timber.d("Returning custom binder.") - binder - } else { - // External controller. - Timber.d("Returning MediaSessionService binder.") - super.onBind(intent) - } - } - - override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { - return binder.session.value?.mediaSession - } - - override fun onTaskRemoved(rootIntent: Intent) { - super.onTaskRemoved(rootIntent) - Timber.d("Task removed. Stopping session and service.") - // Close the navigator to allow the service to be stopped. - binder.closeSession() - stopSelf() - } - - companion object { - - const val SERVICE_INTERFACE = "org.readium.r2.testapp.reader.tts.TtsService" - - fun start(application: Application) { - val intent = intent(application) - ContextCompat.startForegroundService(application, intent) - } - - suspend fun bind(application: Application): TtsService.Binder { - val mediaServiceBinder: CompletableDeferred<TtsService.Binder> = - CompletableDeferred() - - val mediaServiceConnection = object : ServiceConnection { - - override fun onServiceConnected(name: ComponentName?, service: IBinder) { - Timber.d("MediaService bound.") - mediaServiceBinder.complete(service as Binder) - } - - override fun onServiceDisconnected(name: ComponentName) { - Timber.d("MediaService disconnected.") - // Should not happen, do nothing. - } - - override fun onNullBinding(name: ComponentName) { - val errorMessage = "Failed to bind to MediaService." - Timber.e(errorMessage) - val exception = IllegalStateException(errorMessage) - mediaServiceBinder.completeExceptionally(exception) - } - } - - val intent = intent(application) - application.bindService(intent, mediaServiceConnection, 0) - - return mediaServiceBinder.await() - } - - fun stop(application: Application) { - val intent = intent(application) - application.stopService(intent) - } - - private fun intent(application: Application) = - Intent(SERVICE_INTERFACE) - // MediaSessionService.onBind requires the intent to have a non-null action - .apply { setClass(application, TtsService::class.java) } - } -} diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsServiceFacade.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsServiceFacade.kt deleted file mode 100644 index 7bece6912e..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsServiceFacade.kt +++ /dev/null @@ -1,74 +0,0 @@ -package org.readium.r2.testapp.reader.tts - -import android.app.Application -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import org.readium.r2.navigator.media3.tts.AndroidTtsNavigator -import org.readium.r2.shared.ExperimentalReadiumApi - -/** - * Enables to try to close a session without starting the [TtsService] if it is not started. - */ -@OptIn(ExperimentalReadiumApi::class) -class TtsServiceFacade( - private val application: Application -) { - private val coroutineScope: CoroutineScope = - MainScope() - - private val mutex: Mutex = - Mutex() - - private var binder: TtsService.Binder? = - null - - private var bindingJob: Job? = - null - - private val sessionMutable: MutableStateFlow<TtsService.Session?> = - MutableStateFlow(null) - - val session: StateFlow<TtsService.Session?> = - sessionMutable.asStateFlow() - - suspend fun openSession( - bookId: Long, - navigator: AndroidTtsNavigator - ) = mutex.withLock { - if (session.value != null) { - throw CancellationException("A session is already running.") - } - - try { - if (binder == null) { - TtsService.start(application) - val binder = TtsService.bind(application) - this.binder = binder - bindingJob = binder.session - .onEach { sessionMutable.value = it } - .launchIn(coroutineScope) - } - - binder!!.openSession(navigator, bookId) - } catch (e: CancellationException) { - TtsService.stop(application) - throw e - } - } - - suspend fun closeSession() = mutex.withLock { - if (session.value == null) { - throw CancellationException("No session to close.") - } - - withContext(NonCancellable) { - bindingJob!!.cancelAndJoin() - binder!!.closeSession() - sessionMutable.value = null - binder = null - TtsService.stop(application) - } - } -} diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt index eac63df43a..0c006fdb21 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt @@ -8,29 +8,32 @@ package org.readium.r2.testapp.reader.tts import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import org.readium.navigator.media.common.MediaNavigator +import org.readium.navigator.media.tts.AndroidTtsNavigator +import org.readium.navigator.media.tts.AndroidTtsNavigatorFactory +import org.readium.navigator.media.tts.TtsNavigator +import org.readium.navigator.media.tts.android.AndroidTtsEngine +import org.readium.navigator.media.tts.android.AndroidTtsPreferences +import org.readium.navigator.media.tts.android.AndroidTtsSettings import org.readium.r2.navigator.Navigator import org.readium.r2.navigator.VisualNavigator -import org.readium.r2.navigator.media3.api.MediaNavigator -import org.readium.r2.navigator.media3.tts.AndroidTtsNavigator -import org.readium.r2.navigator.media3.tts.AndroidTtsNavigatorFactory -import org.readium.r2.navigator.media3.tts.TtsNavigator -import org.readium.r2.navigator.media3.tts.android.AndroidTtsEngine -import org.readium.r2.navigator.media3.tts.android.AndroidTtsPreferences -import org.readium.r2.navigator.media3.tts.android.AndroidTtsSettings import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Language -import org.readium.r2.testapp.R +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.testapp.reader.MediaService +import org.readium.r2.testapp.reader.MediaServiceFacade import org.readium.r2.testapp.reader.ReaderInitData import org.readium.r2.testapp.reader.VisualReaderInitData import org.readium.r2.testapp.reader.preferences.PreferencesManager import org.readium.r2.testapp.reader.preferences.UserPreferencesViewModel import org.readium.r2.testapp.utils.extensions.mapStateIn +import timber.log.Timber /** * View model controlling a [TtsNavigator] to read a publication aloud. @@ -43,8 +46,8 @@ class TtsViewModel private constructor( private val bookId: Long, private val publication: Publication, private val ttsNavigatorFactory: AndroidTtsNavigatorFactory, - private val ttsServiceFacade: TtsServiceFacade, - private val preferencesManager: PreferencesManager<AndroidTtsPreferences>, + private val mediaServiceFacade: MediaServiceFacade, + private val preferencesManager: PreferencesManager<AndroidTtsPreferences> ) : TtsNavigator.Listener { companion object { @@ -54,7 +57,7 @@ class TtsViewModel private constructor( */ operator fun invoke( viewModelScope: CoroutineScope, - readerInitData: ReaderInitData, + readerInitData: ReaderInitData ): TtsViewModel? { if (readerInitData !is VisualReaderInitData || readerInitData.ttsInitData == null) { return null @@ -64,8 +67,8 @@ class TtsViewModel private constructor( viewModelScope = viewModelScope, bookId = readerInitData.bookId, publication = readerInitData.publication, - ttsNavigatorFactory = readerInitData.ttsInitData.ttsNavigatorFactory, - ttsServiceFacade = readerInitData.ttsInitData.ttsServiceFacade, + ttsNavigatorFactory = readerInitData.ttsInitData.navigatorFactory, + mediaServiceFacade = readerInitData.ttsInitData.mediaServiceFacade, preferencesManager = readerInitData.ttsInitData.preferencesManager ) } @@ -75,7 +78,7 @@ class TtsViewModel private constructor( /** * Emitted when the [TtsNavigator] fails with an error. */ - class OnError(val error: UserException) : Event() + class OnError(val error: TtsError) : Event() /** * Emitted when the selected language cannot be played because it is missing voice data. @@ -83,8 +86,14 @@ class TtsViewModel private constructor( class OnMissingVoiceData(val language: Language) : Event() } + @Suppress("Unchecked_cast") + private val MediaService.Session.ttsNavigator + get() = navigator as? AndroidTtsNavigator + private val navigatorNow: AndroidTtsNavigator? get() = - ttsServiceFacade.session.value?.navigator + mediaServiceFacade.session.value?.ttsNavigator + + private var launchJob: Job? = null private val _events: Channel<Event> = Channel(Channel.BUFFERED) @@ -98,37 +107,35 @@ class TtsViewModel private constructor( bookId = bookId, preferencesManager = preferencesManager ) { preferences -> - val baseEditor = ttsNavigatorFactory.createTtsPreferencesEditor(preferences) + val baseEditor = ttsNavigatorFactory.createPreferencesEditor(preferences) + val voices = navigatorNow?.voices.orEmpty() TtsPreferencesEditor(baseEditor, voices) } val showControls: StateFlow<Boolean> = - ttsServiceFacade.session.mapStateIn(viewModelScope) { + mediaServiceFacade.session.mapStateIn(viewModelScope) { it != null } val isPlaying: StateFlow<Boolean> = - ttsServiceFacade.session.flatMapLatest { session -> + mediaServiceFacade.session.flatMapLatest { session -> session?.navigator?.playback?.map { playback -> playback.playWhenReady } ?: MutableStateFlow(false) }.stateIn(viewModelScope, SharingStarted.Eagerly, false) val position: StateFlow<Locator?> = - ttsServiceFacade.session.flatMapLatest { session -> + mediaServiceFacade.session.flatMapLatest { session -> session?.navigator?.currentLocator ?: MutableStateFlow(null) }.stateIn(viewModelScope, SharingStarted.Eagerly, null) val highlight: StateFlow<Locator?> = - ttsServiceFacade.session.flatMapLatest { session -> - session?.navigator?.utterance?.map { it.utteranceLocator } + mediaServiceFacade.session.flatMapLatest { session -> + session?.ttsNavigator?.location?.map { it.utteranceLocator } ?: MutableStateFlow(null) }.stateIn(viewModelScope, SharingStarted.Eagerly, null) - val voices: Set<AndroidTtsEngine.Voice> get() = - ttsServiceFacade.session.value?.navigator?.voices.orEmpty() - init { - ttsServiceFacade.session + mediaServiceFacade.session .flatMapLatest { it?.navigator?.playback ?: MutableStateFlow(null) } .onEach { playback -> when (playback?.state) { @@ -137,16 +144,19 @@ class TtsViewModel private constructor( is MediaNavigator.State.Ended -> { stop() } - is MediaNavigator.State.Error -> { - onPlaybackError(playback.state as TtsNavigator.State.Error) + is MediaNavigator.State.Failure -> { + onPlaybackError( + (playback.state as TtsNavigator.State.Failure).error + ) } is MediaNavigator.State.Ready -> {} is MediaNavigator.State.Buffering -> {} } - }.launchIn(viewModelScope) + } + .launchIn(viewModelScope) preferencesManager.preferences - .onEach { ttsServiceFacade.session.value?.navigator?.submitPreferences(it) } + .onEach { navigatorNow?.submitPreferences(it) } .launchIn(viewModelScope) } @@ -154,10 +164,11 @@ class TtsViewModel private constructor( * Starts the TTS using the first visible locator in the given [navigator]. */ fun start(navigator: Navigator) { - viewModelScope.launch { - if (ttsServiceFacade.session.value != null) - return@launch + if (launchJob != null) { + return + } + launchJob = viewModelScope.launch { openSession(navigator) } } @@ -167,24 +178,30 @@ class TtsViewModel private constructor( val ttsNavigator = ttsNavigatorFactory.createNavigator( this, - preferencesManager.preferences.value, - start - ) ?: run { - val exception = UserException(R.string.tts_error_initialization) - _events.send(Event.OnError(exception)) + initialLocator = start, + initialPreferences = preferencesManager.preferences.value + ).getOrElse { + val error = TtsError.Initialization(it) + _events.send(Event.OnError(error)) + return + } + + try { + mediaServiceFacade.openSession(bookId, ttsNavigator) + } catch (e: Exception) { + ttsNavigator.close() + val error = TtsError.ServiceError(e) + _events.trySend(Event.OnError(error)) + launchJob = null return } - // playWhenReady must be true for the MediaSessionService to call Service.startForeground - // and prevent crashing ttsNavigator.play() - ttsServiceFacade.openSession(bookId, ttsNavigator) } fun stop() { - viewModelScope.launch { - ttsServiceFacade.closeSession() - } + launchJob = null + mediaServiceFacade.closeSession() } fun play() { @@ -196,34 +213,41 @@ class TtsViewModel private constructor( } fun previous() { - navigatorNow?.goBackward() + navigatorNow?.skipToPreviousUtterance() } fun next() { - navigatorNow?.goForward() + navigatorNow?.skipToNextUtterance() } override fun onStopRequested() { stop() } - private fun onPlaybackError(error: TtsNavigator.State.Error) { - val exception = when (error) { - is TtsNavigator.State.Error.ContentError -> { - UserException(R.string.tts_error_other, cause = error.exception) + private fun onPlaybackError(error: TtsNavigator.Error) { + val event = when (error) { + is TtsNavigator.Error.ContentError -> { + Event.OnError(TtsError.ContentError(error)) } - is TtsNavigator.State.Error.EngineError<*> -> { - when ((error.error as AndroidTtsEngine.Error).kind) { - AndroidTtsEngine.Error.Kind.Network -> - UserException(R.string.tts_error_network) - else -> - UserException(R.string.tts_error_other) - } + is TtsNavigator.Error.EngineError<*> -> { + val engineError = (error.cause as AndroidTtsEngine.Error) + when (engineError) { + is AndroidTtsEngine.Error.LanguageMissingData -> + Event.OnMissingVoiceData(engineError.language) + is AndroidTtsEngine.Error.Network -> { + val ttsError = TtsError.EngineError.Network(engineError) + Event.OnError(ttsError) + } + else -> { + val ttsError = TtsError.EngineError.Other(engineError) + Event.OnError(ttsError) + } + }.also { Timber.e("Error type: $error") } } } viewModelScope.launch { - _events.send(Event.OnError(exception)) + _events.send(event) } } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/search/SearchFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/search/SearchFragment.kt index 9cbf55f440..4e9b239e1b 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/search/SearchFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/search/SearchFragment.kt @@ -54,12 +54,11 @@ class SearchFragment : Fragment(R.layout.fragment_search) { .onEach { binding.noResultLabel.isVisible = it.isEmpty() } .launchIn(viewScope) - viewModel.activityChannel + viewModel.searchChannel .receive(viewLifecycleOwner) { event -> when (event) { - ReaderViewModel.Event.StartNewSearch -> + ReaderViewModel.SearchCommand.StartNewSearch -> binding.searchRecyclerView.scrollToPosition(0) - else -> {} } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/search/SearchPagingSource.kt b/test-app/src/main/java/org/readium/r2/testapp/search/SearchPagingSource.kt index 5446a1bf2b..91b82d1f10 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/search/SearchPagingSource.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/search/SearchPagingSource.kt @@ -8,12 +8,14 @@ package org.readium.r2.testapp.search import androidx.paging.PagingSource import androidx.paging.PagingState -import org.readium.r2.shared.Search +import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.LocatorCollection import org.readium.r2.shared.publication.services.search.SearchTry +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.getOrThrow -@OptIn(Search::class) +@OptIn(ExperimentalReadiumApi::class) class SearchPagingSource( private val listener: Listener? ) : PagingSource<Unit, Locator>() { @@ -30,7 +32,9 @@ class SearchPagingSource( listener ?: return LoadResult.Page(data = emptyList(), prevKey = null, nextKey = null) return try { - val page = listener.next().getOrThrow() + val page = listener.next() + .mapFailure { ErrorException(it) } + .getOrThrow() LoadResult.Page( data = page?.locators ?: emptyList(), prevKey = null, diff --git a/test-app/src/main/java/org/readium/r2/testapp/search/SearchResultAdapter.kt b/test-app/src/main/java/org/readium/r2/testapp/search/SearchResultAdapter.kt index 8e51753244..84adb7c232 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/search/SearchResultAdapter.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/search/SearchResultAdapter.kt @@ -27,7 +27,9 @@ class SearchResultAdapter(private var listener: Listener) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( ItemRecycleSearchBinding.inflate( - LayoutInflater.from(parent.context), parent, false + LayoutInflater.from(parent.context), + parent, + false ) ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt new file mode 100644 index 0000000000..661b31fe42 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.search + +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.services.search.SearchError +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError + +@OptIn(ExperimentalReadiumApi::class) +fun SearchError.toUserError(): UserError = when (this) { + is SearchError.Engine -> UserError(R.string.search_error_other, cause = this) + is SearchError.Reading -> UserError(R.string.search_error_other, cause = this) +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/shared/views/Preferences.kt b/test-app/src/main/java/org/readium/r2/testapp/shared/views/Preferences.kt index 38bf618988..115618a28f 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/shared/views/Preferences.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/shared/views/Preferences.kt @@ -79,7 +79,7 @@ private fun <T> ButtonGroupItem( selectedOption: T?, formatValue: (T) -> String, onClear: (() -> Unit)?, - onSelectedOptionChanged: (T) -> Unit, + onSelectedOptionChanged: (T) -> Unit ) { Item(title, isActive = isActive, onClear = onClear) { ToggleButtonGroup( @@ -143,14 +143,14 @@ private fun <T> MenuItem( ) } ) { dismiss -> - for (value in values) { + for (aValue in values) { DropdownMenuItem( onClick = { dismiss() - onValueChanged(value) + onValueChanged(aValue) } ) { - Text(formatValue(value)) + Text(formatValue(aValue)) } } } @@ -174,7 +174,7 @@ fun <T : Comparable<T>> StepperItem( onDecrement = { preference.decrement(); commit() }, onIncrement = { preference.increment(); commit() }, onClear = { preference.clear(); commit() } - .takeIf { preference.value != null }, + .takeIf { preference.value != null } ) } @@ -235,7 +235,7 @@ fun SwitchItem( onCheckedChange = { preference.set(it); commit() }, onToggle = { preference.toggle(); commit() }, onClear = { preference.clear(); commit() } - .takeIf { preference.value != null }, + .takeIf { preference.value != null } ) } @@ -378,8 +378,11 @@ private fun Item( ) { ListItem( modifier = - if (onClick != null) Modifier.clickable(onClick = onClick) - else Modifier, + if (onClick != null) { + Modifier.clickable(onClick = onClick) + } else { + Modifier + }, text = { val alpha = if (isActive) 1.0f else ContentAlpha.disabled CompositionLocalProvider(LocalContentAlpha provides alpha) { @@ -432,7 +435,7 @@ private fun <T> SelectorListItem( selection: T, formatValue: (T) -> String, onSelected: (T) -> Unit, - enabled: Boolean = values.isNotEmpty(), + enabled: Boolean = values.isNotEmpty() ) { var isExpanded by remember { mutableStateOf(false) } fun dismiss() { isExpanded = false } diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/ContentResolverUtil.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/ContentResolverUtil.kt index 635512a58b..035d8080a0 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/ContentResolverUtil.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/ContentResolverUtil.kt @@ -74,7 +74,8 @@ object ContentResolverUtil { } return try { val contentUri = ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id) + Uri.parse("content://downloads/public_downloads"), + java.lang.Long.valueOf(id) ) getDataColumn(context, contentUri, null, null) } catch (e: NumberFormatException) { @@ -99,7 +100,6 @@ object ContentResolverUtil { return getDataColumn(context, contentUri, selection, selectionArgs) } } else if ("content".equals(uri.scheme!!, ignoreCase = true)) { - // Return the remote address return getDataColumn(context, uri, null, null) } else if ("file".equals(uri.scheme!!, ignoreCase = true)) { @@ -125,7 +125,6 @@ object ContentResolverUtil { selection: String?, selectionArgs: Array<String>? ): String? { - val column = "_data" val projection = arrayOf(column) context.contentResolver.query(uri!!, projection, selection, selectionArgs, null).use { cursor -> diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/CoroutineQueue.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/CoroutineQueue.kt new file mode 100644 index 0000000000..82a7a4eb14 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/CoroutineQueue.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.utils + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope + +/** + * CoroutineScope-like util to execute coroutines in a sequential order (FIFO). + * As with a SupervisorJob, children can be cancelled or fail independently one from the other. + */ +class CoroutineQueue( + dispatcher: CoroutineDispatcher = Dispatchers.Main +) { + private val scope: CoroutineScope = + CoroutineScope(dispatcher + SupervisorJob()) + + private val tasks: Channel<Task<*>> = Channel(Channel.UNLIMITED) + + init { + scope.launch { + for (task in tasks) { + // Don't fail the root job if one task fails. + supervisorScope { + task() + } + } + } + } + + /** + * Launches a coroutine in the queue. + * + * Exceptions thrown by [block] will be ignored. + */ + fun launch(block: suspend () -> Unit) { + tasks.trySendBlocking(Task(block)).getOrThrow() + } + + /** + * Creates a coroutine in the queue and returns its future result + * as an implementation of Deferred. + * + * Exceptions thrown by [block] will be caught and represented in the resulting [Deferred]. + */ + fun <T> async(block: suspend () -> T): Deferred<T> { + val deferred = CompletableDeferred<T>() + val task = Task(block, deferred) + tasks.trySendBlocking(task).getOrThrow() + return deferred + } + + /** + * Launches a coroutine in the queue, and waits for its result. + * + * Exceptions thrown by [block] will be rethrown. + */ + suspend fun <T> await(block: suspend () -> T): T = + async(block).await() + + /** + * Cancels this coroutine queue, including all its children with an optional cancellation cause. + */ + fun cancel(cause: CancellationException? = null) { + scope.cancel(cause) + } + + private class Task<T>( + val task: suspend () -> T, + val deferred: CompletableDeferred<T>? = null + ) { + suspend operator fun invoke() { + try { + val result = task() + deferred?.complete(result) + } catch (e: Exception) { + deferred?.completeExceptionally(e) + } + } + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/Exception.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/Exception.kt new file mode 100644 index 0000000000..b371b79089 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/Exception.kt @@ -0,0 +1,25 @@ +package org.readium.r2.testapp.utils + +import timber.log.Timber + +/** + * Returns the result of the given [closure], or null if an [Exception] was raised. + */ +inline fun <T> tryOrNull(closure: () -> T): T? = + tryOr(null, closure) + +/** + * Returns the result of the given [closure], or [default] if an [Exception] was raised. + */ +inline fun <T> tryOr(default: T, closure: () -> T): T = + try { closure() } catch (e: Exception) { default } + +/** + * Returns the result of the given [closure], or null if an [Exception] was raised. + * The [Exception] will be logged. + */ +inline fun <T> tryOrLog(closure: () -> T): T? = + try { closure() } catch (e: Exception) { + Timber.e(e) + null + } diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/R2DispatcherActivity.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/ImportActivity.kt similarity index 78% rename from test-app/src/main/java/org/readium/r2/testapp/utils/R2DispatcherActivity.kt rename to test-app/src/main/java/org/readium/r2/testapp/utils/ImportActivity.kt index 65fd24eb6a..6b327930eb 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/R2DispatcherActivity.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/ImportActivity.kt @@ -14,28 +14,35 @@ import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle +import androidx.core.content.IntentCompat +import org.readium.r2.testapp.Application import org.readium.r2.testapp.MainActivity import timber.log.Timber -class R2DispatcherActivity : Activity() { +class ImportActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - dispatchIntent(intent) + + importPublication(intent) + + val newIntent = Intent(this, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + startActivity(newIntent) + finish() } - private fun dispatchIntent(intent: Intent) { + private fun importPublication(intent: Intent) { val uri = uriFromIntent(intent) ?: run { Timber.d("Got an empty intent.") return } - val newIntent = Intent(this, MainActivity::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - data = uri - } - startActivity(newIntent) + + val app = application as Application + app.bookshelf.importPublicationFromStorage(uri) } private fun uriFromIntent(intent: Intent): Uri? = @@ -44,7 +51,7 @@ class R2DispatcherActivity : Activity() { if ("text/plain" == intent.type) { intent.getStringExtra(Intent.EXTRA_TEXT).let { Uri.parse(it) } } else { - intent.getParcelableExtra(Intent.EXTRA_STREAM) + IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java) } } else -> { diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/LifecycleMedia2SessionService.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/LifecycleMedia2SessionService.kt deleted file mode 100644 index 7a26c0c4dc..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/LifecycleMedia2SessionService.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.testapp.utils - -import android.content.Intent -import android.os.IBinder -import androidx.annotation.CallSuper -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ServiceLifecycleDispatcher -import androidx.media2.session.MediaSessionService - -/* - * Borrowed from - * https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/LifecycleService.java - */ - -abstract class LifecycleMedia2SessionService : MediaSessionService(), LifecycleOwner { - - @Suppress("LeakingThis") - private val lifecycleDispatcher = ServiceLifecycleDispatcher(this) - - @CallSuper - override fun onCreate() { - lifecycleDispatcher.onServicePreSuperOnCreate() - super.onCreate() - } - - @CallSuper - override fun onBind(intent: Intent): IBinder? { - lifecycleDispatcher.onServicePreSuperOnBind() - return super.onBind(intent) - } - - @CallSuper - override fun onStart(intent: Intent?, startId: Int) { - lifecycleDispatcher.onServicePreSuperOnStart() - super.onStart(intent, startId) - } - - // this method is added only to annotate it with @CallSuper. - // In usual service super.onStartCommand is no-op, but in LifecycleService - // it results in mDispatcher.onServicePreSuperOnStart() call, because - // super.onStartCommand calls onStart(). - @CallSuper - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - return super.onStartCommand(intent, flags, startId) - } - - @CallSuper - override fun onDestroy() { - lifecycleDispatcher.onServicePreSuperOnDestroy() - super.onDestroy() - } - - override fun getLifecycle(): Lifecycle { - return lifecycleDispatcher.lifecycle - } -} diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/SectionDecoration.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/SectionDecoration.kt index bcf41ab96b..d3feb40aa4 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/SectionDecoration.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/SectionDecoration.kt @@ -78,9 +78,20 @@ class SectionDecoration( private fun fixLayoutSize(v: View, parent: ViewGroup) { val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY) - val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED) - val childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingStart + parent.paddingEnd, v.layoutParams.width) - val childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, v.layoutParams.height) + val heightSpec = View.MeasureSpec.makeMeasureSpec( + parent.height, + View.MeasureSpec.UNSPECIFIED + ) + val childWidth = ViewGroup.getChildMeasureSpec( + widthSpec, + parent.paddingStart + parent.paddingEnd, + v.layoutParams.width + ) + val childHeight = ViewGroup.getChildMeasureSpec( + heightSpec, + parent.paddingTop + parent.paddingBottom, + v.layoutParams.height + ) v.measure(childWidth, childHeight) v.layout(0, 0, v.measuredWidth, v.measuredHeight) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/SystemUiManagement.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/SystemUiManagement.kt index 52cd7eb77f..9a1e1129ab 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/SystemUiManagement.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/SystemUiManagement.kt @@ -20,8 +20,8 @@ private fun Activity.isSystemUiVisible(): Boolean { } // Using ViewCompat and WindowInsetsCompat does not work properly in all versions of Android -@Suppress("DEPRECATION") /** Enable fullscreen or immersive mode. */ +@Suppress("DEPRECATION") fun Activity.hideSystemUi() { this.window.decorView.systemUiVisibility = ( View.SYSTEM_UI_FLAG_IMMERSIVE @@ -34,8 +34,8 @@ fun Activity.hideSystemUi() { } // Using ViewCompat and WindowInsetsCompat does not work properly in all versions of Android -@Suppress("DEPRECATION") /** Disable fullscreen or immersive mode. */ +@Suppress("DEPRECATION") fun Activity.showSystemUi() { this.window.decorView.systemUiVisibility = ( View.SYSTEM_UI_FLAG_LAYOUT_STABLE diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/UserError.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/UserError.kt new file mode 100644 index 0000000000..c72f9203e8 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/UserError.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2020 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.utils + +import android.app.Activity +import android.content.Context +import androidx.annotation.PluralsRes +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import com.google.android.material.snackbar.Snackbar +import java.text.DateFormat +import java.util.Date +import org.joda.time.DateTime +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.toDebugDescription +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.extensions.createShareIntent +import timber.log.Timber + +/** + * An error that can be presented to the user using a localized message. + */ +class UserError private constructor( + val content: Content, + val cause: Error? +) { + + constructor(@StringRes userMessageId: Int, vararg args: Any?, cause: Error?) : + this(Content(userMessageId, *args), cause) + + constructor( + @PluralsRes userMessageId: Int, + quantity: Int?, + vararg args: Any?, + cause: Error? + ) : + this(Content(userMessageId, quantity, *args), cause) + + constructor(message: String, cause: Error?) : + this(Content(message), cause) + + /** + * Gets the localized user-facing message for this exception. + */ + fun getUserMessage(context: Context): String = + content.getUserMessage(context) + + /** + * Presents the error in the given activity. + */ + fun show(activity: Activity) { + val message = getUserMessage(activity) + val snackbar = Snackbar.make( + activity.findViewById(android.R.id.content), + message, + Snackbar.LENGTH_LONG + ) + + var details = "UserError: $message" + cause?.toDebugDescription()?.let { + details += "\n$it" + snackbar.setAction(R.string.details) { + AlertDialog.Builder(activity) + .setTitle(R.string.details) + .setMessage(details) + .setPositiveButton(R.string.share) { _, _ -> + activity.startActivity(createShareIntent(activity, details)) + } + .setNegativeButton(R.string.close, null) + .show() + } + } + Timber.e(details) + snackbar.show() + } + + /** + * Provides a way to generate a localized user message. + */ + sealed class Content { + + abstract fun getUserMessage(context: Context): String + + /** + * Holds the parts of a localized string message. + * + * @param userMessageId String resource id of the localized user message. + * @param args Optional arguments to expand in the message. + * @param quantity Quantity to use if the user message is a quantity strings. + */ + class LocalizedString( + private val userMessageId: Int, + private val args: Array<out Any?>, + private val quantity: Int? + ) : Content() { + override fun getUserMessage(context: Context): String { + // Convert complex objects to strings, such as Date, to be interpolated. + val args = args.map { arg -> + when (arg) { + is Date -> DateFormat.getDateInstance().format(arg) + is DateTime -> DateFormat.getDateInstance().format(arg.toDate()) + else -> arg + } + } + + val message = + if (quantity != null) { + context.resources.getQuantityString( + userMessageId, + quantity, + *(args.toTypedArray()) + ) + } else { + context.getString(userMessageId, *(args.toTypedArray())) + } + + return message + } + } + + /** + * Holds an already localized string message. For example, received from an HTTP + * Problem Details object. + */ + class Message(private val message: String) : Content() { + override fun getUserMessage(context: Context): String = message + } + + companion object { + operator fun invoke(@StringRes userMessageId: Int, vararg args: Any?): Content = + LocalizedString(userMessageId, args, null) + operator fun invoke( + @PluralsRes userMessageId: Int, + quantity: Int?, + vararg args: Any? + ): Content = + LocalizedString(userMessageId, args, quantity) + + operator fun invoke(message: String): Content = + Message(message) + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/WebLauncher.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/WebLauncher.kt similarity index 84% rename from readium/shared/src/main/java/org/readium/r2/shared/util/WebLauncher.kt rename to test-app/src/main/java/org/readium/r2/testapp/utils/WebLauncher.kt index 563633b1fe..6fb28171fe 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/WebLauncher.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/WebLauncher.kt @@ -1,4 +1,4 @@ -package org.readium.r2.shared.util +package org.readium.r2.testapp.utils import android.content.ActivityNotFoundException import android.content.Context @@ -6,13 +6,10 @@ import android.content.Intent import android.net.Uri import android.webkit.URLUtil import androidx.browser.customtabs.CustomTabsIntent -import org.readium.r2.shared.InternalReadiumApi -import org.readium.r2.shared.extensions.tryOrLog /** * Opens the given [uri] with a Chrome Custom Tab or the system browser as a fallback. */ -@InternalReadiumApi fun launchWebBrowser(context: Context, uri: Uri) { var url = uri if (url.scheme == null) { diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/compose/AppTheme.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/compose/AppTheme.kt index b26ab96095..4bae27d3de 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/compose/AppTheme.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/compose/AppTheme.kt @@ -7,7 +7,7 @@ package org.readium.r2.testapp.utils.compose import androidx.compose.runtime.Composable -import com.google.android.material.composethemeadapter.MdcTheme +import com.google.accompanist.themeadapter.material.MdcTheme /** * Setup the Compose app-wide theme. diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/compose/ColorPicker.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/compose/ColorPicker.kt index 5f0ab20c5a..7c3ed2aaff 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/compose/ColorPicker.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/compose/ColorPicker.kt @@ -36,7 +36,7 @@ fun ColorPicker(onPick: (Int) -> Unit) { // Yellow listOf("#fff9c4", "#fff176", "#ffeb3b", "#fbc02d", "#f57f17"), // Orange - listOf("#ffe0b2", "#ffb74d", "#ff9800", "#f57c00", "#e65100"), + listOf("#ffe0b2", "#ffb74d", "#ff9800", "#f57c00", "#e65100") ) Column { diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/compose/ComposeBottomSheetDialogFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/compose/ComposeBottomSheetDialogFragment.kt index 79980ac278..5be07bbc52 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/compose/ComposeBottomSheetDialogFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/compose/ComposeBottomSheetDialogFragment.kt @@ -43,11 +43,13 @@ abstract class ComposeBottomSheetDialogFragment( composeView, FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT ) ) } - } else composeView + } else { + composeView + } } @Composable diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/compose/DropdownMenuButton.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/compose/DropdownMenuButton.kt index 53756cf02b..a71ee3a612 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/compose/DropdownMenuButton.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/compose/DropdownMenuButton.kt @@ -21,7 +21,7 @@ fun DropdownMenuButton( fun dismiss() { isExpanded = false } OutlinedButton( - onClick = { isExpanded = true }, + onClick = { isExpanded = true } ) { text() DropdownMenu( diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/compose/ToggleButton.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/compose/ToggleButton.kt index 06aecf18e7..5d658515ca 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/compose/ToggleButton.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/compose/ToggleButton.kt @@ -69,8 +69,11 @@ fun ToggleButton( } ), elevation = - if (selected) ButtonDefaults.elevation(defaultElevation = 2.dp) - else null + if (selected) { + ButtonDefaults.elevation(defaultElevation = 2.dp) + } else { + null + } ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/File.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/File.kt index 81368e780e..610dbf8148 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/File.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/File.kt @@ -11,22 +11,17 @@ package org.readium.r2.testapp.utils.extensions import java.io.File import java.io.FileFilter -import java.io.FileOutputStream -import java.io.IOException -import java.net.URL import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withContext -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.flatMap -import org.readium.r2.shared.util.http.* -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.testapp.BuildConfig -import timber.log.Timber suspend fun File.moveTo(target: File) = withContext(Dispatchers.IO) { - if (!this@moveTo.renameTo(target)) - throw IOException() + if (this@moveTo.renameTo(target)) { + return@withContext + } + + // renameTo might be unable to move a file from a filesystem to another. Copy instead. + copyTo(target) + delete() } /** @@ -37,62 +32,3 @@ fun File.listFilesSafely(filter: FileFilter? = null): List<File> { val array: Array<File>? = if (filter == null) listFiles() else listFiles(filter) return array?.toList() ?: emptyList() } - -suspend fun URL.downloadTo( - dest: File, - maxRedirections: Int = 2 -): Try<Unit, Exception> { - if (maxRedirections == 0) { - return Try.Failure(Exception("Too many HTTP redirections.")) - } - - val urlString = toString() - - if (BuildConfig.DEBUG) Timber.i("download url $urlString") - return DefaultHttpClient().download(HttpRequest(toString()), dest) - .flatMap { - try { - if (BuildConfig.DEBUG) Timber.i("response url ${it.url}") - if (BuildConfig.DEBUG) Timber.i("download destination ${dest.path}") - if (urlString == it.url) { - Try.success(Unit) - } else { - URL(it.url).downloadTo(dest, maxRedirections - 1) - } - } catch (e: Exception) { - Try.failure(e) - } - } -} - -private suspend fun HttpClient.download( - request: HttpRequest, - destination: File, -): HttpTry<HttpResponse> = - try { - stream(request).flatMap { res -> - withContext(Dispatchers.IO) { - res.body.use { input -> - FileOutputStream(destination).use { output -> - val buf = ByteArray(1024 * 8) - var n: Int - var downloadedBytes = 0 - while (-1 != input.read(buf).also { n = it }) { - ensureActive() - downloadedBytes += n - output.write(buf, 0, n) - } - } - } - var response = res.response - if (response.mediaType.matches(MediaType.BINARY)) { - response = response.copy( - mediaType = MediaType.ofFile(destination) ?: response.mediaType - ) - } - Try.success(response) - } - } - } catch (e: Exception) { - Try.failure(HttpException.wrap(e)) - } diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/InputStream.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/InputStream.kt index 494396f5be..3fe0ba21aa 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/InputStream.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/InputStream.kt @@ -15,7 +15,7 @@ import java.io.InputStream import java.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.readium.r2.shared.extensions.tryOrNull +import org.readium.r2.testapp.utils.tryOrNull suspend fun InputStream.toFile(file: File) { withContext(Dispatchers.IO) { diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Intent.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Intent.kt new file mode 100644 index 0000000000..398e2796e6 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Intent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.utils.extensions + +import android.content.Context +import android.content.Intent +import androidx.core.app.ShareCompat + +/** + * Start a new activity to share the given plain [text] to other applications. + */ +fun createShareIntent( + launchingContext: Context, + text: String, + title: String? = null +): Intent { + val intent = + ShareCompat.IntentBuilder(launchingContext) + .setType("text/plain") + .setText(text) + .intent + .setAction(Intent.ACTION_SEND) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + return Intent.createChooser(intent, title) +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Link.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Link.kt deleted file mode 100644 index 3d1ac22f22..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Link.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.readium.r2.testapp.utils.extensions - -import org.readium.r2.shared.publication.Link - -val Link.outlineTitle: String - get() = title ?: href diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Metadata.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Metadata.kt deleted file mode 100644 index c6b241233b..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Metadata.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Module: r2-testapp-kotlin - * Developers: Mickaël Menu - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.testapp.utils.extensions - -import org.readium.r2.shared.publication.Metadata - -val Metadata.authorName: String get() = - authors.firstOrNull()?.name ?: "" diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Number.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Number.kt new file mode 100644 index 0000000000..467f90c359 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Number.kt @@ -0,0 +1,9 @@ +package org.readium.r2.testapp.utils.extensions + +import java.text.NumberFormat + +fun Number.formatPercentage(maximumFractionDigits: Int = 0): String { + val format = NumberFormat.getPercentInstance() + format.maximumFractionDigits = maximumFractionDigits + return format.format(this) +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/URL.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/URL.kt deleted file mode 100644 index 1c22d80cb0..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/URL.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* Module: r2-testapp-kotlin -* Developers: Quentin Gliosca -* -* Copyright (c) 2020. European Digital Reading Lab. All rights reserved. -* Licensed to the Readium Foundation under one or more contributor license agreements. -* Use of this source code is governed by a BSD-style license which is detailed in the -* LICENSE file present in the project repository where this source code is maintained. -*/ - -package org.readium.r2.testapp.utils.extensions - -import java.io.File -import java.io.FileOutputStream -import java.net.URL -import java.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.readium.r2.shared.extensions.extension -import org.readium.r2.shared.extensions.tryOr -import org.readium.r2.shared.extensions.tryOrNull - -suspend fun URL.download(path: String): File? = tryOr(null) { - val file = File(path) - withContext(Dispatchers.IO) { - openStream().use { input -> - FileOutputStream(file).use { output -> - input.copyTo(output) - } - } - } - file -} - -suspend fun URL.copyToTempFile(dir: String): File? = tryOrNull { - val filename = UUID.randomUUID().toString() - val path = "$dir$filename.$extension" - download(path) -} diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Uri.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Uri.kt index 573701b231..b9636881cf 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Uri.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/Uri.kt @@ -6,21 +6,46 @@ package org.readium.r2.testapp.utils.extensions +import android.content.ContentResolver import android.content.Context import android.net.Uri +import android.provider.MediaStore import java.io.File import java.util.* import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.testapp.utils.ContentResolverUtil +import org.readium.r2.testapp.utils.tryOrNull suspend fun Uri.copyToTempFile(context: Context, dir: File): Try<File, Exception> = try { val filename = UUID.randomUUID().toString() - val mediaType = MediaType.ofUri(this, context.contentResolver) - val file = File(dir, "$filename.${mediaType?.fileExtension ?: "tmp"}") + val file = File(dir, "$filename.${extension(context)}") ContentResolverUtil.getContentInputStream(context, this, file) Try.success(file) } catch (e: Exception) { Try.failure(e) } + +private fun Uri.extension(context: Context): String? { + if (scheme == ContentResolver.SCHEME_CONTENT) { + tryOrNull { + context.contentResolver.queryProjection(this, MediaStore.MediaColumns.DISPLAY_NAME) + ?.let { filename -> + File(filename).extension + .takeUnless { it.isBlank() } + } + }?.let { return it } + } + + return path?.let { File(it).extension } +} + +private fun ContentResolver.queryProjection(uri: Uri, projection: String): String? = + tryOrNull<String?> { + query(uri, arrayOf(projection), null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + return cursor.getString(0) + } + return null + } + } diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/LinkExt.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/LinkExt.kt new file mode 100644 index 0000000000..ac6357b39c --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/LinkExt.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.utils.extensions.readium + +import org.readium.r2.shared.publication.Link + +val Link.outlineTitle: String + get() = title ?: href.toString() diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/MetadataExt.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/MetadataExt.kt new file mode 100644 index 0000000000..140baa700d --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/MetadataExt.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.utils.extensions.readium + +import org.readium.r2.shared.publication.Metadata + +val Metadata.authorName: String get() = + authors.firstOrNull()?.name ?: "" diff --git a/test-app/src/main/res/drawable/cnl.png b/test-app/src/main/res/drawable/cnl.png deleted file mode 100644 index 6dfe3affed..0000000000 Binary files a/test-app/src/main/res/drawable/cnl.png and /dev/null differ diff --git a/test-app/src/main/res/drawable/ic_baseline_fast_forward_24.xml b/test-app/src/main/res/drawable/ic_baseline_fast_forward_24.xml deleted file mode 100644 index e3f30c6c70..0000000000 --- a/test-app/src/main/res/drawable/ic_baseline_fast_forward_24.xml +++ /dev/null @@ -1,10 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> - <path - android:fillColor="@android:color/white" - android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z"/> -</vector> diff --git a/test-app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml b/test-app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml deleted file mode 100644 index 81f79b3226..0000000000 --- a/test-app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml +++ /dev/null @@ -1,10 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> - <path - android:fillColor="@android:color/white" - android:pathData="M11,18L11,6l-8.5,6 8.5,6zM11.5,12l8.5,6L20,6l-8.5,6z"/> -</vector> diff --git a/test-app/src/main/res/drawable/ic_baseline_skip_next_24.xml b/test-app/src/main/res/drawable/ic_baseline_skip_next_24.xml deleted file mode 100644 index 4fff2475bf..0000000000 --- a/test-app/src/main/res/drawable/ic_baseline_skip_next_24.xml +++ /dev/null @@ -1,10 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> - <path - android:fillColor="@android:color/white" - android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/> -</vector> diff --git a/test-app/src/main/res/drawable/ic_baseline_skip_previous_24.xml b/test-app/src/main/res/drawable/ic_baseline_skip_previous_24.xml deleted file mode 100644 index 1805b7d158..0000000000 --- a/test-app/src/main/res/drawable/ic_baseline_skip_previous_24.xml +++ /dev/null @@ -1,10 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> - <path - android:fillColor="@android:color/white" - android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z"/> -</vector> diff --git a/test-app/src/main/res/drawable/ic_dashboard_black_24dp.xml b/test-app/src/main/res/drawable/ic_dashboard_black_24dp.xml deleted file mode 100644 index cf9903c88c..0000000000 --- a/test-app/src/main/res/drawable/ic_dashboard_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> - <path - android:fillColor="#FF000000" - android:pathData="M3,13h8L11,3L3,3v10zM3,21h8v-6L3,15v6zM13,21h8L21,11h-8v10zM13,3v6h8L21,3h-8z" /> -</vector> \ No newline at end of file diff --git a/test-app/src/main/res/drawable/ic_info_black_24dp.xml b/test-app/src/main/res/drawable/ic_info_black_24dp.xml deleted file mode 100644 index 31ad0cbddd..0000000000 --- a/test-app/src/main/res/drawable/ic_info_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> - <path - android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" - android:fillColor="#000000"/> -</vector> \ No newline at end of file diff --git a/test-app/src/main/res/drawable/ic_outline_add_24.xml b/test-app/src/main/res/drawable/ic_outline_add_24.xml deleted file mode 100644 index fe04f24c34..0000000000 --- a/test-app/src/main/res/drawable/ic_outline_add_24.xml +++ /dev/null @@ -1,10 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> - <path - android:fillColor="@android:color/black" - android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> -</vector> diff --git a/test-app/src/main/res/drawable/ic_outline_format_align_justify_24.xml b/test-app/src/main/res/drawable/ic_outline_format_align_justify_24.xml deleted file mode 100644 index e58793292e..0000000000 --- a/test-app/src/main/res/drawable/ic_outline_format_align_justify_24.xml +++ /dev/null @@ -1,10 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> - <path - android:fillColor="@android:color/white" - android:pathData="M3,21h18v-2L3,19v2zM3,17h18v-2L3,15v2zM3,13h18v-2L3,11v2zM3,9h18L21,7L3,7v2zM3,3v2h18L21,3L3,3z"/> -</vector> diff --git a/test-app/src/main/res/drawable/ic_outline_format_align_left_24.xml b/test-app/src/main/res/drawable/ic_outline_format_align_left_24.xml deleted file mode 100644 index 4ff0c3acd7..0000000000 --- a/test-app/src/main/res/drawable/ic_outline_format_align_left_24.xml +++ /dev/null @@ -1,11 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal" - android:autoMirrored="true"> - <path - android:fillColor="@android:color/white" - android:pathData="M15,15L3,15v2h12v-2zM15,7L3,7v2h12L15,7zM3,13h18v-2L3,11v2zM3,21h18v-2L3,19v2zM3,3v2h18L21,3L3,3z"/> -</vector> diff --git a/test-app/src/main/res/drawable/ic_outline_light_mode_24.xml b/test-app/src/main/res/drawable/ic_outline_light_mode_24.xml deleted file mode 100644 index 5014eeaf31..0000000000 --- a/test-app/src/main/res/drawable/ic_outline_light_mode_24.xml +++ /dev/null @@ -1,10 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> - <path - android:fillColor="@android:color/black" - android:pathData="M12,9c1.65,0 3,1.35 3,3s-1.35,3 -3,3s-3,-1.35 -3,-3S10.35,9 12,9M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5S14.76,7 12,7L12,7zM2,13l2,0c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1l-2,0c-0.55,0 -1,0.45 -1,1S1.45,13 2,13zM20,13l2,0c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1l-2,0c-0.55,0 -1,0.45 -1,1S19.45,13 20,13zM11,2v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1V2c0,-0.55 -0.45,-1 -1,-1S11,1.45 11,2zM11,20v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1C11.45,19 11,19.45 11,20zM5.99,4.58c-0.39,-0.39 -1.03,-0.39 -1.41,0c-0.39,0.39 -0.39,1.03 0,1.41l1.06,1.06c0.39,0.39 1.03,0.39 1.41,0s0.39,-1.03 0,-1.41L5.99,4.58zM18.36,16.95c-0.39,-0.39 -1.03,-0.39 -1.41,0c-0.39,0.39 -0.39,1.03 0,1.41l1.06,1.06c0.39,0.39 1.03,0.39 1.41,0c0.39,-0.39 0.39,-1.03 0,-1.41L18.36,16.95zM19.42,5.99c0.39,-0.39 0.39,-1.03 0,-1.41c-0.39,-0.39 -1.03,-0.39 -1.41,0l-1.06,1.06c-0.39,0.39 -0.39,1.03 0,1.41s1.03,0.39 1.41,0L19.42,5.99zM7.05,18.36c0.39,-0.39 0.39,-1.03 0,-1.41c-0.39,-0.39 -1.03,-0.39 -1.41,0l-1.06,1.06c-0.39,0.39 -0.39,1.03 0,1.41s1.03,0.39 1.41,0L7.05,18.36z"/> -</vector> diff --git a/test-app/src/main/res/drawable/ic_outline_remove_24.xml b/test-app/src/main/res/drawable/ic_outline_remove_24.xml deleted file mode 100644 index 86894f7150..0000000000 --- a/test-app/src/main/res/drawable/ic_outline_remove_24.xml +++ /dev/null @@ -1,10 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> - <path - android:fillColor="@android:color/black" - android:pathData="M19,13H5v-2h14v2z"/> -</vector> diff --git a/test-app/src/main/res/drawable/ic_outline_wb_sunny_24.xml b/test-app/src/main/res/drawable/ic_outline_wb_sunny_24.xml deleted file mode 100644 index 7315059812..0000000000 --- a/test-app/src/main/res/drawable/ic_outline_wb_sunny_24.xml +++ /dev/null @@ -1,10 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> - <path - android:fillColor="@android:color/black" - android:pathData="M6.76,4.84l-1.8,-1.79 -1.41,1.41 1.79,1.79zM1,10.5h3v2L1,12.5zM11,0.55h2L13,3.5h-2zM19.04,3.045l1.408,1.407 -1.79,1.79 -1.407,-1.408zM17.24,18.16l1.79,1.8 1.41,-1.41 -1.8,-1.79zM20,10.5h3v2h-3zM12,5.5c-3.31,0 -6,2.69 -6,6s2.69,6 6,6 6,-2.69 6,-6 -2.69,-6 -6,-6zM12,15.5c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4zM11,19.5h2v2.95h-2zM3.55,18.54l1.41,1.41 1.79,-1.8 -1.41,-1.41z"/> -</vector> diff --git a/test-app/src/main/res/drawable/icon_font_decrease.png b/test-app/src/main/res/drawable/icon_font_decrease.png deleted file mode 100644 index 7330622b23..0000000000 Binary files a/test-app/src/main/res/drawable/icon_font_decrease.png and /dev/null differ diff --git a/test-app/src/main/res/drawable/icon_font_increase.png b/test-app/src/main/res/drawable/icon_font_increase.png deleted file mode 100644 index 5192b45195..0000000000 Binary files a/test-app/src/main/res/drawable/icon_font_increase.png and /dev/null differ diff --git a/test-app/src/main/res/drawable/rbtn_selector.xml b/test-app/src/main/res/drawable/rbtn_selector.xml deleted file mode 100644 index f6f2850aa8..0000000000 --- a/test-app/src/main/res/drawable/rbtn_selector.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_checked="true"> - <shape> - <solid android:color="@color/colorPrimaryDark" /> - <stroke android:width="1dp" android:color="@color/colorPrimaryDark" /> - </shape> - - </item> - <item android:state_checked="false"> - <shape android:shape="rectangle"> - <solid android:color="#ffffff" /> - <stroke android:width="1dp" android:color="@color/colorPrimaryDark" /> - </shape> - </item> -</selector> \ No newline at end of file diff --git a/test-app/src/main/res/drawable/rbtn_textcolor_selector.xml b/test-app/src/main/res/drawable/rbtn_textcolor_selector.xml deleted file mode 100644 index c0c7414c16..0000000000 --- a/test-app/src/main/res/drawable/rbtn_textcolor_selector.xml +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - - <item android:color="#ffffffff" android:state_checked="true" /> - <item android:color="@color/colorPrimaryDark" /> - -</selector> \ No newline at end of file diff --git a/test-app/src/main/res/layout/activity_epub.xml b/test-app/src/main/res/layout/activity_epub.xml deleted file mode 100644 index 7d5f6b89b9..0000000000 --- a/test-app/src/main/res/layout/activity_epub.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - -<androidx.constraintlayout.widget.ConstraintLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <androidx.fragment.app.FragmentContainerView - android:id="@+id/main_content" - android:tag="@string/epub_navigator_tag" - android:name="org.readium.r2.navigator.epub.EpubNavigatorFragment" - android:layout_width="match_parent" - android:layout_height="match_parent" - /> - -</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/test-app/src/main/res/layout/filter_row.xml b/test-app/src/main/res/layout/filter_row.xml deleted file mode 100644 index 8578df7254..0000000000 --- a/test-app/src/main/res/layout/filter_row.xml +++ /dev/null @@ -1,34 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:padding="10dp"> - - <TextView - android:id="@+id/text" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_weight="1" - android:gravity="start" - android:textAlignment="viewStart" /> - - <TextView - android:id="@+id/count" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_weight="1" - android:gravity="end" - android:textAlignment="viewEnd" /> - -</LinearLayout> diff --git a/test-app/src/main/res/layout/filter_window.xml b/test-app/src/main/res/layout/filter_window.xml deleted file mode 100644 index 1249195c19..0000000000 --- a/test-app/src/main/res/layout/filter_window.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@android:color/white"> - - <ListView - android:id="@+id/facetList" - android:layout_width="match_parent" - android:layout_height="match_parent" /> - -</androidx.constraintlayout.widget.ConstraintLayout> - - diff --git a/test-app/src/main/res/layout/fragment_listview.xml b/test-app/src/main/res/layout/fragment_listview.xml index 38c1cb18fd..6d1a73c315 100644 --- a/test-app/src/main/res/layout/fragment_listview.xml +++ b/test-app/src/main/res/layout/fragment_listview.xml @@ -1,12 +1,9 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann, Mostapha Idoubihi, Paul Stoica - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" diff --git a/test-app/src/main/res/layout/fragment_outline.xml b/test-app/src/main/res/layout/fragment_outline.xml index 6626b3ee4c..7c5f94615e 100644 --- a/test-app/src/main/res/layout/fragment_outline.xml +++ b/test-app/src/main/res/layout/fragment_outline.xml @@ -1,12 +1,9 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann, Mostapha Idoubihi, Paul Stoica - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" diff --git a/test-app/src/main/res/layout/fragment_publication_detail.xml b/test-app/src/main/res/layout/fragment_publication_detail.xml index 8e3490034c..13151bd883 100644 --- a/test-app/src/main/res/layout/fragment_publication_detail.xml +++ b/test-app/src/main/res/layout/fragment_publication_detail.xml @@ -50,6 +50,5 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:background="@color/white" android:visibility="gone" /> </androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/test-app/src/main/res/layout/fragment_screen_reader.xml b/test-app/src/main/res/layout/fragment_screen_reader.xml deleted file mode 100644 index b14817457a..0000000000 --- a/test-app/src/main/res/layout/fragment_screen_reader.xml +++ /dev/null @@ -1,142 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ Copyright 2021 Readium Foundation. All rights reserved. - ~ Use of this source code is governed by the BSD-style license - ~ available in the top-level LICENSE file of the project. - --> - -<androidx.constraintlayout.widget.ConstraintLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/tts_overlay" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@color/colorAccent"> - - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/timeline" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="@color/colorPrimaryDark" - android:orientation="horizontal" - app:layout_constraintBottom_toTopOf="@+id/tts_textView" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> - - <TextView - android:id="@+id/titleView" - android:layout_width="0dp" - android:layout_height="40dp" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:layout_marginBottom="8dp" - android:ellipsize="end" - android:gravity="center" - android:maxLines="1" - android:scrollHorizontally="true" - android:text="@string/chapter" - android:textColor="@android:color/white" - android:textSize="18sp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - </androidx.constraintlayout.widget.ConstraintLayout> - - <TextView - android:id="@+id/tts_textView" - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_centerHorizontal="true" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:layout_marginBottom="8dp" - android:adjustViewBounds="true" - android:cropToPadding="true" - android:gravity="center" - android:scaleType="fitXY" - android:textAlignment="center" - android:textColor="@color/colorPrimaryDark" - android:textSize="24sp" - android:textStyle="italic" - - app:layout_constraintBottom_toTopOf="@+id/controls" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/timeline" /> - - <LinearLayout - android:id="@+id/controls" - android:layout_width="0dp" - android:layout_height="70dp" - android:background="@color/colorPrimaryDark" - android:gravity="center_vertical|center_horizontal" - android:orientation="horizontal" - android:weightSum="5" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent"> - - - <ImageButton - android:id="@+id/prev_chapter" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_weight="1" - android:background="@android:color/transparent" - android:contentDescription="@string/previous_chapter" - android:scaleType="fitCenter" - android:src="@drawable/ic_baseline_skip_previous_24" - app:tint="@android:color/white" /> - - <ImageButton - android:id="@+id/fast_back" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_weight="1" - android:background="@android:color/transparent" - android:contentDescription="@string/previous_sentence" - android:scaleType="fitCenter" - android:src="@drawable/ic_baseline_fast_rewind_24" - app:tint="@android:color/white" /> - - - <ImageButton - android:id="@+id/play_pause" - android:layout_width="50dp" - android:layout_height="50dp" - android:layout_weight="1" - android:background="@android:color/transparent" - android:contentDescription="@string/play_and_pause" - android:scaleType="fitCenter" - android:src="@drawable/ic_baseline_play_arrow_24" - app:tint="@android:color/white" /> - - <ImageButton - android:id="@+id/fast_forward" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_weight="1" - android:background="@android:color/transparent" - android:contentDescription="@string/next_sentence" - android:scaleType="fitCenter" - android:src="@drawable/ic_baseline_fast_forward_24" - app:tint="@android:color/white" /> - - <ImageButton - android:id="@+id/next_chapter" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_weight="1" - android:background="@android:color/transparent" - android:contentDescription="@string/next_chapter" - android:scaleType="fitCenter" - android:src="@drawable/ic_baseline_skip_next_24" - app:tint="@android:color/white" /> - - - </LinearLayout> - -</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/test-app/src/main/res/layout/fragment_search.xml b/test-app/src/main/res/layout/fragment_search.xml index e74690ac3f..125aa93c46 100644 --- a/test-app/src/main/res/layout/fragment_search.xml +++ b/test-app/src/main/res/layout/fragment_search.xml @@ -1,11 +1,9 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ Module: r2-navigator-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" @@ -13,13 +11,12 @@ android:id="@+id/search_overlay" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/colorAccent"> + > <androidx.recyclerview.widget.RecyclerView android:id="@+id/search_recyclerView" android:layout_width="0dp" android:layout_height="0dp" - android:background="@android:color/white" app:layout_constraintBottom_toBottomOf="@id/search_overlay" app:layout_constraintEnd_toEndOf="@id/search_overlay" app:layout_constraintStart_toStartOf="@id/search_overlay" diff --git a/test-app/src/main/res/layout/item_recycle_book.xml b/test-app/src/main/res/layout/item_recycle_book.xml index 2e73da05cd..58e8caa273 100644 --- a/test-app/src/main/res/layout/item_recycle_book.xml +++ b/test-app/src/main/res/layout/item_recycle_book.xml @@ -1,12 +1,8 @@ <!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" diff --git a/test-app/src/main/res/layout/item_recycle_bookmark.xml b/test-app/src/main/res/layout/item_recycle_bookmark.xml index 8de452bea0..1f4e96bb1e 100644 --- a/test-app/src/main/res/layout/item_recycle_bookmark.xml +++ b/test-app/src/main/res/layout/item_recycle_bookmark.xml @@ -1,12 +1,8 @@ <!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann, Mostapha Idoubihi, Paul Stoica - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" diff --git a/test-app/src/main/res/layout/item_recycle_button.xml b/test-app/src/main/res/layout/item_recycle_button.xml index 313d9ad24c..48077f12e5 100644 --- a/test-app/src/main/res/layout/item_recycle_button.xml +++ b/test-app/src/main/res/layout/item_recycle_button.xml @@ -1,13 +1,8 @@ <!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/catalog_LinearLayout" diff --git a/test-app/src/main/res/layout/item_recycle_catalog.xml b/test-app/src/main/res/layout/item_recycle_catalog.xml index f58b8dcd23..d874b1ede0 100644 --- a/test-app/src/main/res/layout/item_recycle_catalog.xml +++ b/test-app/src/main/res/layout/item_recycle_catalog.xml @@ -1,12 +1,8 @@ <!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" diff --git a/test-app/src/main/res/layout/item_recycle_highlight.xml b/test-app/src/main/res/layout/item_recycle_highlight.xml index 972762d133..ee5be596fd 100644 --- a/test-app/src/main/res/layout/item_recycle_highlight.xml +++ b/test-app/src/main/res/layout/item_recycle_highlight.xml @@ -1,12 +1,8 @@ <!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann, Mostapha Idoubihi, Paul Stoica - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" diff --git a/test-app/src/main/res/layout/item_recycle_navigation.xml b/test-app/src/main/res/layout/item_recycle_navigation.xml index a214d867be..b5cf15709b 100644 --- a/test-app/src/main/res/layout/item_recycle_navigation.xml +++ b/test-app/src/main/res/layout/item_recycle_navigation.xml @@ -1,13 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Paul Stoica - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" diff --git a/test-app/src/main/res/layout/item_spinner_days.xml b/test-app/src/main/res/layout/item_spinner_days.xml deleted file mode 100644 index c7cbc57927..0000000000 --- a/test-app/src/main/res/layout/item_spinner_days.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Paul Stoica - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - - -<TextView xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/days_spinner" - style="?android:attr/spinnerDropDownItemStyle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textAlignment="textEnd" - android:textSize="9pt" /> diff --git a/test-app/src/main/res/layout/popup_delete.xml b/test-app/src/main/res/layout/popup_delete.xml deleted file mode 100644 index ffff676268..0000000000 --- a/test-app/src/main/res/layout/popup_delete.xml +++ /dev/null @@ -1,29 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="200dp" - android:layout_height="match_parent" - android:background="@android:color/background_light" - android:gravity="end" - android:orientation="horizontal" - android:padding="5dp"> - - <Button - android:id="@+id/delete" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/popup_delete_button" /> - - -</LinearLayout> - diff --git a/test-app/src/main/res/layout/popup_passphrase.xml b/test-app/src/main/res/layout/popup_passphrase.xml deleted file mode 100644 index a3f13162c3..0000000000 --- a/test-app/src/main/res/layout/popup_passphrase.xml +++ /dev/null @@ -1,126 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@android:color/background_light"> - - <androidx.constraintlayout.widget.ConstraintLayout - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <Button - android:id="@+id/cancel_button" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:text="cancel" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <TextView - android:id="@+id/title" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="12dp" - - android:layout_marginTop="22dp" - android:layout_marginEnd="8dp" - android:text="title" - android:textSize="18sp" - android:textStyle="bold" - app:layout_constraintEnd_toStartOf="@+id/cancel_button" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <TextView - android:id="@+id/description" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="12dp" - android:layout_marginTop="32dp" - android:layout_marginEnd="12dp" - android:text="description" - android:textSize="16sp" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/title" /> - - <TextView - android:id="@+id/hint" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="12dp" - android:layout_marginTop="32dp" - android:layout_marginEnd="12dp" - android:text="hint" - android:textSize="18sp" - android:textStyle="bold" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/description" /> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/passwordLayout" - android:layout_width="395dp" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/hint"> - - <com.google.android.material.textfield.TextInputEditText - android:id="@+id/password" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:hint="passphrase" /> - </com.google.android.material.textfield.TextInputLayout> - - <Button - android:id="@+id/confirm_button" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:text="Continue" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/passwordLayout" /> - - <Button - android:id="@+id/forgot_link" - android:layout_width="0dp" - android:layout_height="20dp" - android:layout_marginStart="8dp" - android:layout_marginTop="16dp" - android:layout_marginEnd="8dp" - android:background="@android:color/transparent" - android:text="Forgot your passphrase?" - android:textAlignment="viewStart" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/confirm_button" /> - - <Button - android:id="@+id/help_link" - android:layout_width="0dp" - android:layout_height="20dp" - android:layout_marginStart="8dp" - android:layout_marginTop="16dp" - android:layout_marginEnd="8dp" - android:background="@android:color/transparent" - android:text="Need more help?" - android:textAlignment="viewStart" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHorizontal_bias="0.0" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/forgot_link" /> - - - </androidx.constraintlayout.widget.ConstraintLayout> - -</ScrollView> \ No newline at end of file diff --git a/test-app/src/main/res/layout/section_header.xml b/test-app/src/main/res/layout/section_header.xml index 5d2d69ee3d..93b05d379a 100644 --- a/test-app/src/main/res/layout/section_header.xml +++ b/test-app/src/main/res/layout/section_header.xml @@ -1,19 +1,16 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/header" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/colorAccent" + android:background="?attr/colorPrimarySurface" + android:textColor="?attr/colorOnPrimarySurface" android:orientation="vertical" android:ellipsize="end" android:maxLines="3" diff --git a/test-app/src/main/res/menu/menu_bookmark.xml b/test-app/src/main/res/menu/menu_bookmark.xml index 205d3fcab9..ce48444b67 100644 --- a/test-app/src/main/res/menu/menu_bookmark.xml +++ b/test-app/src/main/res/menu/menu_bookmark.xml @@ -1,12 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi - ~ - ~ Copyright (c) 2018. Readium Foundation. All rights reserved. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <menu xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/test-app/src/main/res/menu/menu_filter.xml b/test-app/src/main/res/menu/menu_filter.xml deleted file mode 100644 index fa01b16d66..0000000000 --- a/test-app/src/main/res/menu/menu_filter.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto"> - - <item - android:id="@+id/filter" - android:title="@string/menu_item_filter" - app:showAsAction="always" /> - - -</menu> \ No newline at end of file diff --git a/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 81de1c3623..729f828c86 100644 --- a/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,13 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <background android:drawable="@mipmap/ic_launcher_background" /> diff --git a/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 81de1c3623..0000000000 --- a/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - -<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> - <background android:drawable="@mipmap/ic_launcher_background" /> - <foreground android:drawable="@mipmap/ic_launcher_foreground" /> -</adaptive-icon> \ No newline at end of file diff --git a/test-app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/test-app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index a8fa77d215..0000000000 Binary files a/test-app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/test-app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/test-app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index cd237502d0..0000000000 Binary files a/test-app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/test-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/test-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index f6d49bbf04..0000000000 Binary files a/test-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/test-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/test-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index cc955418f4..0000000000 Binary files a/test-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/test-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/test-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index ce7b6208a7..0000000000 Binary files a/test-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/test-app/src/main/res/values/arrays.xml b/test-app/src/main/res/values/arrays.xml index fc15ea0ae4..aeb66d5a55 100644 --- a/test-app/src/main/res/values/arrays.xml +++ b/test-app/src/main/res/values/arrays.xml @@ -1,18 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <string-array name="documentSelectorArray"> - <item>Select from Device</item> - <item>Enter a URL</item> + <item>Import to app storage</item> + <item>Read from shared storage</item> + <item>Stream from the Web</item> </string-array> - <string-array name="font_list"> - <item>Original</item> - <item>PT Serif</item> - <item>Roboto</item> - <item>Source Sans Pro</item> - <item>Vollkorn</item> - <item>OpenDyslexic</item> - <item>AccessibleDfA</item> - <item>IA Writer Duospace</item> - </string-array> </resources> \ No newline at end of file diff --git a/test-app/src/main/res/values/colors.xml b/test-app/src/main/res/values/colors.xml deleted file mode 100644 index 04355ce52f..0000000000 --- a/test-app/src/main/res/values/colors.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> - -<resources> - <color name="white">#ffffff</color> -</resources> diff --git a/test-app/src/main/res/values/refs.xml b/test-app/src/main/res/values/refs.xml deleted file mode 100644 index 8c9763314a..0000000000 --- a/test-app/src/main/res/values/refs.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <item type="layout" name="activity_r2_epub">@layout/activity_reader</item> - <item type="layout" name="activity_r2_audiobook">@layout/activity_reader</item> -</resources> \ No newline at end of file diff --git a/test-app/src/main/res/values/strings.xml b/test-app/src/main/res/values/strings.xml index 6a4bbec932..7a6472b679 100644 --- a/test-app/src/main/res/values/strings.xml +++ b/test-app/src/main/res/values/strings.xml @@ -1,58 +1,20 @@ <!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <resources> - <string name="app_name" translatable="false">R2 Reader</string> - <string name="menu_item_show_epub_list">Show List</string> - <string name="menu_item_add_epub">Add ePub</string> - <string name="menu_item_filter">Filter</string> - <string name="progress_wait_while_searching_book">Please wait.</string> - <string name="progress_wait">Please wait.</string> - <string name="progress_wait_while_downloading_book">Please wait while your publication is being downloaded.</string> - <string name="progress_wait_while_preparing_book">Please wait while your publication is being prepared.</string> - <string name="progress_wait_while_preparing_audiobook">Please wait while your audiobook is being prepared.</string> - <string name="progress_wait_while_loading_feed">Please wait while your feed is loading.</string> - <string name="error_invalid_epub">Invalid Epub</string> - <string name="fallback_no_description">No description</string> - <string name="button_dismiss">DISMISS</string> - <string name="epubactivity_drm_info">DRM Info</string> <string name="epubactivity_set_bookmark">Add a bookmark</string> <string name="epubactivity_settings">User Settings</string> <string name="epubactivity_book_nav_options">Book navigation menu</string> <string name="epubactivity_read_aloud_start">Read Aloud</string> - <string name="epubactivity_read_aloud_stop">Stop Read Aloud</string> <string name="epubactivity_search">Search</string> - <string name="epubactivity_accessibility">Accessibility</string> - <string name="epubactivity_accessibility_screen_reader_start">Start Screen Reader</string> - <string name="epubactivity_accessibility_screen_reader_pause">Pause Screen Reader</string> - <string name="epubactivity_accessibility_screen_reader_resume">Resume Screen Reader</string> - <string name="epubactivity_accessibility_screen_reader_stop">Stop Screen Reader</string> - - <string name="title_activity_opdscatalog">OPDS Catalog</string> - <string name="action_settings">Settings</string> - <string name="title_activity_r2_about">About R2 Reader</string> - <string name="title_activity_opds_list">OPDS Feeds</string> - <string name="title_activity_opds_detail">Detail</string> <string name="title_fragment_drm_management">DRM Management</string> - <string name="title_activity_epub">Detail</string> - <string name="floating_button_add_book">Add a Book</string> - <string name="floating_button_add_feed">Add a OPDS Feed</string> - <string name="about_r2_reader">About R2 Reader</string> <string name="more_options">More Options</string> - <string name="opds_feeds">OPDS Feeds</string> <string name="popup_delete_button">DELETE</string> - <string name="add_publication_to_library">Add a publication to your library</string> - <string name="select_from_your_device">select from your device</string> - <string name="download_from_url">download from a url</string> <string name="app_version_header">Version</string> <string name="app_version_label">App Version:</string> @@ -70,42 +32,8 @@ <string name="catalog_detail_download_button">Download</string> - <string name="menu_label_drm">DRM</string> - <string name="menu_label_settings">Settings</string> - <string name="menu_label_toc">TOC</string> - <string name="usersettings_label_font">Font</string> - <string name="usersettings_appearance_default">Default</string> - <string name="usersettings_appearance_sepia">Sepia</string> - <string name="usersettings_appearance_night">Night</string> - <string name="usersettings_label_scroll_mode">Scroll mode</string> - <string name="usersettings_publisher_default">"Publisher's Default"</string> - <string name="usersettings_publisher_default_accessibility">"Publisher's Default Settings"</string> - <string name="usersettings_label_columns">Columns</string> - <string name="usersettings_auto">Auto</string> - <string name="usersettings_column_one">1</string> - <string name="usersettings_column_two">2</string> - <string name="usersettings_label_page_margins">Page Margins</string> - <string name="usersettings_page_margin_default">0.75</string> - <string name="usersettings_label_word_spacing">Word Spacing</string> - <string name="usersettings_label_letter_spacing">Letter Spacing</string> - <string name="usersettings_label_line_height">Line Height</string> - <string name="usersettings_label_TTS_speech_speed">TTS speed</string> - <!-- Accessibility --> - <string name="usersettings_description_TTS_speech_speed">Set TTS speech speed</string> - <string name="usersettings_description_brightness">Set Brightness</string> - <string name="usersettings_description_font_increase">Increase Font Size</string> - <string name="usersettings_description_font_decrease">Decrease Font Size</string> - <string name="usersettings_description_pm_increase">Increase Page Margins Size</string> - <string name="usersettings_description_pm_decrease">Decrease Page Margins Size</string> - <string name="usersettings_description_ws_increase">Increase Word Spacing</string> - <string name="usersettings_description_ws_decrease">Decrease Word Spacing</string> - <string name="usersettings_description_ls_increase">Increase Letter Spacing</string> - <string name="usersettings_description_ls_decrease">Decrease Letter Spacing</string> - <string name="usersettings_description_lh_increase">Increase Line Height</string> - <string name="usersettings_description_lh_decrease">Decrease Line Height</string> - <string name="drm_information_header">INFORMATION</string> <string name="drm_label_license_type">License Type</string> <string name="drm_label_state">State</string> @@ -117,12 +45,10 @@ <string name="drm_label_copies_left">Copies left</string> <string name="drm_label_start">Start</string> <string name="drm_label_end">End</string> - <string name="drm_label_potential_right_end">Potential right end</string> <string name="drm_label_actions">ACTIONS</string> <string name="drm_label_renew">RENEW</string> <string name="drm_label_return">RETURN</string> <string name="zero">00:00</string> - <string name="chapter">Chapter</string> <string name="chapter_page">Chapter / Page</string> <string name="progression">Progression</string> <string name="timestamp">Timestamp</string> @@ -145,6 +71,9 @@ <string name="cover_image">Cover Image</string> <string name="repfr">repfr</string> + <string name="details">Details</string> + <string name="close">Close</string> + <string name="share">Share</string> <string name="delete">Delete</string> <string name="cancel">Cancel</string> <string name="confirm_delete_book_title">Delete Book?</string> @@ -162,24 +91,87 @@ <string name="invalid_url">Please enter a valid URL</string> <string name="catalog_parse_error">Error parsing Catalog feed. Please check the URL and try again</string> - <string name="publication_image_description">Cover image for %1$s</string> - <string name="return_publication">This will return the publication</string> <string name="return_button">Return</string> - <string name="end_of_chapter">End of chapter</string> + <string name="opening_publication_cannot_render">Cannot render publication.</string> + <string name="opening_publication_audio_engine_initialization">Could not open publication because audio engine initialization failed.</string> + + <string name="import_publication_unexpected_io_exception">Unable to add publication due to an unexpected error on your device</string> + <string name="import_publication_download_failed">Publication download failed.</string> + <string name="import_publication_no_acquisition">Acquisition is not possible.</string> - <string name="unexpected_io_exception">Unable to add publication due to an unexpected error on your device</string> <string name="import_publication_success">Publication added to your library</string> - <string name="unable_add_pub_database">Unable to add publication to the database</string> + <string name="import_publication_unable_add_pub_database">Unable to add publication to the database</string> + + <string name="publication_error_unsupported_asset">Asset format is not supported</string> + <string name="publication_error_network_forbidden">Server denied access to a resource.</string> + <string name="publication_error_network_not_found">A resource has not been found on the server.</string> + <string name="publication_error_network_unreachable">Server cannot be reached.</string> + <string name="publication_error_network_timeout">Server was too long to respond.</string> + <string name="publication_error_network_ssl_handshake">A SSL error occurred.</string> + <string name="publication_error_network_unexpected">An unexpected network error occurred.</string> + <string name="publication_error_filesystem_not_found">A file has not been found.</string> + <string name="publication_error_filesystem_unexpected">An unexpected filesystem error occurred.</string> + <string name="publication_error_filesystem_insufficient_space">There is not enough space left on the device.</string> + <string name="publication_error_incorrect_credentials">Provided credentials were incorrect</string> + <string name="publication_error_restricted">You are not allowed to open this publication</string> + <string name="publication_error_out_of_memory">There is not enough memory on this device to open the publication.</string> + <string name="publication_error_scheme_not_supported">Publication source is not supported.</string> + <string name="publication_error_invalid_publication">Publication looks corrupted.</string> + <string name="publication_error_unexpected">An unexpected error occurred.</string> + + <!-- LcpError User Messages --> + + <string name="lcp_error_license_interaction_not_available">This interaction is not available</string> + <string name="lcp_error_license_profile_not_supported">This License has a profile identifier that this app cannot handle, the publication cannot be processed</string> + <string name="lcp_error_crl_fetching">Can\'t retrieve the Certificate Revocation List</string> + <string name="lcp_error_network">Network error</string> + <string name="lcp_error_runtime">Unexpected LCP error</string> + <string name="lcp_error_unknown">Unknown LCP error</string> + + <string name="lcp_error_license_status.cancelled">This license was cancelled on %1$s</string> + <string name="lcp_error_license_status.returned">This license has been returned on %1$s</string> + <string name="lcp_error_license_status.not_started">This license starts on %1$s</string> + <string name="lcp_error_license_status.expired">This license expired on %1$s</string> + <plurals name="lcp_error_license_status.revoked"> + <item quantity="one">This license was revoked by its provider on %1$s. It was registered by %2$d device.</item> + <item quantity="other">This license was revoked by its provider on %1$s. It was registered by %2$d devices.</item> + </plurals> + + <string name="lcp_error_renew.renew_failed">Your publication could not be renewed properly</string> + <string name="lcp_error_renew.invalid_renewal_period">Incorrect renewal period, your publication could not be renewed</string> + <string name="lcp_error_renew.unexpected_server_error">An unexpected error has occurred on the server</string> + + <string name="lcp_error_return.return_failed">Your publication could not be returned properly</string> + <string name="lcp_error_return.already_returned_or_expired">Your publication has already been returned before or is expired</string> + <string name="lcp_error_return.unexpected_server_error">An unexpected error has occurred on the server</string> + + <string name="lcp_error_parsing">The JSON is not representing a valid document</string> + <string name="lcp_error_parsing.malformed_json">The JSON is malformed and can\'t be parsed</string> + <string name="lcp_error_parsing.license_document">The JSON is not representing a valid License Document</string> + <string name="lcp_error_parsing.status_document">The JSON is not representing a valid Status Document</string> + + <string name="lcp_error_container.open_failed">Can\'t open the license container</string> + <string name="lcp_error_container.file_not_found">License not found in container</string> + <string name="lcp_error_container.read_failed">Can\'t read license from container</string> + <string name="lcp_error_container.write_failed">Can\'t write license in container</string> + + <string name="lcp_error_license_integrity.certificate_revoked">Certificate has been revoked in the CRL</string> + <string name="lcp_error_license_integrity.invalid_certificate_signature">Certificate has not been signed by CA</string> + <string name="lcp_error_license_integrity.invalid_license_signature_date">License has been issued by an expired certificate</string> + <string name="lcp_error_license_integrity.invalid_license_signature">License signature does not match</string> + <string name="lcp_error_license_integrity.invalid_user_key_check">User key check invalid</string> + + <string name="lcp_error_decryption.content_key_decrypt_error">Unable to decrypt encrypted content key from user key</string> + <string name="lcp_error_decryption.content_decrypt_error">Unable to decrypt encrypted content from content key</string> + + <string name="missing_lcp_support">The application was compiled without LCP support.</string> + <string name="reader_error">Error</string> + <string name="opening_error">Could not open publication</string> + <string name="import_error">Import error</string> + <string name="failed_parsing_catalog">Failed parsing Catalog</string> - <string name="next_chapter">Next chapter</string> - <string name="next_sentence">Next sentence</string> - <string name="play_and_pause">Play and pause</string> - <string name="previous_sentence">Previous sentence</string> - <string name="previous_chapter">Previous chapter</string> - <string name="brightness_lower">Brightness lower</string> - <string name="brightness_higher">Brightness higher</string> <string name="no_result">No result</string> <string name="note">Note</string> <string name="add_a_note">Add a note</string> @@ -194,13 +186,8 @@ <string name="go_backward_30_seconds">Go backward 30 seconds</string> <string name="play_or_pause">Play or pause</string> <string name="go_forward_30_seconds">Go forward 30 seconds</string> - <string name="select_a_font">Select a font</string> - <string name="close">Close</string> - <string name="language">Language</string> - <string name="defaultValue">Default</string> <string name="tts_error_initialization">Failed to initialize the TTS engine</string> - <string name="tts_error_language_not_supported">The language %s is not supported</string> <string name="tts_error_language_support_incomplete">The language %s requires additional data. Do you want to download it?</string> <string name="tts_error_network">A networking error occurred</string> <string name="tts_error_other">A TTS error occurred</string> @@ -213,4 +200,11 @@ <string name="tts_settings">Speech settings</string> <string name="tts_stop">Stop</string> <string name="tts_voice">Voice</string> + + <string name="search_error_not_searchable">This publication is not searchable</string> + <string name="search_error_cancelled">The search was cancelled</string> + <string name="search_error_other">An error occurred while searching</string> + + + <string name="error_unexpected">An unexpected error occurred.</string> </resources> diff --git a/test-app/src/main/res/values/styles.xml b/test-app/src/main/res/values/styles.xml index 82d94d96a1..06cbb4f501 100644 --- a/test-app/src/main/res/values/styles.xml +++ b/test-app/src/main/res/values/styles.xml @@ -1,12 +1,8 @@ <!-- - ~ Module: r2-testapp-kotlin - ~ Developers: Aferdita Muriqi, Clément Baumann - ~ - ~ Copyright (c) 2018. European Digital Reading Lab. All rights reserved. - ~ Licensed to the Readium Foundation under one or more contributor license agreements. - ~ Use of this source code is governed by a BSD-style license which is detailed in the - ~ LICENSE file present in the project repository where this source code is maintained. - --> + Copyright 2023 Readium Foundation. All rights reserved. + Use of this source code is governed by the BSD-style license + available in the top-level LICENSE file of the project. +--> <resources> @@ -15,16 +11,4 @@ </style> - <style name="AppTheme.NoActionBar"> - <item name="windowActionBar">false</item> - <item name="windowNoTitle">true</item> - <item name="android:popupAnimationStyle">@null</item> - - </style> - - <style name="TabWidgetTheme" parent="AppTheme"> - <item name="android:textSize">10sp</item> - <item name="android:textAlignment">center</item> - </style> - </resources> \ No newline at end of file diff --git a/test-app/src/main/res/xml/network_security_config.xml b/test-app/src/main/res/xml/network_security_config.xml index 4de82b56d6..a16bcd2095 100644 --- a/test-app/src/main/res/xml/network_security_config.xml +++ b/test-app/src/main/res/xml/network_security_config.xml @@ -1,16 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config cleartextTrafficPermitted="true"> - - <!-- - Required with r2-navigator-kotlin - Used to serve a publication's resources from the local HTTP server - --> - <domain includeSubdomains="false">127.0.0.1</domain> - <domain includeSubdomains="false">localhost</domain> - <!-- - Required with r2-lcp-kotlin + Required with LCP support. The CRL is served from an HTTP server, so we need to explicitly allow clear-text traffic on this domain See https://github.com/readium/r2-lcp-kotlin/issues/59 @@ -29,10 +21,5 @@ <domain includeSubdomains="false">open.minitex.org</domain> <domain includeSubdomains="false">s3.amazonaws.com</domain> - <!-- - Required to stream the Flatland audiobook sample. - --> - <domain includeSubdomains="true">archive.org</domain> - </domain-config> </network-security-config>