diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4043b174a5..7d8f034cf3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -66,13 +66,11 @@ body: ```shell echo "macOS: `sw_vers -productVersion`" echo "platform: `uname -m`" - echo "carthage: `carthage version`" xcodebuild -version ``` placeholder: | macOS: 14.1.2 platform: arm64 - carthage: 0.39.1 Xcode 15.1 Build version 15C65 render: bash diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index fee815bcd0..319f92b156 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -5,16 +5,20 @@ on: branches: [ main, develop ] pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: platform: ${{ 'iOS Simulator' }} - device: ${{ 'iPhone SE (3rd generation)' }} + device: ${{ 'iPhone 16 Pro' }} commit_sha: ${{ github.sha }} - DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer jobs: build: name: Build - runs-on: macos-14 + runs-on: macos-15 if: ${{ !github.event.pull_request.draft }} env: scheme: ${{ 'Readium-Package' }} @@ -26,11 +30,14 @@ jobs: run: | brew update brew install xcodegen - - name: Check Carthage project + # Preload the list of simulator for xcodebuild. The workflow is flaky without it. + xcrun simctl list + - name: Check CocoaPods podspecs run: | - # Check that the Carthage project is up to date. - make carthage-project - git diff --exit-code Support/Carthage/Readium.xcodeproj + # Check that the podspecs are up to date. + make podspecs + git diff --exit-code Support/CocoaPods/ + if git ls-files --others --exclude-standard Support/CocoaPods/ | grep -q .; then echo "Untracked podspec files found. Run 'make podspecs' and commit the result."; exit 1; fi - name: Build run: | set -eo pipefail @@ -39,10 +46,32 @@ jobs: run: | set -eo pipefail xcodebuild test-without-building -scheme "$scheme" -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi + - name: Print Swift package versions + run: | + jq -r '.pins[] | "\(.identity): \(.state.version)"' Package.resolved + + # navigator-ui-tests: + # name: Navigator UI Tests + # runs-on: macos-15 + # if: ${{ !github.event.pull_request.draft }} + # steps: + # - name: Checkout + # uses: actions/checkout@v3 + # - name: Install dependencies + # run: | + # brew update + # brew install xcodegen + # # Preload the list of simulator for xcodebuild. The workflow is flaky without it. + # xcrun simctl list + # - name: Test + # run: | + # set -eo pipefail + # make navigator-ui-tests-project + # xcodebuild test -project Tests/NavigatorTests/UITests/NavigatorUITests.xcodeproj -scheme NavigatorTestHost -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi lint: name: Lint - runs-on: macos-14 + runs-on: macos-15 if: ${{ !github.event.pull_request.draft }} env: scripts: ${{ 'Sources/Navigator/EPUB/Scripts' }} @@ -76,7 +105,7 @@ jobs: int-dev: name: Integration (Local) - runs-on: macos-14 + runs-on: macos-15 if: ${{ !github.event.pull_request.draft }} defaults: run: @@ -89,6 +118,8 @@ jobs: run: | brew update brew install xcodegen + # Preload the list of simulator for xcodebuild. The workflow is flaky without it. + xcrun simctl list - name: Generate project run: make dev lcp=${{ secrets.LCP_URL_SPM }} - name: Build @@ -98,7 +129,7 @@ jobs: int-spm: name: Integration (Swift Package Manager) - runs-on: macos-14 + runs-on: macos-15 if: ${{ !github.event.pull_request.draft }} defaults: run: @@ -117,6 +148,8 @@ jobs: run: | brew update brew install xcodegen + # Preload the list of simulator for xcodebuild. The workflow is flaky without it. + xcrun simctl list - name: Generate project run: make spm lcp=${{ secrets.LCP_URL_SPM }} commit=$commit_sha - name: Build @@ -124,31 +157,4 @@ jobs: set -eo pipefail xcodebuild build -scheme TestApp -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi - int-carthage: - name: Integration (Carthage) - runs-on: macos-14 - if: ${{ !github.event.pull_request.draft && github.ref == 'refs/heads/main' }} - defaults: - run: - working-directory: TestApp - environment: LCP - steps: - - name: Checkout - uses: actions/checkout@v3 - # We can't use the current github.sha with pull_request event, because they will - # reference the merge commit which cannot be fetched with Carthage. - - name: Set commit SHA - if: github.event_name == 'pull_request' - run: | - echo "commit_sha=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_ENV" - - name: Install dependencies - run: | - brew update - brew install xcodegen - - name: Generate project - run: make carthage lcp=${{ secrets.LCP_URL_CARTHAGE }} commit=$commit_sha - - name: Build - run: | - set -eo pipefail - xcodebuild build -scheme TestApp -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000..7de4ca71b4 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,78 @@ +name: Documentation + +on: + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: write + +# Allow only one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build-and-deploy: + runs-on: macos-15 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Determine Version + id: versioning + run: | + git fetch --tags --force + VERSION=$(git describe --tag --match "[0-9]*" --abbrev=0) + echo "READIUM_VERSION=$VERSION" >> $GITHUB_OUTPUT + if [[ $GITHUB_REF == refs/tags/* ]]; then + echo "folder=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + else + echo "folder=latest" >> $GITHUB_OUTPUT + fi + + - name: Generate Documentation + run: | + chmod +x BuildTools/Scripts/generate-docs.sh + ./BuildTools/Scripts/generate-docs.sh ${{ steps.versioning.outputs.READIUM_VERSION }} + ./BuildTools/Scripts/generate-docs.sh latest + + - name: Setup Root Redirect + run: | + cat < docs-site/swift-toolkit/index.html + + + + + + + +

Redirecting to latest documentation...

+ + EOF + + - name: Deploy Versioned Folder 🚀 + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: docs-site/swift-toolkit/${{ steps.versioning.outputs.READIUM_VERSION }} + target-folder: ${{ steps.versioning.outputs.READIUM_VERSION }} + clean: true + + - name: Deploy Latest Folder 🚀 + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: docs-site/swift-toolkit/latest + target-folder: latest + clean: true + + - name: Deploy Root Redirect 🚀 + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: docs-site/swift-toolkit + target-folder: . + clean: false diff --git a/.gitignore b/.gitignore index 0c5fcb4e6e..b45a6002f6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,6 @@ .swiftpm/ Package.resolved -# Carthage -./Carthage/ -Cartfile.resolved - # Xcode ## User settings @@ -56,3 +52,10 @@ playground.xcworkspace ## IntelliJ out/ +## Claude Code +.claude +CLAUDE.md + +# DocC generation +.build-docs +docs-site diff --git a/.swiftformat b/.swiftformat index 7e6cb7635a..9e6980dda3 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,4 +1,3 @@ --swiftversion 5.6 ---exclude Carthage --header "//\n// Copyright {year} 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//" --stripunusedargs closure-only diff --git a/BuildTools/Empty.swift b/BuildTools/Empty.swift index e54e11f6a4..410a1329ce 100644 --- a/BuildTools/Empty.swift +++ b/BuildTools/Empty.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/BuildTools/Package.resolved b/BuildTools/Package.resolved index f85cfb22bb..79fa37f1a3 100644 --- a/BuildTools/Package.resolved +++ b/BuildTools/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/nicklockwood/SwiftFormat", "state": { "branch": null, - "revision": "7ff506897aa5bdaf94f077087a2025b9505da112", - "version": "0.51.9" + "revision": "22a472ced4c621a0e41b982a6f32dec868d09392", + "version": "0.59.1" } } ] diff --git a/BuildTools/Package.swift b/BuildTools/Package.swift index 35ed5b2c1e..87b47e52de 100644 --- a/BuildTools/Package.swift +++ b/BuildTools/Package.swift @@ -11,7 +11,13 @@ let package = Package( name: "BuildTools", platforms: [.macOS(.v10_11)], dependencies: [ - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.51.6"), + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.59.1"), ], - targets: [.target(name: "BuildTools", path: "")] + targets: [ + .target(name: "BuildTools", path: "", exclude: ["Sources"]), + .target( + name: "GeneratePodspecs", + path: "Sources/GeneratePodspecs" + ), + ] ) diff --git a/BuildTools/Scripts/convert-a11y-display-guide-localizations.js b/BuildTools/Scripts/convert-a11y-display-guide-localizations.js deleted file mode 100644 index 9628a342dc..0000000000 --- a/BuildTools/Scripts/convert-a11y-display-guide-localizations.js +++ /dev/null @@ -1,177 +0,0 @@ -/** - * Copyright 2025 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - * - * This script can be used to convert the localized files from https://github.com/w3c/publ-a11y-display-guide-localizations - * into other output formats for various platforms. - */ - -const fs = require('fs'); -const path = require('path'); -const [inputFolder, outputFormat, outputFolder, keyPrefix = ''] = process.argv.slice(2); - -/** - * Ends the script with the given error message. - */ -function fail(message) { - console.error(`Error: ${message}`); - process.exit(1); -} - -/** - * Converter for Apple localized strings. - */ -function convertApple(lang, version, keys, keyPrefix, write) { - let disclaimer = `DO NOT EDIT. File generated automatically from v${version} of the ${lang} JSON strings.`; - - let stringsOutput = `// ${disclaimer}\n\n`; - for (const [key, value] of Object.entries(keys)) { - stringsOutput += `"${keyPrefix}${key}" = "${value}";\n`; - } - let stringsFile = path.join(`Resources/${lang}.lproj`, 'W3CAccessibilityMetadataDisplayGuide.strings'); - write(stringsFile, stringsOutput); - - // Using the "base" language, we will generate a static list of string keys to validate them at compile time. - if (lang == 'en-US') { - writeSwiftExtensions(disclaimer, keys, keyPrefix, write); - } -} - -/** - * Generates a static list of string keys to validate them at compile time. - */ -function writeSwiftExtensions(disclaimer, keys, keyPrefix, write) { - let keysOutput = `// -// Copyright 2025 Readium Foundation. All rights reserved. -// Use of this source code is governed by the BSD-style license -// available in the top-level LICENSE file of the project. -// - -// ${disclaimer}\n\npublic extension AccessibilityDisplayString {\n` - let keysList = Object.keys(keys) - .filter((k) => !k.endsWith("-descriptive")) - .map((k) => removeSuffix(k, "-compact")); - for (const key of keysList) { - keysOutput += ` static let ${convertKebabToCamelCase(key)}: Self = "${keyPrefix}${key}"\n`; - } - keysOutput += "}\n" - write("Publication/Accessibility/AccessibilityDisplayString+Generated.swift", keysOutput); -} - -const converters = { - apple: convertApple -}; - -if (!inputFolder || !outputFormat || !outputFolder) { - console.error('Usage: node convert.js [key-prefix]'); - process.exit(1); -} - -const langFolder = path.join(inputFolder, 'lang'); -if (!fs.existsSync(langFolder)) { - fail(`the specified input folder does not contain a 'lang' directory`); -} - -const convert = converters[outputFormat]; -if (!convert) { - fail(`unrecognized output format: ${outputFormat}, try: ${Object.keys(converters).join(', ')}.`); -} - -fs.readdir(langFolder, (err, langDirs) => { - if (err) { - fail(`reading directory: ${err.message}`); - } - - langDirs.forEach(langDir => { - const langDirPath = path.join(langFolder, langDir); - - fs.readdir(langDirPath, (err, files) => { - if (err) { - fail(`reading language directory ${langDir}: ${err.message}`); - } - - files.forEach(file => { - const filePath = path.join(langDirPath, file); - if (path.extname(file) === '.json') { - fs.readFile(filePath, 'utf8', (err, data) => { - if (err) { - console.error(`Error reading file ${file}: ${err.message}`); - return; - } - - try { - const jsonData = JSON.parse(data); - const version = jsonData["metadata"]["version"]; - convert(langDir, version, parseJsonKeys(jsonData), keyPrefix, write); - } catch (err) { - fail(`parsing JSON from file ${file}: ${err.message}`); - } - }); - } - }); - }); - }); -}); - -/** - * Writes the given content to the file path relative to the outputFolder provided in the CLI arguments. - */ -function write(relativePath, content) { - const outputPath = path.join(outputFolder, relativePath); - const outputDir = path.dirname(outputPath); - - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - fs.writeFile(outputPath, content, 'utf8', err => { - if (err) { - fail(`writing file ${outputPath}: ${err.message}`); - } else { - console.log(`Wrote ${outputPath}`); - } - }); -} - -/** - * Collects the JSON translation keys. - */ -function parseJsonKeys(obj) { - const keys = {}; - for (const key in obj) { - if (key === 'metadata') continue; // Ignore the metadata key - if (typeof obj[key] === 'object') { - for (const subKey in obj[key]) { - if (typeof obj[key][subKey] === 'object') { - for (const innerKey in obj[key][subKey]) { - const fullKey = `${subKey}-${innerKey}`; - keys[fullKey] = obj[key][subKey][innerKey]; - } - } else { - keys[subKey] = obj[key][subKey]; - } - } - } - } - return keys; -} - -function convertKebabToCamelCase(string) { - return string - .split('-') - .map((word, index) => { - if (index === 0) { - return word; - } - return word.charAt(0).toUpperCase() + word.slice(1); - }) - .join(''); -} - -function removeSuffix(str, suffix) { - if (str.endsWith(suffix)) { - return str.slice(0, -suffix.length); - } - return str; -} \ No newline at end of file diff --git a/BuildTools/Scripts/convert-thorium-localizations.js b/BuildTools/Scripts/convert-thorium-localizations.js new file mode 100644 index 0000000000..3e05ec6241 --- /dev/null +++ b/BuildTools/Scripts/convert-thorium-localizations.js @@ -0,0 +1,412 @@ +/** + * Copyright 2026 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + * + * This script converts the localized files from https://github.com/edrlab/thorium-locales/ + * into Apple .strings format. + */ + +const fs = require('fs'); +const fsPromises = fs.promises; +const path = require('path'); +const { generateAccessibilityDisplayStringExtensions } = require('./generate-a11y-extensions'); + +/** + * Plural suffixes used in thorium-locales JSON files (underscore format). + */ +const PLURAL_SUFFIXES = ['_zero', '_one', '_two', '_few', '_many', '_other']; + +/** + * Languages to process from thorium-locales. + */ +const LANGUAGES = ['en', 'fr', 'it']; + +/** + * Represents a localization key with support for plural forms. + */ +class LocalizationKey { + constructor(base, pluralForm = null) { + this.base = base; + this.pluralForm = pluralForm; + } + + stripPrefix(prefix) { + if (!prefix || !this.base.startsWith(prefix)) { + return this; + } + return new LocalizationKey(this.base.slice(prefix.length), this.pluralForm); + } + + toCamelCase() { + const camel = this.base + .split('-') + .map((word, index) => (index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1))) + .join(''); + return new LocalizationKey(camel, this.pluralForm); + } + + matchesAnyPrefix(prefixes) { + return prefixes.some(prefix => this.base.startsWith(prefix)); + } + + toString() { + return this.base; + } +} + +/** + * Represents a localization entry (key-value pair). + */ +class LocalizationEntry { + constructor(key, value, sourceKey = null) { + this.key = key; + this.value = value; + this.sourceKey = sourceKey; + this._placeholders = null; + } + + get placeholders() { + if (this._placeholders === null) { + const regex = /\{\{\s*(\w+)\s*\}\}/g; + const found = []; + let match; + while ((match = regex.exec(this.value)) !== null) { + if (!found.includes(match[1])) { + found.push(match[1]); + } + } + this._placeholders = found; + } + return this._placeholders; + } + + get hasPlaceholders() { + return this.placeholders.length > 0; + } +} + +/** + * Configuration for a thorium-locales project. + */ +class LocaleConfig { + constructor({ + folder, + stripPrefix = '', + outputPrefix = '', + outputFolder, + includePrefixes = null, + tableName = 'Localizable', + keyTransform = null, + postProcess = null, + convertKeysToCamelCase = true + }) { + if (!folder || !outputFolder) { + throw new Error('LocaleConfig requires folder and outputFolder'); + } + this.folder = folder; + this.stripPrefix = stripPrefix; + this.outputPrefix = outputPrefix; + this.outputFolder = outputFolder; + this.includePrefixes = includePrefixes; + this.tableName = tableName; + this.keyTransform = keyTransform; + this.postProcess = postProcess; + this.convertKeysToCamelCase = convertKeysToCamelCase; + } + + transformEntry(entry) { + const sourceKey = entry.key; + let base = sourceKey.base; + + if (this.stripPrefix && base.startsWith(this.stripPrefix)) { + base = base.slice(this.stripPrefix.length); + } + if (this.keyTransform) { + base = this.keyTransform(base); + } + if (this.outputPrefix) { + base = this.outputPrefix + base; + } + + const newKey = new LocalizationKey(base, sourceKey.pluralForm); + return new LocalizationEntry(newKey, entry.value, sourceKey); + } + + shouldInclude(entry) { + if (!this.includePrefixes) { + return true; + } + return entry.key.matchesAnyPrefix(this.includePrefixes); + } +} + +// ============================================================================ +// Apple Strings Converter +// ============================================================================ + +/** + * Converts localization entries to Apple .strings format. + */ +class AppleStringsConverter { + constructor(referenceEntries) { + this._placeholderMappings = this._buildPlaceholderMappings(referenceEntries); + } + + /** + * Generates .strings file content for the given entries. + */ + generate(lang, entries, config) { + const outputEntries = entries.map(entry => config.transformEntry(entry)); + + const disclaimer = `DO NOT EDIT. File generated automatically from the ${lang} JSON strings of https://github.com/edrlab/thorium-locales/.`; + let content = `// ${disclaimer}\n\n`; + + for (const entry of outputEntries) { + content += this._formatEntry(entry, config.convertKeysToCamelCase) + '\n'; + } + + // Extract output keys for postProcess + const outputKeys = outputEntries.map(entry => + this._formatKey(entry.key.stripPrefix(config.outputPrefix)) + ); + + return { content, outputKeys }; + } + + _buildPlaceholderMappings(entries) { + const mappings = new Map(); + for (const entry of entries) { + if (!entry.hasPlaceholders) continue; + const baseKey = entry.key.base; + if (mappings.has(baseKey)) continue; + + const mapping = {}; + entry.placeholders.forEach((name, index) => { + mapping[name] = index + 1; + }); + mappings.set(baseKey, mapping); + } + return mappings; + } + + _getPlaceholderMapping(key) { + return this._placeholderMappings.get(key.base) || {}; + } + + _formatEntry(entry, convertToCamelCase) { + const transformedKey = convertToCamelCase ? entry.key.toCamelCase() : entry.key; + const outputKey = this._formatKey(transformedKey); + const lookupKey = entry.sourceKey || entry.key; + const mapping = this._getPlaceholderMapping(lookupKey); + const escapedValue = this._escape(entry.value); + const convertedValue = this._convertPlaceholders(escapedValue, mapping); + return `"${outputKey}" = "${convertedValue}";`; + } + + _formatKey(key) { + if (key.pluralForm) { + return `${key.base}@${key.pluralForm}`; + } + return key.base; + } + + _escape(value) { + return value + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/%/g, '%%'); + } + + _convertPlaceholders(value, mapping) { + if (Object.keys(mapping).length === 0) { + return value; + } + return value.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, name) => { + const position = mapping[name]; + if (position === undefined) { + return match; + } + const formatSpec = name === 'count' ? 'd' : '@'; + return `%${position}$${formatSpec}`; + }); + } +} + +// ============================================================================ +// Utility Functions +// ============================================================================ + +function fail(message) { + console.error(`Error: ${message}`); + process.exit(1); +} + +function writeFile(relativePath, content) { + const outputDir = path.dirname(relativePath); + + try { + fs.mkdirSync(outputDir, { recursive: true }); + fs.writeFileSync(relativePath, content, 'utf8'); + console.log(`Wrote ${relativePath}`); + } catch (err) { + fail(`Failed to write ${relativePath}: ${err.message}`); + } +} + +// ============================================================================ +// JSON Parsing +// ============================================================================ + +function parseJsonEntries(obj, prefix = '') { + const entries = []; + + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key; + + if (typeof value === 'string') { + const pluralSuffix = PLURAL_SUFFIXES.find(suffix => fullKey.endsWith(suffix)); + if (pluralSuffix) { + const baseKey = fullKey.slice(0, -pluralSuffix.length); + const pluralForm = pluralSuffix.slice(1); + entries.push(new LocalizationEntry(new LocalizationKey(baseKey, pluralForm), value)); + } else { + entries.push(new LocalizationEntry(new LocalizationKey(fullKey), value)); + } + } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + entries.push(...parseJsonEntries(value, fullKey)); + } else { + console.warn(`Warning: Skipping unexpected value type for key "${fullKey}": ${typeof value}`); + } + } + + return entries; +} + +async function loadLanguageEntries(inputFolder, localeFolder) { + const languageEntries = new Map(); + const folderPath = path.join(inputFolder, localeFolder); + + if (!fs.existsSync(folderPath)) { + fail(`the ${localeFolder} folder was not found at ${folderPath}`); + } + + console.log(`Processing folder: ${localeFolder}`); + + const files = await fsPromises.readdir(folderPath); + + for (const file of files) { + if (path.extname(file) !== '.json') continue; + + const lang = path.basename(file, '.json').replace(/_/g, '-'); + if (!LANGUAGES.includes(lang)) continue; + + const filePath = path.join(folderPath, file); + + try { + const data = await fsPromises.readFile(filePath, 'utf8'); + const jsonData = JSON.parse(data); + const entries = parseJsonEntries(jsonData); + + if (!languageEntries.has(lang)) { + languageEntries.set(lang, []); + } + languageEntries.get(lang).push(...entries); + } catch (err) { + fail(`processing ${file}: ${err.message}`); + } + } + + return languageEntries; +} + +// ============================================================================ +// Entry Point +// ============================================================================ + +const PROJECTS = { + lcp: new LocaleConfig({ + folder: 'lcp', + outputPrefix: 'readium.', + outputFolder: 'Sources/LCP/Resources', + includePrefixes: ['lcp.dialog'] + }), + + a11y: new LocaleConfig({ + folder: 'publication-metadata', + stripPrefix: 'publication.metadata.accessibility.display-guide.', + outputPrefix: 'readium.a11y.', + outputFolder: 'Sources/Shared/Resources', + includePrefixes: ['publication.metadata.accessibility.display-guide'], + tableName: 'W3CAccessibilityMetadataDisplayGuide', + keyTransform: key => key.replace(/\./g, '-'), + convertKeysToCamelCase: false, + postProcess: (lang, keys, config) => { + if (lang === 'en') { + generateAccessibilityDisplayStringExtensions( + keys, + 'Sources/Shared/Publication/Accessibility/AccessibilityDisplayString+Generated.swift', + config.outputPrefix, + writeFile + ); + } + } + }) +}; + +const args = process.argv.slice(2); +const [inputFolder, ...projectNames] = args; + +if (!inputFolder) { + console.error('Usage: node convert-thorium-localizations.js [project...]'); + console.error(''); + console.error('Arguments:'); + console.error(' input-folder Path to the cloned thorium-locales repository'); + console.error(' project Optional project name(s) to process (default: all)'); + console.error(''); + console.error(`Available projects: ${Object.keys(PROJECTS).join(', ')}`); + process.exit(1); +} + +const projectsToProcess = projectNames.length > 0 + ? projectNames + : Object.keys(PROJECTS); + +async function processLocales(inputFolder, projectsToProcess) { + for (const projectName of projectsToProcess) { + const config = PROJECTS[projectName]; + if (!config) { + fail(`Unknown project: ${projectName}. Available: ${Object.keys(PROJECTS).join(', ')}`); + } + + console.log(`\nProcessing project: ${projectName}`); + + const languageEntries = await loadLanguageEntries(inputFolder, config.folder); + + // Filter entries + for (const [lang, entries] of languageEntries) { + const filtered = entries.filter(entry => config.shouldInclude(entry)); + languageEntries.set(lang, filtered); + } + + // Create converter with English entries as reference + const englishEntries = languageEntries.get('en') || []; + const converter = new AppleStringsConverter(englishEntries); + + for (const [lang, entries] of languageEntries) { + const { content, outputKeys } = converter.generate(lang, entries, config); + + // Write .strings file + const outputPath = path.join(config.outputFolder, `${lang}.lproj`, `${config.tableName}.strings`); + writeFile(outputPath, content); + + // Run postProcess hook + if (config.postProcess) { + config.postProcess(lang, outputKeys, config); + } + } + } +} + +processLocales(inputFolder, projectsToProcess).catch(err => fail(err.message)); diff --git a/BuildTools/Scripts/generate-a11y-extensions.js b/BuildTools/Scripts/generate-a11y-extensions.js new file mode 100644 index 0000000000..8392f5bcce --- /dev/null +++ b/BuildTools/Scripts/generate-a11y-extensions.js @@ -0,0 +1,68 @@ +/** + * Copyright 2026 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + * + * This module generates Swift extensions for AccessibilityDisplayString + * from localization keys. + */ + +/** + * Generates AccessibilityDisplayString Swift extension from localization keys. + * + * @param {string[]} keys - Array of localization keys (without prefix) + * @param {string} outputPath - Path to write the generated Swift file + * @param {string} keyPrefix - Prefix used in localization keys (e.g., "readium.a11y.") + * @param {function} write - Write function (relativePath, content) => void + */ +function generateAccessibilityDisplayStringExtensions(keys, outputPath, keyPrefix, write) { + const disclaimer = 'DO NOT EDIT. File generated automatically from https://github.com/edrlab/thorium-locales/.'; + + // Filter out -descriptive keys (keep base keys only) and remove -compact suffix + const filteredKeys = keys + .filter(k => !k.endsWith('-descriptive')) + .map(k => removeSuffix(k, '-compact')); + + // Remove duplicates (since we removed -compact suffix, some keys may now be the same) + const uniqueKeys = [...new Set(filteredKeys)]; + + let output = `// ${disclaimer} +public extension AccessibilityDisplayString { +`; + + for (const key of uniqueKeys) { + const swiftName = convertKebabToCamelCase(key); + output += ` static let ${swiftName}: Self = "${keyPrefix}${key}"\n`; + } + + output += '}\n'; + + write(outputPath, output); +} + +/** + * Converts a kebab-case string to camelCase. + */ +function convertKebabToCamelCase(string) { + return string + .split('-') + .map((word, index) => { + if (index === 0) { + return word; + } + return word.charAt(0).toUpperCase() + word.slice(1); + }) + .join(''); +} + +/** + * Removes a suffix from a string if present. + */ +function removeSuffix(str, suffix) { + if (str.endsWith(suffix)) { + return str.slice(0, -suffix.length); + } + return str; +} + +module.exports = { generateAccessibilityDisplayStringExtensions }; diff --git a/BuildTools/Scripts/generate-docs.sh b/BuildTools/Scripts/generate-docs.sh new file mode 100755 index 0000000000..5fc3a65de9 --- /dev/null +++ b/BuildTools/Scripts/generate-docs.sh @@ -0,0 +1,210 @@ +#!/bin/bash +# ============================================================================= +# Readium Swift Toolkit - Documentation Generator +# ============================================================================= +# This script automates the creation of a static DocC documentation site. +# It handles: +# 1. Cross-compiling the Swift package for the iOS Simulator. +# 2. Generating symbol graphs (API metadata) for all modules. +# 3. filtering out 3rd-party dependencies from the docs. +# 4. Assembling the DocC catalog from the 'docs/' folder. +# 5. Converting everything into a static HTML website. +# ============================================================================= + +set -e # Exit immediately if any command exits with a non-zero status. + +# ----------------------------------------------------------------------------- +# 1. Configuration +# ----------------------------------------------------------------------------- +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" +cd "$PROJECT_ROOT" + +echo "📂 Working directory set to: $(pwd)" + +DOC_VERSION="${1:-latest}" +REPO_NAME="swift-toolkit" +# The final folder where the static HTML site will be generated. +OUTPUT_ROOT="docs-site" +# The site inside is nested in a folder matching the repo name. +# This emulates GitHub Pages URL structure (e.g., username.github.io/swift-toolkit/). +SITE_DIR="$OUTPUT_ROOT/$REPO_NAME/$DOC_VERSION" +# A temporary directory for intermediate build artifacts. +TEMP_DIR=".build-docs" +# The location of the "virtual" DocC catalog. +DOCC_CATALOG_DIR="$TEMP_DIR/Readium.docc" +# Where SwiftPM will dump the raw symbol graph JSON files. +SYMBOL_GRAPHS_DIR="$TEMP_DIR/symbol-graphs" + +# ----------------------------------------------------------------------------- +# 2. Argument Parsing +# ----------------------------------------------------------------------------- +SERVE_SITE=false +for arg in "$@"; do + if [ "$arg" == "--serve" ]; then + SERVE_SITE=true + fi +done + +# ----------------------------------------------------------------------------- +# 3. Cleanup +# ----------------------------------------------------------------------------- +# Remove previous outputs to ensure a clean build. +# This prevents stale files or old symbols from appearing in the new site. +rm -rf "$SITE_DIR" +mkdir -p "$DOCC_CATALOG_DIR" +mkdir -p "$SYMBOL_GRAPHS_DIR" +mkdir -p "$SITE_DIR" + +echo "⚙️ Configuring build environment..." + +# ----------------------------------------------------------------------------- +# 4. Environment Setup (Cross-Compilation) +# ----------------------------------------------------------------------------- +# DocC requires a build to generate symbol graphs. +# Because this project imports 'UIKit', it CANNOT be built with macOS. +# It must cross-compile with the iOS Simulator. + +# Find the path to the iOS Simulator SDK on the current machine. +SDK_PATH=$(xcrun --sdk iphonesimulator --show-sdk-path) + +# Determine the host architecture (arm64 for Apple Silicon, x86_64 for Intel) +# and construct a target triple for the compiler (e.g., arm64-apple-ios15.0-simulator). +HOST_ARCH=$(uname -m) +TARGET_TRIPLE="${HOST_ARCH}-apple-ios15.0-simulator" + +echo " • SDK: $SDK_PATH" +echo " • Target: $TARGET_TRIPLE" + +# ----------------------------------------------------------------------------- +# 5. Build & Symbol Generation +# ----------------------------------------------------------------------------- +echo "🔧 Patching Package.swift for macOS compatibility..." +# Define a cleanup function to restore the original file on exit/error +restore_package() { + if [ -f Package.swift.orig ]; then + mv Package.swift.orig Package.swift + fi +} +trap restore_package EXIT + +# Back up the original file +cp Package.swift Package.swift.orig + +# Inject .macOS(.v11) into the platforms array +# This satisfies the dependency graph validation for ReadiumZIPFoundation +sed -i '' 's/\(\.iOS("[^"]*")\)]/\1, .macOS(.v11)]/' Package.swift + +echo "🧹 Cleaning build artifacts..." +# Delete the .build folder to force SwiftPM to re-emit symbol graphs. +# If this isn't done, incremental builds might skip the documentation step. +rm -rf .build + +echo "⚙️ Building symbol graphs..." +# Run 'swift build' with specific flags: +# --sdk / --triple: Forces the build to target iOS Simulator (enabling UIKit). +# -Xswiftc -emit-symbol-graph: Tells the Swift compiler to generate documentation data. +# -Xswiftc -emit-symbol-graph-dir: Tells it where to save the .symbols.json files. +swift build \ + --sdk "$SDK_PATH" \ + --triple "$TARGET_TRIPLE" \ + -Xswiftc -emit-symbol-graph \ + -Xswiftc -emit-symbol-graph-dir -Xswiftc "$SYMBOL_GRAPHS_DIR" + +# ----------------------------------------------------------------------------- +# 6. Filter Dependencies +# ----------------------------------------------------------------------------- +echo "🧹 Filtering dependencies..." +# SwiftPM generates documentation for EVERYTHING in the dependency graph. +# Only Readium modules go in the sidebar. +# Find all .symbols.json files that do NOT start with "Readium" and delete them. +find "$SYMBOL_GRAPHS_DIR" -type f -name "*.symbols.json" ! -name "Readium*" -delete + +# ----------------------------------------------------------------------------- +# 7. Prepare Documentation Catalog +# ----------------------------------------------------------------------------- +echo "📄 Preparing documentation catalog..." + +# We create a temporary DocC bundle structure +# and copy the contents of the 'docs' folder into it. +if [ -d "docs" ]; then + cp -R docs/* "$DOCC_CATALOG_DIR/" +else + echo "⚠️ Warning: 'docs' folder not found. Site may be empty." +fi + +# Validation: Ensure the root landing page exists. +# Without this file, DocC will fail or produce an empty root. +if [ ! -f "$DOCC_CATALOG_DIR/Readium.md" ]; then + echo "❌ Error: docs/Readium.md is missing." + echo " Please create this file with @TechnologyRoot metadata." + exit 1 +fi + +echo "🚀 Generating site..." + +# ----------------------------------------------------------------------------- +# 8. DocC Conversion (Static Site Generation) +# ----------------------------------------------------------------------------- +# Find the 'docc' tool inside Xcode. +DOCC_EXEC=$(xcrun --find docc) + +# Run the conversion: +# --additional-symbol-graph-dir: Where the filtered symbols are stored. +# --transform-for-static-hosting: Generates a site compatible with GitHub Pages. +# --hosting-base-path: Critical for GitHub Pages. Sets the root URL path (e.g. /swift-toolkit/). +$DOCC_EXEC convert "$DOCC_CATALOG_DIR" \ + --additional-symbol-graph-dir "$SYMBOL_GRAPHS_DIR" \ + --output-dir "$SITE_DIR" \ + --fallback-display-name "Readium" \ + --transform-for-static-hosting \ + --hosting-base-path "$REPO_NAME/$DOC_VERSION" + +echo "✅ Documentation generated at: $SITE_DIR" + +# ----------------------------------------------------------------------------- +# 9. Add SPA Routing (Fixes Root & Deep Links) +# ----------------------------------------------------------------------------- +echo "twisted_rightwards_arrows Adding 404 redirect for SPA routing..." + +# This script handles the redirect for both the root path AND deep links. +cat < "$SITE_DIR/404.html" + + + + + Redirecting... + + + + +

Redirecting to documentation...

+ + +EOF + +cp "$SITE_DIR/index.html" "$SITE_DIR/404.html" + +# ----------------------------------------------------------------------------- +# 10. Local Preview +# ----------------------------------------------------------------------------- +if [ "$SERVE_SITE" = true ]; then + URL="http://localhost:8080/$REPO_NAME/$DOC_VERSION/documentation/readium" + echo "🌍 Serving at $URL" + + # Open the browser + open "$URL" 2>/dev/null || true + + # Run a simple Python HTTP server to serve the static files. + # Serve from OUTPUT_ROOT so the subdirectory /swift-toolkit/ exists. + python3 -m http.server -d "$OUTPUT_ROOT" 8080 +else + echo " Run 'BuildTools/Scripts/generate-docs.sh --serve' to preview." +fi diff --git a/BuildTools/Sources/GeneratePodspecs/Specs.swift b/BuildTools/Sources/GeneratePodspecs/Specs.swift new file mode 120000 index 0000000000..ebd3c6f83a --- /dev/null +++ b/BuildTools/Sources/GeneratePodspecs/Specs.swift @@ -0,0 +1 @@ +../../../Support/CocoaPods/Specs.swift \ No newline at end of file diff --git a/BuildTools/Sources/GeneratePodspecs/main.swift b/BuildTools/Sources/GeneratePodspecs/main.swift new file mode 100644 index 0000000000..313a95e206 --- /dev/null +++ b/BuildTools/Sources/GeneratePodspecs/main.swift @@ -0,0 +1,106 @@ +// +// Copyright 2026 Readium Foundation. All rights reserved. +// Use of this source code is governed by the BSD-style license +// available in the top-level LICENSE file of the project. +// + +// Generates Support/CocoaPods/*.podspec from the module definitions in Specs.swift. +// Run from the repo root via: swift run --package-path BuildTools GeneratePodspecs + +import Foundation + +let outputDir = URL(fileURLWithPath: "Support/CocoaPods") + +guard FileManager.default.fileExists(atPath: outputDir.path) else { + fputs("Error: '\(outputDir.path)' not found. Run this tool from the repository root.\n", stderr) + exit(1) +} + +for module in modules { + let content = render(module) + let dest = outputDir.appendingPathComponent("\(module.name).podspec") + do { + try content.write(to: dest, atomically: true, encoding: .utf8) + print("Wrote \(module.name).podspec") + } catch { + fputs("Error: failed to write \(dest.path): \(error)\n", stderr) + exit(1) + } +} + +func render(_ m: ModuleSpec) -> String { + var lines: [String] = [] + + lines.append("# This file is generated by `make podspecs`. Do not edit manually.") + lines.append("# Edit Support/CocoaPods/Specs.swift and run `make podspecs` to regenerate.") + lines.append("") + lines.append("Pod::Spec.new do |s|") + lines.append("") + lines.append(" s.name = \"\(m.name)\"") + lines.append(" s.version = \"\(version)\"") + lines.append(" s.license = \"BSD 3-Clause License\"") + lines.append(" s.summary = \"\(m.summary)\"") + lines.append(" s.homepage = \"http://readium.github.io\"") + lines.append(" s.author = { \"Readium\" => \"contact@readium.org\" }") + lines.append(" s.source = { :git => \"https://github.com/readium/swift-toolkit.git\", :tag => s.version }") + lines.append(" s.requires_arc = true") + + if !m.resourceBundles.isEmpty { + // Sort by key for deterministic output. + let sorted = m.resourceBundles.sorted { $0.key < $1.key } + lines.append(" s.resource_bundles = {") + for (bundleName, patterns) in sorted { + if patterns.count == 1 { + lines.append(" '\(bundleName)' => ['\(patterns[0])'],") + } else { + lines.append(" '\(bundleName)' => [") + for pattern in patterns { + lines.append(" '\(pattern)',") + } + lines.append(" ],") + } + } + lines.append(" }") + } + + lines.append(" s.source_files = \"\(m.sourcePath)/**/*.{m,h,swift}\"") + lines.append(" s.swift_version = '\(swiftVersion)'") + lines.append(" s.platform = :ios") + lines.append(" s.ios.deployment_target = \"\(iosTarget)\"") + + if !m.frameworks.isEmpty { + lines.append(" s.frameworks = \"\(m.frameworks.joined(separator: "\", \""))\"") + } + + if !m.libraries.isEmpty { + let quoted = m.libraries.map { "'\($0)'" }.joined(separator: ", ") + lines.append(" s.libraries = \(quoted)") + } + + if !m.xcconfig.isEmpty { + let sorted = m.xcconfig.sorted { $0.key < $1.key } + let pairs = sorted.map { "'\($0.key)' => '\($0.value)'" }.joined(separator: ", ") + lines.append(" s.xcconfig = { \(pairs) }") + } + + // Required for Swift `package` access level, which needs a -package-name compiler flag. + // SPM sets this automatically from Package.swift `name`; CocoaPods does not. + // All Readium modules share the same package name so that `package` access works across them. + lines.append(" s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-package-name \(packageName)' }") + + if !m.dependencies.isEmpty { + lines.append("") + for dep in m.dependencies { + switch dep { + case let .readium(name): + lines.append(" s.dependency '\(name)', '~> \(version)'") + case let .pod(name, constraint): + lines.append(" s.dependency '\(name)', '\(constraint)'") + } + } + } + + lines.append("") + lines.append("end") + return lines.joined(separator: "\n") + "\n" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2225c6ad2f..c38cce1ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,208 @@ All notable changes to this project will be documented in this file. Take a look ### Added +#### Playground + +* New `Playground` iOS app – a minimal SwiftUI sample demonstrating how to use the Readium Swift Toolkit and to test its API. + * `Recipes/` contains self-contained and explained code you can reuse in your own application. + * `App/` folder contains the scaffolding (file management, navigation, error handling) needed to run the Playground. + +#### Shared + +* Added support for SVG covers in `ResourceCoverService`. SVG images can now be used as publication covers and are rendered to bitmaps (contributed by [@grighakobian](https://github.com/readium/swift-toolkit/pull/751)). + +### Removed + +* Carthage is no longer a supported distribution method. Please migrate to Swift Package Manager or CocoaPods. + +### Changed + +#### Shared + +* All public types that parsed or serialized JSON now use the new type-safe `JSONValue` enum instead of `Any` / `[String: Any]`. See [the migration guide](docs/Migration%20Guide.md) for upgrade instructions. + +#### Navigator + +* The `DirectionalNavigationAdapter`'s policies and animated transitions are now mutable, allowing you to update the adapter's behavior after creation. + +### Fixed + +#### Shared + +* Fixed parsing of URI templates. + * Fixed `URITemplate` not recognizing `{&...}` (form-style query continuation) expressions. + * Fixed `URITemplate` expanding a form-style expression (`{?...}` or `{&...}`) to a bare `?` or `&` when none of the listed variables are provided. It now correctly expands to an empty string. + +#### Navigator + +* [#737](https://github.com/readium/swift-toolkit/issues/737) Improved page turn animations in the EPUB navigator. + * Fixed screen glitches when turning with animations disabled. + * A slide animation is now used when navigating between adjacent resources. +* The EPUB navigator now reports a continuous `locator.locations.totalProgression` value, interpolated from the actual scroll position within the resource's global progression range. Previously, the value was quantized to the nearest position in the position list. +* Fixed a race condition in `EPUBNavigatorViewController` where rapidly calling `apply(decorations:in:)` for the same group could cause multiple highlights to appear simultaneously. + +#### Streamer + +* Fixed parsing of EPUB contributors. + * Fixed `media:narrator` contributors not being recognized as narrators. + * Fixed `dc:creator` elements with a known MARC relator role (e.g. `opf:role="trl"`) being incorrectly routed to the `author` collection instead of the role's collection. + * A known role on a contributor no longer leaks into the `roles` field of the `Contributor` object when it is already expressed by the contributor's collection (e.g. `authors`, `publishers`). + + +## [3.8.0] + +### Added + +#### LCP + +* New Keychain-based implementations of the LCP license and passphrase repositories: `LCPKeychainLicenseRepository` and `LCPKeychainPassphraseRepository`. + * Stored securely in the iOS/macOS Keychain. + * Persist across app reinstalls. + * Optionally synchronized across devices via iCloud Keychain. + +### Changed + +#### Navigator + +* The EPUB navigator no longer requires an HTTP server. Publication resources are now served directly to the web views using a custom URL scheme handler. + * The `httpServer` parameter of `EPUBNavigatorViewController` is deprecated and ignored. + +### Deprecated + +#### Navigator + +* `CBZNavigatorViewController` is now deprecated. + * Open CBZ publications with `EPUBNavigatorViewController` instead, which has more configuration options and preferences. + +#### LCP + +* `ReadiumAdapterLCPSQLite` is now deprecated in favor of the built-in Keychain repositories. See [the migration guide](docs/Migration%20Guide.md) for instructions. + +### Fixed + +* Fixed casting of `ResourceProperties`'s `mediaType` (contributed by [@lbeus](https://github.com/readium/swift-toolkit/pull/719)). + +#### Navigator + +* The first resource of a fixed-layout EPUB is now displayed on its own by default, matching Apple Books behavior. +* Fixed the default spread position for single fixed-layout EPUB spreads that are not the first page. + +#### LCP + +* Fixed the `print` method consuming copy rights instead of print rights. + + +## [3.7.0] + +### Added + +#### Shared + +* Added support for JXL (JPEG XL) bitmap images. JXL is decoded natively on iOS 17+. +* `Publication.cover()` now falls back on the first reading order resource if it's a bitmap image and no cover is declared. + +#### Navigator + +* Support for displaying Divina (image-based publications like CBZ) in the fixed-layout EPUB navigator. +* Bitmap images in the EPUB reading order are now supported as a fixed layout resource. +* Added `offsetFirstPage` preference for fixed-layout EPUBs to control whether the first page is displayed alone or alongside the second page when spreads are enabled. + +#### Streamer + +* The `ImageParser` now extracts metadata from `ComicInfo.xml` files in CBZ archives. +* EPUB manifest item fallbacks are now exposed as `alternates` in the corresponding `Link`. +* EPUBs with only bitmap images in the spine are now treated as Divina publications with fixed layout. + * When an EPUB spine item is HTML with a bitmap image fallback (or vice versa), the image is preferred as the primary link. +* Standalone audio files (e.g. MP3) metadata extraction now includes `narrators` (from the composer metadata fields) and merges artist metadata into `authors`, following conventions used by common audiobook tools. + +### Changed + +* The iOS minimum deployment target is now iOS 15.0. + +#### Shared + +* Accessibility display strings are now sourced from the [thorium-locales](https://github.com/edrlab/thorium-locales/) repository (instead of W3C's repository). Contributions are welcome on [Weblate](https://hosted.weblate.org/projects/thorium-reader/publication-metadata/). + +#### LCP + +* The LCP dialog used by `LCPDialogAuthentication` has been redesigned. + * **Breaking:** The LCP dialog localization string keys have been renamed. If you overrode these strings in your app, you must update them. [See the migration guide](docs/Migration%20Guide.md) for the key mapping. +* LCP localized strings are now sourced from the [thorium-locales](https://github.com/edrlab/thorium-locales/) repository. Contributions are welcome on [Weblate](https://hosted.weblate.org/projects/thorium-reader/readium-lcp/). + +### Deprecated + +#### Streamer + +* The EPUB manifest item `id` attribute is no longer exposed in `Link.properties`. +* Removed title inference based on folder names within image and audio archives. Use the archive's filename instead. + +### Fixed + +#### Navigator + +* PDF documents are now opened off the main thread, preventing UI freezes with large files. +* Fixed providing a custom reading order to the `EPUBNavigatorViewController` (contributed by [@lbeus](https://github.com/readium/swift-toolkit/pull/694)). + + +## [3.6.0] + +### Added + +#### Navigator + +* Added `DragPointerObserver` to recognize drag gestures with pointer events. +* Added `DirectionalNavigationAdapter.onNavigation` callback to be notified when a navigation action is triggered. + * This callback is called before executing any navigation action. + * Useful for hiding UI elements when the user navigates, or implementing analytics. +* Added swipe gesture support for navigating in PDF paginated spread mode. +* Added `fit` preference for fixed-layout publications (PDF and FXL EPUB) to control how pages are scaled within the viewport. + * In the PDF navigator, it is only effective in scroll mode. Paginated mode always uses `page` fit due to PDFKit limitations. + +### Deprecated + +#### Navigator + +* `PDFNavigatorViewController.scalesDocumentToFit` is now deprecated and non-functional. The navigator always scales the document to fit the viewport. + +### Changed + +#### Streamer + +* Support for asynchronous callbacks with `onCreatePublication` (contributed by [@smoores-dev](https://github.com/readium/swift-toolkit/pull/673)). + +#### Navigator + +* The `Fit` enum has been redesigned to fit the PDF implementation. + * **Breaking change:** Update any code using the old `Fit` enum values. +* The fixed-layout navigators (PDF and FXL EPUB)'s content inset behavior has changed: + * iPhone: Continues to apply window safe area insets (to account for notch/Dynamic Island). + * iPad/macOS: Now displays edge-to-edge with no automatic safe area insets. + * You can customize this behavior with `VisualNavigatorDelegate.navigatorContentInset(_:)`. + +### Fixed + +#### Navigator + +* Fixed EPUB fixed-layout spread settings not updating after device rotation when the app was in the background. +* Fixed zoom-to-fit scaling in PDF paginated spread mode when `offsetFirstPage` is enabled. + +#### LCP + +* Fixed crash when an EPUB resource is declared as LCP-encrypted in the manifest but contains unencrypted data. + + +## [3.5.0] + +### Added + #### Navigator * Added `VisualNavigatorDelegate.navigatorContentInset(_:)` to customize the content and safe-area insets used by the navigator. * By default, the navigator uses the window's `safeAreaInsets`, which can cause content to shift when the status bar is shown or hidden (since those insets change). To avoid this, implement `navigatorContentInset(_:)` and return insets that remain stable across status bar visibility changes — for example, a top inset large enough to accommodate the maximum expected status bar height. * Added `[TTSVoice].filterByLanguage(_:)` to filter TTS voices by language and region. * Added `[TTSVoice].sorted()` to sort TTS voices by region, quality, and gender. +* New experimental positioning of EPUB decorations that places highlights behind text to improve legibility with opaque decorations (contributed by [@ddfreiling](https://github.com/readium/swift-toolkit/pull/665)). + * To opt-in, initialize the `EPUBNavigatorViewController.Configuration` object with `decorationTemplates: HTMLDecorationTemplate.defaultTemplates(alpha: 1.0, experimentalPositioning: true)`. #### LCP @@ -472,8 +668,8 @@ All notable changes to this project will be documented in this file. Take a look * New `VisualNavigatorDelegate` APIs to handle keyboard events (contributed by [@lukeslu](https://github.com/readium/swift-toolkit/pull/267)). * This can be used to turn pages with the arrow keys, for example. -* [Support for custom fonts with the EPUB navigator](docs/Guides/EPUB%20Fonts.md). -* A brand new user preferences API for configuring the EPUB and PDF Navigators. This new API is easier and safer to use. To learn how to integrate it in your app, [please refer to the user guide](docs/Guides/Navigator%20Preferences.md) and [migration guide](docs/Migration%20Guide.md). +* [Support for custom fonts with the EPUB navigator](docs/Guides/Navigator/EPUB%20Fonts.md). +* A brand new user preferences API for configuring the EPUB and PDF Navigators. This new API is easier and safer to use. To learn how to integrate it in your app, [please refer to the user guide](docs/Guides/Navigator/Preferences.md) and [migration guide](docs/Migration%20Guide.md). * New EPUB user preferences: * `fontWeight` - Base text font weight. * `textNormalization` - Normalize font style, weight and variants, which improves accessibility. @@ -1004,3 +1200,7 @@ progression. Now if no reading progression is set, the `effectiveReadingProgress [3.2.0]: https://github.com/readium/swift-toolkit/compare/3.1.0...3.2.0 [3.3.0]: https://github.com/readium/swift-toolkit/compare/3.2.0...3.3.0 [3.4.0]: https://github.com/readium/swift-toolkit/compare/3.3.0...3.4.0 +[3.5.0]: https://github.com/readium/swift-toolkit/compare/3.4.0...3.5.0 +[3.6.0]: https://github.com/readium/swift-toolkit/compare/3.5.0...3.6.0 +[3.7.0]: https://github.com/readium/swift-toolkit/compare/3.6.0...3.7.0 +[3.8.0]: https://github.com/readium/swift-toolkit/compare/3.7.0...3.8.0 diff --git a/Cartfile b/Cartfile deleted file mode 100644 index 6e94b1cff9..0000000000 --- a/Cartfile +++ /dev/null @@ -1,9 +0,0 @@ -github "dexman/Minizip" ~> 1.4.0 -github "krzyzanowskim/CryptoSwift" ~> 1.8.0 -github "ra1028/DifferenceKit" ~> 1.3.0 -github "readium/Fuzi" ~> 4.0.0 -github "readium/GCDWebServer" ~> 4.0.0 -github "readium/ZIPFoundation" ~> 3.0.0 -# There's a regression with 2.7.4 in SwiftSoup, because they used iOS 13 APIs without bumping the deployment target. -github "scinfu/SwiftSoup" == 2.7.1 -github "stephencelis/SQLite.swift" ~> 0.15.0 diff --git a/MAINTAINING.md b/MAINTAINING.md index f51cb4d72c..0df28712a5 100644 --- a/MAINTAINING.md +++ b/MAINTAINING.md @@ -1,6 +1,43 @@ # Maintaining the Readium Swift toolkit -## Releasing a new version +## Bumping the Minimum iOS Deployment Target + +To bump the minimum required iOS version, update these files: + +- `README.md`, section "Minimum Requirements" +- `Package.swift` +- `Support/CocoaPods/*.podspec` — edit `iosTarget` in `Support/CocoaPods/Specs.swift`, then run `make podspecs` and commit the generated files + +## Creating a New Package + +A new package is a separately distributable SPM library product. It requires updates to four places. + +### 1. `Package.swift` + +Add a new product and its source/test targets: + +```swift +// products: +.library(name: "Readium", targets: ["Readium"]), + +// targets: +.target( + name: "Readium", + dependencies: ["ReadiumShared", "ReadiumNavigator"], + path: "Sources/" +), +.testTarget( + name: "ReadiumTests", + dependencies: ["Readium"], + path: "Tests/Tests" +), +``` + +### 2. `Support/CocoaPods/Readium.podspec` + +Add an entry to `Support/CocoaPods/Specs.swift` and run `make podspecs` to generate the podspec file. + +## Releasing a New Version You are ready to release a new version of the Swift toolkit? Great, follow these steps: @@ -14,31 +51,26 @@ You are ready to release a new version of the Swift toolkit? Great, follow these ``` 4. Try to run the Test App, adjusting the integration if needed. 5. Delete the Git tag created previously. -3. Update the [migration guide](Documentation/Migration%20Guide.md) in case of breaking changes. -4. Issue the new release. +3. Update the localized strings (`make update-locales`). +4. Review the list of supported features in `README.md`. +5. Update the [migration guide](Documentation/Migration%20Guide.md) in case of breaking changes. +6. Issue the new release. 1. Create a branch with the same name as the future tag, from `develop`. - 2. Bump the version numbers in the `Support/CocoaPods/*.podspec` files. - * :warning: Don't forget to bump the version numbers of the Readium dependencies as well. - 3. Bump the version numbers in `README.md`. + 2. Bump `version` in `Support/CocoaPods/Specs.swift`, run `make podspecs`, and commit the generated files. + 3. Bump the version numbers in `README.md`, and check the "Minimum Requirements" section. 4. Bump the version numbers in `TestApp/Sources/Info.plist`. 5. Close the version in the `CHANGELOG.md`, [for example](https://github.com/readium/swift-toolkit/pull/353/commits/a0714589b3da928dd923ba78f379116715797333#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed). 6. Create a PR to merge in `develop` and verify the CI workflows. - 7. Squash and merge the PR. - 8. Tag the new version from `develop`. - ```shell - git checkout develop - git pull - git tag -a 3.0.1 -m 3.0.1 - git push --tags - ``` - 9. Release the updated Podspecs: + 7. Release the updated Podspecs: ```shell cd Support/CocoaPods pod repo add readium git@github.com:readium/podspecs.git pod repo push readium ReadiumInternal.podspec + pod repo push readium ReadiumShared.podspec + pod repo push readium ReadiumStreamer.podspec pod repo push readium ReadiumNavigator.podspec pod repo push readium ReadiumOPDS.podspec @@ -46,10 +78,17 @@ You are ready to release a new version of the Swift toolkit? Great, follow these pod repo push readium ReadiumAdapterGCDWebServer.podspec pod repo push readium ReadiumAdapterLCPSQLite.podspec ``` -5. Verify you can fetch the new version from the latest Test App with `make spm|carthage|cocoapods version=3.0.1` -7. Announce the release. + 8. Squash and merge the PR. + 9. Tag the new version from `develop`. + ```shell + git checkout develop + git pull + git tag -a 3.0.1 -m 3.0.1 + git push --tags + ``` +7. Verify you can fetch the new version from the latest Test App with `make spm|cocoapods version=3.0.1` +8. Announce the release. 1. Create a new release on GitHub. - 2. Publish a new TestFlight beta with LCP enabled. - * Click on "External Groups" > "Public Beta", then add the new build so that it's available to everyone. -8. Merge `develop` into `main`. - + 2. Write a high-level summary of the changelog for the blog. + 3. Post the blog summary on Discord's `#announcement`, with a link to the GitHub release. +9. Merge `develop` into `main`. diff --git a/Makefile b/Makefile index c068e75bc3..6764191f8b 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,34 @@ SCRIPTS_PATH := Sources/Navigator/EPUB/Scripts -CSS_PATH := Sources/Navigator/EPUB/Assets/Static/readium-css help: @echo "Usage: make \n\n\ - carthage-proj\t\tGenerate the Carthage Xcode project\n\ + playground\t\tGenerate the Playground project\n\ + podspecs\t\tGenerate the CocoaPods podspecs\n\ scripts\t\tBundle the Navigator EPUB scripts\n\ test\t\t\tRun unit tests\n\ lint-format\t\tVerify formatting\n\ format\t\tFormat sources\n\ - update-a11y-l10n\tUpdate the Accessibility Metadata Display Guide localization files\n\ + update-locales\tUpdate the localization files\n\ " -.PHONY: carthage-project -carthage-project: - rm -rf $(SCRIPTS_PATH)/node_modules/ - xcodegen -s Support/Carthage/project.yml --use-cache --cache-path Support/Carthage/.xcodegen +.SILENT: +.PHONY: playground +playground: + cd Playground; \ + find . -name ".DS_Store" -delete; \ + xcodegen --use-cache --cache-path .xcodegen; \ + # The repository might be cloned to a different location than "swift-toolkit". + # XcodeGen will use the name of the folder in the project, which is not desirable. + # This will replace all occurrences of this folder by "swift-toolkit". + perl -i -0777 -pe 'if (/name = (\S+); path = \.\.; /) { my $$n = $$1; s/name = \Q$$n\E; path = \.\./name = swift-toolkit; path = ../; s|/\* \Q$$n\E \*/|/* swift-toolkit */|g; }' Playground/Playground.xcodeproj/project.pbxproj + +.PHONY: podspecs +podspecs: + swift run --package-path BuildTools GeneratePodspecs + +.PHONY: navigator-ui-tests-project +navigator-ui-tests-project: + xcodegen -s Tests/NavigatorTests/UITests/project.yml .PHONY: scripts scripts: @@ -33,20 +47,6 @@ update-scripts: @which corepack >/dev/null 2>&1 || (echo "ERROR: corepack is required, please install it first\nhttps://pnpm.io/installation#using-corepack"; exit 1) pnpm install --dir "$(SCRIPTS_PATH)" -.PHONY: update-css -update-css: - git clone https://github.com/readium/css.git readium-css - rm -rf "$(CSS_PATH)" - cp -r readium-css/css/dist "$(CSS_PATH)" - git -C readium-css rev-parse HEAD > "$(CSS_PATH)/HEAD" - rm -rf readium-css - rm -rf "$(CSS_PATH)/android-fonts-patch" - -.PHONY: test -test: - # To limit to a particular test suite: -only-testing:ReadiumSharedTests - xcodebuild test -scheme "Readium-Package" -destination "platform=iOS Simulator,name=iPhone 15" | xcbeautify -q - .PHONY: lint-format lint-format: swift run --package-path BuildTools swiftformat --lint . @@ -56,11 +56,17 @@ f: format format: swift run --package-path BuildTools swiftformat . -.PHONY: update-a11y-l10n -update-a11y-l10n: - @which node >/dev/null 2>&1 || (echo "ERROR: node is required, please install it first"; exit 1) - rm -rf publ-a11y-display-guide-localizations - git clone https://github.com/w3c/publ-a11y-display-guide-localizations.git - node BuildTools/Scripts/convert-a11y-display-guide-localizations.js publ-a11y-display-guide-localizations apple Sources/Shared readium.a11y. - rm -rf publ-a11y-display-guide-localizations +BRANCH ?= main +.PHONY: update-locales +update-locales: + @which node >/dev/null 2>&1 || (echo "ERROR: node is required, please install it first"; exit 1) +ifndef DIR + rm -rf thorium-locales + git clone -b $(BRANCH) --single-branch --depth 1 https://github.com/edrlab/thorium-locales.git +endif + node BuildTools/Scripts/convert-thorium-localizations.js thorium-locales +ifndef DIR + rm -rf thorium-locales +endif + make format diff --git a/Package.swift b/Package.swift index c281d2038e..278bde6ab4 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,6 @@ // swift-tools-version:5.10 // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -10,7 +10,7 @@ import PackageDescription let package = Package( name: "Readium", defaultLocalization: "en", - platforms: [.iOS("13.4")], + platforms: [.iOS("15.0")], products: [ .library(name: "ReadiumShared", targets: ["ReadiumShared"]), .library(name: "ReadiumStreamer", targets: ["ReadiumStreamer"]), @@ -28,9 +28,10 @@ let package = Package( .package(url: "https://github.com/ra1028/DifferenceKit.git", from: "1.3.0"), .package(url: "https://github.com/readium/Fuzi.git", from: "4.0.0"), .package(url: "https://github.com/readium/GCDWebServer.git", from: "4.0.0"), - .package(url: "https://github.com/readium/ZIPFoundation.git", from: "3.0.0"), - .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.7.0"), + .package(url: "https://github.com/readium/ZIPFoundation.git", from: "3.0.1"), + .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.13.0"), .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.0"), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), ], targets: [ .target( @@ -53,7 +54,10 @@ let package = Package( ), .testTarget( name: "ReadiumSharedTests", - dependencies: ["ReadiumShared"], + dependencies: [ + "ReadiumShared", + "TestPublications", + ], path: "Tests/SharedTests", resources: [ .copy("Fixtures"), @@ -101,7 +105,10 @@ let package = Package( .testTarget( name: "ReadiumNavigatorTests", dependencies: ["ReadiumNavigator"], - path: "Tests/NavigatorTests" + path: "Tests/NavigatorTests", + exclude: [ + "UITests", + ] ), .target( @@ -125,6 +132,7 @@ let package = Package( name: "ReadiumLCP", dependencies: [ "CryptoSwift", + "ReadiumInternal", "ReadiumShared", .product(name: "ReadiumZIPFoundation", package: "ZIPFoundation"), ], @@ -140,7 +148,7 @@ let package = Package( // dependencies: ["ReadiumLCP"], // path: "Tests/LCPTests", // resources: [ - // .copy("Fixtures"), + // .copy("../Fixtures"), // ] // ), @@ -171,5 +179,14 @@ let package = Package( dependencies: ["ReadiumInternal"], path: "Tests/InternalTests" ), + + // Shared test publications used across multiple test targets. + .target( + name: "TestPublications", + path: "Tests/Publications", + resources: [ + .copy("Publications"), + ] + ), ] ) diff --git a/Playground/.xcodegen b/Playground/.xcodegen new file mode 100644 index 0000000000..2e18fb9914 --- /dev/null +++ b/Playground/.xcodegen @@ -0,0 +1,112 @@ +# XCODEGEN VERSION +2.45.3 + +# SPEC +{ + "name" : "Playground", + "options" : { + "bundleIdPrefix" : "org.readium" + }, + "packages" : { + "Readium" : { + "path" : ".." + } + }, + "schemes" : { + "Playground" : { + "build" : { + "targets" : { + "Playground" : "" + } + }, + "test" : { + "testPlans" : [ + { + "defaultPlan" : true, + "path" : "Playground.xctestplan" + } + ] + } + } + }, + "targets" : { + "Playground" : { + "dependencies" : [ + { + "package" : "Readium", + "product" : "ReadiumShared" + }, + { + "package" : "Readium", + "product" : "ReadiumStreamer" + }, + { + "package" : "Readium", + "product" : "ReadiumNavigator" + }, + { + "package" : "Readium", + "product" : "ReadiumAdapterGCDWebServer" + }, + { + "package" : "Readium", + "product" : "ReadiumOPDS" + } + ], + "deploymentTarget" : "16.0", + "platform" : "iOS", + "settings" : { + "SWIFT_APPROACHABLE_CONCURRENCY" : true, + "SWIFT_DEFAULT_ACTOR_ISOLATION" : "MainActor" + }, + "sources" : [ + { + "path" : "Sources" + } + ], + "type" : "application" + } + } +} + +# FILES +Sources +Sources/App +Sources/App/Common +Sources/App/Common/Extensions +Sources/App/Common/Extensions/FileManager+Ext.swift +Sources/App/Common/Extensions/Logger+Ext.swift +Sources/App/Common/UserError.swift +Sources/App/Common/UserError+Readium.swift +Sources/App/Common/Views +Sources/App/Common/Views/HTMLText.swift +Sources/App/Common/Views/JSONView.swift +Sources/App/Data +Sources/App/Data/DocumentList.swift +Sources/App/Data/DocumentRepository.swift +Sources/App/PlaygroundApp.swift +Sources/App/Publication +Sources/App/Publication/PublicationMetadataView.swift +Sources/App/Publication/PublicationView.swift +Sources/Assets.xcassets +Sources/Assets.xcassets/AppIcon.appiconset +Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29.png +Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x-1.png +Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png +Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@3x.png +Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40.png +Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x-1.png +Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png +Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@3x.png +Sources/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png +Sources/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@3x.png +Sources/Assets.xcassets/AppIcon.appiconset/AppIcon76x76.png +Sources/Assets.xcassets/AppIcon.appiconset/AppIcon76x76@2x.png +Sources/Assets.xcassets/AppIcon.appiconset/Contents.json +Sources/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png +Sources/Assets.xcassets/AppIcon.appiconset/readiumlogo_2048-83.5@2x.png +Sources/Assets.xcassets/Contents.json +Sources/Info.plist +Sources/Recipes +Sources/Recipes/A01-OpenPublication.swift +Sources/Recipes/A02-ReadMetadata.swift" diff --git a/Playground/Playground.xcodeproj/project.pbxproj b/Playground/Playground.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..f8ec071e99 --- /dev/null +++ b/Playground/Playground.xcodeproj/project.pbxproj @@ -0,0 +1,474 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 07FE4C9817951411354484C0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 59844953100C517348EF23D0 /* Assets.xcassets */; }; + 1BCA1AEC8E94F11FFEF3A0C8 /* ReadiumAdapterGCDWebServer in Frameworks */ = {isa = PBXBuildFile; productRef = 00010B91AF7FECD80C7CBF85 /* ReadiumAdapterGCDWebServer */; }; + 2EE593DC2038FDF776324278 /* FileManager+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AE9B43C414342BBB5CDDEF /* FileManager+Ext.swift */; }; + 3FA679B6B90C8BD7B5A7DF58 /* JSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB6D3C4C19C2038573D2B90 /* JSONView.swift */; }; + 52805E26E8DF05E511042B97 /* UserError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9F2917BE7D720CB89EBC9C /* UserError.swift */; }; + 52AF5B85A8E5285B966E7CB5 /* DocumentList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8B2B0C8575F4648E445A44 /* DocumentList.swift */; }; + 6C9BEF9E1487605B673E48AA /* PublicationMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD99839EEEAFBFAF8A2264F /* PublicationMetadataView.swift */; }; + 7EBCAA279457CB27B0A3F136 /* PublicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1CFC0B9AEB966345163620 /* PublicationView.swift */; }; + 7FB8FD4A23F144C56E82ED91 /* ReadiumStreamer in Frameworks */ = {isa = PBXBuildFile; productRef = 79CBFD1B8193030A2DB6A839 /* ReadiumStreamer */; }; + AD55D7FC3E0D42E695C0E2D1 /* ReadiumOPDS in Frameworks */ = {isa = PBXBuildFile; productRef = 41FF3979DE8082AF5E23D69D /* ReadiumOPDS */; }; + B19623280F8C457F051B3110 /* PlaygroundApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C0B458C816697C5C670E9 /* PlaygroundApp.swift */; }; + BEB5501D6869FDCA129A87B9 /* A02-ReadMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = D608867E2F9CC0B751114DE9 /* A02-ReadMetadata.swift */; }; + CA6204854C325EE686871A2C /* ReadiumNavigator in Frameworks */ = {isa = PBXBuildFile; productRef = 872E0CB31611AD93E229C627 /* ReadiumNavigator */; }; + D2EF387DADE049BCBDBEE738 /* UserError+Readium.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E5C61917B53108BA01753E /* UserError+Readium.swift */; }; + D4ACEB3498FF70895A6B9405 /* HTMLText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DC581D9DDE636037C5FAB4A /* HTMLText.swift */; }; + D5BAB0C814A7AE71E58FED39 /* A01-OpenPublication.swift in Sources */ = {isa = PBXBuildFile; fileRef = B75178DB65AF67E034F3C4A5 /* A01-OpenPublication.swift */; }; + E1D8BAC3D0B056A27DA65AA1 /* Logger+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B9812F732ED2F093918E79 /* Logger+Ext.swift */; }; + E46744897A309BECE20573D9 /* ReadiumShared in Frameworks */ = {isa = PBXBuildFile; productRef = E01892658E366AE70B7B1386 /* ReadiumShared */; }; + EED944ABA27DA5FBD6370C23 /* DocumentRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7CDF2688AF525420855AC5 /* DocumentRepository.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 21B9812F732ED2F093918E79 /* Logger+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Ext.swift"; sourceTree = ""; }; + 2C1CFC0B9AEB966345163620 /* PublicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicationView.swift; sourceTree = ""; }; + 2E399BE85546465BB3B37527 /* swift-toolkit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = swift-toolkit; path = ..; sourceTree = SOURCE_ROOT; }; + 3A9F2917BE7D720CB89EBC9C /* UserError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserError.swift; sourceTree = ""; }; + 4DC581D9DDE636037C5FAB4A /* HTMLText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLText.swift; sourceTree = ""; }; + 59844953100C517348EF23D0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 5BD99839EEEAFBFAF8A2264F /* PublicationMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicationMetadataView.swift; sourceTree = ""; }; + 5C7CDF2688AF525420855AC5 /* DocumentRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentRepository.swift; sourceTree = ""; }; + 5D8B2B0C8575F4648E445A44 /* DocumentList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentList.swift; sourceTree = ""; }; + A5AE9B43C414342BBB5CDDEF /* FileManager+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Ext.swift"; sourceTree = ""; }; + A7E5C61917B53108BA01753E /* UserError+Readium.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserError+Readium.swift"; sourceTree = ""; }; + B75178DB65AF67E034F3C4A5 /* A01-OpenPublication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "A01-OpenPublication.swift"; sourceTree = ""; }; + B77C0B458C816697C5C670E9 /* PlaygroundApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaygroundApp.swift; sourceTree = ""; }; + BDA9169E926B14087F3B1BA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + CCB6D3C4C19C2038573D2B90 /* JSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONView.swift; sourceTree = ""; }; + D608867E2F9CC0B751114DE9 /* A02-ReadMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "A02-ReadMetadata.swift"; sourceTree = ""; }; + E40DD68F934F5F0D2981ACA1 /* Playground.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Playground.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2F8D7CF22299B8D14091AB8E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E46744897A309BECE20573D9 /* ReadiumShared in Frameworks */, + 7FB8FD4A23F144C56E82ED91 /* ReadiumStreamer in Frameworks */, + CA6204854C325EE686871A2C /* ReadiumNavigator in Frameworks */, + 1BCA1AEC8E94F11FFEF3A0C8 /* ReadiumAdapterGCDWebServer in Frameworks */, + AD55D7FC3E0D42E695C0E2D1 /* ReadiumOPDS in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 125CAF6B72840AFE3F05FC95 /* Views */ = { + isa = PBXGroup; + children = ( + 4DC581D9DDE636037C5FAB4A /* HTMLText.swift */, + CCB6D3C4C19C2038573D2B90 /* JSONView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 3F42D23B0ABB28C66695E5A5 /* Recipes */ = { + isa = PBXGroup; + children = ( + B75178DB65AF67E034F3C4A5 /* A01-OpenPublication.swift */, + D608867E2F9CC0B751114DE9 /* A02-ReadMetadata.swift */, + ); + path = Recipes; + sourceTree = ""; + }; + 3F6B916872C30820F6241E84 /* Products */ = { + isa = PBXGroup; + children = ( + E40DD68F934F5F0D2981ACA1 /* Playground.app */, + ); + name = Products; + sourceTree = ""; + }; + 4D0CAE133B02D7D170557CBE /* App */ = { + isa = PBXGroup; + children = ( + B77C0B458C816697C5C670E9 /* PlaygroundApp.swift */, + B8E0BBE1E017E8FF9795D4AF /* Common */, + 6C3B230691D6AF2F58824345 /* Data */, + 6BE58A63F0E2175D1974228F /* Publication */, + ); + path = App; + sourceTree = ""; + }; + 64AF3FB125E0EA956222B31B /* Extensions */ = { + isa = PBXGroup; + children = ( + A5AE9B43C414342BBB5CDDEF /* FileManager+Ext.swift */, + 21B9812F732ED2F093918E79 /* Logger+Ext.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 6BE58A63F0E2175D1974228F /* Publication */ = { + isa = PBXGroup; + children = ( + 5BD99839EEEAFBFAF8A2264F /* PublicationMetadataView.swift */, + 2C1CFC0B9AEB966345163620 /* PublicationView.swift */, + ); + path = Publication; + sourceTree = ""; + }; + 6C3B230691D6AF2F58824345 /* Data */ = { + isa = PBXGroup; + children = ( + 5D8B2B0C8575F4648E445A44 /* DocumentList.swift */, + 5C7CDF2688AF525420855AC5 /* DocumentRepository.swift */, + ); + path = Data; + sourceTree = ""; + }; + 75054112A41CDCE58ADACF92 /* Packages */ = { + isa = PBXGroup; + children = ( + 2E399BE85546465BB3B37527 /* swift-toolkit */, + ); + name = Packages; + sourceTree = ""; + }; + AABC9BE64A1302199D5D2AF8 /* Sources */ = { + isa = PBXGroup; + children = ( + 59844953100C517348EF23D0 /* Assets.xcassets */, + BDA9169E926B14087F3B1BA2 /* Info.plist */, + 4D0CAE133B02D7D170557CBE /* App */, + 3F42D23B0ABB28C66695E5A5 /* Recipes */, + ); + path = Sources; + sourceTree = ""; + }; + B8E0BBE1E017E8FF9795D4AF /* Common */ = { + isa = PBXGroup; + children = ( + 3A9F2917BE7D720CB89EBC9C /* UserError.swift */, + A7E5C61917B53108BA01753E /* UserError+Readium.swift */, + 64AF3FB125E0EA956222B31B /* Extensions */, + 125CAF6B72840AFE3F05FC95 /* Views */, + ); + path = Common; + sourceTree = ""; + }; + E23F411CF4F5D5291A0322DE = { + isa = PBXGroup; + children = ( + 75054112A41CDCE58ADACF92 /* Packages */, + AABC9BE64A1302199D5D2AF8 /* Sources */, + 3F6B916872C30820F6241E84 /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 739B1FD817D42F0264714A50 /* Playground */ = { + isa = PBXNativeTarget; + buildConfigurationList = CBF67F902A381FB2989A98D4 /* Build configuration list for PBXNativeTarget "Playground" */; + buildPhases = ( + E40BECB45945A673BB0CC3F9 /* Sources */, + C071FD4667C21888C52DF25C /* Resources */, + 2F8D7CF22299B8D14091AB8E /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Playground; + packageProductDependencies = ( + E01892658E366AE70B7B1386 /* ReadiumShared */, + 79CBFD1B8193030A2DB6A839 /* ReadiumStreamer */, + 872E0CB31611AD93E229C627 /* ReadiumNavigator */, + 00010B91AF7FECD80C7CBF85 /* ReadiumAdapterGCDWebServer */, + 41FF3979DE8082AF5E23D69D /* ReadiumOPDS */, + ); + productName = Playground; + productReference = E40DD68F934F5F0D2981ACA1 /* Playground.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DF84942AD828BBDD499F04C0 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + }; + }; + buildConfigurationList = C1581E14B552D0BE7FA2423D /* Build configuration list for PBXProject "Playground" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = E23F411CF4F5D5291A0322DE; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 69DDD3FA2655009065C0DDED /* XCLocalSwiftPackageReference ".." */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 3F6B916872C30820F6241E84 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 739B1FD817D42F0264714A50 /* Playground */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + C071FD4667C21888C52DF25C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 07FE4C9817951411354484C0 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + E40BECB45945A673BB0CC3F9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D5BAB0C814A7AE71E58FED39 /* A01-OpenPublication.swift in Sources */, + BEB5501D6869FDCA129A87B9 /* A02-ReadMetadata.swift in Sources */, + 52AF5B85A8E5285B966E7CB5 /* DocumentList.swift in Sources */, + EED944ABA27DA5FBD6370C23 /* DocumentRepository.swift in Sources */, + 2EE593DC2038FDF776324278 /* FileManager+Ext.swift in Sources */, + D4ACEB3498FF70895A6B9405 /* HTMLText.swift in Sources */, + 3FA679B6B90C8BD7B5A7DF58 /* JSONView.swift in Sources */, + E1D8BAC3D0B056A27DA65AA1 /* Logger+Ext.swift in Sources */, + B19623280F8C457F051B3110 /* PlaygroundApp.swift in Sources */, + 6C9BEF9E1487605B673E48AA /* PublicationMetadataView.swift in Sources */, + 7EBCAA279457CB27B0A3F136 /* PublicationView.swift in Sources */, + D2EF387DADE049BCBDBEE738 /* UserError+Readium.swift in Sources */, + 52805E26E8DF05E511042B97 /* UserError.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 18883859C44E4AE8042B204F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 4F8F737A22C49C70A327F32E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = Sources/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.readium.Playground; + SDKROOT = iphoneos; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + DF83FEB514DF89BC00722881 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = Sources/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.readium.Playground; + SDKROOT = iphoneos; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + FE96C092F5D790A83D093866 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C1581E14B552D0BE7FA2423D /* Build configuration list for PBXProject "Playground" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 18883859C44E4AE8042B204F /* Debug */, + FE96C092F5D790A83D093866 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + CBF67F902A381FB2989A98D4 /* Build configuration list for PBXNativeTarget "Playground" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4F8F737A22C49C70A327F32E /* Debug */, + DF83FEB514DF89BC00722881 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 69DDD3FA2655009065C0DDED /* XCLocalSwiftPackageReference ".." */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ..; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 00010B91AF7FECD80C7CBF85 /* ReadiumAdapterGCDWebServer */ = { + isa = XCSwiftPackageProductDependency; + productName = ReadiumAdapterGCDWebServer; + }; + 41FF3979DE8082AF5E23D69D /* ReadiumOPDS */ = { + isa = XCSwiftPackageProductDependency; + productName = ReadiumOPDS; + }; + 79CBFD1B8193030A2DB6A839 /* ReadiumStreamer */ = { + isa = XCSwiftPackageProductDependency; + productName = ReadiumStreamer; + }; + 872E0CB31611AD93E229C627 /* ReadiumNavigator */ = { + isa = XCSwiftPackageProductDependency; + productName = ReadiumNavigator; + }; + E01892658E366AE70B7B1386 /* ReadiumShared */ = { + isa = XCSwiftPackageProductDependency; + productName = ReadiumShared; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = DF84942AD828BBDD499F04C0 /* Project object */; +} diff --git a/Support/Carthage/Readium.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Playground/Playground.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Support/Carthage/Readium.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Playground/Playground.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Support/Carthage/Readium.xcodeproj/xcshareddata/xcschemes/ReadiumAdapterGCDWebServer.xcscheme b/Playground/Playground.xcodeproj/xcshareddata/xcschemes/Playground.xcscheme similarity index 62% rename from Support/Carthage/Readium.xcodeproj/xcshareddata/xcschemes/ReadiumAdapterGCDWebServer.xcscheme rename to Playground/Playground.xcodeproj/xcshareddata/xcschemes/Playground.xcscheme index 444e9f643f..42d9cba221 100644 --- a/Support/Carthage/Readium.xcodeproj/xcshareddata/xcschemes/ReadiumAdapterGCDWebServer.xcscheme +++ b/Playground/Playground.xcodeproj/xcshareddata/xcschemes/Playground.xcscheme @@ -1,6 +1,6 @@ + BlueprintIdentifier = "739B1FD817D42F0264714A50" + BuildableName = "Playground.app" + BlueprintName = "Playground" + ReferencedContainer = "container:Playground.xcodeproj"> @@ -29,17 +29,25 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" onlyGenerateCoverageForSpecifiedTargets = "NO"> + + + + + BlueprintIdentifier = "739B1FD817D42F0264714A50" + BuildableName = "Playground.app" + BlueprintName = "Playground" + ReferencedContainer = "container:Playground.xcodeproj"> + + - + + BlueprintIdentifier = "739B1FD817D42F0264714A50" + BuildableName = "Playground.app" + BlueprintName = "Playground" + ReferencedContainer = "container:Playground.xcodeproj"> - + - + + BlueprintIdentifier = "739B1FD817D42F0264714A50" + BuildableName = "Playground.app" + BlueprintName = "Playground" + ReferencedContainer = "container:Playground.xcodeproj"> - + diff --git a/Playground/Playground.xctestplan b/Playground/Playground.xctestplan new file mode 100644 index 0000000000..b3bf0b4dac --- /dev/null +++ b/Playground/Playground.xctestplan @@ -0,0 +1,59 @@ +{ + "configurations" : [ + { + "id" : "8C854D1A-8E77-4ADF-98B5-287FB5B8B996", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : false, + "language" : "en", + "region" : "US", + "targetForVariableExpansion" : { + "containerPath" : "container:Playground.xcodeproj", + "identifier" : "BED194980D56484BC288A866", + "name" : "Playground" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:..", + "identifier" : "ReadiumInternalTests", + "name" : "ReadiumInternalTests" + } + }, + { + "target" : { + "containerPath" : "container:..", + "identifier" : "ReadiumOPDSTests", + "name" : "ReadiumOPDSTests" + } + }, + { + "target" : { + "containerPath" : "container:..", + "identifier" : "ReadiumNavigatorTests", + "name" : "ReadiumNavigatorTests" + } + }, + { + "target" : { + "containerPath" : "container:..", + "identifier" : "ReadiumSharedTests", + "name" : "ReadiumSharedTests" + } + }, + { + "target" : { + "containerPath" : "container:..", + "identifier" : "ReadiumStreamerTests", + "name" : "ReadiumStreamerTests" + } + } + ], + "version" : 1 +} diff --git a/Playground/Sources/App/Common/Extensions/FileManager+Ext.swift b/Playground/Sources/App/Common/Extensions/FileManager+Ext.swift new file mode 100644 index 0000000000..2c683d90a7 --- /dev/null +++ b/Playground/Sources/App/Common/Extensions/FileManager+Ext.swift @@ -0,0 +1,19 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation + +extension FileManager { + /// URL to the Documents/ folder. + var documentDirectory: URL { + try! url( + for: .documentDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) + } +} diff --git a/Playground/Sources/App/Common/Extensions/Logger+Ext.swift b/Playground/Sources/App/Common/Extensions/Logger+Ext.swift new file mode 100644 index 0000000000..3100dc392a --- /dev/null +++ b/Playground/Sources/App/Common/Extensions/Logger+Ext.swift @@ -0,0 +1,19 @@ +// +// Copyright 2026 Readium Foundation. All 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 OSLog + +extension Logger { + /// Initialize a logger with the category set to the `type` name. + init(for type: T.Type) { + self.init(subsystem: Bundle.main.bundleIdentifier!, category: String(describing: type)) + } + + /// Logs `error.localizedDescription` at the `.error` level. + func error(_ error: Error) { + self.error("\(error.localizedDescription)") + } +} diff --git a/Playground/Sources/App/Common/UserError+Readium.swift b/Playground/Sources/App/Common/UserError+Readium.swift new file mode 100644 index 0000000000..66db38bbd8 --- /dev/null +++ b/Playground/Sources/App/Common/UserError+Readium.swift @@ -0,0 +1,99 @@ +// +// Copyright 2026 Readium Foundation. All 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 ReadiumShared +import ReadiumStreamer + +/// Generic fallback message for errors that have no meaningful user-facing +/// description. +let unexpected = "Something went wrong. Please try again." + +// MARK: - ReadiumShared Errors + +extension ReadiumShared.AssetRetrieveError: UserErrorConvertible { + var message: String { + switch self { + case .formatNotSupported: "Unsupported file type. Please try a different file." + case let .reading(error): error.message + } + } +} + +extension ReadiumShared.AssetRetrieveURLError: UserErrorConvertible { + var message: String { + switch self { + case .schemeNotSupported, .formatNotSupported: "Unsupported file type. Please try a different file." + case let .reading(error): error.message + } + } +} + +extension ReadiumShared.ReadError: UserErrorConvertible { + var message: String { + switch self { + case let .access(error): error.message + case .decoding: "We couldn't open this content. The file might be corrupted or use a format we don't support. Please try with a different file." + case .unsupportedOperation: unexpected + } + } +} + +extension ReadiumShared.AccessError: UserErrorConvertible { + var message: String { + switch self { + case let .http(error): error.message + case let .fileSystem(error): error.message + case .other: unexpected + } + } +} + +extension ReadiumShared.FileSystemError: UserErrorConvertible { + var message: String { + switch self { + case .fileNotFound: "Couldn't open file. The file was not found." + case .forbidden: "Cannot open file. Access denied." + case .outOfSpace: "There's not enough disk space to proceed. Please free up some space and try again." + case .io: unexpected + } + } +} + +extension ReadiumShared.HTTPError: UserErrorConvertible { + var message: String { + switch self { + case .malformedRequest, .redirection, .cancelled, .other: + "Something went wrong. Please check your internet connection or try again later." + case .malformedResponse: + "Cannot load this content. There's a problem with the server. Please try again later." + case let .errorResponse(response): + switch response.status { + case .unauthorized: "You need to be signed in to access this content." + case .forbidden: "You don't have permission to access this content." + case .notFound: "Content not found." + case .methodNotAllowed: "The server doesn't support the required loading method." + default: unexpected + } + case .timeout: "Connection timed out. Please try again." + case .unreachable: "Could not connect to the server." + case .security: "Secure connection failed. Please try again later." + case .rangeNotSupported: "The server doesn't support the required loading method." + case .offline: "You're offline. Check your internet connection." + case let .fileSystem(error): error.message + } + } +} + +// MARK: - ReadiumStreamer Errors + +extension ReadiumStreamer.PublicationOpenError: UserErrorConvertible { + var message: String { + switch self { + case .formatNotSupported: "Unsupported file type. Please try a different file." + case let .reading(error): error.message + } + } +} diff --git a/Playground/Sources/App/Common/UserError.swift b/Playground/Sources/App/Common/UserError.swift new file mode 100644 index 0000000000..7b08ed756d --- /dev/null +++ b/Playground/Sources/App/Common/UserError.swift @@ -0,0 +1,118 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation +import OSLog +import SwiftUI + +/// An error that should be displayed to the user. +/// +/// It is similar to a `LocalizedError`, but the message is mandatory, and it +/// references a lower-level error. +struct UserError: LocalizedError { + /// The human-readable message shown to the user. + let message: String + + /// The underlying technical error. + let cause: Error? + + /// Creates a `UserError` from any `Error`. + init(_ error: Error) { + if let error = error as? UserErrorConvertible { + self = error.userError + } else { + self.init(error.localizedDescription, cause: error) + } + } + + /// Creates a `UserError` with an explicit message and an optional cause. + init( + _ message: String, + cause: Error? = nil + ) { + self.message = message + self.cause = cause + } + + /// Satisfies `LocalizedError` — routes `localizedDescription` to `message`. + var errorDescription: String? { + message + } + + /// Logs debugging details about this error. + func log(with logger: Logger = Logger()) { + var details = "" + dump(self, to: &details) + logger.error("\(details)") + } +} + +/// Convenience protocol for an object (usually an ``Error``) that can be +/// converted into a ``UserError``. +protocol UserErrorConvertible { + var message: String { get } + var cause: (any Error)? { get } +} + +extension UserErrorConvertible { + var userError: UserError { + UserError(message, cause: cause) + } +} + +extension UserErrorConvertible where Self: Error { + var cause: Error? { + self + } +} + +extension UserError: UserErrorConvertible {} + +extension String: UserErrorConvertible { + var message: String { + self + } + + var cause: (any Error)? { + nil + } +} + +extension View { + /// Presents an alert when the given `error` binding is set. + func alert(error: Binding) -> some View { + modifier(UserErrorAlertModifier(error: error)) + } +} + +/// ViewModifier that presents a system alert whenever `error` is non-nil. +/// +/// Clears the binding when the user dismisses the alert so it can be triggered +/// again by subsequent errors. +private struct UserErrorAlertModifier: ViewModifier { + @Binding var error: UserError? + + func body(content: Self.Content) -> some View { + content + .alert( + "Error", + isPresented: Binding( + get: { error != nil }, + set: { isPresented, _ in + if !isPresented { + error = nil + } + } + ), + presenting: error, + actions: { _ in }, + message: { error in + Text(error.message) + .onAppear { error.log() } + } + ) + } +} diff --git a/Playground/Sources/App/Common/Views/HTMLText.swift b/Playground/Sources/App/Common/Views/HTMLText.swift new file mode 100644 index 0000000000..a08833f54d --- /dev/null +++ b/Playground/Sources/App/Common/Views/HTMLText.swift @@ -0,0 +1,68 @@ +// +// Copyright 2026 Readium Foundation. All 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 SwiftUI + +/// A SwiftUI `Text` view that renders an HTML string as rich attributed text. +struct HTMLText: View { + /// The raw HTML string to render. + private var text: String + + private enum ParsingResult { + case parsing + case parsed(AttributedString) + case failure(Error) + } + + @State private var state: ParsingResult = .parsing + + /// Creates an `HTMLText` view for the given HTML string. + init(_ text: String) { + self.text = text + } + + var body: some View { + Group { + switch state { + case .parsing: + ProgressView() + case let .parsed(text): + Text(text) + case .failure: + // Show the raw string as a fallback. + Text(text) + } + } + .task(id: text) { + state = await parseHTML(text) + } + } + + /// Converts an HTML string into an `AttributedString` on a background + /// thread. + /// + /// Returns `nil` if parsing fails (e.g. malformed HTML), in which case the + /// raw text will be used as fallback. + private func parseHTML(_ html: String) async -> ParsingResult { + await Task.detached { + do { + return try .parsed(AttributedString( + NSAttributedString( + data: Data(html.utf8), + options: [ + .documentType: NSAttributedString.DocumentType.html, + .characterEncoding: String.Encoding.utf8.rawValue, + ], + documentAttributes: nil + ), + including: \.swiftUI + )) + } catch { + return .failure(error) + } + }.value + } +} diff --git a/Playground/Sources/App/Common/Views/JSONView.swift b/Playground/Sources/App/Common/Views/JSONView.swift new file mode 100644 index 0000000000..3cc4f421ba --- /dev/null +++ b/Playground/Sources/App/Common/Views/JSONView.swift @@ -0,0 +1,94 @@ +// +// Copyright 2026 Readium Foundation. All 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 ReadiumShared +import SwiftUI + +/// A scrollable view that displays a JSON dictionary with syntax highlighting. +/// +/// Serialization and colorization run on a detached background task to keep the UI +/// responsive for large manifests. A `ProgressView` is shown until the result is ready. +struct JSONView: View { + /// The JSON dictionary to render. + var json: [String: Any] + + /// The colorized attributed text; `nil` while the background task is running. + @State private var attributedText: AttributedString? + + /// Holds any serialization error to display in an alert. + @State private var error: UserError? + + var body: some View { + ScrollView { + if let attributedText { + Text(attributedText) + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + } else { + ProgressView() + } + } + .alert(error: $error) + .task { + let json = json + do { + attributedText = try await Task.detached(priority: .userInitiated) { + try await colorizeJSON(json) + }.value + + } catch { + self.error = UserError(error) + } + } + } + + /// Serializes `json` to a pretty-printed string and applies token-level syntax highlighting. + /// + /// Uses a greedy left-to-right regex pass with a `claimed` bitmap to ensure each + /// character is colored by at most one pattern (keys take precedence over string values). + /// + /// Color scheme: + /// - **Keys** (string before `:`): green + bold + /// - **String values**: blue + /// - **Numbers**: orange + /// - **Booleans**: purple + /// - **null**: grey + @concurrent private func colorizeJSON(_ json: [String: Any]) async throws -> AttributedString { + let data = try JSONSerialization.data( + withJSONObject: json, + options: [.prettyPrinted, .withoutEscapingSlashes] + ) + + let jsonString = String(data: data, encoding: .utf8)! + let length = (jsonString as NSString).length + let fullRange = NSRange(location: 0, length: length) + + let attributed = NSMutableAttributedString(string: jsonString) + var claimed = [Bool](repeating: false, count: length) + + for (regex, color) in await Self.patterns { + for match in regex.matches(in: jsonString, range: fullRange) { + let range = match.range + let end = range.location + range.length + guard !(range.location ..< end).contains(where: { claimed[$0] }) else { continue } + for i in range.location ..< end { + claimed[i] = true + } + attributed.addAttribute(.foregroundColor, value: color, range: range) + } + } + + return try AttributedString(attributed, including: \.uiKit) + } + + static let patterns: [(NSRegularExpression, UIColor)] = [ + (try! NSRegularExpression(pattern: #""(?:[^"\\]|\\.)*"(?=\s*:)"#), .systemGreen), + (try! NSRegularExpression(pattern: #""(?:[^"\\]|\\.)*""#), .systemBlue), + (try! NSRegularExpression(pattern: #"-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?"#), .systemOrange), + (try! NSRegularExpression(pattern: #"\b(?:true|false)\b"#), .systemPurple), + (try! NSRegularExpression(pattern: #"\bnull\b"#), .systemGray), + ] +} diff --git a/Playground/Sources/App/Data/DocumentList.swift b/Playground/Sources/App/Data/DocumentList.swift new file mode 100644 index 0000000000..34b17c99cd --- /dev/null +++ b/Playground/Sources/App/Data/DocumentList.swift @@ -0,0 +1,83 @@ +// +// Copyright 2026 Readium Foundation. All 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 ReadiumShared +import SwiftUI + +/// Sidebar list of publication files stored in the app's Documents directory. +/// +/// Provides a toolbar button to import new files via the system file picker, +/// handles files opened from other apps via `onOpenURL`, and supports swipe-to- +/// delete. +struct DocumentList: View { + /// The currently selected file URL, shared with the detail pane. + @Binding var selectedFile: URL? + + /// Injected store that tracks the Documents directory. + @EnvironmentObject var documentRepository: DocumentRepository + + /// Controls whether the system file picker sheet is shown. + @State private var showFileImporter: Bool = false + + /// Holds the last error to be displayed in an alert. + @State private var error: UserError? + + var body: some View { + List(selection: $selectedFile) { + ForEach(documentRepository.documents, id: \.self) { file in + Text(file.lastPathComponent) + } + .onDelete { + delete(atOffsets: $0) + } + } + .navigationTitle("Documents") + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button(action: { + showFileImporter = true + }) { + Image(systemName: "document.badge.plus") + } + } + } + .fileImporter( + isPresented: $showFileImporter, + allowedContentTypes: DocumentTypes.main.supportedUTTypes + ) { result in + add(file: try! result.get()) + } + .onOpenURL { + add(file: $0) + } + .alert(error: $error) + } + + /// Copies `file` into the Documents/ directory via the repository. + private func add(file: URL) { + do { + try documentRepository.add(file: file) + } catch { + self.error = UserError(error) + } + } + + /// Deletes the files at `offsets`. + private func delete(atOffsets offsets: IndexSet) { + do { + for file in documentRepository.get(atOffsets: offsets) { + try documentRepository.remove(file) + + if selectedFile == file { + selectedFile = nil + } + } + + } catch { + self.error = UserError(error) + } + } +} diff --git a/Playground/Sources/App/Data/DocumentRepository.swift b/Playground/Sources/App/Data/DocumentRepository.swift new file mode 100644 index 0000000000..79d0400185 --- /dev/null +++ b/Playground/Sources/App/Data/DocumentRepository.swift @@ -0,0 +1,117 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation +import OSLog + +/// Observable store for the publication files in the app's Documents directory. +@MainActor final class DocumentRepository: ObservableObject { + /// The current list of publication files, sorted alphabetically by + /// filename. + @Published private(set) var documents: [URL] = [] + + /// The app's Documents directory. + private let directory = FileManager.default.documentDirectory + + /// Low-level filesystem event source that triggers `loadDocuments()` on any + /// change. + private var dispatchSource: DispatchSourceFileSystemObject? + + private let logger = Logger(for: DocumentRepository.self) + + init() { + watchDirectory() + } + + deinit { + dispatchSource?.cancel() + } + + /// Returns the files at the given index offsets in the current `documents` + /// list. + func get(atOffsets offsets: IndexSet) -> [URL] { + offsets.compactMap { documents.getOrNil($0) } + } + + /// Copies `file` into the Documents directory, replacing any existing file + /// with the same name. + /// + func add(file: URL) throws { + // Security-scoped access is acquired and released after the copy so + // the app can read files selected through the system file picker or + // shared via the Files app. + let isSecurityScoped = file.startAccessingSecurityScopedResource() + defer { + if isSecurityScoped { + file.stopAccessingSecurityScopedResource() + } + } + + let target = directory.appendingPathComponent(file.lastPathComponent) + try? FileManager.default.removeItem(at: target) + try FileManager.default.copyItem(at: file, to: target) + } + + /// Permanently deletes `file` from the Documents directory. + func remove(_ file: URL) throws { + try FileManager.default.removeItem(at: file) + } + + // MARK: - Load and Watch Documents + + /// Begins watching the Documents directory for filesystem events. + private func watchDirectory() { + let path = directory.path + let fileDescriptor = open(path, O_EVTONLY) + guard fileDescriptor != -1 else { + logger.fault("Failed to open directory at \(path)") + return + } + + dispatchSource = DispatchSource.makeFileSystemObjectSource( + fileDescriptor: fileDescriptor, + eventMask: .all, + queue: .global() + ) + + dispatchSource?.setEventHandler { [weak self] in + Task { @MainActor in + self?.loadDocuments() + } + } + + dispatchSource?.setCancelHandler { + close(fileDescriptor) + } + + dispatchSource?.resume() + + loadDocuments() + + logger.notice("Watching directory at \(path)") + } + + /// Reads the Documents directory and updates `documents` with the sorted + /// file list. + private func loadDocuments() { + do { + documents = try FileManager.default + .contentsOfDirectory( + at: directory, + includingPropertiesForKeys: [.isDirectoryKey], + options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants] + ) + // Filter out directories. + .filter { url in + !((try? url.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false) + } + .sorted { $0.lastPathComponent < $1.lastPathComponent } + + } catch { + logger.error(error) + } + } +} diff --git a/Playground/Sources/App/PlaygroundApp.swift b/Playground/Sources/App/PlaygroundApp.swift new file mode 100644 index 0000000000..d382cec858 --- /dev/null +++ b/Playground/Sources/App/PlaygroundApp.swift @@ -0,0 +1,43 @@ +// +// Copyright 2026 Readium Foundation. All 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 OSLog +import SwiftUI + +@main +struct PlaygroundApp: App { + /// Shared store for publication files in the app's Documents directory. + @StateObject private var documentRepository = DocumentRepository() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(documentRepository) + } + } +} + +/// Root layout: a two-column split view with the document list in the sidebar +/// and a `PublicationView` in the detail column. +struct ContentView: View { + /// The file URL selected in the sidebar, or `nil` when nothing is selected. + @State private var selectedFile: URL? + + var body: some View { + NavigationSplitView { + DocumentList(selectedFile: $selectedFile) + } detail: { + if let selectedFile { + PublicationView(file: selectedFile) + .id(selectedFile) + + } else { + Text("No Publication Selected") + .font(.title2) + } + } + } +} diff --git a/Playground/Sources/App/Publication/PublicationMetadataView.swift b/Playground/Sources/App/Publication/PublicationMetadataView.swift new file mode 100644 index 0000000000..d9c3d305f2 --- /dev/null +++ b/Playground/Sources/App/Publication/PublicationMetadataView.swift @@ -0,0 +1,145 @@ +// +// Copyright 2026 Readium Foundation. All 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 ReadiumShared +import SwiftUI + +/// Full metadata display for a publication. +struct PublicationMetadataView: View { + let metadata: PublicationMetadata + + var body: some View { + List { + aboutSection + technicalSection + descriptionSection + } + .navigationTitle("Metadata") + } + + /// Human-readable bibliographic information: title, contributors, dates, + /// language, etc. + private var aboutSection: some View { + Section("About") { + if let title = metadata.title { + LabeledContent("Title", value: title) + } + + if let subtitle = metadata.subtitle { + LabeledContent("Subtitle", value: subtitle) + } + + if !metadata.contributors.authors.isEmpty { + LabeledContent("Author", value: metadata.contributors.authors.joined(separator: ", ")) + } + + if !metadata.contributors.publishers.isEmpty { + LabeledContent("Publisher", value: metadata.contributors.publishers.joined(separator: ", ")) + } + + if let published = metadata.published { + LabeledContent("Published", value: published.formatted(date: .long, time: .omitted)) + } + + if let modified = metadata.modified { + LabeledContent("Modified", value: modified.formatted(date: .long, time: .omitted)) + } + + if let language = metadata.language { + // localizedDescription() is a convenience API from Readium that + // will display the name of the language in the system language. + LabeledContent("Language", value: language.localizedDescription()) + } + + if let pages = metadata.numberOfPages { + LabeledContent("Pages", value: "\(pages)") + } + + if let duration = metadata.duration?.formatted() { + LabeledContent("Duration", value: duration) + } + + if !metadata.subjects.isEmpty { + LabeledContent("Subjects", value: metadata.subjects.joined(separator: ", ")) + } + + if !metadata.series.isEmpty { + LabeledContent("Series", value: metadata.series.joined(separator: ", ")) + } + + if !metadata.collections.isEmpty { + LabeledContent("Collections", value: metadata.collections.joined(separator: ", ")) + } + + NavigationLink("Contributors") { + contributorsDetailView + } + } + } + + private var technicalSection: some View { + Section("Technical") { + if let identifier = metadata.identifier { + LabeledContent("Identifier", value: identifier) + } + + ForEach(metadata.profiles, id: \.self) { profile in + LabeledContent("Profile", value: profile.uri) + } + + if let layout = metadata.layout { + LabeledContent("Layout", value: layout.rawValue) + } + } + } + + /// The publication's description/synopsis, if present. + /// + /// The `description` field may contain HTML markup (e.g. ``, ``, + /// `

`). `HTMLText` parses it into an `AttributedString` for rich + /// rendering. + @ViewBuilder private var descriptionSection: some View { + if let description = metadata.description { + Section("Description") { + HTMLText(description) + .fixedSize(horizontal: false, vertical: true) + } + } + } + + // MARK: - Contributors Detail + + /// Drill-down view listing every contributor grouped by role. + private var contributorsDetailView: some View { + List { + contributorSection(of: "Authors", with: metadata.contributors.authors) + contributorSection(of: "Translators", with: metadata.contributors.translators) + contributorSection(of: "Editors", with: metadata.contributors.editors) + contributorSection(of: "Artists", with: metadata.contributors.artists) + contributorSection(of: "Illustrators", with: metadata.contributors.illustrators) + contributorSection(of: "Letterers", with: metadata.contributors.letterers) + contributorSection(of: "Pencilers", with: metadata.contributors.pencilers) + contributorSection(of: "Colorists", with: metadata.contributors.colorists) + contributorSection(of: "Inkers", with: metadata.contributors.inkers) + contributorSection(of: "Narrators", with: metadata.contributors.narrators) + contributorSection(of: "Contributors", with: metadata.contributors.contributors) + contributorSection(of: "Publishers", with: metadata.contributors.publishers) + contributorSection(of: "Imprints", with: metadata.contributors.imprints) + } + .navigationTitle("Contributors") + } + + /// Renders a titled section for one contributor role. + @ViewBuilder private func contributorSection(of role: String, with names: [String]) -> some View { + if !names.isEmpty { + Section(role) { + ForEach(names, id: \.self) { name in + Text(name) + } + } + } + } +} diff --git a/Playground/Sources/App/Publication/PublicationView.swift b/Playground/Sources/App/Publication/PublicationView.swift new file mode 100644 index 0000000000..faa8a01769 --- /dev/null +++ b/Playground/Sources/App/Publication/PublicationView.swift @@ -0,0 +1,98 @@ +// +// Copyright 2026 Readium Foundation. All 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 ReadiumShared +import SwiftUI + +/// Detail view that opens a publication file. +struct PublicationView: View { + /// The publication file to open. + let file: URL + + /// The opened publication, set after `load()` completes successfully. + @State private var publication: Publication? + + /// The publication's cover image, fetched after `publication` is set. + @State private var cover: UIImage? + + /// Holds the last loading error. + @State private var error: UserError? + + var body: some View { + NavigationStack { + Group { + if let publication { + List { + coverSection + + NavigationLink("Metadata") { + PublicationMetadataView(metadata: readMetadata(of: publication)) + } + + NavigationLink("JSON Manifest") { + JSONView(json: publication.manifest.json) + .navigationTitle("JSON Manifest") + } + } + .listStyle(.insetGrouped) + } else if let error { + VStack(spacing: 12) { + Image(systemName: "exclamationmark.triangle") + .font(.largeTitle) + .foregroundStyle(.secondary) + Text(error.message) + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + } + .padding() + } else { + ProgressView() + } + } + .navigationTitle(publication?.metadata.title ?? "") + .navigationBarTitleDisplayMode(.inline) + .alert(error: $error) + .task { + await load() + } + } + } + + /// Opens the publication file and fetches its cover image. + /// + /// - `file.fileURL` is a safety guard: `DocumentRepository` always vends `file://` + /// URLs, but the check future-proofs against changes to that assumption. + private func load() async { + do { + guard let url = file.anyURL.absoluteURL else { + error = UserError("Not a valid absolute URL") + return + } + let result = try await openPublication(at: url) + publication = result.publication + cover = try? await result.publication.cover().get() + } catch { + self.error = UserError(error) + } + } + + /// Renders the cover image in a full-width card. + @ViewBuilder private var coverSection: some View { + if let cover { + Section { + Image(uiImage: cover) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 300) + .cornerRadius(4) + .shadow(radius: 4) + .padding(20) + .frame(maxWidth: .infinity) + } + .listRowBackground(Color.clear) + } + } +} diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29.png new file mode 100644 index 0000000000..cd4dc66c8b Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29.png differ diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x-1.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x-1.png new file mode 100644 index 0000000000..456b80d9a3 Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x-1.png differ diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png new file mode 100644 index 0000000000..456b80d9a3 Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png differ diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@3x.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@3x.png new file mode 100644 index 0000000000..3a19c7035e Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@3x.png differ diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40.png new file mode 100644 index 0000000000..d76f2d95f1 Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40.png differ diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x-1.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x-1.png new file mode 100644 index 0000000000..e23dbe3ce9 Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x-1.png differ diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png new file mode 100644 index 0000000000..e23dbe3ce9 Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png differ diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@3x.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@3x.png new file mode 100644 index 0000000000..271d22bb33 Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@3x.png differ diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png new file mode 100644 index 0000000000..271d22bb33 Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png differ diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@3x.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@3x.png new file mode 100644 index 0000000000..5b50d102ef Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@3x.png differ diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon76x76.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon76x76.png new file mode 100644 index 0000000000..5aebd3ef25 Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon76x76.png differ diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon76x76@2x.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon76x76@2x.png new file mode 100644 index 0000000000..cd8c38fc6f Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon76x76@2x.png differ diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..17f333f5dc --- /dev/null +++ b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,112 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "AppIcon29x29@2x-1.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "AppIcon29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "AppIcon40x40@2x-1.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "AppIcon40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "AppIcon60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "AppIcon60x60@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "AppIcon29x29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "AppIcon29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "AppIcon40x40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "AppIcon40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "AppIcon76x76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "AppIcon76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "readiumlogo_2048-83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "icon_1024x1024.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png new file mode 100644 index 0000000000..1bd3cf9765 Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png differ diff --git a/Playground/Sources/Assets.xcassets/AppIcon.appiconset/readiumlogo_2048-83.5@2x.png b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/readiumlogo_2048-83.5@2x.png new file mode 100644 index 0000000000..b95bd95c9e Binary files /dev/null and b/Playground/Sources/Assets.xcassets/AppIcon.appiconset/readiumlogo_2048-83.5@2x.png differ diff --git a/Playground/Sources/Assets.xcassets/Contents.json b/Playground/Sources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Playground/Sources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Playground/Sources/Info.plist b/Playground/Sources/Info.plist new file mode 100644 index 0000000000..b1ba4a6f59 --- /dev/null +++ b/Playground/Sources/Info.plist @@ -0,0 +1,349 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Playground + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Playground + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleVersion + 1 + CFBundleShortVersionString + 1.0 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSExceptionDomains + + crl.edrlab.telesec.de + + NSExceptionAllowsInsecureHTTPLoads + + + + + UIBackgroundModes + + audio + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportsDocumentBrowser + + CFBundleDocumentTypes + + + CFBundleTypeName + ZIP Archive + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + public.zip-archive + + + + CFBundleTypeName + EPUB + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + org.idpf.epub-container + + + + CFBundleTypeName + PDF Document + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + com.adobe.pdf + + + + CFBundleTypeName + Comic Book ZIP Archive + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + com.macitbetter.cbz-archive + public.cbz-archive + cx.c3.cbz-archive + com.milke.cbz-archive + com.bitcartel.comicbooklover.cbz + public.archive.cbz + + + + CFBundleTypeName + Readium Audiobook Manifest + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + org.readium.audiobook-manifest + + + + CFBundleTypeName + Readium Web Publication Manifest + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + public.json + + + + CFBundleTypeName + Readium Audiobook + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + org.readium.audiobook + + + + CFBundleTypeName + Audiobook Archive + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + org.readium.zab + + + + CFBundleTypeName + Audio File + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + public.audio + + + + CFBundleTypeName + LCP License Document + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + org.readium.lcpl + + + + CFBundleTypeName + LCP-Protected Audiobook + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + org.readium.lcpa + + + + CFBundleTypeName + LCP-Protected PDF + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + org.readium.lcpdf + + + + UTImportedTypeDeclarations + + + UTTypeConformsTo + + public.content + public.data + public.json + + UTTypeDescription + LCP License Document + UTTypeIdentifier + org.readium.lcpl + UTTypeTagSpecification + + public.filename-extension + lcpl + public.mime-type + application/vnd.readium.lcp.license.v1.0+json + + + + UTTypeConformsTo + + public.content + public.data + public.archive + public.zip-archive + + UTTypeDescription + LCP-Protected PDF + UTTypeIdentifier + org.readium.lcpdf + UTTypeTagSpecification + + public.filename-extension + lcpdf + public.mime-type + application/pdf+lcp + + + + UTTypeConformsTo + + public.content + public.data + public.archive + public.zip-archive + + UTTypeDescription + LCP-Protected Audiobook + UTTypeIdentifier + org.readium.lcpa + UTTypeTagSpecification + + public.filename-extension + lcpa + public.mime-type + application/audiobook+lcp + + + + UTTypeConformsTo + + public.data + public.archive + public.zip-archive + + UTTypeDescription + Readium Audiobook + UTTypeIdentifier + org.readium.audiobook + UTTypeTagSpecification + + public.filename-extension + audiobook + public.mime-type + application/audiobook+zip + + + + UTTypeConformsTo + + public.data + public.content + + UTTypeDescription + Readium Audiobook Manifest + UTTypeIdentifier + org.readium.audiobook-manifest + UTTypeTagSpecification + + public.mime-type + application/audiobook+json + + + + UTTypeConformsTo + + public.data + public.archive + public.zip-archive + + UTTypeDescription + Audiobook Archive + UTTypeIdentifier + org.readium.zab + UTTypeTagSpecification + + public.filename-extension + zab + public.mime-type + application/x.readium.zab+zip + + + + UTTypeConformsTo + + public.data + public.archive + public.zip-archive + + UTTypeDescription + Comic Book ZIP Archive + UTTypeIdentifier + cx.c3.cbz-archive + UTTypeTagSpecification + + com.apple.ostype + + CBZ + + public.filename-extension + cbz + public.mime-type + application/vnd.comicbook+zip + + + + + diff --git a/Playground/Sources/Recipes/A01-OpenPublication.swift b/Playground/Sources/Recipes/A01-OpenPublication.swift new file mode 100644 index 0000000000..5c1ac0efd7 --- /dev/null +++ b/Playground/Sources/Recipes/A01-OpenPublication.swift @@ -0,0 +1,67 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumShared +import ReadiumStreamer + +/// The result of opening a publication: the parsed `Publication` model and its +/// detected format. +/// +/// `format` identifies the publication type (EPUB, PDF, audiobook, etc.). +/// You may persist `format.mediaType` in your bookshelf database and pass it +/// back to `AssetRetriever.retrieve(url:mediaType:)` to skip format detection +/// on re-open. +struct OpenedPublication { + let publication: Publication + let format: Format +} + +/// Opens a publication file. +func openPublication(at url: AbsoluteURL) async throws -> OpenedPublication { + // MARK: 1. Setup dependencies + + // An HTTP client is required even for local files because some publications + // reference remote resources. + let httpClient = DefaultHTTPClient() + + // The AssetRetriever provides read access to the content of a file and + // sniffs its format. It takes an HTTP client because it supports file + // served on a remote HTTP server. + let assetRetriever = AssetRetriever(httpClient: httpClient) + + // The PublicationOpener parses an Asset into a full Publication object. + let publicationOpener = PublicationOpener( + // DefaultPublicationParser handles all the formats supported by Readium + // out of the box (EPUB, PDF, audiobooks, etc.). + parser: DefaultPublicationParser( + httpClient: httpClient, + assetRetriever: assetRetriever, + pdfFactory: DefaultPDFDocumentFactory() + ) + ) + + // MARK: 2. Opens the file and sniffs its format. + + let asset = try await assetRetriever + .retrieve(url: url) + .get() + + // MARK: 3. Parse the asset into a Publication model. + + // Set allowUserInteraction to false if you are opening the publication + // from the background or in a batch, to prevent Readium from displaying a + // user interface. For example, this is used with Content Protections + // (e.g. LCP) to request credentials to unlock the publication. + let publication = try await publicationOpener + .open(asset: asset, allowUserInteraction: true) + .get() + + return OpenedPublication( + publication: publication, + format: asset.format + ) +} diff --git a/Playground/Sources/Recipes/A02-ReadMetadata.swift b/Playground/Sources/Recipes/A02-ReadMetadata.swift new file mode 100644 index 0000000000..a361b26cc3 --- /dev/null +++ b/Playground/Sources/Recipes/A02-ReadMetadata.swift @@ -0,0 +1,164 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumShared + +/// A simplified view of a publication's metadata. +/// +/// Readium's `Metadata` type covers the full Readium Web Publication spec. +/// This struct extracts the fields most useful for a bookshelf or reading app +/// and converts them to their most useful representation (e.g. a String for +/// authors' name). +struct PublicationMetadata { + // MARK: - About + + /// The title of the publication. + let title: String? + + /// The subtitle of the publication + let subtitle: String? + + /// Date of publication. + let published: Date? + + /// Date of modification. + let modified: Date? + + /// Language of this publication. + let language: Language? + + /// Thematic keywords (BISAC, THEMA, etc.) + let subjects: [String] + + /// Number of pages in a pre-paginated publication. + let numberOfPages: Int? + + /// Duration in seconds in an audio publication. + let duration: Duration? + + /// A description for the publication. + /// Warning: It may contain HTML markup. + let description: String? + + /// A series groups related volumes in a defined reading order + /// (e.g. "A Song of Ice and Fire", position 3). + let series: [String] + + /// Collections are named groupings without an implied reading order + /// (e.g. a publisher catalogue or award shortlist). + let collections: [String] + + /// List of contributors grouped by their roles. + let contributors: Contributors + + /// All contributor roles defined by the Readium Web Publication spec. + struct Contributors { + let authors: [String] + let translators: [String] + let editors: [String] + let artists: [String] + let illustrators: [String] + let letterers: [String] + let pencilers: [String] + let colorists: [String] + let inkers: [String] + let narrators: [String] + let contributors: [String] + let publishers: [String] + let imprints: [String] + } + + // MARK: - Technical + + /// The publication's unique ID — often an ISBN for books or a UUID assigned + /// by the authoring tool. + /// + /// For an EPUB, this is sourced from `dc:identifier`. + let identifier: String? + + /// A URI declaring which Readium Web Publication profile this publication + /// conforms to (e.g. `https://readium.org/webpub-manifest/profiles/epub`). + /// The profile determines which navigator and features apply. + let profiles: [Publication.Profile] + + /// Presentation mode for the content. + /// + /// - **reflowable** text reflows to the screen size (typical for novels) + /// - **fixed** layout preserves exact page geometry (picture books, comics, + /// or complex designs). + /// - **scrolled** displays in a continuous scroll (web toons) + let layout: Layout? +} + +/// Extracts a simplified metadata view from an opened publication. +func readMetadata(of publication: Publication) -> PublicationMetadata { + let m = publication.metadata + + /// Contributors can have a display name different from the sort name. For + /// example, "Albert Camus" could be sorted as "Camus, Albert". + func names(_ contributors: [Contributor]) -> [String] { + contributors + .sorted { ($0.sortAs ?? $0.name) < ($1.sortAs ?? $1.name) } + .map(\.name) + } + + /// Subjects can also be sorted using a different sort name. + func subjects(_ src: [Subject]) -> [String] { + src + .sorted { ($0.sortAs ?? $0.name) < ($1.sortAs ?? $1.name) } + .map(\.name) + } + + /// Series and collections can also be sorted using a different sort name. + /// + /// They can also have a position that determines the reading-order of the + /// series. In this example, we will format the book 5 of A Song of Ice and + /// Fire as "A Song of Ice and Fire (5)" + func collections(_ src: [Metadata.Collection]) -> [String] { + src + .sorted { ($0.sortAs ?? $0.name) < ($1.sortAs ?? $1.name) } + .map { + var name = $0.name + if let position = $0.position { + name += " (\(position.formatted(.number)))" + } + return name + } + } + + return PublicationMetadata( + title: m.title, + subtitle: m.subtitle, + published: m.published, + modified: m.modified, + language: m.language, + subjects: subjects(m.subjects), + numberOfPages: m.numberOfPages, + duration: m.duration.map { Duration.seconds($0) }, + description: m.description, + series: collections(m.belongsToSeries), + collections: collections(m.belongsToCollections), + contributors: .init( + authors: names(m.authors), + translators: names(m.translators), + editors: names(m.editors), + artists: names(m.artists), + illustrators: names(m.illustrators), + letterers: names(m.letterers), + pencilers: names(m.pencilers), + colorists: names(m.colorists), + inkers: names(m.inkers), + narrators: names(m.narrators), + contributors: names(m.contributors), + publishers: names(m.publishers), + imprints: names(m.imprints) + ), + identifier: m.identifier, + profiles: publication.manifest.metadata.conformsTo, + layout: m.layout + ) +} diff --git a/Playground/project.yml b/Playground/project.yml new file mode 100644 index 0000000000..fc01d7fabc --- /dev/null +++ b/Playground/project.yml @@ -0,0 +1,37 @@ +name: Playground +options: + bundleIdPrefix: org.readium +packages: + Readium: + path: .. +schemes: + Playground: + build: + targets: + Playground: + test: + testPlans: + - path: Playground.xctestplan + defaultPlan: true +targets: + Playground: + type: application + platform: iOS + deploymentTarget: "16.0" + sources: + - path: Sources + dependencies: + - package: Readium + product: ReadiumShared + - package: Readium + product: ReadiumStreamer + - package: Readium + product: ReadiumNavigator + - package: Readium + product: ReadiumAdapterGCDWebServer + - package: Readium + product: ReadiumOPDS + settings: + SWIFT_APPROACHABLE_CONCURRENCY: Yes + SWIFT_DEFAULT_ACTOR_ISOLATION: MainActor + diff --git a/README.md b/README.md index e398b4975b..fa817b62ae 100644 --- a/README.md +++ b/README.md @@ -7,53 +7,67 @@ ## Features -✅ Implemented      🚧 Partially implemented      📆 Planned      👀 Want to do      ❓ Not planned +✅ Implemented      🚧 Partially implemented      📆 Planned      👀 Want to do      ❌ Not planned ### Formats -| Format | Status | -|---|:---:| -| EPUB 2 | ✅ | -| EPUB 3 | ✅ | -| Readium Web Publication | 🚧 | -| PDF | ✅ | -| Readium Audiobook | ✅ | -| Zipped Audiobook | ✅ | -| Standalone audio files (MP3, AAC, etc.) | ✅ | -| Readium Divina | 🚧 | -| CBZ (Comic Book ZIP) | 🚧 | -| CBR (Comic Book RAR) | ❓ | -| [DAISY](https://daisy.org/activities/standards/daisy/) | 👀 | +#### Ebook and Document Formats + +| Format | Status | +|--------------------------------------------------------|:------:| +| EPUB (reflowable) | ✅ | +| EPUB (fixed-layout) | ✅ | +| PDF | ✅ | +| Readium Web Publication | 🚧 | +| [DAISY](https://daisy.org/activities/standards/daisy/) | 👀 | + +#### Audiobook Formats + +| Format | Status | +|--------------------------------------------------------|:------:| +| Readium Audiobook | ✅ | +| Zipped Audiobook | ✅ | +| Standalone audio files (MP3, AAC, etc.) | ✅ | +| [DAISY](https://daisy.org/activities/standards/daisy/) | 👀 | + +#### Comic Formats + +| Format | Status | +|----------------------|:------:| +| Readium Divina | ✅ | +| CBZ (Comic Book ZIP) | ✅ | +| CBR (Comic Book RAR) | ❌ | ### Features A number of features are implemented only for some publication formats. -| Feature | EPUB (reflow) | EPUB (FXL) | PDF | -|---|:---:|:---:|:---:| -| Pagination | ✅ | ✅ | ✅ | -| Scrolling | ✅ | 👀 | ✅ | -| Right-to-left (RTL) | ✅ | ✅ | ✅ | -| Search in textual content | ✅ | ✅ | 👀 | -| Highlighting (Decoration API) | ✅ | ✅ | 👀 | -| Text-to-speech (TTS) | ✅ | ✅ | 👀 | -| Media overlays | 📆 | 📆 | | +| Feature | EPUB (reflow) | EPUB (FXL) | PDF | +|-------------------------------|:-------------:|:----------:|:---:| +| Pagination | ✅ | ✅ | ✅ | +| Scrolling | ✅ | 👀 | ✅ | +| Right-to-left (RTL) | ✅ | ✅ | ✅ | +| Search in textual content | ✅ | ✅ | 👀 | +| Highlighting (Decoration API) | ✅ | ✅ | 👀 | +| Text-to-speech (TTS) | ✅ | ✅ | 👀 | +| Media overlays | 📆 | 📆 | | ### OPDS Support -| Feature | Status | -|---|:---:| -| [OPDS Catalog 1.2](https://specs.opds.io/opds-1.2) | ✅ | -| [OPDS Catalog 2.0](https://drafts.opds.io/opds-2.0) | ✅ | -| [Authentication for OPDS](https://drafts.opds.io/authentication-for-opds-1.0.html) | 📆 | -| [Readium LCP Automatic Key Retrieval](https://readium.org/lcp-specs/notes/lcp-key-retrieval.html) | 📆 | +| Feature | Status | +|---------------------------------------------------------------------------------------------------|:------:| +| [OPDS Catalog 1.2](https://specs.opds.io/opds-1.2) | ✅ | +| [OPDS Catalog 2.0](https://drafts.opds.io/opds-2.0) | ✅ | +| [Authentication for OPDS](https://drafts.opds.io/authentication-for-opds-1.0.html) | 📆 | +| [OPDS Progression](https://github.com/opds-community/drafts/pull/91) | 📆 | +| [Readium LCP Automatic Key Retrieval](https://readium.org/lcp-specs/notes/lcp-key-retrieval.html) | 📆 | ### DRM Support -| Feature | Status | -|---|:---:| -| [Readium LCP](https://www.edrlab.org/projects/readium-lcp/) | ✅ | -| [Adobe ACS](https://www.adobe.com/fr/solutions/ebook/content-server.html) | ❓ | +| Feature | Status | +|---------------------------------------------------------------------------|:------:| +| [Readium LCP](https://www.edrlab.org/projects/readium-lcp/) | ✅ | +| [Adobe ACS](https://www.adobe.com/fr/solutions/ebook/content-server.html) | ❌ | ## User Guides @@ -72,8 +86,9 @@ Guides are available to help you make the most of the toolkit. * [Navigator](docs/Guides/Navigator/Navigator.md) - an overview of the Navigator to render a `Publication`'s content to the user * [Configuring the Navigator](docs/Guides/Navigator/Preferences.md) – setup and render Navigator user preferences (font size, colors, etc.) -* [Font families in the EPUB navigator](docs/Guides/Navigator/EPUB%20Fonts.md) – support custom font families with reflowable EPUB publications * [Integrating the Navigator with SwiftUI](docs/Guides/Navigator/SwiftUI.md) – glue to setup the Navigator in a SwiftUI application +* [Implementing Highlights](docs/Guides/Navigator/Highlights.md) – add and manage highlights in a publication +* [Font families in the EPUB navigator](docs/Guides/Navigator/EPUB%20Fonts.md) – support custom font families with reflowable EPUB publications ### DRM @@ -87,7 +102,8 @@ Guides are available to help you make the most of the toolkit. | Readium | iOS | Swift compiler | Xcode | |-----------|------|----------------|-------| -| `develop` | 13.4 | 6.0 | 16.2 | +| `develop` | 15.0 | 6.0 | 16.4 | +| 3.8.0 | 15.0 | 6.0 | 16.4 | | 3.0.0 | 13.4 | 5.10 | 15.4 | | 2.5.1 | 11.0 | 5.6.1 | 13.4 | | 2.5.0 | 10.0 | 5.6.1 | 13.4 | @@ -96,7 +112,7 @@ Guides are available to help you make the most of the toolkit. ### Dependencies -The toolkit's libraries are distributed with [Swift Package Manager](#swift-package-manager) (recommended), [Carthage](#carthage) and [CocoaPods](#cocoapods). It's also possible to clone the repository (or a fork) and [depend on the libraries locally](#local-git-clone). +The toolkit's libraries are distributed with [Swift Package Manager](#swift-package-manager) (recommended) and [CocoaPods](#cocoapods). It's also possible to clone the repository (or a fork) and [depend on the libraries locally](#local-git-clone). The [Test App](TestApp) contains examples on how to use all these dependency managers. @@ -108,33 +124,6 @@ You are then free to add one or more Readium libraries to your application. They If you're stuck, find more information at [developer.apple.com](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app). -#### Carthage - -Add the following to your `Cartfile`: - -``` -github "readium/swift-toolkit" ~> 3.4.0 -``` - -Then, [follow the usual Carthage steps](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) to add the Readium libraries to your project. - -Note that Carthage will build all Readium modules and their dependencies, but you are free to add only the ones you are actually using. The Readium libraries are designed to work independently. - -Refer to the following table to know which dependencies are required for each Readium library. - -| | `ReadiumShared` | `ReadiumStreamer` | `ReadiumNavigator` | `ReadiumOPDS` | `ReadiumLCP` | `ReadiumAdapterGCDWebServer` | `ReadiumAdapterLCPSQLite` | -|------------------------|:------------------:|:------------------:|:------------------:|:------------------:|:------------------:|------------------------------|---------------------------| -| **`ReadiumShared`** | | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| **`ReadiumInternal`** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| `CryptoSwift` | | :heavy_check_mark: | | | :heavy_check_mark: | | | -| `DifferenceKit` | | | :heavy_check_mark: | | | | | -| `ReadiumFuzi` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| `ReadiumGCDWebServer` | | | | | | :heavy_check_mark: | | -| `ReadiumZIPFoundation` | :heavy_check_mark: | | | | :heavy_check_mark: | | | -| `Minizip` | :heavy_check_mark: | | | | | | | -| `SQLite.swift` | | | | | | | :heavy_check_mark: | -| `SwiftSoup` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | - #### CocoaPods Add the following `pod` statements to your `Podfile` for the Readium libraries you want to use: @@ -143,11 +132,11 @@ Add the following `pod` statements to your `Podfile` for the Readium libraries y source 'https://github.com/readium/podspecs' source 'https://cdn.cocoapods.org/' -pod 'ReadiumShared', '~> 3.4.0' -pod 'ReadiumStreamer', '~> 3.4.0' -pod 'ReadiumNavigator', '~> 3.4.0' -pod 'ReadiumOPDS', '~> 3.4.0' -pod 'ReadiumLCP', '~> 3.4.0' +pod 'ReadiumShared', '~> 3.8.0' +pod 'ReadiumStreamer', '~> 3.8.0' +pod 'ReadiumNavigator', '~> 3.8.0' +pod 'ReadiumOPDS', '~> 3.8.0' +pod 'ReadiumLCP', '~> 3.8.0' ``` Take a look at [CocoaPods's documentation](https://guides.cocoapods.org/using/using-cocoapods.html) for more information. diff --git a/Sources/Adapters/GCDWebServer/GCDHTTPServer.swift b/Sources/Adapters/GCDWebServer/GCDHTTPServer.swift index 3df551fd16..6624771b58 100644 --- a/Sources/Adapters/GCDWebServer/GCDHTTPServer.swift +++ b/Sources/Adapters/GCDWebServer/GCDHTTPServer.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -46,7 +46,9 @@ public class GCDHTTPServer: HTTPServer, Loggable { /// Creates a new instance of the HTTP server. /// - /// - Parameter logLevel: See `ReadiumGCDWebServer.setLogLevel`. + /// - Parameters: + /// - assetRetriever: The retriever used to fetch assets for the server. + /// - logLevel: See `ReadiumGCDWebServer.setLogLevel`. public init( assetRetriever: AssetRetriever, logLevel: Int = 3 diff --git a/Sources/Adapters/GCDWebServer/ResourceResponse.swift b/Sources/Adapters/GCDWebServer/ResourceResponse.swift index e5a939291e..1624c38efd 100644 --- a/Sources/Adapters/GCDWebServer/ResourceResponse.swift +++ b/Sources/Adapters/GCDWebServer/ResourceResponse.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Adapters/LCPSQLite/Database.swift b/Sources/Adapters/LCPSQLite/Database.swift index 7d33f1417b..4afd1f3406 100644 --- a/Sources/Adapters/LCPSQLite/Database.swift +++ b/Sources/Adapters/LCPSQLite/Database.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Adapters/LCPSQLite/SQLiteLCPLicenseRepository.swift b/Sources/Adapters/LCPSQLite/SQLiteLCPLicenseRepository.swift index a085367688..9dba0b82cf 100644 --- a/Sources/Adapters/LCPSQLite/SQLiteLCPLicenseRepository.swift +++ b/Sources/Adapters/LCPSQLite/SQLiteLCPLicenseRepository.swift @@ -1,14 +1,16 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 Foundation import ReadiumLCP +import ReadiumShared import SQLite -public class LCPSQLiteLicenseRepository: LCPLicenseRepository { +@available(*, deprecated, message: "Use LCPKeychainLicenseRepository from ReadiumLCP instead") +public class LCPSQLiteLicenseRepository: LCPLicenseRepository, Loggable { let licenses = Table("Licenses") let id = SQLite.Expression("id") let printsLeft = SQLite.Expression("printsLeft") @@ -117,4 +119,57 @@ public class LCPSQLiteLicenseRepository: LCPLicenseRepository { copy: get(copiesLeft, for: id) ) } + + /// Migrates all licenses from this SQLite repository to the target + /// keychain repository. + /// + /// This migration transfers consumable rights (print/copy counts) and + /// device registration status to the target repository. The full + /// `LicenseDocument` is not stored in SQLite and will be automatically + /// added to the target repository when each publication is opened + /// for the first time after migration. + /// + /// - Returns: `true` if all the licenses were migrated successfully. + @discardableResult + public func migrate(to target: LCPKeychainLicenseRepository) async throws -> Bool { + let allLicenseData = try db.prepare(licenses).map { row in + try ( + id: row.get(id), + printsLeft: row.get(printsLeft), + copiesLeft: row.get(copiesLeft), + registered: row.get(registered) + ) + } + + var successCount = 0 + var failureCount = 0 + + for licenseData in allLicenseData { + do { + let rights = LCPConsumableUserRights( + print: licenseData.printsLeft, + copy: licenseData.copiesLeft + ) + + try await target.importLicenseRights( + for: licenseData.id, + rights: rights, + registered: licenseData.registered + ) + + successCount += 1 + } catch { + failureCount += 1 + log(.error, "Failed to migrate license \(licenseData.id): \(error)") + } + } + + if failureCount > 0 { + log(.info, "License migration completed with \(successCount) succeeded, \(failureCount) failed") + } else { + log(.info, "License migration completed successfully: \(successCount) licenses migrated") + } + + return failureCount == 0 + } } diff --git a/Sources/Adapters/LCPSQLite/SQLiteLCPPassphraseRepository.swift b/Sources/Adapters/LCPSQLite/SQLiteLCPPassphraseRepository.swift index 43292afe0a..7255273d4f 100644 --- a/Sources/Adapters/LCPSQLite/SQLiteLCPPassphraseRepository.swift +++ b/Sources/Adapters/LCPSQLite/SQLiteLCPPassphraseRepository.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,6 +9,7 @@ import ReadiumLCP import ReadiumShared import SQLite +@available(*, deprecated, message: "Use LCPKeychainPassphraseRepository from ReadiumLCP instead") public class LCPSQLitePassphraseRepository: LCPPassphraseRepository, Loggable { let transactions = Table("Transactions") let licenseId = SQLite.Expression("licenseId") @@ -32,28 +33,24 @@ public class LCPSQLitePassphraseRepository: LCPPassphraseRepository, Loggable { public func passphrase(for licenseID: LicenseDocument.ID) async throws -> LCPPassphraseHash? { try logAndRethrow { try db.prepare(transactions.select(passphrase) - .filter(self.licenseId == licenseID) - ) - .compactMap { try $0.get(passphrase) } - .first + .filter(self.licenseId == licenseID)) + .compactMap { try $0.get(passphrase) } + .first } } public func passphrasesMatching(userID: User.ID?, provider: LicenseDocument.Provider) async throws -> [LCPPassphraseHash] { try logAndRethrow { - var passphrases = - try db.prepare(transactions.select(passphrase) - .filter(self.userId == userID && self.provider == provider) - ) - .compactMap { try $0.get(passphrase) } - - // The legacy SQLite database did not save all the new - // (passphrase, userID, provider) tuples. So we need to fall back - // on checking all the saved passphrases for a match. - passphrases += try db.prepare(transactions.select(passphrase)) + try db.prepare(transactions.select(passphrase) + .filter(self.userId == userID && self.provider == provider)) .compactMap { try $0.get(passphrase) } + } + } - return passphrases + public func passphrases() async throws -> [LCPPassphraseHash] { + try logAndRethrow { + try db.prepare(transactions.select(passphrase)) + .compactMap { try $0.get(passphrase) } } } @@ -71,13 +68,46 @@ public class LCPSQLitePassphraseRepository: LCPPassphraseRepository, Loggable { } } - private func all() -> [String] { - let query = transactions.select(passphrase) - do { - return try db.prepare(query).compactMap { try $0.get(passphrase) } - } catch { - log(.error, error) - return [] + /// Migrates all passphrases from this SQLite repository to the target + /// repository. + /// + /// - Returns: `true` if all the passphrases were migrated successfully. + @discardableResult + public func migrate(to target: LCPPassphraseRepository) async throws -> Bool { + let allPassphraseData = try db.prepare(transactions).map { row in + try ( + licenseId: row.get(licenseId), + passphrase: row.get(passphrase), + provider: row.get(provider), + userId: row.get(userId) + ) + } + + var successCount = 0 + var failureCount = 0 + + for passphraseData in allPassphraseData { + do { + try await target.addPassphrase( + passphraseData.passphrase, + for: passphraseData.licenseId, + userID: passphraseData.userId, + provider: passphraseData.provider + ) + successCount += 1 + } catch { + failureCount += 1 + // Log error but continue with other passphrases + log(.error, "Failed to migrate passphrase for license \(passphraseData.licenseId): \(error)") + } + } + + if failureCount > 0 { + log(.info, "Passphrase migration completed with \(successCount) succeeded, \(failureCount) failed") + } else { + log(.info, "Passphrase migration completed successfully: \(successCount) passphrases migrated") } + + return failureCount == 0 } } diff --git a/Sources/Internal/Extensions/Array.swift b/Sources/Internal/Extensions/Array.swift index 9dad9b4ac8..6c1bf08da8 100644 --- a/Sources/Internal/Extensions/Array.swift +++ b/Sources/Internal/Extensions/Array.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/Collection.swift b/Sources/Internal/Extensions/Collection.swift index 224ca63c7d..f8419f53d7 100644 --- a/Sources/Internal/Extensions/Collection.swift +++ b/Sources/Internal/Extensions/Collection.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/Comparable.swift b/Sources/Internal/Extensions/Comparable.swift index 39bb206a17..3b7480cac6 100644 --- a/Sources/Internal/Extensions/Comparable.swift +++ b/Sources/Internal/Extensions/Comparable.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/Data.swift b/Sources/Internal/Extensions/Data.swift index 45a5d16963..257b17a8fa 100644 --- a/Sources/Internal/Extensions/Data.swift +++ b/Sources/Internal/Extensions/Data.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/Date+ISO8601.swift b/Sources/Internal/Extensions/Date+ISO8601.swift index c5f6a0923c..6f3cc79ec9 100644 --- a/Sources/Internal/Extensions/Date+ISO8601.swift +++ b/Sources/Internal/Extensions/Date+ISO8601.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/Double.swift b/Sources/Internal/Extensions/Double.swift index f707486e72..efe9dde61e 100644 --- a/Sources/Internal/Extensions/Double.swift +++ b/Sources/Internal/Extensions/Double.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/NSRegularExpression.swift b/Sources/Internal/Extensions/NSRegularExpression.swift index 4ca10654ef..614d135e6c 100644 --- a/Sources/Internal/Extensions/NSRegularExpression.swift +++ b/Sources/Internal/Extensions/NSRegularExpression.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/Number.swift b/Sources/Internal/Extensions/Number.swift index 299774baf1..bd6a9da34b 100644 --- a/Sources/Internal/Extensions/Number.swift +++ b/Sources/Internal/Extensions/Number.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/Optional.swift b/Sources/Internal/Extensions/Optional.swift index a73e1ac200..e9fe54171f 100644 --- a/Sources/Internal/Extensions/Optional.swift +++ b/Sources/Internal/Extensions/Optional.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/Range.swift b/Sources/Internal/Extensions/Range.swift index a5ae36ee13..546ce91bc6 100644 --- a/Sources/Internal/Extensions/Range.swift +++ b/Sources/Internal/Extensions/Range.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -10,4 +10,44 @@ public extension Range where Bound == UInt64 { func clampedToInt() -> Range { clamped(to: 0 ..< UInt64(Int.max)) } + + /// Parses an HTTP `Range` header value (RFC 7233) into a byte range. + /// + /// Supports: + /// - `bytes=0-1023` → `0..<1024` + /// - `bytes=1024-` → `1024.. 0 else { return nil } + let start = totalLength > suffix ? totalLength - suffix : 0 + self = start ..< totalLength + return + } + + let parts = spec.split(separator: "-", maxSplits: 1, omittingEmptySubsequences: false) + guard parts.count == 2, let start = UInt64(parts[0]) else { return nil } + + if parts[1].isEmpty { + // Open-ended range: bytes=N- + guard start < totalLength else { return nil } + self = start ..< totalLength + return + } + + // Closed range: bytes=N-M + guard let end = UInt64(parts[1]), end >= start else { return nil } + let clampedEnd = Swift.min(end + 1, totalLength) + guard start < clampedEnd else { return nil } + self = start ..< clampedEnd + } } diff --git a/Sources/Internal/Extensions/Result.swift b/Sources/Internal/Extensions/Result.swift index 669f352d0b..9177e23dac 100644 --- a/Sources/Internal/Extensions/Result.swift +++ b/Sources/Internal/Extensions/Result.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/Sequence.swift b/Sources/Internal/Extensions/Sequence.swift index 10912954d5..cc9ed5727f 100644 --- a/Sources/Internal/Extensions/Sequence.swift +++ b/Sources/Internal/Extensions/Sequence.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/String.swift b/Sources/Internal/Extensions/String.swift index 900c1f0567..0d74535309 100644 --- a/Sources/Internal/Extensions/String.swift +++ b/Sources/Internal/Extensions/String.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/Task.swift b/Sources/Internal/Extensions/Task.swift index aba07ff6b0..f6358ec44a 100644 --- a/Sources/Internal/Extensions/Task.swift +++ b/Sources/Internal/Extensions/Task.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/UInt64.swift b/Sources/Internal/Extensions/UInt64.swift index aa8b17082a..cc8704c9d6 100644 --- a/Sources/Internal/Extensions/UInt64.swift +++ b/Sources/Internal/Extensions/UInt64.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/Extensions/URL.swift b/Sources/Internal/Extensions/URL.swift index cf6f3f298f..a09e089b98 100644 --- a/Sources/Internal/Extensions/URL.swift +++ b/Sources/Internal/Extensions/URL.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/JSON.swift b/Sources/Internal/JSON.swift deleted file mode 100644 index 4189979709..0000000000 --- a/Sources/Internal/JSON.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// Copyright 2025 Readium Foundation. All 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 Foundation - -/// Wraps a dictionary parsed from a JSON string. -/// This is a trick to keep the Web Publication structs equatable without having to override `==` and compare all the other properties. -public struct JSONDictionary: Sendable { - public typealias Key = String - public typealias Value = Sendable - public typealias Wrapped = [Key: Value] - - public var json: Wrapped - - public init() { - json = [:] - } - - public init?(_ json: Any?) { - guard let json = json as? Wrapped else { - return nil - } - self.json = json - } - - public mutating func pop(_ key: Key) -> Value? { - json.removeValue(forKey: key) - } -} - -extension JSONDictionary: Collection { - public typealias Index = DictionaryIndex - public typealias Element = (key: Key, value: Value) - - public var startIndex: Index { - json.startIndex - } - - public var endIndex: Index { - json.endIndex - } - - public subscript(index: Index) -> Iterator.Element { - json[index] - } - - public func index(after i: Index) -> Index { - json.index(after: i) - } -} - -extension JSONDictionary: Equatable { - public static func == (lhs: JSONDictionary, rhs: JSONDictionary) -> Bool { - let l = try? JSONSerialization.data(withJSONObject: lhs.json, options: [.sortedKeys]) - let r = try? JSONSerialization.data(withJSONObject: rhs.json, options: [.sortedKeys]) - return l == r - } -} - -extension JSONDictionary: Hashable { - public func hash(into hasher: inout Hasher) { - let jsonString = (try? JSONSerialization.data(withJSONObject: json, options: [.sortedKeys])) - .map { String(data: $0, encoding: .utf8) } - hasher.combine(jsonString ?? "{}") - } -} - -// MARK: - JSON Parsing - -/// enum Example: String { -/// case hello -/// } -/// let json = ["key": "hello"] -/// let value: Example? = parseRaw(json["key"]) -public func parseRaw(_ json: Any?) -> T? { - guard let rawValue = json as? T.RawValue else { - return nil - } - return T(rawValue: rawValue) -} - -/// let json = [ -/// "multiple": ["hello", "world"] -/// "single": "hello", -/// ] -/// let values1: [String] = parseArray(json["multiple"]) -/// let values2: [String] = parseArray(json["single"], allowingSingle: true) -/// -/// - Parameter allowingSingle: If true, then allows the parsing of both a single value and an array. -public func parseArray(_ json: Any?, allowingSingle: Bool = false) -> [T] { - if let values = json as? [T] { - return values - } else if allowingSingle, let value = json as? T { - return [value] - } else { - return [] - } -} - -/// Casting to Double loses precision and fails with integers, eg. json["key"] as? Double. -public func parseDouble(_ json: Any?) -> Double? { - (json as? NSNumber)?.doubleValue -} - -/// Parses a numeric value, but returns nil if it is not a positive number. -public func parsePositive(_ json: Any?) -> T? { - guard let number = json as? T, number >= 0 else { - return nil - } - return number -} - -public func parsePositiveDouble(_ json: Any?) -> Double? { - guard let double = parseDouble(json), double >= 0 else { - return nil - } - return double -} - -public func parseDate(_ json: Any?) -> Date? { - (json as? String)?.dateFromISO8601 -} - -/// Returns the given JSON object after removing any key with NSNull value. -/// To be used with `encodeIfX` functions for more compact serialization code. -public func makeJSON(_ object: JSONDictionary.Wrapped, additional: JSONDictionary.Wrapped = [:]) -> JSONDictionary.Wrapped { - object.filter { _, value in - !(value is NSNull) - }.merging(additional, uniquingKeysWith: { current, _ in current }) -} - -/// Returns the value if not nil, or NSNull. -public func encodeIfNotNil(_ value: Any?) -> Any { - value ?? NSNull() -} - -/// Returns the raw representable's raw value if not nil, or NSNull. To be used with optional Enum. -public func encodeRawIfNotNil(_ value: T?) -> Any { - value?.rawValue ?? NSNull() -} - -/// Returns the collection if not empty, or NSNull. -public func encodeIfNotEmpty(_ collection: T?) -> Any { - guard let collection = collection else { - return NSNull() - } - return collection.isEmpty ? NSNull() : collection -} diff --git a/Sources/Internal/Keychain.swift b/Sources/Internal/Keychain.swift new file mode 100644 index 0000000000..cd6e33b34c --- /dev/null +++ b/Sources/Internal/Keychain.swift @@ -0,0 +1,241 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation +import Security + +/// Errors occurring in ``Keychain``. +public enum KeychainError: Error { + /// The item was not found in the Keychain. + case itemNotFound + + /// An item with this key already exists. + case duplicateItem + + /// The data retrieved from the Keychain is invalid. + case invalidData + + /// An unhandled Keychain error occurred. + case unhandledError(OSStatus) +} + +/// Utility for managing Keychain operations. +/// +/// This class handles low-level Security framework calls for storing, retrieving, +/// updating, and deleting data from the iOS/macOS Keychain. +public final class Keychain: Sendable { + private let serviceName: String + private let synchronizable: Bool + + /// Initializes a ``Keychain`` with the specified configuration. + /// + /// - Parameters: + /// - serviceName: The service identifier for Keychain items. + /// - synchronizable: Whether items should sync via iCloud Keychain. + public init( + serviceName: String, + synchronizable: Bool = true + ) { + self.serviceName = serviceName + self.synchronizable = synchronizable + } + + /// Saves data to the Keychain with the specified key. + /// + /// - Parameters: + /// - data: The data to save. + /// - key: The account identifier. + public func save(data: Data, forKey key: String) throws(KeychainError) { + var query = baseQuery(forKey: key, forAdding: true) + query[kSecValueData as String] = data + + let status = SecItemAdd(query as CFDictionary, nil) + + guard status == errSecSuccess else { + throw mapError(status) + } + } + + /// Loads data from the Keychain for the specified key. + /// + /// - Parameter key: The account identifier. + /// - Returns: The data if found, or `nil` if no item exists with this key. + public func load(forKey key: String) throws(KeychainError) -> Data? { + var query = baseQuery(forKey: key, forAdding: false) + query[kSecReturnData as String] = true + query[kSecMatchLimit as String] = kSecMatchLimitOne + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + if status == errSecItemNotFound { + return nil + } + + guard status == errSecSuccess else { + throw mapError(status) + } + + guard let data = result as? Data else { + throw KeychainError.invalidData + } + + return data + } + + /// Updates existing data in the Keychain for the specified key. + /// + /// - Parameters: + /// - data: The new data to save. + /// - key: The account identifier. + public func update(data: Data, forKey key: String) throws(KeychainError) { + let query = baseQuery(forKey: key, forAdding: false) + let attributesToUpdate: [String: Any] = [ + kSecValueData as String: data, + ] + + let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) + + guard status == errSecSuccess else { + throw mapError(status) + } + } + + /// Deletes an item from the Keychain for the specified key. + /// + /// - Parameter key: The account identifier. + public func delete(forKey key: String) throws(KeychainError) { + let query = baseQuery(forKey: key, forAdding: false) + let status = SecItemDelete(query as CFDictionary) + + // Success or item not found are both acceptable + guard status == errSecSuccess || status == errSecItemNotFound else { + throw mapError(status) + } + } + + /// Deletes all items for this service from the Keychain. + public func deleteAll() throws(KeychainError) { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: serviceName, + kSecAttrSynchronizable as String: kSecAttrSynchronizableAny, + ] + let status = SecItemDelete(query as CFDictionary) + + guard status == errSecSuccess || status == errSecItemNotFound else { + throw mapError(status) + } + } + + /// Returns all account identifiers (keys) stored for this service. + /// + /// - Returns: An array of account identifiers. + public func allKeys() throws(KeychainError) -> [String] { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: serviceName, + kSecAttrSynchronizable as String: kSecAttrSynchronizableAny, + kSecReturnAttributes as String: true, + kSecMatchLimit as String: kSecMatchLimitAll, + ] + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + if status == errSecItemNotFound { + return [] + } + + guard status == errSecSuccess else { + throw mapError(status) + } + + guard let items = result as? [[String: Any]] else { + return [] + } + + return items.compactMap { $0[kSecAttrAccount as String] as? String } + } + + /// Returns all items stored for this service. + /// + /// - Returns: A dictionary where keys are account identifiers and values are + /// the stored data. + public func allItems() throws(KeychainError) -> [String: Data] { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: serviceName, + kSecAttrSynchronizable as String: kSecAttrSynchronizableAny, + kSecReturnAttributes as String: true, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitAll, + ] + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + if status == errSecItemNotFound { + return [:] + } + + guard status == errSecSuccess else { + throw mapError(status) + } + + guard let items = result as? [[String: Any]] else { + return [:] + } + + var itemsDictionary: [String: Data] = [:] + for item in items { + if let account = item[kSecAttrAccount as String] as? String, + let data = item[kSecValueData as String] as? Data + { + itemsDictionary[account] = data + } + } + + return itemsDictionary + } + + // MARK: - Private Helpers + + /// Creates the base query dictionary for Keychain operations. + /// + /// - Parameters: + /// - key: The account identifier. + /// - forAdding: If `true`, uses the boolean `synchronizable` value for adding items. + /// If `false`, uses `kSecAttrSynchronizableAny` for queries/updates/deletes. + private func baseQuery(forKey key: String, forAdding: Bool) -> [String: Any] { + var query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: serviceName, + kSecAttrAccount as String: key, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock, + ] + + if forAdding { + query[kSecAttrSynchronizable as String] = synchronizable + } else { + query[kSecAttrSynchronizable as String] = kSecAttrSynchronizableAny + } + + return query + } + + /// Maps OSStatus error codes to KeychainError cases. + private func mapError(_ status: OSStatus) -> KeychainError { + switch status { + case errSecItemNotFound: + return .itemNotFound + case errSecDuplicateItem: + return .duplicateItem + default: + return .unhandledError(status) + } + } +} diff --git a/Sources/Internal/Measure.swift b/Sources/Internal/Measure.swift index 04fdbf789c..686df29bd0 100644 --- a/Sources/Internal/Measure.swift +++ b/Sources/Internal/Measure.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Internal/UTI.swift b/Sources/Internal/UTI.swift index 27ea7b73fd..63d1e89262 100644 --- a/Sources/Internal/UTI.swift +++ b/Sources/Internal/UTI.swift @@ -1,59 +1,84 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 CoreServices import Foundation +import UniformTypeIdentifiers /// Uniform Type Identifier. -public struct UTI: ExpressibleByStringLiteral { - /// Type tag class, eg. kUTTagClassMIMEType. +public struct UTI { + /// Type tag class, eg. UTTagClass.mimeType. public enum TagClass { case mediaType, fileExtension + } - var rawString: CFString { - switch self { - case .mediaType: - return kUTTagClassMIMEType - case .fileExtension: - return kUTTagClassFilenameExtension - } + public let type: UTType + + public init(type: UTType) { + self.type = type + } + + public init?(_ identifier: String) { + guard let type = UTType(identifier) else { + return nil } + self.init(type: type) } - public let string: String + public init?(mediaType: String) { + guard let type = UTType(mimeType: mediaType) else { + return nil + } + self.init(type: type) + } - public init(stringLiteral value: StringLiteralType) { - string = value + public init?(fileExtension: String) { + guard let type = UTType(filenameExtension: fileExtension) else { + return nil + } + self.init(type: type) } public var name: String? { - UTTypeCopyDescription(string as CFString)?.takeRetainedValue() as String? + type.localizedDescription + } + + public var string: String { + type.identifier } /// Returns the preferred tag for this `UTI`, with the given type `tagClass`. public func preferredTag(withClass tagClass: TagClass) -> String? { - UTTypeCopyPreferredTagWithClass(string as CFString, tagClass.rawString)?.takeRetainedValue() as String? + switch tagClass { + case .mediaType: + return type.preferredMIMEType + case .fileExtension: + return type.preferredFilenameExtension + } } /// Returns all tags for this `UTI`, with the given type `tagClass`. public func tags(withClass tagClass: TagClass) -> [String] { - UTTypeCopyAllTagsWithClass(string as CFString, tagClass.rawString)?.takeRetainedValue() as? [String] - ?? [] + switch tagClass { + case .mediaType: + return type.tags[.mimeType] ?? [] + case .fileExtension: + return type.tags[.filenameExtension] ?? [] + } } /// Finds the first `UTI` recognizing any of the given `mediaTypes` or `fileExtensions`. public static func findFrom(mediaTypes: [String], fileExtensions: [String]) -> UTI? { for mediaType in mediaTypes { - if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mediaType as CFString, nil)?.takeUnretainedValue() { - return UTI(stringLiteral: uti as String) + if let uti = UTI(mediaType: mediaType) { + return uti } } for fileExtension in fileExtensions { - if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension as CFString, nil)?.takeUnretainedValue() { - return UTI(stringLiteral: uti as String) + if let uti = UTI(fileExtension: fileExtension) { + return uti } } return nil diff --git a/Sources/LCP/Authentications/Base.lproj/LCPDialogViewController.xib b/Sources/LCP/Authentications/Base.lproj/LCPDialogViewController.xib deleted file mode 100644 index 886be11452..0000000000 --- a/Sources/LCP/Authentications/Base.lproj/LCPDialogViewController.xib +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/LCP/Authentications/LCPAuthenticating.swift b/Sources/LCP/Authentications/LCPAuthenticating.swift index da0295db0b..6fdd1c8da6 100644 --- a/Sources/LCP/Authentications/LCPAuthenticating.swift +++ b/Sources/LCP/Authentications/LCPAuthenticating.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -19,11 +19,9 @@ public protocol LCPAuthenticating { /// - reason: Reason why the passphrase is requested. It should be used to prompt the user. /// - allowUserInteraction: Indicates whether the user can be prompted for their passphrase. /// If your implementation requires it and `allowUserInteraction` is false, terminate - /// quickly by sending `nil` to the completion block. + /// quickly by returning `nil`. /// - sender: Free object that can be used by reading apps to give some UX context when /// presenting dialogs. For example, the host `UIViewController`. - /// - completion: Used to return the retrieved passphrase. If the user cancelled, send nil. - /// The passphrase may be already hashed. @MainActor func retrievePassphrase( for license: LCPAuthenticatedLicense, @@ -68,8 +66,4 @@ public struct LCPAuthenticatedLicense { /// License Document being opened. public let document: LicenseDocument - - init(document: LicenseDocument) { - self.document = document - } } diff --git a/Sources/LCP/Authentications/LCPDialog.swift b/Sources/LCP/Authentications/LCPDialog.swift index 5f87c04e14..8cf54c9755 100644 --- a/Sources/LCP/Authentications/LCPDialog.swift +++ b/Sources/LCP/Authentications/LCPDialog.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -46,7 +46,6 @@ import SwiftUI /// } /// } /// ``` -@available(iOS 16.0, *) public struct LCPDialog: View { public enum ErrorMessage { case incorrectPassphrase @@ -54,17 +53,20 @@ public struct LCPDialog: View { var string: String { switch self { case .incorrectPassphrase: - ReadiumLCPLocalizedString("dialog.error.incorrectPassphrase") + ReadiumLCPLocalizedString("dialog.errors.incorrectPassphrase") } } } - public var id: LCPDialog { self } + public var id: LCPDialog { + self + } private let hint: String? private let errorMessage: ErrorMessage? private let onSubmit: (String) -> Void private let onForgotPassphrase: (() -> Void)? + private let onCancel: (() -> Void)? private let openButtonId = "open" @@ -72,12 +74,14 @@ public struct LCPDialog: View { hint: String?, errorMessage: ErrorMessage?, onSubmit: @escaping (String) -> Void, - onForgotPassphrase: (() -> Void)? + onForgotPassphrase: (() -> Void)?, + onCancel: (() -> Void)? = nil ) { self.hint = hint self.errorMessage = errorMessage self.onSubmit = onSubmit self.onForgotPassphrase = onForgotPassphrase + self.onCancel = onCancel } public init( @@ -100,7 +104,7 @@ public struct LCPDialog: View { @State private var passphrase: String = "" public var body: some View { - NavigationStack { + NavigationView { ScrollViewReader { scrollProxy in Form { header @@ -120,20 +124,22 @@ public struct LCPDialog: View { } } } - .scrollDismissesKeyboard(.interactively) + .scrollDismissesKeyboardIfAvailable() .navigationTitle(ReadiumLCPLocalizedStringKey("dialog.title")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { - Button(ReadiumLCPLocalizedStringKey("dialog.cancel"), role: .cancel) { + Button(ReadiumLCPLocalizedStringKey("dialog.actions.cancel"), role: .cancel) { + onCancel?() dismiss() } } } } + .navigationViewStyle(.stack) } - @ViewBuilder private var header: some View { + private var header: some View { Section { HStack { Spacer() @@ -142,31 +148,29 @@ public struct LCPDialog: View { .foregroundStyle(.blue) .font(.system(size: 70)) - Text(ReadiumLCPLocalizedStringKey("dialog.header")) + Text(ReadiumLCPLocalizedStringKey("dialog.message")) .multilineTextAlignment(.center) .padding(.bottom, 16) } Spacer() } - DisclosureGroup(ReadiumLCPLocalizedStringKey("dialog.details.title")) { + DisclosureGroup(ReadiumLCPLocalizedStringKey("dialog.info.title")) { VStack { - Text(ReadiumLCPLocalizedStringKey("dialog.details.body")) + Text(ReadiumLCPLocalizedStringKey("dialog.info.body")) .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .leading) - Text("[\(ReadiumLCPLocalizedString("dialog.details.more"))](https://www.edrlab.org/readium-lcp/)") + Text("[\(ReadiumLCPLocalizedString("dialog.info.more"))](https://www.edrlab.org/readium-lcp/)") .frame(maxWidth: .infinity, alignment: .leading) } } } - .alignmentGuide(.listRowSeparatorLeading) { _ in - 0 - } + .alignListRowSeparatorLeading() .font(.callout) } - @ViewBuilder private var input: some View { + private var input: some View { Section { VStack(alignment: .leading, spacing: 8) { TextField(text: $passphrase) { @@ -188,16 +192,20 @@ public struct LCPDialog: View { .font(.callout) } } + } footer: { + if let hint = hint { + Text(ReadiumLCPLocalizedStringKey("dialog.passphrase.hint", hint)) + } } .listRowSeparator(.hidden) } @ViewBuilder private var buttons: some View { Section { - Button(ReadiumLCPLocalizedStringKey("dialog.continue")) { + Button(ReadiumLCPLocalizedStringKey("dialog.actions.continue")) { submit() } - .bold() + .boldIfAvailable() .id(openButtonId) .disabled(passphrase.isEmpty) .frame(maxWidth: .infinity, alignment: .center) @@ -205,14 +213,10 @@ public struct LCPDialog: View { if let onForgotPassphrase = onForgotPassphrase { Section { - Button(ReadiumLCPLocalizedStringKey("dialog.forgotYourPassphrase"), role: .destructive) { + Button(ReadiumLCPLocalizedStringKey("dialog.actions.recoverPassphrase"), role: .destructive) { onForgotPassphrase() } .frame(maxWidth: .infinity, alignment: .center) - } footer: { - if let hint = hint { - Text(ReadiumLCPLocalizedStringKey("dialog.hint", hint)) - } } } } @@ -227,6 +231,35 @@ public struct LCPDialog: View { } } +private extension View { + @ViewBuilder + func scrollDismissesKeyboardIfAvailable() -> some View { + if #available(iOS 16.0, *) { + scrollDismissesKeyboard(.interactively) + } else { + self + } + } + + @ViewBuilder + func alignListRowSeparatorLeading() -> some View { + if #available(iOS 16.0, *) { + alignmentGuide(.listRowSeparatorLeading) { _ in 0 } + } else { + self + } + } + + @ViewBuilder + func boldIfAvailable() -> some View { + if #available(iOS 16.0, *) { + bold() + } else { + self + } + } +} + #Preview { if #available(iOS 18.0, *) { Spacer().sheet(isPresented: .constant(true)) { diff --git a/Sources/LCP/Authentications/LCPDialogAuthentication.swift b/Sources/LCP/Authentications/LCPDialogAuthentication.swift index 748ff4fe04..43a1c2e7ac 100644 --- a/Sources/LCP/Authentications/LCPDialogAuthentication.swift +++ b/Sources/LCP/Authentications/LCPDialogAuthentication.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -42,11 +42,10 @@ public class LCPDialogAuthentication: LCPAuthenticating, Loggable { continuation.resume(returning: passphrase) } - let navController = UINavigationController(rootViewController: dialogViewController) - navController.modalPresentationStyle = modalPresentationStyle - navController.modalTransitionStyle = modalTransitionStyle + dialogViewController.modalPresentationStyle = modalPresentationStyle + dialogViewController.modalTransitionStyle = modalTransitionStyle - viewController.present(navController, animated: animated) + viewController.present(dialogViewController, animated: animated) } } } diff --git a/Sources/LCP/Authentications/LCPDialogViewController.swift b/Sources/LCP/Authentications/LCPDialogViewController.swift index f439b9874f..ee32b59ef3 100644 --- a/Sources/LCP/Authentications/LCPDialogViewController.swift +++ b/Sources/LCP/Authentications/LCPDialogViewController.swift @@ -1,111 +1,59 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 SafariServices +import SwiftUI import UIKit final class LCPDialogViewController: UIViewController { - @IBOutlet var scrollView: UIScrollView! - @IBOutlet var hintLabel: UILabel! - @IBOutlet var promptLabel: UILabel! - @IBOutlet var messageLabel: UILabel! - @IBOutlet var passphraseField: UITextField! - @IBOutlet var supportButton: UIButton! - @IBOutlet var forgotPassphraseButton: UIButton! - @IBOutlet var continueButton: UIButton! - private let license: LCPAuthenticatedLicense private let reason: LCPAuthenticationReason private let completion: (String?) -> Void - private let supportLinks: [(Link, URL)] init(license: LCPAuthenticatedLicense, reason: LCPAuthenticationReason, completion: @escaping (String?) -> Void) { self.license = license self.reason = reason self.completion = completion - supportLinks = license.supportLinks - .compactMap { link -> (Link, URL)? in - guard let url = URL(string: link.href), UIApplication.shared.canOpenURL(url) else { - return nil - } - return (link, url) - } + super.init(nibName: nil, bundle: nil) - super.init(nibName: nil, bundle: Bundle.module) - - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) + isModalInPresentation = true } @available(*, unavailable) - required init?(coder aDecoder: NSCoder) { + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - deinit { - NotificationCenter.default.removeObserver(self) - } - override func viewDidLoad() { super.viewDidLoad() - if #available(iOS 13.0, *) { - // Prevents swipe down to dismiss the dialog on iOS 13+ - isModalInPresentation = true - } - - var provider = license.document.provider - if let providerHost = URL(string: provider)?.host { - provider = providerHost - } - - supportButton.isHidden = supportLinks.isEmpty - - let label = UILabel() - - switch reason { - case .passphraseNotFound: - label.text = ReadiumLCPLocalizedString("dialog.reason.passphraseNotFound") - case .invalidPassphrase: - label.text = ReadiumLCPLocalizedString("dialog.reason.invalidPassphrase") - passphraseField.layer.borderWidth = 1 - passphraseField.layer.borderColor = UIColor.red.cgColor - } - - label.sizeToFit() - if #available(iOS 13.0, *) { - label.textColor = .label - navigationController?.navigationBar.backgroundColor = .systemBackground - } - - let leftItem = UIBarButtonItem(customView: label) - navigationItem.leftBarButtonItem = leftItem - - promptLabel.text = ReadiumLCPLocalizedString("dialog.prompt.message1") - messageLabel.text = String(format: ReadiumLCPLocalizedString("dialog.prompt.message2"), provider) - forgotPassphraseButton.setTitle(ReadiumLCPLocalizedString("dialog.prompt.forgotPassphrase"), for: .normal) - supportButton.setTitle(ReadiumLCPLocalizedString("dialog.prompt.support"), for: .normal) - continueButton.setTitle(ReadiumLCPLocalizedString("dialog.prompt.continue"), for: .normal) - passphraseField.placeholder = ReadiumLCPLocalizedString("dialog.prompt.passphrase") - hintLabel.text = license.hint - - navigationItem.rightBarButtonItem = UIBarButtonItem( - barButtonSystemItem: .cancel, - target: self, - action: #selector(LCPDialogViewController.cancel(_:)) + let dialog = LCPDialog( + hint: license.hint.orNilIfBlank(), + errorMessage: reason == .invalidPassphrase ? .incorrectPassphrase : nil, + onSubmit: { [weak self] passphrase in + self?.complete(with: passphrase) + }, + onForgotPassphrase: license.hintLink?.url().map { url in + { UIApplication.shared.open(url.url) } + }, + onCancel: { [weak self] in + self?.complete(with: nil) + } ) - } - - @IBAction func authenticate(_ sender: Any) { - let passphrase = passphraseField.text ?? "" - complete(with: passphrase) - } - @IBAction func cancel(_ sender: Any) { - complete(with: nil) + let hostingController = UIHostingController(rootView: dialog) + addChild(hostingController) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(hostingController.view) + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + hostingController.didMove(toParent: self) } private var isCompleted = false @@ -118,103 +66,4 @@ final class LCPDialogViewController: UIViewController { completion(passphrase) dismiss(animated: true) } - - @IBAction func showSupportLink(_ sender: Any) { - guard !supportLinks.isEmpty else { - return - } - - func open(_ url: URL) { - UIApplication.shared.open(url) - } - - if let (_, url) = supportLinks.first, supportLinks.count == 1 { - open(url) - return - } - - let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - for (link, url) in supportLinks { - let title: String = { - if let title = link.title { - return title - } - if let scheme = url.scheme { - switch scheme { - case "http", "https": - return ReadiumLCPLocalizedString("dialog.support.website") - case "tel": - return ReadiumLCPLocalizedString("dialog.support.phone") - case "mailto": - return ReadiumLCPLocalizedString("dialog.support.mail") - default: - break - } - } - return ReadiumLCPLocalizedString("dialog.support") - }() - - let action = UIAlertAction(title: title, style: .default) { _ in - open(url) - } - alert.addAction(action) - } - alert.addAction(UIAlertAction(title: ReadiumLCPLocalizedString("dialog.cancel"), style: .cancel)) - - if let popover = alert.popoverPresentationController, let sender = sender as? UIView { - popover.sourceView = sender - var rect = sender.bounds - rect.origin.x = sender.center.x - 1 - rect.size.width = 2 - popover.sourceRect = rect - } - present(alert, animated: true) - } - - @IBAction func showHintLink(_ sender: Any) { - guard let href = license.hintLink?.href, let url = URL(string: href) else { - return - } - - let browser = SFSafariViewController(url: url) - browser.modalPresentationStyle = .currentContext - present(browser, animated: true) - } - - /// Makes sure the form contents is scrollable when the keyboard is visible. - @objc func keyboardWillChangeFrame(_ note: Notification) { - guard - let window = view.window, - let scrollView = scrollView, - let scrollViewSuperview = scrollView.superview, - let info = note.userInfo - else { - return - } - - var keyboardHeight: CGFloat = 0 - if note.name == UIResponder.keyboardWillChangeFrameNotification { - guard let keyboardFrame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { - return - } - keyboardHeight = keyboardFrame.height - } - - // Calculates the scroll view offsets in the coordinate space of of our window - let scrollViewFrame = scrollViewSuperview.convert(scrollView.frame, to: window) - - var contentInset = scrollView.contentInset - // Bottom inset is the part of keyboard that is covering the tableView - contentInset.bottom = keyboardHeight - (window.frame.height - scrollViewFrame.height - scrollViewFrame.origin.y) + 16 - - self.scrollView.contentInset = contentInset - self.scrollView.scrollIndicatorInsets = contentInset - } -} - -extension LCPDialogViewController: UITextFieldDelegate { - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - authenticate(textField) - return false - } } diff --git a/Sources/LCP/Authentications/LCPObservableAuthentication.swift b/Sources/LCP/Authentications/LCPObservableAuthentication.swift index 354d33c61b..e088eccf98 100644 --- a/Sources/LCP/Authentications/LCPObservableAuthentication.swift +++ b/Sources/LCP/Authentications/LCPObservableAuthentication.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/LCP/Authentications/LCPPassphraseAuthentication.swift b/Sources/LCP/Authentications/LCPPassphraseAuthentication.swift index e80db33013..4940739f34 100644 --- a/Sources/LCP/Authentications/LCPPassphraseAuthentication.swift +++ b/Sources/LCP/Authentications/LCPPassphraseAuthentication.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/LCP/Content Protection/EncryptionParser.swift b/Sources/LCP/Content Protection/EncryptionParser.swift index f7140803d0..191d61394c 100644 --- a/Sources/LCP/Content Protection/EncryptionParser.swift +++ b/Sources/LCP/Content Protection/EncryptionParser.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -21,15 +21,19 @@ private func parseRPFEncryptionData(in container: Container) async -> ReadResult } return await manifestResource - .readAsJSONObject() + .read() + .asJSONObjectValue() .flatMap { json in do { - return try .success(Manifest(json: json)) + guard let parsedManifest = try Manifest(json: json) else { + return .failure(.decoding("Manifest JSON is invalid or could not be parsed")) + } + return .success(parsedManifest) } catch { return .failure(.decoding(error)) } } - .map { manifest in + .map { (manifest: Manifest) -> [AnyURL: ReadiumShared.Encryption] in (manifest.readingOrder + manifest.resources) .reduce([:]) { data, link in var data = data @@ -49,7 +53,7 @@ private func parseEPUBEncryptionData(in container: Container) async -> ReadResul return await encryptionResource.read() .asyncFlatMap { data -> ReadResult in do { - let doc = try await DefaultXMLDocumentFactory().open( + let doc = try DefaultXMLDocumentFactory().open( data: data, namespaces: [.enc, .ds, .comp] ) diff --git a/Sources/LCP/Content Protection/LCPContentProtection.swift b/Sources/LCP/Content Protection/LCPContentProtection.swift index 2254000c76..1f57f4a4f3 100644 --- a/Sources/LCP/Content Protection/LCPContentProtection.swift +++ b/Sources/LCP/Content Protection/LCPContentProtection.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -53,10 +53,10 @@ final class LCPContentProtection: ContentProtection, Loggable { return .failure(.assetNotSupported(DebugError("The asset does not appear to be an LCP License"))) } - return await asset.resource.readAsLCPL() + return await asset.resource.read() + .asLCPL() .mapError { .reading($0) } .asyncFlatMap { licenseDocument in - await assetRetriever.retrieve(link: licenseDocument.publicationLink) .flatMap { publicationAsset in switch publicationAsset { diff --git a/Sources/LCP/Content Protection/LCPDecryptor.swift b/Sources/LCP/Content Protection/LCPDecryptor.swift index 7025ba84a6..04c2823c3e 100644 --- a/Sources/LCP/Content Protection/LCPDecryptor.swift +++ b/Sources/LCP/Content Protection/LCPDecryptor.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,7 +9,6 @@ import ReadiumInternal import ReadiumShared private let lcpScheme = "http://readium.org/2014/01/lcp" -private let AESBlockSize: UInt64 = 16 // bytes /// Decrypts a resource protected with LCP. final class LCPDecryptor { @@ -117,7 +116,7 @@ final class LCPDecryptor { guard let length = length else { return failure(.requiredEstimatedLength) } - guard length >= 2 * AESBlockSize else { + guard length.isValidAESChunk else { return failure(.invalidCBCData) } @@ -207,6 +206,10 @@ final class LCPDecryptor { private extension LCPLicense { func decryptFully(data: ReadResult, isDeflated: Bool) async -> ReadResult { data.flatMap { + guard UInt64($0.count).isValidAESChunk else { + return .failure(.decoding(LCPDecryptor.Error.invalidCBCData)) + } + do { // Decrypts the resource. guard var data = try self.decipher($0) else { @@ -242,3 +245,15 @@ private extension ReadiumShared.Encryption { algorithm == "http://www.w3.org/2001/04/xmlenc#aes256-cbc" } } + +private let AESBlockSize: UInt64 = 16 // bytes + +private extension UInt64 { + /// Checks if this number is a valid CBC length - i.e. a multiple of AES + /// block size and at least 2 blocks (IV + data). + /// If not, the file is likely not actually encrypted despite being declared + /// as such. + var isValidAESChunk: Bool { + self >= 2 * AESBlockSize && self % AESBlockSize == 0 + } +} diff --git a/Sources/LCP/LCPAcquiredPublication.swift b/Sources/LCP/LCPAcquiredPublication.swift index efad0c9a17..aa1f9fe249 100644 --- a/Sources/LCP/LCPAcquiredPublication.swift +++ b/Sources/LCP/LCPAcquiredPublication.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/LCP/LCPClient.swift b/Sources/LCP/LCPClient.swift index c64c07395d..de7330fa5d 100644 --- a/Sources/LCP/LCPClient.swift +++ b/Sources/LCP/LCPClient.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/LCP/LCPError.swift b/Sources/LCP/LCPError.swift index 2d8e464aec..9e156f3ad8 100644 --- a/Sources/LCP/LCPError.swift +++ b/Sources/LCP/LCPError.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -45,7 +45,7 @@ public enum LCPError: Error { case parsing(ParsingError) /// A network request failed with the given error. - case network(Error?) + case network(HTTPError?) /// An unexpected LCP error occured. Please post an issue on r2-lcp-swift with the error message and how to reproduce it. case runtime(String) @@ -81,50 +81,50 @@ public enum StatusError: Error { /// Errors while renewing a loan. public enum RenewError: Error { - // Your publication could not be renewed properly. + /// Your publication could not be renewed properly. case renewFailed - // Incorrect renewal period, your publication could not be renewed. + /// Incorrect renewal period, your publication could not be renewed. case invalidRenewalPeriod(maxRenewDate: Date?) - // An unexpected error has occurred on the licensing server. + /// An unexpected error has occurred on the licensing server. case unexpectedServerError(HTTPError) } /// Errors while returning a loan. public enum ReturnError: Error { - // Your publication could not be returned properly. + /// Your publication could not be returned properly. case returnFailed - // Your publication has already been returned before or is expired. + /// Your publication has already been returned before or is expired. case alreadyReturnedOrExpired - // An unexpected error has occurred on the licensing server. + /// An unexpected error has occurred on the licensing server. case unexpectedServerError(HTTPError) } /// Errors while parsing the License or Status JSON Documents. public enum ParsingError: Error { - // The JSON is malformed and can't be parsed. + /// The JSON is malformed and can't be parsed. case malformedJSON - // The JSON is not representing a valid License Document. + /// The JSON is not representing a valid License Document. case licenseDocument - // The JSON is not representing a valid Status Document. + /// The JSON is not representing a valid Status Document. case statusDocument - // Invalid Link. + /// Invalid Link. case link - // Invalid Encryption. + /// Invalid Encryption. case encryption - // Invalid License Document Signature. + /// Invalid License Document Signature. case signature - // Invalid URL for link with rel %@. + /// Invalid URL for link with rel %@. case url(rel: String) } /// Errors while reading or writing a LCP container (LCPL, EPUB, LCPDF, etc.) public enum ContainerError: Error { - // Can't access the container, it's format is wrong. + /// Can't access the container, it's format is wrong. case openFailed(Error?) - // The file at given relative path is not found in the Container. + /// The file at given relative path is not found in the Container. case fileNotFound(String) - // Can't read the file at given relative path in the Container. + /// Can't read the file at given relative path in the Container. case readFailed(path: String) - // Can't write the file at given relative path in the Container. + /// Can't write the file at given relative path in the Container. case writeFailed(path: String) } diff --git a/Sources/LCP/LCPLicense.swift b/Sources/LCP/LCPLicense.swift index a05434fd5c..5edfe2be16 100644 --- a/Sources/LCP/LCPLicense.swift +++ b/Sources/LCP/LCPLicense.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -42,8 +42,10 @@ public protocol LCPLicense: UserRights { /// Renews the loan by starting a renew LSD interaction. /// - /// - Parameter prefersWebPage: Indicates whether the loan should be renewed through a web page if available, - /// instead of programmatically. + /// - Parameters: + /// - delegate: The delegate used to handle the user interactions required during the renewal process. + /// - prefersWebPage: Indicates whether the loan should be renewed through a web page if available, + /// instead of programmatically. func renewLoan( with delegate: LCPRenewDelegate, prefersWebPage: Bool diff --git a/Sources/LCP/LCPLicenseRepository.swift b/Sources/LCP/LCPLicenseRepository.swift index d69acdc36c..f8d99f9093 100644 --- a/Sources/LCP/LCPLicenseRepository.swift +++ b/Sources/LCP/LCPLicenseRepository.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/LCP/LCPPassphraseRepository.swift b/Sources/LCP/LCPPassphraseRepository.swift index 0c489764ac..b7d369a270 100644 --- a/Sources/LCP/LCPPassphraseRepository.swift +++ b/Sources/LCP/LCPPassphraseRepository.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,21 +9,23 @@ import Foundation /// Represents an LCP passphrase hash. public typealias LCPPassphraseHash = String -/// The passphrase repository stores passphrase hashes associated to a license document, user ID and -/// provider. +/// The passphrase repository stores passphrase hashes associated to a license +/// document, user ID and provider. public protocol LCPPassphraseRepository { /// Returns the passphrase hash associated with the given `licenseID`. func passphrase(for licenseID: LicenseDocument.ID) async throws -> LCPPassphraseHash? - /// Returns a list of passphrase hashes that may match the given `userID`, and `provider`. - func passphrasesMatching( - userID: User.ID?, - provider: LicenseDocument.Provider - ) async throws -> [LCPPassphraseHash] + /// Returns a list of passphrase hashes that may match the given `userID` + /// and `provider`. + func passphrasesMatching(userID: User.ID?, provider: LicenseDocument.Provider) async throws -> [LCPPassphraseHash] + + /// Returns all the saved passphrase hashes. + func passphrases() async throws -> [LCPPassphraseHash] /// Adds a new passphrase hash to the repository. /// - /// If a passphrase is already associated with the given `licenseID`, it will be updated. + /// If a passphrase is already associated with the given `licenseID`, it + /// will be updated. func addPassphrase( _ hash: LCPPassphraseHash, for licenseID: LicenseDocument.ID, diff --git a/Sources/LCP/LCPProgress.swift b/Sources/LCP/LCPProgress.swift index d32996600b..bdf8fa971e 100644 --- a/Sources/LCP/LCPProgress.swift +++ b/Sources/LCP/LCPProgress.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/LCP/LCPRenewDelegate.swift b/Sources/LCP/LCPRenewDelegate.swift index 29b2d34b49..190f948705 100644 --- a/Sources/LCP/LCPRenewDelegate.swift +++ b/Sources/LCP/LCPRenewDelegate.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -54,7 +54,7 @@ public class LCPDefaultRenewDelegate: NSObject, LCPRenewDelegate { } } - private var webPageContinuation: CheckedContinuation? = nil + private var webPageContinuation: CheckedContinuation? } extension LCPDefaultRenewDelegate: UIAdaptivePresentationControllerDelegate { diff --git a/Sources/LCP/LCPService.swift b/Sources/LCP/LCPService.swift index 947edf67a7..55b6c82552 100644 --- a/Sources/LCP/LCPService.swift +++ b/Sources/LCP/LCPService.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -21,12 +21,21 @@ public final class LCPService: Loggable { private let licenses: LicensesService private let assetRetriever: AssetRetriever - /// - Parameter deviceName: Device name used when registering a license to an LSD server. - /// If not provided, the device name will be the default `UIDevice.current.name`. - /// - Parameter deviceId: Device ID used when registering a license to an LSD server. - /// You must ensure the identifier is unique and stable for the device (persist and - /// reuse across app launches). If not provided, the device ID will be generated as - /// a random UUID. + /// - Parameters: + /// - client: The LCP client used for core license operations. + /// - licenseRepository: Repository for managing stored licenses. + /// - passphraseRepository: Repository for managing user passphrases. + /// - assetRetriever: The retriever used to fetch protected assets. + /// - httpClient: The HTTP client used for network requests to LSD/LCP servers. + /// - deviceName: Device name used when registering a license to an LSD server. + /// If not provided, the device name will be `UIDevice.current.name`. Since iOS 16, + /// this returns a generic name (e.g. "iPhone") unless the + /// `com.apple.developer.device-information.user-assigned-device-name` entitlement + /// is added to your app. + /// - deviceId: Device ID used when registering a license to an LSD server. + /// You must ensure the identifier is unique and stable for the device (persist and + /// reuse across app launches). If not provided, the device ID will be generated as + /// a random UUID. public init( client: LCPClient, licenseRepository: LCPLicenseRepository, @@ -95,14 +104,15 @@ public final class LCPService: Loggable { /// 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 + /// 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 + /// Updated licenses can currently be stored only into `Asset`s whose /// source property points to a `file://` URL. /// /// - Parameters: + /// - asset: The asset whose license is to be retrieved. /// - 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 in the given diff --git a/Sources/LCP/License/Container/ContainerLicenseContainer.swift b/Sources/LCP/License/Container/ContainerLicenseContainer.swift index 9e8771b2c2..3ab1994a28 100644 --- a/Sources/LCP/License/Container/ContainerLicenseContainer.swift +++ b/Sources/LCP/License/Container/ContainerLicenseContainer.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/LCP/License/Container/LicenseContainer.swift b/Sources/LCP/License/Container/LicenseContainer.swift index cffcada6cb..fdd8adab38 100644 --- a/Sources/LCP/License/Container/LicenseContainer.swift +++ b/Sources/LCP/License/Container/LicenseContainer.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/LCP/License/Container/ResourceLicenseContainer.swift b/Sources/LCP/License/Container/ResourceLicenseContainer.swift index ed8ea9ef32..1ed7a33e36 100644 --- a/Sources/LCP/License/Container/ResourceLicenseContainer.swift +++ b/Sources/LCP/License/Container/ResourceLicenseContainer.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/LCP/License/LCPError+wrap.swift b/Sources/LCP/License/LCPError+wrap.swift index 9d2d157c40..740fc8b3b4 100644 --- a/Sources/LCP/License/LCPError+wrap.swift +++ b/Sources/LCP/License/LCPError+wrap.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -25,19 +25,16 @@ extension LCPError { return .parsing(error) } - if let error = error as? HTTPError { + if let error = (error as? HTTPError) ?? HTTPError.wrap(error) { return .network(error) } let nsError = error as NSError - switch nsError.domain { - case "R2LCPClient.LCPClientError": + if nsError.domain == "R2LCPClient.LCPClientError" { return .licenseIntegrity(LCPClientError(rawValue: nsError.code) ?? .unknown) - case NSURLErrorDomain: - return .network(nsError) - default: - return .unknown(error) } + + return .unknown(error) } static func wrap(_ completion: @escaping (Result) -> Void) -> (Result) -> Void { diff --git a/Sources/LCP/License/License.swift b/Sources/LCP/License/License.swift index da345b0509..e8fab8f935 100644 --- a/Sources/LCP/License/License.swift +++ b/Sources/LCP/License/License.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,7 +9,7 @@ import ReadiumShared import ReadiumZIPFoundation final class License: Loggable { - // Last Documents which passed the integrity checks. + /// Last Documents which passed the integrity checks. private var documents: ValidatedDocuments // Dependencies @@ -37,19 +37,19 @@ final class License: Loggable { /// Public API extension License: LCPLicense { - public var license: LicenseDocument { + var license: LicenseDocument { documents.license } - public var status: StatusDocument? { + var status: StatusDocument? { documents.status } - public var isRestricted: Bool { + var isRestricted: Bool { documents.context.getOrNil() == nil } - public var error: LCPError? { + var error: LCPError? { switch documents.context { case .success: return nil @@ -65,11 +65,11 @@ extension License: LCPLicense { } } - public var encryptionProfile: String? { + var encryptionProfile: String? { license.encryption.profile } - public func decipher(_ data: Data) throws -> Data? { + func decipher(_ data: Data) throws -> Data? { let context = try documents.context.get() return client.decrypt(data: data, using: context) } @@ -153,7 +153,7 @@ extension License: LCPLicense { return } - rights.copy = max(0, printLeft - pageCount) + rights.print = max(0, printLeft - pageCount) } return allowed @@ -185,7 +185,7 @@ extension License: LCPLicense { } } - // Finds the renew link according to `prefersWebPage`. + /// Finds the renew link according to `prefersWebPage`. func findRenewLink() -> Link? { guard let status = documents.status else { return nil @@ -208,7 +208,7 @@ extension License: LCPLicense { return status.linkWithNoType(for: .renew) } - // Renew the loan by presenting a web page to the user. + /// Renew the loan by presenting a web page to the user. func renewWithWebPage(_ link: Link) async throws -> Data { guard let statusURL = try? license.url(for: .status, preferredType: .lcpStatusDocument), @@ -227,9 +227,9 @@ extension License: LCPLicense { .get() } - // Programmatically renew the loan with a PUT request. + /// Programmatically renew the loan with a PUT request. func renewProgrammatically(_ link: Link) async throws -> Data { - // Asks the delegate for a renew date if there's an `end` parameter. + /// Asks the delegate for a renew date if there's an `end` parameter. func preferredEndDate() async throws -> Date? { (link.templateParameters.contains("end")) ? try await delegate.preferredEndDate(maximum: maxRenewDate) diff --git a/Sources/LCP/License/LicenseValidation.swift b/Sources/LCP/License/LicenseValidation.swift index e0e087c543..cc23150d00 100644 --- a/Sources/LCP/License/LicenseValidation.swift +++ b/Sources/LCP/License/LicenseValidation.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -25,7 +25,7 @@ private let supportedProfiles = [ typealias Context = Result -// Holds the License/Status Documents and the DRM context, once validated. +/// Holds the License/Status Documents and the DRM context, once validated. struct ValidatedDocuments { let license: LicenseDocument let context: Context @@ -54,12 +54,12 @@ final actor LicenseValidation: Loggable { fileprivate let httpClient: HTTPClient fileprivate let passphrases: PassphrasesService - // List of observers notified when the Documents are validated, or if an error occurred. + /// List of observers notified when the Documents are validated, or if an error occurred. fileprivate var observers: [(callback: Observer, policy: ObserverPolicy)] = [] fileprivate let onLicenseValidated: (LicenseDocument) async throws -> Void - // Current state in the validation steps. + /// Current state in the validation steps. private(set) var state: State = .start { didSet { log(.debug, "* \(state)") @@ -90,7 +90,7 @@ final actor LicenseValidation: Loggable { self.onLicenseValidated = onLicenseValidated } - // Raw Document's data to validate. + /// Raw Document's data to validate. enum Document { case license(Data) case status(Data) @@ -153,12 +153,14 @@ extension LicenseValidation { } else { self = .fetchStatus(license) } + case let (.validateLicense(_, _), .failed(error)): self = .failure(error) // 2. Fetch the status document case let (.fetchStatus(license), .retrievedStatusData(data)): self = .validateStatus(license, data) + case let (.fetchStatus(license), .failed(_)): // We ignore any error while fetching the Status Document, as it is optional self = .checkLicenseStatus(license, nil, statusDocumentTakesPrecedence: false) @@ -171,6 +173,7 @@ extension LicenseValidation { } else { self = .checkLicenseStatus(license, status, statusDocumentTakesPrecedence: false) } + case let (.validateStatus(license, _), .failed(_)): // We ignore any error while validating the Status Document, as it is optional self = .checkLicenseStatus(license, nil, statusDocumentTakesPrecedence: false) @@ -178,6 +181,7 @@ extension LicenseValidation { // 3. Get an updated license if needed case let (.fetchLicense(_, status), .retrievedLicenseData(data)): self = .validateLicense(data, status) + case let (.fetchLicense(license, status), .failed(_)): // We ignore any error while fetching the updated License Document // Note: since we failed to get the updated License, then the Status Document will take precedence over the License when checking the status. @@ -194,8 +198,10 @@ extension LicenseValidation { // 5. Get the passphrase associated with the license case let (.requestPassphrase(license, status), .retrievedPassphrase(passphrase)): self = .validateIntegrity(license, status, passphrase: passphrase) + case let (.requestPassphrase, .failed(error)): self = .failure(error) + case let (.requestPassphrase(license, status), .passphraseNotFound): self = .valid(ValidatedDocuments(license, .failure(.missingPassphrase), status)) @@ -207,6 +213,7 @@ extension LicenseValidation { } else { self = .valid(documents) } + case let (.validateIntegrity(_, _, _), .failed(error)): self = .failure(error) @@ -217,6 +224,7 @@ extension LicenseValidation { } else { self = .valid(documents) } + case let (.registerDevice(documents, _), .failed(_)): // We ignore any error while registrating the device self = .valid(documents) @@ -236,25 +244,25 @@ extension LicenseValidation { } fileprivate enum Event { - // Raised when reading the License from its container, or when updating it from an LCP server. + /// Raised when reading the License from its container, or when updating it from an LCP server. case retrievedLicenseData(Data) - // Raised when the License Document is parsed and its structure is validated. + /// Raised when the License Document is parsed and its structure is validated. case validatedLicense(LicenseDocument) - // Raised after fetching the Status Document, or receiving it as a response of an LSD interaction. + /// Raised after fetching the Status Document, or receiving it as a response of an LSD interaction. case retrievedStatusData(Data) - // Raised after parsing and validating a Status Document's data. + /// Raised after parsing and validating a Status Document's data. case validatedStatus(StatusDocument) - // Raised after the License's status was checked, with any occurred status error. + /// Raised after the License's status was checked, with any occurred status error. case checkedLicenseStatus(StatusError?) - // Raised when we retrieved the passphrase from the local database, or from prompting the user. + /// Raised when we retrieved the passphrase from the local database, or from prompting the user. case retrievedPassphrase(String) - // Raised after validating the integrity of the License using liblcp.a. + /// Raised after validating the integrity of the License using liblcp.a. case validatedIntegrity(LCPClientContext) - // Raised when the device is registered, with an optional updated Status Document. + /// Raised when the device is registered, with an optional updated Status Document. case registeredDevice(Data?) - // Raised when any error occurs during the validation workflow. + /// Raised when any error occurs during the validation workflow. case failed(Error) - // Raised when no passphrase could be found or given by the user. + /// Raised when no passphrase could be found or given by the user. case passphraseNotFound } @@ -420,9 +428,9 @@ extension LicenseValidation { typealias Observer = (Result) -> Void enum ObserverPolicy { - // The observer is automatically removed when called. + /// The observer is automatically removed when called. case once - // The observer is called everytime the validation is finished. + /// The observer is called everytime the validation is finished. case always } diff --git a/Sources/LCP/License/Model/Components/LCP/ContentKey.swift b/Sources/LCP/License/Model/Components/LCP/ContentKey.swift index 5f780c37d1..174034decc 100644 --- a/Sources/LCP/License/Model/Components/LCP/ContentKey.swift +++ b/Sources/LCP/License/Model/Components/LCP/ContentKey.swift @@ -1,22 +1,24 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumShared /// Used to encrypt the Publication Resources. /// This is encrypted using the User Key. -public struct ContentKey { +public struct ContentKey: JSONValueDecodable { /// Algorithm used to encrypt the Content Key, identified using the URIs defined in [XML-ENC]. This MUST match the Content Key encryption algorithm named in the Encryption Profile identified in `encryption/profile`. public let algorithm: String /// Encrypted Content Key. public let encryptedValue: String - init(json: [String: Any]) throws { - guard let algorithm = json["algorithm"] as? String, - let encryptedValue = json["encrypted_value"] as? String + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue.object, + let algorithm = json["algorithm"]?.string, + let encryptedValue = json["encrypted_value"]?.string else { throw ParsingError.encryption } diff --git a/Sources/LCP/License/Model/Components/LCP/Encryption.swift b/Sources/LCP/License/Model/Components/LCP/Encryption.swift index f2545a8409..9b00cab48b 100644 --- a/Sources/LCP/License/Model/Components/LCP/Encryption.swift +++ b/Sources/LCP/License/Model/Components/LCP/Encryption.swift @@ -1,12 +1,13 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumShared -public struct Encryption { +public struct Encryption: JSONValueDecodable { /// Identifies the Encryption Profile used by this LCP-protected Publication. public let profile: String /// Used to encrypt the Publication Resources. @@ -14,16 +15,17 @@ public struct Encryption { /// Used to encrypt the Content Key. public let userKey: UserKey - init(json: [String: Any]) throws { - guard let profile = json["profile"] as? String, - let contentKey = json["content_key"] as? [String: Any], - let userKey = json["user_key"] as? [String: Any] + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue.object, + let profile = json["profile"]?.string, + let contentKey = try ContentKey(json: json["content_key"], warnings: warnings), + let userKey = try UserKey(json: json["user_key"], warnings: warnings) else { throw ParsingError.encryption } self.profile = profile - self.contentKey = try ContentKey(json: contentKey) - self.userKey = try UserKey(json: userKey) + self.contentKey = contentKey + self.userKey = userKey } } diff --git a/Sources/LCP/License/Model/Components/LCP/Rights.swift b/Sources/LCP/License/Model/Components/LCP/Rights.swift index 8d5653fc72..aee383bb50 100644 --- a/Sources/LCP/License/Model/Components/LCP/Rights.swift +++ b/Sources/LCP/License/Model/Components/LCP/Rights.swift @@ -1,12 +1,13 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumShared -public struct Rights { +public struct Rights: JSONValueDecodable { /// Maximum number of pages that can be printed over the lifetime of the license. public let print: Int? /// Maximum number of characters that can be copied to the clipboard over the lifetime of the license. @@ -16,14 +17,14 @@ public struct Rights { /// Date and time when the license ends. public let end: Date? /// Implementor-specific rights extensions. Each extension is identified by an URI. - public let extensions: [String: Any] + public let extensions: [String: JSONValue] - init(json: [String: Any]?) throws { - var json = json ?? [:] - self.print = json.removeValue(forKey: "print") as? Int - copy = json.removeValue(forKey: "copy") as? Int - start = (json.removeValue(forKey: "start") as? String)?.dateFromISO8601 - end = (json.removeValue(forKey: "end") as? String)?.dateFromISO8601 + public init?(json: T?, warnings: WarningLogger?) throws { + var json = json?.jsonValue.object ?? [:] + self.print = json.pop("print")?.nonNegative() + copy = json.pop("copy")?.nonNegative() + start = json.pop("start")?.date + end = json.pop("end")?.date extensions = json } } diff --git a/Sources/LCP/License/Model/Components/LCP/Signature.swift b/Sources/LCP/License/Model/Components/LCP/Signature.swift index dd78554ba6..a77104220e 100644 --- a/Sources/LCP/License/Model/Components/LCP/Signature.swift +++ b/Sources/LCP/License/Model/Components/LCP/Signature.swift @@ -1,13 +1,14 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumShared /// Signature allowing to certify the License Document integrity. -public struct Signature { +public struct Signature: JSONValueDecodable { /// Algorithm used to calculate the signature, identified using the URIs given in [XML-SIG]. This MUST match the signature algorithm named in the Encryption Profile identified in `encryption/profile`. public let algorithm: String /// The Provider Certificate: an X509 certificate used by the Content Provider. @@ -15,10 +16,11 @@ public struct Signature { /// Value of the signature. public let value: String - init(json: [String: Any]) throws { - guard let algorithm = json["algorithm"] as? String, - let certificate = json["certificate"] as? String, - let value = json["value"] as? String + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue.object, + let algorithm = json["algorithm"]?.string, + let certificate = json["certificate"]?.string, + let value = json["value"]?.string else { throw ParsingError.signature } diff --git a/Sources/LCP/License/Model/Components/LCP/User.swift b/Sources/LCP/License/Model/Components/LCP/User.swift index cbe313b951..c21be867f8 100644 --- a/Sources/LCP/License/Model/Components/LCP/User.swift +++ b/Sources/LCP/License/Model/Components/LCP/User.swift @@ -1,12 +1,13 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumShared -public struct User { +public struct User: JSONValueDecodable { public typealias ID = String /// Unique identifier for the User at a specific Provider. @@ -16,16 +17,35 @@ public struct User { /// The User’s name. public let name: String? /// Implementor-specific extensions. Each extension is identified by an URI. - public let extensions: [String: Any] + public let extensions: [String: JSONValue] /// A list of which user object values are encrypted in this License Document. public let encrypted: [String] - init(json: [String: Any]?) throws { - var json = json ?? [:] - id = json.removeValue(forKey: "id") as? String - email = json.removeValue(forKey: "email") as? String - name = json.removeValue(forKey: "name") as? String - encrypted = (json.removeValue(forKey: "encrypted") as? [String]) ?? [] - extensions = json + public init( + id: ID? = nil, + email: String? = nil, + name: String? = nil, + extensions: [String: JSONValue] = [:], + encrypted: [String] = [] + ) { + self.id = id + self.email = email + self.name = name + self.extensions = extensions + self.encrypted = encrypted + } + + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue else { + return nil + } + + var dict = json.object ?? [:] + + id = dict.pop("id")?.string + email = dict.pop("email")?.string + name = dict.pop("name")?.string + encrypted = dict.pop("encrypted")?.decode() ?? [] + extensions = dict } } diff --git a/Sources/LCP/License/Model/Components/LCP/UserKey.swift b/Sources/LCP/License/Model/Components/LCP/UserKey.swift index ba25f1e743..92760e92a2 100644 --- a/Sources/LCP/License/Model/Components/LCP/UserKey.swift +++ b/Sources/LCP/License/Model/Components/LCP/UserKey.swift @@ -1,13 +1,14 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumShared /// Used to encrypt the ContentKey. -public struct UserKey { +public struct UserKey: JSONValueDecodable { /// A hint to be displayed to the User to help them remember the User Passphrase. public let textHint: String /// Algorithm used to generate the User Key from the User Passphrase, identified using the URIs defined in [XML-ENC]. This MUST match the User Key hash algorithm named in the Encryption Profile identified in `encryption/profile`. @@ -15,10 +16,11 @@ public struct UserKey { /// The value of the License Document’s `id` field, encrypted using the User Key and the same algorithm identified for Content Key encryption in `encryption/content_key/algorithm`. This is used to verify that the Reading System has the correct User Key. public let keyCheck: String - init(json: [String: Any]) throws { - guard let textHint = json["text_hint"] as? String, - let algorithm = json["algorithm"] as? String, - let keyCheck = json["key_check"] as? String + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue.object, + let textHint = json["text_hint"]?.string, + let algorithm = json["algorithm"]?.string, + let keyCheck = json["key_check"]?.string else { throw ParsingError.encryption } diff --git a/Sources/LCP/License/Model/Components/LSD/Event.swift b/Sources/LCP/License/Model/Components/LSD/Event.swift index da792c868e..7f8e1fdae5 100644 --- a/Sources/LCP/License/Model/Components/LSD/Event.swift +++ b/Sources/LCP/License/Model/Components/LSD/Event.swift @@ -1,23 +1,24 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumShared /// Event related to the change in status of a License Document. -public struct Event { +public struct Event: JSONValueDecodable { public enum EventType: String { - // Signals a successful registration event by a device. + /// Signals a successful registration event by a device. case register - // Signals a successful renew event. + /// Signals a successful renew event. case renew - // Signals a successful return event. + /// Signals a successful return event. case `return` - // Signals a revocation event. + /// Signals a revocation event. case revoke - // Signals a cancellation event. + /// Signals a cancellation event. case cancel } @@ -31,11 +32,12 @@ public struct Event { /// Time and date when the event occurred. public let date: Date // Named timestamp in spec. - init?(json: [String: Any]) { - guard let type = json["type"] as? String, - let name = json["name"] as? String, - let id = json["id"] as? String, - let date = (json["timestamp"] as? String)?.dateFromISO8601 + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue.object, + let type = json["type"]?.string, + let name = json["name"]?.string, + let id = json["id"]?.string, + let date = json["timestamp"]?.date else { return nil } diff --git a/Sources/LCP/License/Model/Components/LSD/PotentialRights.swift b/Sources/LCP/License/Model/Components/LSD/PotentialRights.swift index 4ba1ba6769..f70e910a2e 100644 --- a/Sources/LCP/License/Model/Components/LSD/PotentialRights.swift +++ b/Sources/LCP/License/Model/Components/LSD/PotentialRights.swift @@ -1,16 +1,18 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumShared -public struct PotentialRights { +public struct PotentialRights: JSONValueDecodable { /// Time and Date when the license ends. public let end: Date? - init(json: [String: Any]) throws { - end = (json["end"] as? String)?.dateFromISO8601 + public init?(json: T?, warnings: WarningLogger?) throws { + let json = json?.jsonValue.object + end = json?["end"]?.date } } diff --git a/Sources/LCP/License/Model/Components/Link.swift b/Sources/LCP/License/Model/Components/Link.swift index b737af4bcd..0f676245f4 100644 --- a/Sources/LCP/License/Model/Components/Link.swift +++ b/Sources/LCP/License/Model/Components/Link.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -8,7 +8,7 @@ import Foundation import ReadiumShared /// A Link to a resource. -public struct Link { +public struct Link: JSONValueDecodable { /// The link destination. public let href: String /// Indicates the relationship between the resource and its containing collection. @@ -26,26 +26,26 @@ public struct Link { /// SHA-256 hash of the resource. public let hash: String? - init(json: [String: Any]) throws { - guard let href = json["href"] as? String else { + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue.object, + let href = json["href"]?.string + else { throw ParsingError.link } - if let rel = json["rel"] as? String { - self.rel = [rel] - } else if let rel = json["rel"] as? [String], !rel.isEmpty { - self.rel = rel - } else { + let rel: [String] = json["rel"]?.decode(allowingSingle: true) ?? [] + guard !rel.isEmpty else { throw ParsingError.link } self.href = href - title = json["title"] as? String - type = json["type"] as? String - templated = (json["templated"] as? Bool) ?? false - profile = json["profile"] as? String - length = json["length"] as? Int - hash = json["hash"] as? String + self.rel = rel + title = json["title"]?.string + type = json["type"]?.string + templated = json["templated"]?.bool ?? false + profile = json["profile"]?.string + length = json["length"]?.integer + hash = json["hash"]?.string } /// Gets the valid URL if possible, applying the given template context as query parameters if the link is templated. diff --git a/Sources/LCP/License/Model/Components/Links.swift b/Sources/LCP/License/Model/Components/Links.swift index 02d6979416..3e937dd6f6 100644 --- a/Sources/LCP/License/Model/Components/Links.swift +++ b/Sources/LCP/License/Model/Components/Links.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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,11 +7,11 @@ import Foundation import ReadiumShared -public struct Links { +public struct Links: JSONValueDecodable { private let links: [Link] - init(json: [[String: Any]]) throws { - links = try json.map(Link.init) + public init?(json: T?, warnings: WarningLogger?) throws { + links = json?.jsonValue.decode(warnings: warnings) ?? [] } /// Returns all the links with the given `rel`. diff --git a/Sources/LCP/License/Model/LicenseDocument.swift b/Sources/LCP/License/Model/LicenseDocument.swift index ca47d6ddc5..beeb02ec1c 100644 --- a/Sources/LCP/License/Model/LicenseDocument.swift +++ b/Sources/LCP/License/Model/LicenseDocument.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -13,17 +13,17 @@ public struct LicenseDocument { public typealias ID = String public typealias Provider = String - // The possible rel of Links. + /// The possible rel of Links. public enum Rel: String { - // Location where a Reading System can redirect a User looking for additional information about the User Passphrase. + /// Location where a Reading System can redirect a User looking for additional information about the User Passphrase. case hint - // Location where the Publication associated with the License Document can be downloaded + /// Location where the Publication associated with the License Document can be downloaded case publication - // As defined in the IANA registry of link relations: "Conveys an identifier for the link's context." + /// As defined in the IANA registry of link relations: "Conveys an identifier for the link's context." case `self` - // Support resources for the user (either a website, an email or a telephone number). + /// Support resources for the user (either a website, an email or a telephone number). case support - // Location to the Status Document for this license. + /// Location to the Status Document for this license. case status } @@ -35,7 +35,7 @@ public struct LicenseDocument { public let issued: Date /// Date when the license was last updated. public let updated: Date - // Encryption object. + /// Encryption object. public let encryption: Encryption /// Used to associate the License Document with resources that are not locally available. public let links: Links @@ -53,20 +53,25 @@ public struct LicenseDocument { public let jsonString: String public init(data: Data) throws { - guard - let jsonString = String(data: data, encoding: .utf8), - let deserializedJSON = try? JSONSerialization.jsonObject(with: data) - else { + guard let jsonString = String(data: data, encoding: .utf8) else { + throw ParsingError.malformedJSON + } + + let jsonValue: JSONValue + do { + jsonValue = try JSONValue(jsonData: data) + } catch { throw ParsingError.malformedJSON } - guard let json = deserializedJSON as? [String: Any], - let provider = json["provider"] as? String, - let id = json["id"] as? String, - let issued = (json["issued"] as? String)?.dateFromISO8601, - let encryption = json["encryption"] as? [String: Any], - let links = json["links"] as? [[String: Any]], - let signature = json["signature"] as? [String: Any] + guard let json = jsonValue.object, + let provider = json["provider"]?.string, + let id = json["id"]?.string, + let issued = json["issued"]?.date, + let encryption = try Encryption(json: json["encryption"]), + let links = try Links(json: json["links"]), + let rights = try Rights(json: json["rights"]), + let signature = try Signature(json: json["signature"]) else { throw ParsingError.licenseDocument } @@ -74,16 +79,16 @@ public struct LicenseDocument { self.provider = provider self.id = id self.issued = issued - updated = (json["updated"] as? String)?.dateFromISO8601 ?? issued - self.encryption = try Encryption(json: encryption) - self.links = try Links(json: links) - user = try User(json: json["user"] as? [String: Any]) - rights = try Rights(json: json["rights"] as? [String: Any]) - self.signature = try Signature(json: signature) + updated = json["updated"]?.date ?? issued + self.encryption = encryption + self.links = links + user = try User(json: json["user"]) ?? User() + self.rights = rights + self.signature = signature jsonData = data self.jsonString = jsonString - /// Checks that `links` contains at least one link with `publication` relation. + // Checks that `links` contains at least one link with `publication` relation. guard link(for: .publication) != nil else { throw ParsingError.licenseDocument } diff --git a/Sources/LCP/License/Model/StatusDocument.swift b/Sources/LCP/License/Model/StatusDocument.swift index c9f25d7d82..0857e7d2d2 100644 --- a/Sources/LCP/License/Model/StatusDocument.swift +++ b/Sources/LCP/License/Model/StatusDocument.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -11,17 +11,17 @@ import ReadiumShared /// https://github.com/readium/lcp-specs/blob/master/schema/status.schema.json public struct StatusDocument { public enum Status: String { - // The License Document is available, but the user hasn't accessed the License and/or Status Document yet. + /// The License Document is available, but the user hasn't accessed the License and/or Status Document yet. case ready - // The license is active, and a device has been successfully registered for this license. This is the default value if the License Document does not contain a registration link, or a registration mechanism through the license itself. + /// The license is active, and a device has been successfully registered for this license. This is the default value if the License Document does not contain a registration link, or a registration mechanism through the license itself. case active - // The license is no longer active, it has been invalidated by the Issuer. + /// The license is no longer active, it has been invalidated by the Issuer. case revoked - // The license is no longer active, it has been invalidated by the User. + /// The license is no longer active, it has been invalidated by the User. case returned - // The license is no longer active because it was cancelled prior to activation. + /// The license is no longer active because it was cancelled prior to activation. case cancelled - // The license is no longer active because it has expired. + /// The license is no longer active because it has expired. case expired } @@ -47,19 +47,19 @@ public struct StatusDocument { public let events: [Event] init(data: Data) throws { - guard let deserializedJSON = try? JSONSerialization.jsonObject(with: data) else { + guard let jsonValue = try? JSONValue(jsonData: data) else { throw ParsingError.malformedJSON } - guard let json = deserializedJSON as? [String: Any], - let id = json["id"] as? String, - let statusRaw = json["status"] as? String, + guard let json = jsonValue.object, + let id = json["id"]?.string, + let statusRaw = json["status"]?.string, let status = Status(rawValue: statusRaw), - let message = json["message"] as? String, - let updated = json["updated"] as? [String: Any], - let licenseUpdated = (updated["license"] as? String)?.dateFromISO8601, - let statusUpdated = (updated["status"] as? String)?.dateFromISO8601, - let links = json["links"] as? [[String: Any]] + let message = json["message"]?.string, + let updated = json["updated"]?.object, + let licenseUpdated = updated["license"]?.date, + let statusUpdated = updated["status"]?.date, + let links = try Links(json: json["links"]) else { throw ParsingError.statusDocument } @@ -69,19 +69,10 @@ public struct StatusDocument { self.message = message self.licenseUpdated = licenseUpdated self.updated = statusUpdated - self.links = try Links(json: links) + self.links = links - if let potentialRights = json["potential_rights"] as? [String: Any] { - self.potentialRights = try PotentialRights(json: potentialRights) - } else { - potentialRights = nil - } - - if let events = json["events"] as? [[String: Any]] { - self.events = events.compactMap(Event.init) - } else { - events = [] - } + events = json["events"]?.decode() ?? [] + potentialRights = try? json["potential_rights"]?.decode() } /// Returns the first link containing the given rel. diff --git a/Sources/LCP/Repositories/Keychain/LCPKeychainLicenseRepository.swift b/Sources/LCP/Repositories/Keychain/LCPKeychainLicenseRepository.swift new file mode 100644 index 0000000000..c42db9b1a4 --- /dev/null +++ b/Sources/LCP/Repositories/Keychain/LCPKeychainLicenseRepository.swift @@ -0,0 +1,273 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumInternal +import ReadiumShared + +/// Errors occurring in ``LCPKeychainLicenseRepository``. +public enum LCPKeychainLicenseRepositoryError: Error { + /// The license with the given `id` was not found in the repository. + case licenseNotFound(id: LicenseDocument.ID) + + /// An error occurred while accessing the keychain. + case keychain(KeychainError) + + /// An error occurred while decoding or encoding a License. + case coding(Error) +} + +/// Keychain-based implementation of ``LCPLicenseRepository``. +/// +/// Stores license data securely in the iOS/macOS Keychain with optional iCloud +/// synchronization. +public actor LCPKeychainLicenseRepository: LCPLicenseRepository, Loggable { + /// Internal data structure for storing license information in the Keychain. + private struct License: Codable { + /// Unique identifier for this license. + let licenseID: LicenseDocument.ID + + /// JSON representation of the ``LicenseDocument``. + var licenseJSON: String? + + /// Remaining pages to print. + var printsLeft: Int? + + /// Remaining number of characters to copy. + var copiesLeft: Int? + + /// Date when the device was registered for this license. + var registered: Bool + + /// Date this license was added to the Keychain. + let created: Date + + /// Date this license was updated in the Keychain. + var updated: Date + } + + private let keychain: Keychain + private let encoder: JSONEncoder + private let decoder: JSONDecoder + + /// Initializes a Keychain-based license repository. + /// + /// - Parameters: + /// - synchronizable: Whether items should sync via iCloud Keychain. + public init(synchronizable: Bool = true) { + keychain = Keychain( + serviceName: "org.readium.lcp.licenses", + synchronizable: synchronizable + ) + + encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + + decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + } + + // MARK: - LCPLicenseRepository + + public func addLicense(_ licenseDocument: LicenseDocument) async throws { + if var license = try getLicense(for: licenseDocument.id) { + // License exists - update it without overwriting consumable rights + license.licenseJSON = licenseDocument.jsonString + try updateLicense(license, for: licenseDocument.id) + + } else { + // New license - initialize with rights from license document + let newLicense = License( + licenseID: licenseDocument.id, + licenseJSON: licenseDocument.jsonString, + printsLeft: licenseDocument.rights.print, + copiesLeft: licenseDocument.rights.copy, + registered: false, + created: Date(), + updated: Date() + ) + + try addLicense(newLicense, for: licenseDocument.id) + } + } + + public func license(for id: LicenseDocument.ID) async throws -> LicenseDocument? { + guard + let licenseData = try getLicense(for: id), + let jsonString = licenseData.licenseJSON, + let jsonData = jsonString.data(using: .utf8), + let licenseDocument = try? LicenseDocument(data: jsonData) + else { + return nil + } + + return licenseDocument + } + + public func isDeviceRegistered(for id: LicenseDocument.ID) async throws -> Bool { + try requireLicense(for: id).registered + } + + public func registerDevice(for id: LicenseDocument.ID) async throws { + var license = try requireLicense(for: id) + license.registered = true + try updateLicense(license, for: id) + } + + public func userRights(for id: LicenseDocument.ID) async throws -> LCPConsumableUserRights { + guard let licenseData = try getLicense(for: id) else { + throw LCPKeychainLicenseRepositoryError.licenseNotFound(id: id) + } + + return LCPConsumableUserRights( + print: licenseData.printsLeft, + copy: licenseData.copiesLeft + ) + } + + public func updateUserRights( + for id: LicenseDocument.ID, + with changes: (inout LCPConsumableUserRights) -> Void + ) async throws { + var license = try requireLicense(for: id) + + // Get current rights + var currentRights = LCPConsumableUserRights( + print: license.printsLeft, + copy: license.copiesLeft + ) + + // Apply changes + changes(¤tRights) + + // Update the data + license.printsLeft = currentRights.print + license.copiesLeft = currentRights.copy + + try updateLicense(license, for: id) + } + + /// Removes all licenses from the repository. + public func clear() async throws { + do { + try keychain.deleteAll() + } catch { + throw LCPKeychainLicenseRepositoryError.keychain(error) + } + } + + // MARK: - Migration Support + + /// Imports license rights from an external source without requiring the + /// full ``LicenseDocument``. + /// + /// This is used during migration from repositories that don't store the + /// full document, like the legacy SQLite repositories. + /// + /// When the publication is later opened, `addLicense()` will add the full + /// document while preserving these migrated rights. + /// + /// - Parameters: + /// - licenseID: The license identifier + /// - rights: The consumable user rights to store + /// - registered: Whether the device is registered for this license + public func importLicenseRights( + for licenseID: LicenseDocument.ID, + rights: LCPConsumableUserRights, + registered: Bool + ) async throws { + // We don't overwrite the rights if the license already exists. + guard try getLicense(for: licenseID) == nil else { + return + } + + // Create new entry without the full license document, which will + // be added when the publication is opened again. + let newData = License( + licenseID: licenseID, + licenseJSON: nil, + printsLeft: rights.print, + copiesLeft: rights.copy, + registered: registered, + created: Date(), + updated: Date() + ) + try addLicense(newData, for: licenseID) + } + + // MARK: - Keychain Access + + private func requireLicense(for licenseID: LicenseDocument.ID) throws(LCPKeychainLicenseRepositoryError) -> License { + guard let license = try getLicense(for: licenseID) else { + throw .licenseNotFound(id: licenseID) + } + return license + } + + /// Gets a license from the Keychain for the given license ID. + private func getLicense(for licenseID: LicenseDocument.ID) throws(LCPKeychainLicenseRepositoryError) -> License? { + guard let data = try getFromKeychain(id: licenseID) else { + return nil + } + + return try decode(data) + } + + /// Adds a new license to the Keychain. + private func addLicense(_ license: License, for id: LicenseDocument.ID) throws(LCPKeychainLicenseRepositoryError) { + try addToKeychain(data: encode(license), for: id) + } + + /// Updates an existing license in the Keychain. + private func updateLicense(_ license: License, for id: LicenseDocument.ID) throws(LCPKeychainLicenseRepositoryError) { + var license = license + license.updated = Date() + let data = try encode(license) + try updateKeychain(data: data, for: id) + } + + // MARK: - Low-Level Helpers + + private func getFromKeychain(id: LicenseDocument.ID) throws(LCPKeychainLicenseRepositoryError) -> Data? { + do { + return try keychain.load(forKey: id) + } catch { + throw .keychain(error) + } + } + + private func addToKeychain(data: Data, for id: LicenseDocument.ID) throws(LCPKeychainLicenseRepositoryError) { + do { + try keychain.save(data: data, forKey: id) + } catch { + throw .keychain(error) + } + } + + private func updateKeychain(data: Data, for id: LicenseDocument.ID) throws(LCPKeychainLicenseRepositoryError) { + do { + try keychain.update(data: data, forKey: id) + } catch { + throw .keychain(error) + } + } + + private func decode(_ data: Data) throws(LCPKeychainLicenseRepositoryError) -> License { + do { + return try decoder.decode(License.self, from: data) + } catch { + throw .coding(error) + } + } + + private func encode(_ license: License) throws(LCPKeychainLicenseRepositoryError) -> Data { + do { + return try encoder.encode(license) + } catch { + throw .coding(error) + } + } +} diff --git a/Sources/LCP/Repositories/Keychain/LCPKeychainPassphraseRepository.swift b/Sources/LCP/Repositories/Keychain/LCPKeychainPassphraseRepository.swift new file mode 100644 index 0000000000..13ff5b1f22 --- /dev/null +++ b/Sources/LCP/Repositories/Keychain/LCPKeychainPassphraseRepository.swift @@ -0,0 +1,207 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumInternal +import ReadiumShared + +/// Errors occurring in ``LCPKeychainPassphraseRepository``. +public enum LCPKeychainPassphraseRepositoryError: Error { + /// An error occurred while accessing the keychain. + case keychain(KeychainError) + + /// An error occurred while decoding or encoding a passphrase. + case coding(Error) +} + +/// Keychain-based implementation of ``LCPPassphraseRepository``. +/// +/// Stores passphrase hashes securely in the iOS/macOS Keychain with optional +/// iCloud synchronization. +public actor LCPKeychainPassphraseRepository: LCPPassphraseRepository, Loggable { + /// Internal data structure for storing passphrase information in the + /// Keychain. + private struct Passphrase: Codable { + /// Unique identifier for the license this passphrase belongs to. + let licenseID: LicenseDocument.ID + + /// The hashed passphrase. + var passphraseHash: LCPPassphraseHash + + /// The license provider. + var provider: LicenseDocument.Provider + + /// The user identifier. + var userID: User.ID? + + /// Date this passphrase was added to the Keychain. + let created: Date + + /// Date this passphrase was updated in the Keychain. + var updated: Date + } + + private let keychain: Keychain + private let encoder: JSONEncoder + private let decoder: JSONDecoder + + /// Initializes a Keychain-based passphrase repository. + /// + /// - Parameters: + /// - synchronizable: Whether items should sync via iCloud Keychain. + public init(synchronizable: Bool = true) { + keychain = Keychain( + serviceName: "org.readium.lcp.passphrases", + synchronizable: synchronizable + ) + + encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + + decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + } + + // MARK: - LCPPassphraseRepository + + public func passphrase(for licenseID: LicenseDocument.ID) async throws -> LCPPassphraseHash? { + try getPassphrase(for: licenseID)?.passphraseHash + } + + public func passphrasesMatching( + userID: User.ID?, + provider: LicenseDocument.Provider + ) async throws -> [LCPPassphraseHash] { + try await getAllPassphrases() + .filter { passphrase in + passphrase.provider == provider && (userID == nil || passphrase.userID == userID) + } + .map(\.passphraseHash) + } + + public func passphrases() async throws -> [LCPPassphraseHash] { + try await getAllPassphrases() + .map(\.passphraseHash) + } + + public func addPassphrase( + _ hash: LCPPassphraseHash, + for licenseID: LicenseDocument.ID, + userID: User.ID?, + provider: LicenseDocument.Provider + ) async throws { + if var passphrase = try getPassphrase(for: licenseID) { + passphrase.passphraseHash = hash + passphrase.provider = provider + passphrase.userID = userID + try updatePassphrase(passphrase, for: licenseID) + } else { + let passphrase = Passphrase( + licenseID: licenseID, + passphraseHash: hash, + provider: provider, + userID: userID, + created: Date(), + updated: Date() + ) + + try addPassphrase(passphrase, for: licenseID) + } + } + + /// Removes all passphrases from the repository. + public func clear() async throws { + do { + try keychain.deleteAll() + } catch { + throw LCPKeychainPassphraseRepositoryError.keychain(error) + } + } + + // MARK: - Keychain Access + + private func getAllPassphrases() async throws(LCPKeychainPassphraseRepositoryError) -> [Passphrase] { + try getAllFromKeychain() + .compactMap { _, data in + guard let passphrase = try? decoder.decode(Passphrase.self, from: data) else { + return nil + } + return passphrase + } + } + + /// Gets a passphrase from the Keychain for the given license ID. + private func getPassphrase(for licenseID: LicenseDocument.ID) throws(LCPKeychainPassphraseRepositoryError) -> Passphrase? { + guard let data = try getFromKeychain(id: licenseID) else { + return nil + } + + return try decode(data) + } + + /// Adds a new passphrase to the Keychain. + private func addPassphrase(_ passphrase: Passphrase, for id: LicenseDocument.ID) throws(LCPKeychainPassphraseRepositoryError) { + try addToKeychain(data: encode(passphrase), for: id) + } + + /// Updates an existing passphrase in the Keychain. + private func updatePassphrase(_ passphrase: Passphrase, for id: LicenseDocument.ID) throws(LCPKeychainPassphraseRepositoryError) { + var passphrase = passphrase + passphrase.updated = Date() + let data = try encode(passphrase) + try updateKeychain(data: data, for: id) + } + + // MARK: - Low-Level Helpers + + private func getFromKeychain(id: LicenseDocument.ID) throws(LCPKeychainPassphraseRepositoryError) -> Data? { + do { + return try keychain.load(forKey: id) + } catch { + throw .keychain(error) + } + } + + private func getAllFromKeychain() throws(LCPKeychainPassphraseRepositoryError) -> [String: Data] { + do { + return try keychain.allItems() + } catch { + throw .keychain(error) + } + } + + private func addToKeychain(data: Data, for id: LicenseDocument.ID) throws(LCPKeychainPassphraseRepositoryError) { + do { + try keychain.save(data: data, forKey: id) + } catch { + throw .keychain(error) + } + } + + private func updateKeychain(data: Data, for id: LicenseDocument.ID) throws(LCPKeychainPassphraseRepositoryError) { + do { + try keychain.update(data: data, forKey: id) + } catch { + throw .keychain(error) + } + } + + private func decode(_ data: Data) throws(LCPKeychainPassphraseRepositoryError) -> Passphrase { + do { + return try decoder.decode(Passphrase.self, from: data) + } catch { + throw .coding(error) + } + } + + private func encode(_ passphrase: Passphrase) throws(LCPKeychainPassphraseRepositoryError) -> Data { + do { + return try encoder.encode(passphrase) + } catch { + throw .coding(error) + } + } +} diff --git a/Sources/LCP/Resources/en.lproj/Localizable.strings b/Sources/LCP/Resources/en.lproj/Localizable.strings index 8e15d16ff9..e2ae1f828f 100644 --- a/Sources/LCP/Resources/en.lproj/Localizable.strings +++ b/Sources/LCP/Resources/en.lproj/Localizable.strings @@ -1,44 +1,13 @@ -/* - Copyright 2025 Readium Foundation. All rights reserved. - Use of this source code is governed by the BSD-style license - available in the top-level LICENSE file of the project. -*/ - -/* LCP Dialog Authentication */ - -"ReadiumLCP.dialog.title" = "Enter Password"; -"ReadiumLCP.dialog.cancel" = "Cancel"; -"ReadiumLCP.dialog.continue" = "Continue"; -"ReadiumLCP.dialog.forgotYourPassphrase" = "Forgot Your Password?"; -"ReadiumLCP.dialog.hint" = "**Hint:** %@"; -"ReadiumLCP.dialog.header" = "This publication requires an LCP password to open, which is provided by your library or bookstore. Enter it once, and you're all set to read on this device."; -"ReadiumLCP.dialog.details.title" = "What is LCP?"; -"ReadiumLCP.dialog.details.body" = "This publication is protected by LCP (Licensed Content Protection), a DRM technology that prevents unauthorized copying while keeping your reading experience simple. LCP is an open standard that balances user-friendliness with the needs of publishers."; -"ReadiumLCP.dialog.details.more" = "Learn more…"; -"ReadiumLCP.dialog.passphrase.placeholder" = "Password"; -"ReadiumLCP.dialog.error.incorrectPassphrase" = "Incorrect password."; - -// MARK: - Legacy strings (LCPDialogViewController) - -/* Prompt messages when asking for the passphrase */ -"ReadiumLCP.dialog.prompt.message1" = "This publication is protected by Readium LCP."; -"ReadiumLCP.dialog.prompt.message2" = "In order to open it, we need to know the passphrase required by:\n\n%@\n\nTo help you remember it, the following hint is available:"; -/* Reason to ask for the passphrase when it was not found */ -"ReadiumLCP.dialog.reason.passphraseNotFound" = "Passphrase Required"; -/* Reason to ask for the passphrase when the one entered was incorrect */ -"ReadiumLCP.dialog.reason.invalidPassphrase" = "Incorrect Passphrase"; -/* Forgot passphrase button */ -"ReadiumLCP.dialog.prompt.forgotPassphrase" = "Forgot your passphrase?"; -/* Support button */ -"ReadiumLCP.dialog.prompt.support" = "Need more help?"; -/* Continue button */ -"ReadiumLCP.dialog.prompt.continue" = "Continue"; -/* Passphrase placeholder */ -"ReadiumLCP.dialog.prompt.passphrase" = "Passphrase"; - -/* Button to contact the support when entering the passphrase */ -"ReadiumLCP.dialog.support" = "Support"; -"ReadiumLCP.dialog.support.website" = "Website"; -"ReadiumLCP.dialog.support.phone" = "Phone"; -"ReadiumLCP.dialog.support.mail" = "Mail"; - +// DO NOT EDIT. File generated automatically from the en JSON strings of https://github.com/edrlab/thorium-locales/. + +"readium.lcp.dialog.actions.cancel" = "Cancel"; +"readium.lcp.dialog.actions.continue" = "Continue"; +"readium.lcp.dialog.actions.recoverPassphrase" = "Forgot Your Password?"; +"readium.lcp.dialog.errors.incorrectPassphrase" = "Incorrect password."; +"readium.lcp.dialog.info.body" = "This publication is protected by LCP (Licensed Content Protection), a DRM technology that prevents unauthorized copying while keeping your reading experience simple. LCP is an open standard that balances user-friendliness with the needs of publishers."; +"readium.lcp.dialog.info.more" = "Learn more…"; +"readium.lcp.dialog.info.title" = "What is LCP?"; +"readium.lcp.dialog.message" = "This publication requires an LCP password to open, which is provided by your library or bookstore. Enter it once, and you're all set to read on this device."; +"readium.lcp.dialog.passphrase.hint" = "**Hint:** %1$@"; +"readium.lcp.dialog.passphrase.placeholder" = "Password"; +"readium.lcp.dialog.title" = "Enter Password"; diff --git a/Sources/LCP/Resources/fr.lproj/Localizable.strings b/Sources/LCP/Resources/fr.lproj/Localizable.strings new file mode 100644 index 0000000000..f231575c7b --- /dev/null +++ b/Sources/LCP/Resources/fr.lproj/Localizable.strings @@ -0,0 +1,13 @@ +// DO NOT EDIT. File generated automatically from the fr JSON strings of https://github.com/edrlab/thorium-locales/. + +"readium.lcp.dialog.actions.cancel" = "Annuler"; +"readium.lcp.dialog.actions.continue" = "Continuer"; +"readium.lcp.dialog.actions.recoverPassphrase" = "Mot de passe oublié ?"; +"readium.lcp.dialog.errors.incorrectPassphrase" = "Mot de passe incorrect."; +"readium.lcp.dialog.info.body" = "Cette publication est protégée par LCP (Licensed Content Protection), une technologie DRM qui empêche la copie non autorisée tout en simplifiant votre expérience de lecture. LCP est un standard ouvert qui équilibre la facilité d'utilisation et les besoins des éditeurs."; +"readium.lcp.dialog.info.more" = "En savoir plus…"; +"readium.lcp.dialog.info.title" = "Qu'est-ce que LCP ?"; +"readium.lcp.dialog.message" = "Cette publication est protégée par LCP. Veuillez saisir le mot de passe fourni par votre bibliothèque ou votre libraire pour l'ouvrir."; +"readium.lcp.dialog.passphrase.hint" = "**Indice :** %1$@"; +"readium.lcp.dialog.passphrase.placeholder" = "Mot de passe"; +"readium.lcp.dialog.title" = "Saisir le mot de passe"; diff --git a/Sources/LCP/Resources/it.lproj/Localizable.strings b/Sources/LCP/Resources/it.lproj/Localizable.strings new file mode 100644 index 0000000000..459bf0b777 --- /dev/null +++ b/Sources/LCP/Resources/it.lproj/Localizable.strings @@ -0,0 +1,13 @@ +// DO NOT EDIT. File generated automatically from the it JSON strings of https://github.com/edrlab/thorium-locales/. + +"readium.lcp.dialog.actions.cancel" = "Annulla"; +"readium.lcp.dialog.actions.continue" = "Continua"; +"readium.lcp.dialog.actions.recoverPassphrase" = "Hai dimenticato la password?"; +"readium.lcp.dialog.errors.incorrectPassphrase" = "Password errata."; +"readium.lcp.dialog.info.body" = "Questa pubblicazione è protetta con LCP (Licensed Content Protection), una tecnologia DRM che impedisce la riproduzione non autorizzata mantenendo semplice la tua esperienza di lettura. LCP è uno standard aperto che coniuga la facilità d'uso con le esigenze degli editori."; +"readium.lcp.dialog.info.more" = "Per saperne di più…"; +"readium.lcp.dialog.info.title" = "Che cos'è LCP?"; +"readium.lcp.dialog.message" = "Questa pubblicazione richiede una password LCP per essere aperta, fornita dalla tua biblioteca o dalla tua libreria online. Inseriscila e potrai iniziare a leggere su questa periferica."; +"readium.lcp.dialog.passphrase.hint" = "**Indizio:** %1$@"; +"readium.lcp.dialog.passphrase.placeholder" = "Password"; +"readium.lcp.dialog.title" = "Inserisci la password"; diff --git a/Sources/LCP/Services/CRLService.swift b/Sources/LCP/Services/CRLService.swift index e5b9282c30..043a9413ed 100644 --- a/Sources/LCP/Services/CRLService.swift +++ b/Sources/LCP/Services/CRLService.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,7 +9,7 @@ import ReadiumShared /// Certificate Revocation List final class CRLService { - // Number of days before the CRL cache expires. + /// Number of days before the CRL cache expires. private static let expiration = 7 private static let crlKey = "org.readium.r2-lcp-swift.CRL" diff --git a/Sources/LCP/Services/DeviceService.swift b/Sources/LCP/Services/DeviceService.swift index fdcade3998..f9d10e207a 100644 --- a/Sources/LCP/Services/DeviceService.swift +++ b/Sources/LCP/Services/DeviceService.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -38,7 +38,7 @@ final class DeviceService { self.httpClient = httpClient } - // Device ID and name as query parameters for HTTP requests. + /// Device ID and name as query parameters for HTTP requests. var asQueryParameters: [String: String] { [ "id": id, diff --git a/Sources/LCP/Services/LicensesService.swift b/Sources/LCP/Services/LicensesService.swift index 437f9d78e4..303447acc0 100644 --- a/Sources/LCP/Services/LicensesService.swift +++ b/Sources/LCP/Services/LicensesService.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -8,7 +8,7 @@ import Foundation import ReadiumShared final class LicensesService: Loggable { - // Mapping between an unprotected format to the matching LCP protected format. + /// Mapping between an unprotected format to the matching LCP protected format. private let mediaTypesMapping: [MediaType: MediaType] = [ .readiumAudiobook: .lcpProtectedAudiobook, .pdf: .lcpProtectedPDF, @@ -138,7 +138,7 @@ final class LicensesService: Loggable { _ license: LicenseDocument, in url: FileURL ) async throws { - let _ = try await injectLicenseAndGetFormat(license, in: url, mediaTypeHint: nil) + _ = try await injectLicenseAndGetFormat(license, in: url, mediaTypeHint: nil) } private func injectLicenseAndGetFormat( diff --git a/Sources/LCP/Services/PassphrasesService.swift b/Sources/LCP/Services/PassphrasesService.swift index c36ca3b7ff..6708ec35fe 100644 --- a/Sources/LCP/Services/PassphrasesService.swift +++ b/Sources/LCP/Services/PassphrasesService.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -38,12 +38,7 @@ final class PassphrasesService { return passphrase } - // Look for alternative candidates based on the provider and user ID. - let candidates = try await repository.passphrasesMatching( - userID: license.user.id, - provider: license.provider - ) - var passphrase: LCPPassphraseHash? = findValidPassphrase(in: candidates, for: license) + var passphrase = try await findAlternatePassphrase(for: license) // Fallback on the provided `LCPAuthenticating` implementation. if passphrase == nil, let authentication = authentication { @@ -64,6 +59,22 @@ final class PassphrasesService { return passphrase } + private func findAlternatePassphrase(for license: LicenseDocument) async throws -> LCPPassphraseHash? { + // Look for alternative candidates based on the provider and user ID. + let candidates = try await repository.passphrasesMatching( + userID: license.user.id, + provider: license.provider + ) + if let passphrase = findValidPassphrase(in: candidates, for: license) { + return passphrase + } + + // The legacy SQLite database did not save all the new (passphrase, + // userID, provider) tuples. So we need to fall back on checking all the + // saved passphrases for a match. + return try await findValidPassphrase(in: repository.passphrases(), for: license) + } + private func findValidPassphrase(in hashes: [LCPPassphraseHash], for license: LicenseDocument) -> LCPPassphraseHash? { guard !hashes.isEmpty else { return nil diff --git a/Sources/LCP/Toolkit/Bundle.swift b/Sources/LCP/Toolkit/Bundle.swift index 775f2e014c..cd8019ceda 100644 --- a/Sources/LCP/Toolkit/Bundle.swift +++ b/Sources/LCP/Toolkit/Bundle.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/LCP/Toolkit/DataCompression.swift b/Sources/LCP/Toolkit/DataCompression.swift index 7808a6fa02..e6b93a33ab 100644 --- a/Sources/LCP/Toolkit/DataCompression.swift +++ b/Sources/LCP/Toolkit/DataCompression.swift @@ -1,29 +1,35 @@ -/// -/// DataCompression -/// -/// A libcompression wrapper as an extension for the `Data` type -/// (GZIP, ZLIB, LZFSE, LZMA, LZ4, deflate, RFC-1950, RFC-1951, RFC-1952) -/// -/// Created by Markus Wanke, 2016/12/05 -/// - -/// -/// Apache License, Version 2.0 -/// -/// Copyright 2016, Markus Wanke -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// +// +// Copyright 2026 Readium Foundation. All rights reserved. +// Use of this source code is governed by the BSD-style license +// available in the top-level LICENSE file of the project. +// + +// +// DataCompression +// +// A libcompression wrapper as an extension for the `Data` type +// (GZIP, ZLIB, LZFSE, LZMA, LZ4, deflate, RFC-1950, RFC-1951, RFC-1952) +// +// Created by Markus Wanke, 2016/12/05 +// + +// +// Apache License, Version 2.0 +// +// Copyright 2016, Markus Wanke +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// import Compression import Foundation @@ -210,11 +216,15 @@ public extension Data { } } if has_fname { - while pos < limit, ptr[pos] != 0x0 { pos += 1 } + while pos < limit, ptr[pos] != 0x0 { + pos += 1 + } pos += 1 // skip null byte as well } if has_cmmnt { - while pos < limit, ptr[pos] != 0x0 { pos += 1 } + while pos < limit, ptr[pos] != 0x0 { + pos += 1 + } pos += 1 // skip null byte as well } if has_crc16 { @@ -255,7 +265,7 @@ public struct Crc32: CustomStringConvertible { public init() {} - // C convention function pointer type matching the signature of `libz::crc32` + /// C convention function pointer type matching the signature of `libz::crc32` private typealias ZLibCrc32FuncPtr = @convention(c) ( _ cks: UInt32, _ buf: UnsafePointer, @@ -344,7 +354,7 @@ public struct Adler32: CustomStringConvertible { public init() {} - // C convention function pointer type matching the signature of `libz::adler32` + /// C convention function pointer type matching the signature of `libz::adler32` private typealias ZLibAdler32FuncPtr = @convention(c) ( _ cks: UInt32, _ buf: UnsafePointer, @@ -398,8 +408,7 @@ public struct Adler32: CustomStringConvertible { } private extension Data { - func withUnsafeBytes(_ body: (UnsafePointer) throws -> ResultType) rethrows -> ResultType - { + func withUnsafeBytes(_ body: (UnsafePointer) throws -> ResultType) rethrows -> ResultType { try withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) -> ResultType in try body(rawBufferPointer.bindMemory(to: ContentType.self).baseAddress!) } @@ -419,8 +428,7 @@ private extension Data.CompressionAlgorithm { private typealias Config = (operation: compression_stream_operation, algorithm: compression_algorithm) -private func perform(_ config: Config, source: UnsafePointer, sourceSize: Int, preload: Data = Data()) -> Data? -{ +private func perform(_ config: Config, source: UnsafePointer, sourceSize: Int, preload: Data = Data()) -> Data? { guard config.operation == COMPRESSION_STREAM_ENCODE || sourceSize > 0 else { return nil } let streamBase = UnsafeMutablePointer.allocate(capacity: 1) diff --git a/Sources/LCP/Toolkit/Streamable.swift b/Sources/LCP/Toolkit/ReadResult.swift similarity index 62% rename from Sources/LCP/Toolkit/Streamable.swift rename to Sources/LCP/Toolkit/ReadResult.swift index 2f560f0f77..7107631acb 100644 --- a/Sources/LCP/Toolkit/Streamable.swift +++ b/Sources/LCP/Toolkit/ReadResult.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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,10 +7,10 @@ import Foundation import ReadiumShared -extension Streamable { - /// Reads the whole content as a LCP License Document. - func readAsLCPL() async -> ReadResult { - await read().flatMap { data in +extension ReadResult { + /// Decodes the data as a LCP License Document. + func asLCPL() -> ReadResult { + flatMap { data in do { return try .success(LicenseDocument(data: data)) } catch { diff --git a/Sources/LCP/Toolkit/ReadiumLCPLocalizedString.swift b/Sources/LCP/Toolkit/ReadiumLCPLocalizedString.swift index c67b079002..b8894b94dd 100644 --- a/Sources/LCP/Toolkit/ReadiumLCPLocalizedString.swift +++ b/Sources/LCP/Toolkit/ReadiumLCPLocalizedString.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -13,7 +13,7 @@ func ReadiumLCPLocalizedString(_ key: String, _ values: CVarArg...) -> String { } func ReadiumLCPLocalizedString(_ key: String, _ values: [CVarArg]) -> String { - ReadiumLocalizedString("ReadiumLCP.\(key)", in: Bundle.module, values) + ReadiumLocalizedString("readium.lcp.\(key)", in: Bundle.module, values) } func ReadiumLCPLocalizedStringKey(_ key: String, _ values: CVarArg...) -> LocalizedStringKey { diff --git a/Sources/Navigator/Audiobook/AudioNavigator.swift b/Sources/Navigator/Audiobook/AudioNavigator.swift index 33261e6ba1..a7753ec62a 100644 --- a/Sources/Navigator/Audiobook/AudioNavigator.swift +++ b/Sources/Navigator/Audiobook/AudioNavigator.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -66,7 +66,9 @@ public struct MediaPlaybackInfo { public extension AudioNavigatorDelegate { func navigator(_ navigator: AudioNavigator, playbackDidChange info: MediaPlaybackInfo) {} - func navigator(_ navigator: AudioNavigator, shouldPlayNextResource info: MediaPlaybackInfo) -> Bool { true } + func navigator(_ navigator: AudioNavigator, shouldPlayNextResource info: MediaPlaybackInfo) -> Bool { + true + } func navigator(_ navigator: AudioNavigator, loadedTimeRangesDidChange ranges: [Range]) {} } @@ -112,7 +114,9 @@ public final class AudioNavigator: Navigator, Configurable, AudioSessionUser, Lo private let initialLocation: Locator? private let config: Configuration - public var audioConfiguration: AudioSession.Configuration { config.audioSession } + public var audioConfiguration: AudioSession.Configuration { + config.audioSession + } public init( publication: Publication, diff --git a/Sources/Navigator/Audiobook/Preferences/AudioPreferences.swift b/Sources/Navigator/Audiobook/Preferences/AudioPreferences.swift index ebbaa9f5a1..54e4da684e 100644 --- a/Sources/Navigator/Audiobook/Preferences/AudioPreferences.swift +++ b/Sources/Navigator/Audiobook/Preferences/AudioPreferences.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Audiobook/Preferences/AudioPreferencesEditor.swift b/Sources/Navigator/Audiobook/Preferences/AudioPreferencesEditor.swift index 57e8ace33c..3a92538b14 100644 --- a/Sources/Navigator/Audiobook/Preferences/AudioPreferencesEditor.swift +++ b/Sources/Navigator/Audiobook/Preferences/AudioPreferencesEditor.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Audiobook/Preferences/AudioSettings.swift b/Sources/Navigator/Audiobook/Preferences/AudioSettings.swift index 4e2a658bea..be042b74d7 100644 --- a/Sources/Navigator/Audiobook/Preferences/AudioSettings.swift +++ b/Sources/Navigator/Audiobook/Preferences/AudioSettings.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Audiobook/PublicationMediaLoader.swift b/Sources/Navigator/Audiobook/PublicationMediaLoader.swift index e4b634575d..0c918bb768 100644 --- a/Sources/Navigator/Audiobook/PublicationMediaLoader.swift +++ b/Sources/Navigator/Audiobook/PublicationMediaLoader.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -13,7 +13,7 @@ import ReadiumShared /// /// Useful for local resources or when you need to customize the way HTTP requests are sent. final class PublicationMediaLoader: NSObject, AVAssetResourceLoaderDelegate, Loggable, @unchecked Sendable { - public enum AssetError: Error { + enum AssetError: Error { /// Can't produce an URL to create an AVAsset for the given HREF. case invalidHREF(String) } diff --git a/Sources/Navigator/CBZ/CBZNavigatorViewController.swift b/Sources/Navigator/CBZ/CBZNavigatorViewController.swift index 66c76e5655..c539f87fcf 100644 --- a/Sources/Navigator/CBZ/CBZNavigatorViewController.swift +++ b/Sources/Navigator/CBZ/CBZNavigatorViewController.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -8,9 +8,11 @@ import ReadiumInternal import ReadiumShared import UIKit +@available(*, deprecated, message: "Open a CBZ publication with EPUBNavigatorViewController.") public protocol CBZNavigatorDelegate: VisualNavigatorDelegate {} /// A view controller used to render a CBZ `Publication`. +@available(*, deprecated, message: "Open a CBZ publication with EPUBNavigatorViewController.") open class CBZNavigatorViewController: InputObservableViewController, VisualNavigator, Loggable @@ -254,6 +256,7 @@ open class CBZNavigatorViewController: } } +@available(*, deprecated, message: "Open a CBZ publication with EPUBNavigatorViewController.") extension CBZNavigatorViewController: UIPageViewControllerDataSource { public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let imageVC = viewController as? ImageViewController else { @@ -284,6 +287,7 @@ extension CBZNavigatorViewController: UIPageViewControllerDataSource { } } +@available(*, deprecated, message: "Open a CBZ publication with EPUBNavigatorViewController.") extension CBZNavigatorViewController: UIPageViewControllerDelegate { public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { if completed, let position = currentLocation { diff --git a/Sources/Navigator/CBZ/ImageViewController.swift b/Sources/Navigator/CBZ/ImageViewController.swift index b8712d6e49..3d6644487a 100644 --- a/Sources/Navigator/CBZ/ImageViewController.swift +++ b/Sources/Navigator/CBZ/ImageViewController.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Decorator/DecorableNavigator.swift b/Sources/Navigator/Decorator/DecorableNavigator.swift index 38b09f8d45..2f8749b75d 100644 --- a/Sources/Navigator/Decorator/DecorableNavigator.swift +++ b/Sources/Navigator/Decorator/DecorableNavigator.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -16,7 +16,7 @@ public protocol DecorableNavigator { /// submit the updated list of decorations when there are changes. /// 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. - func apply(decorations: [Decoration], in group: String) + func apply(decorations: [Decoration], in group: DecorationGroup) /// Indicates whether the Navigator supports the given decoration `style`. /// @@ -27,19 +27,27 @@ public protocol DecorableNavigator { /// Registers new callbacks for decoration interactions in the given `group`. /// - /// - Parameter onActivated: Called when the user activates the decoration, e.g. with a click or tap. - func observeDecorationInteractions(inGroup group: String, onActivated: @escaping OnActivatedCallback) + /// - Parameters: + /// - group: The name of the decoration group to observe. + /// - onActivated: Called when the user activates the decoration, e.g. with a click or tap. + func observeDecorationInteractions(inGroup group: DecorationGroup, onActivated: @escaping OnActivatedCallback) /// Called when the user activates a decoration, e.g. with a click or tap. typealias OnActivatedCallback = (_ event: OnDecorationActivatedEvent) -> Void } +/// Identifies a decoration group. +/// +/// Use a descriptive name for each group, typically matching the feature that +/// owns the decorations, e.g. `search`, `annotation`, `tts`. +public typealias DecorationGroup = String + /// Holds the metadata about a decoration activation interaction. public struct OnDecorationActivatedEvent { /// Activated decoration. public let decoration: Decoration /// Name of the group the decoration belongs to. - public let group: String + public let group: DecorationGroup /// Frame of the bounding rect for the decoration, in the coordinate of the navigator view. This is only useful in /// the context of a VisualNavigator. public let rect: CGRect? @@ -52,7 +60,7 @@ public struct OnDecorationActivatedEvent { /// a discrete `locator` in the publication. /// /// For example, decorations can be used to draw highlights, images or buttons. -public struct Decoration: Hashable { +public struct Decoration: Hashable, JSONObjectEncodable { /// An identifier for this decoration. It must be unique in the group the decoration is applied to. public var id: Id @@ -81,7 +89,7 @@ public struct Decoration: Hashable { /// instructions which makes sense for the resource type. public struct Style: Hashable { /// Unique ID for a style. - public struct Id: RawRepresentable, ExpressibleByStringLiteral, Hashable { + public struct Id: RawRepresentable, ExpressibleByStringLiteral, Hashable, JSONValueEncodable { public let rawValue: String public init(rawValue: String) { self.rawValue = rawValue @@ -91,6 +99,10 @@ public struct Decoration: Hashable { self.init(rawValue: value) } + public var jsonValue: JSONValue { + .string(rawValue) + } + // Default Readium style IDs. public static let highlight: Id = "highlight" @@ -123,11 +135,11 @@ public struct Decoration: Hashable { } } - public var json: [String: Any] { - [ + public var jsonObject: [String: JSONValue] { + .init([ "id": id, - "locator": locator.json, - "style": style.id.rawValue, - ] + "locator": locator, + "style": style.id, + ]) } } diff --git a/Sources/Navigator/Decorator/DiffableDecoration.swift b/Sources/Navigator/Decorator/DiffableDecoration.swift index 9a476b5858..75ed4ee8f6 100644 --- a/Sources/Navigator/Decorator/DiffableDecoration.swift +++ b/Sources/Navigator/Decorator/DiffableDecoration.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -10,7 +10,9 @@ import ReadiumShared struct DiffableDecoration: Hashable, Differentiable { let decoration: Decoration - var differenceIdentifier: Decoration.Id { decoration.id } + var differenceIdentifier: Decoration.Id { + decoration.id + } } enum DecorationChange { diff --git a/Sources/Navigator/DirectionalNavigationAdapter.swift b/Sources/Navigator/DirectionalNavigationAdapter.swift index bfa1a574c2..ce32177ef5 100644 --- a/Sources/Navigator/DirectionalNavigationAdapter.swift +++ b/Sources/Navigator/DirectionalNavigationAdapter.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -12,7 +12,7 @@ import Foundation /// /// This takes into account the reading progression of the navigator to turn /// pages in the right direction. -public final class DirectionalNavigationAdapter { +@MainActor public final class DirectionalNavigationAdapter { @available(*, deprecated, renamed: "Edges") public typealias TapEdges = Edges @@ -33,6 +33,8 @@ public final class DirectionalNavigationAdapter { } } + /// Policy controlling how pointer events (touches, mouse clicks) trigger + /// page turns. public struct PointerPolicy { /// The types of pointer that will trigger page turns. public var types: [PointerType] @@ -81,6 +83,7 @@ public final class DirectionalNavigationAdapter { } } + /// Policy controlling how keyboard events trigger page turns. public struct KeyboardPolicy { /// Indicates whether arrow keys should turn pages. public var handleArrowKeys: Bool @@ -97,10 +100,22 @@ public final class DirectionalNavigationAdapter { } } - private let pointerPolicy: PointerPolicy - private let keyboardPolicy: KeyboardPolicy - private let animatedTransition: Bool + /// Policy controlling how pointer events (touches, mouse clicks) trigger + /// page turns. + public var pointerPolicy: PointerPolicy + /// Policy controlling how keyboard events trigger page turns. + public var keyboardPolicy: KeyboardPolicy + + /// Indicates whether page turns should be animated. + public var animatedTransition: Bool + + private let onNavigation: () -> Void + + private var observerTokens: Set = [] + private weak var boundNavigator: (any VisualNavigator)? + + @available(*, deprecated, message: "Use `bind(to:)` instead of notifying the event yourself. See the migration guide.") private weak var navigator: VisualNavigator? /// Initializes a new `DirectionalNavigationAdapter`. @@ -110,59 +125,81 @@ public final class DirectionalNavigationAdapter { /// - keyboardPolicy: Policy on page turns using the keyboard. /// - animatedTransition: Indicates whether the page turns should be /// animated. + /// - onNavigation: Callback called when a navigation is triggered. public init( pointerPolicy: PointerPolicy = PointerPolicy(), keyboardPolicy: KeyboardPolicy = KeyboardPolicy(), - animatedTransition: Bool = false + animatedTransition: Bool = false, + onNavigation: @escaping () -> Void = {} ) { self.pointerPolicy = pointerPolicy self.keyboardPolicy = keyboardPolicy self.animatedTransition = animatedTransition + self.onNavigation = onNavigation + } + + deinit { + guard let nav = boundNavigator else { + return + } + + let tokens = observerTokens + Task { @MainActor [weak nav] in + for token in tokens { + nav?.removeObserver(token) + } + } } /// Binds the adapter to the given visual navigator. /// /// It will automatically observe pointer and key events to turn pages. - @MainActor public func bind(to navigator: VisualNavigator) { - for pointerType in PointerType.allCases { - guard pointerPolicy.types.contains(pointerType) else { - continue - } + /// If the adapter was previously bound to another navigator, it is + /// automatically unbound first. + public func bind(to navigator: VisualNavigator) { + unbind() + boundNavigator = navigator + + observerTokens = [ + navigator.addObserver(.tap { [weak self, weak navigator] event in + guard let self, self.pointerPolicy.types.contains(.touch), let navigator else { + return false + } + return await self.onTap(at: event.location, in: navigator) + }), + navigator.addObserver(.click { [weak self, weak navigator] event in + guard let self, self.pointerPolicy.types.contains(.mouse), let navigator else { + return false + } + return await self.onTap(at: event.location, in: navigator) + }), + navigator.addObserver(.key { [weak self, weak navigator] event in + guard let self, let navigator else { + return false + } + return await self.onKey(event, in: navigator) + }), + ] + } - switch pointerType { - case .touch: - navigator.addObserver(.tap { [self, weak navigator] event in - guard let navigator = navigator else { - return false - } - return await onTap(at: event.location, in: navigator) - }) - case .mouse: - navigator.addObserver(.click { [self, weak navigator] event in - guard let navigator = navigator else { - return false - } - return await onTap(at: event.location, in: navigator) - }) + /// Unbinds the adapter from the previously bounded navigator. + public func unbind() { + if let nav = boundNavigator { + for token in observerTokens { + nav.removeObserver(token) } } - navigator.addObserver(.key { [self, weak navigator] event in - guard let navigator = navigator else { - return false - } - return await onKey(event, in: navigator) - }) + observerTokens = [] + boundNavigator = nil } - @MainActor private func onTap(at point: CGPoint, in navigator: VisualNavigator) async -> Bool { guard !pointerPolicy.ignoreWhileScrolling || !navigator.presentation.scroll else { return false } let bounds = navigator.view.bounds - let options = NavigatorGoOptions(animated: animatedTransition) if pointerPolicy.edges.contains(.horizontal) { let horizontalEdgeSize = pointerPolicy.horizontalEdgeThresholdPercent @@ -172,9 +209,9 @@ public final class DirectionalNavigationAdapter { let rightRange = (bounds.width - horizontalEdgeSize) ... bounds.width if rightRange.contains(point.x) { - return await navigator.goRight(options: options) + return await goRight(in: navigator) } else if leftRange.contains(point.x) { - return await navigator.goLeft(options: options) + return await goLeft(in: navigator) } } @@ -186,9 +223,9 @@ public final class DirectionalNavigationAdapter { let bottomRange = (bounds.height - verticalEdgeSize) ... bounds.height if bottomRange.contains(point.y) { - return await navigator.goForward(options: options) + return await goForward(in: navigator) } else if topRange.contains(point.y) { - return await navigator.goBackward(options: options) + return await goBackward(in: navigator) } } @@ -200,24 +237,44 @@ public final class DirectionalNavigationAdapter { return false } - let options = NavigatorGoOptions(animated: animatedTransition) - switch event.key { case .arrowUp where keyboardPolicy.handleArrowKeys: - return await navigator.goBackward(options: options) + return await goBackward(in: navigator) case .arrowDown where keyboardPolicy.handleArrowKeys: - return await navigator.goForward(options: options) + return await goForward(in: navigator) case .arrowLeft where keyboardPolicy.handleArrowKeys: - return await navigator.goLeft(options: options) + return await goLeft(in: navigator) case .arrowRight where keyboardPolicy.handleArrowKeys: - return await navigator.goRight(options: options) + return await goRight(in: navigator) case .space where keyboardPolicy.handleSpaceKey: - return await navigator.goForward(options: options) + return await goForward(in: navigator) default: return false } } + private func goBackward(in navigator: VisualNavigator) async -> Bool { + await go { await navigator.goBackward(options: $0) } + } + + private func goForward(in navigator: VisualNavigator) async -> Bool { + await go { await navigator.goForward(options: $0) } + } + + private func goLeft(in navigator: VisualNavigator) async -> Bool { + await go { await navigator.goLeft(options: $0) } + } + + private func goRight(in navigator: VisualNavigator) async -> Bool { + await go { await navigator.goRight(options: $0) } + } + + private func go(_ action: (NavigatorGoOptions) async -> Bool) async -> Bool { + onNavigation() + let options = NavigatorGoOptions(animated: animatedTransition) + return await action(options) + } + @available(*, deprecated, message: "Use the new initializer without the navigator parameter and call `bind(to:)`. See the migration guide.") public init( navigator: VisualNavigator, @@ -232,6 +289,7 @@ public final class DirectionalNavigationAdapter { self.navigator = navigator pointerPolicy = PointerPolicy( types: [.touch, .mouse], + edges: tapEdges, ignoreWhileScrolling: !handleTapsWhileScrolling, minimumHorizontalEdgeSize: minimumHorizontalEdgeSize, horizontalEdgeThresholdPercent: horizontalEdgeThresholdPercent, @@ -240,10 +298,10 @@ public final class DirectionalNavigationAdapter { ) keyboardPolicy = KeyboardPolicy() self.animatedTransition = animatedTransition + onNavigation = {} } @available(*, deprecated, message: "Use `bind(to:)` instead of notifying the event yourself. See the migration guide.") - @MainActor @discardableResult public func didTap(at point: CGPoint) async -> Bool { guard let navigator = navigator else { diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadMe.md b/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadMe.md index 518fe6289e..a5588f5585 100644 --- a/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadMe.md +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadMe.md @@ -27,10 +27,6 @@ Disabled user settings: - `hyphens`; - `letter-spacing`. -Added user settings: - -- `font-variant-ligatures` (mapped to `--USER__ligatures` CSS variable). - ## CJK Chinese, Japanese, Korean, and Mongolian can be either written `horizontal-tb` or `vertical-*`. Consequently, there are stylesheets for horizontal and vertical writing modes. @@ -52,6 +48,7 @@ Disabled user settings: - `text-align`; - `hyphens`; +- `ligatures`; - paragraphs’ indent; - `word-spacing`. @@ -88,6 +85,7 @@ Disabled user settings: - `column-count` (number of columns); - `text-align`; - `hyphens`; +- `ligatures`; - paragraphs’ indent; - `word-spacing`. diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadiumCSS-after.css b/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadiumCSS-after.css index 9f837f48a1..2aa47e209e 100644 --- a/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadiumCSS-after.css +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadiumCSS-after.css @@ -1,10 +1,17 @@ -/* - * Readium CSS (v. 2.0.0-beta.18) - * Developers: Jiminy Panoz - * Copyright (c) 2017. Readium Foundation. All rights reserved. +/*! + * Readium CSS v.2.0.0 + * Copyright (c) 2017–2026. 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. -*/ + * Core maintainer: Jiminy Panoz + * Contributors: + * Daniel Weck + * Hadrien Gardeur + * Innovimax + * L. Le Meur + * Mickaël Menu + * k_taka + */ @namespace url("http://www.w3.org/1999/xhtml"); @@ -20,7 +27,7 @@ --RS__pageGutter:0; - --RS__defaultLineLength:40rem; + --RS__defaultLineLength:100%; --RS__colGap:0; @@ -64,11 +71,14 @@ body{ width:100%; max-width:var(--RS__defaultLineLength) !important; - padding:0 var(--RS__pageGutter) !important; margin:0 auto !important; box-sizing:border-box; } +:root:not([style*="readium-scroll-on"]) body{ + padding:0 var(--RS__pageGutter) !important; +} + :root:not([style*="readium-noOverflow-on"]) body{ overflow:hidden; } @@ -133,145 +143,6 @@ body{ padding-right:var(--RS__scrollPaddingRight) !important; } -:root[style*="readium-night-on"]{ - - --RS__selectionTextColor:inherit; - - --RS__selectionBackgroundColor:#b4d8fe; - - --RS__visitedColor:#0099E5; - - --RS__linkColor:#63caff; - - --RS__textColor:#FEFEFE; - - --RS__backgroundColor:#000000; -} - -:root[style*="readium-night-on"] *:not(a){ - color:inherit !important; - background-color:transparent !important; - border-color:currentcolor !important; -} - -:root[style*="readium-night-on"] svg text{ - fill:currentcolor !important; - stroke:none !important; -} - -:root[style*="readium-night-on"] a:link, -:root[style*="readium-night-on"] a:link *{ - color:var(--RS__linkColor) !important; -} - -:root[style*="readium-night-on"] a:visited, -:root[style*="readium-night-on"] a:visited *{ - color:var(--RS__visitedColor) !important; -} - -:root[style*="readium-night-on"] img[class*="gaiji"], -:root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child, -:root[style*="readium-night-on"] *[epub|type~="titlepage"] img:only-child{ - -webkit-filter:invert(100%); - filter:invert(100%); -} - -:root[style*="readium-sepia-on"]{ - - --RS__selectionTextColor:inherit; - - --RS__selectionBackgroundColor:#b4d8fe; - - --RS__visitedColor:#551A8B; - - --RS__linkColor:#0000EE; - - --RS__textColor:#121212; - - --RS__backgroundColor:#faf4e8; -} - -:root[style*="readium-sepia-on"] *:not(a){ - color:inherit !important; - background-color:transparent !important; -} - -:root[style*="readium-sepia-on"] a:link, -:root[style*="readium-sepia-on"] a:link *{ - color:var(--RS__linkColor); -} - -:root[style*="readium-sepia-on"] a:visited, -:root[style*="readium-sepia-on"] a:visited *{ - color:var(--RS__visitedColor); -} - -@media screen and (-ms-high-contrast: active){ - - :root{ - color:windowText !important; - background-color:window !important; - } - - :root :not(#\#):not(#\#):not(#\#), - :root :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#) - :root :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#){ - color:inherit !important; - background-color:inherit !important; - } - - .readiumCSS-mo-active-default{ - color:highlightText !important; - background-color:highlight !important; - } -} - -@media screen and (-ms-high-contrast: white-on-black){ - - :root[style*="readium-night-on"] img[class*="gaiji"], - :root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child, - :root[style*="readium-night-on"] *[epub|type~="titlepage"] img:only-child{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-invert-on"] img{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-darken-on"][style*="readium-invert-on"] img{ - -webkit-filter:brightness(80%); - filter:brightness(80%); - } -} - -@media screen and (inverted-colors){ - - :root[style*="readium-night-on"] img[class*="gaiji"], - :root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child, - :root[style*="readium-night-on"] *[epub|type~="titlepage"] img:only-child{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-invert-on"] img{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-darken-on"][style*="readium-invert-on"] img{ - -webkit-filter:brightness(80%); - filter:brightness(80%); - } -} - -@media screen and (monochrome){ -} - -@media screen and (prefers-reduced-motion){ -} - :root[style*="--USER__backgroundColor"]{ background-color:var(--USER__backgroundColor) !important; } @@ -346,7 +217,15 @@ body{ } :root[style*="--USER__textAlign"] body, -:root[style*="--USER__textAlign"] p:not(blockquote p):not(figcaption p):not(hgroup p), +:root[style*="--USER__textAlign"] p:not( + blockquote p, + figcaption p, + header p, + hgroup p, + :root[style*="readium-experimentalHeaderFiltering-on"] p[class*="title"], + :root[style*="readium-experimentalHeaderFiltering-on"] div:has(+ *) > h1 + p, + :root[style*="readium-experimentalHeaderFiltering-on"] div:has(+ *) > p:has(+ h1) +), :root[style*="--USER__textAlign"] li, :root[style*="--USER__textAlign"] dd{ text-align:var(--USER__textAlign) !important; @@ -383,39 +262,28 @@ body{ font-family:revert !important; } -:root[style*="AccessibleDfA"]{ - font-family:AccessibleDfA, Verdana, Tahoma, "Trebuchet MS", sans-serif !important; -} - -:root[style*="IA Writer Duospace"]{ - font-family:"IA Writer Duospace", Menlo, "DejaVu Sans Mono", "Bitstream Vera Sans Mono", Courier, monospace !important; -} - -:root[style*="AccessibleDfA"],:root[style*="IA Writer Duospace"], :root[style*="readium-a11y-on"]{ font-style:normal !important; font-weight:normal !important; } -:root[style*="AccessibleDfA"] *:not(code):not(var):not(kbd):not(samp),:root[style*="IA Writer Duospace"] *:not(code):not(var):not(kbd):not(samp), -:root[style*="readium-a11y-on"] *:not(code):not(var):not(kbd):not(samp){ +:root[style*="readium-a11y-on"] body *:not(code):not(var):not(kbd):not(samp){ font-family:inherit !important; font-style:inherit !important; font-weight:inherit !important; } -:root[style*="AccessibleDfA"] *,:root[style*="IA Writer Duospace"] *, -:root[style*="readium-a11y-on"] *{ +:root[style*="readium-a11y-on"] body *:not(a){ text-decoration:none !important; +} + +:root[style*="readium-a11y-on"] body *{ font-variant-caps:normal !important; font-variant-numeric:normal !important; font-variant-position:normal !important; } -:root[style*="AccessibleDfA"] sup,:root[style*="IA Writer Duospace"] sup, :root[style*="readium-a11y-on"] sup, -:root[style*="AccessibleDfA"] sub, -:root[style*="IA Writer Duospace"] sub, :root[style*="readium-a11y-on"] sub{ font-size:1rem !important; vertical-align:baseline !important; @@ -425,10 +293,36 @@ body{ zoom:var(--USER__fontSize) !important; } -:root[style*="readium-iOSPatch-on"][style*="--USER__fontSize"] body{ +:root:not([style*="readium-deprecatedFontSize-on"])[style*="readium-iOSPatch-on"][style*="--USER__fontSize"] body{ -webkit-text-size-adjust:var(--USER__fontSize) !important; } +@supports selector(figure:has(> img)){ + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> img), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> video), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> svg), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> canvas), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> iframe), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> audio), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> img:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> video:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> svg:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> canvas:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> iframe:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> audio:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] table{ + zoom:calc(100% / var(--USER__fontSize)) !important; + } + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figcaption, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] caption, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] td, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] th{ + zoom:var(--USER__fontSize) !important; + } +} + @supports not (zoom: 1){ :root[style*="--USER__fontSize"]{ @@ -456,7 +350,15 @@ body{ margin-bottom:var(--USER__paraSpacing) !important; } -:root[style*="--USER__paraIndent"] p{ +:root[style*="--USER__paraIndent"] p:not( + blockquote p, + figcaption p, + header p, + hgroup p, + :root[style*="readium-experimentalHeaderFiltering-on"] p[class*="title"], + :root[style*="readium-experimentalHeaderFiltering-on"] div:has(+ *) > h1 + p, + :root[style*="readium-experimentalHeaderFiltering-on"] div:has(+ *) > p:has(+ h1) +){ text-indent:var(--USER__paraIndent) !important; } @@ -494,6 +396,14 @@ body{ font-variant:none; } +:root[style*="--USER__ligatures"]{ + font-variant-ligatures:var(--USER__ligatures) !important; +} + +:root[style*="--USER__ligatures"] *{ + font-variant-ligatures:inherit !important; +} + :root[style*="--USER__fontWeight"] body{ font-weight:var(--USER__fontWeight) !important; } diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadiumCSS-before.css b/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadiumCSS-before.css index 99ea6292fe..85c9f00a23 100644 --- a/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadiumCSS-before.css +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadiumCSS-before.css @@ -1,10 +1,17 @@ -/* - * Readium CSS (v. 2.0.0-beta.18) - * Developers: Jiminy Panoz - * Copyright (c) 2017. Readium Foundation. All rights reserved. +/*! + * Readium CSS v.2.0.0 + * Copyright (c) 2017–2026. 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. -*/ + * Core maintainer: Jiminy Panoz + * Contributors: + * Daniel Weck + * Hadrien Gardeur + * Innovimax + * L. Le Meur + * Mickaël Menu + * k_taka + */ @namespace url("http://www.w3.org/1999/xhtml"); @@ -218,34 +225,6 @@ math{ --RS__lineHeightCompensation:1.167; } -@font-face{ - font-family:AccessibleDfA; - font-style:normal; - font-weight:normal; - src:local("AccessibleDfA"), url("fonts/AccessibleDfA-Regular.woff2") format("woff2"), url("fonts/AccessibleDfA-Regular.woff") format("woff"); -} - -@font-face{ - font-family:AccessibleDfA; - font-style:normal; - font-weight:bold; - src:local("AccessibleDfA"), url("fonts/AccessibleDfA-Bold.woff2") format("woff2"); -} - -@font-face{ - font-family:AccessibleDfA; - font-style:italic; - font-weight:normal; - src:local("AccessibleDfA"), url("fonts/AccessibleDfA-Italic.woff2") format("woff2"); -} - -@font-face{ - font-family:"IA Writer Duospace"; - font-style:normal; - font-weight:normal; - src:local("iAWriterDuospace-Regular"), url("fonts/iAWriterDuospace-Regular.ttf") format("truetype"); -} - body{ widows:2; orphans:2; @@ -421,6 +400,15 @@ img, svg|svg, video{ break-inside:avoid; } +@supports (zoom: 1) and (not ((-webkit-column-axis: horizontal) and (-webkit-column-progression: normal))){ + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] img, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] svg|svg, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] video{ + zoom:calc(100% / var(--USER__fontSize)); + } +} + audio{ max-width:100%; -webkit-column-break-inside:avoid; diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadiumCSS-default.css b/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadiumCSS-default.css index a95fcd4bc7..1d2df4acc0 100644 --- a/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadiumCSS-default.css +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/ReadiumCSS-default.css @@ -1,10 +1,17 @@ -/* - * Readium CSS (v. 2.0.0-beta.18) - * Developers: Jiminy Panoz - * Copyright (c) 2017. Readium Foundation. All rights reserved. +/*! + * Readium CSS v.2.0.0 + * Copyright (c) 2017–2026. 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. -*/ + * Core maintainer: Jiminy Panoz + * Contributors: + * Daniel Weck + * Hadrien Gardeur + * Innovimax + * L. Le Meur + * Mickaël Menu + * k_taka + */ @namespace url("http://www.w3.org/1999/xhtml"); diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-horizontal/ReadiumCSS-after.css b/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-horizontal/ReadiumCSS-after.css index f4bcc17f67..e48936f8cf 100644 --- a/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-horizontal/ReadiumCSS-after.css +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-horizontal/ReadiumCSS-after.css @@ -1,10 +1,17 @@ -/* - * Readium CSS (v. 2.0.0-beta.18) - * Developers: Jiminy Panoz - * Copyright (c) 2017. Readium Foundation. All rights reserved. +/*! + * Readium CSS v.2.0.0 + * Copyright (c) 2017–2026. 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. -*/ + * Core maintainer: Jiminy Panoz + * Contributors: + * Daniel Weck + * Hadrien Gardeur + * Innovimax + * L. Le Meur + * Mickaël Menu + * k_taka + */ @namespace url("http://www.w3.org/1999/xhtml"); @@ -20,7 +27,7 @@ --RS__pageGutter:0; - --RS__defaultLineLength:40rem; + --RS__defaultLineLength:100%; --RS__colGap:0; @@ -64,11 +71,14 @@ body{ width:100%; max-width:var(--RS__defaultLineLength) !important; - padding:0 var(--RS__pageGutter) !important; margin:0 auto !important; box-sizing:border-box; } +:root:not([style*="readium-scroll-on"]) body{ + padding:0 var(--RS__pageGutter) !important; +} + :root:not([style*="readium-noOverflow-on"]) body{ overflow:hidden; } @@ -133,145 +143,6 @@ body{ padding-right:var(--RS__scrollPaddingRight) !important; } -:root[style*="readium-night-on"]{ - - --RS__selectionTextColor:inherit; - - --RS__selectionBackgroundColor:#b4d8fe; - - --RS__visitedColor:#0099E5; - - --RS__linkColor:#63caff; - - --RS__textColor:#FEFEFE; - - --RS__backgroundColor:#000000; -} - -:root[style*="readium-night-on"] *:not(a){ - color:inherit !important; - background-color:transparent !important; - border-color:currentcolor !important; -} - -:root[style*="readium-night-on"] svg text{ - fill:currentcolor !important; - stroke:none !important; -} - -:root[style*="readium-night-on"] a:link, -:root[style*="readium-night-on"] a:link *{ - color:var(--RS__linkColor) !important; -} - -:root[style*="readium-night-on"] a:visited, -:root[style*="readium-night-on"] a:visited *{ - color:var(--RS__visitedColor) !important; -} - -:root[style*="readium-night-on"] img[class*="gaiji"], -:root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child, -:root[style*="readium-night-on"] *[epub|type~="titlepage"] img:only-child{ - -webkit-filter:invert(100%); - filter:invert(100%); -} - -:root[style*="readium-sepia-on"]{ - - --RS__selectionTextColor:inherit; - - --RS__selectionBackgroundColor:#b4d8fe; - - --RS__visitedColor:#551A8B; - - --RS__linkColor:#0000EE; - - --RS__textColor:#121212; - - --RS__backgroundColor:#faf4e8; -} - -:root[style*="readium-sepia-on"] *:not(a){ - color:inherit !important; - background-color:transparent !important; -} - -:root[style*="readium-sepia-on"] a:link, -:root[style*="readium-sepia-on"] a:link *{ - color:var(--RS__linkColor); -} - -:root[style*="readium-sepia-on"] a:visited, -:root[style*="readium-sepia-on"] a:visited *{ - color:var(--RS__visitedColor); -} - -@media screen and (-ms-high-contrast: active){ - - :root{ - color:windowText !important; - background-color:window !important; - } - - :root :not(#\#):not(#\#):not(#\#), - :root :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#) - :root :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#){ - color:inherit !important; - background-color:inherit !important; - } - - .readiumCSS-mo-active-default{ - color:highlightText !important; - background-color:highlight !important; - } -} - -@media screen and (-ms-high-contrast: white-on-black){ - - :root[style*="readium-night-on"] img[class*="gaiji"], - :root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child, - :root[style*="readium-night-on"] *[epub|type~="titlepage"] img:only-child{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-invert-on"] img{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-darken-on"][style*="readium-invert-on"] img{ - -webkit-filter:brightness(80%); - filter:brightness(80%); - } -} - -@media screen and (inverted-colors){ - - :root[style*="readium-night-on"] img[class*="gaiji"], - :root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child, - :root[style*="readium-night-on"] *[epub|type~="titlepage"] img:only-child{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-invert-on"] img{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-darken-on"][style*="readium-invert-on"] img{ - -webkit-filter:brightness(80%); - filter:brightness(80%); - } -} - -@media screen and (monochrome){ -} - -@media screen and (prefers-reduced-motion){ -} - :root[style*="--USER__backgroundColor"]{ background-color:var(--USER__backgroundColor) !important; } @@ -353,10 +224,36 @@ body{ zoom:var(--USER__fontSize) !important; } -:root[style*="readium-iOSPatch-on"][style*="--USER__fontSize"] body{ +:root:not([style*="readium-deprecatedFontSize-on"])[style*="readium-iOSPatch-on"][style*="--USER__fontSize"] body{ -webkit-text-size-adjust:var(--USER__fontSize) !important; } +@supports selector(figure:has(> img)){ + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> img), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> video), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> svg), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> canvas), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> iframe), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> audio), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> img:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> video:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> svg:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> canvas:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> iframe:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> audio:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] table{ + zoom:calc(100% / var(--USER__fontSize)) !important; + } + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figcaption, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] caption, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] td, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] th{ + zoom:var(--USER__fontSize) !important; + } +} + @supports not (zoom: 1){ :root[style*="--USER__fontSize"]{ diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-horizontal/ReadiumCSS-before.css b/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-horizontal/ReadiumCSS-before.css index 6a99eca103..85c9f00a23 100644 --- a/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-horizontal/ReadiumCSS-before.css +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-horizontal/ReadiumCSS-before.css @@ -1,10 +1,17 @@ -/* - * Readium CSS (v. 2.0.0-beta.18) - * Developers: Jiminy Panoz - * Copyright (c) 2017. Readium Foundation. All rights reserved. +/*! + * Readium CSS v.2.0.0 + * Copyright (c) 2017–2026. 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. -*/ + * Core maintainer: Jiminy Panoz + * Contributors: + * Daniel Weck + * Hadrien Gardeur + * Innovimax + * L. Le Meur + * Mickaël Menu + * k_taka + */ @namespace url("http://www.w3.org/1999/xhtml"); @@ -393,6 +400,15 @@ img, svg|svg, video{ break-inside:avoid; } +@supports (zoom: 1) and (not ((-webkit-column-axis: horizontal) and (-webkit-column-progression: normal))){ + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] img, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] svg|svg, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] video{ + zoom:calc(100% / var(--USER__fontSize)); + } +} + audio{ max-width:100%; -webkit-column-break-inside:avoid; diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-horizontal/ReadiumCSS-default.css b/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-horizontal/ReadiumCSS-default.css index 83c9fce6be..f85fd8b9df 100644 --- a/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-horizontal/ReadiumCSS-default.css +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-horizontal/ReadiumCSS-default.css @@ -1,10 +1,17 @@ -/* - * Readium CSS (v. 2.0.0-beta.18) - * Developers: Jiminy Panoz - * Copyright (c) 2017. Readium Foundation. All rights reserved. +/*! + * Readium CSS v.2.0.0 + * Copyright (c) 2017–2026. 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. -*/ + * Core maintainer: Jiminy Panoz + * Contributors: + * Daniel Weck + * Hadrien Gardeur + * Innovimax + * L. Le Meur + * Mickaël Menu + * k_taka + */ @namespace url("http://www.w3.org/1999/xhtml"); diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-vertical/ReadiumCSS-after.css b/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-vertical/ReadiumCSS-after.css index 601def5e8d..cdd18565d9 100644 --- a/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-vertical/ReadiumCSS-after.css +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-vertical/ReadiumCSS-after.css @@ -1,10 +1,17 @@ -/* - * Readium CSS (v. 2.0.0-beta.18) - * Developers: Jiminy Panoz - * Copyright (c) 2017. Readium Foundation. All rights reserved. +/*! + * Readium CSS v.2.0.0 + * Copyright (c) 2017–2026. 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. -*/ + * Core maintainer: Jiminy Panoz + * Contributors: + * Daniel Weck + * Hadrien Gardeur + * Innovimax + * L. Le Meur + * Mickaël Menu + * k_taka + */ @namespace url("http://www.w3.org/1999/xhtml"); @@ -20,7 +27,7 @@ --RS__pageGutter:0; - --RS__defaultLineLength:40rem; + --RS__defaultLineLength:100%; --RS__colGap:0; @@ -75,11 +82,14 @@ body{ width:100%; max-height:var(--RS__defaultLineLength) !important; - padding:var(--RS__pageGutter) 0 !important; margin:auto 0 !important; box-sizing:border-box; } +:root:not([style*="readium-scroll-on"]) body{ + padding:var(--RS__pageGutter) 0 !important; +} + :root:not([style*="readium-noOverflow-on"]) body{ overflow:hidden; } @@ -140,145 +150,6 @@ body{ padding-right:var(--RS__scrollPaddingRight) !important; } -:root[style*="readium-night-on"]{ - - --RS__selectionTextColor:inherit; - - --RS__selectionBackgroundColor:#b4d8fe; - - --RS__visitedColor:#0099E5; - - --RS__linkColor:#63caff; - - --RS__textColor:#FEFEFE; - - --RS__backgroundColor:#000000; -} - -:root[style*="readium-night-on"] *:not(a){ - color:inherit !important; - background-color:transparent !important; - border-color:currentcolor !important; -} - -:root[style*="readium-night-on"] svg text{ - fill:currentcolor !important; - stroke:none !important; -} - -:root[style*="readium-night-on"] a:link, -:root[style*="readium-night-on"] a:link *{ - color:var(--RS__linkColor) !important; -} - -:root[style*="readium-night-on"] a:visited, -:root[style*="readium-night-on"] a:visited *{ - color:var(--RS__visitedColor) !important; -} - -:root[style*="readium-night-on"] img[class*="gaiji"], -:root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child, -:root[style*="readium-night-on"] *[epub|type~="titlepage"] img:only-child{ - -webkit-filter:invert(100%); - filter:invert(100%); -} - -:root[style*="readium-sepia-on"]{ - - --RS__selectionTextColor:inherit; - - --RS__selectionBackgroundColor:#b4d8fe; - - --RS__visitedColor:#551A8B; - - --RS__linkColor:#0000EE; - - --RS__textColor:#121212; - - --RS__backgroundColor:#faf4e8; -} - -:root[style*="readium-sepia-on"] *:not(a){ - color:inherit !important; - background-color:transparent !important; -} - -:root[style*="readium-sepia-on"] a:link, -:root[style*="readium-sepia-on"] a:link *{ - color:var(--RS__linkColor); -} - -:root[style*="readium-sepia-on"] a:visited, -:root[style*="readium-sepia-on"] a:visited *{ - color:var(--RS__visitedColor); -} - -@media screen and (-ms-high-contrast: active){ - - :root{ - color:windowText !important; - background-color:window !important; - } - - :root :not(#\#):not(#\#):not(#\#), - :root :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#) - :root :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#){ - color:inherit !important; - background-color:inherit !important; - } - - .readiumCSS-mo-active-default{ - color:highlightText !important; - background-color:highlight !important; - } -} - -@media screen and (-ms-high-contrast: white-on-black){ - - :root[style*="readium-night-on"] img[class*="gaiji"], - :root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child, - :root[style*="readium-night-on"] *[epub|type~="titlepage"] img:only-child{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-invert-on"] img{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-darken-on"][style*="readium-invert-on"] img{ - -webkit-filter:brightness(80%); - filter:brightness(80%); - } -} - -@media screen and (inverted-colors){ - - :root[style*="readium-night-on"] img[class*="gaiji"], - :root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child, - :root[style*="readium-night-on"] *[epub|type~="titlepage"] img:only-child{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-invert-on"] img{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-darken-on"][style*="readium-invert-on"] img{ - -webkit-filter:brightness(80%); - filter:brightness(80%); - } -} - -@media screen and (monochrome){ -} - -@media screen and (prefers-reduced-motion){ -} - :root[style*="--USER__backgroundColor"]{ background-color:var(--USER__backgroundColor) !important; } @@ -338,10 +209,36 @@ body{ zoom:var(--USER__fontSize) !important; } -:root[style*="readium-iOSPatch-on"][style*="--USER__fontSize"] body{ +:root:not([style*="readium-deprecatedFontSize-on"])[style*="readium-iOSPatch-on"][style*="--USER__fontSize"] body{ -webkit-text-size-adjust:var(--USER__fontSize) !important; } +@supports selector(figure:has(> img)){ + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> img), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> video), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> svg), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> canvas), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> iframe), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> audio), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> img:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> video:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> svg:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> canvas:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> iframe:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> audio:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] table{ + zoom:calc(100% / var(--USER__fontSize)) !important; + } + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figcaption, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] caption, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] td, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] th{ + zoom:var(--USER__fontSize) !important; + } +} + @supports not (zoom: 1){ :root[style*="--USER__fontSize"]{ diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-vertical/ReadiumCSS-before.css b/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-vertical/ReadiumCSS-before.css index 004a75a63f..2ed2433215 100644 --- a/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-vertical/ReadiumCSS-before.css +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-vertical/ReadiumCSS-before.css @@ -1,10 +1,17 @@ -/* - * Readium CSS (v. 2.0.0-beta.18) - * Developers: Jiminy Panoz - * Copyright (c) 2017. Readium Foundation. All rights reserved. +/*! + * Readium CSS v.2.0.0 + * Copyright (c) 2017–2026. 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. -*/ + * Core maintainer: Jiminy Panoz + * Contributors: + * Daniel Weck + * Hadrien Gardeur + * Innovimax + * L. Le Meur + * Mickaël Menu + * k_taka + */ @namespace url("http://www.w3.org/1999/xhtml"); @@ -393,6 +400,16 @@ img, svg|svg, video{ break-inside:avoid; } +@supports (zoom: 1) and (not ((-webkit-column-axis: horizontal) and (-webkit-column-progression: normal))){ + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] img, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] svg|svg, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] video, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div{ + zoom:calc(100% / var(--USER__fontSize)); + } +} + audio{ max-width:100%; -webkit-column-break-inside:avoid; @@ -402,5 +419,5 @@ audio{ table{ max-height:var(--RS__maxMediaWidth); - box-sizing:var(--RS__boxSizingTable) + box-sizing:var(--RS__boxSizingTable); } \ No newline at end of file diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-vertical/ReadiumCSS-default.css b/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-vertical/ReadiumCSS-default.css index 065c8c1b42..2d26579faf 100644 --- a/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-vertical/ReadiumCSS-default.css +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/cjk-vertical/ReadiumCSS-default.css @@ -1,10 +1,17 @@ -/* - * Readium CSS (v. 2.0.0-beta.18) - * Developers: Jiminy Panoz - * Copyright (c) 2017. Readium Foundation. All rights reserved. +/*! + * Readium CSS v.2.0.0 + * Copyright (c) 2017–2026. 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. -*/ + * Core maintainer: Jiminy Panoz + * Contributors: + * Daniel Weck + * Hadrien Gardeur + * Innovimax + * L. Le Meur + * Mickaël Menu + * k_taka + */ @namespace url("http://www.w3.org/1999/xhtml"); diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/rtl/ReadiumCSS-after.css b/Sources/Navigator/EPUB/Assets/Static/readium-css/rtl/ReadiumCSS-after.css index 1a2c8fa2ce..c5a1bef48c 100644 --- a/Sources/Navigator/EPUB/Assets/Static/readium-css/rtl/ReadiumCSS-after.css +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/rtl/ReadiumCSS-after.css @@ -1,10 +1,17 @@ -/* - * Readium CSS (v. 2.0.0-beta.18) - * Developers: Jiminy Panoz - * Copyright (c) 2017. Readium Foundation. All rights reserved. +/*! + * Readium CSS v.2.0.0 + * Copyright (c) 2017–2026. 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. -*/ + * Core maintainer: Jiminy Panoz + * Contributors: + * Daniel Weck + * Hadrien Gardeur + * Innovimax + * L. Le Meur + * Mickaël Menu + * k_taka + */ @namespace url("http://www.w3.org/1999/xhtml"); @@ -20,7 +27,7 @@ --RS__pageGutter:0; - --RS__defaultLineLength:40rem; + --RS__defaultLineLength:100%; --RS__colGap:0; @@ -64,11 +71,14 @@ body{ width:100%; max-width:var(--RS__defaultLineLength) !important; - padding:0 var(--RS__pageGutter) !important; margin:0 auto !important; box-sizing:border-box; } +:root:not([style*="readium-scroll-on"]) body{ + padding:0 var(--RS__pageGutter) !important; +} + :root:not([style*="readium-noOverflow-on"]) body{ overflow:hidden; } @@ -133,145 +143,6 @@ body{ padding-right:var(--RS__scrollPaddingRight) !important; } -:root[style*="readium-night-on"]{ - - --RS__selectionTextColor:inherit; - - --RS__selectionBackgroundColor:#b4d8fe; - - --RS__visitedColor:#0099E5; - - --RS__linkColor:#63caff; - - --RS__textColor:#FEFEFE; - - --RS__backgroundColor:#000000; -} - -:root[style*="readium-night-on"] *:not(a){ - color:inherit !important; - background-color:transparent !important; - border-color:currentcolor !important; -} - -:root[style*="readium-night-on"] svg text{ - fill:currentcolor !important; - stroke:none !important; -} - -:root[style*="readium-night-on"] a:link, -:root[style*="readium-night-on"] a:link *{ - color:var(--RS__linkColor) !important; -} - -:root[style*="readium-night-on"] a:visited, -:root[style*="readium-night-on"] a:visited *{ - color:var(--RS__visitedColor) !important; -} - -:root[style*="readium-night-on"] img[class*="gaiji"], -:root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child, -:root[style*="readium-night-on"] *[epub|type~="titlepage"] img:only-child{ - -webkit-filter:invert(100%); - filter:invert(100%); -} - -:root[style*="readium-sepia-on"]{ - - --RS__selectionTextColor:inherit; - - --RS__selectionBackgroundColor:#b4d8fe; - - --RS__visitedColor:#551A8B; - - --RS__linkColor:#0000EE; - - --RS__textColor:#121212; - - --RS__backgroundColor:#faf4e8; -} - -:root[style*="readium-sepia-on"] *:not(a){ - color:inherit !important; - background-color:transparent !important; -} - -:root[style*="readium-sepia-on"] a:link, -:root[style*="readium-sepia-on"] a:link *{ - color:var(--RS__linkColor); -} - -:root[style*="readium-sepia-on"] a:visited, -:root[style*="readium-sepia-on"] a:visited *{ - color:var(--RS__visitedColor); -} - -@media screen and (-ms-high-contrast: active){ - - :root{ - color:windowText !important; - background-color:window !important; - } - - :root :not(#\#):not(#\#):not(#\#), - :root :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#) - :root :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#) :not(#\#):not(#\#):not(#\#){ - color:inherit !important; - background-color:inherit !important; - } - - .readiumCSS-mo-active-default{ - color:highlightText !important; - background-color:highlight !important; - } -} - -@media screen and (-ms-high-contrast: white-on-black){ - - :root[style*="readium-night-on"] img[class*="gaiji"], - :root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child, - :root[style*="readium-night-on"] *[epub|type~="titlepage"] img:only-child{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-invert-on"] img{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-darken-on"][style*="readium-invert-on"] img{ - -webkit-filter:brightness(80%); - filter:brightness(80%); - } -} - -@media screen and (inverted-colors){ - - :root[style*="readium-night-on"] img[class*="gaiji"], - :root[style*="readium-night-on"] *[epub\:type~="titlepage"] img:only-child, - :root[style*="readium-night-on"] *[epub|type~="titlepage"] img:only-child{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-invert-on"] img{ - -webkit-filter:none !important; - filter:none !important; - } - - :root[style*="readium-night-on"][style*="readium-darken-on"][style*="readium-invert-on"] img{ - -webkit-filter:brightness(80%); - filter:brightness(80%); - } -} - -@media screen and (monochrome){ -} - -@media screen and (prefers-reduced-motion){ -} - :root[style*="--USER__backgroundColor"]{ background-color:var(--USER__backgroundColor) !important; } @@ -346,7 +217,15 @@ body{ } :root[style*="--USER__textAlign"] body, -:root[style*="--USER__textAlign"] p:not(blockquote p):not(figcaption p):not(hgroup p), +:root[style*="--USER__textAlign"] p:not( + blockquote p, + figcaption p, + header p, + hgroup p, + :root[style*="readium-experimentalHeaderFiltering-on"] p[class*="title"], + :root[style*="readium-experimentalHeaderFiltering-on"] div:has(+ *) > h1 + p, + :root[style*="readium-experimentalHeaderFiltering-on"] div:has(+ *) > p:has(+ h1) +), :root[style*="--USER__textAlign"] li, :root[style*="--USER__textAlign"] dd{ text-align:var(--USER__textAlign) !important; @@ -367,10 +246,36 @@ body{ zoom:var(--USER__fontSize) !important; } -:root[style*="readium-iOSPatch-on"][style*="--USER__fontSize"] body{ +:root:not([style*="readium-deprecatedFontSize-on"])[style*="readium-iOSPatch-on"][style*="--USER__fontSize"] body{ -webkit-text-size-adjust:var(--USER__fontSize) !important; } +@supports selector(figure:has(> img)){ + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> img), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> video), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> svg), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> canvas), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> iframe), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figure:has(> audio), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> img:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> video:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> svg:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> canvas:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> iframe:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] div:has(> audio:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] table{ + zoom:calc(100% / var(--USER__fontSize)) !important; + } + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] figcaption, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] caption, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] td, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] th{ + zoom:var(--USER__fontSize) !important; + } +} + @supports not (zoom: 1){ :root[style*="--USER__fontSize"]{ @@ -398,7 +303,15 @@ body{ margin-bottom:var(--USER__paraSpacing) !important; } -:root[style*="--USER__paraIndent"] p{ +:root[style*="--USER__paraIndent"] p:not( + blockquote p, + figcaption p, + header p, + hgroup p, + :root[style*="readium-experimentalHeaderFiltering-on"] p[class*="title"], + :root[style*="readium-experimentalHeaderFiltering-on"] div:has(+ *) > h1 + p, + :root[style*="readium-experimentalHeaderFiltering-on"] div:has(+ *) > p:has(+ h1) +){ text-indent:var(--USER__paraIndent) !important; } diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/rtl/ReadiumCSS-before.css b/Sources/Navigator/EPUB/Assets/Static/readium-css/rtl/ReadiumCSS-before.css index 6a99eca103..85c9f00a23 100644 --- a/Sources/Navigator/EPUB/Assets/Static/readium-css/rtl/ReadiumCSS-before.css +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/rtl/ReadiumCSS-before.css @@ -1,10 +1,17 @@ -/* - * Readium CSS (v. 2.0.0-beta.18) - * Developers: Jiminy Panoz - * Copyright (c) 2017. Readium Foundation. All rights reserved. +/*! + * Readium CSS v.2.0.0 + * Copyright (c) 2017–2026. 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. -*/ + * Core maintainer: Jiminy Panoz + * Contributors: + * Daniel Weck + * Hadrien Gardeur + * Innovimax + * L. Le Meur + * Mickaël Menu + * k_taka + */ @namespace url("http://www.w3.org/1999/xhtml"); @@ -393,6 +400,15 @@ img, svg|svg, video{ break-inside:avoid; } +@supports (zoom: 1) and (not ((-webkit-column-axis: horizontal) and (-webkit-column-progression: normal))){ + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] img, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] svg|svg, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-deprecatedFontSize-on"]):not([style*="readium-iOSPatch-on"])[style*="--USER__fontSize"] video{ + zoom:calc(100% / var(--USER__fontSize)); + } +} + audio{ max-width:100%; -webkit-column-break-inside:avoid; diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/rtl/ReadiumCSS-default.css b/Sources/Navigator/EPUB/Assets/Static/readium-css/rtl/ReadiumCSS-default.css index f2702105b7..8a0a18760e 100644 --- a/Sources/Navigator/EPUB/Assets/Static/readium-css/rtl/ReadiumCSS-default.css +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/rtl/ReadiumCSS-default.css @@ -1,10 +1,17 @@ -/* - * Readium CSS (v. 2.0.0-beta.18) - * Developers: Jiminy Panoz - * Copyright (c) 2017. Readium Foundation. All rights reserved. +/*! + * Readium CSS v.2.0.0 + * Copyright (c) 2017–2026. 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. -*/ + * Core maintainer: Jiminy Panoz + * Contributors: + * Daniel Weck + * Hadrien Gardeur + * Innovimax + * L. Le Meur + * Mickaël Menu + * k_taka + */ @namespace url("http://www.w3.org/1999/xhtml"); diff --git a/Sources/Navigator/EPUB/Assets/Static/readium-css/webPub/ReadiumCSS-webPub.css b/Sources/Navigator/EPUB/Assets/Static/readium-css/webPub/ReadiumCSS-webPub.css new file mode 100644 index 0000000000..5b38e8b580 --- /dev/null +++ b/Sources/Navigator/EPUB/Assets/Static/readium-css/webPub/ReadiumCSS-webPub.css @@ -0,0 +1,275 @@ +/*! + * Readium CSS v.2.0.0 + * Copyright (c) 2017–2026. 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. + * Core maintainer: Jiminy Panoz + * Contributors: + * Daniel Weck + * Hadrien Gardeur + * Innovimax + * L. Le Meur + * Mickaël Menu + * k_taka + */ + +:root[style*="--USER__textAlign"]{ + text-align:var(--USER__textAlign); +} + +:root[style*="--USER__textAlign"] body, +:root[style*="--USER__textAlign"] p:not( + blockquote p, + figcaption p, + header p, + hgroup p, + :root[style*="readium-experimentalHeaderFiltering-on"] p[class*="title"], + :root[style*="readium-experimentalHeaderFiltering-on"] div:has(+ *) > h1 + p, + :root[style*="readium-experimentalHeaderFiltering-on"] div:has(+ *) > p:has(+ h1) +), +:root[style*="--USER__textAlign"] li, +:root[style*="--USER__textAlign"] dd{ + text-align:var(--USER__textAlign) !important; + -moz-text-align-last:auto !important; + -epub-text-align-last:auto !important; + text-align-last:auto !important; +} + +:root[style*="--USER__bodyHyphens"]{ + -webkit-hyphens:var(--USER__bodyHyphens) !important; + -moz-hyphens:var(--USER__bodyHyphens) !important; + -ms-hyphens:var(--USER__bodyHyphens) !important; + -epub-hyphens:var(--USER__bodyHyphens) !important; + hyphens:var(--USER__bodyHyphens) !important; +} + +:root[style*="--USER__bodyHyphens"] body, +:root[style*="--USER__bodyHyphens"] p, +:root[style*="--USER__bodyHyphens"] li, +:root[style*="--USER__bodyHyphens"] div, +:root[style*="--USER__bodyHyphens"] dd{ + -webkit-hyphens:inherit; + -moz-hyphens:inherit; + -ms-hyphens:inherit; + -epub-hyphens:inherit; + hyphens:inherit; +} + +:root[style*="--USER__fontFamily"]{ + font-family:var(--USER__fontFamily) !important; +} + +:root[style*="--USER__fontFamily"] *{ + font-family:revert !important; +} + +:root[style*="readium-a11y-on"]{ + font-style:normal !important; + font-weight:normal !important; +} + +:root[style*="readium-a11y-on"] body *:not(code):not(var):not(kbd):not(samp){ + font-family:inherit !important; + font-style:inherit !important; + font-weight:inherit !important; +} + +:root[style*="readium-a11y-on"] body *:not(a){ + text-decoration:none !important; +} + +:root[style*="readium-a11y-on"] body *{ + font-variant-caps:normal !important; + font-variant-numeric:normal !important; + font-variant-position:normal !important; +} + +:root[style*="readium-a11y-on"] sup, +:root[style*="readium-a11y-on"] sub{ + font-size:1rem !important; + vertical-align:baseline !important; +} + +:root:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] body{ + zoom:var(--USER__zoom) !important; +} + +:root[style*="readium-iOSPatch-on"][style*="--USER__zoom"] body{ + -webkit-text-size-adjust:var(--USER__zoom) !important; +} + +@supports selector(figure:has(> img)){ + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] figure:has(> img), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] figure:has(> video), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] figure:has(> svg), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] figure:has(> canvas), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] figure:has(> iframe), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] figure:has(> audio), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] div:has(> img:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] div:has(> video:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] div:has(> svg:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] div:has(> canvas:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] div:has(> iframe:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] div:has(> audio:only-child), + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] table{ + zoom:calc(100% / var(--USER__zoom)) !important; + } + + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] figcaption, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] caption, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] td, + :root[style*="readium-experimentalZoom-on"]:not([style*="readium-iOSPatch-on"])[style*="--USER__zoom"] th{ + zoom:var(--USER__zoom) !important; + } +} + +:root[style*="--USER__lineHeight"]{ + line-height:var(--USER__lineHeight) !important; +} + +:root[style*="--USER__lineHeight"] body, +:root[style*="--USER__lineHeight"] p, +:root[style*="--USER__lineHeight"] li, +:root[style*="--USER__lineHeight"] div{ + line-height:inherit; +} + +:root[style*="--USER__paraSpacing"] p{ + margin-top:var(--USER__paraSpacing) !important; + margin-bottom:var(--USER__paraSpacing) !important; +} + +:root[style*="--USER__paraIndent"] p:not( + blockquote p, + figcaption p, + header p, + hgroup p, + :root[style*="readium-experimentalHeaderFiltering-on"] p[class*="title"], + :root[style*="readium-experimentalHeaderFiltering-on"] div:has(+ *) > h1 + p, + :root[style*="readium-experimentalHeaderFiltering-on"] div:has(+ *) > p:has(+ h1) +){ + text-indent:var(--USER__paraIndent) !important; +} + +:root[style*="--USER__paraIndent"] p *, +:root[style*="--USER__paraIndent"] p:first-letter{ + text-indent:0 !important; +} + +:root[style*="--USER__wordSpacing"] h1, +:root[style*="--USER__wordSpacing"] h2, +:root[style*="--USER__wordSpacing"] h3, +:root[style*="--USER__wordSpacing"] h4, +:root[style*="--USER__wordSpacing"] h5, +:root[style*="--USER__wordSpacing"] h6, +:root[style*="--USER__wordSpacing"] p, +:root[style*="--USER__wordSpacing"] li, +:root[style*="--USER__wordSpacing"] div, +:root[style*="--USER__wordSpacing"] dt, +:root[style*="--USER__wordSpacing"] dd{ + word-spacing:var(--USER__wordSpacing); +} + +:root[style*="--USER__letterSpacing"] h1, +:root[style*="--USER__letterSpacing"] h2, +:root[style*="--USER__letterSpacing"] h3, +:root[style*="--USER__letterSpacing"] h4, +:root[style*="--USER__letterSpacing"] h5, +:root[style*="--USER__letterSpacing"] h6, +:root[style*="--USER__letterSpacing"] p, +:root[style*="--USER__letterSpacing"] li, +:root[style*="--USER__letterSpacing"] div, +:root[style*="--USER__letterSpacing"] dt, +:root[style*="--USER__letterSpacing"] dd{ + letter-spacing:var(--USER__letterSpacing); + font-variant:none; +} + +:root[style*="--USER__fontWeight"] body{ + font-weight:var(--USER__fontWeight) !important; +} + +:root[style*="--USER__fontWeight"] b, +:root[style*="--USER__fontWeight"] strong{ + font-weight:bolder; +} + +:root[style*="--USER__fontWidth"] body{ + font-stretch:var(--USER__fontWidth) !important; +} + +:root[style*="--USER__fontOpticalSizing"] body{ + font-optical-sizing:var(--USER__fontOpticalSizing) !important; +} + +:root[style*="readium-noRuby-on"] body rt, +:root[style*="readium-noRuby-on"] body rp{ + display:none; +} + +:root[style*="--USER__ligatures"]{ + font-variant-ligatures:var(--USER__ligatures) !important; +} + +:root[style*="--USER__ligatures"] *{ + font-variant-ligatures:inherit !important; +} + +:root[style*="readium-iPadOSPatch-on"] body{ + -webkit-text-size-adjust:none; +} + +:root[style*="readium-iPadOSPatch-on"] p, +:root[style*="readium-iPadOSPatch-on"] h1, +:root[style*="readium-iPadOSPatch-on"] h2, +:root[style*="readium-iPadOSPatch-on"] h3, +:root[style*="readium-iPadOSPatch-on"] h4, +:root[style*="readium-iPadOSPatch-on"] h5, +:root[style*="readium-iPadOSPatch-on"] h6, +:root[style*="readium-iPadOSPatch-on"] li, +:root[style*="readium-iPadOSPatch-on"] th, +:root[style*="readium-iPadOSPatch-on"] td, +:root[style*="readium-iPadOSPatch-on"] dt, +:root[style*="readium-iPadOSPatch-on"] dd, +:root[style*="readium-iPadOSPatch-on"] pre, +:root[style*="readium-iPadOSPatch-on"] address, +:root[style*="readium-iPadOSPatch-on"] details, +:root[style*="readium-iPadOSPatch-on"] summary, +:root[style*="readium-iPadOSPatch-on"] figcaption, +:root[style*="readium-iPadOSPatch-on"] div:not(:has(p, h1, h2, h3, h4, h5, h6, li, th, td, dt, dd, pre, address, aside, details, figcaption, summary)), +:root[style*="readium-iPadOSPatch-on"] aside:not(:has(p, h1, h2, h3, h4, h5, h6, li, th, td, dt, dd, pre, address, aside, details, figcaption, summary)){ + -webkit-text-zoom:reset; +} + +:root[style*="readium-iPadOSPatch-on"] abbr, +:root[style*="readium-iPadOSPatch-on"] b, +:root[style*="readium-iPadOSPatch-on"] bdi, +:root[style*="readium-iPadOSPatch-on"] bdo, +:root[style*="readium-iPadOSPatch-on"] cite, +:root[style*="readium-iPadOSPatch-on"] code, +:root[style*="readium-iPadOSPatch-on"] dfn, +:root[style*="readium-iPadOSPatch-on"] em, +:root[style*="readium-iPadOSPatch-on"] i, +:root[style*="readium-iPadOSPatch-on"] kbd, +:root[style*="readium-iPadOSPatch-on"] mark, +:root[style*="readium-iPadOSPatch-on"] q, +:root[style*="readium-iPadOSPatch-on"] rp, +:root[style*="readium-iPadOSPatch-on"] rt, +:root[style*="readium-iPadOSPatch-on"] ruby, +:root[style*="readium-iPadOSPatch-on"] s, +:root[style*="readium-iPadOSPatch-on"] samp, +:root[style*="readium-iPadOSPatch-on"] small, +:root[style*="readium-iPadOSPatch-on"] span, +:root[style*="readium-iPadOSPatch-on"] strong, +:root[style*="readium-iPadOSPatch-on"] sub, +:root[style*="readium-iPadOSPatch-on"] sup, +:root[style*="readium-iPadOSPatch-on"] time, +:root[style*="readium-iPadOSPatch-on"] u, +:root[style*="readium-iPadOSPatch-on"] var{ + -webkit-text-zoom:normal; +} + +:root[style*="readium-iPadOSPatch-on"] p:not(:has(b, cite, em, i, q, s, small, span, strong)):first-line{ + -webkit-text-zoom:normal; +} \ No newline at end of file diff --git a/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-one.js b/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-one.js index a1f95ec215..6eb8689fac 100644 --- a/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-one.js +++ b/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-one.js @@ -1,2 +1,2 @@ -(()=>{"use strict";var t={};t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}();var e=function(t){var e=null,n=null,i=null,o=document.getElementById("page");o.addEventListener("load",(function(){var t=o.contentWindow.document.querySelector("meta[name=viewport]");if(t){for(var n,i=/(\w+) *= *([^\s,]+)/g,l={};n=i.exec(t.content);)l[n[1]]=n[2];var a=Number.parseFloat(l.width),s=Number.parseFloat(l.height);a&&s&&(e={width:a,height:s},r())}}));var l=o.closest(".viewport");function r(){if(e&&n&&i){o.style.width=e.width+"px",o.style.height=e.height+"px",o.style.marginTop=i.top-i.bottom+"px";var t=n.width/e.width,l=n.height/e.height,r=Math.min(t,l);document.querySelector("meta[name=viewport]").content="initial-scale="+r+", minimum-scale="+r}}return{isLoading:!1,link:null,load:function(t,e){if(t.link&&t.url){var n=this;n.link=t.link,n.isLoading=!0,o.addEventListener("load",(function i(){o.removeEventListener("load",i),setTimeout((function(){n.isLoading=!1,o.contentWindow.eval(`readium.link = ${JSON.stringify(t.link)};`),e&&e()}),100)})),o.src=t.url}else e&&e()},reset:function(){this.link&&(this.link=null,e=null,o.src="about:blank")},eval:function(t){if(this.link&&!this.isLoading)return o.contentWindow.eval(t)},setViewport:function(t,e){n=t,i=e,r()},show:function(){l.style.display="block"},hide:function(){l.style.display="none"}}}();t.g.spread={load:function(t){0!==t.length&&e.load(t[0],(function(){webkit.messageHandlers.spreadLoaded.postMessage({})}))},eval:function(t,n){var i;if("#"===t||""===t||(null===(i=e.link)||void 0===i?void 0:i.href)===t)return e.eval(n)},setViewport:function(t,n){e.setViewport(t,n)}}})(); +(()=>{"use strict";var t={};t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}();const e={SINGLE:"single",SPREAD_LEFT:"spread-left",SPREAD_RIGHT:"spread-right",SPREAD_CENTER:"spread-center"},n={AUTO:"auto",PAGE:"page",WIDTH:"width"};var i=function(t,i){var l=null,o=null,a=null,r=n.AUTO,s=Object.values(e).includes(i)?i:e.SINGLE,u=document.getElementById("page");u.addEventListener("load",(function(){var t,e,n;l=null!==(t=null!==(e=function(){var t=u.contentWindow.document.querySelector("meta[name=viewport]");if(!t)return null;for(var e,n=/(\w+) *= *([^\s,]+)/g,i={};e=n.exec(t.content);)i[e[1]]=e[2];var l=Number.parseFloat(i.width),o=Number.parseFloat(i.height);return l&&o?{width:l,height:o}:null}())&&void 0!==e?e:(n=u.contentWindow.document.querySelector("img"))&&n.naturalWidth&&n.naturalHeight?{width:n.naturalWidth,height:n.naturalHeight}:null)&&void 0!==t?t:o,d()}));var c=u.closest(".viewport");function d(){if(l&&o&&a){u.style.width=l.width+"px",u.style.height=l.height+"px";var t,i=o.width/l.width,c=o.height/l.height;t=r===n.WIDTH?i:Math.min(i,c);var d=l.height*t,h=s===e.SINGLE||s===e.SPREAD_CENTER;if(r===n.WIDTH&&d>o.height)u.style.top=a.top+"px",u.style.transform=h?"translateX(-50%)":"none";else{var m=a.top-a.bottom;u.style.top="calc(50% + "+m+"px)",u.style.transform=h?"translate(-50%, -50%)":"translateY(-50%)"}document.querySelector("meta[name=viewport]").content="initial-scale="+t+", minimum-scale="+t}}function h(t){u.src.startsWith("blob:")&&URL.revokeObjectURL(u.src),u.src=t}return{isLoading:!1,link:null,load:function(t,e){if(t.link&&t.url){var n=this;n.link=t.link,n.isLoading=!0,u.addEventListener("load",(function i(){u.removeEventListener("load",i),setTimeout((function(){n.isLoading=!1,u.contentWindow.eval(`readium.link = ${JSON.stringify(t.link)};`),e&&e()}),100)}));var i=function(t){if((e=t.link.type)&&e.startsWith("image/")&&!e.includes("svg")){let e=function(t,e){let n=document.implementation.createHTMLDocument(""),i=n.createElement("meta");i.name="viewport",i.content="width=device-width, height=device-height",n.head.appendChild(i);let l=n.createElement("style");l.textContent="body { margin: 0; }\nimg { display: block; width: 100%; height: 100%; object-fit: contain; }",n.head.appendChild(l);let o=n.createElement("img");return o.src=t,e&&(o.alt=e),n.body.appendChild(o),"\n"+n.documentElement.outerHTML}(t.url,t.link.title),n=new Blob([e],{type:"text/html"});return URL.createObjectURL(n)}return t.url;var e}(t);h(i)}else e&&e()},reset:function(){this.link&&(this.link=null,l=null,h("about:blank"))},eval:function(t){if(this.link&&!this.isLoading)return u.contentWindow.eval(t)},setViewport:function(t,e,i){o=t,a=e,Object.values(n).includes(i)&&(r=i),d()},show:function(){c.style.display="block"},hide:function(){c.style.display="none"}}}(0,e.SINGLE);t.g.spread={load:function(t){0!==t.length&&i.load(t[0],(function(){webkit.messageHandlers.spreadLoaded.postMessage({})}))},eval:function(t,e){var n;if("#"===t||""===t||(null===(n=i.link)||void 0===n?void 0:n.href)===t)return i.eval(e)},setViewport:function(t,e,n){i.setViewport(t,e,n)}}})(); //# sourceMappingURL=readium-fixed-wrapper-one.js.map \ No newline at end of file diff --git a/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-two.js b/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-two.js index 8ee445d82e..244222298b 100644 --- a/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-two.js +++ b/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-two.js @@ -1,2 +1,2 @@ -(()=>{"use strict";var t={};function e(t){var e=null,n=null,i=null,o=document.getElementById(t);o.addEventListener("load",(function(){var t=o.contentWindow.document.querySelector("meta[name=viewport]");if(t){for(var n,i=/(\w+) *= *([^\s,]+)/g,r={};n=i.exec(t.content);)r[n[1]]=n[2];var a=Number.parseFloat(r.width),s=Number.parseFloat(r.height);a&&s&&(e={width:a,height:s},l())}}));var r=o.closest(".viewport");function l(){if(e&&n&&i){o.style.width=e.width+"px",o.style.height=e.height+"px",o.style.marginTop=i.top-i.bottom+"px";var t=n.width/e.width,r=n.height/e.height,l=Math.min(t,r);document.querySelector("meta[name=viewport]").content="initial-scale="+l+", minimum-scale="+l}}return{isLoading:!1,link:null,load:function(t,e){if(t.link&&t.url){var n=this;n.link=t.link,n.isLoading=!0,o.addEventListener("load",(function i(){o.removeEventListener("load",i),setTimeout((function(){n.isLoading=!1,o.contentWindow.eval(`readium.link = ${JSON.stringify(t.link)};`),e&&e()}),100)})),o.src=t.url}else e&&e()},reset:function(){this.link&&(this.link=null,e=null,o.src="about:blank")},eval:function(t){if(this.link&&!this.isLoading)return o.contentWindow.eval(t)},setViewport:function(t,e){n=t,i=e,l()},show:function(){r.style.display="block"},hide:function(){r.style.display="none"}}}t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}();var n={left:e("page-left"),right:e("page-right"),center:e("page-center")};function i(t){for(const e in n)t(n[e])}t.g.spread={load:function(t){function e(){n.left.isLoading||n.right.isLoading||n.center.isLoading||webkit.messageHandlers.spreadLoaded.postMessage({})}i((function(t){t.reset(),t.hide()}));for(const i in t){const o=t[i],r=n[o.page];r&&(r.show(),r.load(o,e))}},eval:function(t,e){if("#"===t||""===t)i((function(t){t.eval(e)}));else{var o=function(t){for(const o in n){var e,i=n[o];if((null===(e=i.link)||void 0===e?void 0:e.href)===t)return i}return null}(t);if(o)return o.eval(e)}},setViewport:function(t,e){t.width/=2,n.left.setViewport(t,{top:e.top,right:0,bottom:e.bottom,left:e.left}),n.right.setViewport(t,{top:e.top,right:e.right,bottom:e.bottom,left:0}),n.center.setViewport(t,{top:e.top,right:0,bottom:e.bottom,left:0})}}})(); +(()=>{"use strict";var t={};t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}();const e={SINGLE:"single",SPREAD_LEFT:"spread-left",SPREAD_RIGHT:"spread-right",SPREAD_CENTER:"spread-center"},n={AUTO:"auto",PAGE:"page",WIDTH:"width"};function i(t,i){var o=null,l=null,r=null,a=n.AUTO,s=Object.values(e).includes(i)?i:e.SINGLE,c=document.getElementById(t);c.addEventListener("load",(function(){var t,e,n;o=null!==(t=null!==(e=function(){var t=c.contentWindow.document.querySelector("meta[name=viewport]");if(!t)return null;for(var e,n=/(\w+) *= *([^\s,]+)/g,i={};e=n.exec(t.content);)i[e[1]]=e[2];var o=Number.parseFloat(i.width),l=Number.parseFloat(i.height);return o&&l?{width:o,height:l}:null}())&&void 0!==e?e:(n=c.contentWindow.document.querySelector("img"))&&n.naturalWidth&&n.naturalHeight?{width:n.naturalWidth,height:n.naturalHeight}:null)&&void 0!==t?t:l,h()}));var u=c.closest(".viewport");function h(){if(o&&l&&r){c.style.width=o.width+"px",c.style.height=o.height+"px";var t,i=l.width/o.width,u=l.height/o.height;t=a===n.WIDTH?i:Math.min(i,u);var h=o.height*t,d=s===e.SINGLE||s===e.SPREAD_CENTER;if(a===n.WIDTH&&h>l.height)c.style.top=r.top+"px",c.style.transform=d?"translateX(-50%)":"none";else{var f=r.top-r.bottom;c.style.top="calc(50% + "+f+"px)",c.style.transform=d?"translate(-50%, -50%)":"translateY(-50%)"}document.querySelector("meta[name=viewport]").content="initial-scale="+t+", minimum-scale="+t}}function d(t){c.src.startsWith("blob:")&&URL.revokeObjectURL(c.src),c.src=t}return{isLoading:!1,link:null,load:function(t,e){if(t.link&&t.url){var n=this;n.link=t.link,n.isLoading=!0,c.addEventListener("load",(function i(){c.removeEventListener("load",i),setTimeout((function(){n.isLoading=!1,c.contentWindow.eval(`readium.link = ${JSON.stringify(t.link)};`),e&&e()}),100)}));var i=function(t){if((e=t.link.type)&&e.startsWith("image/")&&!e.includes("svg")){let e=function(t,e){let n=document.implementation.createHTMLDocument(""),i=n.createElement("meta");i.name="viewport",i.content="width=device-width, height=device-height",n.head.appendChild(i);let o=n.createElement("style");o.textContent="body { margin: 0; }\nimg { display: block; width: 100%; height: 100%; object-fit: contain; }",n.head.appendChild(o);let l=n.createElement("img");return l.src=t,e&&(l.alt=e),n.body.appendChild(l),"\n"+n.documentElement.outerHTML}(t.url,t.link.title),n=new Blob([e],{type:"text/html"});return URL.createObjectURL(n)}return t.url;var e}(t);d(i)}else e&&e()},reset:function(){this.link&&(this.link=null,o=null,d("about:blank"))},eval:function(t){if(this.link&&!this.isLoading)return c.contentWindow.eval(t)},setViewport:function(t,e,i){l=t,r=e,Object.values(n).includes(i)&&(a=i),h()},show:function(){u.style.display="block"},hide:function(){u.style.display="none"}}}var o={left:i("page-left",e.SPREAD_LEFT),right:i("page-right",e.SPREAD_RIGHT),center:i("page-center",e.SPREAD_CENTER)};function l(t){for(const e in o)t(o[e])}t.g.spread={load:function(t){function e(){o.left.isLoading||o.right.isLoading||o.center.isLoading||webkit.messageHandlers.spreadLoaded.postMessage({})}l((function(t){t.reset(),t.hide()}));for(const n in t){const i=t[n],l=o[i.page];l&&(l.show(),l.load(i,e))}},eval:function(t,e){if("#"===t||""===t)l((function(t){t.eval(e)}));else{var n=function(t){for(const i in o){var e,n=o[i];if((null===(e=n.link)||void 0===e?void 0:e.href)===t)return n}return null}(t);if(n)return n.eval(e)}},setViewport:function(t,e,n){var i={width:t.width/2,height:t.height};o.left.setViewport(i,{top:e.top,right:0,bottom:e.bottom,left:e.left},n),o.right.setViewport(i,{top:e.top,right:e.right,bottom:e.bottom,left:0},n),o.center.setViewport(t,{top:e.top,right:e.right,bottom:e.bottom,left:e.left},n)}}})(); //# sourceMappingURL=readium-fixed-wrapper-two.js.map \ No newline at end of file diff --git a/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed.js b/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed.js index 5ac7056e48..461f303bd3 100644 --- a/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed.js +++ b/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed.js @@ -1,2 +1,2 @@ -(()=>{var t={9116:(t,e)=>{"use strict";function r(t){return t.split("").reverse().join("")}function n(t){return(t|-t)>>31&1}function o(t,e,r,o){var i=t.P[r],a=t.M[r],u=o>>>31,c=e[r]|u,s=c|a,l=(c&i)+i^i|c,f=a|~(l|i),p=i&l,y=n(f&t.lastRowMask[r])-n(p&t.lastRowMask[r]);return f<<=1,p<<=1,i=(p|=u)|~(s|(f|=n(o)-u)),a=f&s,t.P[r]=i,t.M[r]=a,y}function i(t,e,r){if(0===e.length)return[];r=Math.min(r,e.length);var n=[],i=32,a=Math.ceil(e.length/i)-1,u={P:new Uint32Array(a+1),M:new Uint32Array(a+1),lastRowMask:new Uint32Array(a+1)};u.lastRowMask.fill(1<<31),u.lastRowMask[a]=1<<(e.length-1)%i;for(var c=new Uint32Array(a+1),s=new Map,l=[],f=0;f<256;f++)l.push(c);for(var p=0;p=e.length||e.charCodeAt(m)===y&&(d[h]|=1<0&&v[b]>=r+i;)b-=1;b===a&&v[b]<=r&&(v[b]{"use strict";var n=r(4624),o=r(5096),i=o(n("String.prototype.indexOf"));t.exports=function(t,e){var r=n(t,!!e);return"function"==typeof r&&i(t,".prototype.")>-1?o(r):r}},5096:(t,e,r)=>{"use strict";var n=r(3520),o=r(4624),i=r(5676),a=r(2824),u=o("%Function.prototype.apply%"),c=o("%Function.prototype.call%"),s=o("%Reflect.apply%",!0)||n.call(c,u),l=o("%Object.defineProperty%",!0),f=o("%Math.max%");if(l)try{l({},"a",{value:1})}catch(t){l=null}t.exports=function(t){if("function"!=typeof t)throw new a("a function is required");var e=s(n,c,arguments);return i(e,1+f(0,t.length-(arguments.length-1)),!0)};var p=function(){return s(n,u,arguments)};l?l(t.exports,"apply",{value:p}):t.exports.apply=p},2448:(t,e,r)=>{"use strict";var n=r(3268)(),o=r(4624),i=n&&o("%Object.defineProperty%",!0);if(i)try{i({},"a",{value:1})}catch(t){i=!1}var a=r(6500),u=r(2824),c=r(6168);t.exports=function(t,e,r){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new u("`obj` must be an object or a function`");if("string"!=typeof e&&"symbol"!=typeof e)throw new u("`property` must be a string or a symbol`");if(arguments.length>3&&"boolean"!=typeof arguments[3]&&null!==arguments[3])throw new u("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&"boolean"!=typeof arguments[4]&&null!==arguments[4])throw new u("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&"boolean"!=typeof arguments[5]&&null!==arguments[5])throw new u("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&"boolean"!=typeof arguments[6])throw new u("`loose`, if provided, must be a boolean");var n=arguments.length>3?arguments[3]:null,o=arguments.length>4?arguments[4]:null,s=arguments.length>5?arguments[5]:null,l=arguments.length>6&&arguments[6],f=!!c&&c(t,e);if(i)i(t,e,{configurable:null===s&&f?f.configurable:!s,enumerable:null===n&&f?f.enumerable:!n,value:r,writable:null===o&&f?f.writable:!o});else{if(!l&&(n||o||s))throw new a("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.");t[e]=r}}},2732:(t,e,r)=>{"use strict";var n=r(2812),o="function"==typeof Symbol&&"symbol"==typeof Symbol("foo"),i=Object.prototype.toString,a=Array.prototype.concat,u=r(2448),c=r(3268)(),s=function(t,e,r,n){if(e in t)if(!0===n){if(t[e]===r)return}else if("function"!=typeof(o=n)||"[object Function]"!==i.call(o)||!n())return;var o;c?u(t,e,r,!0):u(t,e,r)},l=function(t,e){var r=arguments.length>2?arguments[2]:{},i=n(e);o&&(i=a.call(i,Object.getOwnPropertySymbols(e)));for(var u=0;u{"use strict";t.exports=EvalError},1152:t=>{"use strict";t.exports=Error},1932:t=>{"use strict";t.exports=RangeError},5028:t=>{"use strict";t.exports=ReferenceError},6500:t=>{"use strict";t.exports=SyntaxError},2824:t=>{"use strict";t.exports=TypeError},5488:t=>{"use strict";t.exports=URIError},9200:(t,e,r)=>{"use strict";var n=r(4624)("%Object.defineProperty%",!0),o=r(4712)(),i=r(4440),a=o?Symbol.toStringTag:null;t.exports=function(t,e){var r=arguments.length>2&&arguments[2]&&arguments[2].force;!a||!r&&i(t,a)||(n?n(t,a,{configurable:!0,enumerable:!1,value:e,writable:!1}):t[a]=e)}},108:(t,e,r)=>{"use strict";var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator,o=r(5988),i=r(648),a=r(1844),u=r(7256);t.exports=function(t){if(o(t))return t;var e,r="default";if(arguments.length>1&&(arguments[1]===String?r="string":arguments[1]===Number&&(r="number")),n&&(Symbol.toPrimitive?e=function(t,e){var r=t[e];if(null!=r){if(!i(r))throw new TypeError(r+" returned for property "+e+" of object "+t+" is not a function");return r}}(t,Symbol.toPrimitive):u(t)&&(e=Symbol.prototype.valueOf)),void 0!==e){var c=e.call(t,r);if(o(c))return c;throw new TypeError("unable to convert exotic object to primitive")}return"default"===r&&(a(t)||u(t))&&(r="string"),function(t,e){if(null==t)throw new TypeError("Cannot call method on "+t);if("string"!=typeof e||"number"!==e&&"string"!==e)throw new TypeError('hint must be "string" or "number"');var r,n,a,u="string"===e?["toString","valueOf"]:["valueOf","toString"];for(a=0;a{"use strict";t.exports=function(t){return null===t||"function"!=typeof t&&"object"!=typeof t}},1480:t=>{"use strict";var e=Object.prototype.toString,r=Math.max,n=function(t,e){for(var r=[],n=0;n{"use strict";var n=r(1480);t.exports=Function.prototype.bind||n},2656:t=>{"use strict";var e=function(){return"string"==typeof function(){}.name},r=Object.getOwnPropertyDescriptor;if(r)try{r([],"length")}catch(t){r=null}e.functionsHaveConfigurableNames=function(){if(!e()||!r)return!1;var t=r((function(){}),"name");return!!t&&!!t.configurable};var n=Function.prototype.bind;e.boundFunctionsHaveNames=function(){return e()&&"function"==typeof n&&""!==function(){}.bind().name},t.exports=e},4624:(t,e,r)=>{"use strict";var n,o=r(1152),i=r(7261),a=r(1932),u=r(5028),c=r(6500),s=r(2824),l=r(5488),f=Function,p=function(t){try{return f('"use strict"; return ('+t+").constructor;")()}catch(t){}},y=Object.getOwnPropertyDescriptor;if(y)try{y({},"")}catch(t){y=null}var d=function(){throw new s},h=y?function(){try{return d}catch(t){try{return y(arguments,"callee").get}catch(t){return d}}}():d,g=r(9800)(),m=r(7e3)(),b=Object.getPrototypeOf||(m?function(t){return t.__proto__}:null),v={},w="undefined"!=typeof Uint8Array&&b?b(Uint8Array):n,x={__proto__:null,"%AggregateError%":"undefined"==typeof AggregateError?n:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?n:ArrayBuffer,"%ArrayIteratorPrototype%":g&&b?b([][Symbol.iterator]()):n,"%AsyncFromSyncIteratorPrototype%":n,"%AsyncFunction%":v,"%AsyncGenerator%":v,"%AsyncGeneratorFunction%":v,"%AsyncIteratorPrototype%":v,"%Atomics%":"undefined"==typeof Atomics?n:Atomics,"%BigInt%":"undefined"==typeof BigInt?n:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?n:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?n:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?n:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":o,"%eval%":eval,"%EvalError%":i,"%Float32Array%":"undefined"==typeof Float32Array?n:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?n:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?n:FinalizationRegistry,"%Function%":f,"%GeneratorFunction%":v,"%Int8Array%":"undefined"==typeof Int8Array?n:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?n:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?n:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":g&&b?b(b([][Symbol.iterator]())):n,"%JSON%":"object"==typeof JSON?JSON:n,"%Map%":"undefined"==typeof Map?n:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&g&&b?b((new Map)[Symbol.iterator]()):n,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?n:Promise,"%Proxy%":"undefined"==typeof Proxy?n:Proxy,"%RangeError%":a,"%ReferenceError%":u,"%Reflect%":"undefined"==typeof Reflect?n:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?n:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&g&&b?b((new Set)[Symbol.iterator]()):n,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?n:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":g&&b?b(""[Symbol.iterator]()):n,"%Symbol%":g?Symbol:n,"%SyntaxError%":c,"%ThrowTypeError%":h,"%TypedArray%":w,"%TypeError%":s,"%Uint8Array%":"undefined"==typeof Uint8Array?n:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?n:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?n:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?n:Uint32Array,"%URIError%":l,"%WeakMap%":"undefined"==typeof WeakMap?n:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?n:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?n:WeakSet};if(b)try{null.error}catch(t){var S=b(b(t));x["%Error.prototype%"]=S}var E=function t(e){var r;if("%AsyncFunction%"===e)r=p("async function () {}");else if("%GeneratorFunction%"===e)r=p("function* () {}");else if("%AsyncGeneratorFunction%"===e)r=p("async function* () {}");else if("%AsyncGenerator%"===e){var n=t("%AsyncGeneratorFunction%");n&&(r=n.prototype)}else if("%AsyncIteratorPrototype%"===e){var o=t("%AsyncGenerator%");o&&b&&(r=b(o.prototype))}return x[e]=r,r},A={__proto__:null,"%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"]},O=r(3520),j=r(4440),T=O.call(Function.call,Array.prototype.concat),P=O.call(Function.apply,Array.prototype.splice),R=O.call(Function.call,String.prototype.replace),C=O.call(Function.call,String.prototype.slice),I=O.call(Function.call,RegExp.prototype.exec),N=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,M=/\\(\\)?/g,$=function(t,e){var r,n=t;if(j(A,n)&&(n="%"+(r=A[n])[0]+"%"),j(x,n)){var o=x[n];if(o===v&&(o=E(n)),void 0===o&&!e)throw new s("intrinsic "+t+" exists, but is not available. Please file an issue!");return{alias:r,name:n,value:o}}throw new c("intrinsic "+t+" does not exist!")};t.exports=function(t,e){if("string"!=typeof t||0===t.length)throw new s("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof e)throw new s('"allowMissing" argument must be a boolean');if(null===I(/^%?[^%]*%?$/,t))throw new c("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var r=function(t){var e=C(t,0,1),r=C(t,-1);if("%"===e&&"%"!==r)throw new c("invalid intrinsic syntax, expected closing `%`");if("%"===r&&"%"!==e)throw new c("invalid intrinsic syntax, expected opening `%`");var n=[];return R(t,N,(function(t,e,r,o){n[n.length]=r?R(o,M,"$1"):e||t})),n}(t),n=r.length>0?r[0]:"",o=$("%"+n+"%",e),i=o.name,a=o.value,u=!1,l=o.alias;l&&(n=l[0],P(r,T([0,1],l)));for(var f=1,p=!0;f=r.length){var m=y(a,d);a=(p=!!m)&&"get"in m&&!("originalValue"in m.get)?m.get:a[d]}else p=j(a,d),a=a[d];p&&!u&&(x[i]=a)}}return a}},6168:(t,e,r)=>{"use strict";var n=r(4624)("%Object.getOwnPropertyDescriptor%",!0);if(n)try{n([],"length")}catch(t){n=null}t.exports=n},3268:(t,e,r)=>{"use strict";var n=r(4624)("%Object.defineProperty%",!0),o=function(){if(n)try{return n({},"a",{value:1}),!0}catch(t){return!1}return!1};o.hasArrayLengthDefineBug=function(){if(!o())return null;try{return 1!==n([],"length",{value:1}).length}catch(t){return!0}},t.exports=o},7e3:t=>{"use strict";var e={foo:{}},r=Object;t.exports=function(){return{__proto__:e}.foo===e.foo&&!({__proto__:null}instanceof r)}},9800:(t,e,r)=>{"use strict";var n="undefined"!=typeof Symbol&&Symbol,o=r(7904);t.exports=function(){return"function"==typeof n&&"function"==typeof Symbol&&"symbol"==typeof n("foo")&&"symbol"==typeof Symbol("bar")&&o()}},7904:t=>{"use strict";t.exports=function(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var t={},e=Symbol("test"),r=Object(e);if("string"==typeof e)return!1;if("[object Symbol]"!==Object.prototype.toString.call(e))return!1;if("[object Symbol]"!==Object.prototype.toString.call(r))return!1;for(e in t[e]=42,t)return!1;if("function"==typeof Object.keys&&0!==Object.keys(t).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(t).length)return!1;var n=Object.getOwnPropertySymbols(t);if(1!==n.length||n[0]!==e)return!1;if(!Object.prototype.propertyIsEnumerable.call(t,e))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var o=Object.getOwnPropertyDescriptor(t,e);if(42!==o.value||!0!==o.enumerable)return!1}return!0}},4712:(t,e,r)=>{"use strict";var n=r(7904);t.exports=function(){return n()&&!!Symbol.toStringTag}},4440:(t,e,r)=>{"use strict";var n=Function.prototype.call,o=Object.prototype.hasOwnProperty,i=r(3520);t.exports=i.call(n,o)},7284:(t,e,r)=>{"use strict";var n=r(4440),o=r(3147)(),i=r(2824),a={assert:function(t,e){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");if(o.assert(t),!a.has(t,e))throw new i("`"+e+"` is not present on `O`")},get:function(t,e){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");var r=o.get(t);return r&&r["$"+e]},has:function(t,e){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");var r=o.get(t);return!!r&&n(r,"$"+e)},set:function(t,e,r){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");var n=o.get(t);n||(n={},o.set(t,n)),n["$"+e]=r}};Object.freeze&&Object.freeze(a),t.exports=a},648:t=>{"use strict";var e,r,n=Function.prototype.toString,o="object"==typeof Reflect&&null!==Reflect&&Reflect.apply;if("function"==typeof o&&"function"==typeof Object.defineProperty)try{e=Object.defineProperty({},"length",{get:function(){throw r}}),r={},o((function(){throw 42}),null,e)}catch(t){t!==r&&(o=null)}else o=null;var i=/^\s*class\b/,a=function(t){try{var e=n.call(t);return i.test(e)}catch(t){return!1}},u=function(t){try{return!a(t)&&(n.call(t),!0)}catch(t){return!1}},c=Object.prototype.toString,s="function"==typeof Symbol&&!!Symbol.toStringTag,l=!(0 in[,]),f=function(){return!1};if("object"==typeof document){var p=document.all;c.call(p)===c.call(document.all)&&(f=function(t){if((l||!t)&&(void 0===t||"object"==typeof t))try{var e=c.call(t);return("[object HTMLAllCollection]"===e||"[object HTML document.all class]"===e||"[object HTMLCollection]"===e||"[object Object]"===e)&&null==t("")}catch(t){}return!1})}t.exports=o?function(t){if(f(t))return!0;if(!t)return!1;if("function"!=typeof t&&"object"!=typeof t)return!1;try{o(t,null,e)}catch(t){if(t!==r)return!1}return!a(t)&&u(t)}:function(t){if(f(t))return!0;if(!t)return!1;if("function"!=typeof t&&"object"!=typeof t)return!1;if(s)return u(t);if(a(t))return!1;var e=c.call(t);return!("[object Function]"!==e&&"[object GeneratorFunction]"!==e&&!/^\[object HTML/.test(e))&&u(t)}},1844:(t,e,r)=>{"use strict";var n=Date.prototype.getDay,o=Object.prototype.toString,i=r(4712)();t.exports=function(t){return"object"==typeof t&&null!==t&&(i?function(t){try{return n.call(t),!0}catch(t){return!1}}(t):"[object Date]"===o.call(t))}},1476:(t,e,r)=>{"use strict";var n,o,i,a,u=r(668),c=r(4712)();if(c){n=u("Object.prototype.hasOwnProperty"),o=u("RegExp.prototype.exec"),i={};var s=function(){throw i};a={toString:s,valueOf:s},"symbol"==typeof Symbol.toPrimitive&&(a[Symbol.toPrimitive]=s)}var l=u("Object.prototype.toString"),f=Object.getOwnPropertyDescriptor;t.exports=c?function(t){if(!t||"object"!=typeof t)return!1;var e=f(t,"lastIndex");if(!e||!n(e,"value"))return!1;try{o(t,a)}catch(t){return t===i}}:function(t){return!(!t||"object"!=typeof t&&"function"!=typeof t)&&"[object RegExp]"===l(t)}},7256:(t,e,r)=>{"use strict";var n=Object.prototype.toString;if(r(9800)()){var o=Symbol.prototype.toString,i=/^Symbol\(.*\)$/;t.exports=function(t){if("symbol"==typeof t)return!0;if("[object Symbol]"!==n.call(t))return!1;try{return function(t){return"symbol"==typeof t.valueOf()&&i.test(o.call(t))}(t)}catch(t){return!1}}}else t.exports=function(t){return!1}},4152:(t,e,r)=>{var n="function"==typeof Map&&Map.prototype,o=Object.getOwnPropertyDescriptor&&n?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,i=n&&o&&"function"==typeof o.get?o.get:null,a=n&&Map.prototype.forEach,u="function"==typeof Set&&Set.prototype,c=Object.getOwnPropertyDescriptor&&u?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,s=u&&c&&"function"==typeof c.get?c.get:null,l=u&&Set.prototype.forEach,f="function"==typeof WeakMap&&WeakMap.prototype?WeakMap.prototype.has:null,p="function"==typeof WeakSet&&WeakSet.prototype?WeakSet.prototype.has:null,y="function"==typeof WeakRef&&WeakRef.prototype?WeakRef.prototype.deref:null,d=Boolean.prototype.valueOf,h=Object.prototype.toString,g=Function.prototype.toString,m=String.prototype.match,b=String.prototype.slice,v=String.prototype.replace,w=String.prototype.toUpperCase,x=String.prototype.toLowerCase,S=RegExp.prototype.test,E=Array.prototype.concat,A=Array.prototype.join,O=Array.prototype.slice,j=Math.floor,T="function"==typeof BigInt?BigInt.prototype.valueOf:null,P=Object.getOwnPropertySymbols,R="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,C="function"==typeof Symbol&&"object"==typeof Symbol.iterator,I="function"==typeof Symbol&&Symbol.toStringTag&&(Symbol.toStringTag,1)?Symbol.toStringTag:null,N=Object.prototype.propertyIsEnumerable,M=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(t){return t.__proto__}:null);function $(t,e){if(t===1/0||t===-1/0||t!=t||t&&t>-1e3&&t<1e3||S.call(/e/,e))return e;var r=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof t){var n=t<0?-j(-t):j(t);if(n!==t){var o=String(n),i=b.call(e,o.length+1);return v.call(o,r,"$&_")+"."+v.call(v.call(i,/([0-9]{3})/g,"$&_"),/_$/,"")}}return v.call(e,r,"$&_")}var k=r(1740),D=k.custom,F=U(D)?D:null;function L(t,e,r){var n="double"===(r.quoteStyle||e)?'"':"'";return n+t+n}function B(t){return v.call(String(t),/"/g,""")}function _(t){return!("[object Array]"!==H(t)||I&&"object"==typeof t&&I in t)}function W(t){return!("[object RegExp]"!==H(t)||I&&"object"==typeof t&&I in t)}function U(t){if(C)return t&&"object"==typeof t&&t instanceof Symbol;if("symbol"==typeof t)return!0;if(!t||"object"!=typeof t||!R)return!1;try{return R.call(t),!0}catch(t){}return!1}t.exports=function t(e,n,o,u){var c=n||{};if(G(c,"quoteStyle")&&"single"!==c.quoteStyle&&"double"!==c.quoteStyle)throw new TypeError('option "quoteStyle" must be "single" or "double"');if(G(c,"maxStringLength")&&("number"==typeof c.maxStringLength?c.maxStringLength<0&&c.maxStringLength!==1/0:null!==c.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var h=!G(c,"customInspect")||c.customInspect;if("boolean"!=typeof h&&"symbol"!==h)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(G(c,"indent")&&null!==c.indent&&"\t"!==c.indent&&!(parseInt(c.indent,10)===c.indent&&c.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(G(c,"numericSeparator")&&"boolean"!=typeof c.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var w=c.numericSeparator;if(void 0===e)return"undefined";if(null===e)return"null";if("boolean"==typeof e)return e?"true":"false";if("string"==typeof e)return q(e,c);if("number"==typeof e){if(0===e)return 1/0/e>0?"0":"-0";var S=String(e);return w?$(e,S):S}if("bigint"==typeof e){var j=String(e)+"n";return w?$(e,j):j}var P=void 0===c.depth?5:c.depth;if(void 0===o&&(o=0),o>=P&&P>0&&"object"==typeof e)return _(e)?"[Array]":"[Object]";var D,z=function(t,e){var r;if("\t"===t.indent)r="\t";else{if(!("number"==typeof t.indent&&t.indent>0))return null;r=A.call(Array(t.indent+1)," ")}return{base:r,prev:A.call(Array(e+1),r)}}(c,o);if(void 0===u)u=[];else if(V(u,e)>=0)return"[Circular]";function X(e,r,n){if(r&&(u=O.call(u)).push(r),n){var i={depth:c.depth};return G(c,"quoteStyle")&&(i.quoteStyle=c.quoteStyle),t(e,i,o+1,u)}return t(e,c,o+1,u)}if("function"==typeof e&&!W(e)){var tt=function(t){if(t.name)return t.name;var e=m.call(g.call(t),/^function\s*([\w$]+)/);return e?e[1]:null}(e),et=Z(e,X);return"[Function"+(tt?": "+tt:" (anonymous)")+"]"+(et.length>0?" { "+A.call(et,", ")+" }":"")}if(U(e)){var rt=C?v.call(String(e),/^(Symbol\(.*\))_[^)]*$/,"$1"):R.call(e);return"object"!=typeof e||C?rt:K(rt)}if((D=e)&&"object"==typeof D&&("undefined"!=typeof HTMLElement&&D instanceof HTMLElement||"string"==typeof D.nodeName&&"function"==typeof D.getAttribute)){for(var nt="<"+x.call(String(e.nodeName)),ot=e.attributes||[],it=0;it"}if(_(e)){if(0===e.length)return"[]";var at=Z(e,X);return z&&!function(t){for(var e=0;e=0)return!1;return!0}(at)?"["+Q(at,z)+"]":"[ "+A.call(at,", ")+" ]"}if(function(t){return!("[object Error]"!==H(t)||I&&"object"==typeof t&&I in t)}(e)){var ut=Z(e,X);return"cause"in Error.prototype||!("cause"in e)||N.call(e,"cause")?0===ut.length?"["+String(e)+"]":"{ ["+String(e)+"] "+A.call(ut,", ")+" }":"{ ["+String(e)+"] "+A.call(E.call("[cause]: "+X(e.cause),ut),", ")+" }"}if("object"==typeof e&&h){if(F&&"function"==typeof e[F]&&k)return k(e,{depth:P-o});if("symbol"!==h&&"function"==typeof e.inspect)return e.inspect()}if(function(t){if(!i||!t||"object"!=typeof t)return!1;try{i.call(t);try{s.call(t)}catch(t){return!0}return t instanceof Map}catch(t){}return!1}(e)){var ct=[];return a&&a.call(e,(function(t,r){ct.push(X(r,e,!0)+" => "+X(t,e))})),J("Map",i.call(e),ct,z)}if(function(t){if(!s||!t||"object"!=typeof t)return!1;try{s.call(t);try{i.call(t)}catch(t){return!0}return t instanceof Set}catch(t){}return!1}(e)){var st=[];return l&&l.call(e,(function(t){st.push(X(t,e))})),J("Set",s.call(e),st,z)}if(function(t){if(!f||!t||"object"!=typeof t)return!1;try{f.call(t,f);try{p.call(t,p)}catch(t){return!0}return t instanceof WeakMap}catch(t){}return!1}(e))return Y("WeakMap");if(function(t){if(!p||!t||"object"!=typeof t)return!1;try{p.call(t,p);try{f.call(t,f)}catch(t){return!0}return t instanceof WeakSet}catch(t){}return!1}(e))return Y("WeakSet");if(function(t){if(!y||!t||"object"!=typeof t)return!1;try{return y.call(t),!0}catch(t){}return!1}(e))return Y("WeakRef");if(function(t){return!("[object Number]"!==H(t)||I&&"object"==typeof t&&I in t)}(e))return K(X(Number(e)));if(function(t){if(!t||"object"!=typeof t||!T)return!1;try{return T.call(t),!0}catch(t){}return!1}(e))return K(X(T.call(e)));if(function(t){return!("[object Boolean]"!==H(t)||I&&"object"==typeof t&&I in t)}(e))return K(d.call(e));if(function(t){return!("[object String]"!==H(t)||I&&"object"==typeof t&&I in t)}(e))return K(X(String(e)));if("undefined"!=typeof window&&e===window)return"{ [object Window] }";if(e===r.g)return"{ [object globalThis] }";if(!function(t){return!("[object Date]"!==H(t)||I&&"object"==typeof t&&I in t)}(e)&&!W(e)){var lt=Z(e,X),ft=M?M(e)===Object.prototype:e instanceof Object||e.constructor===Object,pt=e instanceof Object?"":"null prototype",yt=!ft&&I&&Object(e)===e&&I in e?b.call(H(e),8,-1):pt?"Object":"",dt=(ft||"function"!=typeof e.constructor?"":e.constructor.name?e.constructor.name+" ":"")+(yt||pt?"["+A.call(E.call([],yt||[],pt||[]),": ")+"] ":"");return 0===lt.length?dt+"{}":z?dt+"{"+Q(lt,z)+"}":dt+"{ "+A.call(lt,", ")+" }"}return String(e)};var z=Object.prototype.hasOwnProperty||function(t){return t in this};function G(t,e){return z.call(t,e)}function H(t){return h.call(t)}function V(t,e){if(t.indexOf)return t.indexOf(e);for(var r=0,n=t.length;re.maxStringLength){var r=t.length-e.maxStringLength,n="... "+r+" more character"+(r>1?"s":"");return q(b.call(t,0,e.maxStringLength),e)+n}return L(v.call(v.call(t,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,X),"single",e)}function X(t){var e=t.charCodeAt(0),r={8:"b",9:"t",10:"n",12:"f",13:"r"}[e];return r?"\\"+r:"\\x"+(e<16?"0":"")+w.call(e.toString(16))}function K(t){return"Object("+t+")"}function Y(t){return t+" { ? }"}function J(t,e,r,n){return t+" ("+e+") {"+(n?Q(r,n):A.call(r,", "))+"}"}function Q(t,e){if(0===t.length)return"";var r="\n"+e.prev+e.base;return r+A.call(t,","+r)+"\n"+e.prev}function Z(t,e){var r=_(t),n=[];if(r){n.length=t.length;for(var o=0;o{"use strict";var n;if(!Object.keys){var o=Object.prototype.hasOwnProperty,i=Object.prototype.toString,a=r(9096),u=Object.prototype.propertyIsEnumerable,c=!u.call({toString:null},"toString"),s=u.call((function(){}),"prototype"),l=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],f=function(t){var e=t.constructor;return e&&e.prototype===t},p={$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},y=function(){if("undefined"==typeof window)return!1;for(var t in window)try{if(!p["$"+t]&&o.call(window,t)&&null!==window[t]&&"object"==typeof window[t])try{f(window[t])}catch(t){return!0}}catch(t){return!0}return!1}();n=function(t){var e=null!==t&&"object"==typeof t,r="[object Function]"===i.call(t),n=a(t),u=e&&"[object String]"===i.call(t),p=[];if(!e&&!r&&!n)throw new TypeError("Object.keys called on a non-object");var d=s&&r;if(u&&t.length>0&&!o.call(t,0))for(var h=0;h0)for(var g=0;g{"use strict";var n=Array.prototype.slice,o=r(9096),i=Object.keys,a=i?function(t){return i(t)}:r(9560),u=Object.keys;a.shim=function(){if(Object.keys){var t=function(){var t=Object.keys(arguments);return t&&t.length===arguments.length}(1,2);t||(Object.keys=function(t){return o(t)?u(n.call(t)):u(t)})}else Object.keys=a;return Object.keys||a},t.exports=a},9096:t=>{"use strict";var e=Object.prototype.toString;t.exports=function(t){var r=e.call(t),n="[object Arguments]"===r;return n||(n="[object Array]"!==r&&null!==t&&"object"==typeof t&&"number"==typeof t.length&&t.length>=0&&"[object Function]"===e.call(t.callee)),n}},7636:(t,e,r)=>{"use strict";var n=r(6308),o=r(2824),i=Object;t.exports=n((function(){if(null==this||this!==i(this))throw new o("RegExp.prototype.flags getter called on non-object");var t="";return this.hasIndices&&(t+="d"),this.global&&(t+="g"),this.ignoreCase&&(t+="i"),this.multiline&&(t+="m"),this.dotAll&&(t+="s"),this.unicode&&(t+="u"),this.unicodeSets&&(t+="v"),this.sticky&&(t+="y"),t}),"get flags",!0)},2192:(t,e,r)=>{"use strict";var n=r(2732),o=r(5096),i=r(7636),a=r(9296),u=r(736),c=o(a());n(c,{getPolyfill:a,implementation:i,shim:u}),t.exports=c},9296:(t,e,r)=>{"use strict";var n=r(7636),o=r(2732).supportsDescriptors,i=Object.getOwnPropertyDescriptor;t.exports=function(){if(o&&"gim"===/a/gim.flags){var t=i(RegExp.prototype,"flags");if(t&&"function"==typeof t.get&&"boolean"==typeof RegExp.prototype.dotAll&&"boolean"==typeof RegExp.prototype.hasIndices){var e="",r={};if(Object.defineProperty(r,"hasIndices",{get:function(){e+="d"}}),Object.defineProperty(r,"sticky",{get:function(){e+="y"}}),"dy"===e)return t.get}}return n}},736:(t,e,r)=>{"use strict";var n=r(2732).supportsDescriptors,o=r(9296),i=Object.getOwnPropertyDescriptor,a=Object.defineProperty,u=TypeError,c=Object.getPrototypeOf,s=/a/;t.exports=function(){if(!n||!c)throw new u("RegExp.prototype.flags requires a true ES5 environment that supports property descriptors");var t=o(),e=c(s),r=i(e,"flags");return r&&r.get===t||a(e,"flags",{configurable:!0,enumerable:!1,get:t}),t}},860:(t,e,r)=>{"use strict";var n=r(668),o=r(1476),i=n("RegExp.prototype.exec"),a=r(2824);t.exports=function(t){if(!o(t))throw new a("`regex` must be a RegExp");return function(e){return null!==i(t,e)}}},5676:(t,e,r)=>{"use strict";var n=r(4624),o=r(2448),i=r(3268)(),a=r(6168),u=r(2824),c=n("%Math.floor%");t.exports=function(t,e){if("function"!=typeof t)throw new u("`fn` is not a function");if("number"!=typeof e||e<0||e>4294967295||c(e)!==e)throw new u("`length` must be a positive 32-bit integer");var r=arguments.length>2&&!!arguments[2],n=!0,s=!0;if("length"in t&&a){var l=a(t,"length");l&&!l.configurable&&(n=!1),l&&!l.writable&&(s=!1)}return(n||s||!r)&&(i?o(t,"length",e,!0,!0):o(t,"length",e)),t}},6308:(t,e,r)=>{"use strict";var n=r(2448),o=r(3268)(),i=r(2656).functionsHaveConfigurableNames(),a=TypeError;t.exports=function(t,e){if("function"!=typeof t)throw new a("`fn` is not a function");return arguments.length>2&&!!arguments[2]&&!i||(o?n(t,"name",e,!0,!0):n(t,"name",e)),t}},3147:(t,e,r)=>{"use strict";var n=r(4624),o=r(668),i=r(4152),a=r(2824),u=n("%WeakMap%",!0),c=n("%Map%",!0),s=o("WeakMap.prototype.get",!0),l=o("WeakMap.prototype.set",!0),f=o("WeakMap.prototype.has",!0),p=o("Map.prototype.get",!0),y=o("Map.prototype.set",!0),d=o("Map.prototype.has",!0),h=function(t,e){for(var r,n=t;null!==(r=n.next);n=r)if(r.key===e)return n.next=r.next,r.next=t.next,t.next=r,r};t.exports=function(){var t,e,r,n={assert:function(t){if(!n.has(t))throw new a("Side channel does not contain "+i(t))},get:function(n){if(u&&n&&("object"==typeof n||"function"==typeof n)){if(t)return s(t,n)}else if(c){if(e)return p(e,n)}else if(r)return function(t,e){var r=h(t,e);return r&&r.value}(r,n)},has:function(n){if(u&&n&&("object"==typeof n||"function"==typeof n)){if(t)return f(t,n)}else if(c){if(e)return d(e,n)}else if(r)return function(t,e){return!!h(t,e)}(r,n);return!1},set:function(n,o){u&&n&&("object"==typeof n||"function"==typeof n)?(t||(t=new u),l(t,n,o)):c?(e||(e=new c),y(e,n,o)):(r||(r={key:{},next:null}),function(t,e,r){var n=h(t,e);n?n.value=r:t.next={key:e,next:t.next,value:r}}(r,n,o))}};return n}},9508:(t,e,r)=>{"use strict";var n=r(1700),o=r(3672),i=r(5552),a=r(3816),u=r(5424),c=r(4656),s=r(668),l=r(9800)(),f=r(2192),p=s("String.prototype.indexOf"),y=r(6288),d=function(t){var e=y();if(l&&"symbol"==typeof Symbol.matchAll){var r=i(t,Symbol.matchAll);return r===RegExp.prototype[Symbol.matchAll]&&r!==e?e:r}if(a(t))return e};t.exports=function(t){var e=c(this);if(null!=t){if(a(t)){var r="flags"in t?o(t,"flags"):f(t);if(c(r),p(u(r),"g")<0)throw new TypeError("matchAll requires a global regular expression")}var i=d(t);if(void 0!==i)return n(i,t,[e])}var s=u(e),l=new RegExp(t,"g");return n(d(l),l,[s])}},3732:(t,e,r)=>{"use strict";var n=r(5096),o=r(2732),i=r(9508),a=r(5844),u=r(4148),c=n(i);o(c,{getPolyfill:a,implementation:i,shim:u}),t.exports=c},6288:(t,e,r)=>{"use strict";var n=r(9800)(),o=r(7492);t.exports=function(){return n&&"symbol"==typeof Symbol.matchAll&&"function"==typeof RegExp.prototype[Symbol.matchAll]?RegExp.prototype[Symbol.matchAll]:o}},5844:(t,e,r)=>{"use strict";var n=r(9508);t.exports=function(){if(String.prototype.matchAll)try{"".matchAll(RegExp.prototype)}catch(t){return String.prototype.matchAll}return n}},7492:(t,e,r)=>{"use strict";var n=r(5211),o=r(3672),i=r(4e3),a=r(8652),u=r(4784),c=r(5424),s=r(8645),l=r(2192),f=r(6308),p=r(668)("String.prototype.indexOf"),y=RegExp,d="flags"in RegExp.prototype,h=f((function(t){var e=this;if("Object"!==s(e))throw new TypeError('"this" value must be an Object');var r=c(t),f=function(t,e){var r="flags"in e?o(e,"flags"):c(l(e));return{flags:r,matcher:new t(d&&"string"==typeof r?e:t===y?e.source:e,r)}}(a(e,y),e),h=f.flags,g=f.matcher,m=u(o(e,"lastIndex"));i(g,"lastIndex",m,!0);var b=p(h,"g")>-1,v=p(h,"u")>-1;return n(g,r,b,v)}),"[Symbol.matchAll]",!0);t.exports=h},4148:(t,e,r)=>{"use strict";var n=r(2732),o=r(9800)(),i=r(5844),a=r(6288),u=Object.defineProperty,c=Object.getOwnPropertyDescriptor;t.exports=function(){var t=i();if(n(String.prototype,{matchAll:t},{matchAll:function(){return String.prototype.matchAll!==t}}),o){var e=Symbol.matchAll||(Symbol.for?Symbol.for("Symbol.matchAll"):Symbol("Symbol.matchAll"));if(n(Symbol,{matchAll:e},{matchAll:function(){return Symbol.matchAll!==e}}),u&&c){var r=c(Symbol,e);r&&!r.configurable||u(Symbol,e,{configurable:!1,enumerable:!1,value:e,writable:!1})}var s=a(),l={};l[e]=s;var f={};f[e]=function(){return RegExp.prototype[e]!==s},n(RegExp.prototype,l,f)}return t}},6936:(t,e,r)=>{"use strict";var n=r(4656),o=r(5424),i=r(668)("String.prototype.replace"),a=/^\s$/.test("᠎"),u=a?/^[\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]+/,c=a?/[\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]+$/;t.exports=function(){var t=o(n(this));return i(i(t,u,""),c,"")}},9292:(t,e,r)=>{"use strict";var n=r(5096),o=r(2732),i=r(4656),a=r(6936),u=r(6684),c=r(9788),s=n(u()),l=function(t){return i(t),s(t)};o(l,{getPolyfill:u,implementation:a,shim:c}),t.exports=l},6684:(t,e,r)=>{"use strict";var n=r(6936);t.exports=function(){return String.prototype.trim&&"​"==="​".trim()&&"᠎"==="᠎".trim()&&"_᠎"==="_᠎".trim()&&"᠎_"==="᠎_".trim()?String.prototype.trim:n}},9788:(t,e,r)=>{"use strict";var n=r(2732),o=r(6684);t.exports=function(){var t=o();return n(String.prototype,{trim:t},{trim:function(){return String.prototype.trim!==t}}),t}},1740:()=>{},1056:(t,e,r)=>{"use strict";var n=r(4624),o=r(8536),i=r(8645),a=r(7724),u=r(9132),c=n("%TypeError%");t.exports=function(t,e,r){if("String"!==i(t))throw new c("Assertion failed: `S` must be a String");if(!a(e)||e<0||e>u)throw new c("Assertion failed: `length` must be an integer >= 0 and <= 2**53");if("Boolean"!==i(r))throw new c("Assertion failed: `unicode` must be a Boolean");return r?e+1>=t.length?e+1:e+o(t,e)["[[CodeUnitCount]]"]:e+1}},1700:(t,e,r)=>{"use strict";var n=r(4624),o=r(668),i=n("%TypeError%"),a=r(1720),u=n("%Reflect.apply%",!0)||o("Function.prototype.apply");t.exports=function(t,e){var r=arguments.length>2?arguments[2]:[];if(!a(r))throw new i("Assertion failed: optional `argumentsList`, if provided, must be a List");return u(t,e,r)}},8536:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(668),i=r(1712),a=r(8444),u=r(8645),c=r(2320),s=o("String.prototype.charAt"),l=o("String.prototype.charCodeAt");t.exports=function(t,e){if("String"!==u(t))throw new n("Assertion failed: `string` must be a String");var r=t.length;if(e<0||e>=r)throw new n("Assertion failed: `position` must be >= 0, and < the length of `string`");var o=l(t,e),f=s(t,e),p=i(o),y=a(o);if(!p&&!y)return{"[[CodePoint]]":f,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!1};if(y||e+1===r)return{"[[CodePoint]]":f,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!0};var d=l(t,e+1);return a(d)?{"[[CodePoint]]":c(o,d),"[[CodeUnitCount]]":2,"[[IsUnpairedSurrogate]]":!1}:{"[[CodePoint]]":f,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!0}}},4288:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(8645);t.exports=function(t,e){if("Boolean"!==o(e))throw new n("Assertion failed: Type(done) is not Boolean");return{value:t,done:e}}},2672:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(4436),i=r(8924),a=r(3880),u=r(2968),c=r(8800),s=r(8645);t.exports=function(t,e,r){if("Object"!==s(t))throw new n("Assertion failed: Type(O) is not Object");if(!u(e))throw new n("Assertion failed: IsPropertyKey(P) is not true");return o(a,c,i,t,e,{"[[Configurable]]":!0,"[[Enumerable]]":!1,"[[Value]]":r,"[[Writable]]":!0})}},5211:(t,e,r)=>{"use strict";var n=r(4624),o=r(9800)(),i=n("%TypeError%"),a=n("%IteratorPrototype%",!0),u=r(1056),c=r(4288),s=r(2672),l=r(3672),f=r(6216),p=r(8972),y=r(4e3),d=r(4784),h=r(5424),g=r(8645),m=r(7284),b=r(9200),v=function(t,e,r,n){if("String"!==g(e))throw new i("`S` must be a string");if("Boolean"!==g(r))throw new i("`global` must be a boolean");if("Boolean"!==g(n))throw new i("`fullUnicode` must be a boolean");m.set(this,"[[IteratingRegExp]]",t),m.set(this,"[[IteratedString]]",e),m.set(this,"[[Global]]",r),m.set(this,"[[Unicode]]",n),m.set(this,"[[Done]]",!1)};a&&(v.prototype=f(a)),s(v.prototype,"next",(function(){var t=this;if("Object"!==g(t))throw new i("receiver must be an object");if(!(t instanceof v&&m.has(t,"[[IteratingRegExp]]")&&m.has(t,"[[IteratedString]]")&&m.has(t,"[[Global]]")&&m.has(t,"[[Unicode]]")&&m.has(t,"[[Done]]")))throw new i('"this" value must be a RegExpStringIterator instance');if(m.get(t,"[[Done]]"))return c(void 0,!0);var e=m.get(t,"[[IteratingRegExp]]"),r=m.get(t,"[[IteratedString]]"),n=m.get(t,"[[Global]]"),o=m.get(t,"[[Unicode]]"),a=p(e,r);if(null===a)return m.set(t,"[[Done]]",!0),c(void 0,!0);if(n){if(""===h(l(a,"0"))){var s=d(l(e,"lastIndex")),f=u(r,s,o);y(e,"lastIndex",f,!0)}return c(a,!1)}return m.set(t,"[[Done]]",!0),c(a,!1)})),o&&(b(v.prototype,"RegExp String Iterator"),Symbol.iterator&&"function"!=typeof v.prototype[Symbol.iterator])&&s(v.prototype,Symbol.iterator,(function(){return this})),t.exports=function(t,e,r,n){return new v(t,e,r,n)}},7268:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(320),i=r(4436),a=r(8924),u=r(4936),c=r(3880),s=r(2968),l=r(8800),f=r(5696),p=r(8645);t.exports=function(t,e,r){if("Object"!==p(t))throw new n("Assertion failed: Type(O) is not Object");if(!s(e))throw new n("Assertion failed: IsPropertyKey(P) is not true");var y=o({Type:p,IsDataDescriptor:c,IsAccessorDescriptor:u},r)?r:f(r);if(!o({Type:p,IsDataDescriptor:c,IsAccessorDescriptor:u},y))throw new n("Assertion failed: Desc is not a valid Property Descriptor");return i(c,l,a,t,e,y)}},8924:(t,e,r)=>{"use strict";var n=r(3600),o=r(3504),i=r(8645);t.exports=function(t){return void 0!==t&&n(i,"Property Descriptor","Desc",t),o(t)}},3672:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(4152),i=r(2968),a=r(8645);t.exports=function(t,e){if("Object"!==a(t))throw new n("Assertion failed: Type(O) is not Object");if(!i(e))throw new n("Assertion failed: IsPropertyKey(P) is not true, got "+o(e));return t[e]}},5552:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(3396),i=r(3048),a=r(2968),u=r(4152);t.exports=function(t,e){if(!a(e))throw new n("Assertion failed: IsPropertyKey(P) is not true");var r=o(t,e);if(null!=r){if(!i(r))throw new n(u(e)+" is not a function: "+u(r));return r}}},3396:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(4152),i=r(2968);t.exports=function(t,e){if(!i(e))throw new n("Assertion failed: IsPropertyKey(P) is not true, got "+o(e));return t[e]}},4936:(t,e,r)=>{"use strict";var n=r(4440),o=r(8645),i=r(3600);t.exports=function(t){return void 0!==t&&(i(o,"Property Descriptor","Desc",t),!(!n(t,"[[Get]]")&&!n(t,"[[Set]]")))}},1720:(t,e,r)=>{"use strict";t.exports=r(704)},3048:(t,e,r)=>{"use strict";t.exports=r(648)},211:(t,e,r)=>{"use strict";var n=r(8600)("%Reflect.construct%",!0),o=r(7268);try{o({},"",{"[[Get]]":function(){}})}catch(t){o=null}if(o&&n){var i={},a={};o(a,"length",{"[[Get]]":function(){throw i},"[[Enumerable]]":!0}),t.exports=function(t){try{n(t,a)}catch(t){return t===i}}}else t.exports=function(t){return"function"==typeof t&&!!t.prototype}},3880:(t,e,r)=>{"use strict";var n=r(4440),o=r(8645),i=r(3600);t.exports=function(t){return void 0!==t&&(i(o,"Property Descriptor","Desc",t),!(!n(t,"[[Value]]")&&!n(t,"[[Writable]]")))}},2968:t=>{"use strict";t.exports=function(t){return"string"==typeof t||"symbol"==typeof t}},3816:(t,e,r)=>{"use strict";var n=r(4624)("%Symbol.match%",!0),o=r(1476),i=r(6848);t.exports=function(t){if(!t||"object"!=typeof t)return!1;if(n){var e=t[n];if(void 0!==e)return i(e)}return o(t)}},6216:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Object.create%",!0),i=n("%TypeError%"),a=n("%SyntaxError%"),u=r(1720),c=r(8645),s=r(4672),l=r(7284),f=r(7e3)();t.exports=function(t){if(null!==t&&"Object"!==c(t))throw new i("Assertion failed: `proto` must be null or an object");var e,r=arguments.length<2?[]:arguments[1];if(!u(r))throw new i("Assertion failed: `additionalInternalSlotsList` must be an Array");if(o)e=o(t);else if(f)e={__proto__:t};else{if(null===t)throw new a("native Object.create support is required to create null objects");var n=function(){};n.prototype=t,e=new n}return r.length>0&&s(r,(function(t){l.set(e,t,void 0)})),e}},8972:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(668)("RegExp.prototype.exec"),i=r(1700),a=r(3672),u=r(3048),c=r(8645);t.exports=function(t,e){if("Object"!==c(t))throw new n("Assertion failed: `R` must be an Object");if("String"!==c(e))throw new n("Assertion failed: `S` must be a String");var r=a(t,"exec");if(u(r)){var s=i(r,t,[e]);if(null===s||"Object"===c(s))return s;throw new n('"exec" method must return `null` or an Object')}return o(t,e)}},4656:(t,e,r)=>{"use strict";t.exports=r(176)},8800:(t,e,r)=>{"use strict";var n=r(2808);t.exports=function(t,e){return t===e?0!==t||1/t==1/e:n(t)&&n(e)}},4e3:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(2968),i=r(8800),a=r(8645),u=function(){try{return delete[].length,!0}catch(t){return!1}}();t.exports=function(t,e,r,c){if("Object"!==a(t))throw new n("Assertion failed: `O` must be an Object");if(!o(e))throw new n("Assertion failed: `P` must be a Property Key");if("Boolean"!==a(c))throw new n("Assertion failed: `Throw` must be a Boolean");if(c){if(t[e]=r,u&&!i(t[e],r))throw new n("Attempted to assign to readonly property.");return!0}try{return t[e]=r,!u||i(t[e],r)}catch(t){return!1}}},8652:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Symbol.species%",!0),i=n("%TypeError%"),a=r(211),u=r(8645);t.exports=function(t,e){if("Object"!==u(t))throw new i("Assertion failed: Type(O) is not Object");var r=t.constructor;if(void 0===r)return e;if("Object"!==u(r))throw new i("O.constructor is not an Object");var n=o?r[o]:void 0;if(null==n)return e;if(a(n))return n;throw new i("no constructor found")}},8772:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Number%"),i=n("%RegExp%"),a=n("%TypeError%"),u=n("%parseInt%"),c=r(668),s=r(860),l=c("String.prototype.slice"),f=s(/^0b[01]+$/i),p=s(/^0o[0-7]+$/i),y=s(/^[-+]0x[0-9a-f]+$/i),d=s(new i("["+["…","​","￾"].join("")+"]","g")),h=r(9292),g=r(8645);t.exports=function t(e){if("String"!==g(e))throw new a("Assertion failed: `argument` is not a String");if(f(e))return o(u(l(e,2),2));if(p(e))return o(u(l(e,2),8));if(d(e)||y(e))return NaN;var r=h(e);return r!==e?t(r):o(e)}},6848:t=>{"use strict";t.exports=function(t){return!!t}},9424:(t,e,r)=>{"use strict";var n=r(7220),o=r(2592),i=r(2808),a=r(2931);t.exports=function(t){var e=n(t);return i(e)||0===e?0:a(e)?o(e):e}},4784:(t,e,r)=>{"use strict";var n=r(9132),o=r(9424);t.exports=function(t){var e=o(t);return e<=0?0:e>n?n:e}},7220:(t,e,r)=>{"use strict";var n=r(4624),o=n("%TypeError%"),i=n("%Number%"),a=r(2336),u=r(5556),c=r(8772);t.exports=function(t){var e=a(t)?t:u(t,i);if("symbol"==typeof e)throw new o("Cannot convert a Symbol value to a number");if("bigint"==typeof e)throw new o("Conversion from 'BigInt' to 'number' is not allowed.");return"string"==typeof e?c(e):i(e)}},5556:(t,e,r)=>{"use strict";var n=r(108);t.exports=function(t){return arguments.length>1?n(t,arguments[1]):n(t)}},5696:(t,e,r)=>{"use strict";var n=r(4440),o=r(4624)("%TypeError%"),i=r(8645),a=r(6848),u=r(3048);t.exports=function(t){if("Object"!==i(t))throw new o("ToPropertyDescriptor requires an object");var e={};if(n(t,"enumerable")&&(e["[[Enumerable]]"]=a(t.enumerable)),n(t,"configurable")&&(e["[[Configurable]]"]=a(t.configurable)),n(t,"value")&&(e["[[Value]]"]=t.value),n(t,"writable")&&(e["[[Writable]]"]=a(t.writable)),n(t,"get")){var r=t.get;if(void 0!==r&&!u(r))throw new o("getter must be a function");e["[[Get]]"]=r}if(n(t,"set")){var c=t.set;if(void 0!==c&&!u(c))throw new o("setter must be a function");e["[[Set]]"]=c}if((n(e,"[[Get]]")||n(e,"[[Set]]"))&&(n(e,"[[Value]]")||n(e,"[[Writable]]")))throw new o("Invalid property descriptor. Cannot both specify accessors and a value or writable attribute");return e}},5424:(t,e,r)=>{"use strict";var n=r(4624),o=n("%String%"),i=n("%TypeError%");t.exports=function(t){if("symbol"==typeof t)throw new i("Cannot convert a Symbol value to a string");return o(t)}},8645:(t,e,r)=>{"use strict";var n=r(7936);t.exports=function(t){return"symbol"==typeof t?"Symbol":"bigint"==typeof t?"BigInt":n(t)}},2320:(t,e,r)=>{"use strict";var n=r(4624),o=n("%TypeError%"),i=n("%String.fromCharCode%"),a=r(1712),u=r(8444);t.exports=function(t,e){if(!a(t)||!u(e))throw new o("Assertion failed: `lead` must be a leading surrogate char code, and `trail` must be a trailing surrogate char code");return i(t)+i(e)}},2312:(t,e,r)=>{"use strict";var n=r(8645),o=Math.floor;t.exports=function(t){return"BigInt"===n(t)?t:o(t)}},2592:(t,e,r)=>{"use strict";var n=r(4624),o=r(2312),i=n("%TypeError%");t.exports=function(t){if("number"!=typeof t&&"bigint"!=typeof t)throw new i("argument must be a Number or a BigInt");var e=t<0?-o(-t):o(t);return 0===e?0:e}},176:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%");t.exports=function(t,e){if(null==t)throw new n(e||"Cannot call method on "+t);return t}},7936:t=>{"use strict";t.exports=function(t){return null===t?"Null":void 0===t?"Undefined":"function"==typeof t||"object"==typeof t?"Object":"number"==typeof t?"Number":"boolean"==typeof t?"Boolean":"string"==typeof t?"String":void 0}},8600:(t,e,r)=>{"use strict";t.exports=r(4624)},4436:(t,e,r)=>{"use strict";var n=r(3268),o=r(4624),i=n()&&o("%Object.defineProperty%",!0),a=n.hasArrayLengthDefineBug(),u=a&&r(704),c=r(668)("Object.prototype.propertyIsEnumerable");t.exports=function(t,e,r,n,o,s){if(!i){if(!t(s))return!1;if(!s["[[Configurable]]"]||!s["[[Writable]]"])return!1;if(o in n&&c(n,o)!==!!s["[[Enumerable]]"])return!1;var l=s["[[Value]]"];return n[o]=l,e(n[o],l)}return a&&"length"===o&&"[[Value]]"in s&&u(n)&&n.length!==s["[[Value]]"]?(n.length=s["[[Value]]"],n.length===s["[[Value]]"]):(i(n,o,r(s)),!0)}},704:(t,e,r)=>{"use strict";var n=r(4624)("%Array%"),o=!n.isArray&&r(668)("Object.prototype.toString");t.exports=n.isArray||function(t){return"[object Array]"===o(t)}},3600:(t,e,r)=>{"use strict";var n=r(4624),o=n("%TypeError%"),i=n("%SyntaxError%"),a=r(4440),u=r(7724),c={"Property Descriptor":function(t){var e={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};if(!t)return!1;for(var r in t)if(a(t,r)&&!e[r])return!1;var n=a(t,"[[Value]]"),i=a(t,"[[Get]]")||a(t,"[[Set]]");if(n&&i)throw new o("Property Descriptors may not be both accessor and data descriptors");return!0},"Match Record":r(5092),"Iterator Record":function(t){return a(t,"[[Iterator]]")&&a(t,"[[NextMethod]]")&&a(t,"[[Done]]")},"PromiseCapability Record":function(t){return!!t&&a(t,"[[Resolve]]")&&"function"==typeof t["[[Resolve]]"]&&a(t,"[[Reject]]")&&"function"==typeof t["[[Reject]]"]&&a(t,"[[Promise]]")&&t["[[Promise]]"]&&"function"==typeof t["[[Promise]]"].then},"AsyncGeneratorRequest Record":function(t){return!!t&&a(t,"[[Completion]]")&&a(t,"[[Capability]]")&&c["PromiseCapability Record"](t["[[Capability]]"])},"RegExp Record":function(t){return t&&a(t,"[[IgnoreCase]]")&&"boolean"==typeof t["[[IgnoreCase]]"]&&a(t,"[[Multiline]]")&&"boolean"==typeof t["[[Multiline]]"]&&a(t,"[[DotAll]]")&&"boolean"==typeof t["[[DotAll]]"]&&a(t,"[[Unicode]]")&&"boolean"==typeof t["[[Unicode]]"]&&a(t,"[[CapturingGroupsCount]]")&&"number"==typeof t["[[CapturingGroupsCount]]"]&&u(t["[[CapturingGroupsCount]]"])&&t["[[CapturingGroupsCount]]"]>=0}};t.exports=function(t,e,r,n){var a=c[e];if("function"!=typeof a)throw new i("unknown record type: "+e);if("Object"!==t(n)||!a(n))throw new o(r+" must be a "+e)}},4672:t=>{"use strict";t.exports=function(t,e){for(var r=0;r{"use strict";t.exports=function(t){if(void 0===t)return t;var e={};return"[[Value]]"in t&&(e.value=t["[[Value]]"]),"[[Writable]]"in t&&(e.writable=!!t["[[Writable]]"]),"[[Get]]"in t&&(e.get=t["[[Get]]"]),"[[Set]]"in t&&(e.set=t["[[Set]]"]),"[[Enumerable]]"in t&&(e.enumerable=!!t["[[Enumerable]]"]),"[[Configurable]]"in t&&(e.configurable=!!t["[[Configurable]]"]),e}},2931:(t,e,r)=>{"use strict";var n=r(2808);t.exports=function(t){return("number"==typeof t||"bigint"==typeof t)&&!n(t)&&t!==1/0&&t!==-1/0}},7724:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Math.abs%"),i=n("%Math.floor%"),a=r(2808),u=r(2931);t.exports=function(t){if("number"!=typeof t||a(t)||!u(t))return!1;var e=o(t);return i(e)===e}},1712:t=>{"use strict";t.exports=function(t){return"number"==typeof t&&t>=55296&&t<=56319}},5092:(t,e,r)=>{"use strict";var n=r(4440);t.exports=function(t){return n(t,"[[StartIndex]]")&&n(t,"[[EndIndex]]")&&t["[[StartIndex]]"]>=0&&t["[[EndIndex]]"]>=t["[[StartIndex]]"]&&String(parseInt(t["[[StartIndex]]"],10))===String(t["[[StartIndex]]"])&&String(parseInt(t["[[EndIndex]]"],10))===String(t["[[EndIndex]]"])}},2808:t=>{"use strict";t.exports=Number.isNaN||function(t){return t!=t}},2336:t=>{"use strict";t.exports=function(t){return null===t||"function"!=typeof t&&"object"!=typeof t}},320:(t,e,r)=>{"use strict";var n=r(4624),o=r(4440),i=n("%TypeError%");t.exports=function(t,e){if("Object"!==t.Type(e))return!1;var r={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};for(var n in e)if(o(e,n)&&!r[n])return!1;if(t.IsDataDescriptor(e)&&t.IsAccessorDescriptor(e))throw new i("Property Descriptors may not be both accessor and data descriptors");return!0}},8444:t=>{"use strict";t.exports=function(t){return"number"==typeof t&&t>=56320&&t<=57343}},9132:t=>{"use strict";t.exports=Number.MAX_SAFE_INTEGER||9007199254740991}},e={};function r(n){var o=e[n];if(void 0!==o)return o.exports;var i=e[n]={exports:{}};return t[n](i,i.exports,r),i.exports}r.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return r.d(e,{a:e}),e},r.d=(t,e)=>{for(var n in e)r.o(e,n)&&!r.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";var t=r(9116);function e(e,r,n){let o=0,i=[];for(;-1!==o;)o=e.indexOf(r,o),-1!==o&&(i.push({start:o,end:o+r.length,errors:0}),o+=1);return i.length>0?i:(0,t.c)(e,r,n)}function n(t,r){return 0===r.length||0===t.length?0:1-e(t,r,r.length)[0].errors/r.length}function o(t){switch(t.nodeType){case Node.ELEMENT_NODE:case Node.TEXT_NODE:return t.textContent.length;default:return 0}}function i(t){let e=t.previousSibling,r=0;for(;e;)r+=o(e),e=e.previousSibling;return r}function a(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),n=1;no?(a.push({node:u,offset:o-s}),o=r.shift()):(c=i.nextNode(),s+=u.data.length);for(;void 0!==o&&u&&s===o;)a.push({node:u,offset:u.data.length}),o=r.shift();if(void 0!==o)throw new RangeError("Offset exceeds text length");return a}class u{constructor(t,e){if(e<0)throw new Error("Offset is invalid");this.element=t,this.offset=e}relativeTo(t){if(!t.contains(this.element))throw new Error("Parent is not an ancestor of current element");let e=this.element,r=this.offset;for(;e!==t;)r+=i(e),e=e.parentElement;return new u(e,r)}resolve(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};try{return a(this.element,this.offset)[0]}catch(e){if(0===this.offset&&void 0!==t.direction){const r=document.createTreeWalker(this.element.getRootNode(),NodeFilter.SHOW_TEXT);r.currentNode=this.element;const n=1===t.direction,o=n?r.nextNode():r.previousNode();if(!o)throw e;return{node:o,offset:n?0:o.data.length}}throw e}}static fromCharOffset(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")}}static fromPoint(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");const r=i(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");let r=0;for(let n=0;n2&&void 0!==arguments[2]?arguments[2]:{};this.root=t,this.exact=e,this.context=r}static fromRange(t,e){const r=t.textContent,n=c.fromRange(e).relativeTo(t),o=n.start.offset,i=n.end.offset;return new l(t,r.slice(o,i),{prefix:r.slice(Math.max(0,o-32),o),suffix:r.slice(i,Math.min(r.length,i+32))})}static fromSelector(t,e){const{prefix:r,suffix:n}=e;return new l(t,e.exact,{prefix:r,suffix:n})}toSelector(){return{type:"TextQuoteSelector",exact:this.exact,prefix:this.context.prefix,suffix:this.context.suffix}}toRange(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.toPositionAnchor(t).toRange()}toPositionAnchor(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const r=function(t,r){let o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(0===r.length)return null;const i=Math.min(256,r.length/2),a=e(t,r,i);if(0===a.length)return null;const u=e=>{const i=1-e.errors/r.length,a=o.prefix?n(t.slice(Math.max(0,e.start-o.prefix.length),e.start),o.prefix):1,u=o.suffix?n(t.slice(e.end,e.end+o.suffix.length),o.suffix):1;let c=1;return"number"==typeof o.hint&&(c=1-Math.abs(e.start-o.hint)/t.length),(50*i+20*a+20*u+2*c)/92},c=a.map((t=>({start:t.start,end:t.end,score:u(t)})));return c.sort(((t,e)=>e.score-t.score)),c[0]}(this.root.textContent,this.exact,{...this.context,hint:t.hint});if(!r)throw new Error("Quote not found");return new s(this.root,r.start,r.end)}}var f=r(3732);r.n(f)().shim();const p=!0;function y(){if(!readium.link)return null;const t=readium.link.href;if(!t)return null;const e=function(){const t=window.getSelection();if(!t)return;if(t.isCollapsed)return;const e=t.toString();if(0===e.trim().replace(/\n/g," ").replace(/\s\s+/g," ").length)return;if(!t.anchorNode||!t.focusNode)return;const r=1===t.rangeCount?t.getRangeAt(0):function(t,e,r,n){const o=new Range;if(o.setStart(t,e),o.setEnd(r,n),!o.collapsed)return o;d(">>> createOrderedRange COLLAPSED ... RANGE REVERSE?");const i=new Range;if(i.setStart(r,n),i.setEnd(t,e),!i.collapsed)return d(">>> createOrderedRange RANGE REVERSE OK."),o;d(">>> createOrderedRange RANGE REVERSE ALSO COLLAPSED?!")}(t.anchorNode,t.anchorOffset,t.focusNode,t.focusOffset);if(!r||r.collapsed)return void d("$$$$$$$$$$$$$$$$$ CANNOT GET NON-COLLAPSED SELECTION RANGE?!");const n=document.body.textContent,o=c.fromRange(r).relativeTo(document.body),i=o.start.offset,a=o.end.offset;let u=n.slice(Math.max(0,i-200),i),s=u.search(/\P{L}\p{L}/gu);-1!==s&&(u=u.slice(s+1));let l=n.slice(a,Math.min(n.length,a+200)),f=Array.from(l.matchAll(/\p{L}\P{L}/gu)).pop();return void 0!==f&&f.index>1&&(l=l.slice(0,f.index+1)),{highlight:e,before:u,after:l}}();return e?{href:t,text:e,rect:function(){try{let t=window.getSelection();if(!t)return;return $(t.getRangeAt(0).getBoundingClientRect())}catch(t){return N(t),null}}()}:null}function d(){p&&C.apply(null,arguments)}var h;window.addEventListener("error",(function(t){webkit.messageHandlers.logError.postMessage({message:t.message,filename:t.filename,line:t.lineno})}),!1),window.addEventListener("load",(function(){var t;new ResizeObserver((()=>{t&&window.cancelAnimationFrame(t),t=window.requestAnimationFrame((function(){v=window.innerWidth,function(){const t="readium-virtual-page";var e=document.getElementById(t);if(x()||2!=parseInt(window.getComputedStyle(document.documentElement).getPropertyValue("column-count"))){var r;null===(r=e)||void 0===r||r.remove()}else{var n=document.scrollingElement.scrollWidth/window.innerWidth;Math.round(2*n)/2%1>.1&&(e?e.remove():((e=document.createElement("div")).setAttribute("id",t),e.style.breakBefore="column",e.innerHTML="​",document.body.appendChild(e)))}}(),function(){if(!x()){var t=j(window.scrollX+1);document.scrollingElement.scrollLeft=t}}(),w()}))})).observe(document.body)}),!1);var g,m,b=!1,v=0;function w(){if(readium.isFixedLayout)return;let t=document.scrollingElement;if(x()&&!S()){const e=window.scrollY,r=window.innerHeight,n=t.scrollHeight;h={first:e/n,last:(e+r)/n}}else{let e=window.scrollX;const r=window.innerWidth,n=t.scrollWidth;E()&&(e=Math.abs(e)),h={first:e/n,last:(e+r)/n}}0!==t.scrollWidth&&0!==t.scrollHeight&&(b||window.requestAnimationFrame((function(){var t;t=h,webkit.messageHandlers.progressionChanged.postMessage(t),b=!1})),b=!0)}function x(){return"readium-scroll-on"==document.documentElement.style.getPropertyValue("--USER__view").trim()}function S(){return window.getComputedStyle(document.documentElement).getPropertyValue("writing-mode").startsWith("vertical")}function E(){const t=window.getComputedStyle(document.documentElement);return"rtl"==t.getPropertyValue("direction")||"vertical-rl"==t.getPropertyValue("writing-mode")}function A(t){return x()?document.scrollingElement.scrollTop=t.top+window.scrollY:document.scrollingElement.scrollLeft=j(t.left+window.scrollX),!0}function O(t){var e=window.scrollX,r=window.innerWidth;return document.scrollingElement.scrollLeft=t,Math.abs(e-t)/r>.01}function j(t){const e=t+(E()?-1:1);return e-e%v}function T(t){try{let n=t.locations,o=t.text;var e;if(o&&o.highlight)return n&&n.cssSelector&&(e=document.querySelector(n.cssSelector)),e||(e=document.body),new l(e,o.highlight,{prefix:o.before,suffix:o.after}).toRange();if(n){var r=null;if(!r&&n.cssSelector&&(r=document.querySelector(n.cssSelector)),!r&&n.fragments)for(const t of n.fragments)if(r=document.getElementById(t))break;if(r){let t=document.createRange();return t.setStartBefore(r),t.setEndAfter(r),t}}}catch(t){N(t)}return null}function P(t,e){null===e?R(t):document.documentElement.style.setProperty(t,e,"important")}function R(t){document.documentElement.style.removeProperty(t)}function C(){var t=Array.prototype.slice.call(arguments).join(" ");webkit.messageHandlers.log.postMessage(t)}function I(t){N(new Error(t))}function N(t){webkit.messageHandlers.logError.postMessage({message:t.message})}window.addEventListener("scroll",w),document.addEventListener("selectionchange",(50,g=function(){webkit.messageHandlers.selectionChanged.postMessage(y())},function(){var t=this,e=arguments;clearTimeout(m),m=setTimeout((function(){g.apply(t,e),m=null}),50)}));const M=!1;function $(t){let e=k({x:t.left,y:t.top});const r=t.width,n=t.height,o=e.x,i=e.y;return{width:r,height:n,left:o,top:i,right:o+r,bottom:i+n}}function k(t){if(!frameElement)return t;let e=frameElement.getBoundingClientRect();if(!e)return t;let r=window.top.document.documentElement;return{x:t.x+e.x+r.scrollLeft,y:t.y+e.y+r.scrollTop}}function D(t,e){let r=t.getClientRects();const n=[];for(const t of r)n.push({bottom:t.bottom,height:t.height,left:t.left,right:t.right,top:t.top,width:t.width});const o=W(function(t,e){const r=new Set(t);for(const e of t)if(e.width>1&&e.height>1){for(const n of t)if(e!==n&&r.has(n)&&B(n,e,1)){H("CLIENT RECT: remove contained"),r.delete(e);break}}else H("CLIENT RECT: remove tiny"),r.delete(e);return Array.from(r)}(F(n,1,e)));for(let t=o.length-1;t>=0;t--){const e=o[t];if(!(e.width*e.height>4)){if(!(o.length>1)){H("CLIENT RECT: remove small, but keep otherwise empty!");break}H("CLIENT RECT: remove small"),o.splice(t,1)}}return H(`CLIENT RECT: reduced ${n.length} --\x3e ${o.length}`),o}function F(t,e,r){for(let n=0;nt!==i&&t!==a)),o=L(i,a);return n.push(o),F(n,e,r)}}return t}function L(t,e){const r=Math.min(t.left,e.left),n=Math.max(t.right,e.right),o=Math.min(t.top,e.top),i=Math.max(t.bottom,e.bottom);return{bottom:i,height:i-o,left:r,right:n,top:o,width:n-r}}function B(t,e,r){return _(t,e.left,e.top,r)&&_(t,e.right,e.top,r)&&_(t,e.left,e.bottom,r)&&_(t,e.right,e.bottom,r)}function _(t,e,r,n){return(t.lefte||G(t.right,e,n))&&(t.topr||G(t.bottom,r,n))}function W(t){for(let e=0;et!==e));return Array.prototype.push.apply(a,r),W(a)}}else H("replaceOverlapingRects rect1 === rect2 ??!")}return t}function U(t,e){const r=function(t,e){const r=Math.max(t.left,e.left),n=Math.min(t.right,e.right),o=Math.max(t.top,e.top),i=Math.min(t.bottom,e.bottom);return{bottom:i,height:Math.max(0,i-o),left:r,right:n,top:o,width:Math.max(0,n-r)}}(e,t);if(0===r.height||0===r.width)return[t];const n=[];{const e={bottom:t.bottom,height:0,left:t.left,right:r.left,top:t.top,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}{const e={bottom:r.top,height:0,left:r.left,right:r.right,top:t.top,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}{const e={bottom:t.bottom,height:0,left:r.left,right:r.right,top:r.bottom,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}{const e={bottom:t.bottom,height:0,left:r.right,right:t.right,top:t.top,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}return n}function z(t,e,r){return(t.left=0&&G(t.left,e.right,r))&&(e.left=0&&G(e.left,t.right,r))&&(t.top=0&&G(t.top,e.bottom,r))&&(e.top=0&&G(e.top,t.bottom,r))}function G(t,e,r){return Math.abs(t-e)<=r}function H(){M&&C.apply(null,arguments)}var V,q=[],X="ResizeObserver loop completed with undelivered notifications.";!function(t){t.BORDER_BOX="border-box",t.CONTENT_BOX="content-box",t.DEVICE_PIXEL_CONTENT_BOX="device-pixel-content-box"}(V||(V={}));var K,Y=function(t){return Object.freeze(t)},J=function(t,e){this.inlineSize=t,this.blockSize=e,Y(this)},Q=function(){function t(t,e,r,n){return this.x=t,this.y=e,this.width=r,this.height=n,this.top=this.y,this.left=this.x,this.bottom=this.top+this.height,this.right=this.left+this.width,Y(this)}return t.prototype.toJSON=function(){var t=this;return{x:t.x,y:t.y,top:t.top,right:t.right,bottom:t.bottom,left:t.left,width:t.width,height:t.height}},t.fromRect=function(e){return new t(e.x,e.y,e.width,e.height)},t}(),Z=function(t){return t instanceof SVGElement&&"getBBox"in t},tt=function(t){if(Z(t)){var e=t.getBBox(),r=e.width,n=e.height;return!r&&!n}var o=t,i=o.offsetWidth,a=o.offsetHeight;return!(i||a||t.getClientRects().length)},et=function(t){var e;if(t instanceof Element)return!0;var r=null===(e=null==t?void 0:t.ownerDocument)||void 0===e?void 0:e.defaultView;return!!(r&&t instanceof r.Element)},rt="undefined"!=typeof window?window:{},nt=new WeakMap,ot=/auto|scroll/,it=/^tb|vertical/,at=/msie|trident/i.test(rt.navigator&&rt.navigator.userAgent),ut=function(t){return parseFloat(t||"0")},ct=function(t,e,r){return void 0===t&&(t=0),void 0===e&&(e=0),void 0===r&&(r=!1),new J((r?e:t)||0,(r?t:e)||0)},st=Y({devicePixelContentBoxSize:ct(),borderBoxSize:ct(),contentBoxSize:ct(),contentRect:new Q(0,0,0,0)}),lt=function(t,e){if(void 0===e&&(e=!1),nt.has(t)&&!e)return nt.get(t);if(tt(t))return nt.set(t,st),st;var r=getComputedStyle(t),n=Z(t)&&t.ownerSVGElement&&t.getBBox(),o=!at&&"border-box"===r.boxSizing,i=it.test(r.writingMode||""),a=!n&&ot.test(r.overflowY||""),u=!n&&ot.test(r.overflowX||""),c=n?0:ut(r.paddingTop),s=n?0:ut(r.paddingRight),l=n?0:ut(r.paddingBottom),f=n?0:ut(r.paddingLeft),p=n?0:ut(r.borderTopWidth),y=n?0:ut(r.borderRightWidth),d=n?0:ut(r.borderBottomWidth),h=f+s,g=c+l,m=(n?0:ut(r.borderLeftWidth))+y,b=p+d,v=u?t.offsetHeight-b-t.clientHeight:0,w=a?t.offsetWidth-m-t.clientWidth:0,x=o?h+m:0,S=o?g+b:0,E=n?n.width:ut(r.width)-x-w,A=n?n.height:ut(r.height)-S-v,O=E+h+w+m,j=A+g+v+b,T=Y({devicePixelContentBoxSize:ct(Math.round(E*devicePixelRatio),Math.round(A*devicePixelRatio),i),borderBoxSize:ct(O,j,i),contentBoxSize:ct(E,A,i),contentRect:new Q(f,c,E,A)});return nt.set(t,T),T},ft=function(t,e,r){var n=lt(t,r),o=n.borderBoxSize,i=n.contentBoxSize,a=n.devicePixelContentBoxSize;switch(e){case V.DEVICE_PIXEL_CONTENT_BOX:return a;case V.BORDER_BOX:return o;default:return i}},pt=function(t){var e=lt(t);this.target=t,this.contentRect=e.contentRect,this.borderBoxSize=Y([e.borderBoxSize]),this.contentBoxSize=Y([e.contentBoxSize]),this.devicePixelContentBoxSize=Y([e.devicePixelContentBoxSize])},yt=function(t){if(tt(t))return 1/0;for(var e=0,r=t.parentNode;r;)e+=1,r=r.parentNode;return e},dt=function(){var t=1/0,e=[];q.forEach((function(r){if(0!==r.activeTargets.length){var n=[];r.activeTargets.forEach((function(e){var r=new pt(e.target),o=yt(e.target);n.push(r),e.lastReportedSize=ft(e.target,e.observedBox),ot?e.activeTargets.push(r):e.skippedTargets.push(r))}))}))},gt=[],mt=0,bt={attributes:!0,characterData:!0,childList:!0,subtree:!0},vt=["resize","load","transitionend","animationend","animationstart","animationiteration","keyup","keydown","mouseup","mousedown","mouseover","mouseout","blur","focus"],wt=function(t){return void 0===t&&(t=0),Date.now()+t},xt=!1,St=function(){function t(){var t=this;this.stopped=!0,this.listener=function(){return t.schedule()}}return t.prototype.run=function(t){var e=this;if(void 0===t&&(t=250),!xt){xt=!0;var r,n=wt(t);r=function(){var r=!1;try{r=function(){var t,e=0;for(ht(e);q.some((function(t){return t.activeTargets.length>0}));)e=dt(),ht(e);return q.some((function(t){return t.skippedTargets.length>0}))&&("function"==typeof ErrorEvent?t=new ErrorEvent("error",{message:X}):((t=document.createEvent("Event")).initEvent("error",!1,!1),t.message=X),window.dispatchEvent(t)),e>0}()}finally{if(xt=!1,t=n-wt(),!mt)return;r?e.run(1e3):t>0?e.run(t):e.start()}},function(t){if(!K){var e=0,r=document.createTextNode("");new MutationObserver((function(){return gt.splice(0).forEach((function(t){return t()}))})).observe(r,{characterData:!0}),K=function(){r.textContent="".concat(e?e--:e++)}}gt.push(t),K()}((function(){requestAnimationFrame(r)}))}},t.prototype.schedule=function(){this.stop(),this.run()},t.prototype.observe=function(){var t=this,e=function(){return t.observer&&t.observer.observe(document.body,bt)};document.body?e():rt.addEventListener("DOMContentLoaded",e)},t.prototype.start=function(){var t=this;this.stopped&&(this.stopped=!1,this.observer=new MutationObserver(this.listener),this.observe(),vt.forEach((function(e){return rt.addEventListener(e,t.listener,!0)})))},t.prototype.stop=function(){var t=this;this.stopped||(this.observer&&this.observer.disconnect(),vt.forEach((function(e){return rt.removeEventListener(e,t.listener,!0)})),this.stopped=!0)},t}(),Et=new St,At=function(t){!mt&&t>0&&Et.start(),!(mt+=t)&&Et.stop()},Ot=function(){function t(t,e){this.target=t,this.observedBox=e||V.CONTENT_BOX,this.lastReportedSize={inlineSize:0,blockSize:0}}return t.prototype.isActive=function(){var t,e=ft(this.target,this.observedBox,!0);return t=this.target,Z(t)||function(t){switch(t.tagName){case"INPUT":if("image"!==t.type)break;case"VIDEO":case"AUDIO":case"EMBED":case"OBJECT":case"CANVAS":case"IFRAME":case"IMG":return!0}return!1}(t)||"inline"!==getComputedStyle(t).display||(this.lastReportedSize=e),this.lastReportedSize.inlineSize!==e.inlineSize||this.lastReportedSize.blockSize!==e.blockSize},t}(),jt=function(t,e){this.activeTargets=[],this.skippedTargets=[],this.observationTargets=[],this.observer=t,this.callback=e},Tt=new WeakMap,Pt=function(t,e){for(var r=0;r=0&&(o&&q.splice(q.indexOf(r),1),r.observationTargets.splice(n,1),At(-1))},t.disconnect=function(t){var e=this,r=Tt.get(t);r.observationTargets.slice().forEach((function(r){return e.unobserve(t,r.target)})),r.activeTargets.splice(0,r.activeTargets.length)},t}(),Ct=function(){function t(t){if(0===arguments.length)throw new TypeError("Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.");if("function"!=typeof t)throw new TypeError("Failed to construct 'ResizeObserver': The callback provided as parameter 1 is not a function.");Rt.connect(this,t)}return t.prototype.observe=function(t,e){if(0===arguments.length)throw new TypeError("Failed to execute 'observe' on 'ResizeObserver': 1 argument required, but only 0 present.");if(!et(t))throw new TypeError("Failed to execute 'observe' on 'ResizeObserver': parameter 1 is not of type 'Element");Rt.observe(this,t,e)},t.prototype.unobserve=function(t){if(0===arguments.length)throw new TypeError("Failed to execute 'unobserve' on 'ResizeObserver': 1 argument required, but only 0 present.");if(!et(t))throw new TypeError("Failed to execute 'unobserve' on 'ResizeObserver': parameter 1 is not of type 'Element");Rt.unobserve(this,t)},t.prototype.disconnect=function(){Rt.disconnect(this)},t.toString=function(){return"function ResizeObserver () { [polyfill code] }"},t}();const It=window.ResizeObserver||Ct;let Nt=new Map,Mt=new Map;var $t=0;function kt(t){if(0===Mt.size)return null;for(const[e,r]of Mt)if(r.isActivable())for(const n of r.items.reverse())if(n.clickableElements)for(const r of n.clickableElements){let o=r.getBoundingClientRect().toJSON();if(_(o,t.clientX,t.clientY,1))return{group:e,item:n,element:r,rect:o}}return null}function Dt(t){return t&&t instanceof Element}window.addEventListener("load",(function(){const t=document.body;var e={width:0,height:0};new It((()=>{e.width===t.clientWidth&&e.height===t.clientHeight||(e={width:t.clientWidth,height:t.clientHeight},Mt.forEach((function(t){t.requestLayout()})))})).observe(t)}),!1);const Ft={NONE:"",DESCENDANT:" ",CHILD:" > "},Lt={id:"id",class:"class",tag:"tag",attribute:"attribute",nthchild:"nthchild",nthoftype:"nthoftype"},Bt="CssSelectorGenerator";function _t(t="unknown problem",...e){console.warn(`${Bt}: ${t}`,...e)}const Wt={selectors:[Lt.id,Lt.class,Lt.tag,Lt.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function Ut(t){return t instanceof RegExp}function zt(t){return["string","function"].includes(typeof t)||Ut(t)}function Gt(t){return Array.isArray(t)?t.filter(zt):[]}function Ht(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 Vt(t,e){if(Ht(t))return t.contains(e)||_t("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 r=e.getRootNode({composed:!1});return Ht(r)?(r!==document&&_t("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."),r):e.ownerDocument.querySelector(":root")}function qt(t){return"number"==typeof t?t:Number.POSITIVE_INFINITY}function Xt(t=[]){const[e=[],...r]=t;return 0===r.length?e:r.reduce(((t,e)=>t.filter((t=>e.includes(t)))),e)}function Kt(t){return[].concat(...t)}function Yt(t){const e=t.map((t=>{if(Ut(t))return e=>t.test(e);if("function"==typeof t)return e=>{const r=t(e);return"boolean"!=typeof r?(_t("pattern matcher function invalid","Provided pattern matching function does not return boolean. It's result will be ignored.",t),!1):r};if("string"==typeof t){const e=new RegExp("^"+t.replace(/[|\\{}()[\]^$+?.]/g,"\\$&").replace(/\*/g,".+")+"$");return t=>e.test(t)}return _t("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 Jt(t,e,r){const n=Array.from(Vt(r,t[0]).querySelectorAll(e));return n.length===t.length&&t.every((t=>n.includes(t)))}function Qt(t,e){e=null!=e?e:function(t){return t.ownerDocument.querySelector(":root")}(t);const r=[];let n=t;for(;Dt(n)&&n!==e;)r.push(n),n=n.parentElement;return r}function Zt(t,e){return Xt(t.map((t=>Qt(t,e))))}const te=new RegExp(["^$","\\s"].join("|")),ee=new RegExp(["^$"].join("|")),re=[Lt.nthoftype,Lt.tag,Lt.id,Lt.class,Lt.attribute,Lt.nthchild],ne=Yt(["class","id","ng-*"]);function oe({name:t}){return`[${t}]`}function ie({name:t,value:e}){return`[${t}='${e}']`}function ae({nodeName:t,nodeValue:e}){return{name:(r=t,r.replace(/:/g,"\\:")),value:ve(e)};var r}function ue(t){const e=Array.from(t.attributes).filter((e=>function({nodeName:t},e){const r=e.tagName.toLowerCase();return!(["input","option"].includes(r)&&"value"===t||ne(t))}(e,t))).map(ae);return[...e.map(oe),...e.map(ie)]}function ce(t){return(t.getAttribute("class")||"").trim().split(/\s+/).filter((t=>!ee.test(t))).map((t=>`.${ve(t)}`))}function se(t){const e=t.getAttribute("id")||"",r=`#${ve(e)}`,n=t.getRootNode({composed:!1});return!te.test(e)&&Jt([t],r,n)?[r]:[]}function le(t){const e=t.parentNode;if(e){const r=Array.from(e.childNodes).filter(Dt).indexOf(t);if(r>-1)return[`:nth-child(${r+1})`]}return[]}function fe(t){return[ve(t.tagName.toLowerCase())]}function pe(t){const e=[...new Set(Kt(t.map(fe)))];return 0===e.length||e.length>1?[]:[e[0]]}function ye(t){const e=pe([t])[0],r=t.parentElement;if(r){const n=Array.from(r.children).filter((t=>t.tagName.toLowerCase()===e)),o=n.indexOf(t);if(o>-1)return[`${e}:nth-of-type(${o+1})`]}return[]}function de(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){return Array.from(function*(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){let r=0,n=ge(1);for(;n.length<=t.length&&rt[e]));yield e,n=he(n,t.length-1)}}(t,{maxResults:e}))}function he(t=[],e=0){const r=t.length;if(0===r)return[];const n=[...t];n[r-1]+=1;for(let t=r-1;t>=0;t--)if(n[t]>e){if(0===t)return ge(r+1);n[t-1]++,n[t]=n[t-1]+1}return n[r-1]>e?ge(r+1):n}function ge(t=1){return Array.from(Array(t).keys())}const me=":".charCodeAt(0).toString(16).toUpperCase(),be=/[ !"#$%&'()\[\]{|}<>*+,./;=?@^`~\\]/;function ve(t=""){var e,r;return null!==(r=null===(e=null===CSS||void 0===CSS?void 0:CSS.escape)||void 0===e?void 0:e.call(CSS,t))&&void 0!==r?r:function(t=""){return t.split("").map((t=>":"===t?`\\${me} `:be.test(t)?`\\${t}`:escape(t).replace(/%/g,"\\"))).join("")}(t)}const we={tag:pe,id:function(t){return 0===t.length||t.length>1?[]:se(t[0])},class:function(t){return Xt(t.map(ce))},attribute:function(t){return Xt(t.map(ue))},nthchild:function(t){return Xt(t.map(le))},nthoftype:function(t){return Xt(t.map(ye))}},xe={tag:fe,id:se,class:ce,attribute:ue,nthchild:le,nthoftype:ye};function Se(t){return t.includes(Lt.tag)||t.includes(Lt.nthoftype)?[...t]:[...t,Lt.tag]}function Ee(t={}){const e=[...re];return t[Lt.tag]&&t[Lt.nthoftype]&&e.splice(e.indexOf(Lt.tag),1),e.map((e=>{return(n=t)[r=e]?n[r].join(""):"";var r,n})).join("")}function Ae(t,e,r="",n){const o=function(t,e){return""===e?t:function(t,e){return[...t.map((t=>e+Ft.DESCENDANT+t)),...t.map((t=>e+Ft.CHILD+t))]}(t,e)}(function(t,e,r){const n=function(t,e){const{blacklist:r,whitelist:n,combineWithinSelector:o,maxCombinations:i}=e,a=Yt(r),u=Yt(n);return function(t){const{selectors:e,includeTag:r}=t,n=[].concat(e);return r&&!n.includes("tag")&&n.push("tag"),n}(e).reduce(((e,r)=>{const n=function(t,e){var r;return(null!==(r=we[e])&&void 0!==r?r:()=>[])(t)}(t,r),c=function(t=[],e,r){return t.filter((t=>r(t)||!e(t)))}(n,a,u),s=function(t=[],e){return t.sort(((t,r)=>{const n=e(t),o=e(r);return n&&!o?-1:!n&&o?1:0}))}(c,u);return e[r]=o?de(s,{maxResults:i}):s.map((t=>[t])),e}),{})}(t,r),o=function(t,e){return function(t){const{selectors:e,combineBetweenSelectors:r,includeTag:n,maxCandidates:o}=t,i=r?de(e,{maxResults:o}):e.map((t=>[t]));return n?i.map(Se):i}(e).map((e=>function(t,e){const r={};return t.forEach((t=>{const n=e[t];n.length>0&&(r[t]=n)})),function(t={}){let e=[];return Object.entries(t).forEach((([t,r])=>{e=r.flatMap((r=>0===e.length?[{[t]:r}]:e.map((e=>Object.assign(Object.assign({},e),{[t]:r})))))})),e}(r).map(Ee)}(e,t))).filter((t=>t.length>0))}(n,r),i=Kt(o);return[...new Set(i)]}(t,n.root,n),r);for(const e of o)if(Jt(t,e,n.root))return e;return null}function Oe(t){return{value:t,include:!1}}function je({selectors:t,operator:e}){let r=[...re];t[Lt.tag]&&t[Lt.nthoftype]&&(r=r.filter((t=>t!==Lt.tag)));let n="";return r.forEach((e=>{(t[e]||[]).forEach((({value:t,include:e})=>{e&&(n+=t)}))})),e+n}function Te(t){return[":root",...Qt(t).reverse().map((t=>{const e=function(t,e,r=Ft.NONE){const n={};return e.forEach((e=>{Reflect.set(n,e,function(t,e){return xe[e](t)}(t,e).map(Oe))})),{element:t,operator:r,selectors:n}}(t,[Lt.nthchild],Ft.CHILD);return e.selectors.nthchild.forEach((t=>{t.include=!0})),e})).map(je)].join("")}function Pe(t,e={}){const r=function(t){(t instanceof NodeList||t instanceof HTMLCollection)&&(t=Array.from(t));const e=(Array.isArray(t)?t:[t]).filter(Dt);return[...new Set(e)]}(t),n=function(t,e={}){const r=Object.assign(Object.assign({},Wt),e);return{selectors:(n=r.selectors,Array.isArray(n)?n.filter((t=>{return e=Lt,r=t,Object.values(e).includes(r);var e,r})):[]),whitelist:Gt(r.whitelist),blacklist:Gt(r.blacklist),root:Vt(r.root,t),combineWithinSelector:!!r.combineWithinSelector,combineBetweenSelectors:!!r.combineBetweenSelectors,includeTag:!!r.includeTag,maxCombinations:qt(r.maxCombinations),maxCandidates:qt(r.maxCandidates)};var n}(r[0],e);let o="",i=n.root;function a(){return function(t,e,r="",n){if(0===t.length)return null;const o=[t.length>1?t:[],...Zt(t,e).map((t=>[t]))];for(const t of o){const e=Ae(t,0,r,n);if(e)return{foundElements:t,selector:e}}return null}(r,i,o,n)}let u=a();for(;u;){const{foundElements:t,selector:e}=u;if(Jt(r,e,n.root))return e;i=t[0],o=e,u=a()}return r.length>1?r.map((t=>Pe(t,n))).join(", "):function(t){return t.map(Te).join(", ")}(r)}function Re(t){return null==t?null:-1!==["a","audio","button","canvas","details","input","label","option","select","submit","textarea","video"].indexOf(t.nodeName.toLowerCase())||t.hasAttribute("contenteditable")&&"false"!=t.getAttribute("contenteditable").toLowerCase()?t.outerHTML:t.parentElement?Re(t.parentElement):null}function Ce(t){for(var e=0;e0&&e.top0&&e.left{_e(t)||(We(t),Ue("down",t))})),window.addEventListener("keyup",(t=>{_e(t)||(We(t),Ue("up",t))})),r.g.readium={scrollToId:function(t){let e=document.getElementById(t);return!!e&&(A(e.getBoundingClientRect()),!0)},scrollToPosition:function(t,e){if(t<0||t>1)console.error(`Expected a valid progression in scrollToPosition, got ${t}`);else if(x())if(S()){let e=document.scrollingElement.scrollWidth*t;document.scrollingElement.scrollLeft=-e}else{let e=document.scrollingElement.scrollHeight*t;document.scrollingElement.scrollTop=e}else{let r=document.scrollingElement.scrollWidth*t*("rtl"==e?-1:1);document.scrollingElement.scrollLeft=j(r)}},scrollToLocator:function(t){let e=T(t);return!!e&&function(t){return A(t.getBoundingClientRect())}(e)},scrollLeft:function(t){var e="rtl"==t,r=document.scrollingElement.scrollWidth,n=window.innerWidth,o=window.scrollX-n,i=e?-(r-n):0;return O(Math.max(o,i))},scrollRight:function(t){var e="rtl"==t,r=document.scrollingElement.scrollWidth,n=window.innerWidth,o=window.scrollX+n,i=e?0:r-n;return O(Math.min(o,i))},setCSSProperties:function(t){for(const e in t)P(e,t[e])},setProperty:P,removeProperty:R,registerDecorationTemplates:function(t){var e="";for(const[r,n]of Object.entries(t))Nt.set(r,n),n.stylesheet&&(e+=n.stylesheet+"\n");if(e){let t=document.createElement("style");t.innerHTML=e,document.getElementsByTagName("head")[0].appendChild(t)}},getDecorations:function(t){var e=Mt.get(t);return e||(e=function(t,e){var r=[],n=0,o=null,i=!1;function a(e){let o=t+"-"+n++,i=T(e.locator);if(!i)return void C("Can't locate DOM range for decoration",e);let a={id:o,decoration:e,range:i};r.push(a),c(a)}function u(t){let e=r.findIndex((e=>e.decoration.id===t));if(-1===e)return;let n=r[e];r.splice(e,1),n.clickableElements=null,n.container&&(n.container.remove(),n.container=null)}function c(r){let n=(o||((o=document.createElement("div")).id=t,o.dataset.group=e,o.style.pointerEvents="none",requestAnimationFrame((function(){null!=o&&document.body.append(o)}))),o),i=Nt.get(r.decoration.style);if(!i)return void I(`Unknown decoration style: ${r.decoration.style}`);let a=document.createElement("div");a.id=r.id,a.dataset.style=r.decoration.style,a.style.pointerEvents="none";const u=getComputedStyle(document.body).writingMode,c="vertical-rl"===u||"vertical-lr"===u,s=document.scrollingElement,{scrollLeft:l,scrollTop:f}=s,p=c?window.innerHeight:window.innerWidth,y=c?window.innerWidth:window.innerHeight,d=parseInt(getComputedStyle(document.documentElement).getPropertyValue("column-count"))||1,h=(c?y:p)/d;function g(t,e,r,n){t.style.position="absolute";const o="vertical-rl"===n;if(o||"vertical-lr"===n){if("wrap"===i.width)t.style.width=`${e.width}px`,t.style.height=`${e.height}px`,o?t.style.right=`${-e.right-l+s.clientWidth}px`:t.style.left=`${e.left+l}px`,t.style.top=`${e.top+f}px`;else if("viewport"===i.width){t.style.width=`${e.height}px`,t.style.height=`${p}px`;const r=Math.floor(e.top/p)*p;o?t.style.right=-e.right-l+"px":t.style.left=`${e.left+l}px`,t.style.top=`${r+f}px`}else if("bounds"===i.width)t.style.width=`${r.height}px`,t.style.height=`${p}px`,o?t.style.right=`${-r.right-l+s.clientWidth}px`:t.style.left=`${r.left+l}px`,t.style.top=`${r.top+f}px`;else if("page"===i.width){t.style.width=`${e.height}px`,t.style.height=`${h}px`;const r=Math.floor(e.top/h)*h;o?t.style.right=`${-e.right-l+s.clientWidth}px`:t.style.left=`${e.left+l}px`,t.style.top=`${r+f}px`}}else if("wrap"===i.width)t.style.width=`${e.width}px`,t.style.height=`${e.height}px`,t.style.left=`${e.left+l}px`,t.style.top=`${e.top+f}px`;else if("viewport"===i.width){t.style.width=`${p}px`,t.style.height=`${e.height}px`;const r=Math.floor(e.left/p)*p;t.style.left=`${r+l}px`,t.style.top=`${e.top+f}px`}else if("bounds"===i.width)t.style.width=`${r.width}px`,t.style.height=`${e.height}px`,t.style.left=`${r.left+l}px`,t.style.top=`${e.top+f}px`;else if("page"===i.width){t.style.width=`${h}px`,t.style.height=`${e.height}px`;const r=Math.floor(e.left/h)*h;t.style.left=`${r+l}px`,t.style.top=`${e.top+f}px`}}let m,b=r.range.getBoundingClientRect();try{let t=document.createElement("template");t.innerHTML=r.decoration.element.trim(),m=t.content.firstElementChild}catch(t){return void I(`Invalid decoration element "${r.decoration.element}": ${t.message}`)}if("boxes"===i.layout){const t=!u.startsWith("vertical"),e=(v=r.range.startContainer).nodeType===Node.ELEMENT_NODE?v:v.parentElement,n=getComputedStyle(e).writingMode,o=D(r.range,t).sort(((t,e)=>t.top!==e.top?t.top-e.top:"vertical-rl"===n?e.left-t.left:t.left-e.left));for(let t of o){const e=m.cloneNode(!0);e.style.pointerEvents="none",e.dataset.writingMode=n,g(e,t,b,u),a.append(e)}}else if("bounds"===i.layout){const t=m.cloneNode(!0);t.style.pointerEvents="none",t.dataset.writingMode=u,g(t,b,b,u),a.append(t)}var v;n.append(a),r.container=a,r.clickableElements=Array.from(a.querySelectorAll("[data-activable='1']")),0===r.clickableElements.length&&(r.clickableElements=Array.from(a.children))}function s(){o&&(o.remove(),o=null)}return{add:a,remove:u,update:function(t){u(t.id),a(t)},clear:function(){s(),r.length=0},items:r,requestLayout:function(){s(),r.forEach((t=>c(t)))},isActivable:function(){return i},setActivable:function(){i=!0}}}("r2-decoration-"+$t++,t),Mt.set(t,e)),e},findFirstVisibleLocator:function(){const t=Ce(document.body);return{href:"#",type:"application/xhtml+xml",locations:{cssSelector:Pe(t)},text:{highlight:t.textContent}}}},window.readium.isFixedLayout=!0,webkit.messageHandlers.spreadLoadStarted.postMessage({})})()})(); +(()=>{var t={9116:(t,e)=>{"use strict";function r(t){return t.split("").reverse().join("")}function n(t){return(t|-t)>>31&1}function o(t,e,r,o){var i=t.P[r],a=t.M[r],u=o>>>31,c=e[r]|u,s=c|a,l=(c&i)+i^i|c,f=a|~(l|i),p=i&l,y=n(f&t.lastRowMask[r])-n(p&t.lastRowMask[r]);return f<<=1,p<<=1,i=(p|=u)|~(s|(f|=n(o)-u)),a=f&s,t.P[r]=i,t.M[r]=a,y}function i(t,e,r){if(0===e.length)return[];r=Math.min(r,e.length);var n=[],i=32,a=Math.ceil(e.length/i)-1,u={P:new Uint32Array(a+1),M:new Uint32Array(a+1),lastRowMask:new Uint32Array(a+1)};u.lastRowMask.fill(1<<31),u.lastRowMask[a]=1<<(e.length-1)%i;for(var c=new Uint32Array(a+1),s=new Map,l=[],f=0;f<256;f++)l.push(c);for(var p=0;p=e.length||e.charCodeAt(m)===y&&(d[h]|=1<0&&v[b]>=r+i;)b-=1;b===a&&v[b]<=r&&(v[b]{"use strict";var n=r(4624),o=r(5096),i=o(n("String.prototype.indexOf"));t.exports=function(t,e){var r=n(t,!!e);return"function"==typeof r&&i(t,".prototype.")>-1?o(r):r}},5096:(t,e,r)=>{"use strict";var n=r(3520),o=r(4624),i=r(5676),a=r(2824),u=o("%Function.prototype.apply%"),c=o("%Function.prototype.call%"),s=o("%Reflect.apply%",!0)||n.call(c,u),l=o("%Object.defineProperty%",!0),f=o("%Math.max%");if(l)try{l({},"a",{value:1})}catch(t){l=null}t.exports=function(t){if("function"!=typeof t)throw new a("a function is required");var e=s(n,c,arguments);return i(e,1+f(0,t.length-(arguments.length-1)),!0)};var p=function(){return s(n,u,arguments)};l?l(t.exports,"apply",{value:p}):t.exports.apply=p},2448:(t,e,r)=>{"use strict";var n=r(3268)(),o=r(4624),i=n&&o("%Object.defineProperty%",!0);if(i)try{i({},"a",{value:1})}catch(t){i=!1}var a=r(6500),u=r(2824),c=r(6168);t.exports=function(t,e,r){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new u("`obj` must be an object or a function`");if("string"!=typeof e&&"symbol"!=typeof e)throw new u("`property` must be a string or a symbol`");if(arguments.length>3&&"boolean"!=typeof arguments[3]&&null!==arguments[3])throw new u("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&"boolean"!=typeof arguments[4]&&null!==arguments[4])throw new u("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&"boolean"!=typeof arguments[5]&&null!==arguments[5])throw new u("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&"boolean"!=typeof arguments[6])throw new u("`loose`, if provided, must be a boolean");var n=arguments.length>3?arguments[3]:null,o=arguments.length>4?arguments[4]:null,s=arguments.length>5?arguments[5]:null,l=arguments.length>6&&arguments[6],f=!!c&&c(t,e);if(i)i(t,e,{configurable:null===s&&f?f.configurable:!s,enumerable:null===n&&f?f.enumerable:!n,value:r,writable:null===o&&f?f.writable:!o});else{if(!l&&(n||o||s))throw new a("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.");t[e]=r}}},2732:(t,e,r)=>{"use strict";var n=r(2812),o="function"==typeof Symbol&&"symbol"==typeof Symbol("foo"),i=Object.prototype.toString,a=Array.prototype.concat,u=r(2448),c=r(3268)(),s=function(t,e,r,n){if(e in t)if(!0===n){if(t[e]===r)return}else if("function"!=typeof(o=n)||"[object Function]"!==i.call(o)||!n())return;var o;c?u(t,e,r,!0):u(t,e,r)},l=function(t,e){var r=arguments.length>2?arguments[2]:{},i=n(e);o&&(i=a.call(i,Object.getOwnPropertySymbols(e)));for(var u=0;u{"use strict";t.exports=EvalError},1152:t=>{"use strict";t.exports=Error},1932:t=>{"use strict";t.exports=RangeError},5028:t=>{"use strict";t.exports=ReferenceError},6500:t=>{"use strict";t.exports=SyntaxError},2824:t=>{"use strict";t.exports=TypeError},5488:t=>{"use strict";t.exports=URIError},9200:(t,e,r)=>{"use strict";var n=r(4624)("%Object.defineProperty%",!0),o=r(4712)(),i=r(4440),a=o?Symbol.toStringTag:null;t.exports=function(t,e){var r=arguments.length>2&&arguments[2]&&arguments[2].force;!a||!r&&i(t,a)||(n?n(t,a,{configurable:!0,enumerable:!1,value:e,writable:!1}):t[a]=e)}},108:(t,e,r)=>{"use strict";var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator,o=r(5988),i=r(648),a=r(1844),u=r(7256);t.exports=function(t){if(o(t))return t;var e,r="default";if(arguments.length>1&&(arguments[1]===String?r="string":arguments[1]===Number&&(r="number")),n&&(Symbol.toPrimitive?e=function(t,e){var r=t[e];if(null!=r){if(!i(r))throw new TypeError(r+" returned for property "+e+" of object "+t+" is not a function");return r}}(t,Symbol.toPrimitive):u(t)&&(e=Symbol.prototype.valueOf)),void 0!==e){var c=e.call(t,r);if(o(c))return c;throw new TypeError("unable to convert exotic object to primitive")}return"default"===r&&(a(t)||u(t))&&(r="string"),function(t,e){if(null==t)throw new TypeError("Cannot call method on "+t);if("string"!=typeof e||"number"!==e&&"string"!==e)throw new TypeError('hint must be "string" or "number"');var r,n,a,u="string"===e?["toString","valueOf"]:["valueOf","toString"];for(a=0;a{"use strict";t.exports=function(t){return null===t||"function"!=typeof t&&"object"!=typeof t}},1480:t=>{"use strict";var e=Object.prototype.toString,r=Math.max,n=function(t,e){for(var r=[],n=0;n{"use strict";var n=r(1480);t.exports=Function.prototype.bind||n},2656:t=>{"use strict";var e=function(){return"string"==typeof function(){}.name},r=Object.getOwnPropertyDescriptor;if(r)try{r([],"length")}catch(t){r=null}e.functionsHaveConfigurableNames=function(){if(!e()||!r)return!1;var t=r((function(){}),"name");return!!t&&!!t.configurable};var n=Function.prototype.bind;e.boundFunctionsHaveNames=function(){return e()&&"function"==typeof n&&""!==function(){}.bind().name},t.exports=e},4624:(t,e,r)=>{"use strict";var n,o=r(1152),i=r(7261),a=r(1932),u=r(5028),c=r(6500),s=r(2824),l=r(5488),f=Function,p=function(t){try{return f('"use strict"; return ('+t+").constructor;")()}catch(t){}},y=Object.getOwnPropertyDescriptor;if(y)try{y({},"")}catch(t){y=null}var d=function(){throw new s},h=y?function(){try{return d}catch(t){try{return y(arguments,"callee").get}catch(t){return d}}}():d,g=r(9800)(),m=r(7e3)(),b=Object.getPrototypeOf||(m?function(t){return t.__proto__}:null),v={},w="undefined"!=typeof Uint8Array&&b?b(Uint8Array):n,x={__proto__:null,"%AggregateError%":"undefined"==typeof AggregateError?n:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?n:ArrayBuffer,"%ArrayIteratorPrototype%":g&&b?b([][Symbol.iterator]()):n,"%AsyncFromSyncIteratorPrototype%":n,"%AsyncFunction%":v,"%AsyncGenerator%":v,"%AsyncGeneratorFunction%":v,"%AsyncIteratorPrototype%":v,"%Atomics%":"undefined"==typeof Atomics?n:Atomics,"%BigInt%":"undefined"==typeof BigInt?n:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?n:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?n:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?n:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":o,"%eval%":eval,"%EvalError%":i,"%Float32Array%":"undefined"==typeof Float32Array?n:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?n:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?n:FinalizationRegistry,"%Function%":f,"%GeneratorFunction%":v,"%Int8Array%":"undefined"==typeof Int8Array?n:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?n:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?n:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":g&&b?b(b([][Symbol.iterator]())):n,"%JSON%":"object"==typeof JSON?JSON:n,"%Map%":"undefined"==typeof Map?n:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&g&&b?b((new Map)[Symbol.iterator]()):n,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?n:Promise,"%Proxy%":"undefined"==typeof Proxy?n:Proxy,"%RangeError%":a,"%ReferenceError%":u,"%Reflect%":"undefined"==typeof Reflect?n:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?n:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&g&&b?b((new Set)[Symbol.iterator]()):n,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?n:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":g&&b?b(""[Symbol.iterator]()):n,"%Symbol%":g?Symbol:n,"%SyntaxError%":c,"%ThrowTypeError%":h,"%TypedArray%":w,"%TypeError%":s,"%Uint8Array%":"undefined"==typeof Uint8Array?n:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?n:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?n:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?n:Uint32Array,"%URIError%":l,"%WeakMap%":"undefined"==typeof WeakMap?n:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?n:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?n:WeakSet};if(b)try{null.error}catch(t){var S=b(b(t));x["%Error.prototype%"]=S}var E=function t(e){var r;if("%AsyncFunction%"===e)r=p("async function () {}");else if("%GeneratorFunction%"===e)r=p("function* () {}");else if("%AsyncGeneratorFunction%"===e)r=p("async function* () {}");else if("%AsyncGenerator%"===e){var n=t("%AsyncGeneratorFunction%");n&&(r=n.prototype)}else if("%AsyncIteratorPrototype%"===e){var o=t("%AsyncGenerator%");o&&b&&(r=b(o.prototype))}return x[e]=r,r},A={__proto__:null,"%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"]},O=r(3520),j=r(4440),T=O.call(Function.call,Array.prototype.concat),P=O.call(Function.apply,Array.prototype.splice),R=O.call(Function.call,String.prototype.replace),C=O.call(Function.call,String.prototype.slice),I=O.call(Function.call,RegExp.prototype.exec),N=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,M=/\\(\\)?/g,$=function(t,e){var r,n=t;if(j(A,n)&&(n="%"+(r=A[n])[0]+"%"),j(x,n)){var o=x[n];if(o===v&&(o=E(n)),void 0===o&&!e)throw new s("intrinsic "+t+" exists, but is not available. Please file an issue!");return{alias:r,name:n,value:o}}throw new c("intrinsic "+t+" does not exist!")};t.exports=function(t,e){if("string"!=typeof t||0===t.length)throw new s("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof e)throw new s('"allowMissing" argument must be a boolean');if(null===I(/^%?[^%]*%?$/,t))throw new c("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var r=function(t){var e=C(t,0,1),r=C(t,-1);if("%"===e&&"%"!==r)throw new c("invalid intrinsic syntax, expected closing `%`");if("%"===r&&"%"!==e)throw new c("invalid intrinsic syntax, expected opening `%`");var n=[];return R(t,N,(function(t,e,r,o){n[n.length]=r?R(o,M,"$1"):e||t})),n}(t),n=r.length>0?r[0]:"",o=$("%"+n+"%",e),i=o.name,a=o.value,u=!1,l=o.alias;l&&(n=l[0],P(r,T([0,1],l)));for(var f=1,p=!0;f=r.length){var m=y(a,d);a=(p=!!m)&&"get"in m&&!("originalValue"in m.get)?m.get:a[d]}else p=j(a,d),a=a[d];p&&!u&&(x[i]=a)}}return a}},6168:(t,e,r)=>{"use strict";var n=r(4624)("%Object.getOwnPropertyDescriptor%",!0);if(n)try{n([],"length")}catch(t){n=null}t.exports=n},3268:(t,e,r)=>{"use strict";var n=r(4624)("%Object.defineProperty%",!0),o=function(){if(n)try{return n({},"a",{value:1}),!0}catch(t){return!1}return!1};o.hasArrayLengthDefineBug=function(){if(!o())return null;try{return 1!==n([],"length",{value:1}).length}catch(t){return!0}},t.exports=o},7e3:t=>{"use strict";var e={foo:{}},r=Object;t.exports=function(){return{__proto__:e}.foo===e.foo&&!({__proto__:null}instanceof r)}},9800:(t,e,r)=>{"use strict";var n="undefined"!=typeof Symbol&&Symbol,o=r(7904);t.exports=function(){return"function"==typeof n&&"function"==typeof Symbol&&"symbol"==typeof n("foo")&&"symbol"==typeof Symbol("bar")&&o()}},7904:t=>{"use strict";t.exports=function(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var t={},e=Symbol("test"),r=Object(e);if("string"==typeof e)return!1;if("[object Symbol]"!==Object.prototype.toString.call(e))return!1;if("[object Symbol]"!==Object.prototype.toString.call(r))return!1;for(e in t[e]=42,t)return!1;if("function"==typeof Object.keys&&0!==Object.keys(t).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(t).length)return!1;var n=Object.getOwnPropertySymbols(t);if(1!==n.length||n[0]!==e)return!1;if(!Object.prototype.propertyIsEnumerable.call(t,e))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var o=Object.getOwnPropertyDescriptor(t,e);if(42!==o.value||!0!==o.enumerable)return!1}return!0}},4712:(t,e,r)=>{"use strict";var n=r(7904);t.exports=function(){return n()&&!!Symbol.toStringTag}},4440:(t,e,r)=>{"use strict";var n=Function.prototype.call,o=Object.prototype.hasOwnProperty,i=r(3520);t.exports=i.call(n,o)},7284:(t,e,r)=>{"use strict";var n=r(4440),o=r(3147)(),i=r(2824),a={assert:function(t,e){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");if(o.assert(t),!a.has(t,e))throw new i("`"+e+"` is not present on `O`")},get:function(t,e){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");var r=o.get(t);return r&&r["$"+e]},has:function(t,e){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");var r=o.get(t);return!!r&&n(r,"$"+e)},set:function(t,e,r){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");var n=o.get(t);n||(n={},o.set(t,n)),n["$"+e]=r}};Object.freeze&&Object.freeze(a),t.exports=a},648:t=>{"use strict";var e,r,n=Function.prototype.toString,o="object"==typeof Reflect&&null!==Reflect&&Reflect.apply;if("function"==typeof o&&"function"==typeof Object.defineProperty)try{e=Object.defineProperty({},"length",{get:function(){throw r}}),r={},o((function(){throw 42}),null,e)}catch(t){t!==r&&(o=null)}else o=null;var i=/^\s*class\b/,a=function(t){try{var e=n.call(t);return i.test(e)}catch(t){return!1}},u=function(t){try{return!a(t)&&(n.call(t),!0)}catch(t){return!1}},c=Object.prototype.toString,s="function"==typeof Symbol&&!!Symbol.toStringTag,l=!(0 in[,]),f=function(){return!1};if("object"==typeof document){var p=document.all;c.call(p)===c.call(document.all)&&(f=function(t){if((l||!t)&&(void 0===t||"object"==typeof t))try{var e=c.call(t);return("[object HTMLAllCollection]"===e||"[object HTML document.all class]"===e||"[object HTMLCollection]"===e||"[object Object]"===e)&&null==t("")}catch(t){}return!1})}t.exports=o?function(t){if(f(t))return!0;if(!t)return!1;if("function"!=typeof t&&"object"!=typeof t)return!1;try{o(t,null,e)}catch(t){if(t!==r)return!1}return!a(t)&&u(t)}:function(t){if(f(t))return!0;if(!t)return!1;if("function"!=typeof t&&"object"!=typeof t)return!1;if(s)return u(t);if(a(t))return!1;var e=c.call(t);return!("[object Function]"!==e&&"[object GeneratorFunction]"!==e&&!/^\[object HTML/.test(e))&&u(t)}},1844:(t,e,r)=>{"use strict";var n=Date.prototype.getDay,o=Object.prototype.toString,i=r(4712)();t.exports=function(t){return"object"==typeof t&&null!==t&&(i?function(t){try{return n.call(t),!0}catch(t){return!1}}(t):"[object Date]"===o.call(t))}},1476:(t,e,r)=>{"use strict";var n,o,i,a,u=r(668),c=r(4712)();if(c){n=u("Object.prototype.hasOwnProperty"),o=u("RegExp.prototype.exec"),i={};var s=function(){throw i};a={toString:s,valueOf:s},"symbol"==typeof Symbol.toPrimitive&&(a[Symbol.toPrimitive]=s)}var l=u("Object.prototype.toString"),f=Object.getOwnPropertyDescriptor;t.exports=c?function(t){if(!t||"object"!=typeof t)return!1;var e=f(t,"lastIndex");if(!e||!n(e,"value"))return!1;try{o(t,a)}catch(t){return t===i}}:function(t){return!(!t||"object"!=typeof t&&"function"!=typeof t)&&"[object RegExp]"===l(t)}},7256:(t,e,r)=>{"use strict";var n=Object.prototype.toString;if(r(9800)()){var o=Symbol.prototype.toString,i=/^Symbol\(.*\)$/;t.exports=function(t){if("symbol"==typeof t)return!0;if("[object Symbol]"!==n.call(t))return!1;try{return function(t){return"symbol"==typeof t.valueOf()&&i.test(o.call(t))}(t)}catch(t){return!1}}}else t.exports=function(t){return!1}},4152:(t,e,r)=>{var n="function"==typeof Map&&Map.prototype,o=Object.getOwnPropertyDescriptor&&n?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,i=n&&o&&"function"==typeof o.get?o.get:null,a=n&&Map.prototype.forEach,u="function"==typeof Set&&Set.prototype,c=Object.getOwnPropertyDescriptor&&u?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,s=u&&c&&"function"==typeof c.get?c.get:null,l=u&&Set.prototype.forEach,f="function"==typeof WeakMap&&WeakMap.prototype?WeakMap.prototype.has:null,p="function"==typeof WeakSet&&WeakSet.prototype?WeakSet.prototype.has:null,y="function"==typeof WeakRef&&WeakRef.prototype?WeakRef.prototype.deref:null,d=Boolean.prototype.valueOf,h=Object.prototype.toString,g=Function.prototype.toString,m=String.prototype.match,b=String.prototype.slice,v=String.prototype.replace,w=String.prototype.toUpperCase,x=String.prototype.toLowerCase,S=RegExp.prototype.test,E=Array.prototype.concat,A=Array.prototype.join,O=Array.prototype.slice,j=Math.floor,T="function"==typeof BigInt?BigInt.prototype.valueOf:null,P=Object.getOwnPropertySymbols,R="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,C="function"==typeof Symbol&&"object"==typeof Symbol.iterator,I="function"==typeof Symbol&&Symbol.toStringTag&&(Symbol.toStringTag,1)?Symbol.toStringTag:null,N=Object.prototype.propertyIsEnumerable,M=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(t){return t.__proto__}:null);function $(t,e){if(t===1/0||t===-1/0||t!=t||t&&t>-1e3&&t<1e3||S.call(/e/,e))return e;var r=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof t){var n=t<0?-j(-t):j(t);if(n!==t){var o=String(n),i=b.call(e,o.length+1);return v.call(o,r,"$&_")+"."+v.call(v.call(i,/([0-9]{3})/g,"$&_"),/_$/,"")}}return v.call(e,r,"$&_")}var k=r(1740),D=k.custom,F=U(D)?D:null;function B(t,e,r){var n="double"===(r.quoteStyle||e)?'"':"'";return n+t+n}function L(t){return v.call(String(t),/"/g,""")}function _(t){return!("[object Array]"!==H(t)||I&&"object"==typeof t&&I in t)}function W(t){return!("[object RegExp]"!==H(t)||I&&"object"==typeof t&&I in t)}function U(t){if(C)return t&&"object"==typeof t&&t instanceof Symbol;if("symbol"==typeof t)return!0;if(!t||"object"!=typeof t||!R)return!1;try{return R.call(t),!0}catch(t){}return!1}t.exports=function t(e,n,o,u){var c=n||{};if(G(c,"quoteStyle")&&"single"!==c.quoteStyle&&"double"!==c.quoteStyle)throw new TypeError('option "quoteStyle" must be "single" or "double"');if(G(c,"maxStringLength")&&("number"==typeof c.maxStringLength?c.maxStringLength<0&&c.maxStringLength!==1/0:null!==c.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var h=!G(c,"customInspect")||c.customInspect;if("boolean"!=typeof h&&"symbol"!==h)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(G(c,"indent")&&null!==c.indent&&"\t"!==c.indent&&!(parseInt(c.indent,10)===c.indent&&c.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(G(c,"numericSeparator")&&"boolean"!=typeof c.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var w=c.numericSeparator;if(void 0===e)return"undefined";if(null===e)return"null";if("boolean"==typeof e)return e?"true":"false";if("string"==typeof e)return q(e,c);if("number"==typeof e){if(0===e)return 1/0/e>0?"0":"-0";var S=String(e);return w?$(e,S):S}if("bigint"==typeof e){var j=String(e)+"n";return w?$(e,j):j}var P=void 0===c.depth?5:c.depth;if(void 0===o&&(o=0),o>=P&&P>0&&"object"==typeof e)return _(e)?"[Array]":"[Object]";var D,z=function(t,e){var r;if("\t"===t.indent)r="\t";else{if(!("number"==typeof t.indent&&t.indent>0))return null;r=A.call(Array(t.indent+1)," ")}return{base:r,prev:A.call(Array(e+1),r)}}(c,o);if(void 0===u)u=[];else if(V(u,e)>=0)return"[Circular]";function X(e,r,n){if(r&&(u=O.call(u)).push(r),n){var i={depth:c.depth};return G(c,"quoteStyle")&&(i.quoteStyle=c.quoteStyle),t(e,i,o+1,u)}return t(e,c,o+1,u)}if("function"==typeof e&&!W(e)){var tt=function(t){if(t.name)return t.name;var e=m.call(g.call(t),/^function\s*([\w$]+)/);return e?e[1]:null}(e),et=Z(e,X);return"[Function"+(tt?": "+tt:" (anonymous)")+"]"+(et.length>0?" { "+A.call(et,", ")+" }":"")}if(U(e)){var rt=C?v.call(String(e),/^(Symbol\(.*\))_[^)]*$/,"$1"):R.call(e);return"object"!=typeof e||C?rt:K(rt)}if((D=e)&&"object"==typeof D&&("undefined"!=typeof HTMLElement&&D instanceof HTMLElement||"string"==typeof D.nodeName&&"function"==typeof D.getAttribute)){for(var nt="<"+x.call(String(e.nodeName)),ot=e.attributes||[],it=0;it"}if(_(e)){if(0===e.length)return"[]";var at=Z(e,X);return z&&!function(t){for(var e=0;e=0)return!1;return!0}(at)?"["+Q(at,z)+"]":"[ "+A.call(at,", ")+" ]"}if(function(t){return!("[object Error]"!==H(t)||I&&"object"==typeof t&&I in t)}(e)){var ut=Z(e,X);return"cause"in Error.prototype||!("cause"in e)||N.call(e,"cause")?0===ut.length?"["+String(e)+"]":"{ ["+String(e)+"] "+A.call(ut,", ")+" }":"{ ["+String(e)+"] "+A.call(E.call("[cause]: "+X(e.cause),ut),", ")+" }"}if("object"==typeof e&&h){if(F&&"function"==typeof e[F]&&k)return k(e,{depth:P-o});if("symbol"!==h&&"function"==typeof e.inspect)return e.inspect()}if(function(t){if(!i||!t||"object"!=typeof t)return!1;try{i.call(t);try{s.call(t)}catch(t){return!0}return t instanceof Map}catch(t){}return!1}(e)){var ct=[];return a&&a.call(e,(function(t,r){ct.push(X(r,e,!0)+" => "+X(t,e))})),J("Map",i.call(e),ct,z)}if(function(t){if(!s||!t||"object"!=typeof t)return!1;try{s.call(t);try{i.call(t)}catch(t){return!0}return t instanceof Set}catch(t){}return!1}(e)){var st=[];return l&&l.call(e,(function(t){st.push(X(t,e))})),J("Set",s.call(e),st,z)}if(function(t){if(!f||!t||"object"!=typeof t)return!1;try{f.call(t,f);try{p.call(t,p)}catch(t){return!0}return t instanceof WeakMap}catch(t){}return!1}(e))return Y("WeakMap");if(function(t){if(!p||!t||"object"!=typeof t)return!1;try{p.call(t,p);try{f.call(t,f)}catch(t){return!0}return t instanceof WeakSet}catch(t){}return!1}(e))return Y("WeakSet");if(function(t){if(!y||!t||"object"!=typeof t)return!1;try{return y.call(t),!0}catch(t){}return!1}(e))return Y("WeakRef");if(function(t){return!("[object Number]"!==H(t)||I&&"object"==typeof t&&I in t)}(e))return K(X(Number(e)));if(function(t){if(!t||"object"!=typeof t||!T)return!1;try{return T.call(t),!0}catch(t){}return!1}(e))return K(X(T.call(e)));if(function(t){return!("[object Boolean]"!==H(t)||I&&"object"==typeof t&&I in t)}(e))return K(d.call(e));if(function(t){return!("[object String]"!==H(t)||I&&"object"==typeof t&&I in t)}(e))return K(X(String(e)));if("undefined"!=typeof window&&e===window)return"{ [object Window] }";if(e===r.g)return"{ [object globalThis] }";if(!function(t){return!("[object Date]"!==H(t)||I&&"object"==typeof t&&I in t)}(e)&&!W(e)){var lt=Z(e,X),ft=M?M(e)===Object.prototype:e instanceof Object||e.constructor===Object,pt=e instanceof Object?"":"null prototype",yt=!ft&&I&&Object(e)===e&&I in e?b.call(H(e),8,-1):pt?"Object":"",dt=(ft||"function"!=typeof e.constructor?"":e.constructor.name?e.constructor.name+" ":"")+(yt||pt?"["+A.call(E.call([],yt||[],pt||[]),": ")+"] ":"");return 0===lt.length?dt+"{}":z?dt+"{"+Q(lt,z)+"}":dt+"{ "+A.call(lt,", ")+" }"}return String(e)};var z=Object.prototype.hasOwnProperty||function(t){return t in this};function G(t,e){return z.call(t,e)}function H(t){return h.call(t)}function V(t,e){if(t.indexOf)return t.indexOf(e);for(var r=0,n=t.length;re.maxStringLength){var r=t.length-e.maxStringLength,n="... "+r+" more character"+(r>1?"s":"");return q(b.call(t,0,e.maxStringLength),e)+n}return B(v.call(v.call(t,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,X),"single",e)}function X(t){var e=t.charCodeAt(0),r={8:"b",9:"t",10:"n",12:"f",13:"r"}[e];return r?"\\"+r:"\\x"+(e<16?"0":"")+w.call(e.toString(16))}function K(t){return"Object("+t+")"}function Y(t){return t+" { ? }"}function J(t,e,r,n){return t+" ("+e+") {"+(n?Q(r,n):A.call(r,", "))+"}"}function Q(t,e){if(0===t.length)return"";var r="\n"+e.prev+e.base;return r+A.call(t,","+r)+"\n"+e.prev}function Z(t,e){var r=_(t),n=[];if(r){n.length=t.length;for(var o=0;o{"use strict";var n;if(!Object.keys){var o=Object.prototype.hasOwnProperty,i=Object.prototype.toString,a=r(9096),u=Object.prototype.propertyIsEnumerable,c=!u.call({toString:null},"toString"),s=u.call((function(){}),"prototype"),l=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],f=function(t){var e=t.constructor;return e&&e.prototype===t},p={$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},y=function(){if("undefined"==typeof window)return!1;for(var t in window)try{if(!p["$"+t]&&o.call(window,t)&&null!==window[t]&&"object"==typeof window[t])try{f(window[t])}catch(t){return!0}}catch(t){return!0}return!1}();n=function(t){var e=null!==t&&"object"==typeof t,r="[object Function]"===i.call(t),n=a(t),u=e&&"[object String]"===i.call(t),p=[];if(!e&&!r&&!n)throw new TypeError("Object.keys called on a non-object");var d=s&&r;if(u&&t.length>0&&!o.call(t,0))for(var h=0;h0)for(var g=0;g{"use strict";var n=Array.prototype.slice,o=r(9096),i=Object.keys,a=i?function(t){return i(t)}:r(9560),u=Object.keys;a.shim=function(){if(Object.keys){var t=function(){var t=Object.keys(arguments);return t&&t.length===arguments.length}(1,2);t||(Object.keys=function(t){return o(t)?u(n.call(t)):u(t)})}else Object.keys=a;return Object.keys||a},t.exports=a},9096:t=>{"use strict";var e=Object.prototype.toString;t.exports=function(t){var r=e.call(t),n="[object Arguments]"===r;return n||(n="[object Array]"!==r&&null!==t&&"object"==typeof t&&"number"==typeof t.length&&t.length>=0&&"[object Function]"===e.call(t.callee)),n}},7636:(t,e,r)=>{"use strict";var n=r(6308),o=r(2824),i=Object;t.exports=n((function(){if(null==this||this!==i(this))throw new o("RegExp.prototype.flags getter called on non-object");var t="";return this.hasIndices&&(t+="d"),this.global&&(t+="g"),this.ignoreCase&&(t+="i"),this.multiline&&(t+="m"),this.dotAll&&(t+="s"),this.unicode&&(t+="u"),this.unicodeSets&&(t+="v"),this.sticky&&(t+="y"),t}),"get flags",!0)},2192:(t,e,r)=>{"use strict";var n=r(2732),o=r(5096),i=r(7636),a=r(9296),u=r(736),c=o(a());n(c,{getPolyfill:a,implementation:i,shim:u}),t.exports=c},9296:(t,e,r)=>{"use strict";var n=r(7636),o=r(2732).supportsDescriptors,i=Object.getOwnPropertyDescriptor;t.exports=function(){if(o&&"gim"===/a/gim.flags){var t=i(RegExp.prototype,"flags");if(t&&"function"==typeof t.get&&"boolean"==typeof RegExp.prototype.dotAll&&"boolean"==typeof RegExp.prototype.hasIndices){var e="",r={};if(Object.defineProperty(r,"hasIndices",{get:function(){e+="d"}}),Object.defineProperty(r,"sticky",{get:function(){e+="y"}}),"dy"===e)return t.get}}return n}},736:(t,e,r)=>{"use strict";var n=r(2732).supportsDescriptors,o=r(9296),i=Object.getOwnPropertyDescriptor,a=Object.defineProperty,u=TypeError,c=Object.getPrototypeOf,s=/a/;t.exports=function(){if(!n||!c)throw new u("RegExp.prototype.flags requires a true ES5 environment that supports property descriptors");var t=o(),e=c(s),r=i(e,"flags");return r&&r.get===t||a(e,"flags",{configurable:!0,enumerable:!1,get:t}),t}},860:(t,e,r)=>{"use strict";var n=r(668),o=r(1476),i=n("RegExp.prototype.exec"),a=r(2824);t.exports=function(t){if(!o(t))throw new a("`regex` must be a RegExp");return function(e){return null!==i(t,e)}}},5676:(t,e,r)=>{"use strict";var n=r(4624),o=r(2448),i=r(3268)(),a=r(6168),u=r(2824),c=n("%Math.floor%");t.exports=function(t,e){if("function"!=typeof t)throw new u("`fn` is not a function");if("number"!=typeof e||e<0||e>4294967295||c(e)!==e)throw new u("`length` must be a positive 32-bit integer");var r=arguments.length>2&&!!arguments[2],n=!0,s=!0;if("length"in t&&a){var l=a(t,"length");l&&!l.configurable&&(n=!1),l&&!l.writable&&(s=!1)}return(n||s||!r)&&(i?o(t,"length",e,!0,!0):o(t,"length",e)),t}},6308:(t,e,r)=>{"use strict";var n=r(2448),o=r(3268)(),i=r(2656).functionsHaveConfigurableNames(),a=TypeError;t.exports=function(t,e){if("function"!=typeof t)throw new a("`fn` is not a function");return arguments.length>2&&!!arguments[2]&&!i||(o?n(t,"name",e,!0,!0):n(t,"name",e)),t}},3147:(t,e,r)=>{"use strict";var n=r(4624),o=r(668),i=r(4152),a=r(2824),u=n("%WeakMap%",!0),c=n("%Map%",!0),s=o("WeakMap.prototype.get",!0),l=o("WeakMap.prototype.set",!0),f=o("WeakMap.prototype.has",!0),p=o("Map.prototype.get",!0),y=o("Map.prototype.set",!0),d=o("Map.prototype.has",!0),h=function(t,e){for(var r,n=t;null!==(r=n.next);n=r)if(r.key===e)return n.next=r.next,r.next=t.next,t.next=r,r};t.exports=function(){var t,e,r,n={assert:function(t){if(!n.has(t))throw new a("Side channel does not contain "+i(t))},get:function(n){if(u&&n&&("object"==typeof n||"function"==typeof n)){if(t)return s(t,n)}else if(c){if(e)return p(e,n)}else if(r)return function(t,e){var r=h(t,e);return r&&r.value}(r,n)},has:function(n){if(u&&n&&("object"==typeof n||"function"==typeof n)){if(t)return f(t,n)}else if(c){if(e)return d(e,n)}else if(r)return function(t,e){return!!h(t,e)}(r,n);return!1},set:function(n,o){u&&n&&("object"==typeof n||"function"==typeof n)?(t||(t=new u),l(t,n,o)):c?(e||(e=new c),y(e,n,o)):(r||(r={key:{},next:null}),function(t,e,r){var n=h(t,e);n?n.value=r:t.next={key:e,next:t.next,value:r}}(r,n,o))}};return n}},9508:(t,e,r)=>{"use strict";var n=r(1700),o=r(3672),i=r(5552),a=r(3816),u=r(5424),c=r(4656),s=r(668),l=r(9800)(),f=r(2192),p=s("String.prototype.indexOf"),y=r(6288),d=function(t){var e=y();if(l&&"symbol"==typeof Symbol.matchAll){var r=i(t,Symbol.matchAll);return r===RegExp.prototype[Symbol.matchAll]&&r!==e?e:r}if(a(t))return e};t.exports=function(t){var e=c(this);if(null!=t){if(a(t)){var r="flags"in t?o(t,"flags"):f(t);if(c(r),p(u(r),"g")<0)throw new TypeError("matchAll requires a global regular expression")}var i=d(t);if(void 0!==i)return n(i,t,[e])}var s=u(e),l=new RegExp(t,"g");return n(d(l),l,[s])}},3732:(t,e,r)=>{"use strict";var n=r(5096),o=r(2732),i=r(9508),a=r(5844),u=r(4148),c=n(i);o(c,{getPolyfill:a,implementation:i,shim:u}),t.exports=c},6288:(t,e,r)=>{"use strict";var n=r(9800)(),o=r(7492);t.exports=function(){return n&&"symbol"==typeof Symbol.matchAll&&"function"==typeof RegExp.prototype[Symbol.matchAll]?RegExp.prototype[Symbol.matchAll]:o}},5844:(t,e,r)=>{"use strict";var n=r(9508);t.exports=function(){if(String.prototype.matchAll)try{"".matchAll(RegExp.prototype)}catch(t){return String.prototype.matchAll}return n}},7492:(t,e,r)=>{"use strict";var n=r(5211),o=r(3672),i=r(4e3),a=r(8652),u=r(4784),c=r(5424),s=r(8645),l=r(2192),f=r(6308),p=r(668)("String.prototype.indexOf"),y=RegExp,d="flags"in RegExp.prototype,h=f((function(t){var e=this;if("Object"!==s(e))throw new TypeError('"this" value must be an Object');var r=c(t),f=function(t,e){var r="flags"in e?o(e,"flags"):c(l(e));return{flags:r,matcher:new t(d&&"string"==typeof r?e:t===y?e.source:e,r)}}(a(e,y),e),h=f.flags,g=f.matcher,m=u(o(e,"lastIndex"));i(g,"lastIndex",m,!0);var b=p(h,"g")>-1,v=p(h,"u")>-1;return n(g,r,b,v)}),"[Symbol.matchAll]",!0);t.exports=h},4148:(t,e,r)=>{"use strict";var n=r(2732),o=r(9800)(),i=r(5844),a=r(6288),u=Object.defineProperty,c=Object.getOwnPropertyDescriptor;t.exports=function(){var t=i();if(n(String.prototype,{matchAll:t},{matchAll:function(){return String.prototype.matchAll!==t}}),o){var e=Symbol.matchAll||(Symbol.for?Symbol.for("Symbol.matchAll"):Symbol("Symbol.matchAll"));if(n(Symbol,{matchAll:e},{matchAll:function(){return Symbol.matchAll!==e}}),u&&c){var r=c(Symbol,e);r&&!r.configurable||u(Symbol,e,{configurable:!1,enumerable:!1,value:e,writable:!1})}var s=a(),l={};l[e]=s;var f={};f[e]=function(){return RegExp.prototype[e]!==s},n(RegExp.prototype,l,f)}return t}},6936:(t,e,r)=>{"use strict";var n=r(4656),o=r(5424),i=r(668)("String.prototype.replace"),a=/^\s$/.test("᠎"),u=a?/^[\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]+/,c=a?/[\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]+$/;t.exports=function(){var t=o(n(this));return i(i(t,u,""),c,"")}},9292:(t,e,r)=>{"use strict";var n=r(5096),o=r(2732),i=r(4656),a=r(6936),u=r(6684),c=r(9788),s=n(u()),l=function(t){return i(t),s(t)};o(l,{getPolyfill:u,implementation:a,shim:c}),t.exports=l},6684:(t,e,r)=>{"use strict";var n=r(6936);t.exports=function(){return String.prototype.trim&&"​"==="​".trim()&&"᠎"==="᠎".trim()&&"_᠎"==="_᠎".trim()&&"᠎_"==="᠎_".trim()?String.prototype.trim:n}},9788:(t,e,r)=>{"use strict";var n=r(2732),o=r(6684);t.exports=function(){var t=o();return n(String.prototype,{trim:t},{trim:function(){return String.prototype.trim!==t}}),t}},1740:()=>{},1056:(t,e,r)=>{"use strict";var n=r(4624),o=r(8536),i=r(8645),a=r(7724),u=r(9132),c=n("%TypeError%");t.exports=function(t,e,r){if("String"!==i(t))throw new c("Assertion failed: `S` must be a String");if(!a(e)||e<0||e>u)throw new c("Assertion failed: `length` must be an integer >= 0 and <= 2**53");if("Boolean"!==i(r))throw new c("Assertion failed: `unicode` must be a Boolean");return r?e+1>=t.length?e+1:e+o(t,e)["[[CodeUnitCount]]"]:e+1}},1700:(t,e,r)=>{"use strict";var n=r(4624),o=r(668),i=n("%TypeError%"),a=r(1720),u=n("%Reflect.apply%",!0)||o("Function.prototype.apply");t.exports=function(t,e){var r=arguments.length>2?arguments[2]:[];if(!a(r))throw new i("Assertion failed: optional `argumentsList`, if provided, must be a List");return u(t,e,r)}},8536:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(668),i=r(1712),a=r(8444),u=r(8645),c=r(2320),s=o("String.prototype.charAt"),l=o("String.prototype.charCodeAt");t.exports=function(t,e){if("String"!==u(t))throw new n("Assertion failed: `string` must be a String");var r=t.length;if(e<0||e>=r)throw new n("Assertion failed: `position` must be >= 0, and < the length of `string`");var o=l(t,e),f=s(t,e),p=i(o),y=a(o);if(!p&&!y)return{"[[CodePoint]]":f,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!1};if(y||e+1===r)return{"[[CodePoint]]":f,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!0};var d=l(t,e+1);return a(d)?{"[[CodePoint]]":c(o,d),"[[CodeUnitCount]]":2,"[[IsUnpairedSurrogate]]":!1}:{"[[CodePoint]]":f,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!0}}},4288:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(8645);t.exports=function(t,e){if("Boolean"!==o(e))throw new n("Assertion failed: Type(done) is not Boolean");return{value:t,done:e}}},2672:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(4436),i=r(8924),a=r(3880),u=r(2968),c=r(8800),s=r(8645);t.exports=function(t,e,r){if("Object"!==s(t))throw new n("Assertion failed: Type(O) is not Object");if(!u(e))throw new n("Assertion failed: IsPropertyKey(P) is not true");return o(a,c,i,t,e,{"[[Configurable]]":!0,"[[Enumerable]]":!1,"[[Value]]":r,"[[Writable]]":!0})}},5211:(t,e,r)=>{"use strict";var n=r(4624),o=r(9800)(),i=n("%TypeError%"),a=n("%IteratorPrototype%",!0),u=r(1056),c=r(4288),s=r(2672),l=r(3672),f=r(6216),p=r(8972),y=r(4e3),d=r(4784),h=r(5424),g=r(8645),m=r(7284),b=r(9200),v=function(t,e,r,n){if("String"!==g(e))throw new i("`S` must be a string");if("Boolean"!==g(r))throw new i("`global` must be a boolean");if("Boolean"!==g(n))throw new i("`fullUnicode` must be a boolean");m.set(this,"[[IteratingRegExp]]",t),m.set(this,"[[IteratedString]]",e),m.set(this,"[[Global]]",r),m.set(this,"[[Unicode]]",n),m.set(this,"[[Done]]",!1)};a&&(v.prototype=f(a)),s(v.prototype,"next",(function(){var t=this;if("Object"!==g(t))throw new i("receiver must be an object");if(!(t instanceof v&&m.has(t,"[[IteratingRegExp]]")&&m.has(t,"[[IteratedString]]")&&m.has(t,"[[Global]]")&&m.has(t,"[[Unicode]]")&&m.has(t,"[[Done]]")))throw new i('"this" value must be a RegExpStringIterator instance');if(m.get(t,"[[Done]]"))return c(void 0,!0);var e=m.get(t,"[[IteratingRegExp]]"),r=m.get(t,"[[IteratedString]]"),n=m.get(t,"[[Global]]"),o=m.get(t,"[[Unicode]]"),a=p(e,r);if(null===a)return m.set(t,"[[Done]]",!0),c(void 0,!0);if(n){if(""===h(l(a,"0"))){var s=d(l(e,"lastIndex")),f=u(r,s,o);y(e,"lastIndex",f,!0)}return c(a,!1)}return m.set(t,"[[Done]]",!0),c(a,!1)})),o&&(b(v.prototype,"RegExp String Iterator"),Symbol.iterator&&"function"!=typeof v.prototype[Symbol.iterator])&&s(v.prototype,Symbol.iterator,(function(){return this})),t.exports=function(t,e,r,n){return new v(t,e,r,n)}},7268:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(320),i=r(4436),a=r(8924),u=r(4936),c=r(3880),s=r(2968),l=r(8800),f=r(5696),p=r(8645);t.exports=function(t,e,r){if("Object"!==p(t))throw new n("Assertion failed: Type(O) is not Object");if(!s(e))throw new n("Assertion failed: IsPropertyKey(P) is not true");var y=o({Type:p,IsDataDescriptor:c,IsAccessorDescriptor:u},r)?r:f(r);if(!o({Type:p,IsDataDescriptor:c,IsAccessorDescriptor:u},y))throw new n("Assertion failed: Desc is not a valid Property Descriptor");return i(c,l,a,t,e,y)}},8924:(t,e,r)=>{"use strict";var n=r(3600),o=r(3504),i=r(8645);t.exports=function(t){return void 0!==t&&n(i,"Property Descriptor","Desc",t),o(t)}},3672:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(4152),i=r(2968),a=r(8645);t.exports=function(t,e){if("Object"!==a(t))throw new n("Assertion failed: Type(O) is not Object");if(!i(e))throw new n("Assertion failed: IsPropertyKey(P) is not true, got "+o(e));return t[e]}},5552:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(3396),i=r(3048),a=r(2968),u=r(4152);t.exports=function(t,e){if(!a(e))throw new n("Assertion failed: IsPropertyKey(P) is not true");var r=o(t,e);if(null!=r){if(!i(r))throw new n(u(e)+" is not a function: "+u(r));return r}}},3396:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(4152),i=r(2968);t.exports=function(t,e){if(!i(e))throw new n("Assertion failed: IsPropertyKey(P) is not true, got "+o(e));return t[e]}},4936:(t,e,r)=>{"use strict";var n=r(4440),o=r(8645),i=r(3600);t.exports=function(t){return void 0!==t&&(i(o,"Property Descriptor","Desc",t),!(!n(t,"[[Get]]")&&!n(t,"[[Set]]")))}},1720:(t,e,r)=>{"use strict";t.exports=r(704)},3048:(t,e,r)=>{"use strict";t.exports=r(648)},211:(t,e,r)=>{"use strict";var n=r(8600)("%Reflect.construct%",!0),o=r(7268);try{o({},"",{"[[Get]]":function(){}})}catch(t){o=null}if(o&&n){var i={},a={};o(a,"length",{"[[Get]]":function(){throw i},"[[Enumerable]]":!0}),t.exports=function(t){try{n(t,a)}catch(t){return t===i}}}else t.exports=function(t){return"function"==typeof t&&!!t.prototype}},3880:(t,e,r)=>{"use strict";var n=r(4440),o=r(8645),i=r(3600);t.exports=function(t){return void 0!==t&&(i(o,"Property Descriptor","Desc",t),!(!n(t,"[[Value]]")&&!n(t,"[[Writable]]")))}},2968:t=>{"use strict";t.exports=function(t){return"string"==typeof t||"symbol"==typeof t}},3816:(t,e,r)=>{"use strict";var n=r(4624)("%Symbol.match%",!0),o=r(1476),i=r(6848);t.exports=function(t){if(!t||"object"!=typeof t)return!1;if(n){var e=t[n];if(void 0!==e)return i(e)}return o(t)}},6216:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Object.create%",!0),i=n("%TypeError%"),a=n("%SyntaxError%"),u=r(1720),c=r(8645),s=r(4672),l=r(7284),f=r(7e3)();t.exports=function(t){if(null!==t&&"Object"!==c(t))throw new i("Assertion failed: `proto` must be null or an object");var e,r=arguments.length<2?[]:arguments[1];if(!u(r))throw new i("Assertion failed: `additionalInternalSlotsList` must be an Array");if(o)e=o(t);else if(f)e={__proto__:t};else{if(null===t)throw new a("native Object.create support is required to create null objects");var n=function(){};n.prototype=t,e=new n}return r.length>0&&s(r,(function(t){l.set(e,t,void 0)})),e}},8972:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(668)("RegExp.prototype.exec"),i=r(1700),a=r(3672),u=r(3048),c=r(8645);t.exports=function(t,e){if("Object"!==c(t))throw new n("Assertion failed: `R` must be an Object");if("String"!==c(e))throw new n("Assertion failed: `S` must be a String");var r=a(t,"exec");if(u(r)){var s=i(r,t,[e]);if(null===s||"Object"===c(s))return s;throw new n('"exec" method must return `null` or an Object')}return o(t,e)}},4656:(t,e,r)=>{"use strict";t.exports=r(176)},8800:(t,e,r)=>{"use strict";var n=r(2808);t.exports=function(t,e){return t===e?0!==t||1/t==1/e:n(t)&&n(e)}},4e3:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(2968),i=r(8800),a=r(8645),u=function(){try{return delete[].length,!0}catch(t){return!1}}();t.exports=function(t,e,r,c){if("Object"!==a(t))throw new n("Assertion failed: `O` must be an Object");if(!o(e))throw new n("Assertion failed: `P` must be a Property Key");if("Boolean"!==a(c))throw new n("Assertion failed: `Throw` must be a Boolean");if(c){if(t[e]=r,u&&!i(t[e],r))throw new n("Attempted to assign to readonly property.");return!0}try{return t[e]=r,!u||i(t[e],r)}catch(t){return!1}}},8652:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Symbol.species%",!0),i=n("%TypeError%"),a=r(211),u=r(8645);t.exports=function(t,e){if("Object"!==u(t))throw new i("Assertion failed: Type(O) is not Object");var r=t.constructor;if(void 0===r)return e;if("Object"!==u(r))throw new i("O.constructor is not an Object");var n=o?r[o]:void 0;if(null==n)return e;if(a(n))return n;throw new i("no constructor found")}},8772:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Number%"),i=n("%RegExp%"),a=n("%TypeError%"),u=n("%parseInt%"),c=r(668),s=r(860),l=c("String.prototype.slice"),f=s(/^0b[01]+$/i),p=s(/^0o[0-7]+$/i),y=s(/^[-+]0x[0-9a-f]+$/i),d=s(new i("["+["…","​","￾"].join("")+"]","g")),h=r(9292),g=r(8645);t.exports=function t(e){if("String"!==g(e))throw new a("Assertion failed: `argument` is not a String");if(f(e))return o(u(l(e,2),2));if(p(e))return o(u(l(e,2),8));if(d(e)||y(e))return NaN;var r=h(e);return r!==e?t(r):o(e)}},6848:t=>{"use strict";t.exports=function(t){return!!t}},9424:(t,e,r)=>{"use strict";var n=r(7220),o=r(2592),i=r(2808),a=r(2931);t.exports=function(t){var e=n(t);return i(e)||0===e?0:a(e)?o(e):e}},4784:(t,e,r)=>{"use strict";var n=r(9132),o=r(9424);t.exports=function(t){var e=o(t);return e<=0?0:e>n?n:e}},7220:(t,e,r)=>{"use strict";var n=r(4624),o=n("%TypeError%"),i=n("%Number%"),a=r(2336),u=r(5556),c=r(8772);t.exports=function(t){var e=a(t)?t:u(t,i);if("symbol"==typeof e)throw new o("Cannot convert a Symbol value to a number");if("bigint"==typeof e)throw new o("Conversion from 'BigInt' to 'number' is not allowed.");return"string"==typeof e?c(e):i(e)}},5556:(t,e,r)=>{"use strict";var n=r(108);t.exports=function(t){return arguments.length>1?n(t,arguments[1]):n(t)}},5696:(t,e,r)=>{"use strict";var n=r(4440),o=r(4624)("%TypeError%"),i=r(8645),a=r(6848),u=r(3048);t.exports=function(t){if("Object"!==i(t))throw new o("ToPropertyDescriptor requires an object");var e={};if(n(t,"enumerable")&&(e["[[Enumerable]]"]=a(t.enumerable)),n(t,"configurable")&&(e["[[Configurable]]"]=a(t.configurable)),n(t,"value")&&(e["[[Value]]"]=t.value),n(t,"writable")&&(e["[[Writable]]"]=a(t.writable)),n(t,"get")){var r=t.get;if(void 0!==r&&!u(r))throw new o("getter must be a function");e["[[Get]]"]=r}if(n(t,"set")){var c=t.set;if(void 0!==c&&!u(c))throw new o("setter must be a function");e["[[Set]]"]=c}if((n(e,"[[Get]]")||n(e,"[[Set]]"))&&(n(e,"[[Value]]")||n(e,"[[Writable]]")))throw new o("Invalid property descriptor. Cannot both specify accessors and a value or writable attribute");return e}},5424:(t,e,r)=>{"use strict";var n=r(4624),o=n("%String%"),i=n("%TypeError%");t.exports=function(t){if("symbol"==typeof t)throw new i("Cannot convert a Symbol value to a string");return o(t)}},8645:(t,e,r)=>{"use strict";var n=r(7936);t.exports=function(t){return"symbol"==typeof t?"Symbol":"bigint"==typeof t?"BigInt":n(t)}},2320:(t,e,r)=>{"use strict";var n=r(4624),o=n("%TypeError%"),i=n("%String.fromCharCode%"),a=r(1712),u=r(8444);t.exports=function(t,e){if(!a(t)||!u(e))throw new o("Assertion failed: `lead` must be a leading surrogate char code, and `trail` must be a trailing surrogate char code");return i(t)+i(e)}},2312:(t,e,r)=>{"use strict";var n=r(8645),o=Math.floor;t.exports=function(t){return"BigInt"===n(t)?t:o(t)}},2592:(t,e,r)=>{"use strict";var n=r(4624),o=r(2312),i=n("%TypeError%");t.exports=function(t){if("number"!=typeof t&&"bigint"!=typeof t)throw new i("argument must be a Number or a BigInt");var e=t<0?-o(-t):o(t);return 0===e?0:e}},176:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%");t.exports=function(t,e){if(null==t)throw new n(e||"Cannot call method on "+t);return t}},7936:t=>{"use strict";t.exports=function(t){return null===t?"Null":void 0===t?"Undefined":"function"==typeof t||"object"==typeof t?"Object":"number"==typeof t?"Number":"boolean"==typeof t?"Boolean":"string"==typeof t?"String":void 0}},8600:(t,e,r)=>{"use strict";t.exports=r(4624)},4436:(t,e,r)=>{"use strict";var n=r(3268),o=r(4624),i=n()&&o("%Object.defineProperty%",!0),a=n.hasArrayLengthDefineBug(),u=a&&r(704),c=r(668)("Object.prototype.propertyIsEnumerable");t.exports=function(t,e,r,n,o,s){if(!i){if(!t(s))return!1;if(!s["[[Configurable]]"]||!s["[[Writable]]"])return!1;if(o in n&&c(n,o)!==!!s["[[Enumerable]]"])return!1;var l=s["[[Value]]"];return n[o]=l,e(n[o],l)}return a&&"length"===o&&"[[Value]]"in s&&u(n)&&n.length!==s["[[Value]]"]?(n.length=s["[[Value]]"],n.length===s["[[Value]]"]):(i(n,o,r(s)),!0)}},704:(t,e,r)=>{"use strict";var n=r(4624)("%Array%"),o=!n.isArray&&r(668)("Object.prototype.toString");t.exports=n.isArray||function(t){return"[object Array]"===o(t)}},3600:(t,e,r)=>{"use strict";var n=r(4624),o=n("%TypeError%"),i=n("%SyntaxError%"),a=r(4440),u=r(7724),c={"Property Descriptor":function(t){var e={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};if(!t)return!1;for(var r in t)if(a(t,r)&&!e[r])return!1;var n=a(t,"[[Value]]"),i=a(t,"[[Get]]")||a(t,"[[Set]]");if(n&&i)throw new o("Property Descriptors may not be both accessor and data descriptors");return!0},"Match Record":r(5092),"Iterator Record":function(t){return a(t,"[[Iterator]]")&&a(t,"[[NextMethod]]")&&a(t,"[[Done]]")},"PromiseCapability Record":function(t){return!!t&&a(t,"[[Resolve]]")&&"function"==typeof t["[[Resolve]]"]&&a(t,"[[Reject]]")&&"function"==typeof t["[[Reject]]"]&&a(t,"[[Promise]]")&&t["[[Promise]]"]&&"function"==typeof t["[[Promise]]"].then},"AsyncGeneratorRequest Record":function(t){return!!t&&a(t,"[[Completion]]")&&a(t,"[[Capability]]")&&c["PromiseCapability Record"](t["[[Capability]]"])},"RegExp Record":function(t){return t&&a(t,"[[IgnoreCase]]")&&"boolean"==typeof t["[[IgnoreCase]]"]&&a(t,"[[Multiline]]")&&"boolean"==typeof t["[[Multiline]]"]&&a(t,"[[DotAll]]")&&"boolean"==typeof t["[[DotAll]]"]&&a(t,"[[Unicode]]")&&"boolean"==typeof t["[[Unicode]]"]&&a(t,"[[CapturingGroupsCount]]")&&"number"==typeof t["[[CapturingGroupsCount]]"]&&u(t["[[CapturingGroupsCount]]"])&&t["[[CapturingGroupsCount]]"]>=0}};t.exports=function(t,e,r,n){var a=c[e];if("function"!=typeof a)throw new i("unknown record type: "+e);if("Object"!==t(n)||!a(n))throw new o(r+" must be a "+e)}},4672:t=>{"use strict";t.exports=function(t,e){for(var r=0;r{"use strict";t.exports=function(t){if(void 0===t)return t;var e={};return"[[Value]]"in t&&(e.value=t["[[Value]]"]),"[[Writable]]"in t&&(e.writable=!!t["[[Writable]]"]),"[[Get]]"in t&&(e.get=t["[[Get]]"]),"[[Set]]"in t&&(e.set=t["[[Set]]"]),"[[Enumerable]]"in t&&(e.enumerable=!!t["[[Enumerable]]"]),"[[Configurable]]"in t&&(e.configurable=!!t["[[Configurable]]"]),e}},2931:(t,e,r)=>{"use strict";var n=r(2808);t.exports=function(t){return("number"==typeof t||"bigint"==typeof t)&&!n(t)&&t!==1/0&&t!==-1/0}},7724:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Math.abs%"),i=n("%Math.floor%"),a=r(2808),u=r(2931);t.exports=function(t){if("number"!=typeof t||a(t)||!u(t))return!1;var e=o(t);return i(e)===e}},1712:t=>{"use strict";t.exports=function(t){return"number"==typeof t&&t>=55296&&t<=56319}},5092:(t,e,r)=>{"use strict";var n=r(4440);t.exports=function(t){return n(t,"[[StartIndex]]")&&n(t,"[[EndIndex]]")&&t["[[StartIndex]]"]>=0&&t["[[EndIndex]]"]>=t["[[StartIndex]]"]&&String(parseInt(t["[[StartIndex]]"],10))===String(t["[[StartIndex]]"])&&String(parseInt(t["[[EndIndex]]"],10))===String(t["[[EndIndex]]"])}},2808:t=>{"use strict";t.exports=Number.isNaN||function(t){return t!=t}},2336:t=>{"use strict";t.exports=function(t){return null===t||"function"!=typeof t&&"object"!=typeof t}},320:(t,e,r)=>{"use strict";var n=r(4624),o=r(4440),i=n("%TypeError%");t.exports=function(t,e){if("Object"!==t.Type(e))return!1;var r={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};for(var n in e)if(o(e,n)&&!r[n])return!1;if(t.IsDataDescriptor(e)&&t.IsAccessorDescriptor(e))throw new i("Property Descriptors may not be both accessor and data descriptors");return!0}},8444:t=>{"use strict";t.exports=function(t){return"number"==typeof t&&t>=56320&&t<=57343}},9132:t=>{"use strict";t.exports=Number.MAX_SAFE_INTEGER||9007199254740991}},e={};function r(n){var o=e[n];if(void 0!==o)return o.exports;var i=e[n]={exports:{}};return t[n](i,i.exports,r),i.exports}r.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return r.d(e,{a:e}),e},r.d=(t,e)=>{for(var n in e)r.o(e,n)&&!r.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";var t=r(9116);function e(e,r,n){let o=0,i=[];for(;-1!==o;)o=e.indexOf(r,o),-1!==o&&(i.push({start:o,end:o+r.length,errors:0}),o+=1);return i.length>0?i:(0,t.c)(e,r,n)}function n(t,r){return 0===r.length||0===t.length?0:1-e(t,r,r.length)[0].errors/r.length}function o(t){switch(t.nodeType){case Node.ELEMENT_NODE:case Node.TEXT_NODE:return t.textContent.length;default:return 0}}function i(t){let e=t.previousSibling,r=0;for(;e;)r+=o(e),e=e.previousSibling;return r}function a(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),n=1;no?(a.push({node:u,offset:o-s}),o=r.shift()):(c=i.nextNode(),s+=u.data.length);for(;void 0!==o&&u&&s===o;)a.push({node:u,offset:u.data.length}),o=r.shift();if(void 0!==o)throw new RangeError("Offset exceeds text length");return a}class u{constructor(t,e){if(e<0)throw new Error("Offset is invalid");this.element=t,this.offset=e}relativeTo(t){if(!t.contains(this.element))throw new Error("Parent is not an ancestor of current element");let e=this.element,r=this.offset;for(;e!==t;)r+=i(e),e=e.parentElement;return new u(e,r)}resolve(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};try{return a(this.element,this.offset)[0]}catch(e){if(0===this.offset&&void 0!==t.direction){const r=document.createTreeWalker(this.element.getRootNode(),NodeFilter.SHOW_TEXT);r.currentNode=this.element;const n=1===t.direction,o=n?r.nextNode():r.previousNode();if(!o)throw e;return{node:o,offset:n?0:o.data.length}}throw e}}static fromCharOffset(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")}}static fromPoint(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");const r=i(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");let r=0;for(let n=0;n2&&void 0!==arguments[2]?arguments[2]:{};this.root=t,this.exact=e,this.context=r}static fromRange(t,e){const r=t.textContent,n=c.fromRange(e).relativeTo(t),o=n.start.offset,i=n.end.offset;return new l(t,r.slice(o,i),{prefix:r.slice(Math.max(0,o-32),o),suffix:r.slice(i,Math.min(r.length,i+32))})}static fromSelector(t,e){const{prefix:r,suffix:n}=e;return new l(t,e.exact,{prefix:r,suffix:n})}toSelector(){return{type:"TextQuoteSelector",exact:this.exact,prefix:this.context.prefix,suffix:this.context.suffix}}toRange(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.toPositionAnchor(t).toRange()}toPositionAnchor(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const r=function(t,r){let o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(0===r.length)return null;const i=Math.min(256,r.length/2),a=e(t,r,i);if(0===a.length)return null;const u=e=>{const i=1-e.errors/r.length,a=o.prefix?n(t.slice(Math.max(0,e.start-o.prefix.length),e.start),o.prefix):1,u=o.suffix?n(t.slice(e.end,e.end+o.suffix.length),o.suffix):1;let c=1;return"number"==typeof o.hint&&(c=1-Math.abs(e.start-o.hint)/t.length),(50*i+20*a+20*u+2*c)/92},c=a.map((t=>({start:t.start,end:t.end,score:u(t)})));return c.sort(((t,e)=>e.score-t.score)),c[0]}(this.root.textContent,this.exact,{...this.context,hint:t.hint});if(!r)throw new Error("Quote not found");return new s(this.root,r.start,r.end)}}var f=r(3732);r.n(f)().shim();const p=!0;function y(){if(!readium.link)return null;const t=readium.link.href;if(!t)return null;const e=function(){const t=window.getSelection();if(!t)return;if(t.isCollapsed)return;const e=t.toString();if(0===e.trim().replace(/\n/g," ").replace(/\s\s+/g," ").length)return;if(!t.anchorNode||!t.focusNode)return;const r=1===t.rangeCount?t.getRangeAt(0):function(t,e,r,n){const o=new Range;if(o.setStart(t,e),o.setEnd(r,n),!o.collapsed)return o;d(">>> createOrderedRange COLLAPSED ... RANGE REVERSE?");const i=new Range;if(i.setStart(r,n),i.setEnd(t,e),!i.collapsed)return d(">>> createOrderedRange RANGE REVERSE OK."),o;d(">>> createOrderedRange RANGE REVERSE ALSO COLLAPSED?!")}(t.anchorNode,t.anchorOffset,t.focusNode,t.focusOffset);if(!r||r.collapsed)return void d("$$$$$$$$$$$$$$$$$ CANNOT GET NON-COLLAPSED SELECTION RANGE?!");const n=document.body.textContent,o=c.fromRange(r).relativeTo(document.body),i=o.start.offset,a=o.end.offset;let u=n.slice(Math.max(0,i-200),i),s=u.search(/\P{L}\p{L}/gu);-1!==s&&(u=u.slice(s+1));let l=n.slice(a,Math.min(n.length,a+200)),f=Array.from(l.matchAll(/\p{L}\P{L}/gu)).pop();return void 0!==f&&f.index>1&&(l=l.slice(0,f.index+1)),{highlight:e,before:u,after:l}}();return e?{href:t,text:e,rect:function(){try{let t=window.getSelection();if(!t)return;return k(t.getRangeAt(0).getBoundingClientRect())}catch(t){return M(t),null}}()}:null}function d(){p&&I.apply(null,arguments)}var h;window.addEventListener("error",(function(t){webkit.messageHandlers.logError.postMessage({message:t.message,filename:t.filename,line:t.lineno})}),!1),window.addEventListener("load",(function(){var t;new ResizeObserver((()=>{t&&window.cancelAnimationFrame(t),t=window.requestAnimationFrame((function(){v=window.innerWidth,function(){const t="readium-virtual-page";var e=document.getElementById(t);if(x()||2!=parseInt(window.getComputedStyle(document.documentElement).getPropertyValue("column-count"))){var r;null===(r=e)||void 0===r||r.remove()}else{var n=document.scrollingElement.scrollWidth/window.innerWidth;Math.round(2*n)/2%1>.1&&(e?e.remove():((e=document.createElement("div")).setAttribute("id",t),e.style.breakBefore="column",e.innerHTML="​",document.body.appendChild(e)))}}(),function(){if(!x()){var t=T(window.scrollX+1);document.scrollingElement.scrollLeft=t}}(),w()}))})).observe(document.body)}),!1);var g,m,b=!1,v=0;function w(){if(readium.isFixedLayout)return;let t=document.scrollingElement;if(x()&&!S()){const e=window.scrollY,r=window.innerHeight,n=t.scrollHeight;h={first:e/n,last:(e+r)/n}}else{let e=window.scrollX;const r=window.innerWidth,n=t.scrollWidth;E()&&(e=Math.abs(e)),h={first:e/n,last:(e+r)/n}}0!==t.scrollWidth&&0!==t.scrollHeight&&(b||window.requestAnimationFrame((function(){var t;t=h,webkit.messageHandlers.progressionChanged.postMessage(t),b=!1})),b=!0)}function x(){return"readium-scroll-on"==document.documentElement.style.getPropertyValue("--USER__view").trim()}function S(){return window.getComputedStyle(document.documentElement).getPropertyValue("writing-mode").startsWith("vertical")}function E(){const t=window.getComputedStyle(document.documentElement);return"rtl"==t.getPropertyValue("direction")||"vertical-rl"==t.getPropertyValue("writing-mode")}function A(t,e){return x()?j({top:t.top+window.scrollY,animated:e}):j({left:T(t.left+window.scrollX),animated:e}),!0}function O(t,e){var r=window.scrollX,n=window.innerWidth,o=Math.abs(r-t)/n>.01;return o&&j({left:t,animated:e}),o}function j(){let{left:t,top:e,animated:r}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};document.scrollingElement.scrollTo({left:t,top:e,behavior:r?"smooth":"instant"})}function T(t){const e=t+(E()?-1:1);return e-e%v}function P(t){try{let n=t.locations,o=t.text;var e;if(o&&o.highlight)return n&&n.cssSelector&&(e=document.querySelector(n.cssSelector)),e||(e=document.body),new l(e,o.highlight,{prefix:o.before,suffix:o.after}).toRange();if(n){var r=null;if(!r&&n.cssSelector&&(r=document.querySelector(n.cssSelector)),!r&&n.fragments)for(const t of n.fragments)if(r=document.getElementById(t))break;if(r){let t=document.createRange();return t.setStartBefore(r),t.setEndAfter(r),t}}}catch(t){M(t)}return null}function R(t,e){null===e?C(t):document.documentElement.style.setProperty(t,e,"important")}function C(t){document.documentElement.style.removeProperty(t)}function I(){var t=Array.prototype.slice.call(arguments).join(" ");webkit.messageHandlers.log.postMessage(t)}function N(t){M(new Error(t))}function M(t){webkit.messageHandlers.logError.postMessage({message:t.message})}window.addEventListener("scroll",w),document.addEventListener("selectionchange",(50,g=function(){webkit.messageHandlers.selectionChanged.postMessage(y())},function(){var t=this,e=arguments;clearTimeout(m),m=setTimeout((function(){g.apply(t,e),m=null}),50)}));const $=!1;function k(t){let e=D({x:t.left,y:t.top});const r=t.width,n=t.height,o=e.x,i=e.y;return{width:r,height:n,left:o,top:i,right:o+r,bottom:i+n}}function D(t){if(!frameElement)return t;let e=frameElement.getBoundingClientRect();if(!e)return t;let r=window.top.document.documentElement;return{x:t.x+e.x+r.scrollLeft,y:t.y+e.y+r.scrollTop}}function F(t,e){let r=t.getClientRects();const n=[];for(const t of r)n.push({bottom:t.bottom,height:t.height,left:t.left,right:t.right,top:t.top,width:t.width});const o=U(function(t,e){const r=new Set(t);for(const e of t)if(e.width>1&&e.height>1){for(const n of t)if(e!==n&&r.has(n)&&_(n,e,1)){V("CLIENT RECT: remove contained"),r.delete(e);break}}else V("CLIENT RECT: remove tiny"),r.delete(e);return Array.from(r)}(B(n,1,e)));for(let t=o.length-1;t>=0;t--){const e=o[t];if(!(e.width*e.height>4)){if(!(o.length>1)){V("CLIENT RECT: remove small, but keep otherwise empty!");break}V("CLIENT RECT: remove small"),o.splice(t,1)}}return V(`CLIENT RECT: reduced ${n.length} --\x3e ${o.length}`),o}function B(t,e,r){for(let n=0;nt!==i&&t!==a)),o=L(i,a);return n.push(o),B(n,e,r)}}return t}function L(t,e){const r=Math.min(t.left,e.left),n=Math.max(t.right,e.right),o=Math.min(t.top,e.top),i=Math.max(t.bottom,e.bottom);return{bottom:i,height:i-o,left:r,right:n,top:o,width:n-r}}function _(t,e,r){return W(t,e.left,e.top,r)&&W(t,e.right,e.top,r)&&W(t,e.left,e.bottom,r)&&W(t,e.right,e.bottom,r)}function W(t,e,r,n){return(t.lefte||H(t.right,e,n))&&(t.topr||H(t.bottom,r,n))}function U(t){for(let e=0;et!==e));return Array.prototype.push.apply(a,r),U(a)}}else V("replaceOverlapingRects rect1 === rect2 ??!")}return t}function z(t,e){const r=function(t,e){const r=Math.max(t.left,e.left),n=Math.min(t.right,e.right),o=Math.max(t.top,e.top),i=Math.min(t.bottom,e.bottom);return{bottom:i,height:Math.max(0,i-o),left:r,right:n,top:o,width:Math.max(0,n-r)}}(e,t);if(0===r.height||0===r.width)return[t];const n=[];{const e={bottom:t.bottom,height:0,left:t.left,right:r.left,top:t.top,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}{const e={bottom:r.top,height:0,left:r.left,right:r.right,top:t.top,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}{const e={bottom:t.bottom,height:0,left:r.left,right:r.right,top:r.bottom,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}{const e={bottom:t.bottom,height:0,left:r.right,right:t.right,top:t.top,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}return n}function G(t,e,r){return(t.left=0&&H(t.left,e.right,r))&&(e.left=0&&H(e.left,t.right,r))&&(t.top=0&&H(t.top,e.bottom,r))&&(e.top=0&&H(e.top,t.bottom,r))}function H(t,e,r){return Math.abs(t-e)<=r}function V(){$&&I.apply(null,arguments)}var q,X=[],K="ResizeObserver loop completed with undelivered notifications.";!function(t){t.BORDER_BOX="border-box",t.CONTENT_BOX="content-box",t.DEVICE_PIXEL_CONTENT_BOX="device-pixel-content-box"}(q||(q={}));var Y,J=function(t){return Object.freeze(t)},Q=function(t,e){this.inlineSize=t,this.blockSize=e,J(this)},Z=function(){function t(t,e,r,n){return this.x=t,this.y=e,this.width=r,this.height=n,this.top=this.y,this.left=this.x,this.bottom=this.top+this.height,this.right=this.left+this.width,J(this)}return t.prototype.toJSON=function(){var t=this;return{x:t.x,y:t.y,top:t.top,right:t.right,bottom:t.bottom,left:t.left,width:t.width,height:t.height}},t.fromRect=function(e){return new t(e.x,e.y,e.width,e.height)},t}(),tt=function(t){return t instanceof SVGElement&&"getBBox"in t},et=function(t){if(tt(t)){var e=t.getBBox(),r=e.width,n=e.height;return!r&&!n}var o=t,i=o.offsetWidth,a=o.offsetHeight;return!(i||a||t.getClientRects().length)},rt=function(t){var e;if(t instanceof Element)return!0;var r=null===(e=null==t?void 0:t.ownerDocument)||void 0===e?void 0:e.defaultView;return!!(r&&t instanceof r.Element)},nt="undefined"!=typeof window?window:{},ot=new WeakMap,it=/auto|scroll/,at=/^tb|vertical/,ut=/msie|trident/i.test(nt.navigator&&nt.navigator.userAgent),ct=function(t){return parseFloat(t||"0")},st=function(t,e,r){return void 0===t&&(t=0),void 0===e&&(e=0),void 0===r&&(r=!1),new Q((r?e:t)||0,(r?t:e)||0)},lt=J({devicePixelContentBoxSize:st(),borderBoxSize:st(),contentBoxSize:st(),contentRect:new Z(0,0,0,0)}),ft=function(t,e){if(void 0===e&&(e=!1),ot.has(t)&&!e)return ot.get(t);if(et(t))return ot.set(t,lt),lt;var r=getComputedStyle(t),n=tt(t)&&t.ownerSVGElement&&t.getBBox(),o=!ut&&"border-box"===r.boxSizing,i=at.test(r.writingMode||""),a=!n&&it.test(r.overflowY||""),u=!n&&it.test(r.overflowX||""),c=n?0:ct(r.paddingTop),s=n?0:ct(r.paddingRight),l=n?0:ct(r.paddingBottom),f=n?0:ct(r.paddingLeft),p=n?0:ct(r.borderTopWidth),y=n?0:ct(r.borderRightWidth),d=n?0:ct(r.borderBottomWidth),h=f+s,g=c+l,m=(n?0:ct(r.borderLeftWidth))+y,b=p+d,v=u?t.offsetHeight-b-t.clientHeight:0,w=a?t.offsetWidth-m-t.clientWidth:0,x=o?h+m:0,S=o?g+b:0,E=n?n.width:ct(r.width)-x-w,A=n?n.height:ct(r.height)-S-v,O=E+h+w+m,j=A+g+v+b,T=J({devicePixelContentBoxSize:st(Math.round(E*devicePixelRatio),Math.round(A*devicePixelRatio),i),borderBoxSize:st(O,j,i),contentBoxSize:st(E,A,i),contentRect:new Z(f,c,E,A)});return ot.set(t,T),T},pt=function(t,e,r){var n=ft(t,r),o=n.borderBoxSize,i=n.contentBoxSize,a=n.devicePixelContentBoxSize;switch(e){case q.DEVICE_PIXEL_CONTENT_BOX:return a;case q.BORDER_BOX:return o;default:return i}},yt=function(t){var e=ft(t);this.target=t,this.contentRect=e.contentRect,this.borderBoxSize=J([e.borderBoxSize]),this.contentBoxSize=J([e.contentBoxSize]),this.devicePixelContentBoxSize=J([e.devicePixelContentBoxSize])},dt=function(t){if(et(t))return 1/0;for(var e=0,r=t.parentNode;r;)e+=1,r=r.parentNode;return e},ht=function(){var t=1/0,e=[];X.forEach((function(r){if(0!==r.activeTargets.length){var n=[];r.activeTargets.forEach((function(e){var r=new yt(e.target),o=dt(e.target);n.push(r),e.lastReportedSize=pt(e.target,e.observedBox),ot?e.activeTargets.push(r):e.skippedTargets.push(r))}))}))},mt=[],bt=0,vt={attributes:!0,characterData:!0,childList:!0,subtree:!0},wt=["resize","load","transitionend","animationend","animationstart","animationiteration","keyup","keydown","mouseup","mousedown","mouseover","mouseout","blur","focus"],xt=function(t){return void 0===t&&(t=0),Date.now()+t},St=!1,Et=function(){function t(){var t=this;this.stopped=!0,this.listener=function(){return t.schedule()}}return t.prototype.run=function(t){var e=this;if(void 0===t&&(t=250),!St){St=!0;var r,n=xt(t);r=function(){var r=!1;try{r=function(){var t,e=0;for(gt(e);X.some((function(t){return t.activeTargets.length>0}));)e=ht(),gt(e);return X.some((function(t){return t.skippedTargets.length>0}))&&("function"==typeof ErrorEvent?t=new ErrorEvent("error",{message:K}):((t=document.createEvent("Event")).initEvent("error",!1,!1),t.message=K),window.dispatchEvent(t)),e>0}()}finally{if(St=!1,t=n-xt(),!bt)return;r?e.run(1e3):t>0?e.run(t):e.start()}},function(t){if(!Y){var e=0,r=document.createTextNode("");new MutationObserver((function(){return mt.splice(0).forEach((function(t){return t()}))})).observe(r,{characterData:!0}),Y=function(){r.textContent="".concat(e?e--:e++)}}mt.push(t),Y()}((function(){requestAnimationFrame(r)}))}},t.prototype.schedule=function(){this.stop(),this.run()},t.prototype.observe=function(){var t=this,e=function(){return t.observer&&t.observer.observe(document.body,vt)};document.body?e():nt.addEventListener("DOMContentLoaded",e)},t.prototype.start=function(){var t=this;this.stopped&&(this.stopped=!1,this.observer=new MutationObserver(this.listener),this.observe(),wt.forEach((function(e){return nt.addEventListener(e,t.listener,!0)})))},t.prototype.stop=function(){var t=this;this.stopped||(this.observer&&this.observer.disconnect(),wt.forEach((function(e){return nt.removeEventListener(e,t.listener,!0)})),this.stopped=!0)},t}(),At=new Et,Ot=function(t){!bt&&t>0&&At.start(),!(bt+=t)&&At.stop()},jt=function(){function t(t,e){this.target=t,this.observedBox=e||q.CONTENT_BOX,this.lastReportedSize={inlineSize:0,blockSize:0}}return t.prototype.isActive=function(){var t,e=pt(this.target,this.observedBox,!0);return t=this.target,tt(t)||function(t){switch(t.tagName){case"INPUT":if("image"!==t.type)break;case"VIDEO":case"AUDIO":case"EMBED":case"OBJECT":case"CANVAS":case"IFRAME":case"IMG":return!0}return!1}(t)||"inline"!==getComputedStyle(t).display||(this.lastReportedSize=e),this.lastReportedSize.inlineSize!==e.inlineSize||this.lastReportedSize.blockSize!==e.blockSize},t}(),Tt=function(t,e){this.activeTargets=[],this.skippedTargets=[],this.observationTargets=[],this.observer=t,this.callback=e},Pt=new WeakMap,Rt=function(t,e){for(var r=0;r=0&&(o&&X.splice(X.indexOf(r),1),r.observationTargets.splice(n,1),Ot(-1))},t.disconnect=function(t){var e=this,r=Pt.get(t);r.observationTargets.slice().forEach((function(r){return e.unobserve(t,r.target)})),r.activeTargets.splice(0,r.activeTargets.length)},t}(),It=function(){function t(t){if(0===arguments.length)throw new TypeError("Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.");if("function"!=typeof t)throw new TypeError("Failed to construct 'ResizeObserver': The callback provided as parameter 1 is not a function.");Ct.connect(this,t)}return t.prototype.observe=function(t,e){if(0===arguments.length)throw new TypeError("Failed to execute 'observe' on 'ResizeObserver': 1 argument required, but only 0 present.");if(!rt(t))throw new TypeError("Failed to execute 'observe' on 'ResizeObserver': parameter 1 is not of type 'Element");Ct.observe(this,t,e)},t.prototype.unobserve=function(t){if(0===arguments.length)throw new TypeError("Failed to execute 'unobserve' on 'ResizeObserver': 1 argument required, but only 0 present.");if(!rt(t))throw new TypeError("Failed to execute 'unobserve' on 'ResizeObserver': parameter 1 is not of type 'Element");Ct.unobserve(this,t)},t.prototype.disconnect=function(){Ct.disconnect(this)},t.toString=function(){return"function ResizeObserver () { [polyfill code] }"},t}();const Nt=window.ResizeObserver||It;let Mt=new Map,$t=new Map;var kt=0;function Dt(t){if(0===$t.size)return null;for(const[e,r]of $t)if(r.isActivable())for(const n of r.items.reverse())if(n.clickableElements)for(const r of n.clickableElements){let o=r.getBoundingClientRect().toJSON();if(W(o,t.clientX,t.clientY,1))return{group:e,item:n,element:r,rect:o}}return null}function Ft(t){return t&&t instanceof Element}window.addEventListener("load",(function(){const t=document.body;var e={width:0,height:0};new Nt((()=>{e.width===t.clientWidth&&e.height===t.clientHeight||(e={width:t.clientWidth,height:t.clientHeight},$t.forEach((function(t){t.requestLayout()})))})).observe(t)}),!1);const Bt={NONE:"",DESCENDANT:" ",CHILD:" > "},Lt={id:"id",class:"class",tag:"tag",attribute:"attribute",nthchild:"nthchild",nthoftype:"nthoftype"},_t="CssSelectorGenerator";function Wt(t="unknown problem",...e){console.warn(`${_t}: ${t}`,...e)}const Ut={selectors:[Lt.id,Lt.class,Lt.tag,Lt.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function zt(t){return t instanceof RegExp}function Gt(t){return["string","function"].includes(typeof t)||zt(t)}function Ht(t){return Array.isArray(t)?t.filter(Gt):[]}function Vt(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 qt(t,e){if(Vt(t))return t.contains(e)||Wt("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 r=e.getRootNode({composed:!1});return Vt(r)?(r!==document&&Wt("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."),r):e.ownerDocument.querySelector(":root")}function Xt(t){return"number"==typeof t?t:Number.POSITIVE_INFINITY}function Kt(t=[]){const[e=[],...r]=t;return 0===r.length?e:r.reduce(((t,e)=>t.filter((t=>e.includes(t)))),e)}function Yt(t){return[].concat(...t)}function Jt(t){const e=t.map((t=>{if(zt(t))return e=>t.test(e);if("function"==typeof t)return e=>{const r=t(e);return"boolean"!=typeof r?(Wt("pattern matcher function invalid","Provided pattern matching function does not return boolean. It's result will be ignored.",t),!1):r};if("string"==typeof t){const e=new RegExp("^"+t.replace(/[|\\{}()[\]^$+?.]/g,"\\$&").replace(/\*/g,".+")+"$");return t=>e.test(t)}return Wt("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 Qt(t,e,r){const n=Array.from(qt(r,t[0]).querySelectorAll(e));return n.length===t.length&&t.every((t=>n.includes(t)))}function Zt(t,e){e=null!=e?e:function(t){return t.ownerDocument.querySelector(":root")}(t);const r=[];let n=t;for(;Ft(n)&&n!==e;)r.push(n),n=n.parentElement;return r}function te(t,e){return Kt(t.map((t=>Zt(t,e))))}const ee=new RegExp(["^$","\\s"].join("|")),re=new RegExp(["^$"].join("|")),ne=[Lt.nthoftype,Lt.tag,Lt.id,Lt.class,Lt.attribute,Lt.nthchild],oe=Jt(["class","id","ng-*"]);function ie({name:t}){return`[${t}]`}function ae({name:t,value:e}){return`[${t}='${e}']`}function ue({nodeName:t,nodeValue:e}){return{name:(r=t,r.replace(/:/g,"\\:")),value:we(e)};var r}function ce(t){const e=Array.from(t.attributes).filter((e=>function({nodeName:t},e){const r=e.tagName.toLowerCase();return!(["input","option"].includes(r)&&"value"===t||oe(t))}(e,t))).map(ue);return[...e.map(ie),...e.map(ae)]}function se(t){return(t.getAttribute("class")||"").trim().split(/\s+/).filter((t=>!re.test(t))).map((t=>`.${we(t)}`))}function le(t){const e=t.getAttribute("id")||"",r=`#${we(e)}`,n=t.getRootNode({composed:!1});return!ee.test(e)&&Qt([t],r,n)?[r]:[]}function fe(t){const e=t.parentNode;if(e){const r=Array.from(e.childNodes).filter(Ft).indexOf(t);if(r>-1)return[`:nth-child(${r+1})`]}return[]}function pe(t){return[we(t.tagName.toLowerCase())]}function ye(t){const e=[...new Set(Yt(t.map(pe)))];return 0===e.length||e.length>1?[]:[e[0]]}function de(t){const e=ye([t])[0],r=t.parentElement;if(r){const n=Array.from(r.children).filter((t=>t.tagName.toLowerCase()===e)),o=n.indexOf(t);if(o>-1)return[`${e}:nth-of-type(${o+1})`]}return[]}function he(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){return Array.from(function*(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){let r=0,n=me(1);for(;n.length<=t.length&&rt[e]));yield e,n=ge(n,t.length-1)}}(t,{maxResults:e}))}function ge(t=[],e=0){const r=t.length;if(0===r)return[];const n=[...t];n[r-1]+=1;for(let t=r-1;t>=0;t--)if(n[t]>e){if(0===t)return me(r+1);n[t-1]++,n[t]=n[t-1]+1}return n[r-1]>e?me(r+1):n}function me(t=1){return Array.from(Array(t).keys())}const be=":".charCodeAt(0).toString(16).toUpperCase(),ve=/[ !"#$%&'()\[\]{|}<>*+,./;=?@^`~\\]/;function we(t=""){var e,r;return null!==(r=null===(e=null===CSS||void 0===CSS?void 0:CSS.escape)||void 0===e?void 0:e.call(CSS,t))&&void 0!==r?r:function(t=""){return t.split("").map((t=>":"===t?`\\${be} `:ve.test(t)?`\\${t}`:escape(t).replace(/%/g,"\\"))).join("")}(t)}const xe={tag:ye,id:function(t){return 0===t.length||t.length>1?[]:le(t[0])},class:function(t){return Kt(t.map(se))},attribute:function(t){return Kt(t.map(ce))},nthchild:function(t){return Kt(t.map(fe))},nthoftype:function(t){return Kt(t.map(de))}},Se={tag:pe,id:le,class:se,attribute:ce,nthchild:fe,nthoftype:de};function Ee(t){return t.includes(Lt.tag)||t.includes(Lt.nthoftype)?[...t]:[...t,Lt.tag]}function Ae(t={}){const e=[...ne];return t[Lt.tag]&&t[Lt.nthoftype]&&e.splice(e.indexOf(Lt.tag),1),e.map((e=>{return(n=t)[r=e]?n[r].join(""):"";var r,n})).join("")}function Oe(t,e,r="",n){const o=function(t,e){return""===e?t:function(t,e){return[...t.map((t=>e+Bt.DESCENDANT+t)),...t.map((t=>e+Bt.CHILD+t))]}(t,e)}(function(t,e,r){const n=function(t,e){const{blacklist:r,whitelist:n,combineWithinSelector:o,maxCombinations:i}=e,a=Jt(r),u=Jt(n);return function(t){const{selectors:e,includeTag:r}=t,n=[].concat(e);return r&&!n.includes("tag")&&n.push("tag"),n}(e).reduce(((e,r)=>{const n=function(t,e){var r;return(null!==(r=xe[e])&&void 0!==r?r:()=>[])(t)}(t,r),c=function(t=[],e,r){return t.filter((t=>r(t)||!e(t)))}(n,a,u),s=function(t=[],e){return t.sort(((t,r)=>{const n=e(t),o=e(r);return n&&!o?-1:!n&&o?1:0}))}(c,u);return e[r]=o?he(s,{maxResults:i}):s.map((t=>[t])),e}),{})}(t,r),o=function(t,e){return function(t){const{selectors:e,combineBetweenSelectors:r,includeTag:n,maxCandidates:o}=t,i=r?he(e,{maxResults:o}):e.map((t=>[t]));return n?i.map(Ee):i}(e).map((e=>function(t,e){const r={};return t.forEach((t=>{const n=e[t];n.length>0&&(r[t]=n)})),function(t={}){let e=[];return Object.entries(t).forEach((([t,r])=>{e=r.flatMap((r=>0===e.length?[{[t]:r}]:e.map((e=>Object.assign(Object.assign({},e),{[t]:r})))))})),e}(r).map(Ae)}(e,t))).filter((t=>t.length>0))}(n,r),i=Yt(o);return[...new Set(i)]}(t,n.root,n),r);for(const e of o)if(Qt(t,e,n.root))return e;return null}function je(t){return{value:t,include:!1}}function Te({selectors:t,operator:e}){let r=[...ne];t[Lt.tag]&&t[Lt.nthoftype]&&(r=r.filter((t=>t!==Lt.tag)));let n="";return r.forEach((e=>{(t[e]||[]).forEach((({value:t,include:e})=>{e&&(n+=t)}))})),e+n}function Pe(t){return[":root",...Zt(t).reverse().map((t=>{const e=function(t,e,r=Bt.NONE){const n={};return e.forEach((e=>{Reflect.set(n,e,function(t,e){return Se[e](t)}(t,e).map(je))})),{element:t,operator:r,selectors:n}}(t,[Lt.nthchild],Bt.CHILD);return e.selectors.nthchild.forEach((t=>{t.include=!0})),e})).map(Te)].join("")}function Re(t,e={}){const r=function(t){(t instanceof NodeList||t instanceof HTMLCollection)&&(t=Array.from(t));const e=(Array.isArray(t)?t:[t]).filter(Ft);return[...new Set(e)]}(t),n=function(t,e={}){const r=Object.assign(Object.assign({},Ut),e);return{selectors:(n=r.selectors,Array.isArray(n)?n.filter((t=>{return e=Lt,r=t,Object.values(e).includes(r);var e,r})):[]),whitelist:Ht(r.whitelist),blacklist:Ht(r.blacklist),root:qt(r.root,t),combineWithinSelector:!!r.combineWithinSelector,combineBetweenSelectors:!!r.combineBetweenSelectors,includeTag:!!r.includeTag,maxCombinations:Xt(r.maxCombinations),maxCandidates:Xt(r.maxCandidates)};var n}(r[0],e);let o="",i=n.root;function a(){return function(t,e,r="",n){if(0===t.length)return null;const o=[t.length>1?t:[],...te(t,e).map((t=>[t]))];for(const t of o){const e=Oe(t,0,r,n);if(e)return{foundElements:t,selector:e}}return null}(r,i,o,n)}let u=a();for(;u;){const{foundElements:t,selector:e}=u;if(Qt(r,e,n.root))return e;i=t[0],o=e,u=a()}return r.length>1?r.map((t=>Re(t,n))).join(", "):function(t){return t.map(Pe).join(", ")}(r)}function Ce(t){return null==t?null:-1!==["a","audio","button","canvas","details","input","label","option","select","submit","textarea","video"].indexOf(t.nodeName.toLowerCase())||t.hasAttribute("contenteditable")&&"false"!=t.getAttribute("contenteditable").toLowerCase()?t.outerHTML:t.parentElement?Ce(t.parentElement):null}function Ie(t){for(var e=0;e0&&e.top0&&e.left{We(t)||(Ue(t),ze("down",t))})),window.addEventListener("keyup",(t=>{We(t)||(Ue(t),ze("up",t))})),r.g.readium={scrollToId:function(t,e){let r=document.getElementById(t);return!!r&&(A(r.getBoundingClientRect(),e),!0)},scrollToPosition:function(t,e,r){t<0||t>1?console.error(`Expected a valid progression in scrollToPosition, got ${t}`):x()?S()?j({left:-document.scrollingElement.scrollWidth*t,animated:r}):j({top:document.scrollingElement.scrollHeight*t,animated:r}):j({left:T(document.scrollingElement.scrollWidth*t*("rtl"==e?-1:1)),animated:r})},scrollToLocator:function(t,e){let r=P(t);return!!r&&function(t,e){return A(t.getBoundingClientRect(),e)}(r,e)},scrollLeft:function(t,e){var r="rtl"==t,n=document.scrollingElement.scrollWidth,o=window.innerWidth,i=window.scrollX-o,a=r?-(n-o):0;return O(Math.max(i,a),e)},scrollRight:function(t,e){var r="rtl"==t,n=document.scrollingElement.scrollWidth,o=window.innerWidth,i=window.scrollX+o,a=r?0:n-o;return O(Math.min(i,a),e)},setCSSProperties:function(t){for(const e in t)R(e,t[e])},setProperty:R,removeProperty:C,registerDecorationTemplates:function(t){var e="";for(const[r,n]of Object.entries(t))Mt.set(r,n),n.stylesheet&&(e+=n.stylesheet+"\n");if(e){let t=document.createElement("style");t.innerHTML=e,document.getElementsByTagName("head")[0].appendChild(t)}},getDecorations:function(t){var e=$t.get(t);return e||(e=function(t,e){var r=[],n=0,o=null,i=!1;function a(e){let o=t+"-"+n++,i=P(e.locator);if(!i)return void I("Can't locate DOM range for decoration",e);let a={id:o,decoration:e,range:i};r.push(a),c(a)}function u(t){let e=r.findIndex((e=>e.decoration.id===t));if(-1===e)return;let n=r[e];r.splice(e,1),n.clickableElements=null,n.container&&(n.container.remove(),n.container=null)}function c(r){let n=(o||((o=document.createElement("div")).id=t,o.dataset.group=e,o.style.pointerEvents="none",requestAnimationFrame((function(){null!=o&&document.body.append(o)}))),o),i=Mt.get(r.decoration.style);if(!i)return void N(`Unknown decoration style: ${r.decoration.style}`);let a=document.createElement("div");a.id=r.id,a.dataset.style=r.decoration.style,a.style.pointerEvents="none";const u=getComputedStyle(document.body).writingMode,c="vertical-rl"===u||"vertical-lr"===u,s=document.scrollingElement,{scrollLeft:l,scrollTop:f}=s,p=c?window.innerHeight:window.innerWidth,y=c?window.innerWidth:window.innerHeight,d=parseInt(getComputedStyle(document.documentElement).getPropertyValue("column-count"))||1,h=(c?y:p)/d;function g(t,e,r,n){t.style.position="absolute";const o="vertical-rl"===n;if(o||"vertical-lr"===n){if("wrap"===i.width)t.style.width=`${e.width}px`,t.style.height=`${e.height}px`,o?t.style.right=`${-e.right-l+s.clientWidth}px`:t.style.left=`${e.left+l}px`,t.style.top=`${e.top+f}px`;else if("viewport"===i.width){t.style.width=`${e.height}px`,t.style.height=`${p}px`;const r=Math.floor(e.top/p)*p;o?t.style.right=-e.right-l+"px":t.style.left=`${e.left+l}px`,t.style.top=`${r+f}px`}else if("bounds"===i.width)t.style.width=`${r.height}px`,t.style.height=`${p}px`,o?t.style.right=`${-r.right-l+s.clientWidth}px`:t.style.left=`${r.left+l}px`,t.style.top=`${r.top+f}px`;else if("page"===i.width){t.style.width=`${e.height}px`,t.style.height=`${h}px`;const r=Math.floor(e.top/h)*h;o?t.style.right=`${-e.right-l+s.clientWidth}px`:t.style.left=`${e.left+l}px`,t.style.top=`${r+f}px`}}else if("wrap"===i.width)t.style.width=`${e.width}px`,t.style.height=`${e.height}px`,t.style.left=`${e.left+l}px`,t.style.top=`${e.top+f}px`;else if("viewport"===i.width){t.style.width=`${p}px`,t.style.height=`${e.height}px`;const r=Math.floor(e.left/p)*p;t.style.left=`${r+l}px`,t.style.top=`${e.top+f}px`}else if("bounds"===i.width)t.style.width=`${r.width}px`,t.style.height=`${e.height}px`,t.style.left=`${r.left+l}px`,t.style.top=`${e.top+f}px`;else if("page"===i.width){t.style.width=`${h}px`,t.style.height=`${e.height}px`;const r=Math.floor(e.left/h)*h;t.style.left=`${r+l}px`,t.style.top=`${e.top+f}px`}}let m,b=r.range.getBoundingClientRect();try{let t=document.createElement("template");t.innerHTML=r.decoration.element.trim(),m=t.content.firstElementChild}catch(t){return void N(`Invalid decoration element "${r.decoration.element}": ${t.message}`)}if("boxes"===i.layout){const t=!u.startsWith("vertical"),e=(v=r.range.startContainer).nodeType===Node.ELEMENT_NODE?v:v.parentElement,n=getComputedStyle(e).writingMode,o=F(r.range,t).sort(((t,e)=>t.top!==e.top?t.top-e.top:"vertical-rl"===n?e.left-t.left:t.left-e.left));for(let t of o){const e=m.cloneNode(!0);e.style.pointerEvents="none",e.dataset.writingMode=n,g(e,t,b,u),a.append(e)}}else if("bounds"===i.layout){const t=m.cloneNode(!0);t.style.pointerEvents="none",t.dataset.writingMode=u,g(t,b,b,u),a.append(t)}var v;n.append(a),r.container=a,r.clickableElements=Array.from(a.querySelectorAll("[data-activable='1']")),0===r.clickableElements.length&&(r.clickableElements=Array.from(a.children))}function s(){o&&(o.remove(),o=null)}return{add:a,remove:u,update:function(t){u(t.id),a(t)},clear:function(){s(),r.length=0},items:r,requestLayout:function(){s(),r.forEach((t=>c(t)))},isActivable:function(){return i},setActivable:function(){i=!0}}}("r2-decoration-"+kt++,t),$t.set(t,e)),e},findFirstVisibleLocator:function(){const t=Ie(document.body);return{href:"#",type:"application/xhtml+xml",locations:{cssSelector:Re(t)},text:{highlight:t.textContent}}}},window.readium.isFixedLayout=!0,webkit.messageHandlers.spreadLoadStarted.postMessage({})})()})(); //# sourceMappingURL=readium-fixed.js.map \ No newline at end of file diff --git a/Sources/Navigator/EPUB/Assets/Static/scripts/readium-reflowable.js b/Sources/Navigator/EPUB/Assets/Static/scripts/readium-reflowable.js index 6ec8cd752c..9ca748c2b2 100644 --- a/Sources/Navigator/EPUB/Assets/Static/scripts/readium-reflowable.js +++ b/Sources/Navigator/EPUB/Assets/Static/scripts/readium-reflowable.js @@ -1,2 +1,2 @@ -(()=>{var t={9116:(t,e)=>{"use strict";function r(t){return t.split("").reverse().join("")}function n(t){return(t|-t)>>31&1}function o(t,e,r,o){var i=t.P[r],a=t.M[r],u=o>>>31,c=e[r]|u,s=c|a,l=(c&i)+i^i|c,f=a|~(l|i),p=i&l,y=n(f&t.lastRowMask[r])-n(p&t.lastRowMask[r]);return f<<=1,p<<=1,i=(p|=u)|~(s|(f|=n(o)-u)),a=f&s,t.P[r]=i,t.M[r]=a,y}function i(t,e,r){if(0===e.length)return[];r=Math.min(r,e.length);var n=[],i=32,a=Math.ceil(e.length/i)-1,u={P:new Uint32Array(a+1),M:new Uint32Array(a+1),lastRowMask:new Uint32Array(a+1)};u.lastRowMask.fill(1<<31),u.lastRowMask[a]=1<<(e.length-1)%i;for(var c=new Uint32Array(a+1),s=new Map,l=[],f=0;f<256;f++)l.push(c);for(var p=0;p=e.length||e.charCodeAt(m)===y&&(d[h]|=1<0&&v[b]>=r+i;)b-=1;b===a&&v[b]<=r&&(v[b]{"use strict";var n=r(4624),o=r(5096),i=o(n("String.prototype.indexOf"));t.exports=function(t,e){var r=n(t,!!e);return"function"==typeof r&&i(t,".prototype.")>-1?o(r):r}},5096:(t,e,r)=>{"use strict";var n=r(3520),o=r(4624),i=r(5676),a=r(2824),u=o("%Function.prototype.apply%"),c=o("%Function.prototype.call%"),s=o("%Reflect.apply%",!0)||n.call(c,u),l=o("%Object.defineProperty%",!0),f=o("%Math.max%");if(l)try{l({},"a",{value:1})}catch(t){l=null}t.exports=function(t){if("function"!=typeof t)throw new a("a function is required");var e=s(n,c,arguments);return i(e,1+f(0,t.length-(arguments.length-1)),!0)};var p=function(){return s(n,u,arguments)};l?l(t.exports,"apply",{value:p}):t.exports.apply=p},2448:(t,e,r)=>{"use strict";var n=r(3268)(),o=r(4624),i=n&&o("%Object.defineProperty%",!0);if(i)try{i({},"a",{value:1})}catch(t){i=!1}var a=r(6500),u=r(2824),c=r(6168);t.exports=function(t,e,r){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new u("`obj` must be an object or a function`");if("string"!=typeof e&&"symbol"!=typeof e)throw new u("`property` must be a string or a symbol`");if(arguments.length>3&&"boolean"!=typeof arguments[3]&&null!==arguments[3])throw new u("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&"boolean"!=typeof arguments[4]&&null!==arguments[4])throw new u("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&"boolean"!=typeof arguments[5]&&null!==arguments[5])throw new u("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&"boolean"!=typeof arguments[6])throw new u("`loose`, if provided, must be a boolean");var n=arguments.length>3?arguments[3]:null,o=arguments.length>4?arguments[4]:null,s=arguments.length>5?arguments[5]:null,l=arguments.length>6&&arguments[6],f=!!c&&c(t,e);if(i)i(t,e,{configurable:null===s&&f?f.configurable:!s,enumerable:null===n&&f?f.enumerable:!n,value:r,writable:null===o&&f?f.writable:!o});else{if(!l&&(n||o||s))throw new a("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.");t[e]=r}}},2732:(t,e,r)=>{"use strict";var n=r(2812),o="function"==typeof Symbol&&"symbol"==typeof Symbol("foo"),i=Object.prototype.toString,a=Array.prototype.concat,u=r(2448),c=r(3268)(),s=function(t,e,r,n){if(e in t)if(!0===n){if(t[e]===r)return}else if("function"!=typeof(o=n)||"[object Function]"!==i.call(o)||!n())return;var o;c?u(t,e,r,!0):u(t,e,r)},l=function(t,e){var r=arguments.length>2?arguments[2]:{},i=n(e);o&&(i=a.call(i,Object.getOwnPropertySymbols(e)));for(var u=0;u{"use strict";t.exports=EvalError},1152:t=>{"use strict";t.exports=Error},1932:t=>{"use strict";t.exports=RangeError},5028:t=>{"use strict";t.exports=ReferenceError},6500:t=>{"use strict";t.exports=SyntaxError},2824:t=>{"use strict";t.exports=TypeError},5488:t=>{"use strict";t.exports=URIError},9200:(t,e,r)=>{"use strict";var n=r(4624)("%Object.defineProperty%",!0),o=r(4712)(),i=r(4440),a=o?Symbol.toStringTag:null;t.exports=function(t,e){var r=arguments.length>2&&arguments[2]&&arguments[2].force;!a||!r&&i(t,a)||(n?n(t,a,{configurable:!0,enumerable:!1,value:e,writable:!1}):t[a]=e)}},108:(t,e,r)=>{"use strict";var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator,o=r(5988),i=r(648),a=r(1844),u=r(7256);t.exports=function(t){if(o(t))return t;var e,r="default";if(arguments.length>1&&(arguments[1]===String?r="string":arguments[1]===Number&&(r="number")),n&&(Symbol.toPrimitive?e=function(t,e){var r=t[e];if(null!=r){if(!i(r))throw new TypeError(r+" returned for property "+e+" of object "+t+" is not a function");return r}}(t,Symbol.toPrimitive):u(t)&&(e=Symbol.prototype.valueOf)),void 0!==e){var c=e.call(t,r);if(o(c))return c;throw new TypeError("unable to convert exotic object to primitive")}return"default"===r&&(a(t)||u(t))&&(r="string"),function(t,e){if(null==t)throw new TypeError("Cannot call method on "+t);if("string"!=typeof e||"number"!==e&&"string"!==e)throw new TypeError('hint must be "string" or "number"');var r,n,a,u="string"===e?["toString","valueOf"]:["valueOf","toString"];for(a=0;a{"use strict";t.exports=function(t){return null===t||"function"!=typeof t&&"object"!=typeof t}},1480:t=>{"use strict";var e=Object.prototype.toString,r=Math.max,n=function(t,e){for(var r=[],n=0;n{"use strict";var n=r(1480);t.exports=Function.prototype.bind||n},2656:t=>{"use strict";var e=function(){return"string"==typeof function(){}.name},r=Object.getOwnPropertyDescriptor;if(r)try{r([],"length")}catch(t){r=null}e.functionsHaveConfigurableNames=function(){if(!e()||!r)return!1;var t=r((function(){}),"name");return!!t&&!!t.configurable};var n=Function.prototype.bind;e.boundFunctionsHaveNames=function(){return e()&&"function"==typeof n&&""!==function(){}.bind().name},t.exports=e},4624:(t,e,r)=>{"use strict";var n,o=r(1152),i=r(7261),a=r(1932),u=r(5028),c=r(6500),s=r(2824),l=r(5488),f=Function,p=function(t){try{return f('"use strict"; return ('+t+").constructor;")()}catch(t){}},y=Object.getOwnPropertyDescriptor;if(y)try{y({},"")}catch(t){y=null}var d=function(){throw new s},h=y?function(){try{return d}catch(t){try{return y(arguments,"callee").get}catch(t){return d}}}():d,g=r(9800)(),m=r(7e3)(),b=Object.getPrototypeOf||(m?function(t){return t.__proto__}:null),v={},w="undefined"!=typeof Uint8Array&&b?b(Uint8Array):n,x={__proto__:null,"%AggregateError%":"undefined"==typeof AggregateError?n:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?n:ArrayBuffer,"%ArrayIteratorPrototype%":g&&b?b([][Symbol.iterator]()):n,"%AsyncFromSyncIteratorPrototype%":n,"%AsyncFunction%":v,"%AsyncGenerator%":v,"%AsyncGeneratorFunction%":v,"%AsyncIteratorPrototype%":v,"%Atomics%":"undefined"==typeof Atomics?n:Atomics,"%BigInt%":"undefined"==typeof BigInt?n:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?n:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?n:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?n:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":o,"%eval%":eval,"%EvalError%":i,"%Float32Array%":"undefined"==typeof Float32Array?n:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?n:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?n:FinalizationRegistry,"%Function%":f,"%GeneratorFunction%":v,"%Int8Array%":"undefined"==typeof Int8Array?n:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?n:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?n:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":g&&b?b(b([][Symbol.iterator]())):n,"%JSON%":"object"==typeof JSON?JSON:n,"%Map%":"undefined"==typeof Map?n:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&g&&b?b((new Map)[Symbol.iterator]()):n,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?n:Promise,"%Proxy%":"undefined"==typeof Proxy?n:Proxy,"%RangeError%":a,"%ReferenceError%":u,"%Reflect%":"undefined"==typeof Reflect?n:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?n:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&g&&b?b((new Set)[Symbol.iterator]()):n,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?n:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":g&&b?b(""[Symbol.iterator]()):n,"%Symbol%":g?Symbol:n,"%SyntaxError%":c,"%ThrowTypeError%":h,"%TypedArray%":w,"%TypeError%":s,"%Uint8Array%":"undefined"==typeof Uint8Array?n:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?n:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?n:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?n:Uint32Array,"%URIError%":l,"%WeakMap%":"undefined"==typeof WeakMap?n:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?n:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?n:WeakSet};if(b)try{null.error}catch(t){var E=b(b(t));x["%Error.prototype%"]=E}var S=function t(e){var r;if("%AsyncFunction%"===e)r=p("async function () {}");else if("%GeneratorFunction%"===e)r=p("function* () {}");else if("%AsyncGeneratorFunction%"===e)r=p("async function* () {}");else if("%AsyncGenerator%"===e){var n=t("%AsyncGeneratorFunction%");n&&(r=n.prototype)}else if("%AsyncIteratorPrototype%"===e){var o=t("%AsyncGenerator%");o&&b&&(r=b(o.prototype))}return x[e]=r,r},A={__proto__:null,"%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"]},O=r(3520),j=r(4440),T=O.call(Function.call,Array.prototype.concat),P=O.call(Function.apply,Array.prototype.splice),R=O.call(Function.call,String.prototype.replace),C=O.call(Function.call,String.prototype.slice),I=O.call(Function.call,RegExp.prototype.exec),N=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,M=/\\(\\)?/g,$=function(t,e){var r,n=t;if(j(A,n)&&(n="%"+(r=A[n])[0]+"%"),j(x,n)){var o=x[n];if(o===v&&(o=S(n)),void 0===o&&!e)throw new s("intrinsic "+t+" exists, but is not available. Please file an issue!");return{alias:r,name:n,value:o}}throw new c("intrinsic "+t+" does not exist!")};t.exports=function(t,e){if("string"!=typeof t||0===t.length)throw new s("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof e)throw new s('"allowMissing" argument must be a boolean');if(null===I(/^%?[^%]*%?$/,t))throw new c("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var r=function(t){var e=C(t,0,1),r=C(t,-1);if("%"===e&&"%"!==r)throw new c("invalid intrinsic syntax, expected closing `%`");if("%"===r&&"%"!==e)throw new c("invalid intrinsic syntax, expected opening `%`");var n=[];return R(t,N,(function(t,e,r,o){n[n.length]=r?R(o,M,"$1"):e||t})),n}(t),n=r.length>0?r[0]:"",o=$("%"+n+"%",e),i=o.name,a=o.value,u=!1,l=o.alias;l&&(n=l[0],P(r,T([0,1],l)));for(var f=1,p=!0;f=r.length){var m=y(a,d);a=(p=!!m)&&"get"in m&&!("originalValue"in m.get)?m.get:a[d]}else p=j(a,d),a=a[d];p&&!u&&(x[i]=a)}}return a}},6168:(t,e,r)=>{"use strict";var n=r(4624)("%Object.getOwnPropertyDescriptor%",!0);if(n)try{n([],"length")}catch(t){n=null}t.exports=n},3268:(t,e,r)=>{"use strict";var n=r(4624)("%Object.defineProperty%",!0),o=function(){if(n)try{return n({},"a",{value:1}),!0}catch(t){return!1}return!1};o.hasArrayLengthDefineBug=function(){if(!o())return null;try{return 1!==n([],"length",{value:1}).length}catch(t){return!0}},t.exports=o},7e3:t=>{"use strict";var e={foo:{}},r=Object;t.exports=function(){return{__proto__:e}.foo===e.foo&&!({__proto__:null}instanceof r)}},9800:(t,e,r)=>{"use strict";var n="undefined"!=typeof Symbol&&Symbol,o=r(7904);t.exports=function(){return"function"==typeof n&&"function"==typeof Symbol&&"symbol"==typeof n("foo")&&"symbol"==typeof Symbol("bar")&&o()}},7904:t=>{"use strict";t.exports=function(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var t={},e=Symbol("test"),r=Object(e);if("string"==typeof e)return!1;if("[object Symbol]"!==Object.prototype.toString.call(e))return!1;if("[object Symbol]"!==Object.prototype.toString.call(r))return!1;for(e in t[e]=42,t)return!1;if("function"==typeof Object.keys&&0!==Object.keys(t).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(t).length)return!1;var n=Object.getOwnPropertySymbols(t);if(1!==n.length||n[0]!==e)return!1;if(!Object.prototype.propertyIsEnumerable.call(t,e))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var o=Object.getOwnPropertyDescriptor(t,e);if(42!==o.value||!0!==o.enumerable)return!1}return!0}},4712:(t,e,r)=>{"use strict";var n=r(7904);t.exports=function(){return n()&&!!Symbol.toStringTag}},4440:(t,e,r)=>{"use strict";var n=Function.prototype.call,o=Object.prototype.hasOwnProperty,i=r(3520);t.exports=i.call(n,o)},7284:(t,e,r)=>{"use strict";var n=r(4440),o=r(3147)(),i=r(2824),a={assert:function(t,e){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");if(o.assert(t),!a.has(t,e))throw new i("`"+e+"` is not present on `O`")},get:function(t,e){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");var r=o.get(t);return r&&r["$"+e]},has:function(t,e){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");var r=o.get(t);return!!r&&n(r,"$"+e)},set:function(t,e,r){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");var n=o.get(t);n||(n={},o.set(t,n)),n["$"+e]=r}};Object.freeze&&Object.freeze(a),t.exports=a},648:t=>{"use strict";var e,r,n=Function.prototype.toString,o="object"==typeof Reflect&&null!==Reflect&&Reflect.apply;if("function"==typeof o&&"function"==typeof Object.defineProperty)try{e=Object.defineProperty({},"length",{get:function(){throw r}}),r={},o((function(){throw 42}),null,e)}catch(t){t!==r&&(o=null)}else o=null;var i=/^\s*class\b/,a=function(t){try{var e=n.call(t);return i.test(e)}catch(t){return!1}},u=function(t){try{return!a(t)&&(n.call(t),!0)}catch(t){return!1}},c=Object.prototype.toString,s="function"==typeof Symbol&&!!Symbol.toStringTag,l=!(0 in[,]),f=function(){return!1};if("object"==typeof document){var p=document.all;c.call(p)===c.call(document.all)&&(f=function(t){if((l||!t)&&(void 0===t||"object"==typeof t))try{var e=c.call(t);return("[object HTMLAllCollection]"===e||"[object HTML document.all class]"===e||"[object HTMLCollection]"===e||"[object Object]"===e)&&null==t("")}catch(t){}return!1})}t.exports=o?function(t){if(f(t))return!0;if(!t)return!1;if("function"!=typeof t&&"object"!=typeof t)return!1;try{o(t,null,e)}catch(t){if(t!==r)return!1}return!a(t)&&u(t)}:function(t){if(f(t))return!0;if(!t)return!1;if("function"!=typeof t&&"object"!=typeof t)return!1;if(s)return u(t);if(a(t))return!1;var e=c.call(t);return!("[object Function]"!==e&&"[object GeneratorFunction]"!==e&&!/^\[object HTML/.test(e))&&u(t)}},1844:(t,e,r)=>{"use strict";var n=Date.prototype.getDay,o=Object.prototype.toString,i=r(4712)();t.exports=function(t){return"object"==typeof t&&null!==t&&(i?function(t){try{return n.call(t),!0}catch(t){return!1}}(t):"[object Date]"===o.call(t))}},1476:(t,e,r)=>{"use strict";var n,o,i,a,u=r(668),c=r(4712)();if(c){n=u("Object.prototype.hasOwnProperty"),o=u("RegExp.prototype.exec"),i={};var s=function(){throw i};a={toString:s,valueOf:s},"symbol"==typeof Symbol.toPrimitive&&(a[Symbol.toPrimitive]=s)}var l=u("Object.prototype.toString"),f=Object.getOwnPropertyDescriptor;t.exports=c?function(t){if(!t||"object"!=typeof t)return!1;var e=f(t,"lastIndex");if(!e||!n(e,"value"))return!1;try{o(t,a)}catch(t){return t===i}}:function(t){return!(!t||"object"!=typeof t&&"function"!=typeof t)&&"[object RegExp]"===l(t)}},7256:(t,e,r)=>{"use strict";var n=Object.prototype.toString;if(r(9800)()){var o=Symbol.prototype.toString,i=/^Symbol\(.*\)$/;t.exports=function(t){if("symbol"==typeof t)return!0;if("[object Symbol]"!==n.call(t))return!1;try{return function(t){return"symbol"==typeof t.valueOf()&&i.test(o.call(t))}(t)}catch(t){return!1}}}else t.exports=function(t){return!1}},4152:(t,e,r)=>{var n="function"==typeof Map&&Map.prototype,o=Object.getOwnPropertyDescriptor&&n?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,i=n&&o&&"function"==typeof o.get?o.get:null,a=n&&Map.prototype.forEach,u="function"==typeof Set&&Set.prototype,c=Object.getOwnPropertyDescriptor&&u?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,s=u&&c&&"function"==typeof c.get?c.get:null,l=u&&Set.prototype.forEach,f="function"==typeof WeakMap&&WeakMap.prototype?WeakMap.prototype.has:null,p="function"==typeof WeakSet&&WeakSet.prototype?WeakSet.prototype.has:null,y="function"==typeof WeakRef&&WeakRef.prototype?WeakRef.prototype.deref:null,d=Boolean.prototype.valueOf,h=Object.prototype.toString,g=Function.prototype.toString,m=String.prototype.match,b=String.prototype.slice,v=String.prototype.replace,w=String.prototype.toUpperCase,x=String.prototype.toLowerCase,E=RegExp.prototype.test,S=Array.prototype.concat,A=Array.prototype.join,O=Array.prototype.slice,j=Math.floor,T="function"==typeof BigInt?BigInt.prototype.valueOf:null,P=Object.getOwnPropertySymbols,R="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,C="function"==typeof Symbol&&"object"==typeof Symbol.iterator,I="function"==typeof Symbol&&Symbol.toStringTag&&(Symbol.toStringTag,1)?Symbol.toStringTag:null,N=Object.prototype.propertyIsEnumerable,M=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(t){return t.__proto__}:null);function $(t,e){if(t===1/0||t===-1/0||t!=t||t&&t>-1e3&&t<1e3||E.call(/e/,e))return e;var r=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof t){var n=t<0?-j(-t):j(t);if(n!==t){var o=String(n),i=b.call(e,o.length+1);return v.call(o,r,"$&_")+"."+v.call(v.call(i,/([0-9]{3})/g,"$&_"),/_$/,"")}}return v.call(e,r,"$&_")}var k=r(1740),D=k.custom,L=U(D)?D:null;function F(t,e,r){var n="double"===(r.quoteStyle||e)?'"':"'";return n+t+n}function B(t){return v.call(String(t),/"/g,""")}function _(t){return!("[object Array]"!==G(t)||I&&"object"==typeof t&&I in t)}function W(t){return!("[object RegExp]"!==G(t)||I&&"object"==typeof t&&I in t)}function U(t){if(C)return t&&"object"==typeof t&&t instanceof Symbol;if("symbol"==typeof t)return!0;if(!t||"object"!=typeof t||!R)return!1;try{return R.call(t),!0}catch(t){}return!1}t.exports=function t(e,n,o,u){var c=n||{};if(H(c,"quoteStyle")&&"single"!==c.quoteStyle&&"double"!==c.quoteStyle)throw new TypeError('option "quoteStyle" must be "single" or "double"');if(H(c,"maxStringLength")&&("number"==typeof c.maxStringLength?c.maxStringLength<0&&c.maxStringLength!==1/0:null!==c.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var h=!H(c,"customInspect")||c.customInspect;if("boolean"!=typeof h&&"symbol"!==h)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(H(c,"indent")&&null!==c.indent&&"\t"!==c.indent&&!(parseInt(c.indent,10)===c.indent&&c.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(H(c,"numericSeparator")&&"boolean"!=typeof c.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var w=c.numericSeparator;if(void 0===e)return"undefined";if(null===e)return"null";if("boolean"==typeof e)return e?"true":"false";if("string"==typeof e)return q(e,c);if("number"==typeof e){if(0===e)return 1/0/e>0?"0":"-0";var E=String(e);return w?$(e,E):E}if("bigint"==typeof e){var j=String(e)+"n";return w?$(e,j):j}var P=void 0===c.depth?5:c.depth;if(void 0===o&&(o=0),o>=P&&P>0&&"object"==typeof e)return _(e)?"[Array]":"[Object]";var D,z=function(t,e){var r;if("\t"===t.indent)r="\t";else{if(!("number"==typeof t.indent&&t.indent>0))return null;r=A.call(Array(t.indent+1)," ")}return{base:r,prev:A.call(Array(e+1),r)}}(c,o);if(void 0===u)u=[];else if(V(u,e)>=0)return"[Circular]";function X(e,r,n){if(r&&(u=O.call(u)).push(r),n){var i={depth:c.depth};return H(c,"quoteStyle")&&(i.quoteStyle=c.quoteStyle),t(e,i,o+1,u)}return t(e,c,o+1,u)}if("function"==typeof e&&!W(e)){var tt=function(t){if(t.name)return t.name;var e=m.call(g.call(t),/^function\s*([\w$]+)/);return e?e[1]:null}(e),et=Z(e,X);return"[Function"+(tt?": "+tt:" (anonymous)")+"]"+(et.length>0?" { "+A.call(et,", ")+" }":"")}if(U(e)){var rt=C?v.call(String(e),/^(Symbol\(.*\))_[^)]*$/,"$1"):R.call(e);return"object"!=typeof e||C?rt:K(rt)}if((D=e)&&"object"==typeof D&&("undefined"!=typeof HTMLElement&&D instanceof HTMLElement||"string"==typeof D.nodeName&&"function"==typeof D.getAttribute)){for(var nt="<"+x.call(String(e.nodeName)),ot=e.attributes||[],it=0;it"}if(_(e)){if(0===e.length)return"[]";var at=Z(e,X);return z&&!function(t){for(var e=0;e=0)return!1;return!0}(at)?"["+Q(at,z)+"]":"[ "+A.call(at,", ")+" ]"}if(function(t){return!("[object Error]"!==G(t)||I&&"object"==typeof t&&I in t)}(e)){var ut=Z(e,X);return"cause"in Error.prototype||!("cause"in e)||N.call(e,"cause")?0===ut.length?"["+String(e)+"]":"{ ["+String(e)+"] "+A.call(ut,", ")+" }":"{ ["+String(e)+"] "+A.call(S.call("[cause]: "+X(e.cause),ut),", ")+" }"}if("object"==typeof e&&h){if(L&&"function"==typeof e[L]&&k)return k(e,{depth:P-o});if("symbol"!==h&&"function"==typeof e.inspect)return e.inspect()}if(function(t){if(!i||!t||"object"!=typeof t)return!1;try{i.call(t);try{s.call(t)}catch(t){return!0}return t instanceof Map}catch(t){}return!1}(e)){var ct=[];return a&&a.call(e,(function(t,r){ct.push(X(r,e,!0)+" => "+X(t,e))})),J("Map",i.call(e),ct,z)}if(function(t){if(!s||!t||"object"!=typeof t)return!1;try{s.call(t);try{i.call(t)}catch(t){return!0}return t instanceof Set}catch(t){}return!1}(e)){var st=[];return l&&l.call(e,(function(t){st.push(X(t,e))})),J("Set",s.call(e),st,z)}if(function(t){if(!f||!t||"object"!=typeof t)return!1;try{f.call(t,f);try{p.call(t,p)}catch(t){return!0}return t instanceof WeakMap}catch(t){}return!1}(e))return Y("WeakMap");if(function(t){if(!p||!t||"object"!=typeof t)return!1;try{p.call(t,p);try{f.call(t,f)}catch(t){return!0}return t instanceof WeakSet}catch(t){}return!1}(e))return Y("WeakSet");if(function(t){if(!y||!t||"object"!=typeof t)return!1;try{return y.call(t),!0}catch(t){}return!1}(e))return Y("WeakRef");if(function(t){return!("[object Number]"!==G(t)||I&&"object"==typeof t&&I in t)}(e))return K(X(Number(e)));if(function(t){if(!t||"object"!=typeof t||!T)return!1;try{return T.call(t),!0}catch(t){}return!1}(e))return K(X(T.call(e)));if(function(t){return!("[object Boolean]"!==G(t)||I&&"object"==typeof t&&I in t)}(e))return K(d.call(e));if(function(t){return!("[object String]"!==G(t)||I&&"object"==typeof t&&I in t)}(e))return K(X(String(e)));if("undefined"!=typeof window&&e===window)return"{ [object Window] }";if(e===r.g)return"{ [object globalThis] }";if(!function(t){return!("[object Date]"!==G(t)||I&&"object"==typeof t&&I in t)}(e)&&!W(e)){var lt=Z(e,X),ft=M?M(e)===Object.prototype:e instanceof Object||e.constructor===Object,pt=e instanceof Object?"":"null prototype",yt=!ft&&I&&Object(e)===e&&I in e?b.call(G(e),8,-1):pt?"Object":"",dt=(ft||"function"!=typeof e.constructor?"":e.constructor.name?e.constructor.name+" ":"")+(yt||pt?"["+A.call(S.call([],yt||[],pt||[]),": ")+"] ":"");return 0===lt.length?dt+"{}":z?dt+"{"+Q(lt,z)+"}":dt+"{ "+A.call(lt,", ")+" }"}return String(e)};var z=Object.prototype.hasOwnProperty||function(t){return t in this};function H(t,e){return z.call(t,e)}function G(t){return h.call(t)}function V(t,e){if(t.indexOf)return t.indexOf(e);for(var r=0,n=t.length;re.maxStringLength){var r=t.length-e.maxStringLength,n="... "+r+" more character"+(r>1?"s":"");return q(b.call(t,0,e.maxStringLength),e)+n}return F(v.call(v.call(t,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,X),"single",e)}function X(t){var e=t.charCodeAt(0),r={8:"b",9:"t",10:"n",12:"f",13:"r"}[e];return r?"\\"+r:"\\x"+(e<16?"0":"")+w.call(e.toString(16))}function K(t){return"Object("+t+")"}function Y(t){return t+" { ? }"}function J(t,e,r,n){return t+" ("+e+") {"+(n?Q(r,n):A.call(r,", "))+"}"}function Q(t,e){if(0===t.length)return"";var r="\n"+e.prev+e.base;return r+A.call(t,","+r)+"\n"+e.prev}function Z(t,e){var r=_(t),n=[];if(r){n.length=t.length;for(var o=0;o{"use strict";var n;if(!Object.keys){var o=Object.prototype.hasOwnProperty,i=Object.prototype.toString,a=r(9096),u=Object.prototype.propertyIsEnumerable,c=!u.call({toString:null},"toString"),s=u.call((function(){}),"prototype"),l=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],f=function(t){var e=t.constructor;return e&&e.prototype===t},p={$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},y=function(){if("undefined"==typeof window)return!1;for(var t in window)try{if(!p["$"+t]&&o.call(window,t)&&null!==window[t]&&"object"==typeof window[t])try{f(window[t])}catch(t){return!0}}catch(t){return!0}return!1}();n=function(t){var e=null!==t&&"object"==typeof t,r="[object Function]"===i.call(t),n=a(t),u=e&&"[object String]"===i.call(t),p=[];if(!e&&!r&&!n)throw new TypeError("Object.keys called on a non-object");var d=s&&r;if(u&&t.length>0&&!o.call(t,0))for(var h=0;h0)for(var g=0;g{"use strict";var n=Array.prototype.slice,o=r(9096),i=Object.keys,a=i?function(t){return i(t)}:r(9560),u=Object.keys;a.shim=function(){if(Object.keys){var t=function(){var t=Object.keys(arguments);return t&&t.length===arguments.length}(1,2);t||(Object.keys=function(t){return o(t)?u(n.call(t)):u(t)})}else Object.keys=a;return Object.keys||a},t.exports=a},9096:t=>{"use strict";var e=Object.prototype.toString;t.exports=function(t){var r=e.call(t),n="[object Arguments]"===r;return n||(n="[object Array]"!==r&&null!==t&&"object"==typeof t&&"number"==typeof t.length&&t.length>=0&&"[object Function]"===e.call(t.callee)),n}},7636:(t,e,r)=>{"use strict";var n=r(6308),o=r(2824),i=Object;t.exports=n((function(){if(null==this||this!==i(this))throw new o("RegExp.prototype.flags getter called on non-object");var t="";return this.hasIndices&&(t+="d"),this.global&&(t+="g"),this.ignoreCase&&(t+="i"),this.multiline&&(t+="m"),this.dotAll&&(t+="s"),this.unicode&&(t+="u"),this.unicodeSets&&(t+="v"),this.sticky&&(t+="y"),t}),"get flags",!0)},2192:(t,e,r)=>{"use strict";var n=r(2732),o=r(5096),i=r(7636),a=r(9296),u=r(736),c=o(a());n(c,{getPolyfill:a,implementation:i,shim:u}),t.exports=c},9296:(t,e,r)=>{"use strict";var n=r(7636),o=r(2732).supportsDescriptors,i=Object.getOwnPropertyDescriptor;t.exports=function(){if(o&&"gim"===/a/gim.flags){var t=i(RegExp.prototype,"flags");if(t&&"function"==typeof t.get&&"boolean"==typeof RegExp.prototype.dotAll&&"boolean"==typeof RegExp.prototype.hasIndices){var e="",r={};if(Object.defineProperty(r,"hasIndices",{get:function(){e+="d"}}),Object.defineProperty(r,"sticky",{get:function(){e+="y"}}),"dy"===e)return t.get}}return n}},736:(t,e,r)=>{"use strict";var n=r(2732).supportsDescriptors,o=r(9296),i=Object.getOwnPropertyDescriptor,a=Object.defineProperty,u=TypeError,c=Object.getPrototypeOf,s=/a/;t.exports=function(){if(!n||!c)throw new u("RegExp.prototype.flags requires a true ES5 environment that supports property descriptors");var t=o(),e=c(s),r=i(e,"flags");return r&&r.get===t||a(e,"flags",{configurable:!0,enumerable:!1,get:t}),t}},860:(t,e,r)=>{"use strict";var n=r(668),o=r(1476),i=n("RegExp.prototype.exec"),a=r(2824);t.exports=function(t){if(!o(t))throw new a("`regex` must be a RegExp");return function(e){return null!==i(t,e)}}},5676:(t,e,r)=>{"use strict";var n=r(4624),o=r(2448),i=r(3268)(),a=r(6168),u=r(2824),c=n("%Math.floor%");t.exports=function(t,e){if("function"!=typeof t)throw new u("`fn` is not a function");if("number"!=typeof e||e<0||e>4294967295||c(e)!==e)throw new u("`length` must be a positive 32-bit integer");var r=arguments.length>2&&!!arguments[2],n=!0,s=!0;if("length"in t&&a){var l=a(t,"length");l&&!l.configurable&&(n=!1),l&&!l.writable&&(s=!1)}return(n||s||!r)&&(i?o(t,"length",e,!0,!0):o(t,"length",e)),t}},6308:(t,e,r)=>{"use strict";var n=r(2448),o=r(3268)(),i=r(2656).functionsHaveConfigurableNames(),a=TypeError;t.exports=function(t,e){if("function"!=typeof t)throw new a("`fn` is not a function");return arguments.length>2&&!!arguments[2]&&!i||(o?n(t,"name",e,!0,!0):n(t,"name",e)),t}},3147:(t,e,r)=>{"use strict";var n=r(4624),o=r(668),i=r(4152),a=r(2824),u=n("%WeakMap%",!0),c=n("%Map%",!0),s=o("WeakMap.prototype.get",!0),l=o("WeakMap.prototype.set",!0),f=o("WeakMap.prototype.has",!0),p=o("Map.prototype.get",!0),y=o("Map.prototype.set",!0),d=o("Map.prototype.has",!0),h=function(t,e){for(var r,n=t;null!==(r=n.next);n=r)if(r.key===e)return n.next=r.next,r.next=t.next,t.next=r,r};t.exports=function(){var t,e,r,n={assert:function(t){if(!n.has(t))throw new a("Side channel does not contain "+i(t))},get:function(n){if(u&&n&&("object"==typeof n||"function"==typeof n)){if(t)return s(t,n)}else if(c){if(e)return p(e,n)}else if(r)return function(t,e){var r=h(t,e);return r&&r.value}(r,n)},has:function(n){if(u&&n&&("object"==typeof n||"function"==typeof n)){if(t)return f(t,n)}else if(c){if(e)return d(e,n)}else if(r)return function(t,e){return!!h(t,e)}(r,n);return!1},set:function(n,o){u&&n&&("object"==typeof n||"function"==typeof n)?(t||(t=new u),l(t,n,o)):c?(e||(e=new c),y(e,n,o)):(r||(r={key:{},next:null}),function(t,e,r){var n=h(t,e);n?n.value=r:t.next={key:e,next:t.next,value:r}}(r,n,o))}};return n}},9508:(t,e,r)=>{"use strict";var n=r(1700),o=r(3672),i=r(5552),a=r(3816),u=r(5424),c=r(4656),s=r(668),l=r(9800)(),f=r(2192),p=s("String.prototype.indexOf"),y=r(6288),d=function(t){var e=y();if(l&&"symbol"==typeof Symbol.matchAll){var r=i(t,Symbol.matchAll);return r===RegExp.prototype[Symbol.matchAll]&&r!==e?e:r}if(a(t))return e};t.exports=function(t){var e=c(this);if(null!=t){if(a(t)){var r="flags"in t?o(t,"flags"):f(t);if(c(r),p(u(r),"g")<0)throw new TypeError("matchAll requires a global regular expression")}var i=d(t);if(void 0!==i)return n(i,t,[e])}var s=u(e),l=new RegExp(t,"g");return n(d(l),l,[s])}},3732:(t,e,r)=>{"use strict";var n=r(5096),o=r(2732),i=r(9508),a=r(5844),u=r(4148),c=n(i);o(c,{getPolyfill:a,implementation:i,shim:u}),t.exports=c},6288:(t,e,r)=>{"use strict";var n=r(9800)(),o=r(7492);t.exports=function(){return n&&"symbol"==typeof Symbol.matchAll&&"function"==typeof RegExp.prototype[Symbol.matchAll]?RegExp.prototype[Symbol.matchAll]:o}},5844:(t,e,r)=>{"use strict";var n=r(9508);t.exports=function(){if(String.prototype.matchAll)try{"".matchAll(RegExp.prototype)}catch(t){return String.prototype.matchAll}return n}},7492:(t,e,r)=>{"use strict";var n=r(5211),o=r(3672),i=r(4e3),a=r(8652),u=r(4784),c=r(5424),s=r(8645),l=r(2192),f=r(6308),p=r(668)("String.prototype.indexOf"),y=RegExp,d="flags"in RegExp.prototype,h=f((function(t){var e=this;if("Object"!==s(e))throw new TypeError('"this" value must be an Object');var r=c(t),f=function(t,e){var r="flags"in e?o(e,"flags"):c(l(e));return{flags:r,matcher:new t(d&&"string"==typeof r?e:t===y?e.source:e,r)}}(a(e,y),e),h=f.flags,g=f.matcher,m=u(o(e,"lastIndex"));i(g,"lastIndex",m,!0);var b=p(h,"g")>-1,v=p(h,"u")>-1;return n(g,r,b,v)}),"[Symbol.matchAll]",!0);t.exports=h},4148:(t,e,r)=>{"use strict";var n=r(2732),o=r(9800)(),i=r(5844),a=r(6288),u=Object.defineProperty,c=Object.getOwnPropertyDescriptor;t.exports=function(){var t=i();if(n(String.prototype,{matchAll:t},{matchAll:function(){return String.prototype.matchAll!==t}}),o){var e=Symbol.matchAll||(Symbol.for?Symbol.for("Symbol.matchAll"):Symbol("Symbol.matchAll"));if(n(Symbol,{matchAll:e},{matchAll:function(){return Symbol.matchAll!==e}}),u&&c){var r=c(Symbol,e);r&&!r.configurable||u(Symbol,e,{configurable:!1,enumerable:!1,value:e,writable:!1})}var s=a(),l={};l[e]=s;var f={};f[e]=function(){return RegExp.prototype[e]!==s},n(RegExp.prototype,l,f)}return t}},6936:(t,e,r)=>{"use strict";var n=r(4656),o=r(5424),i=r(668)("String.prototype.replace"),a=/^\s$/.test("᠎"),u=a?/^[\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]+/,c=a?/[\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]+$/;t.exports=function(){var t=o(n(this));return i(i(t,u,""),c,"")}},9292:(t,e,r)=>{"use strict";var n=r(5096),o=r(2732),i=r(4656),a=r(6936),u=r(6684),c=r(9788),s=n(u()),l=function(t){return i(t),s(t)};o(l,{getPolyfill:u,implementation:a,shim:c}),t.exports=l},6684:(t,e,r)=>{"use strict";var n=r(6936);t.exports=function(){return String.prototype.trim&&"​"==="​".trim()&&"᠎"==="᠎".trim()&&"_᠎"==="_᠎".trim()&&"᠎_"==="᠎_".trim()?String.prototype.trim:n}},9788:(t,e,r)=>{"use strict";var n=r(2732),o=r(6684);t.exports=function(){var t=o();return n(String.prototype,{trim:t},{trim:function(){return String.prototype.trim!==t}}),t}},1740:()=>{},1056:(t,e,r)=>{"use strict";var n=r(4624),o=r(8536),i=r(8645),a=r(7724),u=r(9132),c=n("%TypeError%");t.exports=function(t,e,r){if("String"!==i(t))throw new c("Assertion failed: `S` must be a String");if(!a(e)||e<0||e>u)throw new c("Assertion failed: `length` must be an integer >= 0 and <= 2**53");if("Boolean"!==i(r))throw new c("Assertion failed: `unicode` must be a Boolean");return r?e+1>=t.length?e+1:e+o(t,e)["[[CodeUnitCount]]"]:e+1}},1700:(t,e,r)=>{"use strict";var n=r(4624),o=r(668),i=n("%TypeError%"),a=r(1720),u=n("%Reflect.apply%",!0)||o("Function.prototype.apply");t.exports=function(t,e){var r=arguments.length>2?arguments[2]:[];if(!a(r))throw new i("Assertion failed: optional `argumentsList`, if provided, must be a List");return u(t,e,r)}},8536:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(668),i=r(1712),a=r(8444),u=r(8645),c=r(2320),s=o("String.prototype.charAt"),l=o("String.prototype.charCodeAt");t.exports=function(t,e){if("String"!==u(t))throw new n("Assertion failed: `string` must be a String");var r=t.length;if(e<0||e>=r)throw new n("Assertion failed: `position` must be >= 0, and < the length of `string`");var o=l(t,e),f=s(t,e),p=i(o),y=a(o);if(!p&&!y)return{"[[CodePoint]]":f,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!1};if(y||e+1===r)return{"[[CodePoint]]":f,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!0};var d=l(t,e+1);return a(d)?{"[[CodePoint]]":c(o,d),"[[CodeUnitCount]]":2,"[[IsUnpairedSurrogate]]":!1}:{"[[CodePoint]]":f,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!0}}},4288:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(8645);t.exports=function(t,e){if("Boolean"!==o(e))throw new n("Assertion failed: Type(done) is not Boolean");return{value:t,done:e}}},2672:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(4436),i=r(8924),a=r(3880),u=r(2968),c=r(8800),s=r(8645);t.exports=function(t,e,r){if("Object"!==s(t))throw new n("Assertion failed: Type(O) is not Object");if(!u(e))throw new n("Assertion failed: IsPropertyKey(P) is not true");return o(a,c,i,t,e,{"[[Configurable]]":!0,"[[Enumerable]]":!1,"[[Value]]":r,"[[Writable]]":!0})}},5211:(t,e,r)=>{"use strict";var n=r(4624),o=r(9800)(),i=n("%TypeError%"),a=n("%IteratorPrototype%",!0),u=r(1056),c=r(4288),s=r(2672),l=r(3672),f=r(6216),p=r(8972),y=r(4e3),d=r(4784),h=r(5424),g=r(8645),m=r(7284),b=r(9200),v=function(t,e,r,n){if("String"!==g(e))throw new i("`S` must be a string");if("Boolean"!==g(r))throw new i("`global` must be a boolean");if("Boolean"!==g(n))throw new i("`fullUnicode` must be a boolean");m.set(this,"[[IteratingRegExp]]",t),m.set(this,"[[IteratedString]]",e),m.set(this,"[[Global]]",r),m.set(this,"[[Unicode]]",n),m.set(this,"[[Done]]",!1)};a&&(v.prototype=f(a)),s(v.prototype,"next",(function(){var t=this;if("Object"!==g(t))throw new i("receiver must be an object");if(!(t instanceof v&&m.has(t,"[[IteratingRegExp]]")&&m.has(t,"[[IteratedString]]")&&m.has(t,"[[Global]]")&&m.has(t,"[[Unicode]]")&&m.has(t,"[[Done]]")))throw new i('"this" value must be a RegExpStringIterator instance');if(m.get(t,"[[Done]]"))return c(void 0,!0);var e=m.get(t,"[[IteratingRegExp]]"),r=m.get(t,"[[IteratedString]]"),n=m.get(t,"[[Global]]"),o=m.get(t,"[[Unicode]]"),a=p(e,r);if(null===a)return m.set(t,"[[Done]]",!0),c(void 0,!0);if(n){if(""===h(l(a,"0"))){var s=d(l(e,"lastIndex")),f=u(r,s,o);y(e,"lastIndex",f,!0)}return c(a,!1)}return m.set(t,"[[Done]]",!0),c(a,!1)})),o&&(b(v.prototype,"RegExp String Iterator"),Symbol.iterator&&"function"!=typeof v.prototype[Symbol.iterator])&&s(v.prototype,Symbol.iterator,(function(){return this})),t.exports=function(t,e,r,n){return new v(t,e,r,n)}},7268:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(320),i=r(4436),a=r(8924),u=r(4936),c=r(3880),s=r(2968),l=r(8800),f=r(5696),p=r(8645);t.exports=function(t,e,r){if("Object"!==p(t))throw new n("Assertion failed: Type(O) is not Object");if(!s(e))throw new n("Assertion failed: IsPropertyKey(P) is not true");var y=o({Type:p,IsDataDescriptor:c,IsAccessorDescriptor:u},r)?r:f(r);if(!o({Type:p,IsDataDescriptor:c,IsAccessorDescriptor:u},y))throw new n("Assertion failed: Desc is not a valid Property Descriptor");return i(c,l,a,t,e,y)}},8924:(t,e,r)=>{"use strict";var n=r(3600),o=r(3504),i=r(8645);t.exports=function(t){return void 0!==t&&n(i,"Property Descriptor","Desc",t),o(t)}},3672:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(4152),i=r(2968),a=r(8645);t.exports=function(t,e){if("Object"!==a(t))throw new n("Assertion failed: Type(O) is not Object");if(!i(e))throw new n("Assertion failed: IsPropertyKey(P) is not true, got "+o(e));return t[e]}},5552:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(3396),i=r(3048),a=r(2968),u=r(4152);t.exports=function(t,e){if(!a(e))throw new n("Assertion failed: IsPropertyKey(P) is not true");var r=o(t,e);if(null!=r){if(!i(r))throw new n(u(e)+" is not a function: "+u(r));return r}}},3396:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(4152),i=r(2968);t.exports=function(t,e){if(!i(e))throw new n("Assertion failed: IsPropertyKey(P) is not true, got "+o(e));return t[e]}},4936:(t,e,r)=>{"use strict";var n=r(4440),o=r(8645),i=r(3600);t.exports=function(t){return void 0!==t&&(i(o,"Property Descriptor","Desc",t),!(!n(t,"[[Get]]")&&!n(t,"[[Set]]")))}},1720:(t,e,r)=>{"use strict";t.exports=r(704)},3048:(t,e,r)=>{"use strict";t.exports=r(648)},211:(t,e,r)=>{"use strict";var n=r(8600)("%Reflect.construct%",!0),o=r(7268);try{o({},"",{"[[Get]]":function(){}})}catch(t){o=null}if(o&&n){var i={},a={};o(a,"length",{"[[Get]]":function(){throw i},"[[Enumerable]]":!0}),t.exports=function(t){try{n(t,a)}catch(t){return t===i}}}else t.exports=function(t){return"function"==typeof t&&!!t.prototype}},3880:(t,e,r)=>{"use strict";var n=r(4440),o=r(8645),i=r(3600);t.exports=function(t){return void 0!==t&&(i(o,"Property Descriptor","Desc",t),!(!n(t,"[[Value]]")&&!n(t,"[[Writable]]")))}},2968:t=>{"use strict";t.exports=function(t){return"string"==typeof t||"symbol"==typeof t}},3816:(t,e,r)=>{"use strict";var n=r(4624)("%Symbol.match%",!0),o=r(1476),i=r(6848);t.exports=function(t){if(!t||"object"!=typeof t)return!1;if(n){var e=t[n];if(void 0!==e)return i(e)}return o(t)}},6216:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Object.create%",!0),i=n("%TypeError%"),a=n("%SyntaxError%"),u=r(1720),c=r(8645),s=r(4672),l=r(7284),f=r(7e3)();t.exports=function(t){if(null!==t&&"Object"!==c(t))throw new i("Assertion failed: `proto` must be null or an object");var e,r=arguments.length<2?[]:arguments[1];if(!u(r))throw new i("Assertion failed: `additionalInternalSlotsList` must be an Array");if(o)e=o(t);else if(f)e={__proto__:t};else{if(null===t)throw new a("native Object.create support is required to create null objects");var n=function(){};n.prototype=t,e=new n}return r.length>0&&s(r,(function(t){l.set(e,t,void 0)})),e}},8972:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(668)("RegExp.prototype.exec"),i=r(1700),a=r(3672),u=r(3048),c=r(8645);t.exports=function(t,e){if("Object"!==c(t))throw new n("Assertion failed: `R` must be an Object");if("String"!==c(e))throw new n("Assertion failed: `S` must be a String");var r=a(t,"exec");if(u(r)){var s=i(r,t,[e]);if(null===s||"Object"===c(s))return s;throw new n('"exec" method must return `null` or an Object')}return o(t,e)}},4656:(t,e,r)=>{"use strict";t.exports=r(176)},8800:(t,e,r)=>{"use strict";var n=r(2808);t.exports=function(t,e){return t===e?0!==t||1/t==1/e:n(t)&&n(e)}},4e3:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(2968),i=r(8800),a=r(8645),u=function(){try{return delete[].length,!0}catch(t){return!1}}();t.exports=function(t,e,r,c){if("Object"!==a(t))throw new n("Assertion failed: `O` must be an Object");if(!o(e))throw new n("Assertion failed: `P` must be a Property Key");if("Boolean"!==a(c))throw new n("Assertion failed: `Throw` must be a Boolean");if(c){if(t[e]=r,u&&!i(t[e],r))throw new n("Attempted to assign to readonly property.");return!0}try{return t[e]=r,!u||i(t[e],r)}catch(t){return!1}}},8652:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Symbol.species%",!0),i=n("%TypeError%"),a=r(211),u=r(8645);t.exports=function(t,e){if("Object"!==u(t))throw new i("Assertion failed: Type(O) is not Object");var r=t.constructor;if(void 0===r)return e;if("Object"!==u(r))throw new i("O.constructor is not an Object");var n=o?r[o]:void 0;if(null==n)return e;if(a(n))return n;throw new i("no constructor found")}},8772:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Number%"),i=n("%RegExp%"),a=n("%TypeError%"),u=n("%parseInt%"),c=r(668),s=r(860),l=c("String.prototype.slice"),f=s(/^0b[01]+$/i),p=s(/^0o[0-7]+$/i),y=s(/^[-+]0x[0-9a-f]+$/i),d=s(new i("["+["…","​","￾"].join("")+"]","g")),h=r(9292),g=r(8645);t.exports=function t(e){if("String"!==g(e))throw new a("Assertion failed: `argument` is not a String");if(f(e))return o(u(l(e,2),2));if(p(e))return o(u(l(e,2),8));if(d(e)||y(e))return NaN;var r=h(e);return r!==e?t(r):o(e)}},6848:t=>{"use strict";t.exports=function(t){return!!t}},9424:(t,e,r)=>{"use strict";var n=r(7220),o=r(2592),i=r(2808),a=r(2931);t.exports=function(t){var e=n(t);return i(e)||0===e?0:a(e)?o(e):e}},4784:(t,e,r)=>{"use strict";var n=r(9132),o=r(9424);t.exports=function(t){var e=o(t);return e<=0?0:e>n?n:e}},7220:(t,e,r)=>{"use strict";var n=r(4624),o=n("%TypeError%"),i=n("%Number%"),a=r(2336),u=r(5556),c=r(8772);t.exports=function(t){var e=a(t)?t:u(t,i);if("symbol"==typeof e)throw new o("Cannot convert a Symbol value to a number");if("bigint"==typeof e)throw new o("Conversion from 'BigInt' to 'number' is not allowed.");return"string"==typeof e?c(e):i(e)}},5556:(t,e,r)=>{"use strict";var n=r(108);t.exports=function(t){return arguments.length>1?n(t,arguments[1]):n(t)}},5696:(t,e,r)=>{"use strict";var n=r(4440),o=r(4624)("%TypeError%"),i=r(8645),a=r(6848),u=r(3048);t.exports=function(t){if("Object"!==i(t))throw new o("ToPropertyDescriptor requires an object");var e={};if(n(t,"enumerable")&&(e["[[Enumerable]]"]=a(t.enumerable)),n(t,"configurable")&&(e["[[Configurable]]"]=a(t.configurable)),n(t,"value")&&(e["[[Value]]"]=t.value),n(t,"writable")&&(e["[[Writable]]"]=a(t.writable)),n(t,"get")){var r=t.get;if(void 0!==r&&!u(r))throw new o("getter must be a function");e["[[Get]]"]=r}if(n(t,"set")){var c=t.set;if(void 0!==c&&!u(c))throw new o("setter must be a function");e["[[Set]]"]=c}if((n(e,"[[Get]]")||n(e,"[[Set]]"))&&(n(e,"[[Value]]")||n(e,"[[Writable]]")))throw new o("Invalid property descriptor. Cannot both specify accessors and a value or writable attribute");return e}},5424:(t,e,r)=>{"use strict";var n=r(4624),o=n("%String%"),i=n("%TypeError%");t.exports=function(t){if("symbol"==typeof t)throw new i("Cannot convert a Symbol value to a string");return o(t)}},8645:(t,e,r)=>{"use strict";var n=r(7936);t.exports=function(t){return"symbol"==typeof t?"Symbol":"bigint"==typeof t?"BigInt":n(t)}},2320:(t,e,r)=>{"use strict";var n=r(4624),o=n("%TypeError%"),i=n("%String.fromCharCode%"),a=r(1712),u=r(8444);t.exports=function(t,e){if(!a(t)||!u(e))throw new o("Assertion failed: `lead` must be a leading surrogate char code, and `trail` must be a trailing surrogate char code");return i(t)+i(e)}},2312:(t,e,r)=>{"use strict";var n=r(8645),o=Math.floor;t.exports=function(t){return"BigInt"===n(t)?t:o(t)}},2592:(t,e,r)=>{"use strict";var n=r(4624),o=r(2312),i=n("%TypeError%");t.exports=function(t){if("number"!=typeof t&&"bigint"!=typeof t)throw new i("argument must be a Number or a BigInt");var e=t<0?-o(-t):o(t);return 0===e?0:e}},176:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%");t.exports=function(t,e){if(null==t)throw new n(e||"Cannot call method on "+t);return t}},7936:t=>{"use strict";t.exports=function(t){return null===t?"Null":void 0===t?"Undefined":"function"==typeof t||"object"==typeof t?"Object":"number"==typeof t?"Number":"boolean"==typeof t?"Boolean":"string"==typeof t?"String":void 0}},8600:(t,e,r)=>{"use strict";t.exports=r(4624)},4436:(t,e,r)=>{"use strict";var n=r(3268),o=r(4624),i=n()&&o("%Object.defineProperty%",!0),a=n.hasArrayLengthDefineBug(),u=a&&r(704),c=r(668)("Object.prototype.propertyIsEnumerable");t.exports=function(t,e,r,n,o,s){if(!i){if(!t(s))return!1;if(!s["[[Configurable]]"]||!s["[[Writable]]"])return!1;if(o in n&&c(n,o)!==!!s["[[Enumerable]]"])return!1;var l=s["[[Value]]"];return n[o]=l,e(n[o],l)}return a&&"length"===o&&"[[Value]]"in s&&u(n)&&n.length!==s["[[Value]]"]?(n.length=s["[[Value]]"],n.length===s["[[Value]]"]):(i(n,o,r(s)),!0)}},704:(t,e,r)=>{"use strict";var n=r(4624)("%Array%"),o=!n.isArray&&r(668)("Object.prototype.toString");t.exports=n.isArray||function(t){return"[object Array]"===o(t)}},3600:(t,e,r)=>{"use strict";var n=r(4624),o=n("%TypeError%"),i=n("%SyntaxError%"),a=r(4440),u=r(7724),c={"Property Descriptor":function(t){var e={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};if(!t)return!1;for(var r in t)if(a(t,r)&&!e[r])return!1;var n=a(t,"[[Value]]"),i=a(t,"[[Get]]")||a(t,"[[Set]]");if(n&&i)throw new o("Property Descriptors may not be both accessor and data descriptors");return!0},"Match Record":r(5092),"Iterator Record":function(t){return a(t,"[[Iterator]]")&&a(t,"[[NextMethod]]")&&a(t,"[[Done]]")},"PromiseCapability Record":function(t){return!!t&&a(t,"[[Resolve]]")&&"function"==typeof t["[[Resolve]]"]&&a(t,"[[Reject]]")&&"function"==typeof t["[[Reject]]"]&&a(t,"[[Promise]]")&&t["[[Promise]]"]&&"function"==typeof t["[[Promise]]"].then},"AsyncGeneratorRequest Record":function(t){return!!t&&a(t,"[[Completion]]")&&a(t,"[[Capability]]")&&c["PromiseCapability Record"](t["[[Capability]]"])},"RegExp Record":function(t){return t&&a(t,"[[IgnoreCase]]")&&"boolean"==typeof t["[[IgnoreCase]]"]&&a(t,"[[Multiline]]")&&"boolean"==typeof t["[[Multiline]]"]&&a(t,"[[DotAll]]")&&"boolean"==typeof t["[[DotAll]]"]&&a(t,"[[Unicode]]")&&"boolean"==typeof t["[[Unicode]]"]&&a(t,"[[CapturingGroupsCount]]")&&"number"==typeof t["[[CapturingGroupsCount]]"]&&u(t["[[CapturingGroupsCount]]"])&&t["[[CapturingGroupsCount]]"]>=0}};t.exports=function(t,e,r,n){var a=c[e];if("function"!=typeof a)throw new i("unknown record type: "+e);if("Object"!==t(n)||!a(n))throw new o(r+" must be a "+e)}},4672:t=>{"use strict";t.exports=function(t,e){for(var r=0;r{"use strict";t.exports=function(t){if(void 0===t)return t;var e={};return"[[Value]]"in t&&(e.value=t["[[Value]]"]),"[[Writable]]"in t&&(e.writable=!!t["[[Writable]]"]),"[[Get]]"in t&&(e.get=t["[[Get]]"]),"[[Set]]"in t&&(e.set=t["[[Set]]"]),"[[Enumerable]]"in t&&(e.enumerable=!!t["[[Enumerable]]"]),"[[Configurable]]"in t&&(e.configurable=!!t["[[Configurable]]"]),e}},2931:(t,e,r)=>{"use strict";var n=r(2808);t.exports=function(t){return("number"==typeof t||"bigint"==typeof t)&&!n(t)&&t!==1/0&&t!==-1/0}},7724:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Math.abs%"),i=n("%Math.floor%"),a=r(2808),u=r(2931);t.exports=function(t){if("number"!=typeof t||a(t)||!u(t))return!1;var e=o(t);return i(e)===e}},1712:t=>{"use strict";t.exports=function(t){return"number"==typeof t&&t>=55296&&t<=56319}},5092:(t,e,r)=>{"use strict";var n=r(4440);t.exports=function(t){return n(t,"[[StartIndex]]")&&n(t,"[[EndIndex]]")&&t["[[StartIndex]]"]>=0&&t["[[EndIndex]]"]>=t["[[StartIndex]]"]&&String(parseInt(t["[[StartIndex]]"],10))===String(t["[[StartIndex]]"])&&String(parseInt(t["[[EndIndex]]"],10))===String(t["[[EndIndex]]"])}},2808:t=>{"use strict";t.exports=Number.isNaN||function(t){return t!=t}},2336:t=>{"use strict";t.exports=function(t){return null===t||"function"!=typeof t&&"object"!=typeof t}},320:(t,e,r)=>{"use strict";var n=r(4624),o=r(4440),i=n("%TypeError%");t.exports=function(t,e){if("Object"!==t.Type(e))return!1;var r={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};for(var n in e)if(o(e,n)&&!r[n])return!1;if(t.IsDataDescriptor(e)&&t.IsAccessorDescriptor(e))throw new i("Property Descriptors may not be both accessor and data descriptors");return!0}},8444:t=>{"use strict";t.exports=function(t){return"number"==typeof t&&t>=56320&&t<=57343}},9132:t=>{"use strict";t.exports=Number.MAX_SAFE_INTEGER||9007199254740991}},e={};function r(n){var o=e[n];if(void 0!==o)return o.exports;var i=e[n]={exports:{}};return t[n](i,i.exports,r),i.exports}r.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return r.d(e,{a:e}),e},r.d=(t,e)=>{for(var n in e)r.o(e,n)&&!r.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";var t=r(9116);function e(e,r,n){let o=0,i=[];for(;-1!==o;)o=e.indexOf(r,o),-1!==o&&(i.push({start:o,end:o+r.length,errors:0}),o+=1);return i.length>0?i:(0,t.c)(e,r,n)}function n(t,r){return 0===r.length||0===t.length?0:1-e(t,r,r.length)[0].errors/r.length}function o(t){switch(t.nodeType){case Node.ELEMENT_NODE:case Node.TEXT_NODE:return t.textContent.length;default:return 0}}function i(t){let e=t.previousSibling,r=0;for(;e;)r+=o(e),e=e.previousSibling;return r}function a(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),n=1;no?(a.push({node:u,offset:o-s}),o=r.shift()):(c=i.nextNode(),s+=u.data.length);for(;void 0!==o&&u&&s===o;)a.push({node:u,offset:u.data.length}),o=r.shift();if(void 0!==o)throw new RangeError("Offset exceeds text length");return a}class u{constructor(t,e){if(e<0)throw new Error("Offset is invalid");this.element=t,this.offset=e}relativeTo(t){if(!t.contains(this.element))throw new Error("Parent is not an ancestor of current element");let e=this.element,r=this.offset;for(;e!==t;)r+=i(e),e=e.parentElement;return new u(e,r)}resolve(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};try{return a(this.element,this.offset)[0]}catch(e){if(0===this.offset&&void 0!==t.direction){const r=document.createTreeWalker(this.element.getRootNode(),NodeFilter.SHOW_TEXT);r.currentNode=this.element;const n=1===t.direction,o=n?r.nextNode():r.previousNode();if(!o)throw e;return{node:o,offset:n?0:o.data.length}}throw e}}static fromCharOffset(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")}}static fromPoint(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");const r=i(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");let r=0;for(let n=0;n2&&void 0!==arguments[2]?arguments[2]:{};this.root=t,this.exact=e,this.context=r}static fromRange(t,e){const r=t.textContent,n=c.fromRange(e).relativeTo(t),o=n.start.offset,i=n.end.offset;return new l(t,r.slice(o,i),{prefix:r.slice(Math.max(0,o-32),o),suffix:r.slice(i,Math.min(r.length,i+32))})}static fromSelector(t,e){const{prefix:r,suffix:n}=e;return new l(t,e.exact,{prefix:r,suffix:n})}toSelector(){return{type:"TextQuoteSelector",exact:this.exact,prefix:this.context.prefix,suffix:this.context.suffix}}toRange(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.toPositionAnchor(t).toRange()}toPositionAnchor(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const r=function(t,r){let o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(0===r.length)return null;const i=Math.min(256,r.length/2),a=e(t,r,i);if(0===a.length)return null;const u=e=>{const i=1-e.errors/r.length,a=o.prefix?n(t.slice(Math.max(0,e.start-o.prefix.length),e.start),o.prefix):1,u=o.suffix?n(t.slice(e.end,e.end+o.suffix.length),o.suffix):1;let c=1;return"number"==typeof o.hint&&(c=1-Math.abs(e.start-o.hint)/t.length),(50*i+20*a+20*u+2*c)/92},c=a.map((t=>({start:t.start,end:t.end,score:u(t)})));return c.sort(((t,e)=>e.score-t.score)),c[0]}(this.root.textContent,this.exact,{...this.context,hint:t.hint});if(!r)throw new Error("Quote not found");return new s(this.root,r.start,r.end)}}var f=r(3732);r.n(f)().shim();const p=!0;function y(){if(!readium.link)return null;const t=readium.link.href;if(!t)return null;const e=function(){const t=window.getSelection();if(!t)return;if(t.isCollapsed)return;const e=t.toString();if(0===e.trim().replace(/\n/g," ").replace(/\s\s+/g," ").length)return;if(!t.anchorNode||!t.focusNode)return;const r=1===t.rangeCount?t.getRangeAt(0):function(t,e,r,n){const o=new Range;if(o.setStart(t,e),o.setEnd(r,n),!o.collapsed)return o;d(">>> createOrderedRange COLLAPSED ... RANGE REVERSE?");const i=new Range;if(i.setStart(r,n),i.setEnd(t,e),!i.collapsed)return d(">>> createOrderedRange RANGE REVERSE OK."),o;d(">>> createOrderedRange RANGE REVERSE ALSO COLLAPSED?!")}(t.anchorNode,t.anchorOffset,t.focusNode,t.focusOffset);if(!r||r.collapsed)return void d("$$$$$$$$$$$$$$$$$ CANNOT GET NON-COLLAPSED SELECTION RANGE?!");const n=document.body.textContent,o=c.fromRange(r).relativeTo(document.body),i=o.start.offset,a=o.end.offset;let u=n.slice(Math.max(0,i-200),i),s=u.search(/\P{L}\p{L}/gu);-1!==s&&(u=u.slice(s+1));let l=n.slice(a,Math.min(n.length,a+200)),f=Array.from(l.matchAll(/\p{L}\P{L}/gu)).pop();return void 0!==f&&f.index>1&&(l=l.slice(0,f.index+1)),{highlight:e,before:u,after:l}}();return e?{href:t,text:e,rect:function(){try{let t=window.getSelection();if(!t)return;return $(t.getRangeAt(0).getBoundingClientRect())}catch(t){return N(t),null}}()}:null}function d(){p&&C.apply(null,arguments)}var h;window.addEventListener("error",(function(t){webkit.messageHandlers.logError.postMessage({message:t.message,filename:t.filename,line:t.lineno})}),!1),window.addEventListener("load",(function(){var t;new ResizeObserver((()=>{t&&window.cancelAnimationFrame(t),t=window.requestAnimationFrame((function(){v=window.innerWidth,function(){const t="readium-virtual-page";var e=document.getElementById(t);if(x()||2!=parseInt(window.getComputedStyle(document.documentElement).getPropertyValue("column-count"))){var r;null===(r=e)||void 0===r||r.remove()}else{var n=document.scrollingElement.scrollWidth/window.innerWidth;Math.round(2*n)/2%1>.1&&(e?e.remove():((e=document.createElement("div")).setAttribute("id",t),e.style.breakBefore="column",e.innerHTML="​",document.body.appendChild(e)))}}(),function(){if(!x()){var t=j(window.scrollX+1);document.scrollingElement.scrollLeft=t}}(),w()}))})).observe(document.body)}),!1);var g,m,b=!1,v=0;function w(){if(readium.isFixedLayout)return;let t=document.scrollingElement;if(x()&&!E()){const e=window.scrollY,r=window.innerHeight,n=t.scrollHeight;h={first:e/n,last:(e+r)/n}}else{let e=window.scrollX;const r=window.innerWidth,n=t.scrollWidth;S()&&(e=Math.abs(e)),h={first:e/n,last:(e+r)/n}}0!==t.scrollWidth&&0!==t.scrollHeight&&(b||window.requestAnimationFrame((function(){var t;t=h,webkit.messageHandlers.progressionChanged.postMessage(t),b=!1})),b=!0)}function x(){return"readium-scroll-on"==document.documentElement.style.getPropertyValue("--USER__view").trim()}function E(){return window.getComputedStyle(document.documentElement).getPropertyValue("writing-mode").startsWith("vertical")}function S(){const t=window.getComputedStyle(document.documentElement);return"rtl"==t.getPropertyValue("direction")||"vertical-rl"==t.getPropertyValue("writing-mode")}function A(t){return x()?document.scrollingElement.scrollTop=t.top+window.scrollY:document.scrollingElement.scrollLeft=j(t.left+window.scrollX),!0}function O(t){var e=window.scrollX,r=window.innerWidth;return document.scrollingElement.scrollLeft=t,Math.abs(e-t)/r>.01}function j(t){const e=t+(S()?-1:1);return e-e%v}function T(t){try{let n=t.locations,o=t.text;var e;if(o&&o.highlight)return n&&n.cssSelector&&(e=document.querySelector(n.cssSelector)),e||(e=document.body),new l(e,o.highlight,{prefix:o.before,suffix:o.after}).toRange();if(n){var r=null;if(!r&&n.cssSelector&&(r=document.querySelector(n.cssSelector)),!r&&n.fragments)for(const t of n.fragments)if(r=document.getElementById(t))break;if(r){let t=document.createRange();return t.setStartBefore(r),t.setEndAfter(r),t}}}catch(t){N(t)}return null}function P(t,e){null===e?R(t):document.documentElement.style.setProperty(t,e,"important")}function R(t){document.documentElement.style.removeProperty(t)}function C(){var t=Array.prototype.slice.call(arguments).join(" ");webkit.messageHandlers.log.postMessage(t)}function I(t){N(new Error(t))}function N(t){webkit.messageHandlers.logError.postMessage({message:t.message})}window.addEventListener("scroll",w),document.addEventListener("selectionchange",(50,g=function(){webkit.messageHandlers.selectionChanged.postMessage(y())},function(){var t=this,e=arguments;clearTimeout(m),m=setTimeout((function(){g.apply(t,e),m=null}),50)}));const M=!1;function $(t){let e=k({x:t.left,y:t.top});const r=t.width,n=t.height,o=e.x,i=e.y;return{width:r,height:n,left:o,top:i,right:o+r,bottom:i+n}}function k(t){if(!frameElement)return t;let e=frameElement.getBoundingClientRect();if(!e)return t;let r=window.top.document.documentElement;return{x:t.x+e.x+r.scrollLeft,y:t.y+e.y+r.scrollTop}}function D(t,e){let r=t.getClientRects();const n=[];for(const t of r)n.push({bottom:t.bottom,height:t.height,left:t.left,right:t.right,top:t.top,width:t.width});const o=W(function(t,e){const r=new Set(t);for(const e of t)if(e.width>1&&e.height>1){for(const n of t)if(e!==n&&r.has(n)&&B(n,e,1)){G("CLIENT RECT: remove contained"),r.delete(e);break}}else G("CLIENT RECT: remove tiny"),r.delete(e);return Array.from(r)}(L(n,1,e)));for(let t=o.length-1;t>=0;t--){const e=o[t];if(!(e.width*e.height>4)){if(!(o.length>1)){G("CLIENT RECT: remove small, but keep otherwise empty!");break}G("CLIENT RECT: remove small"),o.splice(t,1)}}return G(`CLIENT RECT: reduced ${n.length} --\x3e ${o.length}`),o}function L(t,e,r){for(let n=0;nt!==i&&t!==a)),o=F(i,a);return n.push(o),L(n,e,r)}}return t}function F(t,e){const r=Math.min(t.left,e.left),n=Math.max(t.right,e.right),o=Math.min(t.top,e.top),i=Math.max(t.bottom,e.bottom);return{bottom:i,height:i-o,left:r,right:n,top:o,width:n-r}}function B(t,e,r){return _(t,e.left,e.top,r)&&_(t,e.right,e.top,r)&&_(t,e.left,e.bottom,r)&&_(t,e.right,e.bottom,r)}function _(t,e,r,n){return(t.lefte||H(t.right,e,n))&&(t.topr||H(t.bottom,r,n))}function W(t){for(let e=0;et!==e));return Array.prototype.push.apply(a,r),W(a)}}else G("replaceOverlapingRects rect1 === rect2 ??!")}return t}function U(t,e){const r=function(t,e){const r=Math.max(t.left,e.left),n=Math.min(t.right,e.right),o=Math.max(t.top,e.top),i=Math.min(t.bottom,e.bottom);return{bottom:i,height:Math.max(0,i-o),left:r,right:n,top:o,width:Math.max(0,n-r)}}(e,t);if(0===r.height||0===r.width)return[t];const n=[];{const e={bottom:t.bottom,height:0,left:t.left,right:r.left,top:t.top,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}{const e={bottom:r.top,height:0,left:r.left,right:r.right,top:t.top,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}{const e={bottom:t.bottom,height:0,left:r.left,right:r.right,top:r.bottom,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}{const e={bottom:t.bottom,height:0,left:r.right,right:t.right,top:t.top,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}return n}function z(t,e,r){return(t.left=0&&H(t.left,e.right,r))&&(e.left=0&&H(e.left,t.right,r))&&(t.top=0&&H(t.top,e.bottom,r))&&(e.top=0&&H(e.top,t.bottom,r))}function H(t,e,r){return Math.abs(t-e)<=r}function G(){M&&C.apply(null,arguments)}var V,q=[],X="ResizeObserver loop completed with undelivered notifications.";!function(t){t.BORDER_BOX="border-box",t.CONTENT_BOX="content-box",t.DEVICE_PIXEL_CONTENT_BOX="device-pixel-content-box"}(V||(V={}));var K,Y=function(t){return Object.freeze(t)},J=function(t,e){this.inlineSize=t,this.blockSize=e,Y(this)},Q=function(){function t(t,e,r,n){return this.x=t,this.y=e,this.width=r,this.height=n,this.top=this.y,this.left=this.x,this.bottom=this.top+this.height,this.right=this.left+this.width,Y(this)}return t.prototype.toJSON=function(){var t=this;return{x:t.x,y:t.y,top:t.top,right:t.right,bottom:t.bottom,left:t.left,width:t.width,height:t.height}},t.fromRect=function(e){return new t(e.x,e.y,e.width,e.height)},t}(),Z=function(t){return t instanceof SVGElement&&"getBBox"in t},tt=function(t){if(Z(t)){var e=t.getBBox(),r=e.width,n=e.height;return!r&&!n}var o=t,i=o.offsetWidth,a=o.offsetHeight;return!(i||a||t.getClientRects().length)},et=function(t){var e;if(t instanceof Element)return!0;var r=null===(e=null==t?void 0:t.ownerDocument)||void 0===e?void 0:e.defaultView;return!!(r&&t instanceof r.Element)},rt="undefined"!=typeof window?window:{},nt=new WeakMap,ot=/auto|scroll/,it=/^tb|vertical/,at=/msie|trident/i.test(rt.navigator&&rt.navigator.userAgent),ut=function(t){return parseFloat(t||"0")},ct=function(t,e,r){return void 0===t&&(t=0),void 0===e&&(e=0),void 0===r&&(r=!1),new J((r?e:t)||0,(r?t:e)||0)},st=Y({devicePixelContentBoxSize:ct(),borderBoxSize:ct(),contentBoxSize:ct(),contentRect:new Q(0,0,0,0)}),lt=function(t,e){if(void 0===e&&(e=!1),nt.has(t)&&!e)return nt.get(t);if(tt(t))return nt.set(t,st),st;var r=getComputedStyle(t),n=Z(t)&&t.ownerSVGElement&&t.getBBox(),o=!at&&"border-box"===r.boxSizing,i=it.test(r.writingMode||""),a=!n&&ot.test(r.overflowY||""),u=!n&&ot.test(r.overflowX||""),c=n?0:ut(r.paddingTop),s=n?0:ut(r.paddingRight),l=n?0:ut(r.paddingBottom),f=n?0:ut(r.paddingLeft),p=n?0:ut(r.borderTopWidth),y=n?0:ut(r.borderRightWidth),d=n?0:ut(r.borderBottomWidth),h=f+s,g=c+l,m=(n?0:ut(r.borderLeftWidth))+y,b=p+d,v=u?t.offsetHeight-b-t.clientHeight:0,w=a?t.offsetWidth-m-t.clientWidth:0,x=o?h+m:0,E=o?g+b:0,S=n?n.width:ut(r.width)-x-w,A=n?n.height:ut(r.height)-E-v,O=S+h+w+m,j=A+g+v+b,T=Y({devicePixelContentBoxSize:ct(Math.round(S*devicePixelRatio),Math.round(A*devicePixelRatio),i),borderBoxSize:ct(O,j,i),contentBoxSize:ct(S,A,i),contentRect:new Q(f,c,S,A)});return nt.set(t,T),T},ft=function(t,e,r){var n=lt(t,r),o=n.borderBoxSize,i=n.contentBoxSize,a=n.devicePixelContentBoxSize;switch(e){case V.DEVICE_PIXEL_CONTENT_BOX:return a;case V.BORDER_BOX:return o;default:return i}},pt=function(t){var e=lt(t);this.target=t,this.contentRect=e.contentRect,this.borderBoxSize=Y([e.borderBoxSize]),this.contentBoxSize=Y([e.contentBoxSize]),this.devicePixelContentBoxSize=Y([e.devicePixelContentBoxSize])},yt=function(t){if(tt(t))return 1/0;for(var e=0,r=t.parentNode;r;)e+=1,r=r.parentNode;return e},dt=function(){var t=1/0,e=[];q.forEach((function(r){if(0!==r.activeTargets.length){var n=[];r.activeTargets.forEach((function(e){var r=new pt(e.target),o=yt(e.target);n.push(r),e.lastReportedSize=ft(e.target,e.observedBox),ot?e.activeTargets.push(r):e.skippedTargets.push(r))}))}))},gt=[],mt=0,bt={attributes:!0,characterData:!0,childList:!0,subtree:!0},vt=["resize","load","transitionend","animationend","animationstart","animationiteration","keyup","keydown","mouseup","mousedown","mouseover","mouseout","blur","focus"],wt=function(t){return void 0===t&&(t=0),Date.now()+t},xt=!1,Et=function(){function t(){var t=this;this.stopped=!0,this.listener=function(){return t.schedule()}}return t.prototype.run=function(t){var e=this;if(void 0===t&&(t=250),!xt){xt=!0;var r,n=wt(t);r=function(){var r=!1;try{r=function(){var t,e=0;for(ht(e);q.some((function(t){return t.activeTargets.length>0}));)e=dt(),ht(e);return q.some((function(t){return t.skippedTargets.length>0}))&&("function"==typeof ErrorEvent?t=new ErrorEvent("error",{message:X}):((t=document.createEvent("Event")).initEvent("error",!1,!1),t.message=X),window.dispatchEvent(t)),e>0}()}finally{if(xt=!1,t=n-wt(),!mt)return;r?e.run(1e3):t>0?e.run(t):e.start()}},function(t){if(!K){var e=0,r=document.createTextNode("");new MutationObserver((function(){return gt.splice(0).forEach((function(t){return t()}))})).observe(r,{characterData:!0}),K=function(){r.textContent="".concat(e?e--:e++)}}gt.push(t),K()}((function(){requestAnimationFrame(r)}))}},t.prototype.schedule=function(){this.stop(),this.run()},t.prototype.observe=function(){var t=this,e=function(){return t.observer&&t.observer.observe(document.body,bt)};document.body?e():rt.addEventListener("DOMContentLoaded",e)},t.prototype.start=function(){var t=this;this.stopped&&(this.stopped=!1,this.observer=new MutationObserver(this.listener),this.observe(),vt.forEach((function(e){return rt.addEventListener(e,t.listener,!0)})))},t.prototype.stop=function(){var t=this;this.stopped||(this.observer&&this.observer.disconnect(),vt.forEach((function(e){return rt.removeEventListener(e,t.listener,!0)})),this.stopped=!0)},t}(),St=new Et,At=function(t){!mt&&t>0&&St.start(),!(mt+=t)&&St.stop()},Ot=function(){function t(t,e){this.target=t,this.observedBox=e||V.CONTENT_BOX,this.lastReportedSize={inlineSize:0,blockSize:0}}return t.prototype.isActive=function(){var t,e=ft(this.target,this.observedBox,!0);return t=this.target,Z(t)||function(t){switch(t.tagName){case"INPUT":if("image"!==t.type)break;case"VIDEO":case"AUDIO":case"EMBED":case"OBJECT":case"CANVAS":case"IFRAME":case"IMG":return!0}return!1}(t)||"inline"!==getComputedStyle(t).display||(this.lastReportedSize=e),this.lastReportedSize.inlineSize!==e.inlineSize||this.lastReportedSize.blockSize!==e.blockSize},t}(),jt=function(t,e){this.activeTargets=[],this.skippedTargets=[],this.observationTargets=[],this.observer=t,this.callback=e},Tt=new WeakMap,Pt=function(t,e){for(var r=0;r=0&&(o&&q.splice(q.indexOf(r),1),r.observationTargets.splice(n,1),At(-1))},t.disconnect=function(t){var e=this,r=Tt.get(t);r.observationTargets.slice().forEach((function(r){return e.unobserve(t,r.target)})),r.activeTargets.splice(0,r.activeTargets.length)},t}(),Ct=function(){function t(t){if(0===arguments.length)throw new TypeError("Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.");if("function"!=typeof t)throw new TypeError("Failed to construct 'ResizeObserver': The callback provided as parameter 1 is not a function.");Rt.connect(this,t)}return t.prototype.observe=function(t,e){if(0===arguments.length)throw new TypeError("Failed to execute 'observe' on 'ResizeObserver': 1 argument required, but only 0 present.");if(!et(t))throw new TypeError("Failed to execute 'observe' on 'ResizeObserver': parameter 1 is not of type 'Element");Rt.observe(this,t,e)},t.prototype.unobserve=function(t){if(0===arguments.length)throw new TypeError("Failed to execute 'unobserve' on 'ResizeObserver': 1 argument required, but only 0 present.");if(!et(t))throw new TypeError("Failed to execute 'unobserve' on 'ResizeObserver': parameter 1 is not of type 'Element");Rt.unobserve(this,t)},t.prototype.disconnect=function(){Rt.disconnect(this)},t.toString=function(){return"function ResizeObserver () { [polyfill code] }"},t}();const It=window.ResizeObserver||Ct;let Nt=new Map,Mt=new Map;var $t=0;function kt(t){if(0===Mt.size)return null;for(const[e,r]of Mt)if(r.isActivable())for(const n of r.items.reverse())if(n.clickableElements)for(const r of n.clickableElements){let o=r.getBoundingClientRect().toJSON();if(_(o,t.clientX,t.clientY,1))return{group:e,item:n,element:r,rect:o}}return null}function Dt(t){return t&&t instanceof Element}window.addEventListener("load",(function(){const t=document.body;var e={width:0,height:0};new It((()=>{e.width===t.clientWidth&&e.height===t.clientHeight||(e={width:t.clientWidth,height:t.clientHeight},Mt.forEach((function(t){t.requestLayout()})))})).observe(t)}),!1);const Lt={NONE:"",DESCENDANT:" ",CHILD:" > "},Ft={id:"id",class:"class",tag:"tag",attribute:"attribute",nthchild:"nthchild",nthoftype:"nthoftype"},Bt="CssSelectorGenerator";function _t(t="unknown problem",...e){console.warn(`${Bt}: ${t}`,...e)}const Wt={selectors:[Ft.id,Ft.class,Ft.tag,Ft.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function Ut(t){return t instanceof RegExp}function zt(t){return["string","function"].includes(typeof t)||Ut(t)}function Ht(t){return Array.isArray(t)?t.filter(zt):[]}function Gt(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 Vt(t,e){if(Gt(t))return t.contains(e)||_t("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 r=e.getRootNode({composed:!1});return Gt(r)?(r!==document&&_t("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."),r):e.ownerDocument.querySelector(":root")}function qt(t){return"number"==typeof t?t:Number.POSITIVE_INFINITY}function Xt(t=[]){const[e=[],...r]=t;return 0===r.length?e:r.reduce(((t,e)=>t.filter((t=>e.includes(t)))),e)}function Kt(t){return[].concat(...t)}function Yt(t){const e=t.map((t=>{if(Ut(t))return e=>t.test(e);if("function"==typeof t)return e=>{const r=t(e);return"boolean"!=typeof r?(_t("pattern matcher function invalid","Provided pattern matching function does not return boolean. It's result will be ignored.",t),!1):r};if("string"==typeof t){const e=new RegExp("^"+t.replace(/[|\\{}()[\]^$+?.]/g,"\\$&").replace(/\*/g,".+")+"$");return t=>e.test(t)}return _t("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 Jt(t,e,r){const n=Array.from(Vt(r,t[0]).querySelectorAll(e));return n.length===t.length&&t.every((t=>n.includes(t)))}function Qt(t,e){e=null!=e?e:function(t){return t.ownerDocument.querySelector(":root")}(t);const r=[];let n=t;for(;Dt(n)&&n!==e;)r.push(n),n=n.parentElement;return r}function Zt(t,e){return Xt(t.map((t=>Qt(t,e))))}const te=new RegExp(["^$","\\s"].join("|")),ee=new RegExp(["^$"].join("|")),re=[Ft.nthoftype,Ft.tag,Ft.id,Ft.class,Ft.attribute,Ft.nthchild],ne=Yt(["class","id","ng-*"]);function oe({name:t}){return`[${t}]`}function ie({name:t,value:e}){return`[${t}='${e}']`}function ae({nodeName:t,nodeValue:e}){return{name:(r=t,r.replace(/:/g,"\\:")),value:ve(e)};var r}function ue(t){const e=Array.from(t.attributes).filter((e=>function({nodeName:t},e){const r=e.tagName.toLowerCase();return!(["input","option"].includes(r)&&"value"===t||ne(t))}(e,t))).map(ae);return[...e.map(oe),...e.map(ie)]}function ce(t){return(t.getAttribute("class")||"").trim().split(/\s+/).filter((t=>!ee.test(t))).map((t=>`.${ve(t)}`))}function se(t){const e=t.getAttribute("id")||"",r=`#${ve(e)}`,n=t.getRootNode({composed:!1});return!te.test(e)&&Jt([t],r,n)?[r]:[]}function le(t){const e=t.parentNode;if(e){const r=Array.from(e.childNodes).filter(Dt).indexOf(t);if(r>-1)return[`:nth-child(${r+1})`]}return[]}function fe(t){return[ve(t.tagName.toLowerCase())]}function pe(t){const e=[...new Set(Kt(t.map(fe)))];return 0===e.length||e.length>1?[]:[e[0]]}function ye(t){const e=pe([t])[0],r=t.parentElement;if(r){const n=Array.from(r.children).filter((t=>t.tagName.toLowerCase()===e)),o=n.indexOf(t);if(o>-1)return[`${e}:nth-of-type(${o+1})`]}return[]}function de(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){return Array.from(function*(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){let r=0,n=ge(1);for(;n.length<=t.length&&rt[e]));yield e,n=he(n,t.length-1)}}(t,{maxResults:e}))}function he(t=[],e=0){const r=t.length;if(0===r)return[];const n=[...t];n[r-1]+=1;for(let t=r-1;t>=0;t--)if(n[t]>e){if(0===t)return ge(r+1);n[t-1]++,n[t]=n[t-1]+1}return n[r-1]>e?ge(r+1):n}function ge(t=1){return Array.from(Array(t).keys())}const me=":".charCodeAt(0).toString(16).toUpperCase(),be=/[ !"#$%&'()\[\]{|}<>*+,./;=?@^`~\\]/;function ve(t=""){var e,r;return null!==(r=null===(e=null===CSS||void 0===CSS?void 0:CSS.escape)||void 0===e?void 0:e.call(CSS,t))&&void 0!==r?r:function(t=""){return t.split("").map((t=>":"===t?`\\${me} `:be.test(t)?`\\${t}`:escape(t).replace(/%/g,"\\"))).join("")}(t)}const we={tag:pe,id:function(t){return 0===t.length||t.length>1?[]:se(t[0])},class:function(t){return Xt(t.map(ce))},attribute:function(t){return Xt(t.map(ue))},nthchild:function(t){return Xt(t.map(le))},nthoftype:function(t){return Xt(t.map(ye))}},xe={tag:fe,id:se,class:ce,attribute:ue,nthchild:le,nthoftype:ye};function Ee(t){return t.includes(Ft.tag)||t.includes(Ft.nthoftype)?[...t]:[...t,Ft.tag]}function Se(t={}){const e=[...re];return t[Ft.tag]&&t[Ft.nthoftype]&&e.splice(e.indexOf(Ft.tag),1),e.map((e=>{return(n=t)[r=e]?n[r].join(""):"";var r,n})).join("")}function Ae(t,e,r="",n){const o=function(t,e){return""===e?t:function(t,e){return[...t.map((t=>e+Lt.DESCENDANT+t)),...t.map((t=>e+Lt.CHILD+t))]}(t,e)}(function(t,e,r){const n=function(t,e){const{blacklist:r,whitelist:n,combineWithinSelector:o,maxCombinations:i}=e,a=Yt(r),u=Yt(n);return function(t){const{selectors:e,includeTag:r}=t,n=[].concat(e);return r&&!n.includes("tag")&&n.push("tag"),n}(e).reduce(((e,r)=>{const n=function(t,e){var r;return(null!==(r=we[e])&&void 0!==r?r:()=>[])(t)}(t,r),c=function(t=[],e,r){return t.filter((t=>r(t)||!e(t)))}(n,a,u),s=function(t=[],e){return t.sort(((t,r)=>{const n=e(t),o=e(r);return n&&!o?-1:!n&&o?1:0}))}(c,u);return e[r]=o?de(s,{maxResults:i}):s.map((t=>[t])),e}),{})}(t,r),o=function(t,e){return function(t){const{selectors:e,combineBetweenSelectors:r,includeTag:n,maxCandidates:o}=t,i=r?de(e,{maxResults:o}):e.map((t=>[t]));return n?i.map(Ee):i}(e).map((e=>function(t,e){const r={};return t.forEach((t=>{const n=e[t];n.length>0&&(r[t]=n)})),function(t={}){let e=[];return Object.entries(t).forEach((([t,r])=>{e=r.flatMap((r=>0===e.length?[{[t]:r}]:e.map((e=>Object.assign(Object.assign({},e),{[t]:r})))))})),e}(r).map(Se)}(e,t))).filter((t=>t.length>0))}(n,r),i=Kt(o);return[...new Set(i)]}(t,n.root,n),r);for(const e of o)if(Jt(t,e,n.root))return e;return null}function Oe(t){return{value:t,include:!1}}function je({selectors:t,operator:e}){let r=[...re];t[Ft.tag]&&t[Ft.nthoftype]&&(r=r.filter((t=>t!==Ft.tag)));let n="";return r.forEach((e=>{(t[e]||[]).forEach((({value:t,include:e})=>{e&&(n+=t)}))})),e+n}function Te(t){return[":root",...Qt(t).reverse().map((t=>{const e=function(t,e,r=Lt.NONE){const n={};return e.forEach((e=>{Reflect.set(n,e,function(t,e){return xe[e](t)}(t,e).map(Oe))})),{element:t,operator:r,selectors:n}}(t,[Ft.nthchild],Lt.CHILD);return e.selectors.nthchild.forEach((t=>{t.include=!0})),e})).map(je)].join("")}function Pe(t,e={}){const r=function(t){(t instanceof NodeList||t instanceof HTMLCollection)&&(t=Array.from(t));const e=(Array.isArray(t)?t:[t]).filter(Dt);return[...new Set(e)]}(t),n=function(t,e={}){const r=Object.assign(Object.assign({},Wt),e);return{selectors:(n=r.selectors,Array.isArray(n)?n.filter((t=>{return e=Ft,r=t,Object.values(e).includes(r);var e,r})):[]),whitelist:Ht(r.whitelist),blacklist:Ht(r.blacklist),root:Vt(r.root,t),combineWithinSelector:!!r.combineWithinSelector,combineBetweenSelectors:!!r.combineBetweenSelectors,includeTag:!!r.includeTag,maxCombinations:qt(r.maxCombinations),maxCandidates:qt(r.maxCandidates)};var n}(r[0],e);let o="",i=n.root;function a(){return function(t,e,r="",n){if(0===t.length)return null;const o=[t.length>1?t:[],...Zt(t,e).map((t=>[t]))];for(const t of o){const e=Ae(t,0,r,n);if(e)return{foundElements:t,selector:e}}return null}(r,i,o,n)}let u=a();for(;u;){const{foundElements:t,selector:e}=u;if(Jt(r,e,n.root))return e;i=t[0],o=e,u=a()}return r.length>1?r.map((t=>Pe(t,n))).join(", "):function(t){return t.map(Te).join(", ")}(r)}function Re(t){return null==t?null:-1!==["a","audio","button","canvas","details","input","label","option","select","submit","textarea","video"].indexOf(t.nodeName.toLowerCase())||t.hasAttribute("contenteditable")&&"false"!=t.getAttribute("contenteditable").toLowerCase()?t.outerHTML:t.parentElement?Re(t.parentElement):null}function Ce(t){for(var e=0;e0&&e.top0&&e.left{_e(t)||(We(t),Ue("down",t))})),window.addEventListener("keyup",(t=>{_e(t)||(We(t),Ue("up",t))})),r.g.readium={scrollToId:function(t){let e=document.getElementById(t);return!!e&&(A(e.getBoundingClientRect()),!0)},scrollToPosition:function(t,e){if(t<0||t>1)console.error(`Expected a valid progression in scrollToPosition, got ${t}`);else if(x())if(E()){let e=document.scrollingElement.scrollWidth*t;document.scrollingElement.scrollLeft=-e}else{let e=document.scrollingElement.scrollHeight*t;document.scrollingElement.scrollTop=e}else{let r=document.scrollingElement.scrollWidth*t*("rtl"==e?-1:1);document.scrollingElement.scrollLeft=j(r)}},scrollToLocator:function(t){let e=T(t);return!!e&&function(t){return A(t.getBoundingClientRect())}(e)},scrollLeft:function(t){var e="rtl"==t,r=document.scrollingElement.scrollWidth,n=window.innerWidth,o=window.scrollX-n,i=e?-(r-n):0;return O(Math.max(o,i))},scrollRight:function(t){var e="rtl"==t,r=document.scrollingElement.scrollWidth,n=window.innerWidth,o=window.scrollX+n,i=e?0:r-n;return O(Math.min(o,i))},setCSSProperties:function(t){for(const e in t)P(e,t[e])},setProperty:P,removeProperty:R,registerDecorationTemplates:function(t){var e="";for(const[r,n]of Object.entries(t))Nt.set(r,n),n.stylesheet&&(e+=n.stylesheet+"\n");if(e){let t=document.createElement("style");t.innerHTML=e,document.getElementsByTagName("head")[0].appendChild(t)}},getDecorations:function(t){var e=Mt.get(t);return e||(e=function(t,e){var r=[],n=0,o=null,i=!1;function a(e){let o=t+"-"+n++,i=T(e.locator);if(!i)return void C("Can't locate DOM range for decoration",e);let a={id:o,decoration:e,range:i};r.push(a),c(a)}function u(t){let e=r.findIndex((e=>e.decoration.id===t));if(-1===e)return;let n=r[e];r.splice(e,1),n.clickableElements=null,n.container&&(n.container.remove(),n.container=null)}function c(r){let n=(o||((o=document.createElement("div")).id=t,o.dataset.group=e,o.style.pointerEvents="none",requestAnimationFrame((function(){null!=o&&document.body.append(o)}))),o),i=Nt.get(r.decoration.style);if(!i)return void I(`Unknown decoration style: ${r.decoration.style}`);let a=document.createElement("div");a.id=r.id,a.dataset.style=r.decoration.style,a.style.pointerEvents="none";const u=getComputedStyle(document.body).writingMode,c="vertical-rl"===u||"vertical-lr"===u,s=document.scrollingElement,{scrollLeft:l,scrollTop:f}=s,p=c?window.innerHeight:window.innerWidth,y=c?window.innerWidth:window.innerHeight,d=parseInt(getComputedStyle(document.documentElement).getPropertyValue("column-count"))||1,h=(c?y:p)/d;function g(t,e,r,n){t.style.position="absolute";const o="vertical-rl"===n;if(o||"vertical-lr"===n){if("wrap"===i.width)t.style.width=`${e.width}px`,t.style.height=`${e.height}px`,o?t.style.right=`${-e.right-l+s.clientWidth}px`:t.style.left=`${e.left+l}px`,t.style.top=`${e.top+f}px`;else if("viewport"===i.width){t.style.width=`${e.height}px`,t.style.height=`${p}px`;const r=Math.floor(e.top/p)*p;o?t.style.right=-e.right-l+"px":t.style.left=`${e.left+l}px`,t.style.top=`${r+f}px`}else if("bounds"===i.width)t.style.width=`${r.height}px`,t.style.height=`${p}px`,o?t.style.right=`${-r.right-l+s.clientWidth}px`:t.style.left=`${r.left+l}px`,t.style.top=`${r.top+f}px`;else if("page"===i.width){t.style.width=`${e.height}px`,t.style.height=`${h}px`;const r=Math.floor(e.top/h)*h;o?t.style.right=`${-e.right-l+s.clientWidth}px`:t.style.left=`${e.left+l}px`,t.style.top=`${r+f}px`}}else if("wrap"===i.width)t.style.width=`${e.width}px`,t.style.height=`${e.height}px`,t.style.left=`${e.left+l}px`,t.style.top=`${e.top+f}px`;else if("viewport"===i.width){t.style.width=`${p}px`,t.style.height=`${e.height}px`;const r=Math.floor(e.left/p)*p;t.style.left=`${r+l}px`,t.style.top=`${e.top+f}px`}else if("bounds"===i.width)t.style.width=`${r.width}px`,t.style.height=`${e.height}px`,t.style.left=`${r.left+l}px`,t.style.top=`${e.top+f}px`;else if("page"===i.width){t.style.width=`${h}px`,t.style.height=`${e.height}px`;const r=Math.floor(e.left/h)*h;t.style.left=`${r+l}px`,t.style.top=`${e.top+f}px`}}let m,b=r.range.getBoundingClientRect();try{let t=document.createElement("template");t.innerHTML=r.decoration.element.trim(),m=t.content.firstElementChild}catch(t){return void I(`Invalid decoration element "${r.decoration.element}": ${t.message}`)}if("boxes"===i.layout){const t=!u.startsWith("vertical"),e=(v=r.range.startContainer).nodeType===Node.ELEMENT_NODE?v:v.parentElement,n=getComputedStyle(e).writingMode,o=D(r.range,t).sort(((t,e)=>t.top!==e.top?t.top-e.top:"vertical-rl"===n?e.left-t.left:t.left-e.left));for(let t of o){const e=m.cloneNode(!0);e.style.pointerEvents="none",e.dataset.writingMode=n,g(e,t,b,u),a.append(e)}}else if("bounds"===i.layout){const t=m.cloneNode(!0);t.style.pointerEvents="none",t.dataset.writingMode=u,g(t,b,b,u),a.append(t)}var v;n.append(a),r.container=a,r.clickableElements=Array.from(a.querySelectorAll("[data-activable='1']")),0===r.clickableElements.length&&(r.clickableElements=Array.from(a.children))}function s(){o&&(o.remove(),o=null)}return{add:a,remove:u,update:function(t){u(t.id),a(t)},clear:function(){s(),r.length=0},items:r,requestLayout:function(){s(),r.forEach((t=>c(t)))},isActivable:function(){return i},setActivable:function(){i=!0}}}("r2-decoration-"+$t++,t),Mt.set(t,e)),e},findFirstVisibleLocator:function(){const t=Ce(document.body);return{href:"#",type:"application/xhtml+xml",locations:{cssSelector:Pe(t)},text:{highlight:t.textContent}}}},window.readium.isReflowable=!0,webkit.messageHandlers.spreadLoadStarted.postMessage({}),window.addEventListener("load",(function(){window.requestAnimationFrame((function(){webkit.messageHandlers.spreadLoaded.postMessage({})}));let t=document.createElement("meta");t.setAttribute("name","viewport"),t.setAttribute("content","width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no"),document.head.appendChild(t)}))})()})(); +(()=>{var t={9116:(t,e)=>{"use strict";function r(t){return t.split("").reverse().join("")}function n(t){return(t|-t)>>31&1}function o(t,e,r,o){var i=t.P[r],a=t.M[r],u=o>>>31,c=e[r]|u,s=c|a,l=(c&i)+i^i|c,f=a|~(l|i),p=i&l,y=n(f&t.lastRowMask[r])-n(p&t.lastRowMask[r]);return f<<=1,p<<=1,i=(p|=u)|~(s|(f|=n(o)-u)),a=f&s,t.P[r]=i,t.M[r]=a,y}function i(t,e,r){if(0===e.length)return[];r=Math.min(r,e.length);var n=[],i=32,a=Math.ceil(e.length/i)-1,u={P:new Uint32Array(a+1),M:new Uint32Array(a+1),lastRowMask:new Uint32Array(a+1)};u.lastRowMask.fill(1<<31),u.lastRowMask[a]=1<<(e.length-1)%i;for(var c=new Uint32Array(a+1),s=new Map,l=[],f=0;f<256;f++)l.push(c);for(var p=0;p=e.length||e.charCodeAt(m)===y&&(d[h]|=1<0&&v[b]>=r+i;)b-=1;b===a&&v[b]<=r&&(v[b]{"use strict";var n=r(4624),o=r(5096),i=o(n("String.prototype.indexOf"));t.exports=function(t,e){var r=n(t,!!e);return"function"==typeof r&&i(t,".prototype.")>-1?o(r):r}},5096:(t,e,r)=>{"use strict";var n=r(3520),o=r(4624),i=r(5676),a=r(2824),u=o("%Function.prototype.apply%"),c=o("%Function.prototype.call%"),s=o("%Reflect.apply%",!0)||n.call(c,u),l=o("%Object.defineProperty%",!0),f=o("%Math.max%");if(l)try{l({},"a",{value:1})}catch(t){l=null}t.exports=function(t){if("function"!=typeof t)throw new a("a function is required");var e=s(n,c,arguments);return i(e,1+f(0,t.length-(arguments.length-1)),!0)};var p=function(){return s(n,u,arguments)};l?l(t.exports,"apply",{value:p}):t.exports.apply=p},2448:(t,e,r)=>{"use strict";var n=r(3268)(),o=r(4624),i=n&&o("%Object.defineProperty%",!0);if(i)try{i({},"a",{value:1})}catch(t){i=!1}var a=r(6500),u=r(2824),c=r(6168);t.exports=function(t,e,r){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new u("`obj` must be an object or a function`");if("string"!=typeof e&&"symbol"!=typeof e)throw new u("`property` must be a string or a symbol`");if(arguments.length>3&&"boolean"!=typeof arguments[3]&&null!==arguments[3])throw new u("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&"boolean"!=typeof arguments[4]&&null!==arguments[4])throw new u("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&"boolean"!=typeof arguments[5]&&null!==arguments[5])throw new u("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&"boolean"!=typeof arguments[6])throw new u("`loose`, if provided, must be a boolean");var n=arguments.length>3?arguments[3]:null,o=arguments.length>4?arguments[4]:null,s=arguments.length>5?arguments[5]:null,l=arguments.length>6&&arguments[6],f=!!c&&c(t,e);if(i)i(t,e,{configurable:null===s&&f?f.configurable:!s,enumerable:null===n&&f?f.enumerable:!n,value:r,writable:null===o&&f?f.writable:!o});else{if(!l&&(n||o||s))throw new a("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.");t[e]=r}}},2732:(t,e,r)=>{"use strict";var n=r(2812),o="function"==typeof Symbol&&"symbol"==typeof Symbol("foo"),i=Object.prototype.toString,a=Array.prototype.concat,u=r(2448),c=r(3268)(),s=function(t,e,r,n){if(e in t)if(!0===n){if(t[e]===r)return}else if("function"!=typeof(o=n)||"[object Function]"!==i.call(o)||!n())return;var o;c?u(t,e,r,!0):u(t,e,r)},l=function(t,e){var r=arguments.length>2?arguments[2]:{},i=n(e);o&&(i=a.call(i,Object.getOwnPropertySymbols(e)));for(var u=0;u{"use strict";t.exports=EvalError},1152:t=>{"use strict";t.exports=Error},1932:t=>{"use strict";t.exports=RangeError},5028:t=>{"use strict";t.exports=ReferenceError},6500:t=>{"use strict";t.exports=SyntaxError},2824:t=>{"use strict";t.exports=TypeError},5488:t=>{"use strict";t.exports=URIError},9200:(t,e,r)=>{"use strict";var n=r(4624)("%Object.defineProperty%",!0),o=r(4712)(),i=r(4440),a=o?Symbol.toStringTag:null;t.exports=function(t,e){var r=arguments.length>2&&arguments[2]&&arguments[2].force;!a||!r&&i(t,a)||(n?n(t,a,{configurable:!0,enumerable:!1,value:e,writable:!1}):t[a]=e)}},108:(t,e,r)=>{"use strict";var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator,o=r(5988),i=r(648),a=r(1844),u=r(7256);t.exports=function(t){if(o(t))return t;var e,r="default";if(arguments.length>1&&(arguments[1]===String?r="string":arguments[1]===Number&&(r="number")),n&&(Symbol.toPrimitive?e=function(t,e){var r=t[e];if(null!=r){if(!i(r))throw new TypeError(r+" returned for property "+e+" of object "+t+" is not a function");return r}}(t,Symbol.toPrimitive):u(t)&&(e=Symbol.prototype.valueOf)),void 0!==e){var c=e.call(t,r);if(o(c))return c;throw new TypeError("unable to convert exotic object to primitive")}return"default"===r&&(a(t)||u(t))&&(r="string"),function(t,e){if(null==t)throw new TypeError("Cannot call method on "+t);if("string"!=typeof e||"number"!==e&&"string"!==e)throw new TypeError('hint must be "string" or "number"');var r,n,a,u="string"===e?["toString","valueOf"]:["valueOf","toString"];for(a=0;a{"use strict";t.exports=function(t){return null===t||"function"!=typeof t&&"object"!=typeof t}},1480:t=>{"use strict";var e=Object.prototype.toString,r=Math.max,n=function(t,e){for(var r=[],n=0;n{"use strict";var n=r(1480);t.exports=Function.prototype.bind||n},2656:t=>{"use strict";var e=function(){return"string"==typeof function(){}.name},r=Object.getOwnPropertyDescriptor;if(r)try{r([],"length")}catch(t){r=null}e.functionsHaveConfigurableNames=function(){if(!e()||!r)return!1;var t=r((function(){}),"name");return!!t&&!!t.configurable};var n=Function.prototype.bind;e.boundFunctionsHaveNames=function(){return e()&&"function"==typeof n&&""!==function(){}.bind().name},t.exports=e},4624:(t,e,r)=>{"use strict";var n,o=r(1152),i=r(7261),a=r(1932),u=r(5028),c=r(6500),s=r(2824),l=r(5488),f=Function,p=function(t){try{return f('"use strict"; return ('+t+").constructor;")()}catch(t){}},y=Object.getOwnPropertyDescriptor;if(y)try{y({},"")}catch(t){y=null}var d=function(){throw new s},h=y?function(){try{return d}catch(t){try{return y(arguments,"callee").get}catch(t){return d}}}():d,g=r(9800)(),m=r(7e3)(),b=Object.getPrototypeOf||(m?function(t){return t.__proto__}:null),v={},w="undefined"!=typeof Uint8Array&&b?b(Uint8Array):n,x={__proto__:null,"%AggregateError%":"undefined"==typeof AggregateError?n:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?n:ArrayBuffer,"%ArrayIteratorPrototype%":g&&b?b([][Symbol.iterator]()):n,"%AsyncFromSyncIteratorPrototype%":n,"%AsyncFunction%":v,"%AsyncGenerator%":v,"%AsyncGeneratorFunction%":v,"%AsyncIteratorPrototype%":v,"%Atomics%":"undefined"==typeof Atomics?n:Atomics,"%BigInt%":"undefined"==typeof BigInt?n:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?n:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?n:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?n:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":o,"%eval%":eval,"%EvalError%":i,"%Float32Array%":"undefined"==typeof Float32Array?n:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?n:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?n:FinalizationRegistry,"%Function%":f,"%GeneratorFunction%":v,"%Int8Array%":"undefined"==typeof Int8Array?n:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?n:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?n:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":g&&b?b(b([][Symbol.iterator]())):n,"%JSON%":"object"==typeof JSON?JSON:n,"%Map%":"undefined"==typeof Map?n:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&g&&b?b((new Map)[Symbol.iterator]()):n,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?n:Promise,"%Proxy%":"undefined"==typeof Proxy?n:Proxy,"%RangeError%":a,"%ReferenceError%":u,"%Reflect%":"undefined"==typeof Reflect?n:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?n:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&g&&b?b((new Set)[Symbol.iterator]()):n,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?n:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":g&&b?b(""[Symbol.iterator]()):n,"%Symbol%":g?Symbol:n,"%SyntaxError%":c,"%ThrowTypeError%":h,"%TypedArray%":w,"%TypeError%":s,"%Uint8Array%":"undefined"==typeof Uint8Array?n:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?n:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?n:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?n:Uint32Array,"%URIError%":l,"%WeakMap%":"undefined"==typeof WeakMap?n:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?n:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?n:WeakSet};if(b)try{null.error}catch(t){var S=b(b(t));x["%Error.prototype%"]=S}var E=function t(e){var r;if("%AsyncFunction%"===e)r=p("async function () {}");else if("%GeneratorFunction%"===e)r=p("function* () {}");else if("%AsyncGeneratorFunction%"===e)r=p("async function* () {}");else if("%AsyncGenerator%"===e){var n=t("%AsyncGeneratorFunction%");n&&(r=n.prototype)}else if("%AsyncIteratorPrototype%"===e){var o=t("%AsyncGenerator%");o&&b&&(r=b(o.prototype))}return x[e]=r,r},A={__proto__:null,"%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"]},O=r(3520),j=r(4440),T=O.call(Function.call,Array.prototype.concat),P=O.call(Function.apply,Array.prototype.splice),R=O.call(Function.call,String.prototype.replace),C=O.call(Function.call,String.prototype.slice),I=O.call(Function.call,RegExp.prototype.exec),N=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,M=/\\(\\)?/g,$=function(t,e){var r,n=t;if(j(A,n)&&(n="%"+(r=A[n])[0]+"%"),j(x,n)){var o=x[n];if(o===v&&(o=E(n)),void 0===o&&!e)throw new s("intrinsic "+t+" exists, but is not available. Please file an issue!");return{alias:r,name:n,value:o}}throw new c("intrinsic "+t+" does not exist!")};t.exports=function(t,e){if("string"!=typeof t||0===t.length)throw new s("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof e)throw new s('"allowMissing" argument must be a boolean');if(null===I(/^%?[^%]*%?$/,t))throw new c("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var r=function(t){var e=C(t,0,1),r=C(t,-1);if("%"===e&&"%"!==r)throw new c("invalid intrinsic syntax, expected closing `%`");if("%"===r&&"%"!==e)throw new c("invalid intrinsic syntax, expected opening `%`");var n=[];return R(t,N,(function(t,e,r,o){n[n.length]=r?R(o,M,"$1"):e||t})),n}(t),n=r.length>0?r[0]:"",o=$("%"+n+"%",e),i=o.name,a=o.value,u=!1,l=o.alias;l&&(n=l[0],P(r,T([0,1],l)));for(var f=1,p=!0;f=r.length){var m=y(a,d);a=(p=!!m)&&"get"in m&&!("originalValue"in m.get)?m.get:a[d]}else p=j(a,d),a=a[d];p&&!u&&(x[i]=a)}}return a}},6168:(t,e,r)=>{"use strict";var n=r(4624)("%Object.getOwnPropertyDescriptor%",!0);if(n)try{n([],"length")}catch(t){n=null}t.exports=n},3268:(t,e,r)=>{"use strict";var n=r(4624)("%Object.defineProperty%",!0),o=function(){if(n)try{return n({},"a",{value:1}),!0}catch(t){return!1}return!1};o.hasArrayLengthDefineBug=function(){if(!o())return null;try{return 1!==n([],"length",{value:1}).length}catch(t){return!0}},t.exports=o},7e3:t=>{"use strict";var e={foo:{}},r=Object;t.exports=function(){return{__proto__:e}.foo===e.foo&&!({__proto__:null}instanceof r)}},9800:(t,e,r)=>{"use strict";var n="undefined"!=typeof Symbol&&Symbol,o=r(7904);t.exports=function(){return"function"==typeof n&&"function"==typeof Symbol&&"symbol"==typeof n("foo")&&"symbol"==typeof Symbol("bar")&&o()}},7904:t=>{"use strict";t.exports=function(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var t={},e=Symbol("test"),r=Object(e);if("string"==typeof e)return!1;if("[object Symbol]"!==Object.prototype.toString.call(e))return!1;if("[object Symbol]"!==Object.prototype.toString.call(r))return!1;for(e in t[e]=42,t)return!1;if("function"==typeof Object.keys&&0!==Object.keys(t).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(t).length)return!1;var n=Object.getOwnPropertySymbols(t);if(1!==n.length||n[0]!==e)return!1;if(!Object.prototype.propertyIsEnumerable.call(t,e))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var o=Object.getOwnPropertyDescriptor(t,e);if(42!==o.value||!0!==o.enumerable)return!1}return!0}},4712:(t,e,r)=>{"use strict";var n=r(7904);t.exports=function(){return n()&&!!Symbol.toStringTag}},4440:(t,e,r)=>{"use strict";var n=Function.prototype.call,o=Object.prototype.hasOwnProperty,i=r(3520);t.exports=i.call(n,o)},7284:(t,e,r)=>{"use strict";var n=r(4440),o=r(3147)(),i=r(2824),a={assert:function(t,e){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");if(o.assert(t),!a.has(t,e))throw new i("`"+e+"` is not present on `O`")},get:function(t,e){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");var r=o.get(t);return r&&r["$"+e]},has:function(t,e){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");var r=o.get(t);return!!r&&n(r,"$"+e)},set:function(t,e,r){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new i("`O` is not an object");if("string"!=typeof e)throw new i("`slot` must be a string");var n=o.get(t);n||(n={},o.set(t,n)),n["$"+e]=r}};Object.freeze&&Object.freeze(a),t.exports=a},648:t=>{"use strict";var e,r,n=Function.prototype.toString,o="object"==typeof Reflect&&null!==Reflect&&Reflect.apply;if("function"==typeof o&&"function"==typeof Object.defineProperty)try{e=Object.defineProperty({},"length",{get:function(){throw r}}),r={},o((function(){throw 42}),null,e)}catch(t){t!==r&&(o=null)}else o=null;var i=/^\s*class\b/,a=function(t){try{var e=n.call(t);return i.test(e)}catch(t){return!1}},u=function(t){try{return!a(t)&&(n.call(t),!0)}catch(t){return!1}},c=Object.prototype.toString,s="function"==typeof Symbol&&!!Symbol.toStringTag,l=!(0 in[,]),f=function(){return!1};if("object"==typeof document){var p=document.all;c.call(p)===c.call(document.all)&&(f=function(t){if((l||!t)&&(void 0===t||"object"==typeof t))try{var e=c.call(t);return("[object HTMLAllCollection]"===e||"[object HTML document.all class]"===e||"[object HTMLCollection]"===e||"[object Object]"===e)&&null==t("")}catch(t){}return!1})}t.exports=o?function(t){if(f(t))return!0;if(!t)return!1;if("function"!=typeof t&&"object"!=typeof t)return!1;try{o(t,null,e)}catch(t){if(t!==r)return!1}return!a(t)&&u(t)}:function(t){if(f(t))return!0;if(!t)return!1;if("function"!=typeof t&&"object"!=typeof t)return!1;if(s)return u(t);if(a(t))return!1;var e=c.call(t);return!("[object Function]"!==e&&"[object GeneratorFunction]"!==e&&!/^\[object HTML/.test(e))&&u(t)}},1844:(t,e,r)=>{"use strict";var n=Date.prototype.getDay,o=Object.prototype.toString,i=r(4712)();t.exports=function(t){return"object"==typeof t&&null!==t&&(i?function(t){try{return n.call(t),!0}catch(t){return!1}}(t):"[object Date]"===o.call(t))}},1476:(t,e,r)=>{"use strict";var n,o,i,a,u=r(668),c=r(4712)();if(c){n=u("Object.prototype.hasOwnProperty"),o=u("RegExp.prototype.exec"),i={};var s=function(){throw i};a={toString:s,valueOf:s},"symbol"==typeof Symbol.toPrimitive&&(a[Symbol.toPrimitive]=s)}var l=u("Object.prototype.toString"),f=Object.getOwnPropertyDescriptor;t.exports=c?function(t){if(!t||"object"!=typeof t)return!1;var e=f(t,"lastIndex");if(!e||!n(e,"value"))return!1;try{o(t,a)}catch(t){return t===i}}:function(t){return!(!t||"object"!=typeof t&&"function"!=typeof t)&&"[object RegExp]"===l(t)}},7256:(t,e,r)=>{"use strict";var n=Object.prototype.toString;if(r(9800)()){var o=Symbol.prototype.toString,i=/^Symbol\(.*\)$/;t.exports=function(t){if("symbol"==typeof t)return!0;if("[object Symbol]"!==n.call(t))return!1;try{return function(t){return"symbol"==typeof t.valueOf()&&i.test(o.call(t))}(t)}catch(t){return!1}}}else t.exports=function(t){return!1}},4152:(t,e,r)=>{var n="function"==typeof Map&&Map.prototype,o=Object.getOwnPropertyDescriptor&&n?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,i=n&&o&&"function"==typeof o.get?o.get:null,a=n&&Map.prototype.forEach,u="function"==typeof Set&&Set.prototype,c=Object.getOwnPropertyDescriptor&&u?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,s=u&&c&&"function"==typeof c.get?c.get:null,l=u&&Set.prototype.forEach,f="function"==typeof WeakMap&&WeakMap.prototype?WeakMap.prototype.has:null,p="function"==typeof WeakSet&&WeakSet.prototype?WeakSet.prototype.has:null,y="function"==typeof WeakRef&&WeakRef.prototype?WeakRef.prototype.deref:null,d=Boolean.prototype.valueOf,h=Object.prototype.toString,g=Function.prototype.toString,m=String.prototype.match,b=String.prototype.slice,v=String.prototype.replace,w=String.prototype.toUpperCase,x=String.prototype.toLowerCase,S=RegExp.prototype.test,E=Array.prototype.concat,A=Array.prototype.join,O=Array.prototype.slice,j=Math.floor,T="function"==typeof BigInt?BigInt.prototype.valueOf:null,P=Object.getOwnPropertySymbols,R="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,C="function"==typeof Symbol&&"object"==typeof Symbol.iterator,I="function"==typeof Symbol&&Symbol.toStringTag&&(Symbol.toStringTag,1)?Symbol.toStringTag:null,N=Object.prototype.propertyIsEnumerable,M=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(t){return t.__proto__}:null);function $(t,e){if(t===1/0||t===-1/0||t!=t||t&&t>-1e3&&t<1e3||S.call(/e/,e))return e;var r=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof t){var n=t<0?-j(-t):j(t);if(n!==t){var o=String(n),i=b.call(e,o.length+1);return v.call(o,r,"$&_")+"."+v.call(v.call(i,/([0-9]{3})/g,"$&_"),/_$/,"")}}return v.call(e,r,"$&_")}var k=r(1740),D=k.custom,F=U(D)?D:null;function L(t,e,r){var n="double"===(r.quoteStyle||e)?'"':"'";return n+t+n}function B(t){return v.call(String(t),/"/g,""")}function _(t){return!("[object Array]"!==G(t)||I&&"object"==typeof t&&I in t)}function W(t){return!("[object RegExp]"!==G(t)||I&&"object"==typeof t&&I in t)}function U(t){if(C)return t&&"object"==typeof t&&t instanceof Symbol;if("symbol"==typeof t)return!0;if(!t||"object"!=typeof t||!R)return!1;try{return R.call(t),!0}catch(t){}return!1}t.exports=function t(e,n,o,u){var c=n||{};if(H(c,"quoteStyle")&&"single"!==c.quoteStyle&&"double"!==c.quoteStyle)throw new TypeError('option "quoteStyle" must be "single" or "double"');if(H(c,"maxStringLength")&&("number"==typeof c.maxStringLength?c.maxStringLength<0&&c.maxStringLength!==1/0:null!==c.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var h=!H(c,"customInspect")||c.customInspect;if("boolean"!=typeof h&&"symbol"!==h)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(H(c,"indent")&&null!==c.indent&&"\t"!==c.indent&&!(parseInt(c.indent,10)===c.indent&&c.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(H(c,"numericSeparator")&&"boolean"!=typeof c.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var w=c.numericSeparator;if(void 0===e)return"undefined";if(null===e)return"null";if("boolean"==typeof e)return e?"true":"false";if("string"==typeof e)return q(e,c);if("number"==typeof e){if(0===e)return 1/0/e>0?"0":"-0";var S=String(e);return w?$(e,S):S}if("bigint"==typeof e){var j=String(e)+"n";return w?$(e,j):j}var P=void 0===c.depth?5:c.depth;if(void 0===o&&(o=0),o>=P&&P>0&&"object"==typeof e)return _(e)?"[Array]":"[Object]";var D,z=function(t,e){var r;if("\t"===t.indent)r="\t";else{if(!("number"==typeof t.indent&&t.indent>0))return null;r=A.call(Array(t.indent+1)," ")}return{base:r,prev:A.call(Array(e+1),r)}}(c,o);if(void 0===u)u=[];else if(V(u,e)>=0)return"[Circular]";function X(e,r,n){if(r&&(u=O.call(u)).push(r),n){var i={depth:c.depth};return H(c,"quoteStyle")&&(i.quoteStyle=c.quoteStyle),t(e,i,o+1,u)}return t(e,c,o+1,u)}if("function"==typeof e&&!W(e)){var tt=function(t){if(t.name)return t.name;var e=m.call(g.call(t),/^function\s*([\w$]+)/);return e?e[1]:null}(e),et=Z(e,X);return"[Function"+(tt?": "+tt:" (anonymous)")+"]"+(et.length>0?" { "+A.call(et,", ")+" }":"")}if(U(e)){var rt=C?v.call(String(e),/^(Symbol\(.*\))_[^)]*$/,"$1"):R.call(e);return"object"!=typeof e||C?rt:K(rt)}if((D=e)&&"object"==typeof D&&("undefined"!=typeof HTMLElement&&D instanceof HTMLElement||"string"==typeof D.nodeName&&"function"==typeof D.getAttribute)){for(var nt="<"+x.call(String(e.nodeName)),ot=e.attributes||[],it=0;it"}if(_(e)){if(0===e.length)return"[]";var at=Z(e,X);return z&&!function(t){for(var e=0;e=0)return!1;return!0}(at)?"["+Q(at,z)+"]":"[ "+A.call(at,", ")+" ]"}if(function(t){return!("[object Error]"!==G(t)||I&&"object"==typeof t&&I in t)}(e)){var ut=Z(e,X);return"cause"in Error.prototype||!("cause"in e)||N.call(e,"cause")?0===ut.length?"["+String(e)+"]":"{ ["+String(e)+"] "+A.call(ut,", ")+" }":"{ ["+String(e)+"] "+A.call(E.call("[cause]: "+X(e.cause),ut),", ")+" }"}if("object"==typeof e&&h){if(F&&"function"==typeof e[F]&&k)return k(e,{depth:P-o});if("symbol"!==h&&"function"==typeof e.inspect)return e.inspect()}if(function(t){if(!i||!t||"object"!=typeof t)return!1;try{i.call(t);try{s.call(t)}catch(t){return!0}return t instanceof Map}catch(t){}return!1}(e)){var ct=[];return a&&a.call(e,(function(t,r){ct.push(X(r,e,!0)+" => "+X(t,e))})),J("Map",i.call(e),ct,z)}if(function(t){if(!s||!t||"object"!=typeof t)return!1;try{s.call(t);try{i.call(t)}catch(t){return!0}return t instanceof Set}catch(t){}return!1}(e)){var st=[];return l&&l.call(e,(function(t){st.push(X(t,e))})),J("Set",s.call(e),st,z)}if(function(t){if(!f||!t||"object"!=typeof t)return!1;try{f.call(t,f);try{p.call(t,p)}catch(t){return!0}return t instanceof WeakMap}catch(t){}return!1}(e))return Y("WeakMap");if(function(t){if(!p||!t||"object"!=typeof t)return!1;try{p.call(t,p);try{f.call(t,f)}catch(t){return!0}return t instanceof WeakSet}catch(t){}return!1}(e))return Y("WeakSet");if(function(t){if(!y||!t||"object"!=typeof t)return!1;try{return y.call(t),!0}catch(t){}return!1}(e))return Y("WeakRef");if(function(t){return!("[object Number]"!==G(t)||I&&"object"==typeof t&&I in t)}(e))return K(X(Number(e)));if(function(t){if(!t||"object"!=typeof t||!T)return!1;try{return T.call(t),!0}catch(t){}return!1}(e))return K(X(T.call(e)));if(function(t){return!("[object Boolean]"!==G(t)||I&&"object"==typeof t&&I in t)}(e))return K(d.call(e));if(function(t){return!("[object String]"!==G(t)||I&&"object"==typeof t&&I in t)}(e))return K(X(String(e)));if("undefined"!=typeof window&&e===window)return"{ [object Window] }";if(e===r.g)return"{ [object globalThis] }";if(!function(t){return!("[object Date]"!==G(t)||I&&"object"==typeof t&&I in t)}(e)&&!W(e)){var lt=Z(e,X),ft=M?M(e)===Object.prototype:e instanceof Object||e.constructor===Object,pt=e instanceof Object?"":"null prototype",yt=!ft&&I&&Object(e)===e&&I in e?b.call(G(e),8,-1):pt?"Object":"",dt=(ft||"function"!=typeof e.constructor?"":e.constructor.name?e.constructor.name+" ":"")+(yt||pt?"["+A.call(E.call([],yt||[],pt||[]),": ")+"] ":"");return 0===lt.length?dt+"{}":z?dt+"{"+Q(lt,z)+"}":dt+"{ "+A.call(lt,", ")+" }"}return String(e)};var z=Object.prototype.hasOwnProperty||function(t){return t in this};function H(t,e){return z.call(t,e)}function G(t){return h.call(t)}function V(t,e){if(t.indexOf)return t.indexOf(e);for(var r=0,n=t.length;re.maxStringLength){var r=t.length-e.maxStringLength,n="... "+r+" more character"+(r>1?"s":"");return q(b.call(t,0,e.maxStringLength),e)+n}return L(v.call(v.call(t,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,X),"single",e)}function X(t){var e=t.charCodeAt(0),r={8:"b",9:"t",10:"n",12:"f",13:"r"}[e];return r?"\\"+r:"\\x"+(e<16?"0":"")+w.call(e.toString(16))}function K(t){return"Object("+t+")"}function Y(t){return t+" { ? }"}function J(t,e,r,n){return t+" ("+e+") {"+(n?Q(r,n):A.call(r,", "))+"}"}function Q(t,e){if(0===t.length)return"";var r="\n"+e.prev+e.base;return r+A.call(t,","+r)+"\n"+e.prev}function Z(t,e){var r=_(t),n=[];if(r){n.length=t.length;for(var o=0;o{"use strict";var n;if(!Object.keys){var o=Object.prototype.hasOwnProperty,i=Object.prototype.toString,a=r(9096),u=Object.prototype.propertyIsEnumerable,c=!u.call({toString:null},"toString"),s=u.call((function(){}),"prototype"),l=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],f=function(t){var e=t.constructor;return e&&e.prototype===t},p={$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},y=function(){if("undefined"==typeof window)return!1;for(var t in window)try{if(!p["$"+t]&&o.call(window,t)&&null!==window[t]&&"object"==typeof window[t])try{f(window[t])}catch(t){return!0}}catch(t){return!0}return!1}();n=function(t){var e=null!==t&&"object"==typeof t,r="[object Function]"===i.call(t),n=a(t),u=e&&"[object String]"===i.call(t),p=[];if(!e&&!r&&!n)throw new TypeError("Object.keys called on a non-object");var d=s&&r;if(u&&t.length>0&&!o.call(t,0))for(var h=0;h0)for(var g=0;g{"use strict";var n=Array.prototype.slice,o=r(9096),i=Object.keys,a=i?function(t){return i(t)}:r(9560),u=Object.keys;a.shim=function(){if(Object.keys){var t=function(){var t=Object.keys(arguments);return t&&t.length===arguments.length}(1,2);t||(Object.keys=function(t){return o(t)?u(n.call(t)):u(t)})}else Object.keys=a;return Object.keys||a},t.exports=a},9096:t=>{"use strict";var e=Object.prototype.toString;t.exports=function(t){var r=e.call(t),n="[object Arguments]"===r;return n||(n="[object Array]"!==r&&null!==t&&"object"==typeof t&&"number"==typeof t.length&&t.length>=0&&"[object Function]"===e.call(t.callee)),n}},7636:(t,e,r)=>{"use strict";var n=r(6308),o=r(2824),i=Object;t.exports=n((function(){if(null==this||this!==i(this))throw new o("RegExp.prototype.flags getter called on non-object");var t="";return this.hasIndices&&(t+="d"),this.global&&(t+="g"),this.ignoreCase&&(t+="i"),this.multiline&&(t+="m"),this.dotAll&&(t+="s"),this.unicode&&(t+="u"),this.unicodeSets&&(t+="v"),this.sticky&&(t+="y"),t}),"get flags",!0)},2192:(t,e,r)=>{"use strict";var n=r(2732),o=r(5096),i=r(7636),a=r(9296),u=r(736),c=o(a());n(c,{getPolyfill:a,implementation:i,shim:u}),t.exports=c},9296:(t,e,r)=>{"use strict";var n=r(7636),o=r(2732).supportsDescriptors,i=Object.getOwnPropertyDescriptor;t.exports=function(){if(o&&"gim"===/a/gim.flags){var t=i(RegExp.prototype,"flags");if(t&&"function"==typeof t.get&&"boolean"==typeof RegExp.prototype.dotAll&&"boolean"==typeof RegExp.prototype.hasIndices){var e="",r={};if(Object.defineProperty(r,"hasIndices",{get:function(){e+="d"}}),Object.defineProperty(r,"sticky",{get:function(){e+="y"}}),"dy"===e)return t.get}}return n}},736:(t,e,r)=>{"use strict";var n=r(2732).supportsDescriptors,o=r(9296),i=Object.getOwnPropertyDescriptor,a=Object.defineProperty,u=TypeError,c=Object.getPrototypeOf,s=/a/;t.exports=function(){if(!n||!c)throw new u("RegExp.prototype.flags requires a true ES5 environment that supports property descriptors");var t=o(),e=c(s),r=i(e,"flags");return r&&r.get===t||a(e,"flags",{configurable:!0,enumerable:!1,get:t}),t}},860:(t,e,r)=>{"use strict";var n=r(668),o=r(1476),i=n("RegExp.prototype.exec"),a=r(2824);t.exports=function(t){if(!o(t))throw new a("`regex` must be a RegExp");return function(e){return null!==i(t,e)}}},5676:(t,e,r)=>{"use strict";var n=r(4624),o=r(2448),i=r(3268)(),a=r(6168),u=r(2824),c=n("%Math.floor%");t.exports=function(t,e){if("function"!=typeof t)throw new u("`fn` is not a function");if("number"!=typeof e||e<0||e>4294967295||c(e)!==e)throw new u("`length` must be a positive 32-bit integer");var r=arguments.length>2&&!!arguments[2],n=!0,s=!0;if("length"in t&&a){var l=a(t,"length");l&&!l.configurable&&(n=!1),l&&!l.writable&&(s=!1)}return(n||s||!r)&&(i?o(t,"length",e,!0,!0):o(t,"length",e)),t}},6308:(t,e,r)=>{"use strict";var n=r(2448),o=r(3268)(),i=r(2656).functionsHaveConfigurableNames(),a=TypeError;t.exports=function(t,e){if("function"!=typeof t)throw new a("`fn` is not a function");return arguments.length>2&&!!arguments[2]&&!i||(o?n(t,"name",e,!0,!0):n(t,"name",e)),t}},3147:(t,e,r)=>{"use strict";var n=r(4624),o=r(668),i=r(4152),a=r(2824),u=n("%WeakMap%",!0),c=n("%Map%",!0),s=o("WeakMap.prototype.get",!0),l=o("WeakMap.prototype.set",!0),f=o("WeakMap.prototype.has",!0),p=o("Map.prototype.get",!0),y=o("Map.prototype.set",!0),d=o("Map.prototype.has",!0),h=function(t,e){for(var r,n=t;null!==(r=n.next);n=r)if(r.key===e)return n.next=r.next,r.next=t.next,t.next=r,r};t.exports=function(){var t,e,r,n={assert:function(t){if(!n.has(t))throw new a("Side channel does not contain "+i(t))},get:function(n){if(u&&n&&("object"==typeof n||"function"==typeof n)){if(t)return s(t,n)}else if(c){if(e)return p(e,n)}else if(r)return function(t,e){var r=h(t,e);return r&&r.value}(r,n)},has:function(n){if(u&&n&&("object"==typeof n||"function"==typeof n)){if(t)return f(t,n)}else if(c){if(e)return d(e,n)}else if(r)return function(t,e){return!!h(t,e)}(r,n);return!1},set:function(n,o){u&&n&&("object"==typeof n||"function"==typeof n)?(t||(t=new u),l(t,n,o)):c?(e||(e=new c),y(e,n,o)):(r||(r={key:{},next:null}),function(t,e,r){var n=h(t,e);n?n.value=r:t.next={key:e,next:t.next,value:r}}(r,n,o))}};return n}},9508:(t,e,r)=>{"use strict";var n=r(1700),o=r(3672),i=r(5552),a=r(3816),u=r(5424),c=r(4656),s=r(668),l=r(9800)(),f=r(2192),p=s("String.prototype.indexOf"),y=r(6288),d=function(t){var e=y();if(l&&"symbol"==typeof Symbol.matchAll){var r=i(t,Symbol.matchAll);return r===RegExp.prototype[Symbol.matchAll]&&r!==e?e:r}if(a(t))return e};t.exports=function(t){var e=c(this);if(null!=t){if(a(t)){var r="flags"in t?o(t,"flags"):f(t);if(c(r),p(u(r),"g")<0)throw new TypeError("matchAll requires a global regular expression")}var i=d(t);if(void 0!==i)return n(i,t,[e])}var s=u(e),l=new RegExp(t,"g");return n(d(l),l,[s])}},3732:(t,e,r)=>{"use strict";var n=r(5096),o=r(2732),i=r(9508),a=r(5844),u=r(4148),c=n(i);o(c,{getPolyfill:a,implementation:i,shim:u}),t.exports=c},6288:(t,e,r)=>{"use strict";var n=r(9800)(),o=r(7492);t.exports=function(){return n&&"symbol"==typeof Symbol.matchAll&&"function"==typeof RegExp.prototype[Symbol.matchAll]?RegExp.prototype[Symbol.matchAll]:o}},5844:(t,e,r)=>{"use strict";var n=r(9508);t.exports=function(){if(String.prototype.matchAll)try{"".matchAll(RegExp.prototype)}catch(t){return String.prototype.matchAll}return n}},7492:(t,e,r)=>{"use strict";var n=r(5211),o=r(3672),i=r(4e3),a=r(8652),u=r(4784),c=r(5424),s=r(8645),l=r(2192),f=r(6308),p=r(668)("String.prototype.indexOf"),y=RegExp,d="flags"in RegExp.prototype,h=f((function(t){var e=this;if("Object"!==s(e))throw new TypeError('"this" value must be an Object');var r=c(t),f=function(t,e){var r="flags"in e?o(e,"flags"):c(l(e));return{flags:r,matcher:new t(d&&"string"==typeof r?e:t===y?e.source:e,r)}}(a(e,y),e),h=f.flags,g=f.matcher,m=u(o(e,"lastIndex"));i(g,"lastIndex",m,!0);var b=p(h,"g")>-1,v=p(h,"u")>-1;return n(g,r,b,v)}),"[Symbol.matchAll]",!0);t.exports=h},4148:(t,e,r)=>{"use strict";var n=r(2732),o=r(9800)(),i=r(5844),a=r(6288),u=Object.defineProperty,c=Object.getOwnPropertyDescriptor;t.exports=function(){var t=i();if(n(String.prototype,{matchAll:t},{matchAll:function(){return String.prototype.matchAll!==t}}),o){var e=Symbol.matchAll||(Symbol.for?Symbol.for("Symbol.matchAll"):Symbol("Symbol.matchAll"));if(n(Symbol,{matchAll:e},{matchAll:function(){return Symbol.matchAll!==e}}),u&&c){var r=c(Symbol,e);r&&!r.configurable||u(Symbol,e,{configurable:!1,enumerable:!1,value:e,writable:!1})}var s=a(),l={};l[e]=s;var f={};f[e]=function(){return RegExp.prototype[e]!==s},n(RegExp.prototype,l,f)}return t}},6936:(t,e,r)=>{"use strict";var n=r(4656),o=r(5424),i=r(668)("String.prototype.replace"),a=/^\s$/.test("᠎"),u=a?/^[\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]+/,c=a?/[\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]+$/;t.exports=function(){var t=o(n(this));return i(i(t,u,""),c,"")}},9292:(t,e,r)=>{"use strict";var n=r(5096),o=r(2732),i=r(4656),a=r(6936),u=r(6684),c=r(9788),s=n(u()),l=function(t){return i(t),s(t)};o(l,{getPolyfill:u,implementation:a,shim:c}),t.exports=l},6684:(t,e,r)=>{"use strict";var n=r(6936);t.exports=function(){return String.prototype.trim&&"​"==="​".trim()&&"᠎"==="᠎".trim()&&"_᠎"==="_᠎".trim()&&"᠎_"==="᠎_".trim()?String.prototype.trim:n}},9788:(t,e,r)=>{"use strict";var n=r(2732),o=r(6684);t.exports=function(){var t=o();return n(String.prototype,{trim:t},{trim:function(){return String.prototype.trim!==t}}),t}},1740:()=>{},1056:(t,e,r)=>{"use strict";var n=r(4624),o=r(8536),i=r(8645),a=r(7724),u=r(9132),c=n("%TypeError%");t.exports=function(t,e,r){if("String"!==i(t))throw new c("Assertion failed: `S` must be a String");if(!a(e)||e<0||e>u)throw new c("Assertion failed: `length` must be an integer >= 0 and <= 2**53");if("Boolean"!==i(r))throw new c("Assertion failed: `unicode` must be a Boolean");return r?e+1>=t.length?e+1:e+o(t,e)["[[CodeUnitCount]]"]:e+1}},1700:(t,e,r)=>{"use strict";var n=r(4624),o=r(668),i=n("%TypeError%"),a=r(1720),u=n("%Reflect.apply%",!0)||o("Function.prototype.apply");t.exports=function(t,e){var r=arguments.length>2?arguments[2]:[];if(!a(r))throw new i("Assertion failed: optional `argumentsList`, if provided, must be a List");return u(t,e,r)}},8536:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(668),i=r(1712),a=r(8444),u=r(8645),c=r(2320),s=o("String.prototype.charAt"),l=o("String.prototype.charCodeAt");t.exports=function(t,e){if("String"!==u(t))throw new n("Assertion failed: `string` must be a String");var r=t.length;if(e<0||e>=r)throw new n("Assertion failed: `position` must be >= 0, and < the length of `string`");var o=l(t,e),f=s(t,e),p=i(o),y=a(o);if(!p&&!y)return{"[[CodePoint]]":f,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!1};if(y||e+1===r)return{"[[CodePoint]]":f,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!0};var d=l(t,e+1);return a(d)?{"[[CodePoint]]":c(o,d),"[[CodeUnitCount]]":2,"[[IsUnpairedSurrogate]]":!1}:{"[[CodePoint]]":f,"[[CodeUnitCount]]":1,"[[IsUnpairedSurrogate]]":!0}}},4288:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(8645);t.exports=function(t,e){if("Boolean"!==o(e))throw new n("Assertion failed: Type(done) is not Boolean");return{value:t,done:e}}},2672:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(4436),i=r(8924),a=r(3880),u=r(2968),c=r(8800),s=r(8645);t.exports=function(t,e,r){if("Object"!==s(t))throw new n("Assertion failed: Type(O) is not Object");if(!u(e))throw new n("Assertion failed: IsPropertyKey(P) is not true");return o(a,c,i,t,e,{"[[Configurable]]":!0,"[[Enumerable]]":!1,"[[Value]]":r,"[[Writable]]":!0})}},5211:(t,e,r)=>{"use strict";var n=r(4624),o=r(9800)(),i=n("%TypeError%"),a=n("%IteratorPrototype%",!0),u=r(1056),c=r(4288),s=r(2672),l=r(3672),f=r(6216),p=r(8972),y=r(4e3),d=r(4784),h=r(5424),g=r(8645),m=r(7284),b=r(9200),v=function(t,e,r,n){if("String"!==g(e))throw new i("`S` must be a string");if("Boolean"!==g(r))throw new i("`global` must be a boolean");if("Boolean"!==g(n))throw new i("`fullUnicode` must be a boolean");m.set(this,"[[IteratingRegExp]]",t),m.set(this,"[[IteratedString]]",e),m.set(this,"[[Global]]",r),m.set(this,"[[Unicode]]",n),m.set(this,"[[Done]]",!1)};a&&(v.prototype=f(a)),s(v.prototype,"next",(function(){var t=this;if("Object"!==g(t))throw new i("receiver must be an object");if(!(t instanceof v&&m.has(t,"[[IteratingRegExp]]")&&m.has(t,"[[IteratedString]]")&&m.has(t,"[[Global]]")&&m.has(t,"[[Unicode]]")&&m.has(t,"[[Done]]")))throw new i('"this" value must be a RegExpStringIterator instance');if(m.get(t,"[[Done]]"))return c(void 0,!0);var e=m.get(t,"[[IteratingRegExp]]"),r=m.get(t,"[[IteratedString]]"),n=m.get(t,"[[Global]]"),o=m.get(t,"[[Unicode]]"),a=p(e,r);if(null===a)return m.set(t,"[[Done]]",!0),c(void 0,!0);if(n){if(""===h(l(a,"0"))){var s=d(l(e,"lastIndex")),f=u(r,s,o);y(e,"lastIndex",f,!0)}return c(a,!1)}return m.set(t,"[[Done]]",!0),c(a,!1)})),o&&(b(v.prototype,"RegExp String Iterator"),Symbol.iterator&&"function"!=typeof v.prototype[Symbol.iterator])&&s(v.prototype,Symbol.iterator,(function(){return this})),t.exports=function(t,e,r,n){return new v(t,e,r,n)}},7268:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(320),i=r(4436),a=r(8924),u=r(4936),c=r(3880),s=r(2968),l=r(8800),f=r(5696),p=r(8645);t.exports=function(t,e,r){if("Object"!==p(t))throw new n("Assertion failed: Type(O) is not Object");if(!s(e))throw new n("Assertion failed: IsPropertyKey(P) is not true");var y=o({Type:p,IsDataDescriptor:c,IsAccessorDescriptor:u},r)?r:f(r);if(!o({Type:p,IsDataDescriptor:c,IsAccessorDescriptor:u},y))throw new n("Assertion failed: Desc is not a valid Property Descriptor");return i(c,l,a,t,e,y)}},8924:(t,e,r)=>{"use strict";var n=r(3600),o=r(3504),i=r(8645);t.exports=function(t){return void 0!==t&&n(i,"Property Descriptor","Desc",t),o(t)}},3672:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(4152),i=r(2968),a=r(8645);t.exports=function(t,e){if("Object"!==a(t))throw new n("Assertion failed: Type(O) is not Object");if(!i(e))throw new n("Assertion failed: IsPropertyKey(P) is not true, got "+o(e));return t[e]}},5552:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(3396),i=r(3048),a=r(2968),u=r(4152);t.exports=function(t,e){if(!a(e))throw new n("Assertion failed: IsPropertyKey(P) is not true");var r=o(t,e);if(null!=r){if(!i(r))throw new n(u(e)+" is not a function: "+u(r));return r}}},3396:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(4152),i=r(2968);t.exports=function(t,e){if(!i(e))throw new n("Assertion failed: IsPropertyKey(P) is not true, got "+o(e));return t[e]}},4936:(t,e,r)=>{"use strict";var n=r(4440),o=r(8645),i=r(3600);t.exports=function(t){return void 0!==t&&(i(o,"Property Descriptor","Desc",t),!(!n(t,"[[Get]]")&&!n(t,"[[Set]]")))}},1720:(t,e,r)=>{"use strict";t.exports=r(704)},3048:(t,e,r)=>{"use strict";t.exports=r(648)},211:(t,e,r)=>{"use strict";var n=r(8600)("%Reflect.construct%",!0),o=r(7268);try{o({},"",{"[[Get]]":function(){}})}catch(t){o=null}if(o&&n){var i={},a={};o(a,"length",{"[[Get]]":function(){throw i},"[[Enumerable]]":!0}),t.exports=function(t){try{n(t,a)}catch(t){return t===i}}}else t.exports=function(t){return"function"==typeof t&&!!t.prototype}},3880:(t,e,r)=>{"use strict";var n=r(4440),o=r(8645),i=r(3600);t.exports=function(t){return void 0!==t&&(i(o,"Property Descriptor","Desc",t),!(!n(t,"[[Value]]")&&!n(t,"[[Writable]]")))}},2968:t=>{"use strict";t.exports=function(t){return"string"==typeof t||"symbol"==typeof t}},3816:(t,e,r)=>{"use strict";var n=r(4624)("%Symbol.match%",!0),o=r(1476),i=r(6848);t.exports=function(t){if(!t||"object"!=typeof t)return!1;if(n){var e=t[n];if(void 0!==e)return i(e)}return o(t)}},6216:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Object.create%",!0),i=n("%TypeError%"),a=n("%SyntaxError%"),u=r(1720),c=r(8645),s=r(4672),l=r(7284),f=r(7e3)();t.exports=function(t){if(null!==t&&"Object"!==c(t))throw new i("Assertion failed: `proto` must be null or an object");var e,r=arguments.length<2?[]:arguments[1];if(!u(r))throw new i("Assertion failed: `additionalInternalSlotsList` must be an Array");if(o)e=o(t);else if(f)e={__proto__:t};else{if(null===t)throw new a("native Object.create support is required to create null objects");var n=function(){};n.prototype=t,e=new n}return r.length>0&&s(r,(function(t){l.set(e,t,void 0)})),e}},8972:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(668)("RegExp.prototype.exec"),i=r(1700),a=r(3672),u=r(3048),c=r(8645);t.exports=function(t,e){if("Object"!==c(t))throw new n("Assertion failed: `R` must be an Object");if("String"!==c(e))throw new n("Assertion failed: `S` must be a String");var r=a(t,"exec");if(u(r)){var s=i(r,t,[e]);if(null===s||"Object"===c(s))return s;throw new n('"exec" method must return `null` or an Object')}return o(t,e)}},4656:(t,e,r)=>{"use strict";t.exports=r(176)},8800:(t,e,r)=>{"use strict";var n=r(2808);t.exports=function(t,e){return t===e?0!==t||1/t==1/e:n(t)&&n(e)}},4e3:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%"),o=r(2968),i=r(8800),a=r(8645),u=function(){try{return delete[].length,!0}catch(t){return!1}}();t.exports=function(t,e,r,c){if("Object"!==a(t))throw new n("Assertion failed: `O` must be an Object");if(!o(e))throw new n("Assertion failed: `P` must be a Property Key");if("Boolean"!==a(c))throw new n("Assertion failed: `Throw` must be a Boolean");if(c){if(t[e]=r,u&&!i(t[e],r))throw new n("Attempted to assign to readonly property.");return!0}try{return t[e]=r,!u||i(t[e],r)}catch(t){return!1}}},8652:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Symbol.species%",!0),i=n("%TypeError%"),a=r(211),u=r(8645);t.exports=function(t,e){if("Object"!==u(t))throw new i("Assertion failed: Type(O) is not Object");var r=t.constructor;if(void 0===r)return e;if("Object"!==u(r))throw new i("O.constructor is not an Object");var n=o?r[o]:void 0;if(null==n)return e;if(a(n))return n;throw new i("no constructor found")}},8772:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Number%"),i=n("%RegExp%"),a=n("%TypeError%"),u=n("%parseInt%"),c=r(668),s=r(860),l=c("String.prototype.slice"),f=s(/^0b[01]+$/i),p=s(/^0o[0-7]+$/i),y=s(/^[-+]0x[0-9a-f]+$/i),d=s(new i("["+["…","​","￾"].join("")+"]","g")),h=r(9292),g=r(8645);t.exports=function t(e){if("String"!==g(e))throw new a("Assertion failed: `argument` is not a String");if(f(e))return o(u(l(e,2),2));if(p(e))return o(u(l(e,2),8));if(d(e)||y(e))return NaN;var r=h(e);return r!==e?t(r):o(e)}},6848:t=>{"use strict";t.exports=function(t){return!!t}},9424:(t,e,r)=>{"use strict";var n=r(7220),o=r(2592),i=r(2808),a=r(2931);t.exports=function(t){var e=n(t);return i(e)||0===e?0:a(e)?o(e):e}},4784:(t,e,r)=>{"use strict";var n=r(9132),o=r(9424);t.exports=function(t){var e=o(t);return e<=0?0:e>n?n:e}},7220:(t,e,r)=>{"use strict";var n=r(4624),o=n("%TypeError%"),i=n("%Number%"),a=r(2336),u=r(5556),c=r(8772);t.exports=function(t){var e=a(t)?t:u(t,i);if("symbol"==typeof e)throw new o("Cannot convert a Symbol value to a number");if("bigint"==typeof e)throw new o("Conversion from 'BigInt' to 'number' is not allowed.");return"string"==typeof e?c(e):i(e)}},5556:(t,e,r)=>{"use strict";var n=r(108);t.exports=function(t){return arguments.length>1?n(t,arguments[1]):n(t)}},5696:(t,e,r)=>{"use strict";var n=r(4440),o=r(4624)("%TypeError%"),i=r(8645),a=r(6848),u=r(3048);t.exports=function(t){if("Object"!==i(t))throw new o("ToPropertyDescriptor requires an object");var e={};if(n(t,"enumerable")&&(e["[[Enumerable]]"]=a(t.enumerable)),n(t,"configurable")&&(e["[[Configurable]]"]=a(t.configurable)),n(t,"value")&&(e["[[Value]]"]=t.value),n(t,"writable")&&(e["[[Writable]]"]=a(t.writable)),n(t,"get")){var r=t.get;if(void 0!==r&&!u(r))throw new o("getter must be a function");e["[[Get]]"]=r}if(n(t,"set")){var c=t.set;if(void 0!==c&&!u(c))throw new o("setter must be a function");e["[[Set]]"]=c}if((n(e,"[[Get]]")||n(e,"[[Set]]"))&&(n(e,"[[Value]]")||n(e,"[[Writable]]")))throw new o("Invalid property descriptor. Cannot both specify accessors and a value or writable attribute");return e}},5424:(t,e,r)=>{"use strict";var n=r(4624),o=n("%String%"),i=n("%TypeError%");t.exports=function(t){if("symbol"==typeof t)throw new i("Cannot convert a Symbol value to a string");return o(t)}},8645:(t,e,r)=>{"use strict";var n=r(7936);t.exports=function(t){return"symbol"==typeof t?"Symbol":"bigint"==typeof t?"BigInt":n(t)}},2320:(t,e,r)=>{"use strict";var n=r(4624),o=n("%TypeError%"),i=n("%String.fromCharCode%"),a=r(1712),u=r(8444);t.exports=function(t,e){if(!a(t)||!u(e))throw new o("Assertion failed: `lead` must be a leading surrogate char code, and `trail` must be a trailing surrogate char code");return i(t)+i(e)}},2312:(t,e,r)=>{"use strict";var n=r(8645),o=Math.floor;t.exports=function(t){return"BigInt"===n(t)?t:o(t)}},2592:(t,e,r)=>{"use strict";var n=r(4624),o=r(2312),i=n("%TypeError%");t.exports=function(t){if("number"!=typeof t&&"bigint"!=typeof t)throw new i("argument must be a Number or a BigInt");var e=t<0?-o(-t):o(t);return 0===e?0:e}},176:(t,e,r)=>{"use strict";var n=r(4624)("%TypeError%");t.exports=function(t,e){if(null==t)throw new n(e||"Cannot call method on "+t);return t}},7936:t=>{"use strict";t.exports=function(t){return null===t?"Null":void 0===t?"Undefined":"function"==typeof t||"object"==typeof t?"Object":"number"==typeof t?"Number":"boolean"==typeof t?"Boolean":"string"==typeof t?"String":void 0}},8600:(t,e,r)=>{"use strict";t.exports=r(4624)},4436:(t,e,r)=>{"use strict";var n=r(3268),o=r(4624),i=n()&&o("%Object.defineProperty%",!0),a=n.hasArrayLengthDefineBug(),u=a&&r(704),c=r(668)("Object.prototype.propertyIsEnumerable");t.exports=function(t,e,r,n,o,s){if(!i){if(!t(s))return!1;if(!s["[[Configurable]]"]||!s["[[Writable]]"])return!1;if(o in n&&c(n,o)!==!!s["[[Enumerable]]"])return!1;var l=s["[[Value]]"];return n[o]=l,e(n[o],l)}return a&&"length"===o&&"[[Value]]"in s&&u(n)&&n.length!==s["[[Value]]"]?(n.length=s["[[Value]]"],n.length===s["[[Value]]"]):(i(n,o,r(s)),!0)}},704:(t,e,r)=>{"use strict";var n=r(4624)("%Array%"),o=!n.isArray&&r(668)("Object.prototype.toString");t.exports=n.isArray||function(t){return"[object Array]"===o(t)}},3600:(t,e,r)=>{"use strict";var n=r(4624),o=n("%TypeError%"),i=n("%SyntaxError%"),a=r(4440),u=r(7724),c={"Property Descriptor":function(t){var e={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};if(!t)return!1;for(var r in t)if(a(t,r)&&!e[r])return!1;var n=a(t,"[[Value]]"),i=a(t,"[[Get]]")||a(t,"[[Set]]");if(n&&i)throw new o("Property Descriptors may not be both accessor and data descriptors");return!0},"Match Record":r(5092),"Iterator Record":function(t){return a(t,"[[Iterator]]")&&a(t,"[[NextMethod]]")&&a(t,"[[Done]]")},"PromiseCapability Record":function(t){return!!t&&a(t,"[[Resolve]]")&&"function"==typeof t["[[Resolve]]"]&&a(t,"[[Reject]]")&&"function"==typeof t["[[Reject]]"]&&a(t,"[[Promise]]")&&t["[[Promise]]"]&&"function"==typeof t["[[Promise]]"].then},"AsyncGeneratorRequest Record":function(t){return!!t&&a(t,"[[Completion]]")&&a(t,"[[Capability]]")&&c["PromiseCapability Record"](t["[[Capability]]"])},"RegExp Record":function(t){return t&&a(t,"[[IgnoreCase]]")&&"boolean"==typeof t["[[IgnoreCase]]"]&&a(t,"[[Multiline]]")&&"boolean"==typeof t["[[Multiline]]"]&&a(t,"[[DotAll]]")&&"boolean"==typeof t["[[DotAll]]"]&&a(t,"[[Unicode]]")&&"boolean"==typeof t["[[Unicode]]"]&&a(t,"[[CapturingGroupsCount]]")&&"number"==typeof t["[[CapturingGroupsCount]]"]&&u(t["[[CapturingGroupsCount]]"])&&t["[[CapturingGroupsCount]]"]>=0}};t.exports=function(t,e,r,n){var a=c[e];if("function"!=typeof a)throw new i("unknown record type: "+e);if("Object"!==t(n)||!a(n))throw new o(r+" must be a "+e)}},4672:t=>{"use strict";t.exports=function(t,e){for(var r=0;r{"use strict";t.exports=function(t){if(void 0===t)return t;var e={};return"[[Value]]"in t&&(e.value=t["[[Value]]"]),"[[Writable]]"in t&&(e.writable=!!t["[[Writable]]"]),"[[Get]]"in t&&(e.get=t["[[Get]]"]),"[[Set]]"in t&&(e.set=t["[[Set]]"]),"[[Enumerable]]"in t&&(e.enumerable=!!t["[[Enumerable]]"]),"[[Configurable]]"in t&&(e.configurable=!!t["[[Configurable]]"]),e}},2931:(t,e,r)=>{"use strict";var n=r(2808);t.exports=function(t){return("number"==typeof t||"bigint"==typeof t)&&!n(t)&&t!==1/0&&t!==-1/0}},7724:(t,e,r)=>{"use strict";var n=r(4624),o=n("%Math.abs%"),i=n("%Math.floor%"),a=r(2808),u=r(2931);t.exports=function(t){if("number"!=typeof t||a(t)||!u(t))return!1;var e=o(t);return i(e)===e}},1712:t=>{"use strict";t.exports=function(t){return"number"==typeof t&&t>=55296&&t<=56319}},5092:(t,e,r)=>{"use strict";var n=r(4440);t.exports=function(t){return n(t,"[[StartIndex]]")&&n(t,"[[EndIndex]]")&&t["[[StartIndex]]"]>=0&&t["[[EndIndex]]"]>=t["[[StartIndex]]"]&&String(parseInt(t["[[StartIndex]]"],10))===String(t["[[StartIndex]]"])&&String(parseInt(t["[[EndIndex]]"],10))===String(t["[[EndIndex]]"])}},2808:t=>{"use strict";t.exports=Number.isNaN||function(t){return t!=t}},2336:t=>{"use strict";t.exports=function(t){return null===t||"function"!=typeof t&&"object"!=typeof t}},320:(t,e,r)=>{"use strict";var n=r(4624),o=r(4440),i=n("%TypeError%");t.exports=function(t,e){if("Object"!==t.Type(e))return!1;var r={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};for(var n in e)if(o(e,n)&&!r[n])return!1;if(t.IsDataDescriptor(e)&&t.IsAccessorDescriptor(e))throw new i("Property Descriptors may not be both accessor and data descriptors");return!0}},8444:t=>{"use strict";t.exports=function(t){return"number"==typeof t&&t>=56320&&t<=57343}},9132:t=>{"use strict";t.exports=Number.MAX_SAFE_INTEGER||9007199254740991}},e={};function r(n){var o=e[n];if(void 0!==o)return o.exports;var i=e[n]={exports:{}};return t[n](i,i.exports,r),i.exports}r.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return r.d(e,{a:e}),e},r.d=(t,e)=>{for(var n in e)r.o(e,n)&&!r.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";var t=r(9116);function e(e,r,n){let o=0,i=[];for(;-1!==o;)o=e.indexOf(r,o),-1!==o&&(i.push({start:o,end:o+r.length,errors:0}),o+=1);return i.length>0?i:(0,t.c)(e,r,n)}function n(t,r){return 0===r.length||0===t.length?0:1-e(t,r,r.length)[0].errors/r.length}function o(t){switch(t.nodeType){case Node.ELEMENT_NODE:case Node.TEXT_NODE:return t.textContent.length;default:return 0}}function i(t){let e=t.previousSibling,r=0;for(;e;)r+=o(e),e=e.previousSibling;return r}function a(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),n=1;no?(a.push({node:u,offset:o-s}),o=r.shift()):(c=i.nextNode(),s+=u.data.length);for(;void 0!==o&&u&&s===o;)a.push({node:u,offset:u.data.length}),o=r.shift();if(void 0!==o)throw new RangeError("Offset exceeds text length");return a}class u{constructor(t,e){if(e<0)throw new Error("Offset is invalid");this.element=t,this.offset=e}relativeTo(t){if(!t.contains(this.element))throw new Error("Parent is not an ancestor of current element");let e=this.element,r=this.offset;for(;e!==t;)r+=i(e),e=e.parentElement;return new u(e,r)}resolve(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};try{return a(this.element,this.offset)[0]}catch(e){if(0===this.offset&&void 0!==t.direction){const r=document.createTreeWalker(this.element.getRootNode(),NodeFilter.SHOW_TEXT);r.currentNode=this.element;const n=1===t.direction,o=n?r.nextNode():r.previousNode();if(!o)throw e;return{node:o,offset:n?0:o.data.length}}throw e}}static fromCharOffset(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")}}static fromPoint(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");const r=i(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");let r=0;for(let n=0;n2&&void 0!==arguments[2]?arguments[2]:{};this.root=t,this.exact=e,this.context=r}static fromRange(t,e){const r=t.textContent,n=c.fromRange(e).relativeTo(t),o=n.start.offset,i=n.end.offset;return new l(t,r.slice(o,i),{prefix:r.slice(Math.max(0,o-32),o),suffix:r.slice(i,Math.min(r.length,i+32))})}static fromSelector(t,e){const{prefix:r,suffix:n}=e;return new l(t,e.exact,{prefix:r,suffix:n})}toSelector(){return{type:"TextQuoteSelector",exact:this.exact,prefix:this.context.prefix,suffix:this.context.suffix}}toRange(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.toPositionAnchor(t).toRange()}toPositionAnchor(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const r=function(t,r){let o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(0===r.length)return null;const i=Math.min(256,r.length/2),a=e(t,r,i);if(0===a.length)return null;const u=e=>{const i=1-e.errors/r.length,a=o.prefix?n(t.slice(Math.max(0,e.start-o.prefix.length),e.start),o.prefix):1,u=o.suffix?n(t.slice(e.end,e.end+o.suffix.length),o.suffix):1;let c=1;return"number"==typeof o.hint&&(c=1-Math.abs(e.start-o.hint)/t.length),(50*i+20*a+20*u+2*c)/92},c=a.map((t=>({start:t.start,end:t.end,score:u(t)})));return c.sort(((t,e)=>e.score-t.score)),c[0]}(this.root.textContent,this.exact,{...this.context,hint:t.hint});if(!r)throw new Error("Quote not found");return new s(this.root,r.start,r.end)}}var f=r(3732);r.n(f)().shim();const p=!0;function y(){if(!readium.link)return null;const t=readium.link.href;if(!t)return null;const e=function(){const t=window.getSelection();if(!t)return;if(t.isCollapsed)return;const e=t.toString();if(0===e.trim().replace(/\n/g," ").replace(/\s\s+/g," ").length)return;if(!t.anchorNode||!t.focusNode)return;const r=1===t.rangeCount?t.getRangeAt(0):function(t,e,r,n){const o=new Range;if(o.setStart(t,e),o.setEnd(r,n),!o.collapsed)return o;d(">>> createOrderedRange COLLAPSED ... RANGE REVERSE?");const i=new Range;if(i.setStart(r,n),i.setEnd(t,e),!i.collapsed)return d(">>> createOrderedRange RANGE REVERSE OK."),o;d(">>> createOrderedRange RANGE REVERSE ALSO COLLAPSED?!")}(t.anchorNode,t.anchorOffset,t.focusNode,t.focusOffset);if(!r||r.collapsed)return void d("$$$$$$$$$$$$$$$$$ CANNOT GET NON-COLLAPSED SELECTION RANGE?!");const n=document.body.textContent,o=c.fromRange(r).relativeTo(document.body),i=o.start.offset,a=o.end.offset;let u=n.slice(Math.max(0,i-200),i),s=u.search(/\P{L}\p{L}/gu);-1!==s&&(u=u.slice(s+1));let l=n.slice(a,Math.min(n.length,a+200)),f=Array.from(l.matchAll(/\p{L}\P{L}/gu)).pop();return void 0!==f&&f.index>1&&(l=l.slice(0,f.index+1)),{highlight:e,before:u,after:l}}();return e?{href:t,text:e,rect:function(){try{let t=window.getSelection();if(!t)return;return k(t.getRangeAt(0).getBoundingClientRect())}catch(t){return M(t),null}}()}:null}function d(){p&&I.apply(null,arguments)}var h;window.addEventListener("error",(function(t){webkit.messageHandlers.logError.postMessage({message:t.message,filename:t.filename,line:t.lineno})}),!1),window.addEventListener("load",(function(){var t;new ResizeObserver((()=>{t&&window.cancelAnimationFrame(t),t=window.requestAnimationFrame((function(){v=window.innerWidth,function(){const t="readium-virtual-page";var e=document.getElementById(t);if(x()||2!=parseInt(window.getComputedStyle(document.documentElement).getPropertyValue("column-count"))){var r;null===(r=e)||void 0===r||r.remove()}else{var n=document.scrollingElement.scrollWidth/window.innerWidth;Math.round(2*n)/2%1>.1&&(e?e.remove():((e=document.createElement("div")).setAttribute("id",t),e.style.breakBefore="column",e.innerHTML="​",document.body.appendChild(e)))}}(),function(){if(!x()){var t=T(window.scrollX+1);document.scrollingElement.scrollLeft=t}}(),w()}))})).observe(document.body)}),!1);var g,m,b=!1,v=0;function w(){if(readium.isFixedLayout)return;let t=document.scrollingElement;if(x()&&!S()){const e=window.scrollY,r=window.innerHeight,n=t.scrollHeight;h={first:e/n,last:(e+r)/n}}else{let e=window.scrollX;const r=window.innerWidth,n=t.scrollWidth;E()&&(e=Math.abs(e)),h={first:e/n,last:(e+r)/n}}0!==t.scrollWidth&&0!==t.scrollHeight&&(b||window.requestAnimationFrame((function(){var t;t=h,webkit.messageHandlers.progressionChanged.postMessage(t),b=!1})),b=!0)}function x(){return"readium-scroll-on"==document.documentElement.style.getPropertyValue("--USER__view").trim()}function S(){return window.getComputedStyle(document.documentElement).getPropertyValue("writing-mode").startsWith("vertical")}function E(){const t=window.getComputedStyle(document.documentElement);return"rtl"==t.getPropertyValue("direction")||"vertical-rl"==t.getPropertyValue("writing-mode")}function A(t,e){return x()?j({top:t.top+window.scrollY,animated:e}):j({left:T(t.left+window.scrollX),animated:e}),!0}function O(t,e){var r=window.scrollX,n=window.innerWidth,o=Math.abs(r-t)/n>.01;return o&&j({left:t,animated:e}),o}function j(){let{left:t,top:e,animated:r}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};document.scrollingElement.scrollTo({left:t,top:e,behavior:r?"smooth":"instant"})}function T(t){const e=t+(E()?-1:1);return e-e%v}function P(t){try{let n=t.locations,o=t.text;var e;if(o&&o.highlight)return n&&n.cssSelector&&(e=document.querySelector(n.cssSelector)),e||(e=document.body),new l(e,o.highlight,{prefix:o.before,suffix:o.after}).toRange();if(n){var r=null;if(!r&&n.cssSelector&&(r=document.querySelector(n.cssSelector)),!r&&n.fragments)for(const t of n.fragments)if(r=document.getElementById(t))break;if(r){let t=document.createRange();return t.setStartBefore(r),t.setEndAfter(r),t}}}catch(t){M(t)}return null}function R(t,e){null===e?C(t):document.documentElement.style.setProperty(t,e,"important")}function C(t){document.documentElement.style.removeProperty(t)}function I(){var t=Array.prototype.slice.call(arguments).join(" ");webkit.messageHandlers.log.postMessage(t)}function N(t){M(new Error(t))}function M(t){webkit.messageHandlers.logError.postMessage({message:t.message})}window.addEventListener("scroll",w),document.addEventListener("selectionchange",(50,g=function(){webkit.messageHandlers.selectionChanged.postMessage(y())},function(){var t=this,e=arguments;clearTimeout(m),m=setTimeout((function(){g.apply(t,e),m=null}),50)}));const $=!1;function k(t){let e=D({x:t.left,y:t.top});const r=t.width,n=t.height,o=e.x,i=e.y;return{width:r,height:n,left:o,top:i,right:o+r,bottom:i+n}}function D(t){if(!frameElement)return t;let e=frameElement.getBoundingClientRect();if(!e)return t;let r=window.top.document.documentElement;return{x:t.x+e.x+r.scrollLeft,y:t.y+e.y+r.scrollTop}}function F(t,e){let r=t.getClientRects();const n=[];for(const t of r)n.push({bottom:t.bottom,height:t.height,left:t.left,right:t.right,top:t.top,width:t.width});const o=U(function(t,e){const r=new Set(t);for(const e of t)if(e.width>1&&e.height>1){for(const n of t)if(e!==n&&r.has(n)&&_(n,e,1)){V("CLIENT RECT: remove contained"),r.delete(e);break}}else V("CLIENT RECT: remove tiny"),r.delete(e);return Array.from(r)}(L(n,1,e)));for(let t=o.length-1;t>=0;t--){const e=o[t];if(!(e.width*e.height>4)){if(!(o.length>1)){V("CLIENT RECT: remove small, but keep otherwise empty!");break}V("CLIENT RECT: remove small"),o.splice(t,1)}}return V(`CLIENT RECT: reduced ${n.length} --\x3e ${o.length}`),o}function L(t,e,r){for(let n=0;nt!==i&&t!==a)),o=B(i,a);return n.push(o),L(n,e,r)}}return t}function B(t,e){const r=Math.min(t.left,e.left),n=Math.max(t.right,e.right),o=Math.min(t.top,e.top),i=Math.max(t.bottom,e.bottom);return{bottom:i,height:i-o,left:r,right:n,top:o,width:n-r}}function _(t,e,r){return W(t,e.left,e.top,r)&&W(t,e.right,e.top,r)&&W(t,e.left,e.bottom,r)&&W(t,e.right,e.bottom,r)}function W(t,e,r,n){return(t.lefte||G(t.right,e,n))&&(t.topr||G(t.bottom,r,n))}function U(t){for(let e=0;et!==e));return Array.prototype.push.apply(a,r),U(a)}}else V("replaceOverlapingRects rect1 === rect2 ??!")}return t}function z(t,e){const r=function(t,e){const r=Math.max(t.left,e.left),n=Math.min(t.right,e.right),o=Math.max(t.top,e.top),i=Math.min(t.bottom,e.bottom);return{bottom:i,height:Math.max(0,i-o),left:r,right:n,top:o,width:Math.max(0,n-r)}}(e,t);if(0===r.height||0===r.width)return[t];const n=[];{const e={bottom:t.bottom,height:0,left:t.left,right:r.left,top:t.top,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}{const e={bottom:r.top,height:0,left:r.left,right:r.right,top:t.top,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}{const e={bottom:t.bottom,height:0,left:r.left,right:r.right,top:r.bottom,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}{const e={bottom:t.bottom,height:0,left:r.right,right:t.right,top:t.top,width:0};e.width=e.right-e.left,e.height=e.bottom-e.top,0!==e.height&&0!==e.width&&n.push(e)}return n}function H(t,e,r){return(t.left=0&&G(t.left,e.right,r))&&(e.left=0&&G(e.left,t.right,r))&&(t.top=0&&G(t.top,e.bottom,r))&&(e.top=0&&G(e.top,t.bottom,r))}function G(t,e,r){return Math.abs(t-e)<=r}function V(){$&&I.apply(null,arguments)}var q,X=[],K="ResizeObserver loop completed with undelivered notifications.";!function(t){t.BORDER_BOX="border-box",t.CONTENT_BOX="content-box",t.DEVICE_PIXEL_CONTENT_BOX="device-pixel-content-box"}(q||(q={}));var Y,J=function(t){return Object.freeze(t)},Q=function(t,e){this.inlineSize=t,this.blockSize=e,J(this)},Z=function(){function t(t,e,r,n){return this.x=t,this.y=e,this.width=r,this.height=n,this.top=this.y,this.left=this.x,this.bottom=this.top+this.height,this.right=this.left+this.width,J(this)}return t.prototype.toJSON=function(){var t=this;return{x:t.x,y:t.y,top:t.top,right:t.right,bottom:t.bottom,left:t.left,width:t.width,height:t.height}},t.fromRect=function(e){return new t(e.x,e.y,e.width,e.height)},t}(),tt=function(t){return t instanceof SVGElement&&"getBBox"in t},et=function(t){if(tt(t)){var e=t.getBBox(),r=e.width,n=e.height;return!r&&!n}var o=t,i=o.offsetWidth,a=o.offsetHeight;return!(i||a||t.getClientRects().length)},rt=function(t){var e;if(t instanceof Element)return!0;var r=null===(e=null==t?void 0:t.ownerDocument)||void 0===e?void 0:e.defaultView;return!!(r&&t instanceof r.Element)},nt="undefined"!=typeof window?window:{},ot=new WeakMap,it=/auto|scroll/,at=/^tb|vertical/,ut=/msie|trident/i.test(nt.navigator&&nt.navigator.userAgent),ct=function(t){return parseFloat(t||"0")},st=function(t,e,r){return void 0===t&&(t=0),void 0===e&&(e=0),void 0===r&&(r=!1),new Q((r?e:t)||0,(r?t:e)||0)},lt=J({devicePixelContentBoxSize:st(),borderBoxSize:st(),contentBoxSize:st(),contentRect:new Z(0,0,0,0)}),ft=function(t,e){if(void 0===e&&(e=!1),ot.has(t)&&!e)return ot.get(t);if(et(t))return ot.set(t,lt),lt;var r=getComputedStyle(t),n=tt(t)&&t.ownerSVGElement&&t.getBBox(),o=!ut&&"border-box"===r.boxSizing,i=at.test(r.writingMode||""),a=!n&&it.test(r.overflowY||""),u=!n&&it.test(r.overflowX||""),c=n?0:ct(r.paddingTop),s=n?0:ct(r.paddingRight),l=n?0:ct(r.paddingBottom),f=n?0:ct(r.paddingLeft),p=n?0:ct(r.borderTopWidth),y=n?0:ct(r.borderRightWidth),d=n?0:ct(r.borderBottomWidth),h=f+s,g=c+l,m=(n?0:ct(r.borderLeftWidth))+y,b=p+d,v=u?t.offsetHeight-b-t.clientHeight:0,w=a?t.offsetWidth-m-t.clientWidth:0,x=o?h+m:0,S=o?g+b:0,E=n?n.width:ct(r.width)-x-w,A=n?n.height:ct(r.height)-S-v,O=E+h+w+m,j=A+g+v+b,T=J({devicePixelContentBoxSize:st(Math.round(E*devicePixelRatio),Math.round(A*devicePixelRatio),i),borderBoxSize:st(O,j,i),contentBoxSize:st(E,A,i),contentRect:new Z(f,c,E,A)});return ot.set(t,T),T},pt=function(t,e,r){var n=ft(t,r),o=n.borderBoxSize,i=n.contentBoxSize,a=n.devicePixelContentBoxSize;switch(e){case q.DEVICE_PIXEL_CONTENT_BOX:return a;case q.BORDER_BOX:return o;default:return i}},yt=function(t){var e=ft(t);this.target=t,this.contentRect=e.contentRect,this.borderBoxSize=J([e.borderBoxSize]),this.contentBoxSize=J([e.contentBoxSize]),this.devicePixelContentBoxSize=J([e.devicePixelContentBoxSize])},dt=function(t){if(et(t))return 1/0;for(var e=0,r=t.parentNode;r;)e+=1,r=r.parentNode;return e},ht=function(){var t=1/0,e=[];X.forEach((function(r){if(0!==r.activeTargets.length){var n=[];r.activeTargets.forEach((function(e){var r=new yt(e.target),o=dt(e.target);n.push(r),e.lastReportedSize=pt(e.target,e.observedBox),ot?e.activeTargets.push(r):e.skippedTargets.push(r))}))}))},mt=[],bt=0,vt={attributes:!0,characterData:!0,childList:!0,subtree:!0},wt=["resize","load","transitionend","animationend","animationstart","animationiteration","keyup","keydown","mouseup","mousedown","mouseover","mouseout","blur","focus"],xt=function(t){return void 0===t&&(t=0),Date.now()+t},St=!1,Et=function(){function t(){var t=this;this.stopped=!0,this.listener=function(){return t.schedule()}}return t.prototype.run=function(t){var e=this;if(void 0===t&&(t=250),!St){St=!0;var r,n=xt(t);r=function(){var r=!1;try{r=function(){var t,e=0;for(gt(e);X.some((function(t){return t.activeTargets.length>0}));)e=ht(),gt(e);return X.some((function(t){return t.skippedTargets.length>0}))&&("function"==typeof ErrorEvent?t=new ErrorEvent("error",{message:K}):((t=document.createEvent("Event")).initEvent("error",!1,!1),t.message=K),window.dispatchEvent(t)),e>0}()}finally{if(St=!1,t=n-xt(),!bt)return;r?e.run(1e3):t>0?e.run(t):e.start()}},function(t){if(!Y){var e=0,r=document.createTextNode("");new MutationObserver((function(){return mt.splice(0).forEach((function(t){return t()}))})).observe(r,{characterData:!0}),Y=function(){r.textContent="".concat(e?e--:e++)}}mt.push(t),Y()}((function(){requestAnimationFrame(r)}))}},t.prototype.schedule=function(){this.stop(),this.run()},t.prototype.observe=function(){var t=this,e=function(){return t.observer&&t.observer.observe(document.body,vt)};document.body?e():nt.addEventListener("DOMContentLoaded",e)},t.prototype.start=function(){var t=this;this.stopped&&(this.stopped=!1,this.observer=new MutationObserver(this.listener),this.observe(),wt.forEach((function(e){return nt.addEventListener(e,t.listener,!0)})))},t.prototype.stop=function(){var t=this;this.stopped||(this.observer&&this.observer.disconnect(),wt.forEach((function(e){return nt.removeEventListener(e,t.listener,!0)})),this.stopped=!0)},t}(),At=new Et,Ot=function(t){!bt&&t>0&&At.start(),!(bt+=t)&&At.stop()},jt=function(){function t(t,e){this.target=t,this.observedBox=e||q.CONTENT_BOX,this.lastReportedSize={inlineSize:0,blockSize:0}}return t.prototype.isActive=function(){var t,e=pt(this.target,this.observedBox,!0);return t=this.target,tt(t)||function(t){switch(t.tagName){case"INPUT":if("image"!==t.type)break;case"VIDEO":case"AUDIO":case"EMBED":case"OBJECT":case"CANVAS":case"IFRAME":case"IMG":return!0}return!1}(t)||"inline"!==getComputedStyle(t).display||(this.lastReportedSize=e),this.lastReportedSize.inlineSize!==e.inlineSize||this.lastReportedSize.blockSize!==e.blockSize},t}(),Tt=function(t,e){this.activeTargets=[],this.skippedTargets=[],this.observationTargets=[],this.observer=t,this.callback=e},Pt=new WeakMap,Rt=function(t,e){for(var r=0;r=0&&(o&&X.splice(X.indexOf(r),1),r.observationTargets.splice(n,1),Ot(-1))},t.disconnect=function(t){var e=this,r=Pt.get(t);r.observationTargets.slice().forEach((function(r){return e.unobserve(t,r.target)})),r.activeTargets.splice(0,r.activeTargets.length)},t}(),It=function(){function t(t){if(0===arguments.length)throw new TypeError("Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.");if("function"!=typeof t)throw new TypeError("Failed to construct 'ResizeObserver': The callback provided as parameter 1 is not a function.");Ct.connect(this,t)}return t.prototype.observe=function(t,e){if(0===arguments.length)throw new TypeError("Failed to execute 'observe' on 'ResizeObserver': 1 argument required, but only 0 present.");if(!rt(t))throw new TypeError("Failed to execute 'observe' on 'ResizeObserver': parameter 1 is not of type 'Element");Ct.observe(this,t,e)},t.prototype.unobserve=function(t){if(0===arguments.length)throw new TypeError("Failed to execute 'unobserve' on 'ResizeObserver': 1 argument required, but only 0 present.");if(!rt(t))throw new TypeError("Failed to execute 'unobserve' on 'ResizeObserver': parameter 1 is not of type 'Element");Ct.unobserve(this,t)},t.prototype.disconnect=function(){Ct.disconnect(this)},t.toString=function(){return"function ResizeObserver () { [polyfill code] }"},t}();const Nt=window.ResizeObserver||It;let Mt=new Map,$t=new Map;var kt=0;function Dt(t){if(0===$t.size)return null;for(const[e,r]of $t)if(r.isActivable())for(const n of r.items.reverse())if(n.clickableElements)for(const r of n.clickableElements){let o=r.getBoundingClientRect().toJSON();if(W(o,t.clientX,t.clientY,1))return{group:e,item:n,element:r,rect:o}}return null}function Ft(t){return t&&t instanceof Element}window.addEventListener("load",(function(){const t=document.body;var e={width:0,height:0};new Nt((()=>{e.width===t.clientWidth&&e.height===t.clientHeight||(e={width:t.clientWidth,height:t.clientHeight},$t.forEach((function(t){t.requestLayout()})))})).observe(t)}),!1);const Lt={NONE:"",DESCENDANT:" ",CHILD:" > "},Bt={id:"id",class:"class",tag:"tag",attribute:"attribute",nthchild:"nthchild",nthoftype:"nthoftype"},_t="CssSelectorGenerator";function Wt(t="unknown problem",...e){console.warn(`${_t}: ${t}`,...e)}const Ut={selectors:[Bt.id,Bt.class,Bt.tag,Bt.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function zt(t){return t instanceof RegExp}function Ht(t){return["string","function"].includes(typeof t)||zt(t)}function Gt(t){return Array.isArray(t)?t.filter(Ht):[]}function Vt(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 qt(t,e){if(Vt(t))return t.contains(e)||Wt("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 r=e.getRootNode({composed:!1});return Vt(r)?(r!==document&&Wt("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."),r):e.ownerDocument.querySelector(":root")}function Xt(t){return"number"==typeof t?t:Number.POSITIVE_INFINITY}function Kt(t=[]){const[e=[],...r]=t;return 0===r.length?e:r.reduce(((t,e)=>t.filter((t=>e.includes(t)))),e)}function Yt(t){return[].concat(...t)}function Jt(t){const e=t.map((t=>{if(zt(t))return e=>t.test(e);if("function"==typeof t)return e=>{const r=t(e);return"boolean"!=typeof r?(Wt("pattern matcher function invalid","Provided pattern matching function does not return boolean. It's result will be ignored.",t),!1):r};if("string"==typeof t){const e=new RegExp("^"+t.replace(/[|\\{}()[\]^$+?.]/g,"\\$&").replace(/\*/g,".+")+"$");return t=>e.test(t)}return Wt("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 Qt(t,e,r){const n=Array.from(qt(r,t[0]).querySelectorAll(e));return n.length===t.length&&t.every((t=>n.includes(t)))}function Zt(t,e){e=null!=e?e:function(t){return t.ownerDocument.querySelector(":root")}(t);const r=[];let n=t;for(;Ft(n)&&n!==e;)r.push(n),n=n.parentElement;return r}function te(t,e){return Kt(t.map((t=>Zt(t,e))))}const ee=new RegExp(["^$","\\s"].join("|")),re=new RegExp(["^$"].join("|")),ne=[Bt.nthoftype,Bt.tag,Bt.id,Bt.class,Bt.attribute,Bt.nthchild],oe=Jt(["class","id","ng-*"]);function ie({name:t}){return`[${t}]`}function ae({name:t,value:e}){return`[${t}='${e}']`}function ue({nodeName:t,nodeValue:e}){return{name:(r=t,r.replace(/:/g,"\\:")),value:we(e)};var r}function ce(t){const e=Array.from(t.attributes).filter((e=>function({nodeName:t},e){const r=e.tagName.toLowerCase();return!(["input","option"].includes(r)&&"value"===t||oe(t))}(e,t))).map(ue);return[...e.map(ie),...e.map(ae)]}function se(t){return(t.getAttribute("class")||"").trim().split(/\s+/).filter((t=>!re.test(t))).map((t=>`.${we(t)}`))}function le(t){const e=t.getAttribute("id")||"",r=`#${we(e)}`,n=t.getRootNode({composed:!1});return!ee.test(e)&&Qt([t],r,n)?[r]:[]}function fe(t){const e=t.parentNode;if(e){const r=Array.from(e.childNodes).filter(Ft).indexOf(t);if(r>-1)return[`:nth-child(${r+1})`]}return[]}function pe(t){return[we(t.tagName.toLowerCase())]}function ye(t){const e=[...new Set(Yt(t.map(pe)))];return 0===e.length||e.length>1?[]:[e[0]]}function de(t){const e=ye([t])[0],r=t.parentElement;if(r){const n=Array.from(r.children).filter((t=>t.tagName.toLowerCase()===e)),o=n.indexOf(t);if(o>-1)return[`${e}:nth-of-type(${o+1})`]}return[]}function he(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){return Array.from(function*(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){let r=0,n=me(1);for(;n.length<=t.length&&rt[e]));yield e,n=ge(n,t.length-1)}}(t,{maxResults:e}))}function ge(t=[],e=0){const r=t.length;if(0===r)return[];const n=[...t];n[r-1]+=1;for(let t=r-1;t>=0;t--)if(n[t]>e){if(0===t)return me(r+1);n[t-1]++,n[t]=n[t-1]+1}return n[r-1]>e?me(r+1):n}function me(t=1){return Array.from(Array(t).keys())}const be=":".charCodeAt(0).toString(16).toUpperCase(),ve=/[ !"#$%&'()\[\]{|}<>*+,./;=?@^`~\\]/;function we(t=""){var e,r;return null!==(r=null===(e=null===CSS||void 0===CSS?void 0:CSS.escape)||void 0===e?void 0:e.call(CSS,t))&&void 0!==r?r:function(t=""){return t.split("").map((t=>":"===t?`\\${be} `:ve.test(t)?`\\${t}`:escape(t).replace(/%/g,"\\"))).join("")}(t)}const xe={tag:ye,id:function(t){return 0===t.length||t.length>1?[]:le(t[0])},class:function(t){return Kt(t.map(se))},attribute:function(t){return Kt(t.map(ce))},nthchild:function(t){return Kt(t.map(fe))},nthoftype:function(t){return Kt(t.map(de))}},Se={tag:pe,id:le,class:se,attribute:ce,nthchild:fe,nthoftype:de};function Ee(t){return t.includes(Bt.tag)||t.includes(Bt.nthoftype)?[...t]:[...t,Bt.tag]}function Ae(t={}){const e=[...ne];return t[Bt.tag]&&t[Bt.nthoftype]&&e.splice(e.indexOf(Bt.tag),1),e.map((e=>{return(n=t)[r=e]?n[r].join(""):"";var r,n})).join("")}function Oe(t,e,r="",n){const o=function(t,e){return""===e?t:function(t,e){return[...t.map((t=>e+Lt.DESCENDANT+t)),...t.map((t=>e+Lt.CHILD+t))]}(t,e)}(function(t,e,r){const n=function(t,e){const{blacklist:r,whitelist:n,combineWithinSelector:o,maxCombinations:i}=e,a=Jt(r),u=Jt(n);return function(t){const{selectors:e,includeTag:r}=t,n=[].concat(e);return r&&!n.includes("tag")&&n.push("tag"),n}(e).reduce(((e,r)=>{const n=function(t,e){var r;return(null!==(r=xe[e])&&void 0!==r?r:()=>[])(t)}(t,r),c=function(t=[],e,r){return t.filter((t=>r(t)||!e(t)))}(n,a,u),s=function(t=[],e){return t.sort(((t,r)=>{const n=e(t),o=e(r);return n&&!o?-1:!n&&o?1:0}))}(c,u);return e[r]=o?he(s,{maxResults:i}):s.map((t=>[t])),e}),{})}(t,r),o=function(t,e){return function(t){const{selectors:e,combineBetweenSelectors:r,includeTag:n,maxCandidates:o}=t,i=r?he(e,{maxResults:o}):e.map((t=>[t]));return n?i.map(Ee):i}(e).map((e=>function(t,e){const r={};return t.forEach((t=>{const n=e[t];n.length>0&&(r[t]=n)})),function(t={}){let e=[];return Object.entries(t).forEach((([t,r])=>{e=r.flatMap((r=>0===e.length?[{[t]:r}]:e.map((e=>Object.assign(Object.assign({},e),{[t]:r})))))})),e}(r).map(Ae)}(e,t))).filter((t=>t.length>0))}(n,r),i=Yt(o);return[...new Set(i)]}(t,n.root,n),r);for(const e of o)if(Qt(t,e,n.root))return e;return null}function je(t){return{value:t,include:!1}}function Te({selectors:t,operator:e}){let r=[...ne];t[Bt.tag]&&t[Bt.nthoftype]&&(r=r.filter((t=>t!==Bt.tag)));let n="";return r.forEach((e=>{(t[e]||[]).forEach((({value:t,include:e})=>{e&&(n+=t)}))})),e+n}function Pe(t){return[":root",...Zt(t).reverse().map((t=>{const e=function(t,e,r=Lt.NONE){const n={};return e.forEach((e=>{Reflect.set(n,e,function(t,e){return Se[e](t)}(t,e).map(je))})),{element:t,operator:r,selectors:n}}(t,[Bt.nthchild],Lt.CHILD);return e.selectors.nthchild.forEach((t=>{t.include=!0})),e})).map(Te)].join("")}function Re(t,e={}){const r=function(t){(t instanceof NodeList||t instanceof HTMLCollection)&&(t=Array.from(t));const e=(Array.isArray(t)?t:[t]).filter(Ft);return[...new Set(e)]}(t),n=function(t,e={}){const r=Object.assign(Object.assign({},Ut),e);return{selectors:(n=r.selectors,Array.isArray(n)?n.filter((t=>{return e=Bt,r=t,Object.values(e).includes(r);var e,r})):[]),whitelist:Gt(r.whitelist),blacklist:Gt(r.blacklist),root:qt(r.root,t),combineWithinSelector:!!r.combineWithinSelector,combineBetweenSelectors:!!r.combineBetweenSelectors,includeTag:!!r.includeTag,maxCombinations:Xt(r.maxCombinations),maxCandidates:Xt(r.maxCandidates)};var n}(r[0],e);let o="",i=n.root;function a(){return function(t,e,r="",n){if(0===t.length)return null;const o=[t.length>1?t:[],...te(t,e).map((t=>[t]))];for(const t of o){const e=Oe(t,0,r,n);if(e)return{foundElements:t,selector:e}}return null}(r,i,o,n)}let u=a();for(;u;){const{foundElements:t,selector:e}=u;if(Qt(r,e,n.root))return e;i=t[0],o=e,u=a()}return r.length>1?r.map((t=>Re(t,n))).join(", "):function(t){return t.map(Pe).join(", ")}(r)}function Ce(t){return null==t?null:-1!==["a","audio","button","canvas","details","input","label","option","select","submit","textarea","video"].indexOf(t.nodeName.toLowerCase())||t.hasAttribute("contenteditable")&&"false"!=t.getAttribute("contenteditable").toLowerCase()?t.outerHTML:t.parentElement?Ce(t.parentElement):null}function Ie(t){for(var e=0;e0&&e.top0&&e.left{We(t)||(Ue(t),ze("down",t))})),window.addEventListener("keyup",(t=>{We(t)||(Ue(t),ze("up",t))})),r.g.readium={scrollToId:function(t,e){let r=document.getElementById(t);return!!r&&(A(r.getBoundingClientRect(),e),!0)},scrollToPosition:function(t,e,r){t<0||t>1?console.error(`Expected a valid progression in scrollToPosition, got ${t}`):x()?S()?j({left:-document.scrollingElement.scrollWidth*t,animated:r}):j({top:document.scrollingElement.scrollHeight*t,animated:r}):j({left:T(document.scrollingElement.scrollWidth*t*("rtl"==e?-1:1)),animated:r})},scrollToLocator:function(t,e){let r=P(t);return!!r&&function(t,e){return A(t.getBoundingClientRect(),e)}(r,e)},scrollLeft:function(t,e){var r="rtl"==t,n=document.scrollingElement.scrollWidth,o=window.innerWidth,i=window.scrollX-o,a=r?-(n-o):0;return O(Math.max(i,a),e)},scrollRight:function(t,e){var r="rtl"==t,n=document.scrollingElement.scrollWidth,o=window.innerWidth,i=window.scrollX+o,a=r?0:n-o;return O(Math.min(i,a),e)},setCSSProperties:function(t){for(const e in t)R(e,t[e])},setProperty:R,removeProperty:C,registerDecorationTemplates:function(t){var e="";for(const[r,n]of Object.entries(t))Mt.set(r,n),n.stylesheet&&(e+=n.stylesheet+"\n");if(e){let t=document.createElement("style");t.innerHTML=e,document.getElementsByTagName("head")[0].appendChild(t)}},getDecorations:function(t){var e=$t.get(t);return e||(e=function(t,e){var r=[],n=0,o=null,i=!1;function a(e){let o=t+"-"+n++,i=P(e.locator);if(!i)return void I("Can't locate DOM range for decoration",e);let a={id:o,decoration:e,range:i};r.push(a),c(a)}function u(t){let e=r.findIndex((e=>e.decoration.id===t));if(-1===e)return;let n=r[e];r.splice(e,1),n.clickableElements=null,n.container&&(n.container.remove(),n.container=null)}function c(r){let n=(o||((o=document.createElement("div")).id=t,o.dataset.group=e,o.style.pointerEvents="none",requestAnimationFrame((function(){null!=o&&document.body.append(o)}))),o),i=Mt.get(r.decoration.style);if(!i)return void N(`Unknown decoration style: ${r.decoration.style}`);let a=document.createElement("div");a.id=r.id,a.dataset.style=r.decoration.style,a.style.pointerEvents="none";const u=getComputedStyle(document.body).writingMode,c="vertical-rl"===u||"vertical-lr"===u,s=document.scrollingElement,{scrollLeft:l,scrollTop:f}=s,p=c?window.innerHeight:window.innerWidth,y=c?window.innerWidth:window.innerHeight,d=parseInt(getComputedStyle(document.documentElement).getPropertyValue("column-count"))||1,h=(c?y:p)/d;function g(t,e,r,n){t.style.position="absolute";const o="vertical-rl"===n;if(o||"vertical-lr"===n){if("wrap"===i.width)t.style.width=`${e.width}px`,t.style.height=`${e.height}px`,o?t.style.right=`${-e.right-l+s.clientWidth}px`:t.style.left=`${e.left+l}px`,t.style.top=`${e.top+f}px`;else if("viewport"===i.width){t.style.width=`${e.height}px`,t.style.height=`${p}px`;const r=Math.floor(e.top/p)*p;o?t.style.right=-e.right-l+"px":t.style.left=`${e.left+l}px`,t.style.top=`${r+f}px`}else if("bounds"===i.width)t.style.width=`${r.height}px`,t.style.height=`${p}px`,o?t.style.right=`${-r.right-l+s.clientWidth}px`:t.style.left=`${r.left+l}px`,t.style.top=`${r.top+f}px`;else if("page"===i.width){t.style.width=`${e.height}px`,t.style.height=`${h}px`;const r=Math.floor(e.top/h)*h;o?t.style.right=`${-e.right-l+s.clientWidth}px`:t.style.left=`${e.left+l}px`,t.style.top=`${r+f}px`}}else if("wrap"===i.width)t.style.width=`${e.width}px`,t.style.height=`${e.height}px`,t.style.left=`${e.left+l}px`,t.style.top=`${e.top+f}px`;else if("viewport"===i.width){t.style.width=`${p}px`,t.style.height=`${e.height}px`;const r=Math.floor(e.left/p)*p;t.style.left=`${r+l}px`,t.style.top=`${e.top+f}px`}else if("bounds"===i.width)t.style.width=`${r.width}px`,t.style.height=`${e.height}px`,t.style.left=`${r.left+l}px`,t.style.top=`${e.top+f}px`;else if("page"===i.width){t.style.width=`${h}px`,t.style.height=`${e.height}px`;const r=Math.floor(e.left/h)*h;t.style.left=`${r+l}px`,t.style.top=`${e.top+f}px`}}let m,b=r.range.getBoundingClientRect();try{let t=document.createElement("template");t.innerHTML=r.decoration.element.trim(),m=t.content.firstElementChild}catch(t){return void N(`Invalid decoration element "${r.decoration.element}": ${t.message}`)}if("boxes"===i.layout){const t=!u.startsWith("vertical"),e=(v=r.range.startContainer).nodeType===Node.ELEMENT_NODE?v:v.parentElement,n=getComputedStyle(e).writingMode,o=F(r.range,t).sort(((t,e)=>t.top!==e.top?t.top-e.top:"vertical-rl"===n?e.left-t.left:t.left-e.left));for(let t of o){const e=m.cloneNode(!0);e.style.pointerEvents="none",e.dataset.writingMode=n,g(e,t,b,u),a.append(e)}}else if("bounds"===i.layout){const t=m.cloneNode(!0);t.style.pointerEvents="none",t.dataset.writingMode=u,g(t,b,b,u),a.append(t)}var v;n.append(a),r.container=a,r.clickableElements=Array.from(a.querySelectorAll("[data-activable='1']")),0===r.clickableElements.length&&(r.clickableElements=Array.from(a.children))}function s(){o&&(o.remove(),o=null)}return{add:a,remove:u,update:function(t){u(t.id),a(t)},clear:function(){s(),r.length=0},items:r,requestLayout:function(){s(),r.forEach((t=>c(t)))},isActivable:function(){return i},setActivable:function(){i=!0}}}("r2-decoration-"+kt++,t),$t.set(t,e)),e},findFirstVisibleLocator:function(){const t=Ie(document.body);return{href:"#",type:"application/xhtml+xml",locations:{cssSelector:Re(t)},text:{highlight:t.textContent}}}},window.readium.isReflowable=!0,webkit.messageHandlers.spreadLoadStarted.postMessage({}),window.addEventListener("load",(function(){window.requestAnimationFrame((function(){webkit.messageHandlers.spreadLoaded.postMessage({})}));let t=document.createElement("meta");t.setAttribute("name","viewport"),t.setAttribute("content","width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no"),document.head.appendChild(t)}))})()})(); //# sourceMappingURL=readium-reflowable.js.map \ No newline at end of file diff --git a/Sources/Navigator/EPUB/Assets/fxl-spread-two.html b/Sources/Navigator/EPUB/Assets/fxl-spread-two.html index 466977e6a8..07768fe23e 100644 --- a/Sources/Navigator/EPUB/Assets/fxl-spread-two.html +++ b/Sources/Navigator/EPUB/Assets/fxl-spread-two.html @@ -26,8 +26,8 @@ } #viewport-center { - left: 50%; - transform: translateX(-50%); + width: 100%; + left: 0; } .page { diff --git a/Sources/Navigator/EPUB/CSS/CSSLayout.swift b/Sources/Navigator/EPUB/CSS/CSSLayout.swift index 138c691bb6..60a1c1a33f 100644 --- a/Sources/Navigator/EPUB/CSS/CSSLayout.swift +++ b/Sources/Navigator/EPUB/CSS/CSSLayout.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/EPUB/CSS/CSSProperties.swift b/Sources/Navigator/EPUB/CSS/CSSProperties.swift index 9525680574..3881ac6711 100644 --- a/Sources/Navigator/EPUB/CSS/CSSProperties.swift +++ b/Sources/Navigator/EPUB/CSS/CSSProperties.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -136,7 +136,7 @@ public struct CSSUserProperties: CSSProperties { /// It impacts font style, weight and variant, text decoration, super and subscripts. public var a11yNormalize: Bool? - // Additional overrides for extensions and adjustments. + /// Additional overrides for extensions and adjustments. public var overrides: [String: String?] public init( @@ -359,7 +359,7 @@ public struct CSSRSProperties: CSSProperties { /// The value can be another variable e.g. var(-RS__monospaceTf). public var codeFontFamily: [String]? - // Additional overrides for extensions and adjustments. + /// Additional overrides for extensions and adjustments. public var overrides: [String: String?] public init( @@ -496,7 +496,9 @@ public enum CSSView: String, CSSConvertible { case paged = "readium-paged-on" case scroll = "readium-scroll-on" - public func css() -> String? { rawValue } + public func css() -> String? { + rawValue + } } @available(*, unavailable, message: "Column count is now an integer") @@ -505,14 +507,18 @@ public enum CSSColCount: String, CSSConvertible { case one = "1" case two = "2" - public func css() -> String? { rawValue } + public func css() -> String? { + rawValue + } } public enum CSSAppearance: String, CSSConvertible { case night = "readium-night-on" case sepia = "readium-sepia-on" - public func css() -> String? { rawValue } + public func css() -> String? { + rawValue + } } public struct CSSPercent: CSSConvertible { @@ -522,7 +528,9 @@ public struct CSSPercent: CSSConvertible { self.value = value } - public func css() -> String? { (value * 100).css(unit: "%") } + public func css() -> String? { + (value * 100).css(unit: "%") + } } public protocol CSSColor: CSSConvertible {} @@ -553,7 +561,9 @@ public struct CSSHexColor: CSSColor { self.color = color } - public func css() -> String? { color } + public func css() -> String? { + color + } } public struct CSSIntColor: CSSColor { @@ -580,7 +590,9 @@ public struct CSSCmLength: CSSAbsoluteLength { self.value = value } - public func css() -> String? { value.css(unit: "cm") } + public func css() -> String? { + value.css(unit: "cm") + } } /// Millimeters @@ -591,7 +603,9 @@ public struct CSSMmLength: CSSAbsoluteLength { self.value = value } - public func css() -> String? { value.css(unit: "mm") } + public func css() -> String? { + value.css(unit: "mm") + } } /// Inches @@ -602,7 +616,9 @@ public struct CSSInLength: CSSAbsoluteLength { self.value = value } - public func css() -> String? { value.css(unit: "in") } + public func css() -> String? { + value.css(unit: "in") + } } /// Pixels @@ -613,7 +629,9 @@ public struct CSSPxLength: CSSAbsoluteLength { self.value = value } - public func css() -> String? { value.css(unit: "px") } + public func css() -> String? { + value.css(unit: "px") + } } /// Points @@ -624,7 +642,9 @@ public struct CSSPtLength: CSSAbsoluteLength { self.value = value } - public func css() -> String? { value.css(unit: "pt") } + public func css() -> String? { + value.css(unit: "pt") + } } /// Picas @@ -635,7 +655,9 @@ public struct CSSPcLength: CSSAbsoluteLength { self.value = value } - public func css() -> String? { value.css(unit: "pc") } + public func css() -> String? { + value.css(unit: "pc") + } } public protocol CSSRelativeLength: CSSLength {} @@ -648,7 +670,9 @@ public struct CSSEmLength: CSSRelativeLength { self.value = value } - public func css() -> String? { value.css(unit: "em") } + public func css() -> String? { + value.css(unit: "em") + } } /// Relative to the width of the "0" (zero). @@ -659,7 +683,9 @@ public struct CSSChLength: CSSRelativeLength { self.value = value } - public func css() -> String? { value.css(unit: "ch") } + public func css() -> String? { + value.css(unit: "ch") + } } /// Relative to font-size of the root element. @@ -670,7 +696,9 @@ public struct CSSRemLength: CSSRelativeLength { self.value = value } - public func css() -> String? { value.css(unit: "rem") } + public func css() -> String? { + value.css(unit: "rem") + } } /// Relative to 1% of the width of the viewport. @@ -681,7 +709,9 @@ public struct CSSVwLength: CSSRelativeLength { self.value = value } - public func css() -> String? { value.css(unit: "vw") } + public func css() -> String? { + value.css(unit: "vw") + } } /// Relative to 1% of the height of the viewport. @@ -692,7 +722,9 @@ public struct CSSVhLength: CSSRelativeLength { self.value = value } - public func css() -> String? { value.css(unit: "vh") } + public func css() -> String? { + value.css(unit: "vh") + } } /// Relative to 1% of viewport's smaller dimension. @@ -703,7 +735,9 @@ public struct CSSVMinLength: CSSRelativeLength { self.value = value } - public func css() -> String? { value.css(unit: "vmin") } + public func css() -> String? { + value.css(unit: "vmin") + } } /// Relative to 1% of viewport's larger dimension. @@ -714,7 +748,9 @@ public struct CSSVMaxLength: CSSRelativeLength { self.value = value } - public func css() -> String? { value.css(unit: "vmax") } + public func css() -> String? { + value.css(unit: "vmax") + } } /// Relative to the parent element. @@ -725,7 +761,9 @@ public struct CSSPercentLength: CSSRelativeLength { self.value = value } - public func css() -> String? { (value * 100).css(unit: "%") } + public func css() -> String? { + (value * 100).css(unit: "%") + } } public enum CSSTextAlign: String, CSSConvertible { @@ -734,7 +772,9 @@ public enum CSSTextAlign: String, CSSConvertible { case right case justify - public func css() -> String? { rawValue } + public func css() -> String? { + rawValue + } } /// Line height supports unitless numbers. @@ -756,21 +796,27 @@ public enum CSSHyphens: String, CSSConvertible { case none case auto - public func css() -> String? { rawValue } + public func css() -> String? { + rawValue + } } public enum CSSLigatures: String, CSSConvertible { case none case common = "common-ligatures" - public func css() -> String? { rawValue } + public func css() -> String? { + rawValue + } } public enum CSSBoxSizing: String, CSSConvertible { case contentBox = "content-box" case borderBox = "border-box" - public func css() -> String? { rawValue } + public func css() -> String? { + rawValue + } } private extension Double { diff --git a/Sources/Navigator/EPUB/CSS/HTMLFontFamilyDeclaration.swift b/Sources/Navigator/EPUB/CSS/HTMLFontFamilyDeclaration.swift index c9411d0de1..186e7985d2 100644 --- a/Sources/Navigator/EPUB/CSS/HTMLFontFamilyDeclaration.swift +++ b/Sources/Navigator/EPUB/CSS/HTMLFontFamilyDeclaration.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -19,19 +19,24 @@ public protocol HTMLFontFamilyDeclaration { /// Injects this font family declaration in the given `html` document. /// - /// Use `servingFile` to convert a file URL into an http one to make a local - /// file available to the web views. - func inject(in html: String, servingFile: (FileURL) throws -> HTTPURL) throws -> String + /// Use `servingFile` to convert a file URL into a URL accessible from the + /// web views. + func inject(in html: String, servingFile: (FileURL) throws -> any AbsoluteURL) throws -> String } /// A type-erasing `HTMLFontFamilyDeclaration` object public struct AnyHTMLFontFamilyDeclaration: HTMLFontFamilyDeclaration { private let _fontFamily: () -> FontFamily private let _alternates: () -> [FontFamily] - private let _inject: (String, (FileURL) throws -> HTTPURL) throws -> String + private let _inject: (String, (FileURL) throws -> any AbsoluteURL) throws -> String - public var fontFamily: FontFamily { _fontFamily() } - public var alternates: [FontFamily] { _alternates() } + public var fontFamily: FontFamily { + _fontFamily() + } + + public var alternates: [FontFamily] { + _alternates() + } public init(_ declaration: T) { _fontFamily = { declaration.fontFamily } @@ -39,7 +44,7 @@ public struct AnyHTMLFontFamilyDeclaration: HTMLFontFamilyDeclaration { _inject = { try declaration.inject(in: $0, servingFile: $1) } } - public func inject(in html: String, servingFile: (FileURL) throws -> HTTPURL) throws -> String { + public func inject(in html: String, servingFile: (FileURL) throws -> any AbsoluteURL) throws -> String { try _inject(html, servingFile) } } @@ -65,7 +70,7 @@ public struct CSSFontFamilyDeclaration: HTMLFontFamilyDeclaration { self.fontFaces = fontFaces } - public func inject(in html: String, servingFile: (FileURL) throws -> HTTPURL) throws -> String { + public func inject(in html: String, servingFile: (FileURL) throws -> any AbsoluteURL) throws -> String { var injections = try fontFaces.flatMap { try $0.injections(for: html, servingFile: servingFile) } @@ -109,15 +114,17 @@ public struct CSSFontFace { /// Returns a new CSSFontFace after adding a linked source for this font /// face. /// - /// - Parameter preload: Indicates whether this source will be declared for - /// preloading in the HTML using ``. + /// - Parameters: + /// - file: The URL to the font file to be added as a source. + /// - preload: Indicates whether this source will be declared for + /// preloading in the HTML using ``. public func addingSource(file: FileURL, preload: Bool = false) -> Self { var copy = self copy.sources.append((file, preload)) return copy } - func injections(for html: String, servingFile: (FileURL) throws -> HTTPURL) throws -> [HTMLInjection] { + func injections(for html: String, servingFile: (FileURL) throws -> any AbsoluteURL) throws -> [HTMLInjection] { try sources .filter(\.preload) .map { source in @@ -126,7 +133,7 @@ public struct CSSFontFace { } } - func css(for fontFamily: String, servingFile: (FileURL) throws -> HTTPURL) throws -> String { + func css(for fontFamily: String, servingFile: (FileURL) throws -> any AbsoluteURL) throws -> String { let urls = try sources.map { try servingFile($0.file) } var descriptors: [String: String] = [ "font-family": "\"\(fontFamily)\"", diff --git a/Sources/Navigator/EPUB/CSS/ReadiumCSS.swift b/Sources/Navigator/EPUB/CSS/ReadiumCSS.swift index 78b7a98ca9..7f8331e0a5 100644 --- a/Sources/Navigator/EPUB/CSS/ReadiumCSS.swift +++ b/Sources/Navigator/EPUB/CSS/ReadiumCSS.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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,8 +7,6 @@ import Foundation import ReadiumInternal import ReadiumShared -import SwiftSoup -import UIKit struct ReadiumCSS { var layout: CSSLayout = .init() @@ -16,7 +14,7 @@ struct ReadiumCSS { var userProperties: CSSUserProperties = .init() /// Base URL of the Readium CSS assets. - var baseURL: HTTPURL + var baseURL: any AbsoluteURL var fontFamilyDeclarations: [AnyHTMLFontFamilyDeclaration] = [] } @@ -24,31 +22,6 @@ struct ReadiumCSS { extension ReadiumCSS { mutating func update(with settings: EPUBSettings) { layout = settings.cssLayout - - var overrides: [String: String] = [ - // See https://github.com/readium/css/issues/183 - "--RS__disableOverflow": "readium-noOverflow-on", - - "font-weight": settings.fontWeight - .map { String(format: "%.0f", (Double(CSSStandardFontWeight.normal.rawValue) * $0).clamped(to: 1 ... 1000)) } - ?? "", - ] - - // Applies WebKit patches, ideally: - // - iOS patch for iOS and iPadOS when the site is requested as mobile. - // - iPadOSPatch for iPadOS when the site is requested as desktop. - // - Nothing if MacOS. - // - // See https://github.com/readium/css/issues/189 - switch UIDevice.current.userInterfaceIdiom { - case .pad: - overrides["--USER__iPadOSPatch"] = "readium-iPadOSPatch-on" - case .phone: - overrides["--USER__iOSPatch"] = "readium-iOSPatch-on" - default: - break - } - userProperties = CSSUserProperties( view: settings.scroll ? .scroll : .paged, colCount: settings.columnCount, @@ -76,7 +49,6 @@ extension ReadiumCSS { default: return nil } }(), - lineLength: CSSPercentLength(settings.lineLength), lineHeight: settings.lineHeight.map { .unitless($0) }, paraSpacing: settings.paragraphSpacing.map { CSSRemLength($0) }, paraIndent: settings.paragraphIndent.map { CSSRemLength($0) }, @@ -85,7 +57,11 @@ extension ReadiumCSS { bodyHyphens: settings.hyphens.map { $0 ? .auto : .none }, ligatures: settings.ligatures.map { $0 ? .common : .none }, a11yNormalize: settings.textNormalization, - overrides: overrides + overrides: [ + "font-weight": settings.fontWeight + .map { String(format: "%.0f", (Double(CSSStandardFontWeight.normal.rawValue) * $0).clamped(to: 1 ... 1000)) } + ?? "", + ] ) } @@ -111,14 +87,12 @@ extension ReadiumCSS: HTMLInjectable { /// https://github.com/readium/readium-css/blob/develop/docs/CSS06-stylesheets_order.md func injections(for html: String) throws -> [HTMLInjection] { - let document = try parse(html) - var inj: [HTMLInjection] = [] inj.append(.meta(name: "viewport", content: "width=device-width, height=device-height, initial-scale=1.0")) inj.append(contentsOf: styleInjections(for: html)) inj.append(cssPropertiesInjection()) inj.append(contentsOf: dirInjection()) - try inj.append(contentsOf: langInjections(for: document)) + inj.append(contentsOf: langInjections(for: html)) return inj } @@ -186,19 +160,21 @@ extension ReadiumCSS: HTMLInjectable { /// Injects the `xml:lang` attribute in `html` and `body`. /// /// https://github.com/readium/readium-css/blob/develop/docs/CSS16-internationalization.md#language - private func langInjections(for document: Document) throws -> [HTMLInjection] { + private func langInjections(for html: String) -> [HTMLInjection] { + let langAttrs = ["xml:lang", "lang"] guard let language = layout.language, - let html = try document.getElementsByTag("html").first(), - !html.hasLang(), - let body = document.body() + !HTMLElement.html.hasAttribute(anyOf: langAttrs, in: html) else { return [] } - if body.hasLang() { + if + let bodyLang = HTMLElement.body.attribute(firstOf: langAttrs, in: html), + !bodyLang.isEmpty + { return [ - .langAttribute(on: .html, language: body.lang() ?? language), + .langAttribute(on: .html, language: Language(code: .bcp47(bodyLang))), ] } else { return [ @@ -209,19 +185,6 @@ extension ReadiumCSS: HTMLInjectable { } } -private extension Element { - func hasLang() -> Bool { - hasAttr("xml:lang") || hasAttr("lang") - } - - func lang() -> Language? { - let code = (try? attr("xml:lang")).takeIf { !$0.isEmpty } - ?? (try? attr("lang")).takeIf { !$0.isEmpty } - - return code.map { Language(code: .bcp47($0)) } - } -} - private let dirRegex = try! NSRegularExpression( pattern: "(<(?:html|body)[^>]*)\\s+dir=[\"']\\w*[\"']", options: [.caseInsensitive, .anchorsMatchLines] diff --git a/Sources/Navigator/EPUB/DiffableDecoration+HTML.swift b/Sources/Navigator/EPUB/DiffableDecoration+HTML.swift index adb499a5a7..3c748b2885 100644 --- a/Sources/Navigator/EPUB/DiffableDecoration+HTML.swift +++ b/Sources/Navigator/EPUB/DiffableDecoration+HTML.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -34,9 +34,9 @@ extension DecorationChange { EPUBNavigatorViewController.log(.error, "Decoration style not registered: \(decoration.style.id)") return nil } - var json = decoration.json - json["element"] = style.element(decoration) - guard let jsonString = serializeJSONString(json) else { + var json = decoration.jsonObject + json["element"] = .string(style.element(decoration)) + guard let jsonString = try? json.jsonString() else { EPUBNavigatorViewController.log(.error, "Can't serialize decoration to JSON: \(json)") return nil } diff --git a/Sources/Navigator/EPUB/EPUBExtensions.swift b/Sources/Navigator/EPUB/EPUBExtensions.swift new file mode 100644 index 0000000000..f8d43769ad --- /dev/null +++ b/Sources/Navigator/EPUB/EPUBExtensions.swift @@ -0,0 +1,13 @@ +// +// Copyright 2026 Readium Foundation. All 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 ReadiumShared + +extension Metadata { + var epubLayout: EPUBLayout { + layout == .fixed ? .fixed : .reflowable + } +} diff --git a/Sources/Navigator/EPUB/EPUBFixedSpreadView.swift b/Sources/Navigator/EPUB/EPUBFixedSpreadView.swift index 6b529f4b3c..1a2645ac9f 100644 --- a/Sources/Navigator/EPUB/EPUBFixedSpreadView.swift +++ b/Sources/Navigator/EPUB/EPUBFixedSpreadView.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -47,14 +47,14 @@ final class EPUBFixedSpreadView: EPUBSpreadView { scrollView.backgroundColor = UIColor.clear // Loads the wrapper page into the web view. - let spreadFile = "fxl-spread-\(spread.spread ? "two" : "one")" + let spreadFile = "fxl-spread-\(viewModel.spreadEnabled ? "two" : "one")" if let wrapperPageURL = Bundle.module.url(forResource: spreadFile, withExtension: "html", subdirectory: "Assets"), var wrapperPage = try? String(contentsOf: wrapperPageURL, encoding: .utf8) { wrapperPage = wrapperPage.replacingOccurrences( of: "{{ASSETS_URL}}", - with: viewModel.assetsURL.string + with: viewModel.assetsBaseURL.string ) // The publication's base URL is used to make sure we can access the resources through the iframe with JavaScript. @@ -88,11 +88,13 @@ final class EPUBFixedSpreadView: EPUBSpreadView { insets.right = horizontalInsets let viewportSize = bounds.inset(by: insets).size + let fitString = viewModel.settings.fit.rawValue webView.evaluateJavaScript(""" spread.setViewport( {'width': \(Int(viewportSize.width)), 'height': \(Int(viewportSize.height))}, - {'top': \(Int(insets.top)), 'left': \(Int(insets.left)), 'bottom': \(Int(insets.bottom)), 'right': \(Int(insets.right))} + {'top': \(Int(insets.top)), 'left': \(Int(insets.left)), 'bottom': \(Int(insets.bottom)), 'right': \(Int(insets.right))}, + '\(fitString)' ); """) } @@ -105,7 +107,7 @@ final class EPUBFixedSpreadView: EPUBSpreadView { // to be executed before the spread is loaded. let spreadJSON = spread.jsonString( forBaseURL: viewModel.publicationBaseURL, - readingOrder: viewModel.readingOrder + readingProgression: viewModel.readingProgression ) webView.evaluateJavaScript("spread.load(\(spreadJSON));") } @@ -154,7 +156,7 @@ final class EPUBFixedSpreadView: EPUBSpreadView { private var goToContinuations: [CheckedContinuation] = [] - override func go(to location: PageLocation) async { + override func go(to location: PageLocation, animated: Bool) async { // Fixed layout resources are always fully visible so we don't use the // location. diff --git a/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift b/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift index 55ffe5b318..30a35eeef6 100644 --- a/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift +++ b/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -43,6 +43,7 @@ open class EPUBNavigatorViewController: InputObservableViewController, /// Failed to serve the publication or assets with the provided HTTP /// server. + @available(*, deprecated, message: "The HTTP server is no longer needed for the EPUB navigator.") case serverFailure(Error) } @@ -195,16 +196,19 @@ open class EPUBNavigatorViewController: InputObservableViewController, // All events are ignored when loading spreads, except for `loaded` and `load`. case (.loading, .loaded): self = .idle + case (.loading, _): return false case let (.idle, .jump(locator)): self = .jumping(pendingLocator: locator) + case let (.idle, .move(direction)): self = .moving(direction: direction) case (.jumping, .jumped): self = .idle + // Moving or jumping to another locator is not allowed during a pending jump. case (.jumping, .jump), (.jumping, .move): @@ -212,6 +216,7 @@ open class EPUBNavigatorViewController: InputObservableViewController, case (.moving, .moved): self = .idle + // Moving or jumping to another locator is not allowed during a pending move. case (.moving, .jump), (.moving, .move): @@ -266,9 +271,13 @@ open class EPUBNavigatorViewController: InputObservableViewController, private var positionsByReadingOrder: [[Locator]] = [] private let viewModel: EPUBNavigatorViewModel - public var publication: Publication { viewModel.publication } + public var publication: Publication { + viewModel.publication + } - var config: Configuration { viewModel.config } + var config: Configuration { + viewModel.config + } /// Creates a new instance of `EPUBNavigatorViewController`. /// @@ -279,14 +288,11 @@ open class EPUBNavigatorViewController: InputObservableViewController, /// - readingOrder: Custom order of resources to display. Used for example /// to display a non-linear resource on its own. /// - config: Additional navigator configuration. - /// - httpServer: HTTP server used to serve the publication resources to - /// the web views. public convenience init( publication: Publication, initialLocation: Locator?, readingOrder: [Link]? = nil, - config: Configuration = .init(), - httpServer: HTTPServer + config: Configuration = .init() ) throws { precondition(readingOrder.map { !$0.isEmpty } ?? true) @@ -294,16 +300,16 @@ open class EPUBNavigatorViewController: InputObservableViewController, throw EPUBError.publicationRestricted } - let viewModel = try EPUBNavigatorViewModel( + let viewModel = EPUBNavigatorViewModel( publication: publication, - config: config, - httpServer: httpServer + readingOrder: readingOrder ?? publication.readingOrder, + config: config ) self.init( viewModel: viewModel, initialLocation: initialLocation, - readingOrder: readingOrder ?? publication.readingOrder, + readingOrder: viewModel.readingOrder, positionsByReadingOrder: // Positions and total progression only make sense in the context // of the publication's actual reading order. Therefore when @@ -314,6 +320,23 @@ open class EPUBNavigatorViewController: InputObservableViewController, ) } + /// Creates a new instance of `EPUBNavigatorViewController`. + @available(*, deprecated, message: "The HTTP server is no longer needed for the EPUB navigator.") + public convenience init( + publication: Publication, + initialLocation: Locator?, + readingOrder: [Link]? = nil, + config: Configuration = .init(), + httpServer: HTTPServer + ) throws { + try self.init( + publication: publication, + initialLocation: initialLocation, + readingOrder: readingOrder, + config: config + ) + } + private init( viewModel: EPUBNavigatorViewModel, initialLocation: Locator?, @@ -390,9 +413,15 @@ open class EPUBNavigatorViewController: InputObservableViewController, @objc private func didBecomeActive() { isActive = true + // The device may have rotated since the last time the app was active. + // We may need to refresh the spreads in this situation. Unfortunately, + // the `viewWillTransition(to:with:)` API is called before we receive + // the `didBecomeActive` notification, so we cannot rely on it here. + viewModel.viewSizeWillChange(view.bounds.size) + if needsReloadSpreadsOnActive { needsReloadSpreadsOnActive = false - reloadSpreads(force: true) + reloadSpreads() } } @@ -413,7 +442,7 @@ open class EPUBNavigatorViewController: InputObservableViewController, applySettings() - _reloadSpreads(force: true) + _reloadSpreads() onInitializedCallbacks.complete() } @@ -449,10 +478,8 @@ open class EPUBNavigatorViewController: InputObservableViewController, override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - viewModel.viewSizeWillChange(size) - - coordinator.animate(alongsideTransition: nil) { [weak self] _ in - self?.reloadSpreads(force: false) + if isActive { + viewModel.viewSizeWillChange(size) } } @@ -551,7 +578,7 @@ open class EPUBNavigatorViewController: InputObservableViewController, } paginationView.isScrollEnabled = isPaginationViewScrollingEnabled - reloadSpreads(force: true) + reloadSpreads() } private var spreads: [EPUBSpread] = [] @@ -563,25 +590,31 @@ open class EPUBNavigatorViewController: InputObservableViewController, private var needsReloadSpreadsOnActive = false - private func reloadSpreads(force: Bool) { + private func reloadSpreads() { guard state != .initializing, - isViewLoaded, - isActive + isViewLoaded else { return } - _reloadSpreads(force: force) + guard isActive else { + // If we reload the spreads while the app is in the background, the + // web view will reset to progression 0 instead of the current one. + // We need to wait for the application to return to the foreground + // to maintain the current location. + needsReloadSpreadsOnActive = true + return + } + + _reloadSpreads() } - private func _reloadSpreads(force: Bool) { + private func _reloadSpreads() { let locator = currentLocation guard let paginationView = paginationView, - // Already loaded with the expected amount of spreads? - force || spreads.first?.spread != viewModel.spreadEnabled, on(.load(locator)) else { return @@ -591,7 +624,8 @@ open class EPUBNavigatorViewController: InputObservableViewController, for: publication, readingOrder: readingOrder, readingProgression: viewModel.readingProgression, - spread: viewModel.spreadEnabled + spread: viewModel.spreadEnabled, + offsetFirstPage: viewModel.offsetFirstPage ) let initialIndex: ReadingOrder.Index = { @@ -661,64 +695,15 @@ open class EPUBNavigatorViewController: InputObservableViewController, return (nil, nil) } - let visibleReadingOrder: [(index: Int, href: AnyURL)] = spreadView.spread.readingOrderIndices - .map { ($0, readingOrder[$0].url()) } - - var viewport = Viewport( - readingOrder: visibleReadingOrder.map(\.href), - progressions: visibleReadingOrder.reduce([:]) { progressions, i in - var progressions = progressions - progressions[i.href] = spreadView.progression(in: i.index) - return progressions - }, - positions: nil + let (locator, viewport) = await EPUBViewportAndLocationCalculator.compute( + readingOrderIndices: spreadView.spread.readingOrderIndices, + progression: { spreadView.progression(in: $0) }, + readingOrder: readingOrder, + positionsByReadingOrder: positionsByReadingOrder, + tableOfContentsTitleByHref: tableOfContentsTitleByHref, + fallbackLocator: { [publication] in await publication.locate($0) } ) - - let firstIndex = spreadView.spread.readingOrderIndices.lowerBound - let lastIndex = spreadView.spread.readingOrderIndices.upperBound - let progressionOfFirstResource = spreadView.progression(in: firstIndex) - let progressionOfLastResource = spreadView.progression(in: lastIndex) - let firstProgressionInFirstResource = min(max(progressionOfFirstResource.lowerBound, 0.0), 1.0) - let lastProgressionInLastResource = min(max(progressionOfLastResource.upperBound, 0.0), 1.0) - - let link = readingOrder[firstIndex] - let location: Locator? - - if - // The positions are not always available, for example a Readium - // WebPub doesn't have any unless a Publication Positions Web - // Service is provided - let positionsOfFirstResource = positionsByReadingOrder.getOrNil(firstIndex), - let positionsOfLastResource = positionsByReadingOrder.getOrNil(lastIndex), - !positionsOfFirstResource.isEmpty, - !positionsOfLastResource.isEmpty - { - // Gets the current locator from the positions, and fill its missing - // data. - let firstPositionIndex = Int(ceil(firstProgressionInFirstResource * Double(positionsOfFirstResource.count - 1))) - let lastPositionIndex = (lastProgressionInLastResource == 1.0) - ? positionsOfLastResource.count - 1 - : max(firstPositionIndex, Int(ceil(lastProgressionInLastResource * Double(positionsOfLastResource.count - 1))) - 1) - - location = await positionsOfFirstResource[firstPositionIndex].copy( - title: tableOfContentsTitleByHref[link.url()], - locations: { $0.progression = firstProgressionInFirstResource } - ) - - if - let firstPosition = location?.locations.position, - let lastPosition = positionsOfLastResource[lastPositionIndex].locations.position - { - viewport.positions = firstPosition ... lastPosition - } - - } else { - location = await publication.locate(link)?.copy( - locations: { $0.progression = firstProgressionInFirstResource } - ) - } - - return (location, viewport) + return (locator, viewport) } public func firstVisibleElementLocator() async -> Locator? { @@ -824,28 +809,46 @@ open class EPUBNavigatorViewController: InputObservableViewController, // MARK: - DecorableNavigator - private var decorations: [String: [DiffableDecoration]] = [:] + private var decorations: [DecorationGroup: [DiffableDecoration]] = [:] /// Decoration group callbacks, indexed by the group name. - private var decorationCallbacks: [String: [DecorableNavigator.OnActivatedCallback]] = [:] + private var decorationCallbacks: [DecorationGroup: [DecorableNavigator.OnActivatedCallback]] = [:] + + /// Pending decoration tasks, indexed by group name. Stored to allow + /// cancellation when a new `apply(decorations:in:)` call supersedes a + /// previous one. + private var decorationTasks: [DecorationGroup: Task] = [:] public func supports(decorationStyle style: Decoration.Style.Id) -> Bool { config.decorationTemplates.keys.contains(style) } - public func apply(decorations: [Decoration], in group: String) { - Task { - await initialized() + public func apply(decorations: [Decoration], in group: DecorationGroup) { + decorationTasks[group]?.cancel() + var task: Task? + task = Task { [weak self] in + defer { + if let self, self.decorationTasks[group] == task { + self.decorationTasks[group] = nil + } + } + guard let self else { return } + await self.initialized() - guard let paginationView = paginationView else { + guard + !Task.isCancelled, + let paginationView = self.paginationView + else { return } await withTaskGroup(of: Void.self) { tasks in + guard !Task.isCancelled else { return } + let source = self.decorations[group] ?? [] let target = decorations.map { var d = $0 - d.locator = publication.normalizeLocator(d.locator) + d.locator = self.publication.normalizeLocator(d.locator) return DiffableDecoration(decoration: d) } self.decorations[group] = target @@ -853,6 +856,7 @@ open class EPUBNavigatorViewController: InputObservableViewController, if decorations.isEmpty { for (_, pageView) in paginationView.loadedViews { tasks.addTask { + guard !Task.isCancelled else { return } await (pageView as? EPUBSpreadView)?.evaluateScript( // The updates command are using `requestAnimationFrame()`, so we need it for // `clear()` as well otherwise we might recreate a highlight after it has been @@ -863,11 +867,12 @@ open class EPUBNavigatorViewController: InputObservableViewController, } } else { for (href, changes) in target.changesByHREF(from: source) { - guard let script = changes.javascript(forGroup: group, styles: config.decorationTemplates) else { + guard let script = changes.javascript(forGroup: group, styles: self.config.decorationTemplates) else { continue } tasks.addTask { @MainActor [weak self] in guard + !Task.isCancelled, let spreadView = self?.loadedSpreadViewForHREF(href), spreadView.isSpreadLoaded else { @@ -879,9 +884,10 @@ open class EPUBNavigatorViewController: InputObservableViewController, } } } + decorationTasks[group] = task } - public func observeDecorationInteractions(inGroup group: String, onActivated: @escaping OnActivatedCallback) { + public func observeDecorationInteractions(inGroup group: DecorationGroup, onActivated: @escaping OnActivatedCallback) { var callbacks = decorationCallbacks[group] ?? [] callbacks.append(onActivated) decorationCallbacks[group] = callbacks @@ -905,7 +911,9 @@ open class EPUBNavigatorViewController: InputObservableViewController, // MARK: - Configurable - public var settings: EPUBSettings { viewModel.settings } + public var settings: EPUBSettings { + viewModel.settings + } public func submitPreferences(_ preferences: EPUBPreferences) { viewModel.submitPreferences(preferences) @@ -1031,7 +1039,16 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { // the application's bars. var insets = view.window?.safeAreaInsets ?? .zero - if publication.metadata.layout != .fixed { + switch publication.metadata.epubLayout { + case .fixed: + // With iPadOS and macOS, we aim to display content edge-to-edge + // since there are no physical notches or Dynamic Island like on the + // iPhone. + if UIDevice.current.userInterfaceIdiom != .phone { + insets = .zero + } + + case .reflowable: let configInset = config.contentInset(for: view.traitCollection.verticalSizeClass) insets.top = max(insets.top, configInset.top) insets.bottom = max(insets.bottom, configInset.bottom) @@ -1041,11 +1058,11 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { } func spreadViewDidLoad(_ spreadView: EPUBSpreadView) async { - let templates = config.decorationTemplates.reduce(into: [:]) { styles, item in - styles[item.key.rawValue] = item.value.json + let templates = config.decorationTemplates.reduce(into: [String: JSONValue]()) { styles, item in + styles[item.key.rawValue] = .object(item.value.jsonObject) } - guard let stylesJSON = serializeJSONString(templates) else { + guard let stylesJSON = try? templates.jsonString() else { log(.error, "Can't serialize decoration styles to JSON") return } @@ -1190,7 +1207,7 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { } } - func spreadView(_ spreadView: EPUBSpreadView, didActivateDecoration id: Decoration.Id, inGroup group: String, frame: CGRect?, point: CGPoint?) { + func spreadView(_ spreadView: EPUBSpreadView, didActivateDecoration id: Decoration.Id, inGroup group: DecorationGroup, frame: CGRect?, point: CGPoint?) { guard let callbacks = decorationCallbacks[group].takeIf({ !$0.isEmpty }), let decoration: Decoration = decorations[group]? @@ -1230,15 +1247,7 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { } func spreadViewDidTerminate() { - if !isActive { - // If we reload the spreads while the app is in the background, the - // web view will reset to progression 0 instead of the current one. - // We need to wait for the application to return to the foreground - // to maintain the current location. - needsReloadSpreadsOnActive = true - } else { - reloadSpreads(force: true) - } + reloadSpreads() } } diff --git a/Sources/Navigator/EPUB/EPUBNavigatorViewModel.swift b/Sources/Navigator/EPUB/EPUBNavigatorViewModel.swift index 6e93fecf8b..74b1d65049 100644 --- a/Sources/Navigator/EPUB/EPUBNavigatorViewModel.swift +++ b/Sources/Navigator/EPUB/EPUBNavigatorViewModel.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -20,79 +20,72 @@ enum EPUBScriptScope { case resource(href: AnyURL) } -final class EPUBNavigatorViewModel: Loggable { - enum Error: Swift.Error { - case noHTTPServer - } - +@MainActor final class EPUBNavigatorViewModel: Loggable { let publication: Publication let config: EPUBNavigatorViewController.Configuration let editingActions: EditingActionsController - private let httpServer: HTTPServer? - private let publicationEndpoint: HTTPServerEndpoint? - private(set) var publicationBaseURL: HTTPURL! - let assetsURL: HTTPURL - weak var delegate: EPUBNavigatorViewModelDelegate? - /// Local file URL associated to the HTTP URL used to serve the file on the - /// `httpServer`. This is used to serve custom font files, for example. - @Atomic private var servedFiles: [FileURL: HTTPURL] = [:] + /// The base URL for the publication resources. + private(set) var publicationBaseURL: AbsoluteURL! + + /// The base URL for Readium assets (CSS, scripts, etc.) and fonts. + let assetsBaseURL: any AbsoluteURL - var readingOrder: ReadingOrder { publication.readingOrder } + /// The server used to serve publication resources and static assets to + /// the web view. + let server: WebViewServer + + /// Format sniffer used to infer the media type of resources served with + /// the `server`. + let formatSniffer: FormatSniffer + + weak var delegate: EPUBNavigatorViewModelDelegate? + + let readingOrder: ReadingOrder convenience init( publication: Publication, - config: EPUBNavigatorViewController.Configuration, - httpServer: HTTPServer - ) throws { - let uuidEndpoint: HTTPServerEndpoint = UUID().uuidString - let publicationEndpoint: HTTPServerEndpoint? - if publication.baseURL != nil { - publicationEndpoint = nil - } else { - publicationEndpoint = uuidEndpoint - } + readingOrder: ReadingOrder, + config: EPUBNavigatorViewController.Configuration + ) { + let assetsDirectory = Bundle.module.resourceURL!.fileURL! + .appendingPath("Assets/Static", isDirectory: true) - try self.init( + let formatSniffer = DefaultFormatSniffer() + let server = WebViewServer(scheme: "readium", formatSniffer: formatSniffer) + + // Serve static assets directory. + let assetsBaseURL = server.serve(directory: assetsDirectory, at: "assets") + + self.init( publication: publication, + readingOrder: readingOrder, config: config, - httpServer: httpServer, - publicationEndpoint: publicationEndpoint, - assetsURL: httpServer.serve( - at: "readium", - contentsOf: Bundle.module.resourceURL!.fileURL! - .appendingPath("Assets/Static", isDirectory: true) - ) + server: server, + assetsBaseURL: assetsBaseURL, + formatSniffer: formatSniffer ) if let url = publication.baseURL { + // The publication already has an HTTP base URL (e.g. served + // remotely). Use it directly; the server only needs to serve + // assets. publicationBaseURL = url } else { - publicationBaseURL = try httpServer.serve( - at: uuidEndpoint, // serving the chapters endpoint - publication: publication, - onFailure: { [weak self] request, error in - guard let self = self, let href = request.href else { - return - } - self.delegate?.epubNavigatorViewModel(self, didFailToLoadResourceAt: href, withError: error) - } - ) - } - - if let endpoint = publicationEndpoint { - try httpServer.transformResources(at: endpoint) { [weak self] href, resource in - self?.injectReadiumCSS(in: resource, at: href) ?? resource + // Serve publication resources. + publicationBaseURL = server.serve(at: UUID().uuidString) { [weak self] in + await self?.serve(href: $0) } } } private init( publication: Publication, + readingOrder: ReadingOrder, config: EPUBNavigatorViewController.Configuration, - httpServer: HTTPServer?, - publicationEndpoint: HTTPServerEndpoint?, - assetsURL: HTTPURL + server: WebViewServer, + assetsBaseURL: any AbsoluteURL, + formatSniffer: FormatSniffer ) { var config = config @@ -123,14 +116,15 @@ final class EPUBNavigatorViewModel: Loggable { } self.publication = publication + self.readingOrder = readingOrder self.config = config editingActions = EditingActionsController( actions: config.editingActions, publication: publication ) - self.httpServer = httpServer - self.publicationEndpoint = publicationEndpoint - self.assetsURL = assetsURL + self.server = server + self.assetsBaseURL = assetsBaseURL + self.formatSniffer = formatSniffer preferences = config.preferences settings = EPUBSettings(publication: publication, config: config) @@ -138,7 +132,7 @@ final class EPUBNavigatorViewModel: Loggable { css = ReadiumCSS( layout: CSSLayout(), rsProperties: config.readiumCSSRSProperties, - baseURL: assetsURL.appendingPath("readium-css", isDirectory: true), + baseURL: assetsBaseURL.appendingPath("readium-css", isDirectory: true), fontFamilyDeclarations: config.fontFamilyDeclarations ) @@ -154,30 +148,12 @@ final class EPUBNavigatorViewModel: Loggable { deinit { NotificationCenter.default.removeObserver(self) - - if let endpoint = publicationEndpoint { - try? httpServer?.remove(at: endpoint) - } } func url(to link: Link) -> AnyURL { link.url(relativeTo: publicationBaseURL) } - private func serveFile(at file: FileURL, baseEndpoint: HTTPServerEndpoint) throws -> HTTPURL { - if let url = servedFiles[file] { - return url - } - - guard let httpServer = httpServer else { - throw Error.noHTTPServer - } - let endpoint = baseEndpoint.addingSuffix("/") + file.lastPathSegment - let url = try httpServer.serve(at: endpoint, contentsOf: file) - $servedFiles.write { $0[file] = url } - return url - } - private var needsInvalidatePagination = false private func setNeedsInvalidatePagination() { guard !needsInvalidatePagination else { @@ -190,6 +166,36 @@ final class EPUBNavigatorViewModel: Loggable { } } + // MARK: - Web View Server + + private func serve(href: RelativeURL) async -> (Resource, MediaType)? { + guard var resource = publication.get(href) else { + return nil + } + let mediaType = await resolveMediaType(for: resource, at: href) + resource = injectReadiumCSS(in: resource, at: href) + return (resource, mediaType) + } + + /// Resolves the media type to use to serve the given `resource`. + /// + /// The media type declared in the manifest takes precedence, before falling + /// back on the `Resource` properties and sniffing the `href`. + /// + /// The manifest takes precedence because a file with a `.xml` extension + /// might be declared as `application/xhtml+xml` in the OPF. + private func resolveMediaType(for resource: Resource, at href: RelativeURL) async -> MediaType { + if let mediaType = publication.linkWithHREF(href)?.mediaType { + return mediaType + } + if let mediaType = await resource.properties().getOrNil()?.mediaType { + return mediaType + } + + return href.pathExtension.flatMap { formatSniffer.sniffHints(.init(fileExtension: $0))?.mediaType } + ?? .binary + } + // MARK: - User preferences /// Currently applied settings. @@ -220,6 +226,8 @@ final class EPUBNavigatorViewModel: Loggable { || oldSettings.verticalText != newSettings.verticalText || oldSettings.scroll != newSettings.scroll || oldSettings.spread != newSettings.spread + || oldSettings.fit != newSettings.fit + || oldSettings.offsetFirstPage != newSettings.offsetFirstPage // We don't commit the CSS changes if we invalidate the pagination, as // the resources will be reloaded anyway. @@ -238,11 +246,29 @@ final class EPUBNavigatorViewModel: Loggable { ) } - var readingProgression: ReadingProgression { settings.readingProgression } - var theme: Theme { settings.theme } - var scroll: Bool { settings.scroll } - var verticalText: Bool { settings.verticalText } - var spread: Spread { settings.spread } + var readingProgression: ReadingProgression { + settings.readingProgression + } + + var theme: Theme { + settings.theme + } + + var scroll: Bool { + settings.scroll + } + + var verticalText: Bool { + settings.verticalText + } + + var spread: Spread { + settings.spread + } + + var offsetFirstPage: Bool? { + settings.offsetFirstPage + } // MARK: Spread @@ -279,16 +305,13 @@ final class EPUBNavigatorViewModel: Loggable { // MARK: - Readium CSS private var css: ReadiumCSS - - private func serveFont(at file: FileURL) throws -> HTTPURL { - try serveFile(at: file, baseEndpoint: "custom-fonts/\(UUID().uuidString)") - } + private var servedFonts: [FileURL: AbsoluteURL] = [:] func injectReadiumCSS(in resource: Resource, at href: HREF) -> Resource { guard let link = publication.linkWithHREF(href), link.mediaType?.isHTML == true, - publication.metadata.layout == .reflowable + publication.metadata.epubLayout == .reflowable else { return resource } @@ -301,7 +324,18 @@ final class EPUBNavigatorViewModel: Loggable { do { var content = try css.inject(in: content) for ff in config.fontFamilyDeclarations { - content = try ff.inject(in: content, servingFile: serveFont) + content = try ff.inject( + in: content, + servingFile: { [server] file in + if let url = self.servedFonts[file] { + return url + } + let name = file.lastPathSegment ?? UUID().uuidString + let url = server.serve(file: file, at: "assets/fonts/\(name)") + self.servedFonts[file] = url + return url + } + ) } return content } catch { diff --git a/Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift b/Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift index 24eda8d15a..abdde9284f 100644 --- a/Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift +++ b/Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -33,6 +33,18 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { ) } + override func clear() { + super.clear() + + // Clean up go to continuations. + for continuation in goToContinuations { + continuation.resume() + } + goToContinuations.removeAll() + + scrollDidEnd() + } + override func setupWebView() { super.setupWebView() @@ -71,8 +83,7 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { log(.error, "Only one document at a time can be displayed in a reflowable spread") return } - let link = viewModel.readingOrder[spread.leading] - let url = viewModel.url(to: link) + let url = viewModel.url(to: spread.first.link) webView.load(URLRequest(url: url.url)) } @@ -125,7 +136,7 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { override func progression(in index: ReadingOrder.Index) -> ClosedRange { guard - spread.leading == index, + spread.first.index == index, let progression = progression else { return 0 ... 0 @@ -134,10 +145,8 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { } override func spreadDidLoad() async { - if - let link = viewModel.readingOrder.getOrNil(spread.leading), - let linkJSON = serializeJSONString(link.json) - { + let link = spread.first.link + if let linkJSON = try? link.jsonString() { await evaluateScript("readium.link = \(linkJSON);") } @@ -148,11 +157,11 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { try? await Task.sleep(seconds: 0.2) let location = pendingLocation - await go(to: pendingLocation) + await go(to: location.location, animated: location.animated) // The rendering is sometimes very slow. So in case we don't show the first page of the resource, we add // a generous delay before showing the spread again. - let delayed = !location.isStart + let delayed = !location.location.isStart try? await Task.sleep(seconds: delayed ? 0.3 : 0) } @@ -170,34 +179,57 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { } }() + guard scrollView.bounds.width > 0 else { return false } let offsetX = scrollView.bounds.width * factor - var newOffset = scrollView.contentOffset - newOffset.x += offsetX - let rounded = round(newOffset.x / offsetX) * offsetX - newOffset.x = rounded - guard 0 ..< scrollView.contentSize.width ~= newOffset.x else { + let targetX = round((scrollView.contentOffset.x + offsetX) / offsetX) * offsetX + guard 0 ..< scrollView.contentSize.width ~= targetX else { return false } - scrollView.setContentOffset(newOffset, animated: options.animated) - - // This delay is only used when turning pages in a single resource if - // the page turn is animated. The delay is roughly the length of the - // animation. - // TODO: completion should be implemented using scroll view delegates - try? await Task.sleep(seconds: 0.3) + // We use JavaScript instead of `UIScrollView.setContentOffset()` to + // prevent glitches when turning pages without animation. + // See https://github.com/readium/swift-toolkit/issues/737#issuecomment-4090386881 + // + // `scrollBy` is used instead of `scrollTo` because RTL content uses + // negative `window.scrollX` values in WKWebView, whereas UIKit's + // `contentOffset.x` is always non-negative. A relative displacement + // (`offsetX`) is coordinate-system agnostic and works for both LTR and + // RTL. + let behavior = options.animated ? "smooth" : "instant" + await evaluateScript("window.scrollBy({ left: \(offsetX), behavior: '\(behavior)' });") + + if options.animated { + // Waits for the scroll animation to finish. + await withCheckedContinuation { continuation in + let request = ScrollAnimationRequest(continuation) + pendingScrollAnimation?.resume() + pendingScrollAnimation = request + + // Safety net in case `scrollDidEnd` never fires. The identity + // check on `request` ensures a stale timeout from a previous + // request does not resume a newer one. + Task { @MainActor in + try? await Task.sleep(seconds: 0.8) + scrollDidEnd(for: request) + } + } + } return true } - // Location to scroll to in the resource once the page is loaded. - private var pendingLocation: PageLocation = .start + private struct PendingLocation { + var location: PageLocation + var animated: Bool + } + + /// Location to scroll to in the resource once the page is loaded. + private var pendingLocation: PendingLocation = .init(location: .start, animated: false) - @MainActor - override func go(to location: PageLocation) async { + override func go(to location: PageLocation, animated: Bool) async { guard isSpreadLoaded else { // Delays moving to the location until the document is loaded. - pendingLocation = location + pendingLocation = PendingLocation(location: location, animated: animated) await waitGoToCompletion() return @@ -205,24 +237,22 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { switch location { case let .locator(locator): - await go(to: locator) + await go(to: locator, animated: animated) case .start: - await scroll(toProgression: 0) + await scroll(toProgression: 0, animated: animated) case .end: - await scroll(toProgression: 1) + await scroll(toProgression: 1, animated: animated) } didCompleteGoTo() } - @MainActor private func waitGoToCompletion() async { await withCheckedContinuation { continuation in goToContinuations.append(continuation) } } - @MainActor private func didCompleteGoTo() { for cont in goToContinuations { cont.resume() @@ -230,11 +260,37 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { goToContinuations.removeAll() } - @MainActor private var goToContinuations: [CheckedContinuation] = [] + private var pendingScrollAnimation: ScrollAnimationRequest? + + /// Represents an in-flight animated page turn, waiting for the scroll + /// animation to settle before completing. + private class ScrollAnimationRequest { + private var continuation: CheckedContinuation? + + init(_ continuation: CheckedContinuation) { + self.continuation = continuation + } + + /// Resumes the continuation. Safe to call multiple times; only the + /// first call has any effect. + func resume() { + continuation?.resume() + continuation = nil + } + } + + private func scrollDidEnd(for request: ScrollAnimationRequest? = nil) { + guard request == nil || pendingScrollAnimation === request else { + return + } + pendingScrollAnimation?.resume() + pendingScrollAnimation = nil + } + @discardableResult - private func go(to locator: Locator) async -> Bool { + private func go(to locator: Locator, animated: Bool) async -> Bool { if !["", "#"].contains(locator.href.string) { guard let index = viewModel.readingOrder.firstIndexWithHREF(locator.href), @@ -246,19 +302,19 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { } if locator.text.highlight != nil { - return await scroll(toLocator: locator) + return await scroll(toLocator: locator, animated: animated) // TODO: find the first fragment matching a tag ID (need a regex) } else if let id = locator.locations.fragments.first, !id.isEmpty { - return await scroll(toTagID: id) + return await scroll(toTagID: id, animated: animated) } else { let progression = locator.locations.progression ?? 0 - return await scroll(toProgression: progression) + return await scroll(toProgression: progression, animated: animated) } } /// Scrolls at given progression (from 0.0 to 1.0) @discardableResult - private func scroll(toProgression progression: Double) async -> Bool { + private func scroll(toProgression progression: Double, animated: Bool) async -> Bool { guard progression >= 0, progression <= 1 else { log(.warning, "Scrolling to invalid progression \(progression)") return false @@ -274,15 +330,15 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { return true } else { let dir = viewModel.readingProgression.rawValue - await evaluateScript("readium.scrollToPosition(\'\(progression)\', \'\(dir)\')") + await evaluateScript("readium.scrollToPosition(\'\(progression)\', \'\(dir)\', \(animated))") return true } } /// Scrolls at the tag with ID `tagID`. @discardableResult - private func scroll(toTagID tagID: String) async -> Bool { - let result = await evaluateScript("readium.scrollToId(\'\(tagID)\');") + private func scroll(toTagID tagID: String, animated: Bool) async -> Bool { + let result = await evaluateScript("readium.scrollToId(\'\(tagID)\', \(animated));") switch result { case let .success(value): return (value as? Bool) ?? false @@ -294,11 +350,11 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { /// Scrolls at the snippet matching the given text context. @discardableResult - private func scroll(toLocator locator: Locator) async -> Bool { - guard let json = locator.jsonString else { + private func scroll(toLocator locator: Locator, animated: Bool) async -> Bool { + guard let json = try? locator.jsonString() else { return false } - let result = await evaluateScript("readium.scrollToLocator(\(json));") + let result = await evaluateScript("readium.scrollToLocator(\(json), \(animated));") switch result { case let .success(value): return (value as? Bool) ?? false @@ -310,12 +366,12 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { // MARK: - Progression - // Current progression range in the page. + /// Current progression range in the page. private var progression: ClosedRange? - // To check if a progression change was cancelled or not. + /// To check if a progression change was cancelled or not. private var previousProgression: ClosedRange? - // Called by the javascript code to notify that scrolling ended. + /// Called by the javascript code to notify that scrolling ended. private func progressionDidChange(_ body: Any) { guard isSpreadLoaded, @@ -349,6 +405,8 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { return } previousProgression = nil + + scrollDidEnd() delegate?.spreadViewPagesDidChange(self) } diff --git a/Sources/Navigator/EPUB/EPUBSpread.swift b/Sources/Navigator/EPUB/EPUBSpread.swift index 573c963714..0fd93d9f99 100644 --- a/Sources/Navigator/EPUB/EPUBSpread.swift +++ b/Sources/Navigator/EPUB/EPUBSpread.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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,106 +7,76 @@ import Foundation import ReadiumShared -/// A list of EPUB resources to be displayed together on the screen, as one-page -/// or two-pages spread. -struct EPUBSpread: Loggable { - /// Indicates whether two pages are displayed side by side. - var spread: Bool +/// Common interface for spread types. +protocol EPUBSpreadProtocol { + /// Returns whether the spread contains the resource at the given reading + /// order index. + func contains(index: ReadingOrder.Index) -> Bool - /// Indices for the resources displayed in the spread, in reading order. - /// - /// Note: it's possible to have less links than the amount of `pageCount` - /// available, because a single page might be displayed in a two-page spread - /// (eg. with Properties.Page center, left or right). - var readingOrderIndices: ReadingOrderIndices + /// Return the number of positions contained in the spread. + func positionCount(in readingOrder: ReadingOrder, positionsByReadingOrder: [[Locator]]) -> Int - /// Spread reading progression direction. - var readingProgression: ReadingProgression + /// Returns a JSON representation of the links in the spread. + /// + /// The JSON is an array of link objects in reading progression order. + /// Each link object contains: + /// - link: Link object of the resource in the Publication + /// - url: Full URL to the resource. + /// - page [left|center|right]: (optional) Page position of the linked resource in the spread. + func json(forBaseURL baseURL: AbsoluteURL, readingProgression: ReadingProgression) -> [JSONValue] +} - init(spread: Bool, readingOrderIndices: ReadingOrderIndices, readingProgression: ReadingProgression) { - precondition(!readingOrderIndices.isEmpty, "A spread must have at least one page") - precondition(spread || readingOrderIndices.count == 1, "A one-page spread must have only one page") - precondition(!spread || 1 ... 2 ~= readingOrderIndices.count, "A two-pages spread must have one or two pages max") - self.spread = spread - self.readingOrderIndices = readingOrderIndices - self.readingProgression = readingProgression - } +/// Represents a spread of EPUB resources displayed in the viewport. A spread +/// can contain one or two resources (for FXL). +enum EPUBSpread: EPUBSpreadProtocol { + /// A spread displaying a single resource. + case single(EPUBSingleSpread) + /// A spread displaying two resources side by side (FXL only). + case double(EPUBDoubleSpread) - /// Returns the left-most reading order index in the spread. - var left: ReadingOrder.Index { - switch readingProgression { - case .ltr: - readingOrderIndices.lowerBound - case .rtl: - readingOrderIndices.upperBound + /// Range of reading order indices contained in this spread. + var readingOrderIndices: ReadingOrderIndices { + switch self { + case let .single(spread): + return spread.resource.index ... spread.resource.index + case let .double(spread): + return spread.first.index ... spread.second.index } } - /// Returns the right-most reading order index in the spread. - var right: ReadingOrder.Index { - switch readingProgression { - case .ltr: - readingOrderIndices.upperBound - case .rtl: - readingOrderIndices.lowerBound + /// The leading resource in the reading progression. + var first: EPUBSpreadResource { + switch self { + case let .single(spread): + return spread.resource + case let .double(spread): + return spread.first } } - /// Returns the leading reading order index in the reading progression. - var leading: ReadingOrder.Index { - readingOrderIndices.lowerBound + private var spread: EPUBSpreadProtocol { + switch self { + case let .single(spread): + return spread + case let .double(spread): + return spread + } } - /// Returns whether the spread contains the resource at the given reading - /// order index func contains(index: ReadingOrder.Index) -> Bool { - readingOrderIndices.contains(index) + spread.contains(index: index) } - /// Return the number of positions contained in the spread. func positionCount(in readingOrder: ReadingOrder, positionsByReadingOrder: [[Locator]]) -> Int { - readingOrderIndices - .map { index in - positionsByReadingOrder[index].count - } - .reduce(0, +) + spread.positionCount(in: readingOrder, positionsByReadingOrder: positionsByReadingOrder) } - /// Returns a JSON representation of the links in the spread. - /// The JSON is an array of link objects in reading progression order. - /// Each link object contains: - /// - link: Link object of the resource in the Publication - /// - url: Full URL to the resource. - /// - page [left|center|right]: (optional) Page position of the linked resource in the spread. - func json(forBaseURL baseURL: HTTPURL, readingOrder: ReadingOrder) -> [[String: Any]] { - func makeLinkJSON(_ index: ReadingOrder.Index, page: Properties.Page? = nil) -> [String: Any]? { - guard let link = readingOrder.getOrNil(index) else { - return nil - } - - let page = page ?? link.properties.page ?? readingProgression.startingPage - return [ - "index": index, - "link": link.json, - "url": link.url(relativeTo: baseURL).string, - "page": page.rawValue, - ] - } - - var json: [[String: Any]?] = [] - - if readingOrderIndices.count == 1 { - json.append(makeLinkJSON(leading)) - } else { - json.append(makeLinkJSON(left, page: .left)) - json.append(makeLinkJSON(right, page: .right)) - } - - return json.compactMap { $0 } + func json(forBaseURL baseURL: AbsoluteURL, readingProgression: ReadingProgression) -> [JSONValue] { + spread.json(forBaseURL: baseURL, readingProgression: readingProgression) } - func jsonString(forBaseURL baseURL: HTTPURL, readingOrder: ReadingOrder) -> String { - serializeJSONString(json(forBaseURL: baseURL, readingOrder: readingOrder)) ?? "[]" + func jsonString(forBaseURL baseURL: AbsoluteURL, readingProgression: ReadingProgression) -> String { + (try? json(forBaseURL: baseURL, readingProgression: readingProgression).jsonString()) ?? "[]" } /// Builds a list of spreads for the given Publication. @@ -115,92 +85,108 @@ struct EPUBSpread: Loggable { /// - publication: The Publication to build the spreads for. /// - readingProgression: Reading progression direction used to layout the pages. /// - spread: Indicates whether two pages are displayed side-by-side. + /// - offsetFirstPage: Indicates if the first page should be displayed in its own spread. static func makeSpreads( for publication: Publication, readingOrder: [Link], readingProgression: ReadingProgression, - spread: Bool + spread: Bool, + offsetFirstPage: Bool? = nil ) -> [EPUBSpread] { spread - ? makeTwoPagesSpreads(for: publication, readingOrder: readingOrder, readingProgression: readingProgression) - : makeOnePageSpreads(for: publication, readingOrder: readingOrder, readingProgression: readingProgression) + ? makeTwoPagesSpreads(for: publication, readingOrder: readingOrder, readingProgression: readingProgression, offsetFirstPage: offsetFirstPage) + : makeOnePageSpreads(readingOrder: readingOrder) } /// Builds a list of one-page spreads for the given Publication. private static func makeOnePageSpreads( - for publication: Publication, - readingOrder: [Link], - readingProgression: ReadingProgression + readingOrder: [Link] ) -> [EPUBSpread] { - readingOrder.enumerated().map { index, _ in - EPUBSpread( - spread: false, - readingOrderIndices: index ... index, - readingProgression: readingProgression - ) + readingOrder.enumerated().map { index, link in + .single(EPUBSingleSpread( + resource: EPUBSpreadResource(index: index, link: link) + )) } } /// Builds a list of two-page spreads for the given Publication. + /// + /// `offsetFirstPage` is the user preference used to control if the first + /// resource is displayed on its own. private static func makeTwoPagesSpreads( for publication: Publication, readingOrder: [Link], - readingProgression: ReadingProgression + readingProgression: ReadingProgression, + offsetFirstPage: Bool? ) -> [EPUBSpread] { var spreads: [EPUBSpread] = [] var index = 0 while index < readingOrder.count { - let first = readingOrder[index] + var first = readingOrder[index] - var spread = EPUBSpread( - spread: true, - readingOrderIndices: index ... index, - readingProgression: readingProgression - ) + // The first resource (often the cover) has special rules for its + // position in the spread. + if index == 0 { + if let offsetFirstPage = offsetFirstPage { + // User explicitly chose to offset (or not) the first page. + first.properties.page = offsetFirstPage ? .center : nil + } else if first.properties.page == nil, publication.metadata.layout == .fixed { + // For FXL publications, default to displaying the first + // page (typically a cover) on its own when the publication + // doesn't provide an explicit page position. This is the + // behavior of Apple Books, so it's expected by publishers. + // + // We display it centered rather than on the left or right + // to ensure it fills the entire viewport in portrait mode. + first.properties.page = .center + } + } let nextIndex = index + 1 + // To be displayed together, two pages must be part of a fixed // layout publication and have consecutive position hints // (Properties.Page). if let second = readingOrder.getOrNil(nextIndex), publication.metadata.layout == .fixed, - publication.areConsecutive(first, second, index: index) + areConsecutive(first, second, readingProgression: publication.metadata.readingProgression) { - spread.readingOrderIndices = index ... nextIndex + spreads.append(.double( + EPUBDoubleSpread( + first: EPUBSpreadResource(index: index, link: first), + second: EPUBSpreadResource(index: nextIndex, link: second) + ) + )) index += 1 // Skips the consumed "second" page + + } else { + spreads.append(.single( + EPUBSingleSpread( + resource: EPUBSpreadResource(index: index, link: first) + ) + )) } - spreads.append(spread) index += 1 } return spreads } -} -extension Array where Element == EPUBSpread { - /// Returns the index of the first spread containing a resource with the given `href`. - func firstIndexWithReadingOrderIndex(_ index: ReadingOrder.Index) -> Int? { - firstIndex { spread in - spread.contains(index: index) - } - } -} - -private extension Publication { /// Two resources are consecutive if their position hint (Properties.Page) - /// are paired according to the reading progression. - func areConsecutive(_ first: Link, _ second: Link, index: Int) -> Bool { - guard index > 0 || first.properties.page != nil else { - return false - } - + /// are paired according to the reading progression from the publication + /// (not user preferences). + private static func areConsecutive( + _ first: Link, + _ second: Link, + readingProgression: ReadiumShared.ReadingProgression + ) -> Bool { // Here we use the default publication reading progression instead // of the custom one provided, otherwise the page position hints // might be wrong, and we could end up with only one-page spreads. - switch metadata.readingProgression { + switch readingProgression { case .ltr, .ttb, .auto: let firstPosition = first.properties.page ?? .left let secondPosition = second.properties.page ?? .right @@ -212,3 +198,115 @@ private extension Publication { } } } + +/// A resource displayed in a spread, with its reading order index. +struct EPUBSpreadResource { + /// Index of the resource in the reading order. + let index: ReadingOrder.Index + /// Link to the resource. + let link: Link + + /// Returns a JSON representation of the resource for the spread scripts. + func json(forBaseURL baseURL: AbsoluteURL, page: Properties.Page) -> [String: JSONValue] { + .init([ + "index": index, + "link": link, + "url": link.url(relativeTo: baseURL).string, + "page": page.rawValue, + ]) + } +} + +/// A spread displaying a single resource. +struct EPUBSingleSpread: EPUBSpreadProtocol, Loggable { + /// The resource displayed in the spread. + var resource: EPUBSpreadResource + + func contains(index: ReadingOrder.Index) -> Bool { + resource.index == index + } + + func positionCount(in readingOrder: ReadingOrder, positionsByReadingOrder: [[Locator]]) -> Int { + positionsByReadingOrder.getOrNil(resource.index)?.count ?? 0 + } + + func json(forBaseURL baseURL: AbsoluteURL, readingProgression: ReadingProgression) -> [JSONValue] { + [ + .object(resource.json( + forBaseURL: baseURL, + page: resource.link.properties.page ?? defaultPage(in: readingProgression) + )), + ] + } + + /// Returns the default spread position (left or right) for the single + /// resource, in the given reading progression. + /// + /// The first page (typically a cover) defaults to the starting page (right + /// for LTR). Other unpaired pages default to the leading position they + /// would have had in a spread pair. + private func defaultPage(in readingProgression: ReadingProgression) -> Properties.Page { + let isFirstPage = (resource.index == 0) + return switch readingProgression { + case .ltr: + isFirstPage ? .right : .left + case .rtl: + isFirstPage ? .left : .right + } + } +} + +/// A spread displaying two resources side by side (FXL only). +struct EPUBDoubleSpread: EPUBSpreadProtocol, Loggable { + /// The leading resource in the reading progression. + var first: EPUBSpreadResource + /// The trailing resource in the reading progression. + var second: EPUBSpreadResource + + /// Returns the left resource in the spread. + func left(for readingProgression: ReadingProgression) -> EPUBSpreadResource { + switch readingProgression { + case .ltr: + first + case .rtl: + second + } + } + + /// Returns the right resource in the spread. + func right(for readingProgression: ReadingProgression) -> EPUBSpreadResource { + switch readingProgression { + case .ltr: + second + case .rtl: + first + } + } + + func contains(index: ReadingOrder.Index) -> Bool { + first.index == index || second.index == index + } + + func positionCount(in readingOrder: ReadingOrder, positionsByReadingOrder: [[Locator]]) -> Int { + let firstPositions = positionsByReadingOrder.getOrNil(first.index)?.count ?? 0 + let secondPositions = positionsByReadingOrder.getOrNil(second.index)?.count ?? 0 + return firstPositions + secondPositions + } + + func json(forBaseURL baseURL: AbsoluteURL, readingProgression: ReadingProgression) -> [JSONValue] { + [ + .object(left(for: readingProgression).json(forBaseURL: baseURL, page: .left)), + .object(right(for: readingProgression).json(forBaseURL: baseURL, page: .right)), + ] + } +} + +extension Array where Element == EPUBSpread { + /// Returns the index of the first spread containing a resource with the + /// given `href`. + func firstIndexWithReadingOrderIndex(_ index: ReadingOrder.Index) -> Int? { + firstIndex { spread in + spread.contains(index: index) + } + } +} diff --git a/Sources/Navigator/EPUB/EPUBSpreadView.swift b/Sources/Navigator/EPUB/EPUBSpreadView.swift index 27a52c2b95..b4eaa87834 100644 --- a/Sources/Navigator/EPUB/EPUBSpreadView.swift +++ b/Sources/Navigator/EPUB/EPUBSpreadView.swift @@ -1,11 +1,10 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 ReadiumShared -import SwiftSoup @preconcurrency import WebKit protocol EPUBSpreadViewDelegate: AnyObject { @@ -22,7 +21,7 @@ protocol EPUBSpreadViewDelegate: AnyObject { func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String, clickEvent: ClickEvent?) /// Called when the user tapped on a decoration. - func spreadView(_ spreadView: EPUBSpreadView, didActivateDecoration id: Decoration.Id, inGroup group: String, frame: CGRect?, point: CGPoint?) + func spreadView(_ spreadView: EPUBSpreadView, didActivateDecoration id: Decoration.Id, inGroup group: DecorationGroup, frame: CGRect?, point: CGPoint?) /// Called when the text selection changes. func spreadView(_ spreadView: EPUBSpreadView, selectionDidChange text: Locator.Text?, frame: CGRect) @@ -51,7 +50,7 @@ class EPUBSpreadView: UIView, Loggable, PageView { let webView: WebView - private var lastClick: ClickEvent? = nil + private var lastClick: ClickEvent? /// If YES, the content will be faded in once loaded. let animatedLoad: Bool @@ -60,6 +59,7 @@ class EPUBSpreadView: UIView, Loggable, PageView { private var activityIndicatorStopWorkItem: DispatchWorkItem? private(set) var isSpreadLoaded = false + private var spreadLoadTask: Task? required init( viewModel: EPUBNavigatorViewModel, @@ -70,7 +70,18 @@ class EPUBSpreadView: UIView, Loggable, PageView { self.viewModel = viewModel self.spread = spread self.animatedLoad = animatedLoad - webView = WebView(editingActions: viewModel.editingActions) + + let config = WKWebViewConfiguration() + config.setURLSchemeHandler(viewModel.server, forURLScheme: viewModel.server.scheme) + config.mediaTypesRequiringUserActionForPlayback = .all + + // Disable the Apple Intelligence Writing tools in the web views. + // See https://github.com/readium/swift-toolkit/issues/509#issuecomment-2577780749 + if #available(iOS 18.0, *) { + config.writingToolsBehavior = .none + } + + webView = WebView(editingActions: viewModel.editingActions, configuration: config) super.init(frame: .zero) @@ -95,6 +106,18 @@ class EPUBSpreadView: UIView, Loggable, PageView { deinit { NotificationCenter.default.removeObserver(self) + clear() + } + + /// Called when the spread view is removed from the view hierarchy, to + /// clear pending operations and retain cycles. + func clear() { + webView.stopLoading() + + spreadLoadTask?.cancel() + spreadLoadTask = nil + + // Disable JS messages to break WKUserContentController reference. disableJSMessages() } @@ -126,14 +149,18 @@ class EPUBSpreadView: UIView, Loggable, PageView { webView.scrollView } + override func willMove(toSuperview newSuperview: UIView?) { + super.willMove(toSuperview: newSuperview) + + if newSuperview == nil { + clear() + } + } + override func didMoveToSuperview() { super.didMoveToSuperview() - if superview == nil { - disableJSMessages() - // Fixing an iOS 9 bug by explicitly clearing scrollView.delegate before deinitialization - scrollView.delegate = nil - } else { + if superview != nil { enableJSMessages() scrollView.delegate = self } @@ -150,9 +177,9 @@ class EPUBSpreadView: UIView, Loggable, PageView { log(.trace, "Evaluate script: \(script)") return await withCheckedContinuation { continuation in - webView.evaluateJavaScript(script) { res, error in + webView.evaluateJavaScript(script) { [weak self] res, error in if let error = error { - self.log(.error, error) + self?.log(.error, error) continuation.resume(returning: .failure(error)) } else { continuation.resume(returning: .success(res ?? ())) @@ -266,7 +293,8 @@ class EPUBSpreadView: UIView, Loggable, PageView { /// Called by the javascript code when the spread contents is fully loaded. /// The JS message `spreadLoaded` needs to be emitted by a subclass script, EPUBSpreadView's scripts don't. private func spreadDidLoad(_ body: Any) { - Task { @MainActor in + spreadLoadTask?.cancel() + spreadLoadTask = Task { @MainActor in isSpreadLoaded = true applySettings() await spreadDidLoad() @@ -322,7 +350,7 @@ class EPUBSpreadView: UIView, Loggable, PageView { let selection = body as? [String: Any], let hrefString = selection["href"] as? String, let href = AnyURL(string: hrefString), - let text = try? Locator.Text(json: selection["text"]), + let text = try? Locator.Text(json: JSONValue(selection["text"])), var frame = CGRect(json: selection["rect"]) else { focusedResource = nil @@ -350,7 +378,7 @@ class EPUBSpreadView: UIView, Loggable, PageView { 0 ... 1 } - func go(to location: PageLocation) async { + func go(to location: PageLocation, animated: Bool) async { fatalError("go(to:) must be implemented in subclasses") } @@ -374,10 +402,16 @@ class EPUBSpreadView: UIView, Loggable, PageView { func findFirstVisibleElementLocator() async -> Locator? { let result = await evaluateScript("readium.findFirstVisibleLocator()") do { - let resource = viewModel.readingOrder[spread.leading] - let locator = try Locator(json: result.get())? - .copy(href: resource.url(), mediaType: resource.mediaType ?? .xhtml) - return locator + let link = spread.first.link + + guard + let json = try JSONValue(result.get()), + let locator = try Locator(json: json) + else { + return nil + } + return locator.copy(href: link.url(), mediaType: link.mediaType ?? .xhtml) + } catch { log(.error, error) return nil @@ -426,7 +460,7 @@ class EPUBSpreadView: UIView, Loggable, PageView { } } - // Removes message handlers (preventing strong reference cycle). + /// Removes message handlers (preventing strong reference cycle). private func disableJSMessages() { guard JSMessagesEnabled else { return @@ -515,12 +549,12 @@ extension EPUBSpreadView: WKNavigationDelegate { var policy: WKNavigationActionPolicy = .allow if navigationAction.navigationType == .linkActivated { - if let url = navigationAction.request.url?.httpURL { + if let url = navigationAction.request.url { // Check if url is internal or external if let relativeURL = viewModel.publicationBaseURL.relativize(url) { delegate?.spreadView(self, didTapOnInternalLink: relativeURL.string, clickEvent: lastClick) } else { - delegate?.spreadView(self, didTapOnExternalURL: url.url) + delegate?.spreadView(self, didTapOnExternalURL: url) } policy = .cancel @@ -603,7 +637,7 @@ private extension EPUBSpreadView { return } - trace("stopping activity indicator because spread \(viewModel.readingOrder[spread.leading].href) did not load") + trace("stopping activity indicator because spread \(spread.first.link.href) did not load") activityIndicatorView?.stopAnimating() } @@ -745,7 +779,6 @@ private extension KeyEvent { key = .tab case "Space": key = .space - case "ArrowDown": key = .arrowDown case "ArrowLeft": @@ -754,7 +787,6 @@ private extension KeyEvent { key = .arrowRight case "ArrowUp": key = .arrowUp - case "End": key = .end case "Home": @@ -763,7 +795,6 @@ private extension KeyEvent { key = .pageDown case "PageUp": key = .pageUp - case "MetaLeft", "MetaRight": key = .command case "ControlLeft", "ControlRight": @@ -772,12 +803,10 @@ private extension KeyEvent { key = .option case "ShiftLeft", "ShiftRight": key = .shift - case "Backspace": key = .backspace case "Escape": key = .escape - default: guard let char = dict["key"] as? String else { return nil diff --git a/Sources/Navigator/EPUB/EPUBViewportAndLocationCalculator.swift b/Sources/Navigator/EPUB/EPUBViewportAndLocationCalculator.swift new file mode 100644 index 0000000000..2d81b754e0 --- /dev/null +++ b/Sources/Navigator/EPUB/EPUBViewportAndLocationCalculator.swift @@ -0,0 +1,122 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumShared + +/// Computes the current `Locator` and `Viewport` from a spread's visible +/// progressions and the publication's position list. +enum EPUBViewportAndLocationCalculator { + /// Computes the locator and viewport for the currently visible spread. + /// + /// - Parameters: + /// - readingOrderIndices: Closed range of reading-order indices visible + /// in the spread (single value for reflowable, two values for FXL + /// double spreads). + /// - progression: Returns the visible scroll progression range (0–1) + /// for a given reading-order index. For fixed-layout resources this + /// is always `0...1`. + /// - readingOrder: The publication's reading order links. + /// - positionsByReadingOrder: Positions grouped by reading-order index. + /// May be empty if the publication has no positions. + /// - tableOfContentsTitleByHref: Mapping from resource URL to its table- + /// of-contents title, used to populate `Locator.title`. + /// - fallbackLocator: Called with the first visible link when no + /// positions are available; should return a basic locator for that + /// link (e.g. from `Publication.locate(_:)`). + static func compute( + readingOrderIndices: ClosedRange, + progression: (Int) -> ClosedRange, + readingOrder: [Link], + positionsByReadingOrder: [[Locator]], + tableOfContentsTitleByHref: [AnyURL: String], + fallbackLocator: (Link) async -> Locator? + ) async -> (locator: Locator?, viewport: EPUBNavigatorViewController.Viewport) { + let visibleReadingOrder: [(index: Int, href: AnyURL)] = readingOrderIndices + .map { ($0, readingOrder[$0].url()) } + + var viewport = EPUBNavigatorViewController.Viewport( + readingOrder: visibleReadingOrder.map(\.href), + progressions: visibleReadingOrder.reduce(into: [:]) { acc, i in + acc[i.href] = progression(i.index) + }, + positions: nil + ) + + let firstIndex = readingOrderIndices.lowerBound + let lastIndex = readingOrderIndices.upperBound + let firstProgressionInFirstResource = min(max(progression(firstIndex).lowerBound, 0.0), 1.0) + let lastProgressionInLastResource = min(max(progression(lastIndex).upperBound, 0.0), 1.0) + + let link = readingOrder[firstIndex] + let locator: Locator? + + if + // The positions are not always available, for example a Readium + // WebPub doesn't have any unless a Publication Positions Web + // Service is provided. + let positionsOfFirstResource = positionsByReadingOrder.getOrNil(firstIndex), + let positionsOfLastResource = positionsByReadingOrder.getOrNil(lastIndex), + !positionsOfFirstResource.isEmpty, + !positionsOfLastResource.isEmpty + { + // Map the resource progression (0–1) to a position index using + // ceil, so the reported position advances as soon as the reader + // enters it. This pairs with lastPositionIndex which uses + // ceil(x) - 1 to find the last fully-entered position. + let firstPositionIndex = Int(ceil( + firstProgressionInFirstResource * Double(positionsOfFirstResource.count - 1) + )) + let lastPositionIndex: Int = (lastProgressionInLastResource == 1.0) + ? positionsOfLastResource.count - 1 + : max( + // In a single-resource spread, clamp against firstPositionIndex + // to prevent an invalid lastPositionIndex < firstPositionIndex + // range. In a two-resource spread the two indices are into + // different arrays, so clamp against 0 instead. + firstIndex == lastIndex ? firstPositionIndex : 0, + Int(ceil(lastProgressionInLastResource * Double(positionsOfLastResource.count - 1))) - 1 + ) + + // Compute a continuous totalProgression by linearly interpolating + // the resource-level progression within the resource's global + // range. The resource's range spans from the totalProgression of + // its first position to the totalProgression of the next resource's + // first position (or 1.0 for the last resource). + let resourceTotalProgressionStart = positionsOfFirstResource.first?.locations.totalProgression ?? 0.0 + let resourceTotalProgressionEnd = positionsByReadingOrder.getOrNil(firstIndex + 1)? + .first?.locations.totalProgression ?? 1.0 + let continuousTotalProgression = + resourceTotalProgressionStart + + firstProgressionInFirstResource + * (resourceTotalProgressionEnd - resourceTotalProgressionStart) + + // Build the locator from the nearest position, then override + // progression fields with the actual continuous scroll values. + locator = positionsOfFirstResource[firstPositionIndex].copy( + title: tableOfContentsTitleByHref[link.url()], + locations: { + $0.progression = firstProgressionInFirstResource + $0.totalProgression = continuousTotalProgression + } + ) + + if + let firstPosition = locator?.locations.position, + let lastPosition = positionsOfLastResource[lastPositionIndex].locations.position + { + viewport.positions = firstPosition ... lastPosition + } + + } else { + locator = await fallbackLocator(link)?.copy( + locations: { $0.progression = firstProgressionInFirstResource } + ) + } + + return (locator, viewport) + } +} diff --git a/Sources/Navigator/EPUB/HTMLDecorationTemplate.swift b/Sources/Navigator/EPUB/HTMLDecorationTemplate.swift index b5824197f8..ef1f945b96 100644 --- a/Sources/Navigator/EPUB/HTMLDecorationTemplate.swift +++ b/Sources/Navigator/EPUB/HTMLDecorationTemplate.swift @@ -1,15 +1,15 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 Foundation -import SwiftSoup +import ReadiumShared import UIKit /// An `HTMLDecorationTemplate` renders a `Decoration` into a set of HTML elements and associated stylesheet. -public struct HTMLDecorationTemplate { +public struct HTMLDecorationTemplate: JSONObjectEncodable { /// Determines the number of created HTML elements and their position relative to the matching DOM range. public enum Layout: String { /// A single HTML element covering the smallest region containing all CSS border boxes. @@ -46,40 +46,51 @@ public struct HTMLDecorationTemplate { self.init(layout: layout, width: width, element: { _ in element }, stylesheet: stylesheet) } - public var json: [String: Any] { - [ + public var jsonObject: [String: JSONValue] { + .init([ "layout": layout.rawValue, "width": width.rawValue, - "stylesheet": stylesheet as Any, - ] + "stylesheet": stylesheet, + ]) } /// Creates the default list of decoration styles with associated HTML templates. + /// + /// - Parameters: + /// - defaultTint: Default highlight/underline color when the decoration + /// has no tint set. + /// - lineWeight: Thickness in pixels of the underline stroke. + /// - cornerRadius: Border radius in pixels applied to each decoration box. + /// - alpha: Opacity of the highlight fill color (0–1). + /// - experimentalPositioning: When true, places decorations behind the + /// publication text using a negative z-index, preventing the highlight + /// from affecting text color. This may not work with all publications. public static func defaultTemplates( defaultTint: UIColor = .yellow, lineWeight: Int = 2, cornerRadius: Int = 3, - alpha: Double = 0.3 + alpha: Double = 0.3, + experimentalPositioning: Bool = false ) -> [Decoration.Style.Id: HTMLDecorationTemplate] { let padding = UIEdgeInsets(top: 0, left: 1, bottom: 0, right: 1) return [ - .highlight: .highlight(defaultTint: defaultTint, padding: padding, lineWeight: lineWeight, cornerRadius: cornerRadius, alpha: alpha), - .underline: .underline(defaultTint: defaultTint, padding: padding, lineWeight: lineWeight, cornerRadius: cornerRadius, alpha: alpha), + .highlight: .highlight(defaultTint: defaultTint, padding: padding, lineWeight: lineWeight, cornerRadius: cornerRadius, alpha: alpha, experimentalPositioning: experimentalPositioning), + .underline: .underline(defaultTint: defaultTint, padding: padding, lineWeight: lineWeight, cornerRadius: cornerRadius, alpha: alpha, experimentalPositioning: experimentalPositioning), ] } /// Creates a new decoration template for the `highlight` style. - public static func highlight(defaultTint: UIColor, padding: UIEdgeInsets, lineWeight: Int, cornerRadius: Int, alpha: Double) -> HTMLDecorationTemplate { - makeTemplate(asHighlight: true, defaultTint: defaultTint, padding: padding, lineWeight: lineWeight, cornerRadius: cornerRadius, alpha: alpha) + public static func highlight(defaultTint: UIColor, padding: UIEdgeInsets, lineWeight: Int, cornerRadius: Int, alpha: Double, experimentalPositioning: Bool = false) -> HTMLDecorationTemplate { + makeTemplate(asHighlight: true, defaultTint: defaultTint, padding: padding, lineWeight: lineWeight, cornerRadius: cornerRadius, alpha: alpha, experimentalPositioning: experimentalPositioning) } /// Creates a new decoration template for the `underline` style. - public static func underline(defaultTint: UIColor, padding: UIEdgeInsets, lineWeight: Int, cornerRadius: Int, alpha: Double) -> HTMLDecorationTemplate { - makeTemplate(asHighlight: false, defaultTint: defaultTint, padding: padding, lineWeight: lineWeight, cornerRadius: cornerRadius, alpha: alpha) + public static func underline(defaultTint: UIColor, padding: UIEdgeInsets, lineWeight: Int, cornerRadius: Int, alpha: Double, experimentalPositioning: Bool = false) -> HTMLDecorationTemplate { + makeTemplate(asHighlight: false, defaultTint: defaultTint, padding: padding, lineWeight: lineWeight, cornerRadius: cornerRadius, alpha: alpha, experimentalPositioning: experimentalPositioning) } /// - Parameter asHighlight: When true, the non active style is of an highlight. Otherwise, it is an underline. - private static func makeTemplate(asHighlight: Bool, defaultTint: UIColor, padding: UIEdgeInsets, lineWeight: Int, cornerRadius: Int, alpha: Double) -> HTMLDecorationTemplate { + private static func makeTemplate(asHighlight: Bool, defaultTint: UIColor, padding: UIEdgeInsets, lineWeight: Int, cornerRadius: Int, alpha: Double, experimentalPositioning: Bool = false) -> HTMLDecorationTemplate { let className = makeUniqueClassName(key: asHighlight ? "highlight" : "underline") return HTMLDecorationTemplate( layout: .boxes, @@ -94,6 +105,11 @@ public struct HTMLDecorationTemplate { if !asHighlight || isActive { css += "--underline-color: \(tint.cssValue());" } + if experimentalPositioning { + // Experimental positioning: + // Decoration is placed behind the publication's text, to prevent it from affecting text-color. + css += "--decoration-z-index: -1;" + } return "

" }, stylesheet: @@ -104,6 +120,7 @@ public struct HTMLDecorationTemplate { border-radius: \(cornerRadius)px; box-sizing: border-box; border: 0 solid var(--underline-color); + z-index: var(--decoration-z-index); } /* Horizontal (default) */ @@ -121,7 +138,7 @@ public struct HTMLDecorationTemplate { [data-writing-mode="vertical-lr"].\(className), [data-writing-mode="sideways-lr"].\(className) { border-right-width: \(lineWeight)px; - } + } """ ) } diff --git a/Sources/Navigator/EPUB/Preferences/EPUBPreferences+Legacy.swift b/Sources/Navigator/EPUB/Preferences/EPUBPreferences+Legacy.swift index fbac10c668..f74058ac41 100644 --- a/Sources/Navigator/EPUB/Preferences/EPUBPreferences+Legacy.swift +++ b/Sources/Navigator/EPUB/Preferences/EPUBPreferences+Legacy.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/EPUB/Preferences/EPUBPreferences.swift b/Sources/Navigator/EPUB/Preferences/EPUBPreferences.swift index 7b19576774..d2a22aaf4a 100644 --- a/Sources/Navigator/EPUB/Preferences/EPUBPreferences.swift +++ b/Sources/Navigator/EPUB/Preferences/EPUBPreferences.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -23,6 +23,13 @@ public struct EPUBPreferences: ConfigurablePreferences { /// Darkens images by the given percentage. public var darkenImages: Double? + /// Method for fitting the content of a fixed-layout publication within the + /// viewport. + /// + /// - `auto` or `page`: Fit entire page within viewport (default). + /// - `width`: Fit page width, allow vertical scrolling if needed. + public var fit: Fit? + /// Default typeface for the text. public var fontFamily: FontFamily? @@ -56,6 +63,12 @@ public struct EPUBPreferences: ConfigurablePreferences { /// Leading line height. public var lineHeight: Double? + /// Indicates whether the first page should be displayed alone and centered + /// instead of alongside the second page. + /// + /// This is only effective if spreads are enabled. + public var offsetFirstPage: Bool? + /// Text indentation for paragraphs. public var paragraphIndent: Double? @@ -99,6 +112,7 @@ public struct EPUBPreferences: ConfigurablePreferences { blendImages: Bool? = nil, columnCount: Int? = nil, darkenImages: Double? = nil, + fit: Fit? = nil, fontFamily: FontFamily? = nil, fontSize: Double? = nil, fontWeight: Double? = nil, @@ -110,6 +124,7 @@ public struct EPUBPreferences: ConfigurablePreferences { ligatures: Bool? = nil, lineLength: Double? = nil, lineHeight: Double? = nil, + offsetFirstPage: Bool? = nil, paragraphIndent: Double? = nil, paragraphSpacing: Double? = nil, readingProgression: ReadingProgression? = nil, @@ -126,6 +141,7 @@ public struct EPUBPreferences: ConfigurablePreferences { self.blendImages = blendImages self.columnCount = columnCount self.darkenImages = darkenImages + self.fit = fit self.fontFamily = fontFamily self.fontSize = fontSize.map { max($0, 0) } self.fontWeight = fontWeight?.clamped(to: 0.0 ... 2.5) @@ -137,6 +153,7 @@ public struct EPUBPreferences: ConfigurablePreferences { self.ligatures = ligatures self.lineLength = lineLength.map { max($0, 0) } self.lineHeight = lineHeight + self.offsetFirstPage = offsetFirstPage self.paragraphIndent = paragraphIndent self.paragraphSpacing = paragraphSpacing.map { max($0, 0) } self.readingProgression = readingProgression @@ -156,6 +173,7 @@ public struct EPUBPreferences: ConfigurablePreferences { blendImages: other.blendImages ?? blendImages, columnCount: other.columnCount ?? columnCount, darkenImages: other.darkenImages ?? darkenImages, + fit: other.fit ?? fit, fontFamily: other.fontFamily ?? fontFamily, fontSize: other.fontSize ?? fontSize, fontWeight: other.fontWeight ?? fontWeight, @@ -167,6 +185,7 @@ public struct EPUBPreferences: ConfigurablePreferences { ligatures: other.ligatures ?? ligatures, lineLength: other.lineLength ?? lineLength, lineHeight: other.lineHeight ?? lineHeight, + offsetFirstPage: other.offsetFirstPage ?? offsetFirstPage, paragraphIndent: other.paragraphIndent ?? paragraphIndent, paragraphSpacing: other.paragraphSpacing ?? paragraphSpacing, readingProgression: other.readingProgression ?? readingProgression, @@ -186,6 +205,7 @@ public struct EPUBPreferences: ConfigurablePreferences { public func filterSharedPreferences() -> EPUBPreferences { var prefs = self prefs.language = nil + prefs.offsetFirstPage = nil prefs.readingProgression = nil prefs.spread = nil prefs.verticalText = nil @@ -197,6 +217,7 @@ public struct EPUBPreferences: ConfigurablePreferences { public func filterPublicationPreferences() -> EPUBPreferences { EPUBPreferences( language: language, + offsetFirstPage: offsetFirstPage, readingProgression: readingProgression, spread: spread, verticalText: verticalText @@ -204,16 +225,24 @@ public struct EPUBPreferences: ConfigurablePreferences { } @available(*, unavailable, message: "Use lineLength instead") - public var pageMargins: Double? { nil } + public var pageMargins: Double? { + nil + } @available(*, unavailable, message: "Not available anymore") - public var typeScale: Double? { nil } + public var typeScale: Double? { + nil + } @available(*, unavailable, message: "Not needed anymore") - public var publisherStyles: Bool? { nil } + public var publisherStyles: Bool? { + nil + } @available(*, unavailable, message: "Use invertImages or darkenImages instead") - public var imageFilter: ImageFilter? { nil } + public var imageFilter: ImageFilter? { + nil + } @available(*, unavailable, message: "Use the other initializer") public init( @@ -242,5 +271,7 @@ public struct EPUBPreferences: ConfigurablePreferences { typeScale: Double? = nil, verticalText: Bool? = nil, wordSpacing: Double? = nil - ) { fatalError() } + ) { + fatalError() + } } diff --git a/Sources/Navigator/EPUB/Preferences/EPUBPreferencesEditor.swift b/Sources/Navigator/EPUB/Preferences/EPUBPreferencesEditor.swift index a07dacda1a..58a238e055 100644 --- a/Sources/Navigator/EPUB/Preferences/EPUBPreferencesEditor.swift +++ b/Sources/Navigator/EPUB/Preferences/EPUBPreferencesEditor.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -21,12 +21,7 @@ public final class EPUBPreferencesEditor: StatefulPreferencesEditor = + enumPreference( + preference: \.fit, + setting: \.fit, + defaultEffectiveValue: defaults.fit ?? .auto, + isEffective: { [layout] _ in layout == .fixed }, + supportedValues: [.auto, .page, .width] + ) + /// Default typeface for the text. /// /// Only effective with reflowable publications. @@ -288,6 +295,25 @@ public final class EPUBPreferencesEditor: StatefulPreferencesEditor = + preference( + preference: \.offsetFirstPage, + setting: \.offsetFirstPage, + isEffective: { [layout] in + layout == .fixed + && $0.settings.spread != .never + } + ) + /// Text indentation for paragraphs. /// /// Only effective when: @@ -457,14 +483,22 @@ public final class EPUBPreferencesEditor: StatefulPreferencesEditor { fatalError() } + public var pageMargins: AnyRangePreference { + fatalError() + } @available(*, unavailable, message: "Not available anymore") - public var typeScale: AnyRangePreference { fatalError() } + public var typeScale: AnyRangePreference { + fatalError() + } @available(*, unavailable, message: "Not needed anymore") - public var publisherStyles: AnyPreference { fatalError() } + public var publisherStyles: AnyPreference { + fatalError() + } @available(*, unavailable, message: "Use darkenImages and invertImages instead") - public var imageFilter: AnyEnumPreference { fatalError() } + public var imageFilter: AnyEnumPreference { + fatalError() + } } diff --git a/Sources/Navigator/EPUB/Preferences/EPUBSettings.swift b/Sources/Navigator/EPUB/Preferences/EPUBSettings.swift index 684da00792..ba374ce0e6 100644 --- a/Sources/Navigator/EPUB/Preferences/EPUBSettings.swift +++ b/Sources/Navigator/EPUB/Preferences/EPUBSettings.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -15,6 +15,7 @@ public struct EPUBSettings: ConfigurableSettings { public var blendImages: Bool? public var columnCount: Int public var darkenImages: Double? + public var fit: Fit public var fontFamily: FontFamily? public var fontSize: Double public var fontWeight: Double? @@ -26,6 +27,7 @@ public struct EPUBSettings: ConfigurableSettings { public var ligatures: Bool? public var lineLength: Double public var lineHeight: Double? + public var offsetFirstPage: Bool? public var paragraphIndent: Double? public var paragraphSpacing: Double? public var readingProgression: ReadingProgression @@ -49,6 +51,7 @@ public struct EPUBSettings: ConfigurableSettings { blendImages: Bool?, columnCount: Int, darkenImages: Double?, + fit: Fit, fontFamily: FontFamily?, fontSize: Double, fontWeight: Double?, @@ -60,6 +63,7 @@ public struct EPUBSettings: ConfigurableSettings { ligatures: Bool?, lineLength: Double, lineHeight: Double?, + offsetFirstPage: Bool?, paragraphIndent: Double?, paragraphSpacing: Double?, readingProgression: ReadingProgression, @@ -76,6 +80,7 @@ public struct EPUBSettings: ConfigurableSettings { self.blendImages = blendImages self.columnCount = columnCount self.darkenImages = darkenImages + self.fit = fit self.fontFamily = fontFamily self.fontSize = fontSize self.fontWeight = fontWeight @@ -87,6 +92,7 @@ public struct EPUBSettings: ConfigurableSettings { self.ligatures = ligatures self.lineLength = lineLength self.lineHeight = lineHeight + self.offsetFirstPage = offsetFirstPage self.paragraphIndent = paragraphIndent self.paragraphSpacing = paragraphSpacing self.readingProgression = readingProgression @@ -131,8 +137,8 @@ public struct EPUBSettings: ConfigurableSettings { ?? defaults.scroll ?? false - /// We disable pagination with vertical text, because CSS columns don't support it properly. - /// See https://github.com/readium/swift-toolkit/discussions/370 + // We disable pagination with vertical text, because CSS columns don't support it properly. + // See https://github.com/readium/swift-toolkit/discussions/370 if verticalText { scroll = true } @@ -144,6 +150,9 @@ public struct EPUBSettings: ConfigurableSettings { ?? defaults.columnCount ?? 1, darkenImages: preferences.darkenImages, + fit: preferences.fit + ?? defaults.fit + ?? .auto, fontFamily: preferences.fontFamily, fontSize: preferences.fontSize ?? defaults.fontSize @@ -164,6 +173,8 @@ public struct EPUBSettings: ConfigurableSettings { ?? 1.0, lineHeight: preferences.lineHeight ?? defaults.lineHeight, + offsetFirstPage: preferences.offsetFirstPage + ?? defaults.offsetFirstPage, paragraphIndent: preferences.paragraphIndent ?? defaults.paragraphIndent, paragraphSpacing: preferences.paragraphSpacing @@ -188,13 +199,19 @@ public struct EPUBSettings: ConfigurableSettings { } @available(*, unavailable, message: "Not supported anymore") - public var typeScale: Double? { nil } + public var typeScale: Double? { + nil + } @available(*, unavailable, message: "Use lineLength") - public var pageMargins: Double? { nil } + public var pageMargins: Double? { + nil + } @available(*, unavailable, message: "Not needed anymore") - public var publisherStyles: Bool? { nil } + public var publisherStyles: Bool? { + nil + } @available(*, unavailable, message: "Use the other initializer") public init( @@ -223,7 +240,9 @@ public struct EPUBSettings: ConfigurableSettings { typeScale: Double?, verticalText: Bool, wordSpacing: Double? - ) { fatalError() } + ) { + fatalError() + } } /// Default setting values for the EPUB navigator. @@ -234,6 +253,7 @@ public struct EPUBSettings: ConfigurableSettings { /// See `EPUBPreferences`. public struct EPUBDefaults { public var columnCount: Int? + public var fit: Fit? public var fontSize: Double? public var fontWeight: Double? public var hyphens: Bool? @@ -242,6 +262,7 @@ public struct EPUBDefaults { public var ligatures: Bool? public var lineLength: Double? public var lineHeight: Double? + public var offsetFirstPage: Bool? public var paragraphIndent: Double? public var paragraphSpacing: Double? public var readingProgression: ReadingProgression? @@ -253,6 +274,7 @@ public struct EPUBDefaults { public init( columnCount: Int? = nil, + fit: Fit? = nil, fontSize: Double? = nil, fontWeight: Double? = nil, hyphens: Bool? = nil, @@ -261,6 +283,7 @@ public struct EPUBDefaults { ligatures: Bool? = nil, lineLength: Double? = nil, lineHeight: Double? = nil, + offsetFirstPage: Bool? = nil, paragraphIndent: Double? = nil, paragraphSpacing: Double? = nil, readingProgression: ReadingProgression? = nil, @@ -268,10 +291,10 @@ public struct EPUBDefaults { spread: Spread? = nil, textAlign: TextAlignment? = nil, textNormalization: Bool? = nil, - typeScale: Double? = nil, wordSpacing: Double? = nil ) { self.columnCount = columnCount + self.fit = fit self.fontSize = fontSize self.fontWeight = fontWeight self.hyphens = hyphens @@ -280,6 +303,7 @@ public struct EPUBDefaults { self.ligatures = ligatures self.lineLength = lineLength self.lineHeight = lineHeight + self.offsetFirstPage = offsetFirstPage self.paragraphIndent = paragraphIndent self.paragraphSpacing = paragraphSpacing self.readingProgression = readingProgression @@ -291,20 +315,29 @@ public struct EPUBDefaults { } @available(*, unavailable, message: "Use lineLength instead") - public var pageMargins: Double? { nil } + public var pageMargins: Double? { + nil + } @available(*, unavailable, message: "Not supported anymore") - public var typeScale: Double? { nil } + public var typeScale: Double? { + nil + } @available(*, unavailable, message: "Not needed anymore") - public var publisherStyles: Bool? { nil } + public var publisherStyles: Bool? { + nil + } @available(*, unavailable, message: "Not supported anymore as a defaults") - public var imageFilter: ImageFilter? { nil } + public var imageFilter: ImageFilter? { + nil + } @available(*, unavailable, message: "Use the other initializer") public init( columnCount: ColumnCount? = nil, + fit: Fit? = nil, fontSize: Double? = nil, fontWeight: Double? = nil, hyphens: Bool? = nil, @@ -313,6 +346,7 @@ public struct EPUBDefaults { letterSpacing: Double? = nil, ligatures: Bool? = nil, lineHeight: Double? = nil, + offsetFirstPage: Bool? = nil, pageMargins: Double? = nil, paragraphIndent: Double? = nil, paragraphSpacing: Double? = nil, @@ -324,7 +358,9 @@ public struct EPUBDefaults { textNormalization: Bool? = nil, typeScale: Double? = nil, wordSpacing: Double? = nil - ) { fatalError() } + ) { + fatalError() + } } private extension Language { diff --git a/Sources/Navigator/EPUB/Scripts/package.json b/Sources/Navigator/EPUB/Scripts/package.json index 4aad9b38ec..bd7814bf9a 100644 --- a/Sources/Navigator/EPUB/Scripts/package.json +++ b/Sources/Navigator/EPUB/Scripts/package.json @@ -7,6 +7,7 @@ "private": true, "scripts": { "bundle": "webpack", + "bundle:minify": "MINIFY_CSS=true webpack", "lint": "eslint src", "checkformat": "prettier --check '**/*.js'", "format": "prettier --list-different --write '**/*.js'" @@ -17,7 +18,10 @@ "devDependencies": { "@babel/core": "^7.23.9", "@babel/preset-env": "^7.23.9", + "@readium/css": "^2.0.0", "babel-loader": "^8.3.0", + "clean-css": "^5.3.3", + "copy-webpack-plugin": "^14.0.0", "eslint": "^7.32.0", "prettier": "2.3.1", "webpack": "^5.90.1", diff --git a/Sources/Navigator/EPUB/Scripts/pnpm-lock.yaml b/Sources/Navigator/EPUB/Scripts/pnpm-lock.yaml index 75eb2605e6..27b19169e8 100644 --- a/Sources/Navigator/EPUB/Scripts/pnpm-lock.yaml +++ b/Sources/Navigator/EPUB/Scripts/pnpm-lock.yaml @@ -25,9 +25,18 @@ devDependencies: '@babel/preset-env': specifier: ^7.23.9 version: 7.23.9(@babel/core@7.23.9) + '@readium/css': + specifier: ^2.0.0 + version: 2.0.0 babel-loader: specifier: ^8.3.0 version: 8.3.0(@babel/core@7.23.9)(webpack@5.90.1) + clean-css: + specifier: ^5.3.3 + version: 5.3.3 + copy-webpack-plugin: + specifier: ^14.0.0 + version: 14.0.0(webpack@5.90.1) eslint: specifier: ^7.32.0 version: 7.32.0 @@ -1305,6 +1314,10 @@ packages: resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} dev: false + /@readium/css@2.0.0: + resolution: {integrity: sha512-Cr+AlHf5JqdwWPQ3ihw4HzsN3BkYyhOnD/fx6/+Y1QrTr1cIj1/xpGmixYyxn3kfLME00CX+OfhJeScYB1ANIw==} + dev: true + /@types/eslint-scope@3.7.7: resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} dependencies: @@ -1512,6 +1525,17 @@ packages: hasBin: true dev: true + /ajv-formats@2.1.1(ajv@8.12.0): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.12.0 + dev: true + /ajv-keywords@3.5.2(ajv@6.12.6): resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -1520,6 +1544,15 @@ packages: ajv: 6.12.6 dev: true + /ajv-keywords@5.1.0(ajv@8.12.0): + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + dependencies: + ajv: 8.12.0 + fast-deep-equal: 3.1.3 + dev: true + /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -1726,6 +1759,13 @@ packages: engines: {node: '>=6.0'} dev: true + /clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + dependencies: + source-map: 0.6.1 + dev: true + /clone-deep@4.0.1: resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} engines: {node: '>=6'} @@ -1781,6 +1821,20 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true + /copy-webpack-plugin@14.0.0(webpack@5.90.1): + resolution: {integrity: sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==} + engines: {node: '>= 20.9.0'} + peerDependencies: + webpack: ^5.1.0 + dependencies: + glob-parent: 6.0.2 + normalize-path: 3.0.0 + schema-utils: 4.3.3 + serialize-javascript: 7.0.3 + tinyglobby: 0.2.15 + webpack: 5.90.1(webpack-cli@5.1.4) + dev: true + /core-js-compat@3.35.1: resolution: {integrity: sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==} dependencies: @@ -2104,6 +2158,18 @@ packages: engines: {node: '>= 4.9.1'} dev: true + /fdir@6.5.0(picomatch@4.0.3): + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + dependencies: + picomatch: 4.0.3 + dev: true + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2209,6 +2275,13 @@ packages: is-glob: 4.0.3 dev: true + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + /glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} dev: true @@ -2646,6 +2719,11 @@ packages: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} dev: false @@ -2732,6 +2810,11 @@ packages: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: true + /picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + dev: true + /pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -2907,6 +2990,16 @@ packages: ajv-keywords: 3.5.2(ajv@6.12.6) dev: true + /schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + ajv-keywords: 5.1.0(ajv@8.12.0) + dev: true + /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2926,6 +3019,11 @@ packages: randombytes: 2.1.0 dev: true + /serialize-javascript@7.0.3: + resolution: {integrity: sha512-h+cZ/XXarqDgCjo+YSyQU/ulDEESGGf8AMK9pPNmhNSl/FzPl6L8pMp1leca5z6NuG6tvV/auC8/43tmovowww==} + engines: {node: '>=20.0.0'} + dev: true + /set-function-length@1.2.1: resolution: {integrity: sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==} engines: {node: '>= 0.4'} @@ -3142,6 +3240,14 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + dev: true + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} diff --git a/Sources/Navigator/EPUB/Scripts/src/fixed-page.js b/Sources/Navigator/EPUB/Scripts/src/fixed-page.js index f5bc1c12e1..15baa4bcf4 100644 --- a/Sources/Navigator/EPUB/Scripts/src/fixed-page.js +++ b/Sources/Navigator/EPUB/Scripts/src/fixed-page.js @@ -4,30 +4,66 @@ // available in the top-level LICENSE file of the project. // +// Page layout types. +export const PageType = { + SINGLE: "single", + SPREAD_LEFT: "spread-left", + SPREAD_RIGHT: "spread-right", + SPREAD_CENTER: "spread-center", +}; + +// Fit modes for scaling content. +export const Fit = { + AUTO: "auto", + PAGE: "page", + WIDTH: "width", +}; + // Manages a fixed layout resource embedded in an iframe. -export function FixedPage(iframeId) { +// @param iframeId - ID of the iframe element +// @param pageType - Type of page layout from PageType enum +export function FixedPage(iframeId, pageType) { // Fixed dimensions for the page, extracted from the viewport meta tag. var _pageSize = null; // Available viewport size to fill with the resource. var _viewportSize = null; // Margins that should not overlap the content. var _safeAreaInsets = null; + // Fit mode for scaling the page. + var _fit = Fit.AUTO; + // Type of page layout (determines centering behavior). + var _pageType = Object.values(PageType).includes(pageType) + ? pageType + : PageType.SINGLE; // iFrame containing the page. var _iframe = document.getElementById(iframeId); - _iframe.addEventListener("load", loadPageSize); + _iframe.addEventListener("load", onLoad); // Viewport element containing the iFrame. var _viewport = _iframe.closest(".viewport"); + function onLoad() { + // Parses the page size from the viewport meta tag of the loaded resource, + // or extracts natural dimensions from images loaded directly in the iframe. + // As a fallback, we consider that the page spans the size of the viewport. + _pageSize = + parsePageSizeFromViewportMetaTag() ?? + parsePageSizeFromEmbeddedImage() ?? + _viewportSize; + + layoutPage(); + } + // Parses the page size from the viewport meta tag of the loaded resource. - function loadPageSize() { + function parsePageSizeFromViewportMetaTag() { var viewport = _iframe.contentWindow.document.querySelector( "meta[name=viewport]" ); if (!viewport) { - return; + return null; } + var regex = /(\w+) *= *([^\s,]+)/g; var properties = {}; var match; @@ -36,13 +72,26 @@ export function FixedPage(iframeId) { } var width = Number.parseFloat(properties.width); var height = Number.parseFloat(properties.height); - if (width && height) { - _pageSize = { width: width, height: height }; - layoutPage(); + if (!width || !height) { + return null; } + + return { width: width, height: height }; } - // Layouts the page iframe to center its content and scale it to fill the available viewport. + // Parses the page size from the natural dimensions of images loaded directly in the iframe. + // + // When a browser loads an image URL in an iframe, it renders the image + // in a minimal HTML document with an element. + function parsePageSizeFromEmbeddedImage() { + var img = _iframe.contentWindow.document.querySelector("img"); + if (!img || !img.naturalWidth || !img.naturalHeight) { + return null; + } + return { width: img.naturalWidth, height: img.naturalHeight }; + } + + // Layouts the page iframe and scale it according to the current fit mode. function layoutPage() { if (!_pageSize || !_viewportSize || !_safeAreaInsets) { return; @@ -50,19 +99,72 @@ export function FixedPage(iframeId) { _iframe.style.width = _pageSize.width + "px"; _iframe.style.height = _pageSize.height + "px"; - _iframe.style.marginTop = - _safeAreaInsets.top - _safeAreaInsets.bottom + "px"; // Calculates the zoom scale required to fit the content to the viewport. var widthRatio = _viewportSize.width / _pageSize.width; var heightRatio = _viewportSize.height / _pageSize.height; - var scale = Math.min(widthRatio, heightRatio); + var scale; + + switch (_fit) { + case Fit.WIDTH: + // Fit to width only. + scale = widthRatio; + break; + // Auto is equivalent to page in paginated mode, we don't have a scroll mode for FXL. + case Fit.AUTO: + case Fit.PAGE: + default: + // Fit both dimensions. + scale = Math.min(widthRatio, heightRatio); + break; + } + + // Calculate the scaled height of the content + var scaledHeight = _pageSize.height * scale; + + // Determine the appropriate transform based on page type. + // Single page and center page in spread need horizontal centering. + // Left/right pages in spread don't need horizontal transform. + var needsHorizontalCenter = + _pageType === PageType.SINGLE || _pageType === PageType.SPREAD_CENTER; + + // For width fit, if content overflows vertically, align to top + // For page fit, center the content vertically + if (_fit === Fit.WIDTH && scaledHeight > _viewportSize.height) { + // Content overflows: align to top with safe area inset + // Override the CSS centering + _iframe.style.top = _safeAreaInsets.top + "px"; + if (needsHorizontalCenter) { + _iframe.style.transform = "translateX(-50%)"; + } else { + _iframe.style.transform = "none"; + } + } else { + // Content fits or is page fit: center vertically + // Keep the CSS centering but adjust for safe area insets + var verticalOffset = _safeAreaInsets.top - _safeAreaInsets.bottom; + _iframe.style.top = "calc(50% + " + verticalOffset + "px)"; + if (needsHorizontalCenter) { + _iframe.style.transform = "translate(-50%, -50%)"; + } else { + _iframe.style.transform = "translateY(-50%)"; + } + } // Sets the viewport of the wrapper page (this page) to scale the iframe. var viewport = document.querySelector("meta[name=viewport]"); viewport.content = "initial-scale=" + scale + ", minimum-scale=" + scale; } + // Sets the iframe source URL. + function setIframeSrc(url) { + // Release the memory of a previously created blob URL, if needed. + if (_iframe.src.startsWith("blob:")) { + URL.revokeObjectURL(_iframe.src); + } + _iframe.src = url; + } + return { // Returns whether the page is currently loading its contents. isLoading: false, @@ -101,17 +203,20 @@ export function FixedPage(iframeId) { } _iframe.addEventListener("load", loaded); - _iframe.src = resource.url; + + var url = resourceUrl(resource); + setIframeSrc(url); }, - // Resets the page and empty its contents. + // Resets the page and empties its contents. reset: function () { if (!this.link) { return; } this.link = null; _pageSize = null; - _iframe.src = "about:blank"; + + setIframeSrc("about:blank"); }, // Evaluates a script in the context of the page. @@ -123,9 +228,12 @@ export function FixedPage(iframeId) { }, // Updates the available viewport to display the resource. - setViewport: function (viewportSize, safeAreaInsets) { + setViewport: function (viewportSize, safeAreaInsets, fit) { _viewportSize = viewportSize; _safeAreaInsets = safeAreaInsets; + if (Object.values(Fit).includes(fit)) { + _fit = fit; + } layoutPage(); }, @@ -140,3 +248,46 @@ export function FixedPage(iframeId) { }, }; } + +// Returns the URL to load for the given resource. +// Bitmap images are wrapped in an HTML document with alt text for accessibility. +function resourceUrl(resource) { + if (isBitmapMediaType(resource.link.type)) { + let html = generateImageWrapper(resource.url, resource.link.title); + let blob = new Blob([html], { type: "text/html" }); + return URL.createObjectURL(blob); + } else { + return resource.url; + } +} + +// Helper to detect bitmap media types. +function isBitmapMediaType(type) { + if (!type) return false; + return type.startsWith("image/") && !type.includes("svg"); +} + +// Generate an HTML wrapper with alt text for the bitmap at `imageUrl`. +function generateImageWrapper(imageUrl, altText) { + let doc = document.implementation.createHTMLDocument(""); + + let meta = doc.createElement("meta"); + meta.name = "viewport"; + meta.content = "width=device-width, height=device-height"; + doc.head.appendChild(meta); + + let style = doc.createElement("style"); + style.textContent = + "body { margin: 0; }\n" + + "img { display: block; width: 100%; height: 100%; object-fit: contain; }"; + doc.head.appendChild(style); + + let img = doc.createElement("img"); + img.src = imageUrl; + if (altText) { + img.alt = altText; + } + doc.body.appendChild(img); + + return "\n" + doc.documentElement.outerHTML; +} diff --git a/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-one.js b/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-one.js index ea0224dcb8..f1f6f43061 100644 --- a/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-one.js +++ b/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-one.js @@ -6,9 +6,9 @@ // Script used for the single spread wrapper HTML page for fixed layout resources. -import { FixedPage } from "./fixed-page"; +import { FixedPage, PageType } from "./fixed-page"; -var page = FixedPage("page"); +var page = FixedPage("page", PageType.SINGLE); // Public API called from Swift. global.spread = { @@ -30,7 +30,7 @@ global.spread = { }, // Updates the available viewport to display the resources. - setViewport: function (viewportSize, safeAreaInsets) { - page.setViewport(viewportSize, safeAreaInsets); + setViewport: function (viewportSize, safeAreaInsets, fit) { + page.setViewport(viewportSize, safeAreaInsets, fit); }, }; diff --git a/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-two.js b/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-two.js index a2848eec90..90ebe221d6 100644 --- a/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-two.js +++ b/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-two.js @@ -6,12 +6,12 @@ // Script used for the single spread wrapper HTML page for fixed layout resources. -import { FixedPage } from "./fixed-page"; +import { FixedPage, PageType } from "./fixed-page"; var pages = { - left: FixedPage("page-left"), - right: FixedPage("page-right"), - center: FixedPage("page-center"), + left: FixedPage("page-left", PageType.SPREAD_LEFT), + right: FixedPage("page-right", PageType.SPREAD_RIGHT), + center: FixedPage("page-center", PageType.SPREAD_CENTER), }; function forEachPage(callback) { @@ -76,28 +76,44 @@ global.spread = { }, // Updates the available viewport to display the resources. - setViewport: function (viewportSize, safeAreaInsets) { - viewportSize.width /= 2; + setViewport: function (viewportSize, safeAreaInsets, fit) { + var halfViewportSize = { + width: viewportSize.width / 2, + height: viewportSize.height, + }; - pages.left.setViewport(viewportSize, { - top: safeAreaInsets.top, - right: 0, - bottom: safeAreaInsets.bottom, - left: safeAreaInsets.left, - }); + pages.left.setViewport( + halfViewportSize, + { + top: safeAreaInsets.top, + right: 0, + bottom: safeAreaInsets.bottom, + left: safeAreaInsets.left, + }, + fit + ); - pages.right.setViewport(viewportSize, { - top: safeAreaInsets.top, - right: safeAreaInsets.right, - bottom: safeAreaInsets.bottom, - left: 0, - }); + pages.right.setViewport( + halfViewportSize, + { + top: safeAreaInsets.top, + right: safeAreaInsets.right, + bottom: safeAreaInsets.bottom, + left: 0, + }, + fit + ); - pages.center.setViewport(viewportSize, { - top: safeAreaInsets.top, - right: 0, - bottom: safeAreaInsets.bottom, - left: 0, - }); + // Center pages use the full viewport to fit the screen. + pages.center.setViewport( + viewportSize, + { + top: safeAreaInsets.top, + right: safeAreaInsets.right, + bottom: safeAreaInsets.bottom, + left: safeAreaInsets.left, + }, + fit + ); }, }; diff --git a/Sources/Navigator/EPUB/Scripts/src/utils.js b/Sources/Navigator/EPUB/Scripts/src/utils.js index 3b25be58df..831159cfb2 100644 --- a/Sources/Navigator/EPUB/Scripts/src/utils.js +++ b/Sources/Navigator/EPUB/Scripts/src/utils.js @@ -167,18 +167,18 @@ export function isRTL() { } // Scroll to the given TagId in document and snap. -export function scrollToId(id) { +export function scrollToId(id, animated) { let element = document.getElementById(id); if (!element) { return false; } - scrollToRect(element.getBoundingClientRect()); + scrollToRect(element.getBoundingClientRect(), animated); return true; } // Position must be in the range [0 - 1], 0-100%. -export function scrollToPosition(position, dir) { +export function scrollToPosition(position, dir, animated) { if (position < 0 || position > 1) { console.error( `Expected a valid progression in scrollToPosition, got ${position}` @@ -189,16 +189,16 @@ export function scrollToPosition(position, dir) { if (isScrollModeEnabled()) { if (!isVerticalWritingMode()) { let offset = document.scrollingElement.scrollHeight * position; - document.scrollingElement.scrollTop = offset; + scrollTo({ top: offset, animated }); } else { let offset = document.scrollingElement.scrollWidth * position; - document.scrollingElement.scrollLeft = -offset; + scrollTo({ left: -offset, animated }); } } else { var documentWidth = document.scrollingElement.scrollWidth; var factor = dir == "rtl" ? -1 : 1; let offset = documentWidth * position * factor; - document.scrollingElement.scrollLeft = snapOffset(offset); + scrollTo({ left: snapOffset(offset), animated }); } } @@ -206,59 +206,73 @@ export function scrollToPosition(position, dir) { // // The expected text argument is a Locator object, as defined here: // https://readium.org/architecture/models/locators/ -export function scrollToLocator(locator) { +export function scrollToLocator(locator, animated) { let range = rangeFromLocator(locator); if (!range) { return false; } - return scrollToRange(range); + return scrollToRange(range, animated); } -function scrollToRange(range) { - return scrollToRect(range.getBoundingClientRect()); +function scrollToRange(range, animated) { + return scrollToRect(range.getBoundingClientRect(), animated); } -function scrollToRect(rect) { +function scrollToRect(rect, animated) { if (isScrollModeEnabled()) { - document.scrollingElement.scrollTop = rect.top + window.scrollY; + scrollTo({ top: rect.top + window.scrollY, animated }); } else { - document.scrollingElement.scrollLeft = snapOffset( - rect.left + window.scrollX - ); + scrollTo({ left: snapOffset(rect.left + window.scrollX), animated }); } return true; } // Returns false if the page is already at the left-most scroll offset. -export function scrollLeft(dir) { +export function scrollLeft(dir, animated) { var isRTL = dir == "rtl"; var documentWidth = document.scrollingElement.scrollWidth; var pageWidth = window.innerWidth; var offset = window.scrollX - pageWidth; var minOffset = isRTL ? -(documentWidth - pageWidth) : 0; - return scrollToOffset(Math.max(offset, minOffset)); + return scrollToOffset(Math.max(offset, minOffset), animated); } -// Returns false if the page is already at the right-most scroll offset. -export function scrollRight(dir) { +// Returns false if the page is already scrolled at the right-most scroll +// offset. +export function scrollRight(dir, animated) { var isRTL = dir == "rtl"; var documentWidth = document.scrollingElement.scrollWidth; var pageWidth = window.innerWidth; var offset = window.scrollX + pageWidth; var maxOffset = isRTL ? 0 : documentWidth - pageWidth; - return scrollToOffset(Math.min(offset, maxOffset)); + return scrollToOffset(Math.min(offset, maxOffset), animated); } // Scrolls to the given left offset. // Returns false if the page scroll position is already close enough to the given offset. -function scrollToOffset(offset) { +function scrollToOffset(offset, animated) { var currentOffset = window.scrollX; var pageWidth = window.innerWidth; - document.scrollingElement.scrollLeft = offset; + // In some case the scrollX cannot reach the position respecting to innerWidth var diff = Math.abs(currentOffset - offset) / pageWidth; - return diff > 0.01; + var moved = diff > 0.01; + + if (moved) { + scrollTo({ left: offset, animated }); + } + + return moved; +} + +// Scrolls to the given position. +function scrollTo({ left, top, animated } = {}) { + document.scrollingElement.scrollTo({ + left, + top, + behavior: animated ? "smooth" : "instant", + }); } // Snap the offset to the screen width (page width). diff --git a/Sources/Navigator/EPUB/Scripts/webpack.config.js b/Sources/Navigator/EPUB/Scripts/webpack.config.js index d5072bca02..ea411824d9 100644 --- a/Sources/Navigator/EPUB/Scripts/webpack.config.js +++ b/Sources/Navigator/EPUB/Scripts/webpack.config.js @@ -1,8 +1,11 @@ const path = require("path"); +const CopyPlugin = require("copy-webpack-plugin"); +const CleanCSS = require("clean-css"); module.exports = { mode: "production", devtool: "source-map", + // devtool: "eval-source-map", entry: { reflowable: "./src/index-reflowable.js", fixed: "./src/index-fixed.js", @@ -27,4 +30,26 @@ module.exports = { }, ], }, + plugins: [ + new CopyPlugin({ + patterns: [ + { + from: "node_modules/@readium/css/css/dist", + to: "../readium-css", + transform(content, path) { + if (path.endsWith(".css") && process.env.MINIFY_CSS === "true") { + return new CleanCSS({ + level: { + 1: { + specialComments: 0, + }, + }, + }).minify(content).styles; + } + return content; + }, + }, + ], + }), + ], }; diff --git a/Sources/Navigator/EPUB/WebViewServer.swift b/Sources/Navigator/EPUB/WebViewServer.swift new file mode 100644 index 0000000000..e85bac9337 --- /dev/null +++ b/Sources/Navigator/EPUB/WebViewServer.swift @@ -0,0 +1,358 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumInternal +import ReadiumShared +import WebKit + +/// A generic `WKURLSchemeHandler` that serves files, directories, and +/// arbitrary resources at named routes using a custom URL scheme (e.g. +/// `readium://`). +@MainActor final class WebViewServer: NSObject, WKURLSchemeHandler, Loggable { + /// The custom scheme used to serve the content. + let scheme: String + + /// Format sniffer used to infer the media type of served resources. + let formatSniffer: FormatSniffer + + init(scheme: String, formatSniffer: FormatSniffer) { + self.scheme = scheme + self.formatSniffer = formatSniffer + super.init() + } + + // MARK: - Route registration + + private enum RouteHandler { + case file(FileURL) + case directory(FileURL) + case resources(@MainActor (RelativeURL) async -> (Resource, MediaType)?) + } + + /// Registered routes, sorted by reverse alphabetical order to ensure + /// longest-prefix matching of routes sharing a common prefix. + private var routes: [(path: String, baseURL: AbsoluteURL, handler: RouteHandler)] = [] + + /// Serves a single local file at the given route. + /// + /// - Returns: The absolute URL (e.g. `readium://assets/fonts/abc/Font.otf`) + /// to the served file. + @discardableResult + func serve(file: FileURL, at route: String) -> AbsoluteURL { + let route = normalizedRoute(route) + let baseURL = AnyURL(string: "\(scheme)://\(route)")!.absoluteURL! + insertRoute((path: route, baseURL: baseURL, handler: .file(file))) + return baseURL + } + + /// Serves a local directory at the given route. + /// + /// All files under the directory are accessible. + /// + /// - Returns: The absolute base URL (e.g. `readium://assets/`) to the + /// served directory. + @discardableResult + func serve(directory: FileURL, at route: String) -> AbsoluteURL { + let route = normalizedRoute(route, isDirectory: true) + let baseURL = AnyURL(string: "\(scheme)://\(route)")!.absoluteURL! + insertRoute((path: route, baseURL: baseURL, handler: .directory(directory))) + return baseURL + } + + /// Serves resources at the given route using a handler callback. + /// + /// The handler receives a relative URL and returns a `Resource`, or + /// `nil` for 404. Returned resources are automatically wrapped in a + /// `BufferingResource` cache. + /// + /// Returns the base URL (e.g. `readium://{uuid}/`). + @discardableResult + func serve(at route: String, handler: @escaping @MainActor (RelativeURL) async -> (Resource, MediaType)?) -> AbsoluteURL { + let route = normalizedRoute(route, isDirectory: true) + let baseURL = AnyURL(string: "\(scheme)://\(route)")!.absoluteURL! + insertRoute((path: route, baseURL: baseURL, handler: .resources(handler))) + return baseURL + } + + /// Removes the handler at the given route. + func remove(at route: String) { + let route = normalizedRoute(route) + routes.removeAll { $0.path.hasPrefix(route) } + } + + private func normalizedRoute(_ route: String, isDirectory: Bool = false) -> String { + var r = route.removingPrefix("/") + if isDirectory { + r = r.addingSuffix("/") + } + return r + } + + private func insertRoute(_ entry: (path: String, baseURL: AbsoluteURL, handler: RouteHandler)) { + // Remove any existing route with the same path. + routes.removeAll { $0.path == entry.path } + routes.append(entry) + // Reverse alphabetical order ensures longest-prefix matching: + // routes sharing a common prefix are grouped with longer ones first. + routes.sort { $0.path > $1.path } + } + + // MARK: - Active tasks & caching + + /// Tracks active tasks for cancellation support. + private var activeTasks: [ObjectIdentifier: Task] = [:] + + /// Bounded cache of buffered resources keyed by publication-relative URL. + /// + /// Reusing the same ``Resource`` across requests lets compressed ZIP + /// resources benefit from forward-seek optimization instead of + /// decompressing from offset 0 on every request. + /// + /// Oldest entries are evicted when the cache exceeds its capacity. + private var resourceCache = BoundedResourceCache() + + // MARK: - WKURLSchemeHandler + + func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) { + let taskID = ObjectIdentifier(urlSchemeTask) + activeTasks[taskID] = Task { + await serve(urlSchemeTask) + _ = activeTasks.removeValue(forKey: taskID) + } + } + + func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) { + let taskID = ObjectIdentifier(urlSchemeTask) + activeTasks.removeValue(forKey: taskID)?.cancel() + } + + // MARK: - Serving + + private func serve(_ urlSchemeTask: WKURLSchemeTask) async { + guard let requestURL = urlSchemeTask.request.url else { + await fail(urlSchemeTask, with: URLError(.badURL)) + return + } + + // Find the matching route (longest prefix wins). + for route in routes { + switch route.handler { + case let .file(file): + guard route.baseURL.isEquivalentTo(requestURL) else { + continue + } + await serveFile(urlSchemeTask, at: file, requestURL: requestURL) + return + + case let .directory(directory): + guard + let relativeURL = route.baseURL.relativize(requestURL), + let file = directory.resolve(relativeURL)?.fileURL, + directory.isParent(of: file) + else { + continue + } + await serveFile(urlSchemeTask, at: file, requestURL: requestURL) + return + + case let .resources(handler): + guard let relativeURL = route.baseURL.relativize(requestURL) else { + continue + } + await serveResource( + urlSchemeTask, + relativeURL: relativeURL, + handler: handler, + requestURL: requestURL + ) + return + } + } + + await fail(urlSchemeTask, with: URLError(.fileDoesNotExist)) + } + + /// Serves a resource from a handler callback, with caching. + private func serveResource( + _ urlSchemeTask: WKURLSchemeTask, + relativeURL: RelativeURL, + handler: @MainActor (RelativeURL) async -> (Resource, MediaType)?, + requestURL: URL + ) async { + // Reuse a cached buffered resource to benefit from forward-seek + // optimization and read-ahead buffering, or create and cache a new + // one. + let resource: Resource + let mediaType: MediaType + if let (cachedResource, cachedMediaType) = resourceCache[relativeURL] { + resource = cachedResource + mediaType = cachedMediaType + } else { + guard let (newResource, newMediaType) = await handler(relativeURL) else { + await fail(urlSchemeTask, with: URLError(.fileDoesNotExist)) + return + } + resource = newResource.buffered(size: 256 * 1024) + mediaType = newMediaType + resourceCache.set(relativeURL, resource: resource, mediaType: mediaType) + } + + await serveResource( + resource, + with: urlSchemeTask, + mediaType: mediaType, + requestURL: requestURL + ) + } + + /// Reads a local file and sends it as a response. + private func serveFile( + _ urlSchemeTask: WKURLSchemeTask, + at file: FileURL, + requestURL: URL + ) async { + await serveResource( + FileResource(file: file), + with: urlSchemeTask, + mediaType: mediaTypeFromURL(file), + requestURL: requestURL + ) + } + + private func serveResource( + _ resource: Resource, + with urlSchemeTask: WKURLSchemeTask, + mediaType: MediaType?, + requestURL: URL + ) async { + // Try to serve a byte range if the client requested one and the + // resource length is known. + if + let totalLength = await (try? resource.estimatedLength().get()).flatMap({ $0 }), + let range = urlSchemeTask.request.byteRange(in: totalLength) + { + let result = await resource.read(range: range) + switch result { + case let .success(data): + await respond(urlSchemeTask, with: data, range: range, totalLength: totalLength, mediaType: mediaType, url: requestURL) + case let .failure(error): + log(.error, "Failed to read resource \(requestURL.path) range \(range): \(error)") + await fail(urlSchemeTask, with: URLError(.resourceUnavailable)) + } + return + } + + // Full read fallback. + let result = await resource.read() + switch result { + case let .success(data): + await respond(urlSchemeTask, with: data, range: nil, totalLength: UInt64(data.count), mediaType: mediaType, url: requestURL) + case let .failure(error): + log(.error, "Failed to read resource \(requestURL.path): \(error)") + await fail(urlSchemeTask, with: URLError(.resourceUnavailable)) + } + } + + private func mediaTypeFromURL(_ url: URLConvertible) -> MediaType? { + guard let ext = url.anyURL.pathExtension else { + return nil + } + return formatSniffer.sniffHints(FormatHints(fileExtension: ext))?.mediaType + } + + // MARK: - Response helpers + + /// Sends data as a response, optionally as a 206 Partial Content when a + /// byte range was requested. + /// + /// - Parameters: + /// - range: The byte range being served, or `nil` for a full 200 + /// response. + /// - totalLength: The total size of the resource (used in + /// `Content-Range`). + private func respond( + _ urlSchemeTask: WKURLSchemeTask, + with data: Data, + range: Range?, + totalLength: UInt64, + mediaType: MediaType?, + url: URL + ) async { + var headers: [String: String] = [ + "Content-Length": "\(data.count)", + "Accept-Ranges": "bytes", + ] + + if let mediaType { + headers["Content-Type"] = mediaType.string + } + + let statusCode: Int + if let range = range { + statusCode = 206 + headers["Content-Range"] = "bytes \(range.lowerBound)-\(range.upperBound - 1)/\(totalLength)" + } else { + statusCode = 200 + } + + guard let response = HTTPURLResponse( + url: url, + statusCode: statusCode, + httpVersion: "HTTP/1.1", + headerFields: headers + ) else { + await fail(urlSchemeTask, with: URLError(.unknown)) + return + } + + // Guard against task cancellation to avoid calling WKURLSchemeTask + // methods after WebKit has stopped the task. + guard !Task.isCancelled else { return } + urlSchemeTask.didReceive(response) + urlSchemeTask.didReceive(data) + urlSchemeTask.didFinish() + } + + private func fail(_ urlSchemeTask: WKURLSchemeTask, with error: Error) async { + guard !Task.isCancelled else { return } + urlSchemeTask.didFailWithError(error) + } +} + +private extension URLRequest { + /// Parses an HTTP `Range` header value (RFC 7233) into a byte range. + func byteRange(in totalLength: UInt64) -> Range? { + Range(httpRange: value(forHTTPHeaderField: "Range") ?? "", in: totalLength) + } +} + +/// A simple bounded FIFO cache for ``Resource`` instances. +/// +/// Evicts the oldest entries when the number of cached resources exceeds +/// ``capacity``, preventing unbounded memory growth as the user navigates +/// through chapters. +private struct BoundedResourceCache { + private let capacity = 8 + private var entries: [RelativeURL: (Resource, MediaType)] = [:] + private var order: [RelativeURL] = [] + + subscript(key: RelativeURL) -> (Resource, MediaType)? { + entries[key] + } + + mutating func set(_ key: RelativeURL, resource: Resource, mediaType: MediaType) { + if entries[key] == nil { + order.append(key) + } + entries[key] = (resource, mediaType) + + while order.count > capacity { + let evicted = order.removeFirst() + entries.removeValue(forKey: evicted) + } + } +} diff --git a/Sources/Navigator/EditingAction.swift b/Sources/Navigator/EditingAction.swift index 7203c9b9ac..4a001d6c9b 100644 --- a/Sources/Navigator/EditingAction.swift +++ b/Sources/Navigator/EditingAction.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Input/CompositeInputObserver.swift b/Sources/Navigator/Input/CompositeInputObserver.swift index 18716b4d63..0beb8783eb 100644 --- a/Sources/Navigator/Input/CompositeInputObserver.swift +++ b/Sources/Navigator/Input/CompositeInputObserver.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Input/InputObservable+Legacy.swift b/Sources/Navigator/Input/InputObservable+Legacy.swift index b2074bc4f9..150357025c 100644 --- a/Sources/Navigator/Input/InputObservable+Legacy.swift +++ b/Sources/Navigator/Input/InputObservable+Legacy.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Input/InputObservable.swift b/Sources/Navigator/Input/InputObservable.swift index e34236397c..363c0ab49c 100644 --- a/Sources/Navigator/Input/InputObservable.swift +++ b/Sources/Navigator/Input/InputObservable.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,19 +9,19 @@ import Foundation /// A type broadcasting user input events (e.g. touch or keyboard events) to /// a set of observers. @MainActor public protocol InputObservable { - /// Registers a new ``InputObserver`` for the observable receiver. + /// Registers a new `InputObserver` for the observable receiver. /// /// - Returns: An opaque token which can be used to remove the observer with - /// `removeInputObserver`. + /// `removeObserver`. @discardableResult func addObserver(_ observer: InputObserving) -> InputObservableToken - /// Unregisters an ``InputObserver`` from this receiver using the given - /// `token` returned by `addInputObserver`. + /// Unregisters an `InputObserver` from this receiver using the given + /// `token` returned by `addObserver`. func removeObserver(_ token: InputObservableToken) } -/// A token which can be used to remove an ``InputObserver`` from an +/// A token which can be used to remove an `InputObserver` from an /// ``InputObservable``. public struct InputObservableToken: Hashable, Identifiable { public let id: AnyHashable diff --git a/Sources/Navigator/Input/InputObservableViewController.swift b/Sources/Navigator/Input/InputObservableViewController.swift index bc96642d4f..fa6ba1744d 100644 --- a/Sources/Navigator/Input/InputObservableViewController.swift +++ b/Sources/Navigator/Input/InputObservableViewController.swift @@ -1,12 +1,12 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 UIKit -/// Base implementation of ``UIViewController`` which implements +/// Base implementation of `UIViewController` which implements /// ``InputObservable`` to forward UIKit touches and presses events to /// observers. open class InputObservableViewController: UIViewController, InputObservable { @@ -31,7 +31,9 @@ open class InputObservableViewController: UIViewController, InputObservable { // MARK: - UIResponder - override open var canBecomeFirstResponder: Bool { true } + override open var canBecomeFirstResponder: Bool { + true + } override open func resignFirstResponder() -> Bool { // Force end editing of the view to make sure any subview is also @@ -197,7 +199,6 @@ extension Key { self = .shift case .keyboardEscape: self = .escape - default: let character = key.charactersIgnoringModifiers guard character != "" else { diff --git a/Sources/Navigator/Input/InputObserving.swift b/Sources/Navigator/Input/InputObserving.swift index 35ddb1b751..762e9e0d04 100644 --- a/Sources/Navigator/Input/InputObserving.swift +++ b/Sources/Navigator/Input/InputObserving.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Input/InputObservingGestureRecognizerAdapter.swift b/Sources/Navigator/Input/InputObservingGestureRecognizerAdapter.swift index 3383b71724..7a51105365 100644 --- a/Sources/Navigator/Input/InputObservingGestureRecognizerAdapter.swift +++ b/Sources/Navigator/Input/InputObservingGestureRecognizerAdapter.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Input/Key/Key.swift b/Sources/Navigator/Input/Key/Key.swift index 7ecd05ce39..8c5f5de167 100644 --- a/Sources/Navigator/Input/Key/Key.swift +++ b/Sources/Navigator/Input/Key/Key.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -8,7 +8,7 @@ import Foundation import UIKit public enum Key: Equatable, CustomStringConvertible { - // Printable character. + /// Printable character. case character(String) // Whitespace keys. diff --git a/Sources/Navigator/Input/Key/KeyEvent.swift b/Sources/Navigator/Input/Key/KeyEvent.swift index bef1086144..e170a27163 100644 --- a/Sources/Navigator/Input/Key/KeyEvent.swift +++ b/Sources/Navigator/Input/Key/KeyEvent.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Input/Key/KeyModifiers.swift b/Sources/Navigator/Input/Key/KeyModifiers.swift index 99338e2fa6..98542fce85 100644 --- a/Sources/Navigator/Input/Key/KeyModifiers.swift +++ b/Sources/Navigator/Input/Key/KeyModifiers.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Input/Key/KeyObserver.swift b/Sources/Navigator/Input/Key/KeyObserver.swift index b13b39000e..ddf1bdcd95 100644 --- a/Sources/Navigator/Input/Key/KeyObserver.swift +++ b/Sources/Navigator/Input/Key/KeyObserver.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Input/Pointer/ActivatePointerObserver.swift b/Sources/Navigator/Input/Pointer/ActivatePointerObserver.swift index c952296105..5765e05bf9 100644 --- a/Sources/Navigator/Input/Pointer/ActivatePointerObserver.swift +++ b/Sources/Navigator/Input/Pointer/ActivatePointerObserver.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Input/Pointer/DragPointerObserver.swift b/Sources/Navigator/Input/Pointer/DragPointerObserver.swift new file mode 100644 index 0000000000..2b3728ba23 --- /dev/null +++ b/Sources/Navigator/Input/Pointer/DragPointerObserver.swift @@ -0,0 +1,138 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation + +public extension InputObserving where Self == DragPointerObserver { + static func drag( + onStart: @MainActor @escaping (PointerEvent) -> Bool = { _ in false }, + onMove: @MainActor @escaping (PointerEvent) -> Bool = { _ in false }, + onEnd: @MainActor @escaping (PointerEvent) -> Bool = { _ in false }, + onCancel: @MainActor @escaping (PointerEvent) -> Bool = { _ in false } + ) -> DragPointerObserver { + DragPointerObserver( + onStart: onStart, + onMove: onMove, + onEnd: onEnd, + onCancel: onCancel + ) + } +} + +/// Pointer observer recognizing drag gestures. +@MainActor public final class DragPointerObserver: InputObserving { + private let onStart: @MainActor (PointerEvent) -> Bool + private let onMove: @MainActor (PointerEvent) -> Bool + private let onEnd: @MainActor (PointerEvent) -> Bool + private let onCancel: @MainActor (PointerEvent) -> Bool + + public init( + onStart: @MainActor @escaping (PointerEvent) -> Bool, + onMove: @MainActor @escaping (PointerEvent) -> Bool, + onEnd: @MainActor @escaping (PointerEvent) -> Bool, + onCancel: @MainActor @escaping (PointerEvent) -> Bool + ) { + self.onStart = onStart + self.onMove = onMove + self.onEnd = onEnd + self.onCancel = onCancel + } + + private var state: State = .idle + + private enum State { + case idle + case pending(id: AnyHashable, startLocation: CGPoint) + case dragging(id: AnyHashable, lastEvent: PointerEvent) + case failed(activePointers: Set) + } + + private enum Action { + case start(PointerEvent) + case move(PointerEvent) + case end(PointerEvent) + case cancel(PointerEvent) + case none + } + + public func didReceive(_ event: KeyEvent) async -> Bool { + false + } + + public func didReceive(_ event: PointerEvent) async -> Bool { + let (newState, action) = transition(state: state, event: event) + state = newState + + switch action { + case let .start(event): + return onStart(event) + case let .move(event): + return onMove(event) + case let .end(event): + return onEnd(event) + case let .cancel(event): + return onCancel(event) + case .none: + return false + } + } + + private func transition(state: State, event: PointerEvent) -> (State, Action) { + let id = event.pointer.id + + switch (state, event.phase) { + case (.idle, .down): + return (.pending(id: id, startLocation: event.location), .none) + + case let (.pending(pendingID, _), .down) where pendingID != id: + return (.failed(activePointers: [pendingID, id]), .none) + + case let (.pending(pendingID, _), .cancel) where pendingID == id: + return (.idle, .none) + + case let (.pending(pendingID, startLocation), .move) where pendingID == id: + // Check if pointer has moved enough to start dragging. + if abs(startLocation.x - event.location.x) > 1 || abs(startLocation.y - event.location.y) > 1 { + return (.dragging(id: pendingID, lastEvent: event), .start(event)) + } else { + return (.pending(id: pendingID, startLocation: startLocation), .none) + } + + case let (.pending(pendingID, _), .up) where pendingID == id: + // Pointer went up without moving - this is a tap, not a drag. + return (.idle, .none) + + case let (.dragging(draggingID, lastEvent), .down) where draggingID != id: + // Second pointer detected during drag - cancel the drag + return (.failed(activePointers: [draggingID, id]), .cancel(lastEvent)) + + case let (.dragging(draggingID, lastEvent), .cancel) where draggingID == id: + return (.idle, .cancel(lastEvent)) + + case let (.dragging(draggingID, _), .move) where draggingID == id: + return (.dragging(id: draggingID, lastEvent: event), .move(event)) + + case let (.dragging(draggingID, _), .up) where draggingID == id: + return (.idle, .end(event)) + + case var (.failed(activePointers), .down): + activePointers.insert(id) + return (.failed(activePointers: activePointers), .none) + + case var (.failed(activePointers), .up), + var (.failed(activePointers), .cancel): + activePointers.remove(id) + if activePointers.isEmpty { + return (.idle, .none) + } else { + return (.failed(activePointers: activePointers), .none) + } + + default: + return (state, .none) + } + } +} diff --git a/Sources/Navigator/Input/Pointer/PointerEvent.swift b/Sources/Navigator/Input/Pointer/PointerEvent.swift index aeabdf0f48..f2a5973ef5 100644 --- a/Sources/Navigator/Input/Pointer/PointerEvent.swift +++ b/Sources/Navigator/Input/Pointer/PointerEvent.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Navigator.swift b/Sources/Navigator/Navigator.swift index 36652b3955..004154c87c 100644 --- a/Sources/Navigator/Navigator.swift +++ b/Sources/Navigator/Navigator.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -25,7 +25,7 @@ public protocol Navigator: AnyObject { @discardableResult func go(to locator: Locator, options: NavigatorGoOptions) async -> Bool - /// Moves to the position in the publication targeted by the given link. + // Moves to the position in the publication targeted by the given link. /// - Returns: Whether the navigator is able to move to the locator. The /// completion block is only called if true was returned. @@ -49,22 +49,16 @@ public protocol Navigator: AnyObject { func goBackward(options: NavigatorGoOptions) async -> Bool } -public struct NavigatorGoOptions { +public struct NavigatorGoOptions: Hashable { /// Indicates whether the move should be animated when possible. public var animated: Bool = false /// Extension point for navigator implementations. - public var otherOptions: [String: Any] { - get { otherOptionsJSON.json } - set { otherOptionsJSON = JSONDictionary(newValue) ?? JSONDictionary() } - } - - // Trick to keep the struct equatable despite [String: Any] - private var otherOptionsJSON: JSONDictionary + public var otherOptions: [String: JSONValue] - public init(animated: Bool = false, otherOptions: [String: Any] = [:]) { + public init(animated: Bool = false, otherOptions: [String: JSONValue] = [:]) { self.animated = animated - otherOptionsJSON = JSONDictionary(otherOptions) ?? JSONDictionary() + self.otherOptions = .init(otherOptions) } public static var none: NavigatorGoOptions { diff --git a/Sources/Navigator/PDF/PDFDocumentHolder.swift b/Sources/Navigator/PDF/PDFDocumentHolder.swift index 990ca9e6ba..aaebce28a8 100644 --- a/Sources/Navigator/PDF/PDFDocumentHolder.swift +++ b/Sources/Navigator/PDF/PDFDocumentHolder.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -26,7 +26,7 @@ extension PDFDocumentHolder: ReadiumShared.PDFDocumentFactory { return document } - public func open(resource: Resource, at href: HREF, password: String?) async throws -> ReadiumShared.PDFDocument { + func open(resource: Resource, at href: HREF, password: String?) async throws -> ReadiumShared.PDFDocument { guard let document = document, self.href == href.anyURL else { throw PDFDocumentError.openFailed } diff --git a/Sources/Navigator/PDF/PDFDocumentView.swift b/Sources/Navigator/PDF/PDFDocumentView.swift index d8de952200..bb61f6873c 100644 --- a/Sources/Navigator/PDF/PDFDocumentView.swift +++ b/Sources/Navigator/PDF/PDFDocumentView.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -50,11 +50,31 @@ public final class PDFDocumentView: PDFView { } private func updateContentInset() { - let insets = documentViewDelegate?.pdfDocumentViewContentInset(self) ?? window?.safeAreaInsets ?? .zero + let insets = contentInset firstScrollView?.contentInset.top = insets.top firstScrollView?.contentInset.bottom = insets.bottom } + private var contentInset: UIEdgeInsets { + if let contentInset = documentViewDelegate?.pdfDocumentViewContentInset(self) { + return contentInset + } + + // We apply the window's safe area insets (representing the system + // status bar, but ignoring app bars) on iPhones only because in most + // cases we prefer to display the content edge-to-edge. + // iPhones are a special case because they are the only devices with a + // physical notch (or Dynamic Island) which is included in the window's + // safe area insets. Therefore, we must always take it into account to + // avoid hiding the content. + if UIDevice.current.userInterfaceIdiom == .phone { + return window?.safeAreaInsets ?? .zero + } else { + // Edge-to-edge on macOS and iPadOS. + return .zero + } + } + override public func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { super.canPerformAction(action, withSender: sender) && editingActions.canPerformAction(action) } @@ -70,4 +90,209 @@ public final class PDFDocumentView: PDFView { editingActions.buildMenu(with: builder) super.buildMenu(with: builder) } + + var isPaginated: Bool { + isUsingPageViewController || displayMode == .twoUp || displayMode == .singlePage + } + + var isSpreadEnabled: Bool { + displayMode == .twoUp || displayMode == .twoUpContinuous + } + + /// Returns whether the document is currently zoomed to match the given + /// `fit`. + func isAtScaleFactor(for fit: Fit) -> Bool { + let scaleFactorToFit = scaleFactor(for: fit) + // 1% tolerance for floating point comparison + let tolerance: CGFloat = 0.01 + return abs(scaleFactor - scaleFactorToFit) < tolerance + } + + /// Calculates the appropriate scale factor based on the fit preference. + /// + /// Only used in scroll mode, as the paginated mode doesn't support custom + /// scale factors without visual hiccups when swiping pages. + func scaleFactor(for fit: Fit) -> CGFloat { + // While a `width` fit works in scroll mode, the pagination mode has + // critical limitations when zooming larger than the page fit, so it + // does not support a `width` fit. + // + // - Visual snap: There is no API to pre-set the zoom scale for the next + // page. PDFView resets the scale per page, causing a visible snap + // when swiping. We don’t see the issue with edge taps. + // - Incorrect anchoring: When zooming larger than the page fit, the + // viewport centers vertically instead of showing the top. The API to + // fix this works in scroll mode but is ignored in paginated mode. + // + // So we only support a `page` fit in paginated mode. + if isPaginated { + return scaleFactorForSizeToFitVisiblePages + } + + switch fit { + case .auto, .width: + // Use PDFKit's default auto-fit behavior + return scaleFactorForSizeToFit + case .page: + return scaleFactorForLargestPage + } + } + + /// Calculates the scale factor to fit the visible pages (by area) to the + /// viewport. + private var scaleFactorForSizeToFitVisiblePages: CGFloat { + // The native `scaleFactorForSizeToFit` is incorrect when displaying + // paginated spreads, so we need to use a custom implementation. + if !isPaginated || !isSpreadEnabled { + scaleFactorForSizeToFit + } else { + calculateScale( + for: spreadSize(for: visiblePages), + viewSize: bounds.size, + insets: contentInset + ) + } + } + + /// Calculates the scale factor to fit the largest page or spread (by area) + /// to the viewport. + private var scaleFactorForLargestPage: CGFloat { + guard let document = document else { + return 1.0 + } + + // Check cache before expensive calculation + let viewSize = bounds.size + let insets = contentInset + if + let cached = cachedScaleFactorForLargestPage, + cached.document == ObjectIdentifier(document), + cached.viewSize == viewSize, + cached.contentInset == insets, + cached.spread == isSpreadEnabled, + cached.displaysAsBook == displaysAsBook + { + return cached.scaleFactor + } + + var maxSize: CGSize = .zero + var maxArea: CGFloat = 0 + + if !isSpreadEnabled { + // No spreads: find largest individual page + for pageIndex in 0 ..< document.pageCount { + guard let page = document.page(at: pageIndex) else { continue } + let pageSize = page.bounds(for: displayBox).size + let area = pageSize.width * pageSize.height + + if area > maxArea { + maxArea = area + maxSize = pageSize + } + } + } else { + // Spreads enabled: find largest spread + let pageCount = document.pageCount + + if displaysAsBook, pageCount > 0 { + // First page displayed alone - check its size + if let firstPage = document.page(at: 0) { + let firstSize = firstPage.bounds(for: displayBox).size + let firstArea = firstSize.width * firstSize.height + if firstArea > maxArea { + maxArea = firstArea + maxSize = firstSize + } + } + } + + // Check spreads (pairs of pages) + let startIndex = displaysAsBook ? 1 : 0 + for pageIndex in stride(from: startIndex, to: pageCount, by: 2) { + let leftIndex = pageIndex + let rightIndex = pageIndex + 1 + + guard let leftPage = document.page(at: leftIndex) else { continue } + + if rightIndex < pageCount, let rightPage = document.page(at: rightIndex) { + // Two-page spread + let currentSpreadSize = spreadSize(for: [leftPage, rightPage]) + let spreadArea = currentSpreadSize.width * currentSpreadSize.height + + if spreadArea > maxArea { + maxArea = spreadArea + maxSize = currentSpreadSize + } + } else { + // Last page alone (odd page count) + let leftSize = leftPage.bounds(for: displayBox).size + let singleArea = leftSize.width * leftSize.height + if singleArea > maxArea { + maxArea = singleArea + maxSize = leftSize + } + } + } + } + + let scale = calculateScale( + for: maxSize, + viewSize: viewSize, + insets: insets + ) + + cachedScaleFactorForLargestPage = ( + document: ObjectIdentifier(document), + scaleFactor: scale, + viewSize: viewSize, + contentInset: insets, + spread: isSpreadEnabled, + displaysAsBook: displaysAsBook + ) + return scale + } + + /// Cache for expensive largest page scale calculation. + private var cachedScaleFactorForLargestPage: ( + document: ObjectIdentifier, + scaleFactor: CGFloat, + viewSize: CGSize, + contentInset: UIEdgeInsets, + spread: Bool, + displaysAsBook: Bool + )? + + /// Calculates the combined size of pages laid out side-by-side horizontally. + private func spreadSize(for pages: [PDFPage]) -> CGSize { + var size = CGSize.zero + for page in pages { + let pageBounds = page.bounds(for: displayBox) + size.height = max(size.height, pageBounds.height) + size.width += pageBounds.width + } + return size + } + + /// Calculates the scale factor needed to fit the given content size within + /// the available viewport, accounting for content insets. + private func calculateScale( + for contentSize: CGSize, + viewSize: CGSize, + insets: UIEdgeInsets + ) -> CGFloat { + guard contentSize.width > 0, contentSize.height > 0 else { + return 1.0 + } + + let availableSize = CGSize( + width: viewSize.width - insets.left - insets.right, + height: viewSize.height - insets.top - insets.bottom + ) + + let widthScale = availableSize.width / contentSize.width + let heightScale = availableSize.height / contentSize.height + + // Use the smaller scale to ensure both dimensions fit + return min(widthScale, heightScale) + } } diff --git a/Sources/Navigator/PDF/PDFNavigatorViewController.swift b/Sources/Navigator/PDF/PDFNavigatorViewController.swift index 93ac53efe7..3ec4ff4204 100644 --- a/Sources/Navigator/PDF/PDFNavigatorViewController.swift +++ b/Sources/Navigator/PDF/PDFNavigatorViewController.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -56,9 +56,6 @@ open class PDFNavigatorViewController: case openPDFFailed } - /// Whether the pages is always scaled to fit the screen, unless the user zoomed in. - public var scalesDocumentToFit = true - public weak var delegate: PDFNavigatorDelegate? public private(set) var pdfView: PDFDocumentView? private var pdfViewDefaultBackgroundColor: UIColor! @@ -76,6 +73,8 @@ open class PDFNavigatorViewController: // Holds a reference to make sure they are not garbage-collected. private var tapGestureController: PDFTapGestureController? private var clickGestureController: PDFTapGestureController? + private var swipeLeftGestureRecognizer: UISwipeGestureRecognizer? + private var swipeRightGestureRecognizer: UISwipeGestureRecognizer? private let server: HTTPServer? private let publicationEndpoint: HTTPServerEndpoint? @@ -184,7 +183,7 @@ open class PDFNavigatorViewController: super.viewWillAppear(animated) // Hack to layout properly the first page when opening the PDF. - if let pdfView = pdfView, scalesDocumentToFit { + if let pdfView = pdfView { pdfView.scaleFactor = pdfView.minScaleFactor if let page = pdfView.currentPage { pdfView.go(to: page.bounds(for: pdfView.displayBox), on: page) @@ -195,14 +194,13 @@ open class PDFNavigatorViewController: override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - if let pdfView = pdfView, scalesDocumentToFit { - // Makes sure that the PDF is always properly scaled down when rotating the screen, if the user didn't zoom in. - let isAtMinScaleFactor = (pdfView.scaleFactor == pdfView.minScaleFactor) + if let pdfView = pdfView { + // Makes sure that the PDF is always properly scaled when rotating + // the screen, if the user didn't set a custom zoom. + let isAtScaleFactor = pdfView.isAtScaleFactor(for: settings.fit) + coordinator.animate(alongsideTransition: { _ in - self.updateScaleFactors() - if isAtMinScaleFactor { - pdfView.scaleFactor = pdfView.minScaleFactor - } + self.updateScaleFactors(zoomToFit: isAtScaleFactor) // Reset the PDF view to update the spread if needed. if self.settings.spread == .auto { @@ -263,11 +261,14 @@ open class PDFNavigatorViewController: target: self, action: #selector(didClick) ) + swipeLeftGestureRecognizer = recognizeSwipe(in: pdfView, direction: .left) + swipeRightGestureRecognizer = recognizeSwipe(in: pdfView, direction: .right) apply(settings: settings, to: pdfView) delegate?.navigator(self, setupPDFView: pdfView) NotificationCenter.default.addObserver(self, selector: #selector(pageDidChange), name: .PDFViewPageChanged, object: pdfView) + NotificationCenter.default.addObserver(self, selector: #selector(visiblePagesDidChange), name: .PDFViewVisiblePagesChanged, object: pdfView) NotificationCenter.default.addObserver(self, selector: #selector(selectionDidChange), name: .PDFViewSelectionChanged, object: pdfView) if let locator = locator { @@ -328,7 +329,7 @@ open class PDFNavigatorViewController: pdfView.displaysRTL = isRTL pdfView.displaysPageBreaks = true - pdfView.autoScales = !scalesDocumentToFit + pdfView.autoScales = false if let scrollView = pdfView.firstScrollView { let showScrollbar = settings.visibleScrollbar @@ -341,6 +342,10 @@ open class PDFNavigatorViewController: } pdfView.backgroundColor = settings.backgroundColor?.uiColor ?? pdfViewDefaultBackgroundColor + + let enableSwipes = !settings.scroll && spread + swipeLeftGestureRecognizer?.isEnabled = enableSwipes + swipeRightGestureRecognizer?.isEnabled = enableSwipes } @objc private func didTap(_ gesture: UITapGestureRecognizer) { @@ -367,6 +372,25 @@ open class PDFNavigatorViewController: delegate?.navigator(self, didTapAt: location) } + private func recognizeSwipe(in view: UIView, direction: UISwipeGestureRecognizer.Direction) -> UISwipeGestureRecognizer { + let recognizer = UISwipeGestureRecognizer(target: self, action: #selector(didSwipe)) + recognizer.direction = direction + recognizer.numberOfTouchesRequired = 1 + view.addGestureRecognizer(recognizer) + return recognizer + } + + @objc private func didSwipe(_ gesture: UISwipeGestureRecognizer) { + switch gesture.direction { + case .left: + Task { await goRight(options: .animated) } + case .right: + Task { await goLeft(options: .animated) } + default: + break + } + } + @objc private func pageDidChange() { guard let locator = currentPosition else { return @@ -374,6 +398,15 @@ open class PDFNavigatorViewController: delegate?.navigator(self, locationDidChange: locator) } + @objc private func visiblePagesDidChange() { + // In paginated mode, we want to refresh the scale factors to properly + // fit the newly visible pages. This is especially important for + // paginated spreads. + if !settings.scroll { + updateScaleFactors(zoomToFit: true) + } + } + @discardableResult private func go(to locator: Locator, isJump: Bool) async -> Bool { let locator = publication.normalizeLocator(locator) @@ -418,7 +451,7 @@ open class PDFNavigatorViewController: } if currentResourceIndex != index { - guard let document = PDFDocument(url: url.url) else { + guard let document = await makeDocument(at: url) else { log(.error, "Can't open PDF document at \(url)") return false } @@ -426,7 +459,7 @@ open class PDFNavigatorViewController: currentResourceIndex = index documentHolder.set(document, at: href) pdfView.document = document - updateScaleFactors() + updateScaleFactors(zoomToFit: true) } guard let document = pdfView.document else { @@ -446,12 +479,36 @@ open class PDFNavigatorViewController: return true } - private func updateScaleFactors() { - guard let pdfView = pdfView, scalesDocumentToFit else { + private func makeDocument(at url: AbsoluteURL) async -> PDFKit.PDFDocument? { + let task = Task.detached(priority: .userInitiated) { + PDFDocument(url: url.url) + } + return await task.value + } + + /// Updates the scale factors to match the currently visible pages. + /// + /// - Parameter zoomToFit: When true, the document will be zoomed to fit the + /// visible pages. + private func updateScaleFactors(zoomToFit: Bool) { + guard let pdfView = pdfView else { return } - pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit + + let scaleFactorToFit = pdfView.scaleFactor(for: settings.fit) + + if settings.scroll { + // Allow zooming out to 25% in scroll mode. + pdfView.minScaleFactor = 0.25 + } else { + pdfView.minScaleFactor = scaleFactorToFit + } + pdfView.maxScaleFactor = 4.0 + + if zoomToFit { + pdfView.scaleFactor = scaleFactorToFit + } } private func pageNumber(for locator: Locator) -> Int? { @@ -528,7 +585,9 @@ open class PDFNavigatorViewController: // MARK: - SelectableNavigator - public var currentSelection: Selection? { editingActions.selection } + public var currentSelection: Selection? { + editingActions.selection + } public func clearSelection() { pdfView?.clearSelection() @@ -597,7 +656,7 @@ open class PDFNavigatorViewController: public var currentLocation: Locator? { currentPosition?.copy(text: { [weak self] in - /// Adds some context for bookmarking + // Adds some context for bookmarking if let page = self?.pdfView?.currentPage { $0 = .init(highlight: String(page.string?.prefix(280) ?? "")) } diff --git a/Sources/Navigator/PDF/PDFTapGestureController.swift b/Sources/Navigator/PDF/PDFTapGestureController.swift index c3a7671f23..a652c23a02 100644 --- a/Sources/Navigator/PDF/PDFTapGestureController.swift +++ b/Sources/Navigator/PDF/PDFTapGestureController.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/PDF/Preferences/PDFPreferences.swift b/Sources/Navigator/PDF/Preferences/PDFPreferences.swift index 8c32120247..723565f582 100644 --- a/Sources/Navigator/PDF/Preferences/PDFPreferences.swift +++ b/Sources/Navigator/PDF/Preferences/PDFPreferences.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -14,7 +14,13 @@ public struct PDFPreferences: ConfigurablePreferences { /// Background color behind the document pages. public var backgroundColor: Color? - /// Indicates if the first page should be displayed in its own spread. + /// Method for fitting the pages within the viewport. + public var fit: Fit? + + /// Indicates whether the first page should be displayed alone instead of + /// alongside the second page. + /// + /// This is only effective if spreads are enabled. public var offsetFirstPage: Bool? /// Spacing between pages in points. @@ -41,6 +47,7 @@ public struct PDFPreferences: ConfigurablePreferences { public init( backgroundColor: Color? = nil, + fit: Fit? = nil, offsetFirstPage: Bool? = nil, pageSpacing: Double? = nil, readingProgression: ReadingProgression? = nil, @@ -51,6 +58,7 @@ public struct PDFPreferences: ConfigurablePreferences { ) { precondition(pageSpacing == nil || pageSpacing! >= 0) self.backgroundColor = backgroundColor + self.fit = fit self.offsetFirstPage = offsetFirstPage self.pageSpacing = pageSpacing self.readingProgression = readingProgression @@ -63,6 +71,7 @@ public struct PDFPreferences: ConfigurablePreferences { public func merging(_ other: PDFPreferences) -> PDFPreferences { PDFPreferences( backgroundColor: other.backgroundColor ?? backgroundColor, + fit: other.fit ?? fit, offsetFirstPage: other.offsetFirstPage ?? offsetFirstPage, pageSpacing: other.pageSpacing ?? pageSpacing, readingProgression: other.readingProgression ?? readingProgression, diff --git a/Sources/Navigator/PDF/Preferences/PDFPreferencesEditor.swift b/Sources/Navigator/PDF/Preferences/PDFPreferencesEditor.swift index 28d1f9e256..9124041194 100644 --- a/Sources/Navigator/PDF/Preferences/PDFPreferencesEditor.swift +++ b/Sources/Navigator/PDF/Preferences/PDFPreferencesEditor.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -32,7 +32,20 @@ public final class PDFPreferencesEditor: StatefulPreferencesEditor = + enumPreference( + preference: \.fit, + setting: \.fit, + defaultEffectiveValue: defaults.fit ?? .auto, + isEffective: { $0.settings.scroll }, + supportedValues: [.auto, .page, .width] + ) + + /// Indicates whether the first page should be displayed alone instead of + /// alongside the second page. /// /// Only effective when `spread` is not off. public lazy var offsetFirstPage: AnyPreference = diff --git a/Sources/Navigator/PDF/Preferences/PDFSettings.swift b/Sources/Navigator/PDF/Preferences/PDFSettings.swift index 82ac91463a..936daee8ab 100644 --- a/Sources/Navigator/PDF/Preferences/PDFSettings.swift +++ b/Sources/Navigator/PDF/Preferences/PDFSettings.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -13,6 +13,7 @@ import ReadiumShared /// See `PDFPreferences` public struct PDFSettings: ConfigurableSettings { public let backgroundColor: Color? + public let fit: Fit public let offsetFirstPage: Bool public let pageSpacing: Double public let readingProgression: ReadingProgression @@ -25,6 +26,10 @@ public struct PDFSettings: ConfigurableSettings { backgroundColor = preferences.backgroundColor ?? defaults.backgroundColor + fit = preferences.fit + ?? defaults.fit + ?? .auto + offsetFirstPage = preferences.offsetFirstPage ?? defaults.offsetFirstPage ?? false @@ -64,6 +69,7 @@ public struct PDFSettings: ConfigurableSettings { /// See `PDFPreferences`. public struct PDFDefaults { public var backgroundColor: Color? + public var fit: Fit? public var offsetFirstPage: Bool? public var pageSpacing: Double? public var readingProgression: ReadingProgression? @@ -74,6 +80,7 @@ public struct PDFDefaults { public init( backgroundColor: Color? = nil, + fit: Fit? = nil, offsetFirstPage: Bool? = nil, pageSpacing: Double? = nil, readingProgression: ReadingProgression? = nil, @@ -83,6 +90,7 @@ public struct PDFDefaults { visibleScrollbar: Bool? = nil ) { self.backgroundColor = backgroundColor + self.fit = fit self.offsetFirstPage = offsetFirstPage self.pageSpacing = pageSpacing self.readingProgression = readingProgression diff --git a/Sources/Navigator/Preferences/Configurable.swift b/Sources/Navigator/Preferences/Configurable.swift index a07b169198..7eee624d9c 100644 --- a/Sources/Navigator/Preferences/Configurable.swift +++ b/Sources/Navigator/Preferences/Configurable.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -67,7 +67,9 @@ public class AnyConfigurable< _editor = configurable.editor(of:) } - public var settings: Settings { _settings() } + public var settings: Settings { + _settings() + } public func submitPreferences(_ preferences: Preferences) { _submitPreferences(preferences) diff --git a/Sources/Navigator/Preferences/MappedPreference.swift b/Sources/Navigator/Preferences/MappedPreference.swift index 3659b0f7d6..c66514db3a 100644 --- a/Sources/Navigator/Preferences/MappedPreference.swift +++ b/Sources/Navigator/Preferences/MappedPreference.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -149,9 +149,17 @@ public class MappedPreference: Preference { self.to = to } - public var value: NewValue? { original.value.map(from) } - public var effectiveValue: NewValue { from(original.effectiveValue) } - public var isEffective: Bool { original.isEffective } + public var value: NewValue? { + original.value.map(from) + } + + public var effectiveValue: NewValue { + from(original.effectiveValue) + } + + public var isEffective: Bool { + original.isEffective + } public func set(_ value: NewValue?) { original.set(value.map(to)) diff --git a/Sources/Navigator/Preferences/Preference.swift b/Sources/Navigator/Preferences/Preference.swift index 2240648519..85f11f3637 100644 --- a/Sources/Navigator/Preferences/Preference.swift +++ b/Sources/Navigator/Preferences/Preference.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -82,9 +82,17 @@ public extension Preference { /// A type-erasing `Preference` object. public class AnyPreference: Preference { - public var value: Value? { _value() } - public var effectiveValue: Value { _effectiveValue() } - public var isEffective: Bool { _isEffective() } + public var value: Value? { + _value() + } + + public var effectiveValue: Value { + _effectiveValue() + } + + public var isEffective: Bool { + _isEffective() + } private let _value: () -> Value? private let _effectiveValue: () -> Value @@ -112,7 +120,9 @@ public extension EnumPreference { /// A type-erasing `EnumPreference` object. public class AnyEnumPreference: AnyPreference, EnumPreference { - public var supportedValues: [Value] { _supportedValues() } + public var supportedValues: [Value] { + _supportedValues() + } private let _supportedValues: () -> [Value] @@ -131,7 +141,9 @@ public extension RangePreference { /// A type-erasing `Preference` object. public class AnyRangePreference: AnyPreference, RangePreference { - public var supportedRange: ClosedRange { _supportedRange() } + public var supportedRange: ClosedRange { + _supportedRange() + } private let _supportedRange: () -> ClosedRange private let _increment: () -> Void diff --git a/Sources/Navigator/Preferences/PreferencesEditor.swift b/Sources/Navigator/Preferences/PreferencesEditor.swift index 4e3c488a80..407f7b890b 100644 --- a/Sources/Navigator/Preferences/PreferencesEditor.swift +++ b/Sources/Navigator/Preferences/PreferencesEditor.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -38,7 +38,9 @@ public class StatefulPreferencesEditor Bool { true } - func navigator(_ navigator: SelectableNavigator, canPerformAction action: EditingAction, for selection: Selection) -> Bool { true } + func navigator(_ navigator: SelectableNavigator, shouldShowMenuForSelection selection: Selection) -> Bool { + true + } + + func navigator(_ navigator: SelectableNavigator, canPerformAction action: EditingAction, for selection: Selection) -> Bool { + true + } } diff --git a/Sources/Navigator/TTS/AVTTSEngine.swift b/Sources/Navigator/TTS/AVTTSEngine.swift index d3177c46b3..22cdb90638 100644 --- a/Sources/Navigator/TTS/AVTTSEngine.swift +++ b/Sources/Navigator/TTS/AVTTSEngine.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -146,7 +146,7 @@ public class AVTTSEngine: NSObject, TTSEngine, AVSpeechSynthesizerDelegate, Logg @available(*, unavailable) required init?(coder: NSCoder) { - fatalError("Not supported") + fatalError("init(coder:) has not been implemented") } } diff --git a/Sources/Navigator/TTS/PublicationSpeechSynthesizer.swift b/Sources/Navigator/TTS/PublicationSpeechSynthesizer.swift index efbc73ac1b..51c20f59b2 100644 --- a/Sources/Navigator/TTS/PublicationSpeechSynthesizer.swift +++ b/Sources/Navigator/TTS/PublicationSpeechSynthesizer.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -156,7 +156,7 @@ public class PublicationSpeechSynthesizer: Loggable { ) } - private var currentTask: Task? = nil + private var currentTask: Task? private lazy var engine: TTSEngine = engineFactory() @@ -177,7 +177,7 @@ public class PublicationSpeechSynthesizer: Loggable { } /// Cache for the last requested voice, for performance. - private var lastUsedVoice: TTSVoice? = nil + private var lastUsedVoice: TTSVoice? /// (Re)starts the synthesizer from the given locator or the beginning of the publication. public func start(from startLocator: Locator? = nil) { @@ -245,7 +245,7 @@ public class PublicationSpeechSynthesizer: Loggable { } /// `Content.Iterator` used to iterate through the `publication`. - private var publicationIterator: ContentIterator? = nil { + private var publicationIterator: ContentIterator? { didSet { utterances = CursorList() } @@ -320,8 +320,7 @@ public class PublicationSpeechSynthesizer: Loggable { return .right(utterance.language ?? config.defaultLanguage ?? publication.metadata.language - ?? Language.current - ) + ?? Language.current) } } diff --git a/Sources/Navigator/TTS/TTSEngine.swift b/Sources/Navigator/TTS/TTSEngine.swift index 3d513bd3d5..66ea8e5eab 100644 --- a/Sources/Navigator/TTS/TTSEngine.swift +++ b/Sources/Navigator/TTS/TTSEngine.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/TTS/TTSVoice.swift b/Sources/Navigator/TTS/TTSVoice.swift index d84c060914..d482ef0f4e 100644 --- a/Sources/Navigator/TTS/TTSVoice.swift +++ b/Sources/Navigator/TTS/TTSVoice.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -106,15 +106,13 @@ public extension [TTSVoice] { // 3. Add remaining regions ordered by localized name. ordered.append(contentsOf: regions.sorted { ($0.localizedName(in: displayLocale) ?? $0.code) < ($1.localizedName(in: displayLocale) ?? $1.code) - } - ) + }) ordered = ordered.removingDuplicates() // Assign priorities: lower Int = higher priority let priorities = Dictionary(uniqueKeysWithValues: - ordered.enumerated().map { idx, region in (region, idx) } - ) + ordered.enumerated().map { idx, region in (region, idx) }) return (language, priorities) }) @@ -132,11 +130,11 @@ public extension [TTSVoice] { if let region = voice.language.region, let regionPriorities = regionPrioritiesByLanguage[language] - { - regionPriorities[region] ?? .max - } else { - .max - } + { + regionPriorities[region] ?? .max + } else { + .max + } return ( language: language.localizedLanguage(in: displayLocale) ?? voice.language.code.bcp47, @@ -230,7 +228,7 @@ private let defaultRegionByLanguage: [Language.Code: Language.Region] = [ .bcp47("yue"): "HK", ] -// Quality order priority: higher to lower +/// Quality order priority: higher to lower private let qualityPriorities: [TTSVoice.Quality: Int] = [ .higher: 0, .high: 1, @@ -239,7 +237,7 @@ private let qualityPriorities: [TTSVoice.Quality: Int] = [ .lower: 4, ] -// Gender order priority: female > male > unspecified +/// Gender order priority: female > male > unspecified private let genderPriorities: [TTSVoice.Gender: Int] = [ .female: 0, .male: 1, diff --git a/Sources/Navigator/Toolkit/CompletionList.swift b/Sources/Navigator/Toolkit/CompletionList.swift index c9b2b2df71..a6a2bba587 100644 --- a/Sources/Navigator/Toolkit/CompletionList.swift +++ b/Sources/Navigator/Toolkit/CompletionList.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Toolkit/CursorList.swift b/Sources/Navigator/Toolkit/CursorList.swift index 40c5cd4a95..1b2e27522b 100644 --- a/Sources/Navigator/Toolkit/CursorList.swift +++ b/Sources/Navigator/Toolkit/CursorList.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Toolkit/Extensions/Bundle.swift b/Sources/Navigator/Toolkit/Extensions/Bundle.swift index 285f422278..6d7a1c8589 100644 --- a/Sources/Navigator/Toolkit/Extensions/Bundle.swift +++ b/Sources/Navigator/Toolkit/Extensions/Bundle.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Toolkit/Extensions/CGRect.swift b/Sources/Navigator/Toolkit/Extensions/CGRect.swift index da3e6d6c82..7918813320 100644 --- a/Sources/Navigator/Toolkit/Extensions/CGRect.swift +++ b/Sources/Navigator/Toolkit/Extensions/CGRect.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Toolkit/Extensions/Language.swift b/Sources/Navigator/Toolkit/Extensions/Language.swift index db6224296c..f83734b840 100644 --- a/Sources/Navigator/Toolkit/Extensions/Language.swift +++ b/Sources/Navigator/Toolkit/Extensions/Language.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Toolkit/Extensions/Range.swift b/Sources/Navigator/Toolkit/Extensions/Range.swift index 82035f5063..5e7b9a2c48 100644 --- a/Sources/Navigator/Toolkit/Extensions/Range.swift +++ b/Sources/Navigator/Toolkit/Extensions/Range.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Toolkit/Extensions/UIColor.swift b/Sources/Navigator/Toolkit/Extensions/UIColor.swift index be90574a70..6520aeff6d 100644 --- a/Sources/Navigator/Toolkit/Extensions/UIColor.swift +++ b/Sources/Navigator/Toolkit/Extensions/UIColor.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Toolkit/Extensions/UIView.swift b/Sources/Navigator/Toolkit/Extensions/UIView.swift index de65e35d02..72e6ba4d3e 100644 --- a/Sources/Navigator/Toolkit/Extensions/UIView.swift +++ b/Sources/Navigator/Toolkit/Extensions/UIView.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -8,9 +8,9 @@ import Foundation import UIKit extension UIView { - // Finds the first `UIScrollView` in the view hierarchy. - // - // https://medium.com/@wailord/the-particulars-of-the-safe-area-and-contentinsetadjustmentbehavior-in-ios-11-9b842018eeaa#077b + /// Finds the first `UIScrollView` in the view hierarchy. + /// + /// https://medium.com/@wailord/the-particulars-of-the-safe-area-and-contentinsetadjustmentbehavior-in-ios-11-9b842018eeaa#077b var firstScrollView: UIScrollView? { sequence(first: self) { $0.subviews.first } .first { $0 is UIScrollView } diff --git a/Sources/Navigator/Toolkit/Extensions/WKWebView.swift b/Sources/Navigator/Toolkit/Extensions/WKWebView.swift index 2c8aada663..d23a7a4f60 100644 --- a/Sources/Navigator/Toolkit/Extensions/WKWebView.swift +++ b/Sources/Navigator/Toolkit/Extensions/WKWebView.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Toolkit/HTMLInjection.swift b/Sources/Navigator/Toolkit/HTMLInjection.swift index b56d8970c8..1b91e21054 100644 --- a/Sources/Navigator/Toolkit/HTMLInjection.swift +++ b/Sources/Navigator/Toolkit/HTMLInjection.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -17,7 +17,9 @@ protocol HTMLInjectable { } extension HTMLInjectable { - func willInject(in html: String) -> String { html } + func willInject(in html: String) -> String { + html + } /// Injects the receiver in the given `html` document. func inject(in html: String) throws -> String { @@ -68,15 +70,16 @@ struct HTMLElement: Hashable { /// Locates the `location` of this element in the given `html` document. func locate(_ location: Location, in html: String) -> String.Index? { + let nsRange = NSRange(html.startIndex..., in: html) switch location { case .start: - return startRegex.matches(in: html).first? + return startRegex.firstMatch(in: html, range: nsRange)? .range(in: html)?.upperBound case .end: - return endRegex.matches(in: html).first? + return endRegex.firstMatch(in: html, range: nsRange)? .range(in: html)?.lowerBound case .attributes: - return startRegex.matches(in: html).first + return startRegex.firstMatch(in: html, range: nsRange) .flatMap { $0.range(in: html) } .map { html.index($0.lowerBound, offsetBy: tag.count + 1) } } @@ -93,6 +96,49 @@ struct HTMLElement: Hashable { } } +extension HTMLElement { + /// Returns the trimmed value of the first attribute from `names` found in + /// this element's opening tag in `html`. Names are tried in order; the + /// first match wins. + /// + /// - Returns: `""` if the attribute is present but blank, `nil` if the + /// attribute is absent. + func attribute(firstOf names: [String], in html: String) -> String? { + guard let tagRange = startTagRange(in: html) else { return nil } + let tag = String(html[tagRange]) + let nsRange = NSRange(tag.startIndex..., in: tag) + for name in names { + let escaped = NSRegularExpression.escapedPattern(for: name) + let regex = regex(for: "\\s\(escaped)\\s*=\\s*[\"']([^\"']*)[\"']") + if let match = regex.firstMatch(in: tag, range: nsRange), + let valueRange = Range(match.range(at: 1), in: tag) + { + return String(tag[valueRange]).trimmingCharacters(in: .whitespaces) + } + } + return nil + } + + /// Returns true if this element's opening tag in `html` has any attribute + /// from `names`, regardless of its value (including blank values). + func hasAttribute(anyOf names: [String], in html: String) -> Bool { + guard let tagRange = startTagRange(in: html) else { return false } + let tag = String(html[tagRange]) + let nsRange = NSRange(tag.startIndex..., in: tag) + for name in names { + let escaped = NSRegularExpression.escapedPattern(for: name) + let regex = regex(for: "\\s\(escaped)\\s*=") + if regex.firstMatch(in: tag, range: nsRange) != nil { return true } + } + return false + } + + private func startTagRange(in html: String) -> Range? { + let nsRange = NSRange(html.startIndex..., in: html) + return startRegex.firstMatch(in: html, range: nsRange)?.range(in: html) + } +} + extension HTMLElement: CustomStringConvertible { var description: String { "<\(tag)>" @@ -174,3 +220,15 @@ extension HTMLInjection { private func escapeAttribute(_ value: String) -> String { value.replacingOccurrences(of: "\"", with: """) } + +private let regexCache: Cache = Cache() + +private func regex(for pattern: String) -> NSRegularExpression { + let key = pattern as NSString + if let cached = regexCache[key] { + return cached + } + let regex = NSRegularExpression(pattern, options: [.caseInsensitive]) + regexCache[key] = regex + return regex +} diff --git a/Sources/Navigator/Toolkit/PaginationView.swift b/Sources/Navigator/Toolkit/PaginationView.swift index 5035417cee..b72488c731 100644 --- a/Sources/Navigator/Toolkit/PaginationView.swift +++ b/Sources/Navigator/Toolkit/PaginationView.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -31,7 +31,7 @@ enum PageLocation: Equatable { protocol PageView { /// Moves the page to the given internal location. - func go(to location: PageLocation) async + func go(to location: PageLocation, animated: Bool) async } protocol PaginationViewDelegate: AnyObject { @@ -93,6 +93,11 @@ final class PaginationView: UIView, Loggable { private let scrollView = UIScrollView() + /// Set while a transition animation is in progress to prevent + /// `layoutSubviews` from resetting `contentOffset` and interrupting the + /// animation. + private var isAnimatingContentOffset = false + /// Allows the scroll view to scroll. var isScrollEnabled: Bool { didSet { scrollView.isScrollEnabled = isScrollEnabled } @@ -130,11 +135,11 @@ final class PaginationView: UIView, Loggable { } @available(*, unavailable) - public required init?(coder aDecoder: NSCoder) { + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override public func layoutSubviews() { + override func layoutSubviews() { guard !loadedViews.isEmpty else { scrollView.contentSize = bounds.size return @@ -147,7 +152,21 @@ final class PaginationView: UIView, Loggable { view.frame = CGRect(origin: CGPoint(x: xOffsetForIndex(index), y: 0), size: size) } - scrollView.contentOffset.x = xOffsetForIndex(currentIndex) + if !isAnimatingContentOffset { + scrollView.contentOffset.x = xOffsetForIndex(currentIndex) + } + } + + override func willMove(toSuperview newSuperview: UIView?) { + super.willMove(toSuperview: newSuperview) + + if newSuperview == nil { + // Remove all spread views to break retain cycles + for (_, view) in loadedViews { + view.removeFromSuperview() + } + loadedViews.removeAll() + } } override func didMoveToWindow() { @@ -249,7 +268,7 @@ final class PaginationView: UIView, Loggable { return } - await view.go(to: location) + await view.go(to: location, animated: false) await loadNextPage() } @@ -310,38 +329,83 @@ final class PaginationView: UIView, Loggable { return false } + let shouldAnimate = options.animated && !UIAccessibility.isReduceMotionEnabled + if currentIndex == index { - await scrollToView(at: index, location: location) + await scrollToView(at: index, location: location, animated: shouldAnimate) + } else if abs(currentIndex - index) == 1 { + await slideToView(at: index, location: location, animated: shouldAnimate) } else { - await fadeToView(at: index, location: location, animated: options.animated) + await fadeToView(at: index, location: location, animated: shouldAnimate) } return true } + private func slideToView(at index: Int, location: PageLocation, animated: Bool) async { + let fromOffset = scrollView.contentOffset + let targetOffset = CGPoint(x: xOffsetForIndex(index), y: fromOffset.y) + let translationX = fromOffset.x - targetOffset.x + + // We use a snapshot of the current view for two reasons: + // + // 1. The current view might get flushed when calling + // `setCurrentIndex()`, but we want to keep it on the screen during + // the animation. + // 2. A workaround for visual glitches, see https://github.com/readium/swift-toolkit/issues/737#issuecomment-4090386881 + let snapshot = snapshotView(afterScreenUpdates: false) + if let snapshot { + snapshot.frame = bounds + addSubview(snapshot) + } else { + log(.warning, "Could not take a snapshot before sliding to view at index \(index); page transition may flash") + } + + isAnimatingContentOffset = true + scrollView.isScrollEnabled = false + + defer { + snapshot?.removeFromSuperview() + isAnimatingContentOffset = false + scrollView.isScrollEnabled = isScrollEnabled + } + + setCurrentIndex(index, location: location) + + scrollView.contentOffset = fromOffset + + if animated { + await animate(duration: 0.3) { + snapshot?.transform = CGAffineTransform(translationX: translationX, y: 0) + self.scrollView.contentOffset = targetOffset + } + } else { + scrollView.contentOffset = targetOffset + } + + // There are visual glitches when scrolling web views into view. + // To prevent these, we wait a few ms before removing the snapshot. + // See https://github.com/readium/swift-toolkit/issues/737#issuecomment-4090386881 + if !animated { + try? await Task.sleep(seconds: 0.1) + } + } + private func fadeToView(at index: Int, location: PageLocation, animated: Bool) async { func fade(to alpha: CGFloat) async { - if animated { - await withCheckedContinuation { continuation in - UIView.animate(withDuration: 0.15, animations: { - self.alpha = alpha - }) { _ in - continuation.resume() - } - } - } else { + await animate(duration: animated ? 0.15 : 0) { self.alpha = alpha } } await fade(to: 0) - await scrollToView(at: index, location: location) + await scrollToView(at: index, location: location, animated: false) await fade(to: 1) } - private func scrollToView(at index: Int, location: PageLocation) async { + private func scrollToView(at index: Int, location: PageLocation, animated: Bool) async { guard currentIndex != index else { if let view = currentView { - await view.go(to: location) + await view.go(to: location, animated: animated) } return } @@ -355,16 +419,32 @@ final class PaginationView: UIView, Loggable { y: scrollView.contentOffset.y ), size: scrollView.frame.size - ), animated: false) + ), animated: animated) + } + + private func animate(duration: TimeInterval, animations: @escaping () -> Void) async { + if duration > 0 { + await withCheckedContinuation { continuation in + UIView.animate( + withDuration: duration, + animations: animations, + completion: { _ in + continuation.resume() + } + ) + } + } else { + animations() + } } } extension PaginationView: UIScrollViewDelegate { - /// We disable the scroll once the user releases the drag to prevent scrolling through more than 1 resource at a - /// time. Otherwise, because the pagination view's scroll view would have the focus during the scroll gesture, the - /// scrollable content of the resources would be skipped. - /// Note: using this approach might provide a better experience: - /// https://oleb.net/blog/2014/05/scrollviews-inside-scrollviews/ + // We disable the scroll once the user releases the drag to prevent scrolling through more than 1 resource at a + // time. Otherwise, because the pagination view's scroll view would have the focus during the scroll gesture, the + // scrollable content of the resources would be skipped. + // Note: using this approach might provide a better experience: + // https://oleb.net/blog/2014/05/scrollviews-inside-scrollviews/ func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { scrollView.isScrollEnabled = false @@ -380,7 +460,12 @@ extension PaginationView: UIScrollViewDelegate { } } - public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + // A programmatic slide animation sets isScrollEnabled = false and drives the + // content offset directly. If a delegate callback fires during or just after + // that window it could call setCurrentIndex with a stale offset, so we bail out. + guard !isAnimatingContentOffset else { return } + scrollView.isScrollEnabled = isScrollEnabled let currentOffset = (readingProgression == .rtl) diff --git a/Sources/Navigator/Toolkit/ReadiumNavigatorLocalizedString.swift b/Sources/Navigator/Toolkit/ReadiumNavigatorLocalizedString.swift index 7f3267023e..619a1d5e1e 100644 --- a/Sources/Navigator/Toolkit/ReadiumNavigatorLocalizedString.swift +++ b/Sources/Navigator/Toolkit/ReadiumNavigatorLocalizedString.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Toolkit/TargetAction.swift b/Sources/Navigator/Toolkit/TargetAction.swift index 84ab698732..8195efc7d5 100644 --- a/Sources/Navigator/Toolkit/TargetAction.swift +++ b/Sources/Navigator/Toolkit/TargetAction.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Navigator/Toolkit/WebView.swift b/Sources/Navigator/Toolkit/WebView.swift index 7d843722b1..fe561b87e6 100644 --- a/Sources/Navigator/Toolkit/WebView.swift +++ b/Sources/Navigator/Toolkit/WebView.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -12,21 +12,14 @@ import WebKit final class WebView: WKWebView { private let editingActions: EditingActionsController - init(editingActions: EditingActionsController) { - self.editingActions = editingActions - - let config = WKWebViewConfiguration() - config.mediaTypesRequiringUserActionForPlayback = .all + convenience init(editingActions: EditingActionsController) { + self.init(editingActions: editingActions, configuration: WKWebViewConfiguration()) + } - // Disable the Apple Intelligence Writing tools in the web views. - // See https://github.com/readium/swift-toolkit/issues/509#issuecomment-2577780749 - #if compiler(>=6.0) - if #available(iOS 18.0, *) { - config.writingToolsBehavior = .none - } - #endif + init(editingActions: EditingActionsController, configuration: WKWebViewConfiguration) { + self.editingActions = editingActions - super.init(frame: .zero, configuration: config) + super.init(frame: .zero, configuration: configuration) #if DEBUG && swift(>=5.8) if #available(macOS 13.3, iOS 16.4, *) { diff --git a/Sources/Navigator/VisualNavigator.swift b/Sources/Navigator/VisualNavigator.swift index 5edc793f52..c9bf1ccca5 100644 --- a/Sources/Navigator/VisualNavigator.swift +++ b/Sources/Navigator/VisualNavigator.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -19,20 +19,18 @@ public protocol VisualNavigator: Navigator, InputObservable { /// Moves to the left content portion (eg. page) relative to the reading /// progression direction. /// - /// - Parameter completion: Called when the transition is completed. - /// - Returns: Whether the navigator is able to move to the previous - /// content portion. The completion block is only called if true was - /// returned. + /// - Parameter options: Options for moving the content to the left. + /// - Returns: Whether the navigator was able to move to the left content + /// portion. @discardableResult func goLeft(options: NavigatorGoOptions) async -> Bool /// Moves to the right content portion (eg. page) relative to the reading /// progression direction. /// - /// - Parameter completion: Called when the transition is completed. - /// - Returns: Whether the navigator is able to move to the previous - /// content portion. The completion block is only called if true was - /// returned. + /// - Parameter options: Options for moving the content to the right. + /// - Returns: Whether the navigator was able to move to the right content + /// portion. @discardableResult func goRight(options: NavigatorGoOptions) async -> Bool diff --git a/Sources/OPDS/OPDS1Parser.swift b/Sources/OPDS/OPDS1Parser.swift index 535926b15e..020f54d3c0 100644 --- a/Sources/OPDS/OPDS1Parser.swift +++ b/Sources/OPDS/OPDS1Parser.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,16 +9,16 @@ import ReadiumFuzi import ReadiumShared public enum OPDS1ParserError: Error { - // The title is missing from the feed. + /// The title is missing from the feed. case missingTitle - // Root is not found + /// Root is not found case rootNotFound } public enum OPDSParserOpenSearchHelperError: Error { - // Search link not found in feed + /// Search link not found in feed case searchLinkNotFound - // OpenSearch document is invalid + /// OpenSearch document is invalid case searchDocumentIsInvalid } @@ -30,7 +30,10 @@ struct MimeTypeParameters { public class OPDS1Parser: Loggable { /// Parse an OPDS feed or publication. /// Feed can only be v1 (XML). - /// - parameter url: The feed URL + /// - Parameters: + /// - url: The feed URL. + /// - completion: A closure called when the parsing is complete, returning the parsed data + /// or an error if the operation failed. public static func parseURL(url: URL, completion: @escaping (ParseData?, Error?) -> Void) { URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data, let response = response else { @@ -152,9 +155,9 @@ public class OPDS1Parser: Loggable { let href = link.attr("href"), let absoluteHref = URLHelper.getAbsolute(href: href, base: feedURL) { - var properties: [String: Any] = [:] - if let facetElementCount = link.attr("count").map(Int.init) { - properties["numberOfItems"] = facetElementCount + var properties: [String: JSONValue] = [:] + if let facetElementCount = link.attr("count").flatMap(Int.init) { + properties["numberOfItems"] = .integer(facetElementCount) } let newLink = Link( @@ -183,7 +186,7 @@ public class OPDS1Parser: Loggable { if let rel = link.attributes["rel"], !rel.isEmpty { rels.append(.init(rel)) } - var properties: [String: Any] = [:] + var properties: [String: JSONValue] = [:] let isFacet = rels.contains(.opdsFacet) if isFacet { @@ -192,8 +195,8 @@ public class OPDS1Parser: Loggable { rels.append(.self) } - if let facetElementCount = link.attr("count").map(Int.init) { - properties["numberOfItems"] = facetElementCount + if let facetElementCount = link.attr("count").flatMap(Int.init) { + properties["numberOfItems"] = .integer(facetElementCount) } } @@ -219,8 +222,11 @@ public class OPDS1Parser: Loggable { /// Parse an OPDS publication. /// Publication can only be v1 (XML). - /// - parameter document: The XMLDocument data - /// - Returns: The resulting Publication + /// - Parameters: + /// - document: The XMLDocument data. + /// - feedURL: The base URL of the feed, used to resolve relative links. + /// - Returns: The resulting `Publication`, or `nil` if the entry couldn't be parsed. + /// - Throws: An error if the XML parsing or validation fails. public static func parseEntry(document: ReadiumFuzi.XMLDocument, feedURL: URL) throws -> Publication? { guard let root = document.root else { throw OPDS1ParserError.rootNotFound @@ -229,7 +235,10 @@ public class OPDS1Parser: Loggable { } /// Fetch an Open Search template from an OPDS feed. - /// - parameter feed: The OPDS feed + /// - Parameters: + /// - feed: The OPDS feed to search for the template. + /// - completion: A closure called with the OpenSearch template as a `String` if found, + /// or an `Error` if the fetch or parsing failed. public static func fetchOpenSearchTemplate(feed: Feed, completion: @escaping (String?, Error?) -> Void) { guard let openSearchHref = feed.links.firstWithRel(.search)?.href, let openSearchURL = URL(string: openSearchHref) @@ -301,7 +310,7 @@ public class OPDS1Parser: Loggable { } static func parseEntry(entry: ReadiumFuzi.XMLElement, feedURL: URL) -> Publication? { - // Shortcuts to get tag(s)' string value. + /// Shortcuts to get tag(s)' string value. func tag(_ name: String) -> String? { entry.firstChild(tag: name)?.stringValue } @@ -345,8 +354,8 @@ public class OPDS1Parser: Loggable { publishers: tags("publisher").map { Contributor(name: $0) }, description: tag("content") ?? tag("summary"), otherMetadata: [ - "rights": tags("rights").joined(separator: " "), - ] + "rights": .string(tags("rights").joined(separator: " ")), + ] as [String: JSONValue] ) // Links. @@ -357,13 +366,13 @@ public class OPDS1Parser: Loggable { continue } - var properties: [String: Any] = [:] - if let price = parsePrice(link: linkElement)?.json, !price.isEmpty { - properties["price"] = price + var properties: [String: JSONValue] = [:] + if let price = parsePrice(link: linkElement) { + properties["price"] = .object(price.jsonObject) } - let indirectAcquisition = parseIndirectAcquisition(children: linkElement.children(tag: "indirectAcquisition")).json + let indirectAcquisition = parseIndirectAcquisition(children: linkElement.children(tag: "indirectAcquisition")) if !indirectAcquisition.isEmpty { - properties["indirectAcquisition"] = indirectAcquisition + properties["indirectAcquisition"] = indirectAcquisition.jsonValue } let link = Link( diff --git a/Sources/OPDS/OPDS2Parser.swift b/Sources/OPDS/OPDS2Parser.swift index 98deabb553..bd7189e43f 100644 --- a/Sources/OPDS/OPDS2Parser.swift +++ b/Sources/OPDS/OPDS2Parser.swift @@ -1,11 +1,10 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 Foundation - import ReadiumShared public enum OPDS2ParserError: Error { @@ -22,7 +21,10 @@ public enum OPDS2ParserError: Error { public class OPDS2Parser: Loggable { /// Parse an OPDS feed or publication. /// Feed can only be v2 (JSON). - /// - parameter url: The feed URL + /// - Parameters: + /// - url: The feed URL. + /// - completion: A closure called when the parsing is complete, returning the + /// parsed `ParseData` on success, or an `Error` if the operation failed. public static func parseURL(url: URL, completion: @escaping (ParseData?, Error?) -> Void) { URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data, let response = response else { @@ -48,11 +50,9 @@ public class OPDS2Parser: Loggable { public static func parse(jsonData: Data, url: URL, response: URLResponse) throws -> ParseData { var parseData = ParseData(url: url, response: response, version: .OPDS2) - guard let jsonRoot = try? JSONSerialization.jsonObject(with: jsonData, options: []) else { - throw OPDS2ParserError.invalidJSON - } - - guard let topLevelDict = jsonRoot as? [String: Any] else { + guard let jsonRoot = try? JSONValue(jsonData: jsonData), + let topLevelDict = jsonRoot.object + else { throw OPDS2ParserError.invalidJSON } @@ -63,8 +63,7 @@ public class OPDS2Parser: Loggable { topLevelDict["facets"] == nil { // Publication only - parseData.publication = try Publication(json: topLevelDict) - + parseData.publication = try Publication(json: jsonRoot) } else { // Feed parseData.feed = try parse(feedURL: url, jsonDict: topLevelDict) @@ -78,14 +77,17 @@ public class OPDS2Parser: Loggable { /// Parse an OPDS feed. /// Feed can only be v2 (JSON). - /// - parameter jsonDict: The json top level dictionary - /// - Returns: The resulting Feed - public static func parse(feedURL: URL, jsonDict: [String: Any]) throws -> Feed { - guard let metadataDict = jsonDict["metadata"] as? [String: Any] else { + /// - Parameters: + /// - feedURL: The URL of the feed being parsed, used to resolve relative links. + /// - jsonDict: The JSON top-level dictionary. + /// - Returns: The resulting `Feed` object. + /// - Throws: An error if the JSON structure is invalid or missing required OPDS fields. + public static func parse(feedURL: URL, jsonDict: [String: JSONValue]) throws -> Feed { + guard let metadataDict = jsonDict["metadata"]?.object else { throw OPDS2ParserError.metadataNotFound } - guard let title = metadataDict["title"] as? String else { + guard let title = metadataDict["title"]?.string else { throw OPDS2ParserError.missingTitle } @@ -95,38 +97,35 @@ public class OPDS2Parser: Loggable { for (k, v) in jsonDict { switch k { case "@context": - switch v { - case let s as String: + if let s = v.string { feed.context.append(s) - case let sArr as [String]: - feed.context.append(contentsOf: sArr) - default: - continue + } else if let sArr = v.array { + feed.context.append(contentsOf: sArr.compactMap(\.string)) } case "metadata": // Already handled above continue case "links": - guard let links = v as? [[String: Any]] else { + guard let links = v.array else { throw OPDS2ParserError.invalidLink } try parseLinks(feed: feed, feedURL: feedURL, links: links) case "facets": - guard let facets = v as? [[String: Any]] else { + guard let facets = v.array else { throw OPDS2ParserError.invalidFacet } try parseFacets(feed: feed, feedURL: feedURL, facets: facets) case "publications": - guard let publications = v as? [[String: Any]] else { + guard let publications = v.array else { throw OPDS2ParserError.invalidPublication } try parsePublications(feed: feed, feedURL: feedURL, publications: publications) case "navigation": - guard let navLinks = v as? [[String: Any]] else { + guard let navLinks = v.array else { throw OPDS2ParserError.invalidNavigation } try parseNavigation(feed: feed, feedURL: feedURL, navLinks: navLinks) case "groups": - guard let groups = v as? [[String: Any]] else { + guard let groups = v.array else { throw OPDS2ParserError.invalidGroup } try parseGroups(feed: feed, feedURL: feedURL, groups: groups) @@ -138,50 +137,54 @@ public class OPDS2Parser: Loggable { return feed } - static func parseMetadata(opdsMetadata: OpdsMetadata, metadataDict: [String: Any]) { + static func parseMetadata(opdsMetadata: OpdsMetadata, metadataDict: [String: JSONValue]) { for (k, v) in metadataDict { switch k { case "title": - if let title = v as? String { + if let title = v.string { opdsMetadata.title = title } case "numberOfItems": - opdsMetadata.numberOfItem = v as? Int + opdsMetadata.numberOfItem = v.integer case "itemsPerPage": - opdsMetadata.itemsPerPage = v as? Int + opdsMetadata.itemsPerPage = v.integer case "modified": - if let dateStr = v as? String { + if let dateStr = v.string { opdsMetadata.modified = dateStr.dateFromISO8601 } case "@type": - opdsMetadata.rdfType = v as? String + opdsMetadata.rdfType = v.string case "currentPage": - opdsMetadata.currentPage = v as? Int + opdsMetadata.currentPage = v.integer default: continue } } } - static func parseFacets(feed: Feed, feedURL: URL, facets: [[String: Any]]) throws { - for facetDict in facets { - guard let metadata = facetDict["metadata"] as? [String: Any] else { + static func parseFacets(feed: Feed, feedURL: URL, facets: [JSONValue]) throws { + for facetValue in facets { + guard let facetDict = facetValue.object else { continue } + guard let metadata = facetDict["metadata"]?.object else { throw OPDS2ParserError.invalidFacet } - guard let title = metadata["title"] as? String else { + guard let title = metadata["title"]?.string else { throw OPDS2ParserError.invalidFacet } + let facet = Facet(title: title) parseMetadata(opdsMetadata: facet.metadata, metadataDict: metadata) + for (k, v) in facetDict { if k == "links" { - guard let links = v as? [[String: Any]] else { + guard let links = v.array else { throw OPDS2ParserError.invalidFacet } - for linkDict in links { - var link = try Link(json: linkDict) - try link.normalizeHREFs(to: feedURL) - facet.links.append(link) + for linkValue in links { + if var link = try Link(json: linkValue) { + try link.normalizeHREFs(to: feedURL) + facet.links.append(link) + } } } } @@ -189,68 +192,75 @@ public class OPDS2Parser: Loggable { } } - static func parseLinks(feed: Feed, feedURL: URL, links: [[String: Any]]) throws { - for linkDict in links { - var link = try Link(json: linkDict) - try link.normalizeHREFs(to: feedURL) - feed.links.append(link) + static func parseLinks(feed: Feed, feedURL: URL, links: [JSONValue]) throws { + for linkValue in links { + if var link = try Link(json: linkValue) { + try link.normalizeHREFs(to: feedURL) + feed.links.append(link) + } } } - static func parsePublications(feed: Feed, feedURL: URL, publications: [[String: Any]]) throws { - for pubDict in publications { - let pub = try Publication(json: pubDict) + static func parsePublications(feed: Feed, feedURL: URL, publications: [JSONValue]) throws { + for pubValue in publications { + let pub = try Publication(json: pubValue) feed.publications.append(pub) } } - static func parseNavigation(feed: Feed, feedURL: URL, navLinks: [[String: Any]]) throws { - for navDict in navLinks { - var link = try Link(json: navDict) - try link.normalizeHREFs(to: feedURL) - feed.navigation.append(link) + static func parseNavigation(feed: Feed, feedURL: URL, navLinks: [JSONValue]) throws { + for navValue in navLinks { + if var link = try Link(json: navValue) { + try link.normalizeHREFs(to: feedURL) + feed.navigation.append(link) + } } } - static func parseGroups(feed: Feed, feedURL: URL, groups: [[String: Any]]) throws { - for groupDict in groups { - guard let metadata = groupDict["metadata"] as? [String: Any] else { + static func parseGroups(feed: Feed, feedURL: URL, groups: [JSONValue]) throws { + for groupValue in groups { + guard let groupDict = groupValue.object else { continue } + guard let metadata = groupDict["metadata"]?.object else { throw OPDS2ParserError.invalidGroup } - guard let title = metadata["title"] as? String else { + guard let title = metadata["title"]?.string else { throw OPDS2ParserError.invalidGroup } + let group = Group(title: title) parseMetadata(opdsMetadata: group.metadata, metadataDict: metadata) + for (k, v) in groupDict { switch k { case "metadata": // Already handled above continue case "links": - guard let links = v as? [[String: Any]] else { + guard let links = v.array else { throw OPDS2ParserError.invalidGroup } - for linkDict in links { - var link = try Link(json: linkDict) - try link.normalizeHREFs(to: feedURL) - group.links.append(link) + for linkValue in links { + if var link = try Link(json: linkValue) { + try link.normalizeHREFs(to: feedURL) + group.links.append(link) + } } case "navigation": - guard let links = v as? [[String: Any]] else { + guard let links = v.array else { throw OPDS2ParserError.invalidGroup } - for linkDict in links { - var link = try Link(json: linkDict) - try link.normalizeHREFs(to: feedURL) - group.navigation.append(link) + for linkValue in links { + if var link = try Link(json: linkValue) { + try link.normalizeHREFs(to: feedURL) + group.navigation.append(link) + } } case "publications": - guard let publications = v as? [[String: Any]] else { + guard let publications = v.array else { throw OPDS2ParserError.invalidGroup } - for pubDict in publications { - let publication = try Publication(json: pubDict) + for pubValue in publications { + let publication = try Publication(json: pubValue) group.publications.append(publication) } default: diff --git a/Sources/OPDS/OPDSParser.swift b/Sources/OPDS/OPDSParser.swift index 4b1125ed89..80031b9397 100644 --- a/Sources/OPDS/OPDSParser.swift +++ b/Sources/OPDS/OPDSParser.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -17,7 +17,10 @@ public enum OPDSParser { /// Parse an OPDS feed or publication. /// Feed can be v1 (XML) or v2 (JSON). - /// - parameter url: The feed URL + /// - Parameters: + /// - url: The feed URL. + /// - completion: A closure called when the parsing is complete, returning the + /// parsed `ParseData` on success, or an `Error` if the operation failed. public static func parseURL(url: URL, completion: @escaping (ParseData?, Error?) -> Void) { feedURL = url diff --git a/Sources/OPDS/ParseData.swift b/Sources/OPDS/ParseData.swift index 2ae11131a9..116c57ef0d 100644 --- a/Sources/OPDS/ParseData.swift +++ b/Sources/OPDS/ParseData.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/OPDS/URLHelper.swift b/Sources/OPDS/URLHelper.swift index 0fe7c4555e..5157fc5dff 100644 --- a/Sources/OPDS/URLHelper.swift +++ b/Sources/OPDS/URLHelper.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/OPDS/XMLNamespace.swift b/Sources/OPDS/XMLNamespace.swift index b379820a38..940929c4cf 100644 --- a/Sources/OPDS/XMLNamespace.swift +++ b/Sources/OPDS/XMLNamespace.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Shared/Logger/Loggable.swift b/Sources/Shared/Logger/Loggable.swift index 2c6ee4b7a8..b15a9e7e19 100644 --- a/Sources/Shared/Logger/Loggable.swift +++ b/Sources/Shared/Logger/Loggable.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Shared/Logger/Logger.swift b/Sources/Shared/Logger/Logger.swift index d8ed23b876..c92a9bc24f 100644 --- a/Sources/Shared/Logger/Logger.swift +++ b/Sources/Shared/Logger/Logger.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,7 +9,10 @@ import Foundation /// Initialize the Logger. /// Default logger is the `LoggerStub` class /// -/// - Parameter customLogger: The Logger that will be used for printing logs. +/// - Parameters: +/// - level: The minimum severity level for logs to be processed. +/// - customLogger: The Logger that will be used for printing logs. +/// Defaults to a `LoggerStub` which may perform no-op logging. public func ReadiumEnableLog(withMinimumSeverityLevel level: SeverityLevel, customLogger: LoggerType = LoggerStub()) { Logger.sharedInstance.setupLogger(logger: customLogger) Logger.sharedInstance.setMinimumSeverityLevel(at: level) @@ -28,10 +31,10 @@ public final class Logger { /// throughout the framework. There is a default implementation `StubLogger` /// available. You can define your own implementation by applying the /// `Loggable` protocol to your xLogger class. - internal var activeLogger: LoggerType? + var activeLogger: LoggerType? /// The minimum severity level for logs to be displayed. - internal var minimumSeverityLevel: SeverityLevel? + var minimumSeverityLevel: SeverityLevel? private(set) static var sharedInstance = Logger() @@ -63,7 +66,7 @@ public final class Logger { // MARK: - Internal methods. - internal func log(_ value: Any?, at level: SeverityLevel, file: String, line: Int) { + func log(_ value: Any?, at level: SeverityLevel, file: String, line: Int) { if let minimumSeverityLevel = minimumSeverityLevel { guard level.numericValue >= minimumSeverityLevel.numericValue else { return diff --git a/Sources/Shared/Logger/LoggerStub.swift b/Sources/Shared/Logger/LoggerStub.swift index 3a5d5f2c55..c4f55e5554 100644 --- a/Sources/Shared/Logger/LoggerStub.swift +++ b/Sources/Shared/Logger/LoggerStub.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Shared/OPDS/Facet.swift b/Sources/Shared/OPDS/Facet.swift index fc38245855..8eb52e1c28 100644 --- a/Sources/Shared/OPDS/Facet.swift +++ b/Sources/Shared/OPDS/Facet.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Shared/OPDS/Feed.swift b/Sources/Shared/OPDS/Feed.swift index 9c77f8b816..1f2f0a97cb 100644 --- a/Sources/Shared/OPDS/Feed.swift +++ b/Sources/Shared/OPDS/Feed.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -21,7 +21,7 @@ public class Feed { /// Return a String representing the URL of the searchLink of the feed. /// /// - Returns: The HREF value of the search link - internal func getSearchLinkHref() -> String? { + func getSearchLinkHref() -> String? { links.firstWithRel(.search)?.href } } diff --git a/Sources/Shared/OPDS/Group.swift b/Sources/Shared/OPDS/Group.swift index 960fed4da1..992abf7bd1 100644 --- a/Sources/Shared/OPDS/Group.swift +++ b/Sources/Shared/OPDS/Group.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Shared/OPDS/OPDSAcquisition.swift b/Sources/Shared/OPDS/OPDSAcquisition.swift index 902fa779f6..c85bcaa741 100644 --- a/Sources/Shared/OPDS/OPDSAcquisition.swift +++ b/Sources/Shared/OPDS/OPDSAcquisition.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,51 +9,38 @@ import ReadiumInternal /// OPDS Acquisition Object /// https://drafts.opds.io/schema/acquisition-object.schema.json -public struct OPDSAcquisition: Equatable { +public struct OPDSAcquisition: Equatable, JSONObjectEncodable, JSONValueDecodable { public var type: String public var children: [OPDSAcquisition] = [] - public var mediaType: MediaType? { MediaType(type) } + public var mediaType: MediaType? { + MediaType(type) + } public init(type: String, children: [OPDSAcquisition] = []) { self.type = type self.children = children } - public init?(json: Any?, warnings: WarningLogger? = nil) throws { - guard let jsonObject = json as? [String: Any], - let type = jsonObject["type"] as? String + public init?(json: T?, warnings: WarningLogger?) throws { + let json = json?.jsonValue + + guard + let jsonObject = json?.object, + let type = jsonObject["type"]?.string else { warnings?.log("`type` is required", model: Self.self, source: json) throw JSONError.parsing(Self.self) } self.type = type - children = [OPDSAcquisition](json: jsonObject["child"], warnings: warnings) + children = jsonObject["child"]?.decode(warnings: warnings) ?? [] } - public var json: [String: Any] { - makeJSON([ + public var jsonObject: [String: JSONValue] { + .init([ "type": type, - "child": encodeIfNotEmpty(children.json), + "child": children.orNullIfEmpty, ]) } } - -public extension Array where Element == OPDSAcquisition { - /// Parses multiple JSON acquisitions into an array of OPDSAcquisitions. - /// eg. let acquisitions = [OPDSAcquisition](json: [...]) - init(json: Any?, warnings: WarningLogger? = nil) { - self.init() - guard let json = json as? [[String: Any]] else { - return - } - - let acquisitions = json.compactMap { try? OPDSAcquisition(json: $0, warnings: warnings) } - append(contentsOf: acquisitions) - } - - var json: [[String: Any]] { - map(\.json) - } -} diff --git a/Sources/Shared/OPDS/OPDSAvailability.swift b/Sources/Shared/OPDS/OPDSAvailability.swift index d013952745..9d967cbe8c 100644 --- a/Sources/Shared/OPDS/OPDSAvailability.swift +++ b/Sources/Shared/OPDS/OPDSAvailability.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,7 +9,7 @@ import ReadiumInternal /// Indicated the availability of a given resource. /// https://drafts.opds.io/schema/properties.schema.json -public struct OPDSAvailability: Equatable { +public struct OPDSAvailability: Equatable, JSONValueDecodable, JSONObjectEncodable { public let state: State /// Timestamp for the previous state change. @@ -24,12 +24,12 @@ public struct OPDSAvailability: Equatable { self.until = until } - public init?(json: Any?, warnings: WarningLogger? = nil) throws { - if json == nil { + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue else { return nil } - guard let jsonObject = json as? [String: Any], - let state: State = parseRaw(jsonObject["state"]) + guard let jsonObject = json.object, + let state: State = jsonObject["state"]?.decode() else { warnings?.log("`state` is required", model: Self.self, source: json) throw JSONError.parsing(Self.self) @@ -37,16 +37,16 @@ public struct OPDSAvailability: Equatable { self.init( state: state, - since: parseDate(jsonObject["since"]), - until: parseDate(jsonObject["until"]) + since: jsonObject["since"]?.date, + until: jsonObject["until"]?.date ) } - public var json: [String: Any] { - makeJSON([ - "state": encodeRawIfNotNil(state), - "since": encodeIfNotNil(since?.iso8601), - "until": encodeIfNotNil(until?.iso8601), + public var jsonObject: [String: JSONValue] { + .init([ + "state": state.rawValue, + "since": since?.iso8601, + "until": until?.iso8601, ]) } diff --git a/Sources/Shared/OPDS/OPDSCopies.swift b/Sources/Shared/OPDS/OPDSCopies.swift index 0cece14339..08679620bc 100644 --- a/Sources/Shared/OPDS/OPDSCopies.swift +++ b/Sources/Shared/OPDS/OPDSCopies.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,7 +9,7 @@ import ReadiumInternal /// Library-specific feature that contains information about the copies that a library has acquired. /// https://drafts.opds.io/schema/properties.schema.json -public struct OPDSCopies: Equatable { +public struct OPDSCopies: Equatable, JSONValueDecodable, JSONObjectEncodable { public let total: Int? public let available: Int? @@ -18,25 +18,25 @@ public struct OPDSCopies: Equatable { self.available = available } - public init?(json: Any?, warnings: WarningLogger? = nil) throws { - if json == nil { + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue else { return nil } - guard let jsonObject = json as? [String: Any] else { + guard let jsonObject = json.object else { warnings?.log("Invalid Copies object", model: Self.self, source: json) throw JSONError.parsing(Self.self) } self.init( - total: parsePositive(jsonObject["total"]), - available: parsePositive(jsonObject["available"]) + total: jsonObject["total"]?.nonNegative(), + available: jsonObject["available"]?.nonNegative() ) } - public var json: [String: Any] { - makeJSON([ - "total": encodeIfNotNil(total), - "available": encodeIfNotNil(available), + public var jsonObject: [String: JSONValue] { + .init([ + "total": total, + "available": available, ]) } } diff --git a/Sources/Shared/OPDS/OPDSHolds.swift b/Sources/Shared/OPDS/OPDSHolds.swift index 4804418f3b..12676477b1 100644 --- a/Sources/Shared/OPDS/OPDSHolds.swift +++ b/Sources/Shared/OPDS/OPDSHolds.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,7 +9,7 @@ import ReadiumInternal /// Library-specific features when a specific book is unavailable but provides a hold list. /// https://drafts.opds.io/schema/properties.schema.json -public struct OPDSHolds: Equatable { +public struct OPDSHolds: Equatable, JSONValueDecodable, JSONObjectEncodable { public let total: Int? public let position: Int? @@ -18,25 +18,25 @@ public struct OPDSHolds: Equatable { self.position = position } - public init?(json: Any?, warnings: WarningLogger? = nil) throws { - if json == nil { + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue else { return nil } - guard let jsonObject = json as? [String: Any] else { + guard let jsonObject = json.object else { warnings?.log("Invalid Holds object", model: Self.self, source: json) throw JSONError.parsing(Self.self) } self.init( - total: parsePositive(jsonObject["total"]), - position: parsePositive(jsonObject["position"]) + total: jsonObject["total"]?.nonNegative(), + position: jsonObject["position"]?.nonNegative() ) } - public var json: [String: Any] { - makeJSON([ - "total": encodeIfNotNil(total), - "position": encodeIfNotNil(position), + public var jsonObject: [String: JSONValue] { + .init([ + "total": total, + "position": position, ]) } } diff --git a/Sources/Shared/OPDS/OPDSPrice.swift b/Sources/Shared/OPDS/OPDSPrice.swift index 9da287c135..80416bb2fb 100644 --- a/Sources/Shared/OPDS/OPDSPrice.swift +++ b/Sources/Shared/OPDS/OPDSPrice.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,10 +9,10 @@ import ReadiumInternal /// The price of a publication in an OPDS link. /// https://drafts.opds.io/schema/properties.schema.json -public struct OPDSPrice: Equatable { +public struct OPDSPrice: Equatable, JSONValueDecodable, JSONObjectEncodable { public var currency: String // eg. EUR - // Should only be used for display purposes, because of precision issues inherent with Double and the JSON parsing. + /// Should only be used for display purposes, because of precision issues inherent with Double and the JSON parsing. public var value: Double public init(currency: String, value: Double) { @@ -20,13 +20,14 @@ public struct OPDSPrice: Equatable { self.value = value } - public init?(json: Any?, warnings: WarningLogger? = nil) throws { - if json == nil { + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue else { return nil } - guard let jsonObject = json as? [String: Any], - let currency = jsonObject["currency"] as? String, - let value = parsePositiveDouble(jsonObject["value"]) + + guard let jsonObject = json.object, + let currency = jsonObject["currency"]?.string, + let value: Double = jsonObject["value"]?.nonNegative() else { warnings?.log("`currency` and `value` are required", model: Self.self, source: json) throw JSONError.parsing(Self.self) @@ -36,10 +37,10 @@ public struct OPDSPrice: Equatable { self.value = value } - public var json: [String: Any] { - [ + public var jsonObject: [String: JSONValue] { + .init([ "currency": currency, "value": value, - ] + ]) } } diff --git a/Sources/Shared/OPDS/OpdsMetadata.swift b/Sources/Shared/OPDS/OpdsMetadata.swift index 7661313574..26b9d0aa62 100644 --- a/Sources/Shared/OPDS/OpdsMetadata.swift +++ b/Sources/Shared/OPDS/OpdsMetadata.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Shared/Publication/Accessibility/Accessibility.swift b/Sources/Shared/Publication/Accessibility/Accessibility.swift index fbe8d01985..9ff67cb0bf 100644 --- a/Sources/Shared/Publication/Accessibility/Accessibility.swift +++ b/Sources/Shared/Publication/Accessibility/Accessibility.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -11,7 +11,7 @@ import ReadiumInternal /// /// https://www.w3.org/2021/a11y-discov-vocab/latest/ /// https://readium.org/webpub-manifest/schema/a11y.schema.json -public struct Accessibility: Hashable, Sendable { +public struct Accessibility: Hashable, Sendable, JSONValueDecodable, JSONObjectEncodable { /// An established standard to which the described resource conforms. public var conformsTo: [Profile] @@ -53,7 +53,7 @@ public struct Accessibility: Hashable, Sendable { public var exemptions: [Exemption] /// Accessibility profile. - public struct Profile: Hashable, Sendable { + public struct Profile: Hashable, RawRepresentable, Sendable { public let uri: String public init(_ uri: String) { @@ -108,9 +108,19 @@ public struct Accessibility: Hashable, Sendable { || self == Self.epubA11y11WCAG21AAA || self == Self.epubA11y11WCAG22AAA } + + // MARK: - RawRepresentable + + public var rawValue: String { + uri + } + + public init?(rawValue: String) { + self.init(rawValue) + } } - public struct Certification: Hashable, Sendable { + public struct Certification: Hashable, Sendable, JSONValueDecodable, JSONObjectEncodable { /// Identifies a party responsible for the testing and certification of the accessibility of a Publication. /// /// https://www.w3.org/TR/epub-a11y/#certifiedBy @@ -133,9 +143,29 @@ public struct Accessibility: Hashable, Sendable { self.credential = credential self.report = report } + + public init?(json: T?, warnings: (any WarningLogger)?) throws { + guard let json = json?.jsonValue.object else { + return nil + } + + self.init( + certifiedBy: json["certifiedBy"]?.string, + credential: json["credential"]?.string, + report: json["report"]?.string + ) + } + + public var jsonObject: [String: JSONValue] { + .init([ + "certifiedBy": certifiedBy, + "credential": credential, + "report": report, + ]) + } } - public struct AccessMode: Hashable, Sendable { + public struct AccessMode: RawRepresentable, Hashable, Sendable { public let id: String public init(_ id: String) { @@ -178,6 +208,16 @@ public struct Accessibility: Hashable, Sendable { /// Indicates that the resource contains information encoded in visual form. public static let visual = AccessMode("visual") + + // MARK: - RawRepresentable + + public var rawValue: String { + id + } + + public init?(rawValue: String) { + self.init(rawValue) + } } public enum PrimaryAccessMode: String, Hashable, Sendable { @@ -197,7 +237,7 @@ public struct Accessibility: Hashable, Sendable { case visual } - public struct Feature: Hashable, Sendable { + public struct Feature: Hashable, RawRepresentable, Sendable { public let id: String public init(_ id: String) { @@ -431,9 +471,19 @@ public struct Accessibility: Hashable, Sendable { /// Indicates that the content can be rendered without additional word /// segmentation. public static let withoutAdditionalWordSegmentation = Feature("withoutAdditionalWordSegmentation") + + // MARK: - RawRepresentable + + public var rawValue: String { + id + } + + public init?(rawValue: String) { + self.init(rawValue) + } } - public struct Hazard: Hashable, Sendable { + public struct Hazard: Hashable, RawRepresentable, Sendable { public let id: String public init(_ id: String) { @@ -482,6 +532,16 @@ public struct Accessibility: Hashable, Sendable { /// Indicates that the resource does not contain any hazards. public static let none = Hazard("none") + + // MARK: - RawRepresentable + + public var rawValue: String { + id + } + + public init?(rawValue: String) { + self.init(rawValue) + } } /// ``Exemption`` allows content creators to identify publications that do @@ -491,7 +551,7 @@ public struct Accessibility: Hashable, Sendable { /// While this list is currently limited to exemptions covered by the /// European Accessibility Act, it will be extended to cover additional /// exemptions in the future. - public struct Exemption: Hashable, Sendable { + public struct Exemption: Hashable, RawRepresentable, Sendable { public let id: String public init(_ id: String) { @@ -525,6 +585,16 @@ public struct Accessibility: Hashable, Sendable { /// requirements. /// https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32019L0882#d1e1798-70-1 public static let eaaMicroenterprise = Exemption("eaa-microenterprise") + + // MARK: - RawRepresentable + + public var rawValue: String { + id + } + + public init?(rawValue: String) { + self.init(rawValue) + } } public init( @@ -547,62 +617,40 @@ public struct Accessibility: Hashable, Sendable { self.exemptions = exemptions } - public init?(json: Any?, warnings: WarningLogger? = nil) throws { - guard json != nil else { + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue else { return nil } - guard let jsonObject = json as? [String: Any] else { + guard let jsonObject = json.object else { warnings?.log("Invalid Accessibility object", model: Self.self, source: json, severity: .moderate) throw JSONError.parsing(Self.self) } - self.init( - conformsTo: (parseArray(jsonObject["conformsTo"], allowingSingle: true) as [String]) - .map(Profile.init), - certification: (jsonObject["certification"] as? [String: Any]) - .map { - Certification( - certifiedBy: $0["certifiedBy"] as? String, - credential: $0["credential"] as? String, - report: $0["report"] as? String - ) - } + try self.init( + conformsTo: jsonObject["conformsTo"]?.decode(allowingSingle: true) ?? [], + certification: jsonObject["certification"]?.decode() .takeIf { $0.certifiedBy != nil || $0.credential != nil || $0.report != nil }, - summary: jsonObject["summary"] as? String, - accessModes: parseArray(jsonObject["accessMode"]).map(AccessMode.init), - accessModesSufficient: (jsonObject["accessModeSufficient"] as? [Any] ?? []) - .map { json -> [Accessibility.PrimaryAccessMode] in - if let str = json as? String, let value = PrimaryAccessMode(rawValue: str) { - return [value] - } else if let strs = json as? [String] { - return strs.compactMap(PrimaryAccessMode.init(rawValue:)) - } else { - return [] - } - } + summary: jsonObject["summary"]?.string, + accessModes: jsonObject["accessMode"]?.decode() ?? [], + accessModesSufficient: (jsonObject["accessModeSufficient"]?.array ?? []) + .map { $0.decode(allowingSingle: true) } .filter { !$0.isEmpty }, - features: parseArray(jsonObject["feature"]).map(Feature.init), - hazards: parseArray(jsonObject["hazard"]).map(Hazard.init), - exemptions: parseArray(jsonObject["exemption"]).map(Exemption.init) + features: jsonObject["feature"]?.decode() ?? [], + hazards: jsonObject["hazard"]?.decode() ?? [], + exemptions: jsonObject["exemption"]?.decode() ?? [] ) } - public var json: [String: Any] { - makeJSON([ - "conformsTo": encodeIfNotEmpty(conformsTo.map(\.uri)), - "certification": encodeIfNotEmpty(certification.map { - makeJSON([ - "certifiedBy": encodeIfNotNil($0.certifiedBy), - "credential": encodeIfNotNil($0.credential), - "report": encodeIfNotNil($0.report), - ]) - }), - "summary": encodeIfNotNil(summary), - "accessMode": encodeIfNotEmpty(accessModes.map(\.id)), - "accessModeSufficient": encodeIfNotEmpty(accessModesSufficient.map { $0.map(\.rawValue) }), - "feature": encodeIfNotEmpty(features.map(\.id)), - "hazard": encodeIfNotEmpty(hazards.map(\.id)), - "exemption": encodeIfNotEmpty(exemptions.map(\.id)), + public var jsonObject: [String: JSONValue] { + .init([ + "conformsTo": conformsTo.map(\.uri).orNullIfEmpty, + "certification": certification, + "summary": summary, + "accessMode": accessModes.map(\.id).orNullIfEmpty, + "accessModeSufficient": accessModesSufficient.map { $0.map(\.rawValue) }.orNullIfEmpty, + "feature": features.map(\.id).orNullIfEmpty, + "hazard": hazards.map(\.id).orNullIfEmpty, + "exemption": exemptions.map(\.id).orNullIfEmpty, ]) } } diff --git a/Sources/Shared/Publication/Accessibility/AccessibilityDisplayString+Generated.swift b/Sources/Shared/Publication/Accessibility/AccessibilityDisplayString+Generated.swift index e4738f066c..b5970ad504 100644 --- a/Sources/Shared/Publication/Accessibility/AccessibilityDisplayString+Generated.swift +++ b/Sources/Shared/Publication/Accessibility/AccessibilityDisplayString+Generated.swift @@ -1,27 +1,33 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // -// DO NOT EDIT. File generated automatically from v2.0.c of the en-US JSON strings. - +/// DO NOT EDIT. File generated automatically from https://github.com/edrlab/thorium-locales/. public extension AccessibilityDisplayString { - static let waysOfReadingTitle: Self = "readium.a11y.ways-of-reading-title" - static let waysOfReadingNonvisualReadingAltText: Self = "readium.a11y.ways-of-reading-nonvisual-reading-alt-text" - static let waysOfReadingNonvisualReadingNoMetadata: Self = "readium.a11y.ways-of-reading-nonvisual-reading-no-metadata" - static let waysOfReadingNonvisualReadingNone: Self = "readium.a11y.ways-of-reading-nonvisual-reading-none" - static let waysOfReadingNonvisualReadingNotFully: Self = "readium.a11y.ways-of-reading-nonvisual-reading-not-fully" - static let waysOfReadingNonvisualReadingReadable: Self = "readium.a11y.ways-of-reading-nonvisual-reading-readable" - static let waysOfReadingPrerecordedAudioComplementary: Self = "readium.a11y.ways-of-reading-prerecorded-audio-complementary" - static let waysOfReadingPrerecordedAudioNoMetadata: Self = "readium.a11y.ways-of-reading-prerecorded-audio-no-metadata" - static let waysOfReadingPrerecordedAudioOnly: Self = "readium.a11y.ways-of-reading-prerecorded-audio-only" - static let waysOfReadingPrerecordedAudioSynchronized: Self = "readium.a11y.ways-of-reading-prerecorded-audio-synchronized" - static let waysOfReadingVisualAdjustmentsModifiable: Self = "readium.a11y.ways-of-reading-visual-adjustments-modifiable" - static let waysOfReadingVisualAdjustmentsUnknown: Self = "readium.a11y.ways-of-reading-visual-adjustments-unknown" - static let waysOfReadingVisualAdjustmentsUnmodifiable: Self = "readium.a11y.ways-of-reading-visual-adjustments-unmodifiable" - static let conformanceTitle: Self = "readium.a11y.conformance-title" - static let conformanceDetailsTitle: Self = "readium.a11y.conformance-details-title" + static let accessibilitySummaryNoMetadata: Self = "readium.a11y.accessibility-summary-no-metadata" + static let accessibilitySummaryPublisherContact: Self = "readium.a11y.accessibility-summary-publisher-contact" + static let accessibilitySummaryTitle: Self = "readium.a11y.accessibility-summary-title" + static let additionalAccessibilityInformationAria: Self = "readium.a11y.additional-accessibility-information-aria" + static let additionalAccessibilityInformationAudioDescriptions: Self = "readium.a11y.additional-accessibility-information-audio-descriptions" + static let additionalAccessibilityInformationBraille: Self = "readium.a11y.additional-accessibility-information-braille" + static let additionalAccessibilityInformationColorNotSoleMeansOfConveyingInformation: Self = "readium.a11y.additional-accessibility-information-color-not-sole-means-of-conveying-information" + static let additionalAccessibilityInformationDyslexiaReadability: Self = "readium.a11y.additional-accessibility-information-dyslexia-readability" + static let additionalAccessibilityInformationFullRubyAnnotations: Self = "readium.a11y.additional-accessibility-information-full-ruby-annotations" + static let additionalAccessibilityInformationHighContrastBetweenForegroundAndBackgroundAudio: Self = "readium.a11y.additional-accessibility-information-high-contrast-between-foreground-and-background-audio" + static let additionalAccessibilityInformationHighContrastBetweenTextAndBackground: Self = "readium.a11y.additional-accessibility-information-high-contrast-between-text-and-background" + static let additionalAccessibilityInformationLargePrint: Self = "readium.a11y.additional-accessibility-information-large-print" + static let additionalAccessibilityInformationPageBreaks: Self = "readium.a11y.additional-accessibility-information-page-breaks" + static let additionalAccessibilityInformationRubyAnnotations: Self = "readium.a11y.additional-accessibility-information-ruby-annotations" + static let additionalAccessibilityInformationSignLanguage: Self = "readium.a11y.additional-accessibility-information-sign-language" + static let additionalAccessibilityInformationTactileGraphics: Self = "readium.a11y.additional-accessibility-information-tactile-graphics" + static let additionalAccessibilityInformationTactileObjects: Self = "readium.a11y.additional-accessibility-information-tactile-objects" + static let additionalAccessibilityInformationTextToSpeechHinting: Self = "readium.a11y.additional-accessibility-information-text-to-speech-hinting" + static let additionalAccessibilityInformationTitle: Self = "readium.a11y.additional-accessibility-information-title" + static let additionalAccessibilityInformationUltraHighContrastBetweenTextAndBackground: Self = "readium.a11y.additional-accessibility-information-ultra-high-contrast-between-text-and-background" + static let additionalAccessibilityInformationVisiblePageNumbering: Self = "readium.a11y.additional-accessibility-information-visible-page-numbering" + static let additionalAccessibilityInformationWithoutBackgroundSounds: Self = "readium.a11y.additional-accessibility-information-without-background-sounds" static let conformanceA: Self = "readium.a11y.conformance-a" static let conformanceAa: Self = "readium.a11y.conformance-aa" static let conformanceAaa: Self = "readium.a11y.conformance-aaa" @@ -38,26 +44,10 @@ public extension AccessibilityDisplayString { static let conformanceDetailsWcag20: Self = "readium.a11y.conformance-details-wcag-2-0" static let conformanceDetailsWcag21: Self = "readium.a11y.conformance-details-wcag-2-1" static let conformanceDetailsWcag22: Self = "readium.a11y.conformance-details-wcag-2-2" + static let conformanceDetailsTitle: Self = "readium.a11y.conformance-details-title" static let conformanceNo: Self = "readium.a11y.conformance-no" + static let conformanceTitle: Self = "readium.a11y.conformance-title" static let conformanceUnknownStandard: Self = "readium.a11y.conformance-unknown-standard" - static let navigationTitle: Self = "readium.a11y.navigation-title" - static let navigationIndex: Self = "readium.a11y.navigation-index" - static let navigationNoMetadata: Self = "readium.a11y.navigation-no-metadata" - static let navigationPageNavigation: Self = "readium.a11y.navigation-page-navigation" - static let navigationStructural: Self = "readium.a11y.navigation-structural" - static let navigationToc: Self = "readium.a11y.navigation-toc" - static let richContentTitle: Self = "readium.a11y.rich-content-title" - static let richContentAccessibleChemistryAsLatex: Self = "readium.a11y.rich-content-accessible-chemistry-as-latex" - static let richContentAccessibleChemistryAsMathml: Self = "readium.a11y.rich-content-accessible-chemistry-as-mathml" - static let richContentAccessibleMathAsLatex: Self = "readium.a11y.rich-content-accessible-math-as-latex" - static let richContentAccessibleMathAsMathml: Self = "readium.a11y.rich-content-accessible-math-as-mathml" - static let richContentAccessibleMathDescribed: Self = "readium.a11y.rich-content-accessible-math-described" - static let richContentClosedCaptions: Self = "readium.a11y.rich-content-closed-captions" - static let richContentExtended: Self = "readium.a11y.rich-content-extended" - static let richContentOpenCaptions: Self = "readium.a11y.rich-content-open-captions" - static let richContentTranscript: Self = "readium.a11y.rich-content-transcript" - static let richContentUnknown: Self = "readium.a11y.rich-content-unknown" - static let hazardsTitle: Self = "readium.a11y.hazards-title" static let hazardsFlashing: Self = "readium.a11y.hazards-flashing" static let hazardsFlashingNone: Self = "readium.a11y.hazards-flashing-none" static let hazardsFlashingUnknown: Self = "readium.a11y.hazards-flashing-unknown" @@ -69,30 +59,39 @@ public extension AccessibilityDisplayString { static let hazardsSound: Self = "readium.a11y.hazards-sound" static let hazardsSoundNone: Self = "readium.a11y.hazards-sound-none" static let hazardsSoundUnknown: Self = "readium.a11y.hazards-sound-unknown" + static let hazardsTitle: Self = "readium.a11y.hazards-title" static let hazardsUnknown: Self = "readium.a11y.hazards-unknown" - static let accessibilitySummaryTitle: Self = "readium.a11y.accessibility-summary-title" - static let accessibilitySummaryNoMetadata: Self = "readium.a11y.accessibility-summary-no-metadata" - static let accessibilitySummaryPublisherContact: Self = "readium.a11y.accessibility-summary-publisher-contact" - static let legalConsiderationsTitle: Self = "readium.a11y.legal-considerations-title" static let legalConsiderationsExempt: Self = "readium.a11y.legal-considerations-exempt" static let legalConsiderationsNoMetadata: Self = "readium.a11y.legal-considerations-no-metadata" - static let additionalAccessibilityInformationTitle: Self = "readium.a11y.additional-accessibility-information-title" - static let additionalAccessibilityInformationAria: Self = "readium.a11y.additional-accessibility-information-aria" - static let additionalAccessibilityInformationAudioDescriptions: Self = "readium.a11y.additional-accessibility-information-audio-descriptions" - static let additionalAccessibilityInformationBraille: Self = "readium.a11y.additional-accessibility-information-braille" - static let additionalAccessibilityInformationColorNotSoleMeansOfConveyingInformation: Self = "readium.a11y.additional-accessibility-information-color-not-sole-means-of-conveying-information" - static let additionalAccessibilityInformationDyslexiaReadability: Self = "readium.a11y.additional-accessibility-information-dyslexia-readability" - static let additionalAccessibilityInformationFullRubyAnnotations: Self = "readium.a11y.additional-accessibility-information-full-ruby-annotations" - static let additionalAccessibilityInformationHighContrastBetweenForegroundAndBackgroundAudio: Self = "readium.a11y.additional-accessibility-information-high-contrast-between-foreground-and-background-audio" - static let additionalAccessibilityInformationHighContrastBetweenTextAndBackground: Self = "readium.a11y.additional-accessibility-information-high-contrast-between-text-and-background" - static let additionalAccessibilityInformationLargePrint: Self = "readium.a11y.additional-accessibility-information-large-print" - static let additionalAccessibilityInformationPageBreaks: Self = "readium.a11y.additional-accessibility-information-page-breaks" - static let additionalAccessibilityInformationRubyAnnotations: Self = "readium.a11y.additional-accessibility-information-ruby-annotations" - static let additionalAccessibilityInformationSignLanguage: Self = "readium.a11y.additional-accessibility-information-sign-language" - static let additionalAccessibilityInformationTactileGraphics: Self = "readium.a11y.additional-accessibility-information-tactile-graphics" - static let additionalAccessibilityInformationTactileObjects: Self = "readium.a11y.additional-accessibility-information-tactile-objects" - static let additionalAccessibilityInformationTextToSpeechHinting: Self = "readium.a11y.additional-accessibility-information-text-to-speech-hinting" - static let additionalAccessibilityInformationUltraHighContrastBetweenTextAndBackground: Self = "readium.a11y.additional-accessibility-information-ultra-high-contrast-between-text-and-background" - static let additionalAccessibilityInformationVisiblePageNumbering: Self = "readium.a11y.additional-accessibility-information-visible-page-numbering" - static let additionalAccessibilityInformationWithoutBackgroundSounds: Self = "readium.a11y.additional-accessibility-information-without-background-sounds" + static let legalConsiderationsTitle: Self = "readium.a11y.legal-considerations-title" + static let navigationIndex: Self = "readium.a11y.navigation-index" + static let navigationNoMetadata: Self = "readium.a11y.navigation-no-metadata" + static let navigationPageNavigation: Self = "readium.a11y.navigation-page-navigation" + static let navigationStructural: Self = "readium.a11y.navigation-structural" + static let navigationTitle: Self = "readium.a11y.navigation-title" + static let navigationToc: Self = "readium.a11y.navigation-toc" + static let richContentAccessibleChemistryAsLatex: Self = "readium.a11y.rich-content-accessible-chemistry-as-latex" + static let richContentAccessibleChemistryAsMathml: Self = "readium.a11y.rich-content-accessible-chemistry-as-mathml" + static let richContentAccessibleMathAsLatex: Self = "readium.a11y.rich-content-accessible-math-as-latex" + static let richContentAccessibleMathDescribed: Self = "readium.a11y.rich-content-accessible-math-described" + static let richContentClosedCaptions: Self = "readium.a11y.rich-content-closed-captions" + static let richContentExtendedDescriptions: Self = "readium.a11y.rich-content-extended-descriptions" + static let richContentMathAsMathml: Self = "readium.a11y.rich-content-math-as-mathml" + static let richContentOpenCaptions: Self = "readium.a11y.rich-content-open-captions" + static let richContentTitle: Self = "readium.a11y.rich-content-title" + static let richContentTranscript: Self = "readium.a11y.rich-content-transcript" + static let richContentUnknown: Self = "readium.a11y.rich-content-unknown" + static let waysOfReadingNonvisualReadingAltText: Self = "readium.a11y.ways-of-reading-nonvisual-reading-alt-text" + static let waysOfReadingNonvisualReadingNoMetadata: Self = "readium.a11y.ways-of-reading-nonvisual-reading-no-metadata" + static let waysOfReadingNonvisualReadingNone: Self = "readium.a11y.ways-of-reading-nonvisual-reading-none" + static let waysOfReadingNonvisualReadingNotFully: Self = "readium.a11y.ways-of-reading-nonvisual-reading-not-fully" + static let waysOfReadingNonvisualReadingReadable: Self = "readium.a11y.ways-of-reading-nonvisual-reading-readable" + static let waysOfReadingPrerecordedAudioComplementary: Self = "readium.a11y.ways-of-reading-prerecorded-audio-complementary" + static let waysOfReadingPrerecordedAudioNoMetadata: Self = "readium.a11y.ways-of-reading-prerecorded-audio-no-metadata" + static let waysOfReadingPrerecordedAudioOnly: Self = "readium.a11y.ways-of-reading-prerecorded-audio-only" + static let waysOfReadingPrerecordedAudioSynchronized: Self = "readium.a11y.ways-of-reading-prerecorded-audio-synchronized" + static let waysOfReadingTitle: Self = "readium.a11y.ways-of-reading-title" + static let waysOfReadingVisualAdjustmentsModifiable: Self = "readium.a11y.ways-of-reading-visual-adjustments-modifiable" + static let waysOfReadingVisualAdjustmentsUnknown: Self = "readium.a11y.ways-of-reading-visual-adjustments-unknown" + static let waysOfReadingVisualAdjustmentsUnmodifiable: Self = "readium.a11y.ways-of-reading-visual-adjustments-unmodifiable" } diff --git a/Sources/Shared/Publication/Accessibility/AccessibilityMetadataDisplayGuide.swift b/Sources/Shared/Publication/Accessibility/AccessibilityMetadataDisplayGuide.swift index aeb73d3036..44708469be 100644 --- a/Sources/Shared/Publication/Accessibility/AccessibilityMetadataDisplayGuide.swift +++ b/Sources/Shared/Publication/Accessibility/AccessibilityMetadataDisplayGuide.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -110,7 +110,9 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { public let id: AccessibilityDisplayString = .waysOfReadingTitle - public var localizedTitle: String { id.localized } + public var localizedTitle: String { + id.localized + } /// "Ways of reading" should be rendered even if there is no metadata. public let shouldDisplay: Bool = true @@ -267,7 +269,9 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { public struct Navigation: AccessibilityDisplayField { /// Indicates whether no information about navigation features is /// available. - public var noMetadata: Bool { !tableOfContents && !index && !headings && !page } + public var noMetadata: Bool { + !tableOfContents && !index && !headings && !page + } /// Table of contents to all chapters of the text via links. public var tableOfContents: Bool @@ -283,9 +287,13 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { public let id: AccessibilityDisplayString = .navigationTitle - public var localizedTitle: String { id.localized } + public var localizedTitle: String { + id.localized + } - public var shouldDisplay: Bool { !noMetadata } + public var shouldDisplay: Bool { + !noMetadata + } public var statements: [AccessibilityDisplayStatement] { Array { @@ -377,20 +385,24 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { public let id: AccessibilityDisplayString = .richContentTitle - public var localizedTitle: String { id.localized } + public var localizedTitle: String { + id.localized + } - public var shouldDisplay: Bool { !noMetadata } + public var shouldDisplay: Bool { + !noMetadata + } public var statements: [AccessibilityDisplayStatement] { Array { if extendedAltTextDescriptions { - $0.append(.richContentExtended) + $0.append(.richContentExtendedDescriptions) } if mathFormula { $0.append(.richContentAccessibleMathDescribed) } if mathFormulaAsMathML { - $0.append(.richContentAccessibleMathAsMathml) + $0.append(.richContentMathAsMathml) } if mathFormulaAsLaTeX { $0.append(.richContentAccessibleMathAsLatex) @@ -507,9 +519,13 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { public let id: AccessibilityDisplayString = .additionalAccessibilityInformationTitle - public var localizedTitle: String { id.localized } + public var localizedTitle: String { + id.localized + } - public var shouldDisplay: Bool { !noMetadata } + public var shouldDisplay: Bool { + !noMetadata + } public var statements: [AccessibilityDisplayStatement] { Array { @@ -649,9 +665,13 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { public let id: AccessibilityDisplayString = .hazardsTitle - public var localizedTitle: String { id.localized } + public var localizedTitle: String { + id.localized + } - public var shouldDisplay: Bool { !noMetadata } + public var shouldDisplay: Bool { + !noMetadata + } public var statements: [AccessibilityDisplayStatement] { Array { @@ -766,7 +786,9 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { public let id: AccessibilityDisplayString = .conformanceTitle - public var localizedTitle: String { id.localized } + public var localizedTitle: String { + id.localized + } /// "Conformance" should be rendered even if there is no metadata. public let shouldDisplay: Bool = true @@ -818,7 +840,9 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { /// https://w3c.github.io/publ-a11y/a11y-meta-display-guide/2.0/guidelines/#legal-considerations public struct Legal: AccessibilityDisplayField { /// No information is available. - public var noMetadata: Bool { !exemption } + public var noMetadata: Bool { + !exemption + } /// This publication claims an accessibility exemption in some /// jurisdictions. @@ -826,9 +850,13 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { public let id: AccessibilityDisplayString = .legalConsiderationsTitle - public var localizedTitle: String { id.localized } + public var localizedTitle: String { + id.localized + } - public var shouldDisplay: Bool { !noMetadata } + public var shouldDisplay: Bool { + !noMetadata + } public var statements: [AccessibilityDisplayStatement] { Array { @@ -865,9 +893,13 @@ public struct AccessibilityMetadataDisplayGuide: Sendable, Equatable { public let id: AccessibilityDisplayString = .accessibilitySummaryTitle - public var localizedTitle: String { id.localized } + public var localizedTitle: String { + id.localized + } - public var shouldDisplay: Bool { summary != nil } + public var shouldDisplay: Bool { + summary != nil + } public var statements: [AccessibilityDisplayStatement] { Array { @@ -930,7 +962,7 @@ public struct AccessibilityDisplayStatement: Sendable, Equatable, Identifiable { /// family and font size, spaces between paragraphs, sentences, words, and /// letters, as well as color of background and text) /// - /// Some statements contain HTTP links; so we use an ``NSAttributedString``. + /// Some statements contain HTTP links; so we use an `NSAttributedString`. /// /// - Parameter descriptive: When true, will return the long descriptive /// statement. @@ -966,7 +998,7 @@ public struct AccessibilityDisplayStatement: Sendable, Equatable, Identifiable { /// /// See https://w3c.github.io/publ-a11y/a11y-meta-display-guide/2.0/draft/localizations/ public struct AccessibilityDisplayString: RawRepresentable, ExpressibleByStringLiteral, Sendable, Hashable { - // Special key for the provided summary, which is not localized. + /// Special key for the provided summary, which is not localized. static let accessibilitySummary: Self = "readium.a11y.accessibility-summary" public let rawValue: String @@ -997,30 +1029,38 @@ public struct AccessibilityDisplayString: RawRepresentable, ExpressibleByStringL /// - Parameter descriptive: When true, will return the long descriptive /// statement. public func localized(descriptive: Bool) -> NSAttributedString { - NSAttributedString(string: bundleString("\(rawValue)-\(descriptive ? "descriptive" : "compact")") - .trimmingCharacters(in: .whitespacesAndNewlines) - ) + // Try the suffixed key first, then fall back to the unsuffixed key + // for strings where compact and descriptive variants are identical. + let suffixedKey = "\(rawValue)-\(descriptive ? "descriptive" : "compact")" + var string = bundleString(suffixedKey) + if string == suffixedKey { + string = bundleString(rawValue) + } + return NSAttributedString(string: string.trimmingCharacters(in: .whitespacesAndNewlines)) } private func bundleString(_ key: String, _ values: CVarArg...) -> String { - bundleString(key, in: Bundle.module, table: "W3CAccessibilityMetadataDisplayGuide", values) - } - - /// Returns the localized string in the main bundle, or fallback on the given - /// bundle if not found. - private func bundleString(_ key: String, in bundle: Bundle, table: String? = nil, _ values: [CVarArg]) -> String { - let defaultValue = bundle.localizedString(forKey: key, value: nil, table: table) - var string = Bundle.main.localizedString(forKey: key, value: defaultValue, table: nil) - if !values.isEmpty { - string = String(format: string, locale: .current, arguments: values) - } - return string + ReadiumLocalizedString(key, in: Bundle.module, table: "W3CAccessibilityMetadataDisplayGuide", values) } } -// Syntactic sugar +/// Syntactic sugar private extension Array where Element == AccessibilityDisplayStatement { mutating func append(_ string: AccessibilityDisplayString) { append(AccessibilityDisplayStatement(string: string)) } } + +// MARK: - Deprecated Aliases + +public extension AccessibilityDisplayString { + @available(*, deprecated, renamed: "richContentExtendedDescriptions") + static var richContentExtended: Self { + richContentExtendedDescriptions + } + + @available(*, deprecated, renamed: "richContentMathAsMathml") + static var richContentAccessibleMathAsMathml: Self { + richContentMathAsMathml + } +} diff --git a/Sources/Shared/Publication/Contributor.swift b/Sources/Shared/Publication/Contributor.swift index 46f52c7de7..d5a0eaef6f 100644 --- a/Sources/Shared/Publication/Contributor.swift +++ b/Sources/Shared/Publication/Contributor.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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,11 +7,13 @@ import Foundation import ReadiumInternal -/// https://readium.org/webpub-manifest/schema/contributor-object.schema.json -public struct Contributor: Hashable, Sendable { +/// https://readium.org/webpub-manifest/schema/contributor.schema.json +public struct Contributor: Hashable, Sendable, JSONValueDecodable, JSONObjectEncodable { /// The name of the contributor. public var localizedName: LocalizedString - public var name: String { localizedName.string } + public var name: String { + localizedName.string + } /// An unambiguous reference to this contributor. public var identifier: String? @@ -28,7 +30,15 @@ public struct Contributor: Hashable, Sendable { /// Used to retrieve similar publications for the given contributor. public var links: [Link] - public init(name: LocalizedStringConvertible, identifier: String? = nil, sortAs: String? = nil, roles: [String] = [], role: String? = nil, position: Double? = nil, links: [Link] = []) { + public init( + name: LocalizedStringConvertible, + identifier: String? = nil, + sortAs: String? = nil, + roles: [String] = [], + role: String? = nil, + position: Double? = nil, + links: [Link] = [] + ) { // convenience to set a single role during construction var roles = roles if let role = role { @@ -43,56 +53,40 @@ public struct Contributor: Hashable, Sendable { self.links = links } - public init?(json: Any, warnings: WarningLogger? = nil) throws { - if let name = json as? String { - self.init(name: name) + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue else { + return nil + } - } else if let json = json as? [String: Any], let name = try? LocalizedString(json: json["name"], warnings: warnings) { + if let name = json.string { + self.init(name: name) + } else if let dict = json.object { + guard let name: LocalizedString = try? dict["name"]?.decode(warnings: warnings) else { + warnings?.log("Invalid Contributor object", model: Self.self, source: json, severity: .moderate) + throw JSONError.parsing(Self.self) + } self.init( name: name, - identifier: json["identifier"] as? String, - sortAs: json["sortAs"] as? String, - roles: parseArray(json["role"], allowingSingle: true), - position: parseDouble(json["position"]), - links: .init(json: json["links"], warnings: warnings) + identifier: dict["identifier"]?.string, + sortAs: dict["sortAs"]?.string, + roles: dict["role"]?.decode(allowingSingle: true) ?? [], + position: dict["position"]?.double, + links: dict["links"]?.decode(warnings: warnings) ?? [] ) - } else { warnings?.log("Invalid Contributor object", model: Self.self, source: json, severity: .moderate) throw JSONError.parsing(Self.self) } } - public var json: [String: Any] { - makeJSON([ - "name": localizedName.json, - "identifier": encodeIfNotNil(identifier), - "sortAs": encodeIfNotNil(sortAs), - "role": encodeIfNotEmpty(roles), - "position": encodeIfNotNil(position), - "links": encodeIfNotEmpty(links.json), + public var jsonObject: [String: JSONValue] { + .init([ + "name": localizedName, + "identifier": identifier, + "sortAs": sortAs, + "role": roles.orNullIfEmpty, + "position": position, + "links": links.orNullIfEmpty, ]) } } - -public extension Array where Element == Contributor { - /// Parses multiple JSON contributors into an array of Contributors. - /// eg. let authors = [Contributor](json: ["Apple", "Pear"]) - init(json: Any?, warnings: WarningLogger? = nil) { - self.init() - guard let json = json else { - return - } - - if let json = json as? [Any] { - let contributors = json.compactMap { try? Contributor(json: $0, warnings: warnings) } - append(contentsOf: contributors) - } else if let contributor = try? Contributor(json: json, warnings: warnings) { - append(contributor) - } - } - - var json: [[String: Any]] { - map(\.json) - } -} diff --git a/Sources/Shared/Publication/Extensions/Archive/Properties+Archive.swift b/Sources/Shared/Publication/Extensions/Archive/Properties+Archive.swift index b530040f73..500d35b1e3 100644 --- a/Sources/Shared/Publication/Extensions/Archive/Properties+Archive.swift +++ b/Sources/Shared/Publication/Extensions/Archive/Properties+Archive.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,7 +9,7 @@ import Foundation /// Archive Link Properties Extension public extension Properties { /// Holds information about how the resource is stored in the publication archive. - struct Archive: Equatable { + struct Archive: Equatable, JSONValueDecodable, JSONObjectEncodable { /// The length of the entry stored in the archive. It might be a compressed length if the entry is deflated. public let entryLength: UInt64 /// Indicates whether the entry was compressed before being stored in the archive. @@ -20,15 +20,14 @@ public extension Properties { self.isEntryCompressed = isEntryCompressed } - public init?(json: Any?, warnings: WarningLogger? = nil) throws { - if json == nil { + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue else { return nil } guard - let jsonObject = json as? [String: Any], - let length: UInt64 = (jsonObject["entryLength"] as? NSNumber)?.uint64Value, - length >= 0, - let isCompressed = jsonObject["isEntryCompressed"] as? Bool + let jsonObject = json.object, + let length: UInt64 = jsonObject["entryLength"]?.nonNegative(), + let isCompressed = jsonObject["isEntryCompressed"]?.bool else { warnings?.log("`entryLength` and `isEntryCompressed` are required", model: Self.self, source: json) throw JSONError.parsing(Self.self) @@ -40,16 +39,16 @@ public extension Properties { ) } - public var json: [String: Any] { - [ - "entryLength": entryLength as NSNumber, + public var jsonObject: [String: JSONValue] { + .init([ + "entryLength": entryLength, "isEntryCompressed": isEntryCompressed, - ] + ]) } } /// Provides information about how the resource is stored in the publication archive. var archive: Archive? { - try? Archive(json: otherProperties["https://readium.org/webpub-manifest/properties#archive"], warnings: self) + try? otherProperties["https://readium.org/webpub-manifest/properties#archive"]?.decode(warnings: self) } } diff --git a/Sources/Shared/Publication/Extensions/Audio/Locator+Audio.swift b/Sources/Shared/Publication/Extensions/Audio/Locator+Audio.swift index 4aafa53a6a..4d9fb60002 100644 --- a/Sources/Shared/Publication/Extensions/Audio/Locator+Audio.swift +++ b/Sources/Shared/Publication/Extensions/Audio/Locator+Audio.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Shared/Publication/Extensions/EPUB/EPUBLayout.swift b/Sources/Shared/Publication/Extensions/EPUB/EPUBLayout.swift index 62963d391e..3b9a8b9940 100644 --- a/Sources/Shared/Publication/Extensions/EPUB/EPUBLayout.swift +++ b/Sources/Shared/Publication/Extensions/EPUB/EPUBLayout.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Shared/Publication/Extensions/EPUB/EPUBMediaOverlay.swift b/Sources/Shared/Publication/Extensions/EPUB/EPUBMediaOverlay.swift new file mode 100644 index 0000000000..c496d4a30c --- /dev/null +++ b/Sources/Shared/Publication/Extensions/EPUB/EPUBMediaOverlay.swift @@ -0,0 +1,41 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumInternal + +/// EPUB Media Overlay metadata. +/// https://readium.org/webpub-manifest/profiles/epub#5-metadata +public struct EPUBMediaOverlay: Equatable, Sendable, JSONValueDecodable, JSONObjectEncodable { + /// Author-defined CSS class name to apply to the currently-playing EPUB + /// Content Document element. + public var activeClass: String? + + /// Author-defined CSS class name to apply to the EPUB Content Document's + /// document element when playback is active. + public var playbackActiveClass: String? + + public init(activeClass: String? = nil, playbackActiveClass: String? = nil) { + self.activeClass = activeClass + self.playbackActiveClass = playbackActiveClass + } + + public init?(json: T?, warnings: WarningLogger?) throws { + guard let jsonObject = json?.jsonValue.object else { return nil } + + activeClass = jsonObject["activeClass"]?.string + playbackActiveClass = jsonObject["playbackActiveClass"]?.string + + guard activeClass != nil || playbackActiveClass != nil else { return nil } + } + + public var jsonObject: [String: JSONValue] { + .init([ + "activeClass": activeClass, + "playbackActiveClass": playbackActiveClass, + ]) + } +} diff --git a/Sources/Shared/Publication/Extensions/EPUB/Metadata+EPUB.swift b/Sources/Shared/Publication/Extensions/EPUB/Metadata+EPUB.swift new file mode 100644 index 0000000000..79c2d79310 --- /dev/null +++ b/Sources/Shared/Publication/Extensions/EPUB/Metadata+EPUB.swift @@ -0,0 +1,16 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation + +private let mediaOverlayKey = "mediaOverlay" + +public extension Metadata { + /// Media overlay CSS class names for this publication. + var mediaOverlay: EPUBMediaOverlay? { + try? otherMetadata[mediaOverlayKey]?.decode() + } +} diff --git a/Sources/Shared/Publication/Extensions/EPUB/Properties+EPUB.swift b/Sources/Shared/Publication/Extensions/EPUB/Properties+EPUB.swift index 501ce61225..134a64e169 100644 --- a/Sources/Shared/Publication/Extensions/EPUB/Properties+EPUB.swift +++ b/Sources/Shared/Publication/Extensions/EPUB/Properties+EPUB.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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,24 +7,12 @@ import Foundation import ReadiumInternal -private let containsKey = "contains" -private let layoutKey = "layout" -private let overflowKey = "overflow" -private let spreadKey = "spread" -private let encryptedKey = "encrypted" - /// EPUB Link Properties Extension /// https://readium.org/webpub-manifest/schema/extensions/epub/properties.schema.json public extension Properties { /// Identifies content contained in the linked resource, that cannot be strictly identified /// using a media type. var contains: [String] { - parseArray(otherProperties["contains"]) - } - - /// Hint about the nature of the layout for the linked resources. - @available(*, unavailable, message: "This was removed from RWPM. You can still use the EPUB extensibility to access the original value.") - var layout: EPUBLayout? { - parseRaw(otherProperties["layout"]) + otherProperties["contains"]?.decode() ?? [] } } diff --git a/Sources/Shared/Publication/Extensions/EPUB/Publication+EPUB.swift b/Sources/Shared/Publication/Extensions/EPUB/Publication+EPUB.swift index cf7e59abd5..5a0123970e 100644 --- a/Sources/Shared/Publication/Extensions/EPUB/Publication+EPUB.swift +++ b/Sources/Shared/Publication/Extensions/EPUB/Publication+EPUB.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Shared/Publication/Extensions/Encryption/Encryption.swift b/Sources/Shared/Publication/Extensions/Encryption/Encryption.swift index e4c9815ebe..eb6e1ccec5 100644 --- a/Sources/Shared/Publication/Extensions/Encryption/Encryption.swift +++ b/Sources/Shared/Publication/Extensions/Encryption/Encryption.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -9,7 +9,7 @@ import ReadiumInternal /// Indicates that a resource is encrypted/obfuscated and provides relevant information for /// decryption. -public struct Encryption: Equatable { +public struct Encryption: Equatable, JSONValueDecodable, JSONObjectEncodable { /// Identifies the algorithm used to encrypt the resource. public let algorithm: String // URI @@ -33,13 +33,13 @@ public struct Encryption: Equatable { self.scheme = scheme } - public init?(json: Any?, warnings: WarningLogger? = nil) throws { + public init?(json: T?, warnings: WarningLogger?) throws { // Convenience when parsing parent structures. - if json == nil { + guard let json = json?.jsonValue else { return nil } - guard let jsonObject = json as? [String: Any], - let algorithm = jsonObject["algorithm"] as? String + guard let jsonObject = json.object, + let algorithm = jsonObject["algorithm"]?.string else { warnings?.log("`algorithm` is required", model: Self.self, source: json) throw JSONError.parsing(Self.self) @@ -47,23 +47,23 @@ public struct Encryption: Equatable { self.init( algorithm: algorithm, - compression: jsonObject["compression"] as? String, - originalLength: jsonObject["originalLength"] as? Int + compression: jsonObject["compression"]?.string, + originalLength: jsonObject["originalLength"]?.integer // Fallback on `original-length` for legacy reasons // See https://github.com/readium/webpub-manifest/pull/43 - ?? jsonObject["original-length"] as? Int, - profile: jsonObject["profile"] as? String, - scheme: jsonObject["scheme"] as? String + ?? jsonObject["original-length"]?.integer, + profile: jsonObject["profile"]?.string, + scheme: jsonObject["scheme"]?.string ) } - public var json: [String: Any] { - makeJSON([ + public var jsonObject: [String: JSONValue] { + .init([ "algorithm": algorithm, - "compression": encodeIfNotNil(compression), - "originalLength": encodeIfNotNil(originalLength), - "profile": encodeIfNotNil(profile), - "scheme": encodeIfNotNil(scheme), + "compression": compression, + "originalLength": originalLength, + "profile": profile, + "scheme": scheme, ]) } } diff --git a/Sources/Shared/Publication/Extensions/Encryption/Properties+Encryption.swift b/Sources/Shared/Publication/Extensions/Encryption/Properties+Encryption.swift index 7184f412df..fc92496f8d 100644 --- a/Sources/Shared/Publication/Extensions/Encryption/Properties+Encryption.swift +++ b/Sources/Shared/Publication/Extensions/Encryption/Properties+Encryption.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -12,6 +12,6 @@ public extension Properties { /// Indicates that a resource is encrypted/obfuscated and provides relevant information for /// decryption. var encryption: Encryption? { - try? Encryption(json: otherProperties["encrypted"], warnings: self) + try? otherProperties["encrypted"]?.decode(warnings: self) } } diff --git a/Sources/Shared/Publication/Extensions/HTML/DOMRange.swift b/Sources/Shared/Publication/Extensions/HTML/DOMRange.swift index d82f309b58..c586b0e46f 100644 --- a/Sources/Shared/Publication/Extensions/HTML/DOMRange.swift +++ b/Sources/Shared/Publication/Extensions/HTML/DOMRange.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -22,7 +22,7 @@ import ReadiumInternal /// represents a "collapsed" range that has identical `start` and `end` boundary points. /// /// https://github.com/readium/architecture/blob/master/models/locators/extensions/html.md#the-domrange-object -public struct DOMRange: JSONEquatable { +public struct DOMRange: Hashable, JSONValueDecodable, JSONObjectEncodable { /// A serializable representation of the "start" boundary point of the DOM Range. let start: Point @@ -34,24 +34,27 @@ public struct DOMRange: JSONEquatable { self.end = end } - public init?(json: Any?, warnings: WarningLogger? = nil) throws { + public init?(json: T?, warnings: WarningLogger?) throws { // Convenience when parsing parent structures. - if json == nil { + guard let json = json?.jsonValue else { return nil } - guard let jsonObject = json as? [String: Any], - let start = try? Point(json: jsonObject["start"], warnings: warnings) + guard let jsonObject = json.object, + let start: Point = try? jsonObject["start"]?.decode(warnings: warnings) else { warnings?.log("`start` is required", model: Self.self, source: json, severity: .moderate) throw JSONError.parsing(Self.self) } - self.init(start: start, end: try? Point(json: jsonObject["end"], warnings: warnings)) + self.init( + start: start, + end: try? jsonObject["end"]?.decode(warnings: warnings) + ) } - public var json: [String: Any] { - makeJSON([ - "start": encodeIfNotEmpty(start.json), - "end": encodeIfNotEmpty(end?.json), + public var jsonObject: [String: JSONValue] { + .init([ + "start": start.orNullIfEmpty, + "end": end?.orNullIfEmpty, ]) } @@ -68,7 +71,7 @@ public struct DOMRange: JSONEquatable { /// node). /// /// https://github.com/readium/architecture/blob/master/models/locators/extensions/html.md#the-start-and-end-object - public struct Point: JSONEquatable { + public struct Point: Hashable, JSONValueDecodable, JSONObjectEncodable { let cssSelector: String let textNodeIndex: Int let charOffset: Int? @@ -79,14 +82,14 @@ public struct DOMRange: JSONEquatable { self.charOffset = charOffset } - public init?(json: Any?, warnings: WarningLogger? = nil) throws { + public init?(json: T?, warnings: WarningLogger?) throws { // Convenience when parsing parent structures. - if json == nil { + guard let json = json?.jsonValue else { return nil } - guard let jsonObject = json as? [String: Any], - let cssSelector = jsonObject["cssSelector"] as? String, - let textNodeIndex: Int = parsePositive(jsonObject["textNodeIndex"]) + guard let jsonObject = json.object, + let cssSelector = jsonObject["cssSelector"]?.string, + let textNodeIndex: Int = jsonObject["textNodeIndex"]?.nonNegative() else { warnings?.log("`cssSelector` and `textNodeIndex` are required", model: Self.self, source: json, severity: .moderate) throw JSONError.parsing(Self.self) @@ -94,18 +97,18 @@ public struct DOMRange: JSONEquatable { self.init( cssSelector: cssSelector, textNodeIndex: textNodeIndex, - charOffset: parsePositive(jsonObject["charOffset"]) + charOffset: jsonObject["charOffset"]?.nonNegative() // The model was using `offset` before, so we still parse it to ensure backward-compatibility for // reading apps having persisted legacy Locator models. - ?? parsePositive(jsonObject["offset"]) + ?? jsonObject["offset"]?.nonNegative() ) } - public var json: [String: Any] { - makeJSON([ + public var jsonObject: [String: JSONValue] { + .init([ "cssSelector": cssSelector, "textNodeIndex": textNodeIndex, - "charOffset": encodeIfNotNil(charOffset), + "charOffset": charOffset, ]) } } diff --git a/Sources/Shared/Publication/Extensions/HTML/Locator+HTML.swift b/Sources/Shared/Publication/Extensions/HTML/Locator+HTML.swift index 61be92254a..b0878f2a88 100644 --- a/Sources/Shared/Publication/Extensions/HTML/Locator+HTML.swift +++ b/Sources/Shared/Publication/Extensions/HTML/Locator+HTML.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -11,7 +11,7 @@ import Foundation public extension Locator.Locations { /// A CSS Selector. var cssSelector: String? { - otherLocations["cssSelector"] as? String + otherLocations["cssSelector"]?.string } /// `partialCFI` is an expression conforming to the "right-hand" side of the EPUB CFI syntax, @@ -20,11 +20,11 @@ public extension Locator.Locations { /// that the wrapping `epubcfi(***)` syntax is not used for the `partialCFI` string, i.e. /// the "fragment" part of the CFI grammar is ignored. var partialCFI: String? { - otherLocations["partialCfi"] as? String + otherLocations["partialCfi"]?.string } /// An HTML DOM range. var domRange: DOMRange? { - try? DOMRange(json: otherLocations["domRange"], warnings: self) + try? otherLocations["domRange"]?.decode(warnings: self) } } diff --git a/Sources/Shared/Publication/Extensions/OPDS/Properties+OPDS.swift b/Sources/Shared/Publication/Extensions/OPDS/Properties+OPDS.swift index f8f25e21e5..8611e7676f 100644 --- a/Sources/Shared/Publication/Extensions/OPDS/Properties+OPDS.swift +++ b/Sources/Shared/Publication/Extensions/OPDS/Properties+OPDS.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -12,39 +12,39 @@ import ReadiumInternal public extension Properties { /// Provides a hint about the expected number of items returned. var numberOfItems: Int? { - parsePositive(otherProperties["numberOfItems"]) + otherProperties["numberOfItems"]?.nonNegative() } /// The price of a publication is tied to its acquisition link. var price: OPDSPrice? { - try? OPDSPrice(json: otherProperties["price"], warnings: self) + try? otherProperties["price"]?.decode(warnings: self) } /// Indirect acquisition provides a hint for the expected media type that will be acquired after /// additional steps. var indirectAcquisitions: [OPDSAcquisition] { - [OPDSAcquisition](json: otherProperties["indirectAcquisition"], warnings: self) + otherProperties["indirectAcquisition"]?.decode(warnings: self) ?? [] } /// Library-specific features when a specific book is unavailable but provides a hold list. var holds: OPDSHolds? { - try? OPDSHolds(json: otherProperties["holds"], warnings: self) + try? otherProperties["holds"]?.decode(warnings: self) } /// Library-specific feature that contains information about the copies that a library has /// acquired. var copies: OPDSCopies? { - try? OPDSCopies(json: otherProperties["copies"], warnings: self) + try? otherProperties["copies"]?.decode(warnings: self) } /// Indicated the availability of a given resource. var availability: OPDSAvailability? { - try? OPDSAvailability(json: otherProperties["availability"], warnings: self) + try? otherProperties["availability"]?.decode(warnings: self) } /// Indicates that the linked resource supports authentication with the associated Authentication Document. /// See https://drafts.opds.io/authentication-for-opds-1.0.html var authenticate: Link? { - otherProperties["authenticate"].flatMap { try? Link(json: $0) } + try? otherProperties["authenticate"]?.decode(warnings: self) } } diff --git a/Sources/Shared/Publication/Extensions/OPDS/Publication+OPDS.swift b/Sources/Shared/Publication/Extensions/OPDS/Publication+OPDS.swift index c3a8fdc60f..3018cf6966 100644 --- a/Sources/Shared/Publication/Extensions/OPDS/Publication+OPDS.swift +++ b/Sources/Shared/Publication/Extensions/OPDS/Publication+OPDS.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Shared/Publication/Extensions/Presentation/Metadata+Presentation.swift b/Sources/Shared/Publication/Extensions/Presentation/Metadata+Presentation.swift deleted file mode 100644 index 843c561d1c..0000000000 --- a/Sources/Shared/Publication/Extensions/Presentation/Metadata+Presentation.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Copyright 2025 Readium Foundation. All 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 Foundation - -/// Presentation extensions for `Metadata`. -public extension Metadata { - @available(*, unavailable, message: "This was removed from RWPM. You can still use the EPUB extensibility to access the original values.") - var presentation: Presentation { fatalError() } -} diff --git a/Sources/Shared/Publication/Extensions/Presentation/Presentation.swift b/Sources/Shared/Publication/Extensions/Presentation/Presentation.swift deleted file mode 100644 index c39e6f9bd7..0000000000 --- a/Sources/Shared/Publication/Extensions/Presentation/Presentation.swift +++ /dev/null @@ -1,135 +0,0 @@ -// -// Copyright 2025 Readium Foundation. All 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 Foundation -import ReadiumInternal - -/// The Presentation Hints extension defines a number of hints for User Agents about the way content -/// should be presented to the user. -/// -/// https://readium.org/webpub-manifest/extensions/presentation.html -/// 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, -/// `Presentation.defaultX` and `Presentation.X.default` can be used. -@available(*, unavailable, message: "This was removed from RWPM. You can still use the EPUB extensibility to access the original values.") -public struct Presentation: Equatable { - /// Specifies whether or not the parts of a linked resource that flow out of the viewport are - /// clipped. - public let clipped: Bool? - - /// continuous Indicates how the progression between resources from the [readingOrder] should be - /// handled. - public let continuous: Bool? - - /// Suggested method for constraining a resource inside the viewport. - public let fit: Fit? - - /// Suggested orientation for the device when displaying the linked resource. - public let orientation: Orientation? - - /// Indicates if the overflow of linked resources from the `readingOrder` or `resources` should - /// be handled using dynamic pagination or scrolling. - public let overflow: Overflow? - - /// Indicates the condition to be met for the linked resource to be rendered within a synthetic - /// spread. - public let spread: Spread? - - /// Hint about the nature of the layout for the linked resources (EPUB extension). - public let layout: EPUBLayout? - - public init(clipped: Bool? = nil, continuous: Bool? = nil, fit: Fit? = nil, orientation: Orientation? = nil, overflow: Overflow? = nil, spread: Spread? = nil, layout: EPUBLayout? = nil) { - self.clipped = clipped - self.continuous = continuous - self.fit = fit - self.orientation = orientation - self.overflow = overflow - self.spread = spread - self.layout = layout - } - - public init(json: Any?, warnings: WarningLogger? = nil) throws { - guard json != nil else { - self.init() - return - } - guard let jsonObject = json as? [String: Any] else { - warnings?.log("Invalid JSON object", model: Self.self, source: json) - throw JSONError.parsing(Self.self) - } - - self.init( - clipped: jsonObject["clipped"] as? Bool, - continuous: jsonObject["continuous"] as? Bool, - fit: parseRaw(jsonObject["fit"]), - orientation: parseRaw(jsonObject["orientation"]), - overflow: parseRaw(jsonObject["overflow"]), - spread: parseRaw(jsonObject["spread"]), - layout: parseRaw(jsonObject["layout"]) - ) - } - - public var json: [String: Any] { - makeJSON([ - "clipped": encodeIfNotNil(clipped), - "continuous": encodeIfNotNil(continuous), - "fit": encodeRawIfNotNil(fit), - "orientation": encodeRawIfNotNil(orientation), - "overflow": encodeRawIfNotNil(overflow), - "spread": encodeRawIfNotNil(spread), - "layout": encodeRawIfNotNil(layout), - ]) - } - - /// Suggested method for constraining a resource inside the viewport. - public enum Fit: String { - /// The content is centered and scaled to fit both dimensions into the viewport. - case contain - /// The content is centered and scaled to fill the viewport. - case cover - /// The content is centered and scaled to fit the viewport width. - case width - /// The content is centered and scaled to fit the viewport height. - case height - } - - /// Suggested orientation for the device when displaying the linked resource. - public enum Orientation: String { - case landscape, portrait, auto - } - - /// Indicates if the overflow of linked resources from the `readingOrder` or `resources` should - /// be handled using dynamic pagination or scrolling. - public enum Overflow: String { - /// Content overflow should be handled using dynamic pagination. - case paginated - /// Content overflow should be handled using scrolling. - case scrolled - /// The User Agent can decide how overflow should be handled. - case auto - } - - /// Indicates how the linked resource should be displayed in a reading environment that - /// displays synthetic spreads. - public enum Page: String { - case left, right, center - } - - /// Indicates the condition to be met for the linked resource to be rendered within a synthetic - /// spread. - public enum Spread: String { - /// The resource should be displayed in a spread only if the device is in landscape mode. - case landscape - /// The resource should be displayed in a spread whatever the device orientation is. - case both - /// The resource should never be displayed in a spread. - case none - /// The resource is left to the User Agent. - case auto - } -} diff --git a/Sources/Shared/Publication/Extensions/Presentation/Properties+Presentation.swift b/Sources/Shared/Publication/Extensions/Presentation/Properties+Presentation.swift deleted file mode 100644 index 2c5d6dd507..0000000000 --- a/Sources/Shared/Publication/Extensions/Presentation/Properties+Presentation.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Copyright 2025 Readium Foundation. All 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 Foundation -import ReadiumInternal - -/// Presentation extensions for link `Properties`. -public extension Properties { - /// Specifies whether or not the parts of a linked resource that flow out of the viewport are - /// clipped. - @available(*, unavailable, message: "This was removed from RWPM.") - var clipped: Bool? { - otherProperties["clipped"] as? Bool - } - - /// Suggested method for constraining a resource inside the viewport. - @available(*, unavailable, message: "This was removed from RWPM.") - var fit: Presentation.Fit? { - parseRaw(otherProperties["fit"]) - } - - /// Suggested orientation for the device when displaying the linked resource. - @available(*, unavailable, message: "This was removed from RWPM. You can still use the EPUB extensibility to access the original value.") - var orientation: Presentation.Orientation? { - parseRaw(otherProperties["orientation"]) - } - - /// Indicates if the overflow of linked resources from the `readingOrder` or `resources` should - /// be handled using dynamic pagination or scrolling. - @available(*, unavailable, message: "This was removed from RWPM. You can still use the EPUB extensibility to access the original value.") - var overflow: Presentation.Overflow? { - parseRaw(otherProperties["overflow"]) - } - - /// Indicates the condition to be met for the linked resource to be rendered within a synthetic - /// spread. - @available(*, unavailable, message: "This was removed from RWPM. You can still use the EPUB extensibility to access the original value.") - var spread: Presentation.Spread? { - parseRaw(otherProperties["spread"]) - } -} diff --git a/Sources/Shared/Publication/GuidedNavigation/GuidedNavigationDocument.swift b/Sources/Shared/Publication/GuidedNavigation/GuidedNavigationDocument.swift new file mode 100644 index 0000000000..39cd0af098 --- /dev/null +++ b/Sources/Shared/Publication/GuidedNavigation/GuidedNavigationDocument.swift @@ -0,0 +1,40 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumInternal + +/// Represents a Guided Navigation Document, as defined in the +/// Readium Guided Navigation specification. +/// +/// https://readium.org/guided-navigation/ +public struct GuidedNavigationDocument: Hashable, Sendable, JSONValueDecodable { + /// A sequence of resources and/or media fragments into these resources, + /// meant to be presented sequentially to the user. + public var guided: [GuidedNavigationObject] + + public init(guided: [GuidedNavigationObject]) { + self.guided = guided + } + + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue else { + return nil + } + guard let jsonObject = json.object else { + warnings?.log("Invalid Guided Navigation Document", model: Self.self, source: json, severity: .moderate) + throw JSONError.parsing(Self.self) + } + + let guided: [GuidedNavigationObject] = jsonObject["guided"]?.decode(warnings: warnings) ?? [] + guard !guided.isEmpty else { + warnings?.log("Guided Navigation Document requires a non-empty guided array", model: Self.self, source: json, severity: .moderate) + throw JSONError.parsing(Self.self) + } + + self.init(guided: guided) + } +} diff --git a/Sources/Shared/Publication/GuidedNavigation/GuidedNavigationObject.swift b/Sources/Shared/Publication/GuidedNavigation/GuidedNavigationObject.swift new file mode 100644 index 0000000000..433f13eb59 --- /dev/null +++ b/Sources/Shared/Publication/GuidedNavigation/GuidedNavigationObject.swift @@ -0,0 +1,552 @@ +// +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumInternal + +/// Represents a single Guided Navigation Object, as defined in the +/// Readium Guided Navigation specification. +/// +/// https://readium.org/guided-navigation/ +public struct GuidedNavigationObject: Hashable, Sendable, JSONValueDecodable { + public typealias ID = String + + /// Unique identifier for this object, in the scope of the containing Guided + /// Navigation Document. + public let id: ID? + + /// References to resources referenced by the current Guided Navigation + /// Object. + public let refs: Refs? + + /// Textual equivalent of the resources or fragment of the resources + /// referenced by the current Guided Navigation Object. + public let text: Text? + + /// Convey the structural semantics of a publication. + public let roles: [Role] + + /// Text, audio or image description for the current Guided Navigation + /// Object. + public let description: Description? + + /// Items that are children of the containing Guided Navigation Object. + public let children: [GuidedNavigationObject] + + public init?( + id: ID? = nil, + refs: Refs? = nil, + text: Text? = nil, + roles: [Role] = [], + description: Description? = nil, + children: [GuidedNavigationObject] = [] + ) { + guard refs != nil || text != nil || !children.isEmpty else { + return nil + } + self.id = id + self.refs = refs + self.text = text + self.roles = roles + self.description = description + self.children = children + } + + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue else { + return nil + } + guard let jsonObject = json.object else { + warnings?.log("Invalid Guided Navigation Object", model: Self.self, source: json, severity: .moderate) + throw JSONError.parsing(Self.self) + } + + let refs = try Refs(json: json, warnings: warnings) + let text = try Text(json: jsonObject["text"], warnings: warnings) + let children: [GuidedNavigationObject] = jsonObject["children"]?.decode(warnings: warnings) ?? [] + + guard refs != nil || text != nil || !children.isEmpty else { + warnings?.log("Guided Navigation Object requires at least one of audioref, imgref, textref, videoref, text, or children", model: Self.self, source: json, severity: .moderate) + throw JSONError.parsing(Self.self) + } + + let description = try Description(json: jsonObject["description"], warnings: warnings) + + self.init( + id: jsonObject["id"]?.string, + refs: refs, + text: text, + roles: jsonObject["role"]?.decode(warnings: warnings) ?? [], + description: description, + children: children + ) + } + + /// Represents a collection of Guided Navigation References declared in a + /// Readium Guided Navigation Object. + public struct Refs: Hashable, Sendable, JSONValueDecodable { + /// References a textual resource or a fragment of it. + public let text: AnyURL? + + /// References an image or a fragment of it. + public let img: AnyURL? + + /// References an audio resource or a fragment of it. + public let audio: AnyURL? + + /// References a video clip or a fragment of it. + public let video: AnyURL? + + public init?( + text: AnyURL? = nil, + img: AnyURL? = nil, + audio: AnyURL? = nil, + video: AnyURL? = nil + ) { + guard text != nil || img != nil || audio != nil || video != nil else { + return nil + } + + self.audio = audio + self.img = img + self.text = text + self.video = video + } + + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue else { + return nil + } + guard let jsonObject = json.object else { + warnings?.log("Invalid Guided Navigation Refs", model: Self.self, source: json, severity: .moderate) + throw JSONError.parsing(Self.self) + } + let text = jsonObject["textref"]?.string.flatMap(AnyURL.init(string:)) + let img = jsonObject["imgref"]?.string.flatMap(AnyURL.init(string:)) + let audio = jsonObject["audioref"]?.string.flatMap(AnyURL.init(string:)) + let video = jsonObject["videoref"]?.string.flatMap(AnyURL.init(string:)) + + self.init(text: text, img: img, audio: audio, video: video) + } + } + + /// Represents the text content of a Guided Navigation Object. + /// + /// Can be either a bare string (normalized to `plain`) or an object with + /// `plain`, `ssml`, and `language` properties. + public struct Text: Hashable, Sendable, JSONValueDecodable { + public let plain: String? + public let ssml: String? + public let language: Language? + + public init?( + plain: String? = nil, + ssml: String? = nil, + language: Language? = nil + ) { + guard plain?.isEmpty == false || ssml?.isEmpty == false else { + return nil + } + self.plain = plain + self.ssml = ssml + self.language = language + } + + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue else { + return nil + } + if let string = json.string { + self.init(plain: string) + } else if let obj = json.object { + let plain = obj["plain"]?.string + let ssml = obj["ssml"]?.string + guard plain?.isEmpty == false || ssml?.isEmpty == false else { + warnings?.log("Guided Navigation String requires at least one of plain, or ssml", model: Self.self, source: json, severity: .moderate) + return nil + } + + self.init( + plain: plain, + ssml: ssml, + language: obj["language"]?.string.map { Language(code: .bcp47($0)) } + ) + } else { + warnings?.log("Invalid Guided Navigation Text", model: Self.self, source: json, severity: .moderate) + throw JSONError.parsing(Self.self) + } + } + } + + /// Represents the description for a Guided Navigation object. + public struct Description: Hashable, Sendable, JSONValueDecodable { + /// References to resources referenced by this description. + public let refs: Refs? + + /// Textual equivalent of the resources or fragment of the resources + /// referenced by this description. + public let text: Text? + + public init?( + refs: Refs? = nil, + text: Text? = nil + ) { + guard refs != nil || text != nil else { + return nil + } + self.refs = refs + self.text = text + } + + public init?(json: T?, warnings: WarningLogger?) throws { + guard let json = json?.jsonValue else { + return nil + } + guard let jsonObject = json.object else { + warnings?.log("Invalid Guided Navigation Description", model: Self.self, source: json, severity: .moderate) + throw JSONError.parsing(Self.self) + } + + let refs = try Refs(json: json, warnings: warnings) + let text = try Text(json: jsonObject["text"], warnings: warnings) + + guard refs != nil || text != nil else { + warnings?.log("Guided Navigation Description requires at least one of audioref, imgref, textref, videoref, or text", model: Self.self, source: json, severity: .moderate) + throw JSONError.parsing(Self.self) + } + + self.init(refs: refs, text: text) + } + } + + /// Represents a role for a Guided Navigation Object. + /// + /// See https://readium.org/guided-navigation/roles + public struct Role: Hashable, Sendable, JSONValueDecodable { + public let id: String + + public init(_ id: String) { + self.id = id + } + + public init?(json: T?, warnings: WarningLogger?) throws { + guard let id = json?.jsonValue.string else { return nil } + self.init(id) + } + + /// A sequential container for objects and/or child containers. + public static let sequence = Role("sequence") + + // MARK: Inherited from DPUB ARIA 1.0 + + /// A short summary of the principle ideas, concepts and conclusions of + /// the work, or of a section or excerpt within it. + public static let abstract = Role("abstract") + + /// A section or statement that acknowledges significant contributions + /// by persons, organizations, governments and other entities to the + /// realization of the work. + public static let acknowledgments = Role("acknowledgments") + + /// A closing statement from the author or a person of importance, + /// typically providing insight into how the content came to be written. + public static let afterword = Role("afterword") + + /// A section of supplemental information located after the primary + /// content that informs the content but is not central to it. + public static let appendix = Role("appendix") + + /// A link that allows the user to return to a related location in the + /// content (e.g., from a footnote to its reference or from a glossary + /// definition to where a term is used). + public static let backlink = Role("backlink") + + /// A list of external references cited in the work, which may be to + /// print or digital sources. + public static let bibliography = Role("bibliography") + + /// A reference to a bibliography entry. + public static let biblioref = Role("biblioref") + + /// A major thematic section of content in a work. + public static let chapter = Role("chapter") + + /// A short section of production notes particular to the edition + /// (e.g., describing the typeface used), often located at the end of a + /// work. + public static let colophon = Role("colophon") + + /// A concluding section or statement that summarizes the work or wraps + /// up the narrative. + public static let conclusion = Role("conclusion") + + /// An image that sets the mood or tone for the work and typically + /// includes the title and author. + public static let cover = Role("cover") + + /// An acknowledgment of the source of integrated content from + /// third-party sources, such as photos. + public static let credit = Role("credit") + + /// A collection of credits. + public static let credits = Role("credits") + + /// An inscription at the front of the work, typically addressed in + /// tribute to one or more persons close to the author. + public static let dedication = Role("dedication") + + /// A collection of notes at the end of a work or a section within it. + public static let endnotes = Role("endnotes") + + /// A quotation set at the start of the work or a section that + /// establishes the theme or sets the mood. + public static let epigraph = Role("epigraph") + + /// A concluding section of narrative that wraps up or comments on the + /// actions and events of the work, typically from a future perspective. + public static let epilogue = Role("epilogue") + + /// A set of corrections discovered after initial publication of the + /// work, sometimes referred to as corrigenda. + public static let errata = Role("errata") + + /// An illustration of the usage of a defined term or phrase. + public static let example = Role("example") + + /// Ancillary information, such as a citation or commentary, that + /// provides additional context to a referenced passage of text. + public static let footnote = Role("footnote") + + /// A brief dictionary of new, uncommon, or specialized terms used in + /// the content. + public static let glossary = Role("glossary") + + /// A reference to a glossary definition. + public static let glossref = Role("glossref") + + /// A navigational aid that provides a detailed list of links to key + /// subjects, names and other important topics covered in the work. + public static let index = Role("index") + + /// A preliminary section that typically introduces the scope or nature + /// of the work. + public static let introduction = Role("introduction") + + /// A reference to a footnote or endnote, typically appearing as a + /// superscripted number or symbol in the main body of text. + public static let noteref = Role("noteref") + + /// Notifies the user of consequences that might arise from an action + /// or event. Examples include warnings, cautions and dangers. + public static let notice = Role("notice") + + /// A separator denoting the position before which a break occurs + /// between two contiguous pages in a statically paginated version of + /// the content. + public static let pagebreak = Role("pagebreak") + + /// A navigational aid that provides a list of links to the pagebreaks + /// in the content. + public static let pagelist = Role("pagelist") + + /// A major structural division in a work that contains a set of + /// related sections dealing with a particular subject, narrative arc or + /// similar encapsulated theme. + public static let part = Role("part") + + /// An introductory section that precedes the work, typically written by + /// the author of the work. + public static let preface = Role("preface") + + /// An introductory section that sets the background to a work, + /// typically part of the narrative. + public static let prologue = Role("prologue") + + /// A distinctively placed or highlighted quotation from the current + /// content designed to draw attention to a topic or highlight a key + /// point. + public static let pullquote = Role("pullquote") + + /// A section of content structured as a series of questions and + /// answers, such as an interview or list of frequently asked questions. + public static let qna = Role("qna") + + /// An explanatory or alternate title for the work, or a section or + /// component within it. + public static let subtitle = Role("subtitle") + + /// Helpful information that clarifies some aspect of the content or + /// assists in its comprehension. + public static let tip = Role("tip") + + /// A navigational aid that provides an ordered list of links to the + /// major sectional headings in the content. + public static let toc = Role("toc") + + // MARK: Inherited from HTML and/or ARIA + + /// A self-contained composition in a document, page, application, or + /// site, which is intended to be independently distributable or + /// reusable. + public static let article = Role("article") + + /// Secondary or supplementary content. + public static let aside = Role("aside") + + /// Embedded sound content in a document. + public static let audio = Role("audio") + + /// A section that is quoted from another source. + public static let blockquote = Role("blockquote") + + /// Represents the content of an HTML document. + public static let body = Role("body") + + /// A caption for an image or a table. + public static let caption = Role("caption") + + /// A single cell of tabular data or content. + public static let cell = Role("cell") + + /// The header cell for a column, establishing a relationship between + /// it and the other cells in the same column. + public static let columnheader = Role("columnheader") + + /// A supporting section of the document, designed to be complementary + /// to the main content at a similar level in the DOM hierarchy. + public static let complementary = Role("complementary") + + /// A definition of a term or concept. + public static let definition = Role("definition") + + /// A disclosure widget that can be expanded. + public static let details = Role("details") + + /// An illustration, diagram, photo, code listing or similar, + /// referenced from the text of a work, and typically annotated with a + /// title, caption and/or credits. + public static let figure = Role("figure") + + /// Introductory content, typically a group of introductory or + /// navigational aids. + public static let header = Role("header") + + /// A heading for a section of the page. + public static let heading1 = Role("heading1") + + /// A heading for a section of the page. + public static let heading2 = Role("heading2") + + /// A heading for a section of the page. + public static let heading3 = Role("heading3") + + /// A heading for a section of the page. + public static let heading4 = Role("heading4") + + /// A heading for a section of the page. + public static let heading5 = Role("heading5") + + /// A heading for a section of the page. + public static let heading6 = Role("heading6") + + /// An image. + public static let image = Role("image") + + /// A structure that contains an enumeration of related content items. + public static let list = Role("list") + + /// A single item in an enumeration. + public static let listItem = Role("listItem") + + /// Content that is directly related to or expands upon the central + /// topic of the document. + public static let main = Role("main") + + /// Content that represents a mathematical expression. + public static let math = Role("math") + + /// A section of a page that links to other pages or to parts within + /// the page. + public static let navigation = Role("navigation") + + /// A paragraph. + public static let paragraph = Role("paragraph") + + /// Preformatted text which is to be presented exactly as written. + public static let preformatted = Role("preformatted") + + /// An element being used only for presentation and therefore that does + /// not have any accessibility semantics. + public static let presentation = Role("presentation") + + /// Content that is relevant to a specific, author-specified purpose + /// and sufficiently important that users will likely want to be able to + /// navigate to the section easily. + public static let region = Role("region") + + /// A row of data or content in a tabular structure. + public static let row = Role("row") + + /// The header cell for a row, establishing a relationship between it + /// and the other cells in the same row. + public static let rowheader = Role("rowheader") + + /// A generic standalone section of a document, which doesn't have a + /// more specific semantic element to represent it. + public static let section = Role("section") + + /// A divider that separates and distinguishes sections of content or + /// groups of menu items. + public static let separator = Role("separator") + + /// A summary of an element contained in details. + public static let summary = Role("summary") + + /// A structure containing data or content laid out in tabular form. + public static let table = Role("table") + + /// A word or phrase with a corresponding definition. + public static let term = Role("term") + + /// Embedded videos, movies, or audio files with captions in a + /// document. + public static let video = Role("video") + + // MARK: Inherited from EPUB SSV 1.1 + + /// An area in a comic panel that contains the words, spoken or thought, + /// of a character. + public static let bubble = Role("bubble") + + /// An introductory section that precedes the work, typically not + /// written by the author of the work. + public static let foreword = Role("foreword") + + /// A collection of references to audio clips. + public static let landmarks = Role("landmarks") + + /// A listing of audio clips included in the work. + public static let loa = Role("loa") + + /// A listing of illustrations included in the work. + public static let loi = Role("loi") + + /// A listing of tables included in the work. + public static let lot = Role("lot") + + /// A listing of video clips included in the work. + public static let lov = Role("lov") + + /// An individual frame, or drawing. + public static let panel = Role("panel") + + /// A group of panels (e.g., a strip). + public static let panelGroup = Role("panelGroup") + + /// An area of text in a comic panel that represents a sound. + public static let sound = Role("sound") + } +} diff --git a/Sources/Shared/Publication/HREFNormalizer.swift b/Sources/Shared/Publication/HREFNormalizer.swift index 5d8ce6cec5..8ef9f7ae9c 100644 --- a/Sources/Shared/Publication/HREFNormalizer.swift +++ b/Sources/Shared/Publication/HREFNormalizer.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Shared/Publication/Layout.swift b/Sources/Shared/Publication/Layout.swift index 971e945f34..1e0651172f 100644 --- a/Sources/Shared/Publication/Layout.swift +++ b/Sources/Shared/Publication/Layout.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // diff --git a/Sources/Shared/Publication/Link.swift b/Sources/Shared/Publication/Link.swift index 4b6f745eea..3fbd30b971 100644 --- a/Sources/Shared/Publication/Link.swift +++ b/Sources/Shared/Publication/Link.swift @@ -1,5 +1,5 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All rights reserved. // Use of this source code is governed by the BSD-style license // available in the top-level LICENSE file of the project. // @@ -14,7 +14,7 @@ public enum LinkError: Error, Equatable { /// Link Object for the Readium Web Publication Manifest. /// https://readium.org/webpub-manifest/schema/link.schema.json -public struct Link: JSONEquatable, Hashable, Sendable { +public struct Link: Hashable, Sendable, JSONValueDecodable, JSONObjectEncodable { /// URI or URI template of the linked resource. /// Note: a String because templates are lost with URL. public var href: String // URI @@ -91,18 +91,17 @@ public struct Link: JSONEquatable, Hashable, Sendable { self.children = children } - public init( - json: Any, - warnings: WarningLogger? = nil - ) throws { - guard let jsonObject = json as? JSONDictionary.Wrapped, - var href = jsonObject["href"] as? String + public init?(json: T?, warnings: WarningLogger?) throws { + let json = json?.jsonValue + + guard let jsonObject = json?.object, + var href = jsonObject["href"]?.string else { warnings?.log("`href` is required", model: Self.self, source: json) throw JSONError.parsing(Self.self) } - let templated = (jsonObject["templated"] as? Bool) ?? false + let templated = jsonObject["templated"]?.bool ?? false // 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 @@ -117,36 +116,36 @@ public struct Link: JSONEquatable, Hashable, Sendable { self.init( href: href, - mediaType: (jsonObject["type"] as? String).flatMap { MediaType($0) }, + mediaType: jsonObject["type"]?.decode(), templated: templated, - title: jsonObject["title"] as? String, - rels: .init(json: jsonObject["rel"]), - properties: (try? Properties(json: jsonObject["properties"], warnings: warnings)) ?? Properties(), - height: parsePositive(jsonObject["height"]), - width: parsePositive(jsonObject["width"]), - bitrate: parsePositiveDouble(jsonObject["bitrate"]), - duration: parsePositiveDouble(jsonObject["duration"]), - languages: parseArray(jsonObject["language"], allowingSingle: true), - alternates: .init(json: jsonObject["alternate"], warnings: warnings), - children: .init(json: jsonObject["children"], warnings: warnings) + title: jsonObject["title"]?.string, + rels: jsonObject["rel"]?.decode(allowingSingle: true) ?? [], + properties: (try? jsonObject["properties"]?.decode(warnings: warnings)) ?? Properties(), + height: jsonObject["height"]?.nonNegative(), + width: jsonObject["width"]?.nonNegative(), + bitrate: jsonObject["bitrate"]?.nonNegative(), + duration: jsonObject["duration"]?.nonNegative(), + languages: jsonObject["language"]?.decode(allowingSingle: true) ?? [], + alternates: jsonObject["alternate"]?.decode(warnings: warnings) ?? [], + children: jsonObject["children"]?.decode(warnings: warnings) ?? [] ) } - public var json: JSONDictionary.Wrapped { - makeJSON([ + public var jsonObject: [String: JSONValue] { + .init([ "href": href, - "type": encodeIfNotNil(mediaType?.string), + "type": mediaType?.string, "templated": templated, - "title": encodeIfNotNil(title), - "rel": encodeIfNotEmpty(rels.json), - "properties": encodeIfNotEmpty(properties.json), - "height": encodeIfNotNil(height), - "width": encodeIfNotNil(width), - "bitrate": encodeIfNotNil(bitrate), - "duration": encodeIfNotNil(duration), - "language": encodeIfNotEmpty(languages), - "alternate": encodeIfNotEmpty(alternates.json), - "children": encodeIfNotEmpty(children.json), + "title": title, + "rel": rels.orNullIfEmpty, + "properties": properties.orNullIfEmpty, + "height": height, + "width": width, + "bitrate": bitrate, + "duration": duration, + "language": languages.orNullIfEmpty, + "alternate": alternates.orNullIfEmpty, + "children": children.orNullIfEmpty, ]) } @@ -172,8 +171,8 @@ public struct Link: JSONEquatable, Hashable, Sendable { /// /// If the HREF is a template, the `parameters` are used to expand it /// according to RFC 6570. - public func url( - relativeTo baseURL: T?, + public func url( + relativeTo baseURL: URLConvertible?, parameters: [String: LosslessStringConvertible] = [:] ) -> AnyURL { let url = url(parameters: parameters) @@ -202,31 +201,18 @@ public struct Link: JSONEquatable, Hashable, Sendable { } /// Merges in the given additional other `properties`. - public mutating func addProperties(_ properties: JSONDictionary.Wrapped) { + public mutating func addProperties(_ properties: [String: JSONValue]) { self.properties.add(properties) } } -public extension Array where Element == Link { - /// Parses multiple JSON links into an array of Link. - /// eg. let links = [Link](json: [["href", "http://link1"], ["href", "http://link2"]]) - init( - json: Any?, - warnings: WarningLogger? = nil - ) { - self.init() - guard let json = json as? [Any] else { - return - } - - let links = json.compactMap { try? Link(json: $0, warnings: warnings) } - append(contentsOf: links) - } - - var json: [JSONDictionary.Wrapped] { - map(\.json) +extension Link: URLConvertible { + public var anyURL: AnyURL { + url() } +} +public extension [Link] { /// Finds the first link with the given relation. func firstWithRel(_ rel: LinkRelation) -> Link? { first { $0.rels.contains(rel) } @@ -288,6 +274,11 @@ public extension Array where Element == Link { allSatisfy { $0.mediaType?.isHTML == true } } + /// Returns whether any resource in the collection matches the given media type. + func anyMatchingMediaType(_ mediaType: MediaType) -> Bool { + contains { mediaType.matches($0.mediaType) } + } + /// Returns whether all the resources in the collection are matching the given media type. func allMatchingMediaType(_ mediaType: MediaType) -> Bool { allSatisfy { mediaType.matches($0.mediaType) } diff --git a/Sources/Shared/Publication/LinkRelation.swift b/Sources/Shared/Publication/LinkRelation.swift index a6f70b86af..c58e75d7e8 100644 --- a/Sources/Shared/Publication/LinkRelation.swift +++ b/Sources/Shared/Publication/LinkRelation.swift @@ -1,22 +1,31 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumInternal /// Link relations as defined in https://readium.org/webpub-manifest/relationships.html -public struct LinkRelation: Sendable { +public struct LinkRelation: Sendable, Hashable, RawRepresentable, JSONValueEncodable { + public var rawValue: String + /// The string representation of this link relation. - public let string: String + public var string: String { + rawValue + } - public init(_ string: String) { + public init?(rawValue: String) { // > Registered relation type names MUST conform to the reg-rel-type rule // > (see Section 3.3) and MUST be compared character by character in a // > case-insensitive fashion. // https://tools.ietf.org/html/rfc8288#section-2.1.1 - self.string = string.lowercased() + self.rawValue = rawValue.lowercased() + } + + public init(_ string: String) { + self.init(rawValue: string)! } public func hasPrefix(_ prefix: String) -> Bool { @@ -35,6 +44,10 @@ public struct LinkRelation: Sendable { hasPrefix("http://opds-spec.org/acquisition") } + public var jsonValue: JSONValue { + .string(string) + } + // MARK: - Known Link Relations /// Designates a substitute for the link's context. @@ -45,6 +58,8 @@ public struct LinkRelation: Sendable { public static let cover = LinkRelation("cover") /// Links to a manifest. public static let manifest = LinkRelation("manifest") + /// Identifies a related resource. + public static let related = LinkRelation("related") /// Refers to a URI or templated URI that will perform a search. public static let search = LinkRelation("search") /// Conveys an identifier for the link's context. @@ -124,16 +139,16 @@ public struct LinkRelation: Sendable { // Authentication for OPDS – https://drafts.opds.io/authentication-for-opds-1.0.html - // Location where a client can authenticate the user with OAuth. + /// Location where a client can authenticate the user with OAuth. public static let opdsAuthenticate = LinkRelation("authenticate") - // Location where a client can refresh the Access Token by sending a Refresh Token. + /// Location where a client can refresh the Access Token by sending a Refresh Token. public static let opdsRefresh = LinkRelation("refresh") - // Logo associated to the Catalog provider. + /// Logo associated to the Catalog provider. public static let opdsLogo = LinkRelation("logo") - // Location where a user can register. + /// Location where a user can register. public static let opdsRegister = LinkRelation("register") - // Support resources for the user (either a website, an email or a telephone number). + /// Support resources for the user (either a website, an email or a telephone number). public static let opdsHelp = LinkRelation("help") } @@ -143,33 +158,7 @@ extension LinkRelation: ExpressibleByStringLiteral { } } -extension LinkRelation: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(string) - } - - public var hashValue: Int { - string.hashValue - } -} - -public extension Array where Element == LinkRelation { - /// Parses multiple JSON relations into an array of `LinkRelation`. - init(json: Any?) { - self.init() - - if let json = json as? String { - append(LinkRelation(json)) - } else if let json = json as? [String] { - let rels = json.compactMap { LinkRelation($0) } - append(contentsOf: rels) - } - } - - var json: [String] { - map(\.string) - } - +public extension [LinkRelation] { func contains(_ other: String) -> Bool { contains(LinkRelation(other)) } diff --git a/Sources/Shared/Publication/LocalizedString.swift b/Sources/Shared/Publication/LocalizedString.swift index d3de980d0e..f4cd6c8fda 100644 --- a/Sources/Shared/Publication/LocalizedString.swift +++ b/Sources/Shared/Publication/LocalizedString.swift @@ -1,56 +1,55 @@ // -// Copyright 2025 Readium Foundation. All rights reserved. +// Copyright 2026 Readium Foundation. All 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 Foundation +import ReadiumInternal /// Represents a potentially localized string. /// Can be either: /// - a single nonlocalized string /// - a dictionary of localized strings indexed by the BCP 47 language tag -public enum LocalizedString: Hashable, Sendable { +public enum LocalizedString: Hashable, Sendable, JSONValueDecodable, JSONValueEncodable { case nonlocalized(String) case localized([String: String]) /// Parses the given JSON representation of the localized string. - /// "anyOf": [ - /// { - /// "type": "string" - /// }, - /// { - /// "description": "The language in a language map must be a valid BCP 47 tag.", - /// "type": "object", - /// "patternProperties": { - /// "^((?(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?([A-Za-z]{2,3}(-(?[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-(?