├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ └── Readability.xcscheme ├── Example └── ReadabilityTest │ ├── ReadabilityTest.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── ReadabilityTest.xcscheme │ └── ReadabilityTest │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── ReadabilityTestApp.swift │ ├── ReaderTextView.swift │ └── ReaderWebView.swift ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources ├── Readability │ ├── Internal │ │ ├── HTMLFetcher.swift │ │ └── ReadabilityRunner.swift │ ├── Options.swift │ ├── Readability.swift │ ├── Resources │ │ ├── ReadabilityBasic.js │ │ ├── ReadabilitySanitized.js │ │ └── ReadabilitySanitized.js.LICENSE.txt │ └── exported.swift ├── ReadabilityCore │ ├── ReadabilityMessageHandler.swift │ ├── ReadabilityMessageType.swift │ ├── ReadabilityResult.swift │ ├── ReaderAvailability.swift │ ├── ReaderContentGeneratable.swift │ ├── ReaderStyle.swift │ └── ScriptLoader.swift └── ReadabilityUI │ ├── Internal │ └── ReaderContentGenerator.swift │ ├── ReadabilityWebCoordinator.swift │ ├── ReaderControllable.swift │ ├── Resources │ ├── AtDocumentEnd.js │ ├── AtDocumentStart.js │ ├── AtDocumentStart.js.LICENSE.txt │ └── Reader.html │ └── exported.swift ├── Tests └── ReadabilityTests │ └── ReadabilityTests.swift ├── bootstrap.sh ├── nestfile.yaml ├── package-lock.json ├── package.json ├── webpack-resources ├── AtDocumentStart.js ├── ReadabilityBasic.js ├── ReadabilitySanitized.js ├── Reader.css └── Reader.html └── webpack.config.js /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift 5 | 6 | on: 7 | pull_request: 8 | branches: [ "main" ] 9 | 10 | jobs: 11 | build: 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Build 17 | run: swift build -v 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | node_modules/ 10 | repomix-output.txt 11 | .nest/ 12 | 13 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/Readability.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Example/ReadabilityTest/ReadabilityTest.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 426477962D42D0C300525E86 /* WebUI in Frameworks */ = {isa = PBXBuildFile; productRef = 426477952D42D0C300525E86 /* WebUI */; }; 11 | 42C9CCD42D49F867001EFE8F /* ReadabilityUI in Frameworks */ = {isa = PBXBuildFile; productRef = 42C9CCD32D49F867001EFE8F /* ReadabilityUI */; }; 12 | 42C9CCE72D49F95F001EFE8F /* Readability in Frameworks */ = {isa = PBXBuildFile; productRef = 42C9CCE62D49F95F001EFE8F /* Readability */; }; 13 | /* End PBXBuildFile section */ 14 | 15 | /* Begin PBXFileReference section */ 16 | 426477D32D436C1400525E86 /* swift-readability */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "swift-readability"; path = ../..; sourceTree = ""; }; 17 | 42C736A72D42C13D0065FFAD /* ReadabilityTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReadabilityTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; 18 | /* End PBXFileReference section */ 19 | 20 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 21 | 42C736A92D42C13D0065FFAD /* ReadabilityTest */ = { 22 | isa = PBXFileSystemSynchronizedRootGroup; 23 | path = ReadabilityTest; 24 | sourceTree = ""; 25 | }; 26 | /* End PBXFileSystemSynchronizedRootGroup section */ 27 | 28 | /* Begin PBXFrameworksBuildPhase section */ 29 | 42C736A42D42C13D0065FFAD /* Frameworks */ = { 30 | isa = PBXFrameworksBuildPhase; 31 | buildActionMask = 2147483647; 32 | files = ( 33 | 426477962D42D0C300525E86 /* WebUI in Frameworks */, 34 | 42C9CCE72D49F95F001EFE8F /* Readability in Frameworks */, 35 | 42C9CCD42D49F867001EFE8F /* ReadabilityUI in Frameworks */, 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 426477A02D42DCC500525E86 /* Frameworks */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | 426477D32D436C1400525E86 /* swift-readability */, 46 | ); 47 | name = Frameworks; 48 | sourceTree = ""; 49 | }; 50 | 42C7369E2D42C13D0065FFAD = { 51 | isa = PBXGroup; 52 | children = ( 53 | 42C736A92D42C13D0065FFAD /* ReadabilityTest */, 54 | 426477A02D42DCC500525E86 /* Frameworks */, 55 | 42C736A82D42C13D0065FFAD /* Products */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | 42C736A82D42C13D0065FFAD /* Products */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 42C736A72D42C13D0065FFAD /* ReadabilityTest.app */, 63 | ); 64 | name = Products; 65 | sourceTree = ""; 66 | }; 67 | /* End PBXGroup section */ 68 | 69 | /* Begin PBXNativeTarget section */ 70 | 42C736A62D42C13D0065FFAD /* ReadabilityTest */ = { 71 | isa = PBXNativeTarget; 72 | buildConfigurationList = 42C736B52D42C13F0065FFAD /* Build configuration list for PBXNativeTarget "ReadabilityTest" */; 73 | buildPhases = ( 74 | 42C736A32D42C13D0065FFAD /* Sources */, 75 | 42C736A42D42C13D0065FFAD /* Frameworks */, 76 | 42C736A52D42C13D0065FFAD /* Resources */, 77 | ); 78 | buildRules = ( 79 | ); 80 | dependencies = ( 81 | ); 82 | fileSystemSynchronizedGroups = ( 83 | 42C736A92D42C13D0065FFAD /* ReadabilityTest */, 84 | ); 85 | name = ReadabilityTest; 86 | packageProductDependencies = ( 87 | 426477952D42D0C300525E86 /* WebUI */, 88 | 42C9CCD32D49F867001EFE8F /* ReadabilityUI */, 89 | 42C9CCE62D49F95F001EFE8F /* Readability */, 90 | ); 91 | productName = ReadabilityTest; 92 | productReference = 42C736A72D42C13D0065FFAD /* ReadabilityTest.app */; 93 | productType = "com.apple.product-type.application"; 94 | }; 95 | /* End PBXNativeTarget section */ 96 | 97 | /* Begin PBXProject section */ 98 | 42C7369F2D42C13D0065FFAD /* Project object */ = { 99 | isa = PBXProject; 100 | attributes = { 101 | BuildIndependentTargetsInParallel = 1; 102 | LastSwiftUpdateCheck = 1610; 103 | LastUpgradeCheck = 1610; 104 | TargetAttributes = { 105 | 42C736A62D42C13D0065FFAD = { 106 | CreatedOnToolsVersion = 16.1; 107 | }; 108 | }; 109 | }; 110 | buildConfigurationList = 42C736A22D42C13D0065FFAD /* Build configuration list for PBXProject "ReadabilityTest" */; 111 | developmentRegion = en; 112 | hasScannedForEncodings = 0; 113 | knownRegions = ( 114 | en, 115 | Base, 116 | ); 117 | mainGroup = 42C7369E2D42C13D0065FFAD; 118 | minimizedProjectReferenceProxies = 1; 119 | packageReferences = ( 120 | 426477942D42D0C300525E86 /* XCRemoteSwiftPackageReference "WebUI" */, 121 | 426477E52D436CB600525E86 /* XCLocalSwiftPackageReference "../../../swift-readability" */, 122 | ); 123 | preferredProjectObjectVersion = 77; 124 | productRefGroup = 42C736A82D42C13D0065FFAD /* Products */; 125 | projectDirPath = ""; 126 | projectRoot = ""; 127 | targets = ( 128 | 42C736A62D42C13D0065FFAD /* ReadabilityTest */, 129 | ); 130 | }; 131 | /* End PBXProject section */ 132 | 133 | /* Begin PBXResourcesBuildPhase section */ 134 | 42C736A52D42C13D0065FFAD /* Resources */ = { 135 | isa = PBXResourcesBuildPhase; 136 | buildActionMask = 2147483647; 137 | files = ( 138 | ); 139 | runOnlyForDeploymentPostprocessing = 0; 140 | }; 141 | /* End PBXResourcesBuildPhase section */ 142 | 143 | /* Begin PBXSourcesBuildPhase section */ 144 | 42C736A32D42C13D0065FFAD /* Sources */ = { 145 | isa = PBXSourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | ); 149 | runOnlyForDeploymentPostprocessing = 0; 150 | }; 151 | /* End PBXSourcesBuildPhase section */ 152 | 153 | /* Begin XCBuildConfiguration section */ 154 | 42C736B32D42C13F0065FFAD /* Debug */ = { 155 | isa = XCBuildConfiguration; 156 | buildSettings = { 157 | ALWAYS_SEARCH_USER_PATHS = NO; 158 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 159 | CLANG_ANALYZER_NONNULL = YES; 160 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 161 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 162 | CLANG_ENABLE_MODULES = YES; 163 | CLANG_ENABLE_OBJC_ARC = YES; 164 | CLANG_ENABLE_OBJC_WEAK = YES; 165 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 166 | CLANG_WARN_BOOL_CONVERSION = YES; 167 | CLANG_WARN_COMMA = YES; 168 | CLANG_WARN_CONSTANT_CONVERSION = YES; 169 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 170 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 171 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 172 | CLANG_WARN_EMPTY_BODY = YES; 173 | CLANG_WARN_ENUM_CONVERSION = YES; 174 | CLANG_WARN_INFINITE_RECURSION = YES; 175 | CLANG_WARN_INT_CONVERSION = YES; 176 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 177 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 178 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 179 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 180 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 181 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 182 | CLANG_WARN_STRICT_PROTOTYPES = YES; 183 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 184 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 185 | CLANG_WARN_UNREACHABLE_CODE = YES; 186 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 187 | COPY_PHASE_STRIP = NO; 188 | DEBUG_INFORMATION_FORMAT = dwarf; 189 | ENABLE_STRICT_OBJC_MSGSEND = YES; 190 | ENABLE_TESTABILITY = YES; 191 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 192 | GCC_C_LANGUAGE_STANDARD = gnu17; 193 | GCC_DYNAMIC_NO_PIC = NO; 194 | GCC_NO_COMMON_BLOCKS = YES; 195 | GCC_OPTIMIZATION_LEVEL = 0; 196 | GCC_PREPROCESSOR_DEFINITIONS = ( 197 | "DEBUG=1", 198 | "$(inherited)", 199 | ); 200 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 201 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 202 | GCC_WARN_UNDECLARED_SELECTOR = YES; 203 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 204 | GCC_WARN_UNUSED_FUNCTION = YES; 205 | GCC_WARN_UNUSED_VARIABLE = YES; 206 | IPHONEOS_DEPLOYMENT_TARGET = 18.1; 207 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 208 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 209 | MTL_FAST_MATH = YES; 210 | ONLY_ACTIVE_ARCH = YES; 211 | SDKROOT = iphoneos; 212 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 213 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 214 | }; 215 | name = Debug; 216 | }; 217 | 42C736B42D42C13F0065FFAD /* Release */ = { 218 | isa = XCBuildConfiguration; 219 | buildSettings = { 220 | ALWAYS_SEARCH_USER_PATHS = NO; 221 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 222 | CLANG_ANALYZER_NONNULL = YES; 223 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 224 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 225 | CLANG_ENABLE_MODULES = YES; 226 | CLANG_ENABLE_OBJC_ARC = YES; 227 | CLANG_ENABLE_OBJC_WEAK = YES; 228 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 229 | CLANG_WARN_BOOL_CONVERSION = YES; 230 | CLANG_WARN_COMMA = YES; 231 | CLANG_WARN_CONSTANT_CONVERSION = YES; 232 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 233 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 234 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 235 | CLANG_WARN_EMPTY_BODY = YES; 236 | CLANG_WARN_ENUM_CONVERSION = YES; 237 | CLANG_WARN_INFINITE_RECURSION = YES; 238 | CLANG_WARN_INT_CONVERSION = YES; 239 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 240 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 241 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 242 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 243 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 244 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 245 | CLANG_WARN_STRICT_PROTOTYPES = YES; 246 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 247 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 248 | CLANG_WARN_UNREACHABLE_CODE = YES; 249 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 250 | COPY_PHASE_STRIP = NO; 251 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 252 | ENABLE_NS_ASSERTIONS = NO; 253 | ENABLE_STRICT_OBJC_MSGSEND = YES; 254 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 255 | GCC_C_LANGUAGE_STANDARD = gnu17; 256 | GCC_NO_COMMON_BLOCKS = YES; 257 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 258 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 259 | GCC_WARN_UNDECLARED_SELECTOR = YES; 260 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 261 | GCC_WARN_UNUSED_FUNCTION = YES; 262 | GCC_WARN_UNUSED_VARIABLE = YES; 263 | IPHONEOS_DEPLOYMENT_TARGET = 18.1; 264 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 265 | MTL_ENABLE_DEBUG_INFO = NO; 266 | MTL_FAST_MATH = YES; 267 | SDKROOT = iphoneos; 268 | SWIFT_COMPILATION_MODE = wholemodule; 269 | VALIDATE_PRODUCT = YES; 270 | }; 271 | name = Release; 272 | }; 273 | 42C736B62D42C13F0065FFAD /* Debug */ = { 274 | isa = XCBuildConfiguration; 275 | buildSettings = { 276 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 277 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 278 | CODE_SIGN_STYLE = Automatic; 279 | CURRENT_PROJECT_VERSION = 1; 280 | DEVELOPMENT_ASSET_PATHS = "\"ReadabilityTest/Preview Content\""; 281 | DEVELOPMENT_TEAM = G8RH83B4LT; 282 | ENABLE_PREVIEWS = YES; 283 | GENERATE_INFOPLIST_FILE = YES; 284 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 285 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 286 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 287 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 288 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 289 | LD_RUNPATH_SEARCH_PATHS = ( 290 | "$(inherited)", 291 | "@executable_path/Frameworks", 292 | ); 293 | MARKETING_VERSION = 1.0; 294 | PRODUCT_BUNDLE_IDENTIFIER = com.ryu.ReadabilityTest; 295 | PRODUCT_NAME = "$(TARGET_NAME)"; 296 | SWIFT_EMIT_LOC_STRINGS = YES; 297 | SWIFT_VERSION = 6.0; 298 | TARGETED_DEVICE_FAMILY = "1,2"; 299 | }; 300 | name = Debug; 301 | }; 302 | 42C736B72D42C13F0065FFAD /* Release */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 306 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 307 | CODE_SIGN_STYLE = Automatic; 308 | CURRENT_PROJECT_VERSION = 1; 309 | DEVELOPMENT_ASSET_PATHS = "\"ReadabilityTest/Preview Content\""; 310 | DEVELOPMENT_TEAM = G8RH83B4LT; 311 | ENABLE_PREVIEWS = YES; 312 | GENERATE_INFOPLIST_FILE = YES; 313 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 314 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 315 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 316 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 317 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 318 | LD_RUNPATH_SEARCH_PATHS = ( 319 | "$(inherited)", 320 | "@executable_path/Frameworks", 321 | ); 322 | MARKETING_VERSION = 1.0; 323 | PRODUCT_BUNDLE_IDENTIFIER = com.ryu.ReadabilityTest; 324 | PRODUCT_NAME = "$(TARGET_NAME)"; 325 | SWIFT_EMIT_LOC_STRINGS = YES; 326 | SWIFT_VERSION = 6.0; 327 | TARGETED_DEVICE_FAMILY = "1,2"; 328 | }; 329 | name = Release; 330 | }; 331 | /* End XCBuildConfiguration section */ 332 | 333 | /* Begin XCConfigurationList section */ 334 | 42C736A22D42C13D0065FFAD /* Build configuration list for PBXProject "ReadabilityTest" */ = { 335 | isa = XCConfigurationList; 336 | buildConfigurations = ( 337 | 42C736B32D42C13F0065FFAD /* Debug */, 338 | 42C736B42D42C13F0065FFAD /* Release */, 339 | ); 340 | defaultConfigurationIsVisible = 0; 341 | defaultConfigurationName = Release; 342 | }; 343 | 42C736B52D42C13F0065FFAD /* Build configuration list for PBXNativeTarget "ReadabilityTest" */ = { 344 | isa = XCConfigurationList; 345 | buildConfigurations = ( 346 | 42C736B62D42C13F0065FFAD /* Debug */, 347 | 42C736B72D42C13F0065FFAD /* Release */, 348 | ); 349 | defaultConfigurationIsVisible = 0; 350 | defaultConfigurationName = Release; 351 | }; 352 | /* End XCConfigurationList section */ 353 | 354 | /* Begin XCLocalSwiftPackageReference section */ 355 | 426477E52D436CB600525E86 /* XCLocalSwiftPackageReference "../../../swift-readability" */ = { 356 | isa = XCLocalSwiftPackageReference; 357 | relativePath = "../../../swift-readability"; 358 | }; 359 | /* End XCLocalSwiftPackageReference section */ 360 | 361 | /* Begin XCRemoteSwiftPackageReference section */ 362 | 426477942D42D0C300525E86 /* XCRemoteSwiftPackageReference "WebUI" */ = { 363 | isa = XCRemoteSwiftPackageReference; 364 | repositoryURL = "https://github.com/cybozu/WebUI.git"; 365 | requirement = { 366 | kind = upToNextMajorVersion; 367 | minimumVersion = 3.0.3; 368 | }; 369 | }; 370 | /* End XCRemoteSwiftPackageReference section */ 371 | 372 | /* Begin XCSwiftPackageProductDependency section */ 373 | 426477952D42D0C300525E86 /* WebUI */ = { 374 | isa = XCSwiftPackageProductDependency; 375 | package = 426477942D42D0C300525E86 /* XCRemoteSwiftPackageReference "WebUI" */; 376 | productName = WebUI; 377 | }; 378 | 42C9CCD32D49F867001EFE8F /* ReadabilityUI */ = { 379 | isa = XCSwiftPackageProductDependency; 380 | productName = ReadabilityUI; 381 | }; 382 | 42C9CCE62D49F95F001EFE8F /* Readability */ = { 383 | isa = XCSwiftPackageProductDependency; 384 | productName = Readability; 385 | }; 386 | /* End XCSwiftPackageProductDependency section */ 387 | }; 388 | rootObject = 42C7369F2D42C13D0065FFAD /* Project object */; 389 | } 390 | -------------------------------------------------------------------------------- /Example/ReadabilityTest/ReadabilityTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/ReadabilityTest/ReadabilityTest.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "1c06d2bcc97772f66ae1505048d9523cfaa6f23e2efb7faca652adfa6eab795e", 3 | "pins" : [ 4 | { 5 | "identity" : "webui", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/cybozu/WebUI", 8 | "state" : { 9 | "revision" : "0112c53e383c3ac5b27d0be452356fcd97f147e8", 10 | "version" : "3.0.3" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Example/ReadabilityTest/ReadabilityTest.xcodeproj/xcshareddata/xcschemes/ReadabilityTest.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Example/ReadabilityTest/ReadabilityTest/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/ReadabilityTest/ReadabilityTest/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Example/ReadabilityTest/ReadabilityTest/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ReadabilityTest/ReadabilityTest/ContentView.swift: -------------------------------------------------------------------------------- 1 | import Readability 2 | import SwiftUI 3 | import WebKit 4 | import WebUI 5 | 6 | struct ContentView: View { 7 | var body: some View { 8 | NavigationStack { 9 | List { 10 | NavigationLink("ReaderTextView") { 11 | ReaderTextView() 12 | } 13 | NavigationLink("ReaderWebView") { 14 | ReaderWebView() 15 | } 16 | } 17 | } 18 | } 19 | } 20 | 21 | #Preview { 22 | ContentView() 23 | } 24 | -------------------------------------------------------------------------------- /Example/ReadabilityTest/ReadabilityTest/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ReadabilityTest/ReadabilityTest/ReadabilityTestApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct ReadabilityTestApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Example/ReadabilityTest/ReadabilityTest/ReaderTextView.swift: -------------------------------------------------------------------------------- 1 | import Readability 2 | import SwiftUI 3 | 4 | struct ReaderTextView: View { 5 | @State var content: String = "" 6 | @State var urlString: String = "" 7 | @State var isLoading = false 8 | @State var isPresented = true 9 | 10 | private let readability = Readability() 11 | 12 | var body: some View { 13 | ScrollView { 14 | Text(content) 15 | } 16 | .searchable(text: $urlString, isPresented: $isPresented) 17 | .onSubmit(of: .search) { 18 | if let url = URL(string: urlString) { 19 | withLoading { 20 | content = try await readability.parse(url: url).textContent 21 | } 22 | } 23 | } 24 | .overlay { 25 | if isLoading { 26 | ProgressView() 27 | } 28 | } 29 | } 30 | 31 | private func withLoading(_ operation: @escaping () async throws -> Void) { 32 | isLoading = true 33 | Task { 34 | do { 35 | try await operation() 36 | } catch { 37 | print(error) 38 | } 39 | isLoading = false 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Example/ReadabilityTest/ReadabilityTest/ReaderWebView.swift: -------------------------------------------------------------------------------- 1 | import Observation 2 | import ReadabilityUI 3 | import SwiftUI 4 | import WebKit 5 | import WebUI 6 | 7 | @Observable 8 | @MainActor 9 | final class ReaderWebModel { 10 | var configuration: WKWebViewConfiguration? 11 | var isLoading = false 12 | var urlString = "" 13 | var isPresented = true 14 | var readerHTMLCaches: [URL: String] = [:] 15 | var isReaderAvailable = false 16 | var isReaderPresenting = false 17 | 18 | let webCoordinator = ReadabilityWebCoordinator(initialStyle: .init(theme: .dark, fontSize: .size5)) 19 | } 20 | 21 | struct ReaderWebView: View { 22 | @Bindable var model = ReaderWebModel() 23 | 24 | var body: some View { 25 | Group { 26 | if let configuration = model.configuration { 27 | core(configuration: configuration) 28 | } else { 29 | ProgressView() 30 | } 31 | } 32 | .task { 33 | model.configuration = try? await model.webCoordinator.createReadableWebViewConfiguration() 34 | } 35 | .overlay { 36 | if model.isLoading { 37 | ProgressView() 38 | } 39 | } 40 | } 41 | 42 | private func core(configuration: WKWebViewConfiguration) -> some View { 43 | WebViewReader { proxy in 44 | VStack(spacing: 0) { 45 | ProgressView(value: proxy.estimatedProgress, total: 1) 46 | .progressViewStyle(.linear) 47 | WebView(configuration: configuration) 48 | .uiDelegate(ReadabilityUIDelegate()) 49 | .navigationDelegate( 50 | NavigationDelegate(didFinish: { 51 | Task { @MainActor in 52 | model.isReaderPresenting = try await proxy.isReaderMode() 53 | } 54 | }) 55 | ) 56 | .ignoresSafeArea(edges: .bottom) 57 | .searchable(text: $model.urlString, isPresented: $model.isPresented) 58 | .onSubmit(of: .search) { 59 | if let url = URL(string: model.urlString) { 60 | proxy.load(request: URLRequest(url: url)) 61 | } 62 | } 63 | .task { 64 | for await html in model.webCoordinator.contentParsed { 65 | if let url = proxy.url { 66 | model.readerHTMLCaches[url] = html 67 | } 68 | } 69 | } 70 | .task { 71 | for await availability in model.webCoordinator.availabilityChanged { 72 | model.isReaderAvailable = availability == .available 73 | } 74 | } 75 | .safeAreaInset(edge: .bottom) { 76 | bottomBar(proxy: proxy) 77 | } 78 | } 79 | } 80 | } 81 | 82 | private func withLoading(_ operation: @escaping () async throws -> Void) { 83 | model.isLoading = true 84 | Task { 85 | do { 86 | try await operation() 87 | } catch { 88 | print(error) 89 | } 90 | model.isLoading = false 91 | } 92 | } 93 | 94 | private func bottomBar(proxy: WebViewProxy) -> some View { 95 | HStack(spacing: 12) { 96 | Group { 97 | Button { 98 | proxy.goBack() 99 | } label: { 100 | Image(systemName: "chevron.backward") 101 | .resizable() 102 | .scaledToFit() 103 | } 104 | .disabled(!proxy.canGoBack) 105 | Button { 106 | proxy.goForward() 107 | } label: { 108 | Image(systemName: "chevron.forward") 109 | .resizable() 110 | .scaledToFit() 111 | } 112 | .disabled(!proxy.canGoForward) 113 | 114 | if let url = proxy.url, 115 | let html = model.readerHTMLCaches[url] 116 | { 117 | Button { 118 | Task { 119 | if model.isReaderPresenting { 120 | try await proxy.hideReaderContent() 121 | } else { 122 | try await proxy.showReaderContent(with: html) 123 | } 124 | model.isReaderPresenting.toggle() 125 | } 126 | } label: { 127 | Image(systemName: "text.page") 128 | .resizable() 129 | .scaledToFit() 130 | } 131 | .symbolVariant(model.isReaderPresenting ? .fill : .none) 132 | 133 | Menu { 134 | Menu("Theme") { 135 | ForEach(ReaderStyle.Theme.allCases, id: \.self) { theme in 136 | Button(theme.rawValue) { 137 | Task { 138 | try await proxy.set(theme: theme) 139 | } 140 | } 141 | } 142 | } 143 | Menu("FontSize") { 144 | ForEach(ReaderStyle.FontSize.allCases, id: \.self) { fontSize in 145 | Button(fontSize.rawValue.description) { 146 | Task { 147 | try await proxy.set(fontSize: fontSize) 148 | } 149 | } 150 | } 151 | } 152 | } label: { 153 | Image(systemName: "paintpalette") 154 | } 155 | .disabled(!model.isReaderPresenting) 156 | } 157 | } 158 | .frame(width: 15) 159 | } 160 | .padding() 161 | .background(.ultraThinMaterial) 162 | .clipShape(.capsule) 163 | } 164 | } 165 | 166 | final class ReadabilityUIDelegate: NSObject, WKUIDelegate { 167 | func webView(_ webView: WKWebView, createWebViewWith _: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures _: WKWindowFeatures) -> WKWebView? { 168 | if navigationAction.targetFrame == nil { 169 | webView.load(navigationAction.request) 170 | } 171 | return nil 172 | } 173 | } 174 | 175 | final class NavigationDelegate: NSObject, WKNavigationDelegate { 176 | let didFinish: () -> Void 177 | 178 | init(didFinish: @escaping () -> Void) { 179 | self.didFinish = didFinish 180 | } 181 | 182 | func webView(_: WKWebView, didFinish _: WKNavigation!) { 183 | didFinish() 184 | } 185 | } 186 | 187 | extension WebViewProxy: @retroactive ReaderControllable { 188 | public func evaluateJavaScript(_ javascriptString: String) async throws -> Any { 189 | let result: Any? = try await evaluateJavaScript(javascriptString) 190 | return result ?? () 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ryu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: bootstrap 2 | bootstrap: 3 | ./bootstrap.sh 4 | 5 | .PHONY: format 6 | format: 7 | .nest/bin/swiftformat . 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-readability", 8 | platforms: [ 9 | .macOS(.v11), 10 | .iOS(.v14), 11 | .visionOS(.v1), 12 | ], 13 | products: [ 14 | // Products define the executables and libraries a package produces, making them visible to other packages. 15 | .library( 16 | name: "Readability", 17 | targets: ["Readability"] 18 | ), 19 | .library( 20 | name: "ReadabilityUI", 21 | targets: ["ReadabilityUI"] 22 | ), 23 | ], 24 | targets: [ 25 | // Targets are the basic building blocks of a package, defining a module or a test suite. 26 | // Targets can depend on other targets in this package and products from dependencies. 27 | .target( 28 | name: "Readability", 29 | dependencies: [ 30 | "ReadabilityCore", 31 | ], 32 | resources: [ 33 | .process("Resources"), 34 | ], 35 | swiftSettings: [ 36 | .swiftLanguageMode(.v6), 37 | ] 38 | ), 39 | .target( 40 | name: "ReadabilityUI", 41 | dependencies: [ 42 | "ReadabilityCore", 43 | ], 44 | resources: [ 45 | .process("Resources"), 46 | ], 47 | swiftSettings: [ 48 | .swiftLanguageMode(.v6), 49 | ] 50 | ), 51 | .target( 52 | name: "ReadabilityCore", 53 | swiftSettings: [ 54 | .swiftLanguageMode(.v6), 55 | ] 56 | ), 57 | .testTarget( 58 | name: "ReadabilityTests", 59 | dependencies: ["Readability"] 60 | ), 61 | ] 62 | ) 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Readability 2 | A Swift library that wraps [@mozilla/readability](https://github.com/@mozilla/readability) and generalizes the Firefox Reader, which enhances web pages for better reading. 3 | This library provides a seamless way to detect, parse, and display reader-friendly content from any web page by integrating with WKWebView. 4 | 5 | ![Language:Swift](https://img.shields.io/static/v1?label=Language&message=Swift&color=orange&style=flat-square) 6 | ![License:MIT](https://img.shields.io/static/v1?label=License&message=MIT&color=blue&style=flat-square) 7 | [![Latest Release](https://img.shields.io/github/v/release/Ryu0118/swift-readability?style=flat-square)](https://github.com/Ryu0118/swift-readability/releases/latest) 8 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FRyu0118%2Fswift-readability%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/Ryu0118/swift-readability) 9 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FRyu0118%2Fswift-readability%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/Ryu0118/swift-readability) 10 | [![X](https://img.shields.io/twitter/follow/ryu_hu03?style=social)](https://x.com/ryu_hu03) 11 | 12 | 13 | | light | dark | sepia | 14 | | ---- | ---- | ---- | 15 | | | | | 16 | 17 | 18 | ## Features 19 | - **Parsing**
20 | Parse a URL or HTML string into a structured article using [@mozilla/readability](https://github.com/@mozilla/readability). 21 | - **WKWebView Integration**
22 | Easily integrate with WKWebView. 23 | - **Reader Mode Overlay**
24 | Easily toggle a reader overlay with customizable themes and font sizes. 25 | 26 | ## Requirements 27 | 28 | - **Swift:** 6.0 or later 29 | - **Xcode:** 16.0 or later 30 | 31 | ## Installation 32 | swift-readability is available via the Swift Package Manager 33 | ```Swift 34 | .package(url: "https://github.com/Ryu0118/swift-readability", exact: "0.1.0") 35 | ``` 36 | 37 | ## Usage 38 | ### Basic Parsing 39 | You can parse an article either from a URL or directly from an HTML string.
40 | 41 | Parsing from a URL: 42 | ```swift 43 | import Readability 44 | 45 | let readability = Readability() 46 | let result = try await readability.parse(url: URL(string: "https://example.com/article")!) 47 | ``` 48 | 49 | Parsing from an HTML string: 50 | ```swift 51 | import Readability 52 | 53 | let html = """ 54 | 55 | 56 | 57 | """ 58 | let result = try await readability.parse(html: html) 59 | ``` 60 | 61 | ### Implementing Reader Mode with WKWebView 62 | swift-readability provides `ReadabilityWebCoordinator` that prepares a `WKWebView` configuration, and exposes two asynchronous streams: `contentParsed` (emitting generated reader HTML) and `availabilityChanged` (emitting reader mode availability updates). This configuration enables your `WKWebView` to detect when a web page is suitable for reader mode, generate a reader-friendly HTML overlay, and toggle reader mode dynamically. 63 | 64 | ```swift 65 | import ReadabilityUI 66 | 67 | let coordinator = ReadabilityWebCoordinator(initialStyle: ReaderStyle(theme: .dark, fontSize: .size5)) 68 | let configuration = try await coordinator.createReadableWebViewConfiguration() 69 | let webView = WKWebView(frame: .zero, configuration: configuration) 70 | 71 | // Process generated reader HTML asynchronously. 72 | for await html in coordinator.contentParsed { 73 | do { 74 | try await webView.showReaderContent(with: html) 75 | } catch { 76 | // Handle the error here. 77 | } 78 | } 79 | 80 | // Monitor reader mode availability asynchronously. 81 | for await availability in coordinator.availabilityChanged { 82 | // For example, update your UI to enable or disable the reader mode button. 83 | } 84 | ``` 85 | 86 | ### ReaderControllable Protocol 87 | 88 | Below are usage examples for each of the functions provided by the `ReaderControllable` protocol extension. Since `WKWebView` conforms to `ReaderControllable`, you can call these methods directly on your `WKWebView` instance. 89 | 90 | > [!WARNING] 91 | > Changes to the reader style (theme and font size) are only available when the web view is in Reader Mode. 92 | 93 | ```swift 94 | import ReadabilityUI 95 | 96 | // Set the entire reader style (theme and font size) 97 | try await webView.set(style: ReaderStyle(theme: .dark, fontSize: .size5)) 98 | 99 | // Set only the reader theme (supports sepia, light, and dark). 100 | try await webView.set(theme: .sepia) 101 | 102 | // Set only the font size 103 | try await webView.set(fontSize: .size7) 104 | 105 | // Show the reader overlay using the HTML received from the ReadabilityWebCoordinator.contentParsed(_:) event. 106 | try await webView.showReaderContent(with: html) 107 | 108 | // Hide the reader overlay. 109 | try await webView.hideReaderContent() 110 | 111 | // Determine if the web view is currently in reader mode. 112 | let isReaderMode = try await webView.isReaderMode() 113 | ``` 114 | 115 | If you are using a SwiftUI wrapper library for WKWebView (such as [Cybozu/WebUI](https://github.com/cybozu/WebUI)) that does not expose the WKWebView instance, you can conform any object that has an evaluateJavaScript method to ReaderControllable. For example: 116 | ```swift 117 | import WebUI 118 | import ReadabilityUI 119 | 120 | extension WebViewProxy: @retroactive ReaderControllable { 121 | public func evaluateJavaScript(_ javascriptString: String) async throws -> Any { 122 | let result: Any? = try await evaluateJavaScript(javascriptString) 123 | return result ?? () 124 | } 125 | } 126 | ``` 127 | By conforming `WebViewProxy` to `ReaderControllable`, you can control the reader from the proxy, for example: 128 | ```swift 129 | WebViewReader { proxy in 130 | WebView(configuration: configuration) 131 | .task { 132 | for await html in coordinator.contentParsed { 133 | if let url = proxy.url { 134 | try? await proxy.showReaderContent(with: html) 135 | try? await proxy.set(theme: .dark) 136 | try? await proxy.set(fontSize: .size8) 137 | } 138 | } 139 | } 140 | } 141 | ``` 142 | 143 | ## Example (Integrating with SwiftUI) 144 | For a more detailed implementation of integrating swift-readability with SwiftUI using [Cybozu/WebUI](https://github.com/cybozu/WebUI), please refer to the [Example](./Example) provided in this repository. 145 | 146 | ## Build 147 | Before building the Swift Package, please complete the following steps: 148 | 1. Verify that npm is installed. 149 | 2. Then, run `make bootstrap`. 150 | 151 |
If you modify the source code in the webpack-resources folder, please run `npm run build` 152 | 153 | ## Credits 154 | This project leverages several open source projects: 155 | 156 | - [@mozilla/readability](https://github.com/mozilla/readability) for parsing web pages and generating reader-friendly content (licensed under the MIT License). 157 | - [mozilla-mobile/firefox-ios](https://github.com/mozilla-mobile/firefox-ios) for inspiration on Reader Mode functionality (licensed under the MPL 2.0). 158 | - [Cybozu/WebUI](https://github.com/Cybozu/WebUI) for the SwiftUI integration example (licensed under the MIT License). 159 | - [cure53/DOMPurify](https://github.com/cure53/DOMPurify) for sanitizing HTML content (licensed under the MIT License). 160 | 161 | In addition, the following files are distributed under the Mozilla Public License, Version 2.0 (MPL 2.0): 162 | - [webpack-resources/AtDocumentStart.js](./webpack-resources/AtDocumentStart.js) 163 | - [webpack-resources/ReadabilitySanitized.js](./webpack-resources/ReadabilitySanitized.js) 164 | - [webpack-resources/Reader.html](./webpack-resources/Reader.html) 165 | - [webpack-resources/Reader.css](./webpack-resources/Reader.css) 166 | - [Sources/ReadabilityUI/Resources/AtDocumentStart.js](./Sources/ReadabilityUI/Resources/AtDocumentStart.js) 167 | - [Sources/ReadabilityUI/Resources/Reader.html](./Sources/ReadabilityUI/Resources/Reader.html) 168 | - [Sources/Readability/Resources/ReadabilitySanitized.js](./Sources/Readability/Resources/ReadabilitySanitized.js) 169 | - [Sources/ReadabilityCore/ReaderStyle.swift](./Sources/ReadabilityCore/ReaderStyle.swift) 170 | -------------------------------------------------------------------------------- /Sources/Readability/Internal/HTMLFetcher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct HTMLFetcher { 4 | enum Error: LocalizedError { 5 | case noValidEncodingFound 6 | 7 | var errorDescription: String? { 8 | switch self { 9 | case .noValidEncodingFound: 10 | "No valid encoding found" 11 | } 12 | } 13 | } 14 | 15 | private let session = URLSession.shared 16 | 17 | func fetch(url: URL) async throws -> String { 18 | let (htmlData, _) = try await URLSession.shared.data(from: url) 19 | let encodings: [String.Encoding] = [ 20 | .utf8, 21 | .shiftJIS, 22 | .ascii, 23 | .utf16, 24 | .utf16LittleEndian, 25 | .utf32, 26 | .utf32LittleEndian, 27 | .isoLatin1, 28 | .japaneseEUC, 29 | .windowsCP1250, 30 | .windowsCP1251, 31 | .windowsCP1252, 32 | ] 33 | 34 | var html: String? 35 | for encoding in encodings { 36 | if let string = String(data: htmlData, encoding: encoding) { 37 | html = string 38 | break 39 | } 40 | } 41 | 42 | guard let html else { 43 | throw Error.noValidEncodingFound 44 | } 45 | 46 | return html 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/Readability/Internal/ReadabilityRunner.swift: -------------------------------------------------------------------------------- 1 | import ReadabilityCore 2 | import SwiftUI 3 | import WebKit 4 | 5 | /// A runner class responsible for processing HTML content and producing a `ReadabilityResult`. 6 | /// This class uses a WKWebView to load HTML and execute JavaScript for parsing. 7 | @MainActor 8 | final class ReadabilityRunner { 9 | private let webView: WKWebView 10 | 11 | // The message handler that listens for events from the injected JavaScript. 12 | private weak var messageHandler: ReadabilityMessageHandler? 13 | // The script loader for fetching JavaScript resources from the bundle. 14 | private let scriptLoader = ScriptLoader(bundle: .module) 15 | 16 | private let encoder = JSONEncoder() 17 | 18 | init() { 19 | let configuration = WKWebViewConfiguration() 20 | let messageHandler = ReadabilityMessageHandler( 21 | mode: .generateReadabilityResult, 22 | readerContentGenerator: EmptyContentGenerator() 23 | ) 24 | 25 | configuration.userContentController.add(messageHandler, name: "readabilityMessageHandler") 26 | 27 | self.messageHandler = messageHandler 28 | webView = WKWebView(frame: .zero, configuration: configuration) 29 | } 30 | 31 | func parseHTML( 32 | _ html: String, 33 | options: Readability.Options?, 34 | baseURL: URL? = nil 35 | ) async throws -> ReadabilityResult { 36 | let shouldSanitize = options?.shouldSanitize ?? false 37 | let script = try await scriptLoader 38 | .load(shouldSanitize ? .readabilitySanitized : .readabilityBasic) 39 | .replacingOccurrences( 40 | of: "__READABILITY_OPTION__", 41 | with: generateJSONOptions(options: options) 42 | ) 43 | 44 | let endScript = WKUserScript( 45 | source: script, 46 | injectionTime: .atDocumentEnd, 47 | forMainFrameOnly: true 48 | ) 49 | 50 | webView.configuration.userContentController.addUserScript(endScript) 51 | webView.loadHTMLString(html, baseURL: baseURL) 52 | 53 | return try await withCheckedThrowingContinuation { [weak self] continuation in 54 | self?.messageHandler?.subscribeEvent { event in 55 | switch event { 56 | case let .contentParsed(readabilityResult): 57 | continuation.resume(returning: readabilityResult) 58 | self?.messageHandler?.subscribeEvent(nil) 59 | case let .availabilityChanged(availability): 60 | if availability == .unavailable { 61 | continuation.resume(throwing: Error.readerIsUnavailable) 62 | self?.messageHandler?.subscribeEvent(nil) 63 | } 64 | default: 65 | break 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | extension ReadabilityRunner { 73 | private func generateJSONOptions(options: Readability.Options?) throws -> String { 74 | if let options = options { 75 | let data = try encoder.encode(options) 76 | return String(data: data, encoding: .utf8) ?? "{}" 77 | } else { 78 | return "{}" 79 | } 80 | } 81 | } 82 | 83 | extension ReadabilityRunner { 84 | /// Errors that can occur during HTML parsing. 85 | enum Error: Swift.Error { 86 | /// Indicates that the reader became unavailable during parsing. 87 | case readerIsUnavailable 88 | } 89 | } 90 | 91 | /// A placeholder content generator that conforms to `ReaderContentGeneratable` and does not generate any content. 92 | private struct EmptyContentGenerator: ReaderContentGeneratable { 93 | func generate(_: ReadabilityResult, initialStyle _: ReaderStyle) async -> String? { 94 | nil 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/Readability/Options.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Readability { 4 | /// Options for configuring Mozilla Readability. 5 | /// 6 | /// Corresponds to the `options` object in: 7 | /// ``` 8 | /// new Readability(document, options) 9 | /// ``` 10 | struct Options: Encodable, Sendable { 11 | /// Whether to enable debugging/logging. 12 | /// - Default: `false` 13 | public var debug: Bool 14 | 15 | /// The maximum number of DOM elements to parse. 16 | /// `0` means no limit. 17 | /// - Default: `0` (no limit) 18 | public var maxElemsToParse: Int 19 | 20 | /// The number of top candidates to consider when analysing how tight 21 | /// the competition is among candidates. 22 | /// - Default: `5` 23 | public var nbTopCandidates: Int 24 | 25 | /// The minimum number of characters an article must have in order 26 | /// for Readability to return a result. 27 | /// - Default: `500` 28 | public var charThreshold: Int 29 | 30 | /// When `keepClasses` is `false`, only classes in this list are preserved. 31 | /// - Default: `[]` (no classes preserved) 32 | public var classesToPreserve: [String] 33 | 34 | /// Whether to preserve all classes on HTML elements. 35 | /// If `false`, only the classes in `classesToPreserve` are kept. 36 | /// - Default: `false` 37 | public var keepClasses: Bool 38 | 39 | /// Whether to skip JSON-LD parsing. 40 | /// If `true`, metadata from JSON-LD is ignored. 41 | /// - Default: `false` 42 | public var disableJSONLD: Bool 43 | 44 | /// Controls how the content property is produced from the root DOM element. 45 | /// By default (`el => el.innerHTML` in JS) it returns an HTML string. 46 | /// In Swift, a direct function pointer to JavaScript is not straightforward, 47 | /// so we store a string or some identifier for how we want it serialized. 48 | /// 49 | /// - Default: `nil` (uses the default serializer `el.innerHTML`) 50 | public var serializer: String? 51 | 52 | /// A regular expression (as a string) that matches video URLs to be allowed. 53 | /// If `nil`, the default regex is applied on the JS side. 54 | /// - Default: `nil` 55 | public var allowedVideoRegex: String? 56 | 57 | /// A number added to the base link density threshold during "shadiness" checks. 58 | /// This can be used to penalize or reward nodes with high link density. 59 | /// - Default: `0` 60 | public var linkDensityModifier: Double 61 | 62 | /// Whether to sanitize the document before parsing. 63 | /// If `true`, the document will be sanitized using DOMPurify before Readability parsing. 64 | /// - Default: `false` 65 | public var shouldSanitize: Bool 66 | 67 | public init( 68 | debug: Bool = false, 69 | maxElemsToParse: Int = 0, 70 | nbTopCandidates: Int = 5, 71 | charThreshold: Int = 500, 72 | classesToPreserve: [String] = [], 73 | keepClasses: Bool = false, 74 | disableJSONLD: Bool = false, 75 | serializer: String? = nil, 76 | allowedVideoRegex: String? = nil, 77 | linkDensityModifier: Double = 0, 78 | shouldSanitize: Bool = false 79 | ) { 80 | self.debug = debug 81 | self.maxElemsToParse = maxElemsToParse 82 | self.nbTopCandidates = nbTopCandidates 83 | self.charThreshold = charThreshold 84 | self.classesToPreserve = classesToPreserve 85 | self.keepClasses = keepClasses 86 | self.disableJSONLD = disableJSONLD 87 | self.serializer = serializer 88 | self.allowedVideoRegex = allowedVideoRegex 89 | self.linkDensityModifier = linkDensityModifier 90 | self.shouldSanitize = shouldSanitize 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/Readability/Readability.swift: -------------------------------------------------------------------------------- 1 | import ReadabilityCore 2 | import SwiftUI 3 | import WebKit 4 | 5 | /// A public interface for parsing web pages using Mozilla's Readability library. 6 | /// This struct provides asynchronous methods to parse HTML or a URL into a structured `ReadabilityResult`. 7 | @MainActor 8 | public struct Readability { 9 | private let runner: ReadabilityRunner 10 | 11 | public init() { 12 | runner = ReadabilityRunner() 13 | } 14 | 15 | /// Parses the web page at the specified URL and returns a `ReadabilityResult`. 16 | /// 17 | /// - Parameters: 18 | /// - url: The URL of the web page to parse. 19 | /// - options: Optional configuration options for parsing. 20 | /// - Returns: A `ReadabilityResult` containing the parsed content. 21 | /// - Throws: An error if fetching or parsing fails. 22 | public func parse( 23 | url: URL, 24 | options: Readability.Options? = nil 25 | ) async throws -> ReadabilityResult { 26 | // Fetch HTML content from the URL. 27 | let html = try await HTMLFetcher().fetch(url: url) 28 | // Parse the fetched HTML content. 29 | return try await parse( 30 | html: html, 31 | options: options, 32 | baseURL: nil 33 | ) 34 | } 35 | 36 | /// Parses the provided HTML string and returns a `ReadabilityResult`. 37 | /// 38 | /// - Parameters: 39 | /// - html: The HTML content to parse. 40 | /// - options: Optional configuration options for parsing. 41 | /// - baseURL: The base URL for the HTML content. 42 | /// - Returns: A `ReadabilityResult` containing the parsed content. 43 | /// - Throws: An error if parsing fails. 44 | public func parse( 45 | html: String, 46 | options: Readability.Options?, 47 | baseURL: URL? 48 | ) async throws -> ReadabilityResult { 49 | try await runner.parseHTML( 50 | html, 51 | options: options, 52 | baseURL: baseURL 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Readability/Resources/ReadabilityBasic.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={804:e=>{var t={unlikelyCandidates:/-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,okMaybeItsACandidate:/and|article|body|column|content|main|shadow/i};function i(e){return(!e.style||"none"!=e.style.display)&&!e.hasAttribute("hidden")&&(!e.hasAttribute("aria-hidden")||"true"!=e.getAttribute("aria-hidden")||e.className&&e.className.indexOf&&-1!==e.className.indexOf("fallback-image"))}e.exports=function(e,a={}){"function"==typeof a&&(a={visibilityChecker:a});var r={minScore:20,minContentLength:140,visibilityChecker:i};a=Object.assign(r,a);var n=e.querySelectorAll("p, pre, article"),s=e.querySelectorAll("div > br");if(s.length){var l=new Set(n);[].forEach.call(s,(function(e){l.add(e.parentNode)})),n=Array.from(l)}var o=0;return[].some.call(n,(function(e){if(!a.visibilityChecker(e))return!1;var i=e.className+" "+e.id;if(t.unlikelyCandidates.test(i)&&!t.okMaybeItsACandidate.test(i))return!1;if(e.matches("li p"))return!1;var r=e.textContent.trim().length;return!(ra.minScore}))}},238:e=>{function t(e,t){if(t&&t.documentElement)e=t,t=arguments[2];else if(!e||!e.documentElement)throw new Error("First argument to Readability constructor should be a document object.");if(t=t||{},this._doc=e,this._docJSDOMParser=this._doc.firstChild.__JSDOMParser__,this._articleTitle=null,this._articleByline=null,this._articleDir=null,this._articleSiteName=null,this._attempts=[],this._debug=!!t.debug,this._maxElemsToParse=t.maxElemsToParse||this.DEFAULT_MAX_ELEMS_TO_PARSE,this._nbTopCandidates=t.nbTopCandidates||this.DEFAULT_N_TOP_CANDIDATES,this._charThreshold=t.charThreshold||this.DEFAULT_CHAR_THRESHOLD,this._classesToPreserve=this.CLASSES_TO_PRESERVE.concat(t.classesToPreserve||[]),this._keepClasses=!!t.keepClasses,this._serializer=t.serializer||function(e){return e.innerHTML},this._disableJSONLD=!!t.disableJSONLD,this._allowedVideoRegex=t.allowedVideoRegex||this.REGEXPS.videos,this._flags=this.FLAG_STRIP_UNLIKELYS|this.FLAG_WEIGHT_CLASSES|this.FLAG_CLEAN_CONDITIONALLY,this._debug){let e=function(e){if(e.nodeType==e.TEXT_NODE)return`${e.nodeName} ("${e.textContent}")`;let t=Array.from(e.attributes||[],(function(e){return`${e.name}="${e.value}"`})).join(" ");return`<${e.localName} ${t}>`};this.log=function(){if("undefined"!=typeof console){let t=Array.from(arguments,(t=>t&&t.nodeType==this.ELEMENT_NODE?e(t):t));t.unshift("Reader: (Readability)"),console.log.apply(console,t)}else if("undefined"!=typeof dump){var t=Array.prototype.map.call(arguments,(function(t){return t&&t.nodeName?e(t):t})).join(" ");dump("Reader: (Readability) "+t+"\n")}}}else this.log=function(){}}t.prototype={FLAG_STRIP_UNLIKELYS:1,FLAG_WEIGHT_CLASSES:2,FLAG_CLEAN_CONDITIONALLY:4,ELEMENT_NODE:1,TEXT_NODE:3,DEFAULT_MAX_ELEMS_TO_PARSE:0,DEFAULT_N_TOP_CANDIDATES:5,DEFAULT_TAGS_TO_SCORE:"section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","),DEFAULT_CHAR_THRESHOLD:500,REGEXPS:{unlikelyCandidates:/-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,okMaybeItsACandidate:/and|article|body|column|content|main|shadow/i,positive:/article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,negative:/-ad-|hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,extraneous:/print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,byline:/byline|author|dateline|writtenby|p-author/i,replaceFonts:/<(\/?)font[^>]*>/gi,normalize:/\s{2,}/g,videos:/\/\/(www\.)?((dailymotion|youtube|youtube-nocookie|player\.vimeo|v\.qq)\.com|(archive|upload\.wikimedia)\.org|player\.twitch\.tv)/i,shareElements:/(\b|_)(share|sharedaddy)(\b|_)/i,nextLink:/(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,prevLink:/(prev|earl|old|new|<|«)/i,tokenize:/\W+/g,whitespace:/^\s*$/,hasContent:/\S$/,hashUrl:/^#.+/,srcsetUrl:/(\S+)(\s+[\d.]+[xw])?(\s*(?:,|$))/g,b64DataUrl:/^data:\s*([^\s;,]+)\s*;\s*base64\s*,/i,commas:/\u002C|\u060C|\uFE50|\uFE10|\uFE11|\u2E41|\u2E34|\u2E32|\uFF0C/g,jsonLdArticleTypes:/^Article|AdvertiserContentArticle|NewsArticle|AnalysisNewsArticle|AskPublicNewsArticle|BackgroundNewsArticle|OpinionNewsArticle|ReportageNewsArticle|ReviewNewsArticle|Report|SatiricalArticle|ScholarlyArticle|MedicalScholarlyArticle|SocialMediaPosting|BlogPosting|LiveBlogPosting|DiscussionForumPosting|TechArticle|APIReference$/},UNLIKELY_ROLES:["menu","menubar","complementary","navigation","alert","alertdialog","dialog"],DIV_TO_P_ELEMS:new Set(["BLOCKQUOTE","DL","DIV","IMG","OL","P","PRE","TABLE","UL"]),ALTER_TO_DIV_EXCEPTIONS:["DIV","ARTICLE","SECTION","P"],PRESENTATIONAL_ATTRIBUTES:["align","background","bgcolor","border","cellpadding","cellspacing","frame","hspace","rules","style","valign","vspace"],DEPRECATED_SIZE_ATTRIBUTE_ELEMS:["TABLE","TH","TD","HR","PRE"],PHRASING_ELEMS:["ABBR","AUDIO","B","BDO","BR","BUTTON","CITE","CODE","DATA","DATALIST","DFN","EM","EMBED","I","IMG","INPUT","KBD","LABEL","MARK","MATH","METER","NOSCRIPT","OBJECT","OUTPUT","PROGRESS","Q","RUBY","SAMP","SCRIPT","SELECT","SMALL","SPAN","STRONG","SUB","SUP","TEXTAREA","TIME","VAR","WBR"],CLASSES_TO_PRESERVE:["page"],HTML_ESCAPE_MAP:{lt:"<",gt:">",amp:"&",quot:'"',apos:"'"},_postProcessContent:function(e){this._fixRelativeUris(e),this._simplifyNestedElements(e),this._keepClasses||this._cleanClasses(e)},_removeNodes:function(e,t){if(this._docJSDOMParser&&e._isLiveNodeList)throw new Error("Do not pass live node lists to _removeNodes");for(var i=e.length-1;i>=0;i--){var a=e[i],r=a.parentNode;r&&(t&&!t.call(this,a,i,e)||r.removeChild(a))}},_replaceNodeTags:function(e,t){if(this._docJSDOMParser&&e._isLiveNodeList)throw new Error("Do not pass live node lists to _replaceNodeTags");for(const i of e)this._setNodeTag(i,t)},_forEachNode:function(e,t){Array.prototype.forEach.call(e,t,this)},_findNode:function(e,t){return Array.prototype.find.call(e,t,this)},_someNode:function(e,t){return Array.prototype.some.call(e,t,this)},_everyNode:function(e,t){return Array.prototype.every.call(e,t,this)},_concatNodeLists:function(){var e=Array.prototype.slice,t=e.call(arguments).map((function(t){return e.call(t)}));return Array.prototype.concat.apply([],t)},_getAllNodesWithTag:function(e,t){return e.querySelectorAll?e.querySelectorAll(t.join(",")):[].concat.apply([],t.map((function(t){var i=e.getElementsByTagName(t);return Array.isArray(i)?i:Array.from(i)})))},_cleanClasses:function(e){var t=this._classesToPreserve,i=(e.getAttribute("class")||"").split(/\s+/).filter((function(e){return-1!=t.indexOf(e)})).join(" ");for(i?e.setAttribute("class",i):e.removeAttribute("class"),e=e.firstElementChild;e;e=e.nextElementSibling)this._cleanClasses(e)},_fixRelativeUris:function(e){var t=this._doc.baseURI,i=this._doc.documentURI;function a(e){if(t==i&&"#"==e.charAt(0))return e;try{return new URL(e,t).href}catch(e){}return e}var r=this._getAllNodesWithTag(e,["a"]);this._forEachNode(r,(function(e){var t=e.getAttribute("href");if(t)if(0===t.indexOf("javascript:"))if(1===e.childNodes.length&&e.childNodes[0].nodeType===this.TEXT_NODE){var i=this._doc.createTextNode(e.textContent);e.parentNode.replaceChild(i,e)}else{for(var r=this._doc.createElement("span");e.firstChild;)r.appendChild(e.firstChild);e.parentNode.replaceChild(r,e)}else e.setAttribute("href",a(t))}));var n=this._getAllNodesWithTag(e,["img","picture","figure","video","audio","source"]);this._forEachNode(n,(function(e){var t=e.getAttribute("src"),i=e.getAttribute("poster"),r=e.getAttribute("srcset");if(t&&e.setAttribute("src",a(t)),i&&e.setAttribute("poster",a(i)),r){var n=r.replace(this.REGEXPS.srcsetUrl,(function(e,t,i,r){return a(t)+(i||"")+r}));e.setAttribute("srcset",n)}}))},_simplifyNestedElements:function(e){for(var t=e;t;){if(t.parentNode&&["DIV","SECTION"].includes(t.tagName)&&(!t.id||!t.id.startsWith("readability"))){if(this._isElementWithoutContent(t)){t=this._removeAndGetNext(t);continue}if(this._hasSingleTagInsideElement(t,"DIV")||this._hasSingleTagInsideElement(t,"SECTION")){for(var i=t.children[0],a=0;a»] /.test(t))a=/ [\\\/>»] /.test(t),r(t=i.replace(/(.*)[\|\-\\\/>»] .*/gi,"$1"))<3&&(t=i.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi,"$1"));else if(-1!==t.indexOf(": ")){var n=this._concatNodeLists(e.getElementsByTagName("h1"),e.getElementsByTagName("h2")),s=t.trim();this._someNode(n,(function(e){return e.textContent.trim()===s}))||(r(t=i.substring(i.lastIndexOf(":")+1))<3?t=i.substring(i.indexOf(":")+1):r(i.substr(0,i.indexOf(":")))>5&&(t=i))}else if(t.length>150||t.length<15){var l=e.getElementsByTagName("h1");1===l.length&&(t=this._getInnerText(l[0]))}var o=r(t=t.trim().replace(this.REGEXPS.normalize," "));return o<=4&&(!a||o!=r(i.replace(/[\|\-\\\/>»]+/g,""))-1)&&(t=i),t},_prepDocument:function(){var e=this._doc;this._removeNodes(this._getAllNodesWithTag(e,["style"])),e.body&&this._replaceBrs(e.body),this._replaceNodeTags(this._getAllNodesWithTag(e,["font"]),"SPAN")},_nextNode:function(e){for(var t=e;t&&t.nodeType!=this.ELEMENT_NODE&&this.REGEXPS.whitespace.test(t.textContent);)t=t.nextSibling;return t},_replaceBrs:function(e){this._forEachNode(this._getAllNodesWithTag(e,["br"]),(function(e){for(var t=e.nextSibling,i=!1;(t=this._nextNode(t))&&"BR"==t.tagName;){i=!0;var a=t.nextSibling;t.parentNode.removeChild(t),t=a}if(i){var r=this._doc.createElement("p");for(e.parentNode.replaceChild(r,e),t=r.nextSibling;t;){if("BR"==t.tagName){var n=this._nextNode(t.nextSibling);if(n&&"BR"==n.tagName)break}if(!this._isPhrasingContent(t))break;var s=t.nextSibling;r.appendChild(t),t=s}for(;r.lastChild&&this._isWhitespace(r.lastChild);)r.removeChild(r.lastChild);"P"===r.parentNode.tagName&&this._setNodeTag(r.parentNode,"DIV")}}))},_setNodeTag:function(e,t){if(this.log("_setNodeTag",e,t),this._docJSDOMParser)return e.localName=t.toLowerCase(),e.tagName=t.toUpperCase(),e;for(var i=e.ownerDocument.createElement(t);e.firstChild;)i.appendChild(e.firstChild);e.parentNode.replaceChild(i,e),e.readability&&(i.readability=e.readability);for(var a=0;a!i.includes(e))).join(" ").length/a.join(" ").length:0},_checkByline:function(e,t){if(this._articleByline)return!1;if(void 0!==e.getAttribute)var i=e.getAttribute("rel"),a=e.getAttribute("itemprop");return!(!("author"===i||a&&-1!==a.indexOf("author")||this.REGEXPS.byline.test(t))||!this._isValidByline(e.textContent)||(this._articleByline=e.textContent.trim(),0))},_getNodeAncestors:function(e,t){t=t||0;for(var i=0,a=[];e.parentNode&&(a.push(e.parentNode),!t||++i!==t);)e=e.parentNode;return a},_grabArticle:function(e){this.log("**** grabArticle ****");var t=this._doc,i=null!==e;if(!(e=e||this._doc.body))return this.log("No body found in document. Abort."),null;for(var a=e.innerHTML;;){this.log("Starting grabArticle loop");var r=this._flagIsActive(this.FLAG_STRIP_UNLIKELYS),n=[],s=this._doc.documentElement;let V=!0;for(;s;){"HTML"===s.tagName&&(this._articleLang=s.getAttribute("lang"));var l=s.className+" "+s.id;if(this._isProbablyVisible(s))if("true"!=s.getAttribute("aria-modal")||"dialog"!=s.getAttribute("role"))if(this._checkByline(s,l))s=this._removeAndGetNext(s);else if(V&&this._headerDuplicatesTitle(s))this.log("Removing header: ",s.textContent.trim(),this._articleTitle.trim()),V=!1,s=this._removeAndGetNext(s);else{if(r){if(this.REGEXPS.unlikelyCandidates.test(l)&&!this.REGEXPS.okMaybeItsACandidate.test(l)&&!this._hasAncestorTag(s,"table")&&!this._hasAncestorTag(s,"code")&&"BODY"!==s.tagName&&"A"!==s.tagName){this.log("Removing unlikely candidate - "+l),s=this._removeAndGetNext(s);continue}if(this.UNLIKELY_ROLES.includes(s.getAttribute("role"))){this.log("Removing content with role "+s.getAttribute("role")+" - "+l),s=this._removeAndGetNext(s);continue}}if("DIV"!==s.tagName&&"SECTION"!==s.tagName&&"HEADER"!==s.tagName&&"H1"!==s.tagName&&"H2"!==s.tagName&&"H3"!==s.tagName&&"H4"!==s.tagName&&"H5"!==s.tagName&&"H6"!==s.tagName||!this._isElementWithoutContent(s)){if(-1!==this.DEFAULT_TAGS_TO_SCORE.indexOf(s.tagName)&&n.push(s),"DIV"===s.tagName){for(var o=null,h=s.firstChild;h;){var c=h.nextSibling;if(this._isPhrasingContent(h))null!==o?o.appendChild(h):this._isWhitespace(h)||(o=t.createElement("p"),s.replaceChild(o,h),o.appendChild(h));else if(null!==o){for(;o.lastChild&&this._isWhitespace(o.lastChild);)o.removeChild(o.lastChild);o=null}h=c}if(this._hasSingleTagInsideElement(s,"P")&&this._getLinkDensity(s)<.25){var d=s.children[0];s.parentNode.replaceChild(d,s),s=d,n.push(s)}else this._hasChildBlockElement(s)||(s=this._setNodeTag(s,"P"),n.push(s))}s=this._getNextNode(s)}else s=this._removeAndGetNext(s)}else s=this._removeAndGetNext(s);else this.log("Removing hidden node - "+l),s=this._removeAndGetNext(s)}var g=[];this._forEachNode(n,(function(e){if(e.parentNode&&void 0!==e.parentNode.tagName){var t=this._getInnerText(e);if(!(t.length<25)){var i=this._getNodeAncestors(e,5);if(0!==i.length){var a=0;a+=1,a+=t.split(this.REGEXPS.commas).length,a+=Math.min(Math.floor(t.length/100),3),this._forEachNode(i,(function(e,t){if(e.tagName&&e.parentNode&&void 0!==e.parentNode.tagName){if(void 0===e.readability&&(this._initializeNode(e),g.push(e)),0===t)var i=1;else i=1===t?2:3*t;e.readability.contentScore+=a/i}}))}}}}));for(var u=[],m=0,_=g.length;m<_;m+=1){var f=g[m],p=f.readability.contentScore*(1-this._getLinkDensity(f));f.readability.contentScore=p,this.log("Candidate:",f,"with score "+p);for(var N=0;NE.readability.contentScore){u.splice(N,0,f),u.length>this._nbTopCandidates&&u.pop();break}}}var b,T=u[0]||null,y=!1;if(null===T||"BODY"===T.tagName){for(T=t.createElement("DIV"),y=!0;e.firstChild;)this.log("Moving child out:",e.firstChild),T.appendChild(e.firstChild);e.appendChild(T),this._initializeNode(T)}else if(T){for(var v=[],A=1;A=.75&&v.push(this._getNodeAncestors(u[A]));if(v.length>=3)for(b=T.parentNode;"BODY"!==b.tagName;){for(var S=0,C=0;C=3){T=b;break}b=b.parentNode}T.readability||this._initializeNode(T),b=T.parentNode;for(var L=T.readability.contentScore,x=L/3;"BODY"!==b.tagName;)if(b.readability){var I=b.readability.contentScore;if(IL){T=b;break}L=b.readability.contentScore,b=b.parentNode}else b=b.parentNode;for(b=T.parentNode;"BODY"!=b.tagName&&1==b.children.length;)b=(T=b).parentNode;T.readability||this._initializeNode(T)}var D=t.createElement("DIV");i&&(D.id="readability-content");for(var R=Math.max(10,.2*T.readability.contentScore),O=(b=T.parentNode).children,P=0,w=O.length;P=R)M=!0;else if("P"===B.nodeName){var k=this._getLinkDensity(B),H=this._getInnerText(B),U=H.length;(U>80&&k<.25||U<80&&U>0&&0===k&&-1!==H.search(/\.( |$)/))&&(M=!0)}}M&&(this.log("Appending node:",B),-1===this.ALTER_TO_DIV_EXCEPTIONS.indexOf(B.nodeName)&&(this.log("Altering sibling:",B,"to div."),B=this._setNodeTag(B,"DIV")),D.appendChild(B),O=b.children,P-=1,w-=1)}if(this._debug&&this.log("Article content pre-prep: "+D.innerHTML),this._prepArticle(D),this._debug&&this.log("Article content post-prep: "+D.innerHTML),y)T.id="readability-page-1",T.className="page";else{var F=t.createElement("DIV");for(F.id="readability-page-1",F.className="page";D.firstChild;)F.appendChild(D.firstChild);D.appendChild(F)}this._debug&&this.log("Article content after paging: "+D.innerHTML);var W=!0,X=this._getInnerText(D,!0).length;if(X0&&e.length<100},_unescapeHtmlEntities:function(e){if(!e)return e;var t=this.HTML_ESCAPE_MAP;return e.replace(/&(quot|amp|apos|lt|gt);/g,(function(e,i){return t[i]})).replace(/&#(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi,(function(e,t,i){var a=parseInt(t||i,t?16:10);return String.fromCharCode(a)}))},_getJSONLD:function(e){var t,i=this._getAllNodesWithTag(e,["script"]);return this._forEachNode(i,(function(e){if(!t&&"application/ld+json"===e.getAttribute("type"))try{var i=e.textContent.replace(/^\s*\s*$/g,""),a=JSON.parse(i);if(!a["@context"]||!a["@context"].match(/^https?\:\/\/schema\.org$/))return;if(!a["@type"]&&Array.isArray(a["@graph"])&&(a=a["@graph"].find((function(e){return(e["@type"]||"").match(this.REGEXPS.jsonLdArticleTypes)}))),!a||!a["@type"]||!a["@type"].match(this.REGEXPS.jsonLdArticleTypes))return;if(t={},"string"==typeof a.name&&"string"==typeof a.headline&&a.name!==a.headline){var r=this._getArticleTitle(),n=this._textSimilarity(a.name,r)>.75,s=this._textSimilarity(a.headline,r)>.75;t.title=s&&!n?a.headline:a.name}else"string"==typeof a.name?t.title=a.name.trim():"string"==typeof a.headline&&(t.title=a.headline.trim());return a.author&&("string"==typeof a.author.name?t.byline=a.author.name.trim():Array.isArray(a.author)&&a.author[0]&&"string"==typeof a.author[0].name&&(t.byline=a.author.filter((function(e){return e&&"string"==typeof e.name})).map((function(e){return e.name.trim()})).join(", "))),"string"==typeof a.description&&(t.excerpt=a.description.trim()),a.publisher&&"string"==typeof a.publisher.name&&(t.siteName=a.publisher.name.trim()),void("string"==typeof a.datePublished&&(t.datePublished=a.datePublished.trim()))}catch(e){this.log(e.message)}})),t||{}},_getArticleMetadata:function(e){var t={},i={},a=this._doc.getElementsByTagName("meta"),r=/\s*(article|dc|dcterm|og|twitter)\s*:\s*(author|creator|description|published_time|title|site_name)\s*/gi,n=/^\s*(?:(dc|dcterm|og|twitter|weibo:(article|webpage))\s*[\.:]\s*)?(author|creator|description|title|site_name)\s*$/i;return this._forEachNode(a,(function(e){var t=e.getAttribute("name"),a=e.getAttribute("property"),s=e.getAttribute("content");if(s){var l=null,o=null;a&&(l=a.match(r))&&(o=l[0].toLowerCase().replace(/\s/g,""),i[o]=s.trim()),!l&&t&&n.test(t)&&(o=t,s&&(o=o.toLowerCase().replace(/\s/g,"").replace(/\./g,":"),i[o]=s.trim()))}})),t.title=e.title||i["dc:title"]||i["dcterm:title"]||i["og:title"]||i["weibo:article:title"]||i["weibo:webpage:title"]||i.title||i["twitter:title"],t.title||(t.title=this._getArticleTitle()),t.byline=e.byline||i["dc:creator"]||i["dcterm:creator"]||i.author,t.excerpt=e.excerpt||i["dc:description"]||i["dcterm:description"]||i["og:description"]||i["weibo:article:description"]||i["weibo:webpage:description"]||i.description||i["twitter:description"],t.siteName=e.siteName||i["og:site_name"],t.publishedTime=e.datePublished||i["article:published_time"]||null,t.title=this._unescapeHtmlEntities(t.title),t.byline=this._unescapeHtmlEntities(t.byline),t.excerpt=this._unescapeHtmlEntities(t.excerpt),t.siteName=this._unescapeHtmlEntities(t.siteName),t.publishedTime=this._unescapeHtmlEntities(t.publishedTime),t},_isSingleImage:function(e){return"IMG"===e.tagName||1===e.children.length&&""===e.textContent.trim()&&this._isSingleImage(e.children[0])},_unwrapNoscriptImages:function(e){var t=Array.from(e.getElementsByTagName("img"));this._forEachNode(t,(function(e){for(var t=0;t0&&r>i)return!1;if(e.parentNode.tagName===t&&(!a||a(e.parentNode)))return!0;e=e.parentNode,r++}return!1},_getRowAndColumnCount:function(e){for(var t=0,i=0,a=e.getElementsByTagName("tr"),r=0;r0)a._readabilityDataTable=!0;else if(["col","colgroup","tfoot","thead","th"].some((function(e){return!!a.getElementsByTagName(e)[0]})))this.log("Data table because found data-y descendant"),a._readabilityDataTable=!0;else if(a.getElementsByTagName("table")[0])a._readabilityDataTable=!1;else{var n=this._getRowAndColumnCount(a);n.rows>=10||n.columns>4?a._readabilityDataTable=!0:a._readabilityDataTable=n.rows*n.columns>10}}else a._readabilityDataTable=!1;else a._readabilityDataTable=!1}},_fixLazyImages:function(e){this._forEachNode(this._getAllNodesWithTag(e,["img","picture","figure"]),(function(e){if(e.src&&this.REGEXPS.b64DataUrl.test(e.src)){if("image/svg+xml"===this.REGEXPS.b64DataUrl.exec(e.src)[1])return;for(var t=!1,i=0;ia+=this._getInnerText(e,!0).length)),a/i},_cleanConditionally:function(e,t){this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)&&this._removeNodes(this._getAllNodesWithTag(e,[t]),(function(e){var i=function(e){return e._readabilityDataTable},a="ul"===t||"ol"===t;if(!a){var r=0,n=this._getAllNodesWithTag(e,["ul","ol"]);this._forEachNode(n,(e=>r+=this._getInnerText(e).length)),a=r/this._getInnerText(e).length>.9}if("table"===t&&i(e))return!1;if(this._hasAncestorTag(e,"table",-1,i))return!1;if(this._hasAncestorTag(e,"code"))return!1;var s=this._getClassWeight(e);if(this.log("Cleaning Conditionally",e),s+0<0)return!0;if(this._getCharCount(e,",")<10){for(var l=e.getElementsByTagName("p").length,o=e.getElementsByTagName("img").length,h=e.getElementsByTagName("li").length-100,c=e.getElementsByTagName("input").length,d=this._getTextDensity(e,["h1","h2","h3","h4","h5","h6"]),g=0,u=this._getAllNodesWithTag(e,["object","embed","iframe"]),m=0;m1&&l/o<.5&&!this._hasAncestorTag(e,"figure")||!a&&h>l||c>Math.floor(l/3)||!a&&d<.9&&p<25&&(0===o||o>2)&&!this._hasAncestorTag(e,"figure")||!a&&s<25&&f>.2||s>=25&&f>.5||1===g&&p<75||g>1;if(a&&N){for(var E=0;E1)return N;if(o==e.getElementsByTagName("li").length)return!1}return N}return!1}))},_cleanMatchedNodes:function(e,t){for(var i=this._getNextNode(e,!0),a=this._getNextNode(e);a&&a!=i;)a=t.call(this,a,a.className+" "+a.id)?this._removeAndGetNext(a):this._getNextNode(a)},_cleanHeaders:function(e){let t=this._getAllNodesWithTag(e,["h1","h2"]);this._removeNodes(t,(function(e){let t=this._getClassWeight(e)<0;return t&&this.log("Removing header with low class weight:",e),t}))},_headerDuplicatesTitle:function(e){if("H1"!=e.tagName&&"H2"!=e.tagName)return!1;var t=this._getInnerText(e,!1);return this.log("Evaluating similarity of header:",t,this._articleTitle),this._textSimilarity(this._articleTitle,t)>.75},_flagIsActive:function(e){return(this._flags&e)>0},_removeFlag:function(e){this._flags=this._flags&~e},_isProbablyVisible:function(e){return(!e.style||"none"!=e.style.display)&&(!e.style||"hidden"!=e.style.visibility)&&!e.hasAttribute("hidden")&&(!e.hasAttribute("aria-hidden")||"true"!=e.getAttribute("aria-hidden")||e.className&&e.className.indexOf&&-1!==e.className.indexOf("fallback-image"))},parse:function(){if(this._maxElemsToParse>0){var e=this._doc.getElementsByTagName("*").length;if(e>this._maxElemsToParse)throw new Error("Aborting parsing document; "+e+" elements found")}this._unwrapNoscriptImages(this._doc);var t=this._disableJSONLD?{}:this._getJSONLD(this._doc);this._removeScripts(this._doc),this._prepDocument();var i=this._getArticleMetadata(t);this._articleTitle=i.title;var a=this._grabArticle();if(!a)return null;if(this.log("Grabbed: "+a.innerHTML),this._postProcessContent(a),!i.excerpt){var r=a.getElementsByTagName("p");r.length>0&&(i.excerpt=r[0].textContent.trim())}var n=a.textContent;return{title:this._articleTitle,byline:i.byline||this._articleByline,dir:this._articleDir,lang:this._articleLang,content:this._serializer(a),textContent:n,length:n.length,excerpt:i.excerpt,siteName:i.siteName||this._articleSiteName,publishedTime:i.publishedTime}}},e.exports=t},396:(e,t,i)=>{var a=i(238),r=i(804);e.exports={Readability:a,isProbablyReaderable:r}}},t={};function i(a){var r=t[a];if(void 0!==r)return r.exports;var n=t[a]={exports:{}};return e[a](n,n.exports,i),n.exports}i.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return i.d(t,{a:t}),t},i.d=(e,t)=>{for(var a in t)i.o(t,a)&&!i.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";var e=i(396);function t(e){webkit.messageHandlers.readabilityMessageHandler.postMessage({Type:"StateChange",Value:e})}(0,e.isProbablyReaderable)(document)?t("Available"):t("Unavailable");var a=document.cloneNode(!0);const r=new e.Readability(a,__READABILITY_OPTION__).parse();webkit.messageHandlers.readabilityMessageHandler.postMessage({Type:"ContentParsed",Value:JSON.stringify(r)})})()})(); -------------------------------------------------------------------------------- /Sources/Readability/Resources/ReadabilitySanitized.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see ReadabilitySanitized.js.LICENSE.txt */ 2 | (()=>{var e={804:e=>{var t={unlikelyCandidates:/-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,okMaybeItsACandidate:/and|article|body|column|content|main|shadow/i};function i(e){return(!e.style||"none"!=e.style.display)&&!e.hasAttribute("hidden")&&(!e.hasAttribute("aria-hidden")||"true"!=e.getAttribute("aria-hidden")||e.className&&e.className.indexOf&&-1!==e.className.indexOf("fallback-image"))}e.exports=function(e,n={}){"function"==typeof n&&(n={visibilityChecker:n});var r={minScore:20,minContentLength:140,visibilityChecker:i};n=Object.assign(r,n);var a=e.querySelectorAll("p, pre, article"),o=e.querySelectorAll("div > br");if(o.length){var s=new Set(a);[].forEach.call(o,(function(e){s.add(e.parentNode)})),a=Array.from(s)}var l=0;return[].some.call(a,(function(e){if(!n.visibilityChecker(e))return!1;var i=e.className+" "+e.id;if(t.unlikelyCandidates.test(i)&&!t.okMaybeItsACandidate.test(i))return!1;if(e.matches("li p"))return!1;var r=e.textContent.trim().length;return!(rn.minScore}))}},238:e=>{function t(e,t){if(t&&t.documentElement)e=t,t=arguments[2];else if(!e||!e.documentElement)throw new Error("First argument to Readability constructor should be a document object.");if(t=t||{},this._doc=e,this._docJSDOMParser=this._doc.firstChild.__JSDOMParser__,this._articleTitle=null,this._articleByline=null,this._articleDir=null,this._articleSiteName=null,this._attempts=[],this._debug=!!t.debug,this._maxElemsToParse=t.maxElemsToParse||this.DEFAULT_MAX_ELEMS_TO_PARSE,this._nbTopCandidates=t.nbTopCandidates||this.DEFAULT_N_TOP_CANDIDATES,this._charThreshold=t.charThreshold||this.DEFAULT_CHAR_THRESHOLD,this._classesToPreserve=this.CLASSES_TO_PRESERVE.concat(t.classesToPreserve||[]),this._keepClasses=!!t.keepClasses,this._serializer=t.serializer||function(e){return e.innerHTML},this._disableJSONLD=!!t.disableJSONLD,this._allowedVideoRegex=t.allowedVideoRegex||this.REGEXPS.videos,this._flags=this.FLAG_STRIP_UNLIKELYS|this.FLAG_WEIGHT_CLASSES|this.FLAG_CLEAN_CONDITIONALLY,this._debug){let e=function(e){if(e.nodeType==e.TEXT_NODE)return`${e.nodeName} ("${e.textContent}")`;let t=Array.from(e.attributes||[],(function(e){return`${e.name}="${e.value}"`})).join(" ");return`<${e.localName} ${t}>`};this.log=function(){if("undefined"!=typeof console){let t=Array.from(arguments,(t=>t&&t.nodeType==this.ELEMENT_NODE?e(t):t));t.unshift("Reader: (Readability)"),console.log.apply(console,t)}else if("undefined"!=typeof dump){var t=Array.prototype.map.call(arguments,(function(t){return t&&t.nodeName?e(t):t})).join(" ");dump("Reader: (Readability) "+t+"\n")}}}else this.log=function(){}}t.prototype={FLAG_STRIP_UNLIKELYS:1,FLAG_WEIGHT_CLASSES:2,FLAG_CLEAN_CONDITIONALLY:4,ELEMENT_NODE:1,TEXT_NODE:3,DEFAULT_MAX_ELEMS_TO_PARSE:0,DEFAULT_N_TOP_CANDIDATES:5,DEFAULT_TAGS_TO_SCORE:"section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","),DEFAULT_CHAR_THRESHOLD:500,REGEXPS:{unlikelyCandidates:/-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,okMaybeItsACandidate:/and|article|body|column|content|main|shadow/i,positive:/article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,negative:/-ad-|hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,extraneous:/print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,byline:/byline|author|dateline|writtenby|p-author/i,replaceFonts:/<(\/?)font[^>]*>/gi,normalize:/\s{2,}/g,videos:/\/\/(www\.)?((dailymotion|youtube|youtube-nocookie|player\.vimeo|v\.qq)\.com|(archive|upload\.wikimedia)\.org|player\.twitch\.tv)/i,shareElements:/(\b|_)(share|sharedaddy)(\b|_)/i,nextLink:/(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,prevLink:/(prev|earl|old|new|<|«)/i,tokenize:/\W+/g,whitespace:/^\s*$/,hasContent:/\S$/,hashUrl:/^#.+/,srcsetUrl:/(\S+)(\s+[\d.]+[xw])?(\s*(?:,|$))/g,b64DataUrl:/^data:\s*([^\s;,]+)\s*;\s*base64\s*,/i,commas:/\u002C|\u060C|\uFE50|\uFE10|\uFE11|\u2E41|\u2E34|\u2E32|\uFF0C/g,jsonLdArticleTypes:/^Article|AdvertiserContentArticle|NewsArticle|AnalysisNewsArticle|AskPublicNewsArticle|BackgroundNewsArticle|OpinionNewsArticle|ReportageNewsArticle|ReviewNewsArticle|Report|SatiricalArticle|ScholarlyArticle|MedicalScholarlyArticle|SocialMediaPosting|BlogPosting|LiveBlogPosting|DiscussionForumPosting|TechArticle|APIReference$/},UNLIKELY_ROLES:["menu","menubar","complementary","navigation","alert","alertdialog","dialog"],DIV_TO_P_ELEMS:new Set(["BLOCKQUOTE","DL","DIV","IMG","OL","P","PRE","TABLE","UL"]),ALTER_TO_DIV_EXCEPTIONS:["DIV","ARTICLE","SECTION","P"],PRESENTATIONAL_ATTRIBUTES:["align","background","bgcolor","border","cellpadding","cellspacing","frame","hspace","rules","style","valign","vspace"],DEPRECATED_SIZE_ATTRIBUTE_ELEMS:["TABLE","TH","TD","HR","PRE"],PHRASING_ELEMS:["ABBR","AUDIO","B","BDO","BR","BUTTON","CITE","CODE","DATA","DATALIST","DFN","EM","EMBED","I","IMG","INPUT","KBD","LABEL","MARK","MATH","METER","NOSCRIPT","OBJECT","OUTPUT","PROGRESS","Q","RUBY","SAMP","SCRIPT","SELECT","SMALL","SPAN","STRONG","SUB","SUP","TEXTAREA","TIME","VAR","WBR"],CLASSES_TO_PRESERVE:["page"],HTML_ESCAPE_MAP:{lt:"<",gt:">",amp:"&",quot:'"',apos:"'"},_postProcessContent:function(e){this._fixRelativeUris(e),this._simplifyNestedElements(e),this._keepClasses||this._cleanClasses(e)},_removeNodes:function(e,t){if(this._docJSDOMParser&&e._isLiveNodeList)throw new Error("Do not pass live node lists to _removeNodes");for(var i=e.length-1;i>=0;i--){var n=e[i],r=n.parentNode;r&&(t&&!t.call(this,n,i,e)||r.removeChild(n))}},_replaceNodeTags:function(e,t){if(this._docJSDOMParser&&e._isLiveNodeList)throw new Error("Do not pass live node lists to _replaceNodeTags");for(const i of e)this._setNodeTag(i,t)},_forEachNode:function(e,t){Array.prototype.forEach.call(e,t,this)},_findNode:function(e,t){return Array.prototype.find.call(e,t,this)},_someNode:function(e,t){return Array.prototype.some.call(e,t,this)},_everyNode:function(e,t){return Array.prototype.every.call(e,t,this)},_concatNodeLists:function(){var e=Array.prototype.slice,t=e.call(arguments).map((function(t){return e.call(t)}));return Array.prototype.concat.apply([],t)},_getAllNodesWithTag:function(e,t){return e.querySelectorAll?e.querySelectorAll(t.join(",")):[].concat.apply([],t.map((function(t){var i=e.getElementsByTagName(t);return Array.isArray(i)?i:Array.from(i)})))},_cleanClasses:function(e){var t=this._classesToPreserve,i=(e.getAttribute("class")||"").split(/\s+/).filter((function(e){return-1!=t.indexOf(e)})).join(" ");for(i?e.setAttribute("class",i):e.removeAttribute("class"),e=e.firstElementChild;e;e=e.nextElementSibling)this._cleanClasses(e)},_fixRelativeUris:function(e){var t=this._doc.baseURI,i=this._doc.documentURI;function n(e){if(t==i&&"#"==e.charAt(0))return e;try{return new URL(e,t).href}catch(e){}return e}var r=this._getAllNodesWithTag(e,["a"]);this._forEachNode(r,(function(e){var t=e.getAttribute("href");if(t)if(0===t.indexOf("javascript:"))if(1===e.childNodes.length&&e.childNodes[0].nodeType===this.TEXT_NODE){var i=this._doc.createTextNode(e.textContent);e.parentNode.replaceChild(i,e)}else{for(var r=this._doc.createElement("span");e.firstChild;)r.appendChild(e.firstChild);e.parentNode.replaceChild(r,e)}else e.setAttribute("href",n(t))}));var a=this._getAllNodesWithTag(e,["img","picture","figure","video","audio","source"]);this._forEachNode(a,(function(e){var t=e.getAttribute("src"),i=e.getAttribute("poster"),r=e.getAttribute("srcset");if(t&&e.setAttribute("src",n(t)),i&&e.setAttribute("poster",n(i)),r){var a=r.replace(this.REGEXPS.srcsetUrl,(function(e,t,i,r){return n(t)+(i||"")+r}));e.setAttribute("srcset",a)}}))},_simplifyNestedElements:function(e){for(var t=e;t;){if(t.parentNode&&["DIV","SECTION"].includes(t.tagName)&&(!t.id||!t.id.startsWith("readability"))){if(this._isElementWithoutContent(t)){t=this._removeAndGetNext(t);continue}if(this._hasSingleTagInsideElement(t,"DIV")||this._hasSingleTagInsideElement(t,"SECTION")){for(var i=t.children[0],n=0;n»] /.test(t))n=/ [\\\/>»] /.test(t),r(t=i.replace(/(.*)[\|\-\\\/>»] .*/gi,"$1"))<3&&(t=i.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi,"$1"));else if(-1!==t.indexOf(": ")){var a=this._concatNodeLists(e.getElementsByTagName("h1"),e.getElementsByTagName("h2")),o=t.trim();this._someNode(a,(function(e){return e.textContent.trim()===o}))||(r(t=i.substring(i.lastIndexOf(":")+1))<3?t=i.substring(i.indexOf(":")+1):r(i.substr(0,i.indexOf(":")))>5&&(t=i))}else if(t.length>150||t.length<15){var s=e.getElementsByTagName("h1");1===s.length&&(t=this._getInnerText(s[0]))}var l=r(t=t.trim().replace(this.REGEXPS.normalize," "));return l<=4&&(!n||l!=r(i.replace(/[\|\-\\\/>»]+/g,""))-1)&&(t=i),t},_prepDocument:function(){var e=this._doc;this._removeNodes(this._getAllNodesWithTag(e,["style"])),e.body&&this._replaceBrs(e.body),this._replaceNodeTags(this._getAllNodesWithTag(e,["font"]),"SPAN")},_nextNode:function(e){for(var t=e;t&&t.nodeType!=this.ELEMENT_NODE&&this.REGEXPS.whitespace.test(t.textContent);)t=t.nextSibling;return t},_replaceBrs:function(e){this._forEachNode(this._getAllNodesWithTag(e,["br"]),(function(e){for(var t=e.nextSibling,i=!1;(t=this._nextNode(t))&&"BR"==t.tagName;){i=!0;var n=t.nextSibling;t.parentNode.removeChild(t),t=n}if(i){var r=this._doc.createElement("p");for(e.parentNode.replaceChild(r,e),t=r.nextSibling;t;){if("BR"==t.tagName){var a=this._nextNode(t.nextSibling);if(a&&"BR"==a.tagName)break}if(!this._isPhrasingContent(t))break;var o=t.nextSibling;r.appendChild(t),t=o}for(;r.lastChild&&this._isWhitespace(r.lastChild);)r.removeChild(r.lastChild);"P"===r.parentNode.tagName&&this._setNodeTag(r.parentNode,"DIV")}}))},_setNodeTag:function(e,t){if(this.log("_setNodeTag",e,t),this._docJSDOMParser)return e.localName=t.toLowerCase(),e.tagName=t.toUpperCase(),e;for(var i=e.ownerDocument.createElement(t);e.firstChild;)i.appendChild(e.firstChild);e.parentNode.replaceChild(i,e),e.readability&&(i.readability=e.readability);for(var n=0;n!i.includes(e))).join(" ").length/n.join(" ").length:0},_checkByline:function(e,t){if(this._articleByline)return!1;if(void 0!==e.getAttribute)var i=e.getAttribute("rel"),n=e.getAttribute("itemprop");return!(!("author"===i||n&&-1!==n.indexOf("author")||this.REGEXPS.byline.test(t))||!this._isValidByline(e.textContent)||(this._articleByline=e.textContent.trim(),0))},_getNodeAncestors:function(e,t){t=t||0;for(var i=0,n=[];e.parentNode&&(n.push(e.parentNode),!t||++i!==t);)e=e.parentNode;return n},_grabArticle:function(e){this.log("**** grabArticle ****");var t=this._doc,i=null!==e;if(!(e=e||this._doc.body))return this.log("No body found in document. Abort."),null;for(var n=e.innerHTML;;){this.log("Starting grabArticle loop");var r=this._flagIsActive(this.FLAG_STRIP_UNLIKELYS),a=[],o=this._doc.documentElement;let X=!0;for(;o;){"HTML"===o.tagName&&(this._articleLang=o.getAttribute("lang"));var s=o.className+" "+o.id;if(this._isProbablyVisible(o))if("true"!=o.getAttribute("aria-modal")||"dialog"!=o.getAttribute("role"))if(this._checkByline(o,s))o=this._removeAndGetNext(o);else if(X&&this._headerDuplicatesTitle(o))this.log("Removing header: ",o.textContent.trim(),this._articleTitle.trim()),X=!1,o=this._removeAndGetNext(o);else{if(r){if(this.REGEXPS.unlikelyCandidates.test(s)&&!this.REGEXPS.okMaybeItsACandidate.test(s)&&!this._hasAncestorTag(o,"table")&&!this._hasAncestorTag(o,"code")&&"BODY"!==o.tagName&&"A"!==o.tagName){this.log("Removing unlikely candidate - "+s),o=this._removeAndGetNext(o);continue}if(this.UNLIKELY_ROLES.includes(o.getAttribute("role"))){this.log("Removing content with role "+o.getAttribute("role")+" - "+s),o=this._removeAndGetNext(o);continue}}if("DIV"!==o.tagName&&"SECTION"!==o.tagName&&"HEADER"!==o.tagName&&"H1"!==o.tagName&&"H2"!==o.tagName&&"H3"!==o.tagName&&"H4"!==o.tagName&&"H5"!==o.tagName&&"H6"!==o.tagName||!this._isElementWithoutContent(o)){if(-1!==this.DEFAULT_TAGS_TO_SCORE.indexOf(o.tagName)&&a.push(o),"DIV"===o.tagName){for(var l=null,c=o.firstChild;c;){var h=c.nextSibling;if(this._isPhrasingContent(c))null!==l?l.appendChild(c):this._isWhitespace(c)||(l=t.createElement("p"),o.replaceChild(l,c),l.appendChild(c));else if(null!==l){for(;l.lastChild&&this._isWhitespace(l.lastChild);)l.removeChild(l.lastChild);l=null}c=h}if(this._hasSingleTagInsideElement(o,"P")&&this._getLinkDensity(o)<.25){var d=o.children[0];o.parentNode.replaceChild(d,o),o=d,a.push(o)}else this._hasChildBlockElement(o)||(o=this._setNodeTag(o,"P"),a.push(o))}o=this._getNextNode(o)}else o=this._removeAndGetNext(o)}else o=this._removeAndGetNext(o);else this.log("Removing hidden node - "+s),o=this._removeAndGetNext(o)}var u=[];this._forEachNode(a,(function(e){if(e.parentNode&&void 0!==e.parentNode.tagName){var t=this._getInnerText(e);if(!(t.length<25)){var i=this._getNodeAncestors(e,5);if(0!==i.length){var n=0;n+=1,n+=t.split(this.REGEXPS.commas).length,n+=Math.min(Math.floor(t.length/100),3),this._forEachNode(i,(function(e,t){if(e.tagName&&e.parentNode&&void 0!==e.parentNode.tagName){if(void 0===e.readability&&(this._initializeNode(e),u.push(e)),0===t)var i=1;else i=1===t?2:3*t;e.readability.contentScore+=n/i}}))}}}}));for(var m=[],g=0,p=u.length;gE.readability.contentScore){m.splice(N,0,f),m.length>this._nbTopCandidates&&m.pop();break}}}var T,b=m[0]||null,y=!1;if(null===b||"BODY"===b.tagName){for(b=t.createElement("DIV"),y=!0;e.firstChild;)this.log("Moving child out:",e.firstChild),b.appendChild(e.firstChild);e.appendChild(b),this._initializeNode(b)}else if(b){for(var A=[],v=1;v=.75&&A.push(this._getNodeAncestors(m[v]));if(A.length>=3)for(T=b.parentNode;"BODY"!==T.tagName;){for(var S=0,C=0;C=3){b=T;break}T=T.parentNode}b.readability||this._initializeNode(b),T=b.parentNode;for(var L=b.readability.contentScore,x=L/3;"BODY"!==T.tagName;)if(T.readability){var R=T.readability.contentScore;if(RL){b=T;break}L=T.readability.contentScore,T=T.parentNode}else T=T.parentNode;for(T=b.parentNode;"BODY"!=T.tagName&&1==T.children.length;)T=(b=T).parentNode;b.readability||this._initializeNode(b)}var D=t.createElement("DIV");i&&(D.id="readability-content");for(var I=Math.max(10,.2*b.readability.contentScore),O=(T=b.parentNode).children,w=0,M=O.length;w=I)k=!0;else if("P"===P.nodeName){var B=this._getLinkDensity(P),H=this._getInnerText(P),G=H.length;(G>80&&B<.25||G<80&&G>0&&0===B&&-1!==H.search(/\.( |$)/))&&(k=!0)}}k&&(this.log("Appending node:",P),-1===this.ALTER_TO_DIV_EXCEPTIONS.indexOf(P.nodeName)&&(this.log("Altering sibling:",P,"to div."),P=this._setNodeTag(P,"DIV")),D.appendChild(P),O=T.children,w-=1,M-=1)}if(this._debug&&this.log("Article content pre-prep: "+D.innerHTML),this._prepArticle(D),this._debug&&this.log("Article content post-prep: "+D.innerHTML),y)b.id="readability-page-1",b.className="page";else{var F=t.createElement("DIV");for(F.id="readability-page-1",F.className="page";D.firstChild;)F.appendChild(D.firstChild);D.appendChild(F)}this._debug&&this.log("Article content after paging: "+D.innerHTML);var z=!0,W=this._getInnerText(D,!0).length;if(W0&&e.length<100},_unescapeHtmlEntities:function(e){if(!e)return e;var t=this.HTML_ESCAPE_MAP;return e.replace(/&(quot|amp|apos|lt|gt);/g,(function(e,i){return t[i]})).replace(/&#(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi,(function(e,t,i){var n=parseInt(t||i,t?16:10);return String.fromCharCode(n)}))},_getJSONLD:function(e){var t,i=this._getAllNodesWithTag(e,["script"]);return this._forEachNode(i,(function(e){if(!t&&"application/ld+json"===e.getAttribute("type"))try{var i=e.textContent.replace(/^\s*\s*$/g,""),n=JSON.parse(i);if(!n["@context"]||!n["@context"].match(/^https?\:\/\/schema\.org$/))return;if(!n["@type"]&&Array.isArray(n["@graph"])&&(n=n["@graph"].find((function(e){return(e["@type"]||"").match(this.REGEXPS.jsonLdArticleTypes)}))),!n||!n["@type"]||!n["@type"].match(this.REGEXPS.jsonLdArticleTypes))return;if(t={},"string"==typeof n.name&&"string"==typeof n.headline&&n.name!==n.headline){var r=this._getArticleTitle(),a=this._textSimilarity(n.name,r)>.75,o=this._textSimilarity(n.headline,r)>.75;t.title=o&&!a?n.headline:n.name}else"string"==typeof n.name?t.title=n.name.trim():"string"==typeof n.headline&&(t.title=n.headline.trim());return n.author&&("string"==typeof n.author.name?t.byline=n.author.name.trim():Array.isArray(n.author)&&n.author[0]&&"string"==typeof n.author[0].name&&(t.byline=n.author.filter((function(e){return e&&"string"==typeof e.name})).map((function(e){return e.name.trim()})).join(", "))),"string"==typeof n.description&&(t.excerpt=n.description.trim()),n.publisher&&"string"==typeof n.publisher.name&&(t.siteName=n.publisher.name.trim()),void("string"==typeof n.datePublished&&(t.datePublished=n.datePublished.trim()))}catch(e){this.log(e.message)}})),t||{}},_getArticleMetadata:function(e){var t={},i={},n=this._doc.getElementsByTagName("meta"),r=/\s*(article|dc|dcterm|og|twitter)\s*:\s*(author|creator|description|published_time|title|site_name)\s*/gi,a=/^\s*(?:(dc|dcterm|og|twitter|weibo:(article|webpage))\s*[\.:]\s*)?(author|creator|description|title|site_name)\s*$/i;return this._forEachNode(n,(function(e){var t=e.getAttribute("name"),n=e.getAttribute("property"),o=e.getAttribute("content");if(o){var s=null,l=null;n&&(s=n.match(r))&&(l=s[0].toLowerCase().replace(/\s/g,""),i[l]=o.trim()),!s&&t&&a.test(t)&&(l=t,o&&(l=l.toLowerCase().replace(/\s/g,"").replace(/\./g,":"),i[l]=o.trim()))}})),t.title=e.title||i["dc:title"]||i["dcterm:title"]||i["og:title"]||i["weibo:article:title"]||i["weibo:webpage:title"]||i.title||i["twitter:title"],t.title||(t.title=this._getArticleTitle()),t.byline=e.byline||i["dc:creator"]||i["dcterm:creator"]||i.author,t.excerpt=e.excerpt||i["dc:description"]||i["dcterm:description"]||i["og:description"]||i["weibo:article:description"]||i["weibo:webpage:description"]||i.description||i["twitter:description"],t.siteName=e.siteName||i["og:site_name"],t.publishedTime=e.datePublished||i["article:published_time"]||null,t.title=this._unescapeHtmlEntities(t.title),t.byline=this._unescapeHtmlEntities(t.byline),t.excerpt=this._unescapeHtmlEntities(t.excerpt),t.siteName=this._unescapeHtmlEntities(t.siteName),t.publishedTime=this._unescapeHtmlEntities(t.publishedTime),t},_isSingleImage:function(e){return"IMG"===e.tagName||1===e.children.length&&""===e.textContent.trim()&&this._isSingleImage(e.children[0])},_unwrapNoscriptImages:function(e){var t=Array.from(e.getElementsByTagName("img"));this._forEachNode(t,(function(e){for(var t=0;t0&&r>i)return!1;if(e.parentNode.tagName===t&&(!n||n(e.parentNode)))return!0;e=e.parentNode,r++}return!1},_getRowAndColumnCount:function(e){for(var t=0,i=0,n=e.getElementsByTagName("tr"),r=0;r0)n._readabilityDataTable=!0;else if(["col","colgroup","tfoot","thead","th"].some((function(e){return!!n.getElementsByTagName(e)[0]})))this.log("Data table because found data-y descendant"),n._readabilityDataTable=!0;else if(n.getElementsByTagName("table")[0])n._readabilityDataTable=!1;else{var a=this._getRowAndColumnCount(n);a.rows>=10||a.columns>4?n._readabilityDataTable=!0:n._readabilityDataTable=a.rows*a.columns>10}}else n._readabilityDataTable=!1;else n._readabilityDataTable=!1}},_fixLazyImages:function(e){this._forEachNode(this._getAllNodesWithTag(e,["img","picture","figure"]),(function(e){if(e.src&&this.REGEXPS.b64DataUrl.test(e.src)){if("image/svg+xml"===this.REGEXPS.b64DataUrl.exec(e.src)[1])return;for(var t=!1,i=0;in+=this._getInnerText(e,!0).length)),n/i},_cleanConditionally:function(e,t){this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)&&this._removeNodes(this._getAllNodesWithTag(e,[t]),(function(e){var i=function(e){return e._readabilityDataTable},n="ul"===t||"ol"===t;if(!n){var r=0,a=this._getAllNodesWithTag(e,["ul","ol"]);this._forEachNode(a,(e=>r+=this._getInnerText(e).length)),n=r/this._getInnerText(e).length>.9}if("table"===t&&i(e))return!1;if(this._hasAncestorTag(e,"table",-1,i))return!1;if(this._hasAncestorTag(e,"code"))return!1;var o=this._getClassWeight(e);if(this.log("Cleaning Conditionally",e),o+0<0)return!0;if(this._getCharCount(e,",")<10){for(var s=e.getElementsByTagName("p").length,l=e.getElementsByTagName("img").length,c=e.getElementsByTagName("li").length-100,h=e.getElementsByTagName("input").length,d=this._getTextDensity(e,["h1","h2","h3","h4","h5","h6"]),u=0,m=this._getAllNodesWithTag(e,["object","embed","iframe"]),g=0;g1&&s/l<.5&&!this._hasAncestorTag(e,"figure")||!n&&c>s||h>Math.floor(s/3)||!n&&d<.9&&_<25&&(0===l||l>2)&&!this._hasAncestorTag(e,"figure")||!n&&o<25&&f>.2||o>=25&&f>.5||1===u&&_<75||u>1;if(n&&N){for(var E=0;E1)return N;if(l==e.getElementsByTagName("li").length)return!1}return N}return!1}))},_cleanMatchedNodes:function(e,t){for(var i=this._getNextNode(e,!0),n=this._getNextNode(e);n&&n!=i;)n=t.call(this,n,n.className+" "+n.id)?this._removeAndGetNext(n):this._getNextNode(n)},_cleanHeaders:function(e){let t=this._getAllNodesWithTag(e,["h1","h2"]);this._removeNodes(t,(function(e){let t=this._getClassWeight(e)<0;return t&&this.log("Removing header with low class weight:",e),t}))},_headerDuplicatesTitle:function(e){if("H1"!=e.tagName&&"H2"!=e.tagName)return!1;var t=this._getInnerText(e,!1);return this.log("Evaluating similarity of header:",t,this._articleTitle),this._textSimilarity(this._articleTitle,t)>.75},_flagIsActive:function(e){return(this._flags&e)>0},_removeFlag:function(e){this._flags=this._flags&~e},_isProbablyVisible:function(e){return(!e.style||"none"!=e.style.display)&&(!e.style||"hidden"!=e.style.visibility)&&!e.hasAttribute("hidden")&&(!e.hasAttribute("aria-hidden")||"true"!=e.getAttribute("aria-hidden")||e.className&&e.className.indexOf&&-1!==e.className.indexOf("fallback-image"))},parse:function(){if(this._maxElemsToParse>0){var e=this._doc.getElementsByTagName("*").length;if(e>this._maxElemsToParse)throw new Error("Aborting parsing document; "+e+" elements found")}this._unwrapNoscriptImages(this._doc);var t=this._disableJSONLD?{}:this._getJSONLD(this._doc);this._removeScripts(this._doc),this._prepDocument();var i=this._getArticleMetadata(t);this._articleTitle=i.title;var n=this._grabArticle();if(!n)return null;if(this.log("Grabbed: "+n.innerHTML),this._postProcessContent(n),!i.excerpt){var r=n.getElementsByTagName("p");r.length>0&&(i.excerpt=r[0].textContent.trim())}var a=n.textContent;return{title:this._articleTitle,byline:i.byline||this._articleByline,dir:this._articleDir,lang:this._articleLang,content:this._serializer(n),textContent:a,length:a.length,excerpt:i.excerpt,siteName:i.siteName||this._articleSiteName,publishedTime:i.publishedTime}}},e.exports=t},396:(e,t,i)=>{var n=i(238),r=i(804);e.exports={Readability:n,isProbablyReaderable:r}},454:e=>{"use strict";const{entries:t,setPrototypeOf:i,isFrozen:n,getPrototypeOf:r,getOwnPropertyDescriptor:a}=Object;let{freeze:o,seal:s,create:l}=Object,{apply:c,construct:h}="undefined"!=typeof Reflect&&Reflect;o||(o=function(e){return e}),s||(s=function(e){return e}),c||(c=function(e,t,i){return e.apply(t,i)}),h||(h=function(e,t){return new e(...t)});const d=v(Array.prototype.forEach),u=v(Array.prototype.pop),m=v(Array.prototype.push),g=v(String.prototype.toLowerCase),p=v(String.prototype.toString),f=v(String.prototype.match),_=v(String.prototype.replace),N=v(String.prototype.indexOf),E=v(String.prototype.trim),T=v(Object.prototype.hasOwnProperty),b=v(RegExp.prototype.test),y=(A=TypeError,function(){for(var e=arguments.length,t=new Array(e),i=0;i1?i-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:g;i&&i(e,null);let a=t.length;for(;a--;){let i=t[a];if("string"==typeof i){const e=r(i);e!==i&&(n(t)||(t[a]=e),i=e)}e[i]=!0}return e}function C(e){for(let t=0;t/gm),z=s(/\$\{[\w\W]*}/gm),W=s(/^data-[\-\w.\u00B7-\uFFFF]+$/),j=s(/^aria-[\-\w]+$/),X=s(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Y=s(/^(?:\w+script|data):/i),V=s(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),$=s(/^html$/i),q=s(/^[a-z][.\w]*(-[.\w]+)+$/i);var K=Object.freeze({__proto__:null,ARIA_ATTR:j,ATTR_WHITESPACE:V,CUSTOM_ELEMENT:q,DATA_ATTR:W,DOCTYPE_NAME:$,ERB_EXPR:F,IS_ALLOWED_URI:X,IS_SCRIPT_OR_DATA:Y,MUSTACHE_EXPR:G,TMPLIT_EXPR:z});const J=function(){return"undefined"==typeof window?null:window};var Z=function e(){let i=arguments.length>0&&void 0!==arguments[0]?arguments[0]:J();const n=t=>e(t);if(n.version="3.2.3",n.removed=[],!i||!i.document||9!==i.document.nodeType)return n.isSupported=!1,n;let{document:r}=i;const a=r,s=a.currentScript,{DocumentFragment:c,HTMLTemplateElement:h,Node:A,Element:v,NodeFilter:C,NamedNodeMap:G=i.NamedNodeMap||i.MozNamedAttrMap,HTMLFormElement:F,DOMParser:z,trustedTypes:W}=i,j=v.prototype,Y=x(j,"cloneNode"),V=x(j,"remove"),q=x(j,"nextSibling"),Z=x(j,"childNodes"),Q=x(j,"parentNode");if("function"==typeof h){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ee,te="";const{implementation:ie,createNodeIterator:ne,createDocumentFragment:re,getElementsByTagName:ae}=r,{importNode:oe}=a;let se={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};n.isSupported="function"==typeof t&&"function"==typeof Q&&ie&&void 0!==ie.createHTMLDocument;const{MUSTACHE_EXPR:le,ERB_EXPR:ce,TMPLIT_EXPR:he,DATA_ATTR:de,ARIA_ATTR:ue,IS_SCRIPT_OR_DATA:me,ATTR_WHITESPACE:ge,CUSTOM_ELEMENT:pe}=K;let{IS_ALLOWED_URI:fe}=K,_e=null;const Ne=S({},[...R,...D,...I,...w,...P]);let Ee=null;const Te=S({},[...k,...U,...B,...H]);let be=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ye=null,Ae=null,ve=!0,Se=!0,Ce=!1,Le=!0,xe=!1,Re=!0,De=!1,Ie=!1,Oe=!1,we=!1,Me=!1,Pe=!1,ke=!0,Ue=!1,Be=!0,He=!1,Ge={},Fe=null;const ze=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let We=null;const je=S({},["audio","video","img","source","image","track"]);let Xe=null;const Ye=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ve="http://www.w3.org/1998/Math/MathML",$e="http://www.w3.org/2000/svg",qe="http://www.w3.org/1999/xhtml";let Ke=qe,Je=!1,Ze=null;const Qe=S({},[Ve,$e,qe],p);let et=S({},["mi","mo","mn","ms","mtext"]),tt=S({},["annotation-xml"]);const it=S({},["title","style","font","a","script"]);let nt=null;const rt=["application/xhtml+xml","text/html"];let at=null,ot=null;const st=r.createElement("form"),lt=function(e){return e instanceof RegExp||e instanceof Function},ct=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ot||ot!==e){if(e&&"object"==typeof e||(e={}),e=L(e),nt=-1===rt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,at="application/xhtml+xml"===nt?p:g,_e=T(e,"ALLOWED_TAGS")?S({},e.ALLOWED_TAGS,at):Ne,Ee=T(e,"ALLOWED_ATTR")?S({},e.ALLOWED_ATTR,at):Te,Ze=T(e,"ALLOWED_NAMESPACES")?S({},e.ALLOWED_NAMESPACES,p):Qe,Xe=T(e,"ADD_URI_SAFE_ATTR")?S(L(Ye),e.ADD_URI_SAFE_ATTR,at):Ye,We=T(e,"ADD_DATA_URI_TAGS")?S(L(je),e.ADD_DATA_URI_TAGS,at):je,Fe=T(e,"FORBID_CONTENTS")?S({},e.FORBID_CONTENTS,at):ze,ye=T(e,"FORBID_TAGS")?S({},e.FORBID_TAGS,at):{},Ae=T(e,"FORBID_ATTR")?S({},e.FORBID_ATTR,at):{},Ge=!!T(e,"USE_PROFILES")&&e.USE_PROFILES,ve=!1!==e.ALLOW_ARIA_ATTR,Se=!1!==e.ALLOW_DATA_ATTR,Ce=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Le=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,xe=e.SAFE_FOR_TEMPLATES||!1,Re=!1!==e.SAFE_FOR_XML,De=e.WHOLE_DOCUMENT||!1,we=e.RETURN_DOM||!1,Me=e.RETURN_DOM_FRAGMENT||!1,Pe=e.RETURN_TRUSTED_TYPE||!1,Oe=e.FORCE_BODY||!1,ke=!1!==e.SANITIZE_DOM,Ue=e.SANITIZE_NAMED_PROPS||!1,Be=!1!==e.KEEP_CONTENT,He=e.IN_PLACE||!1,fe=e.ALLOWED_URI_REGEXP||X,Ke=e.NAMESPACE||qe,et=e.MATHML_TEXT_INTEGRATION_POINTS||et,tt=e.HTML_INTEGRATION_POINTS||tt,be=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&<(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(be.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&<(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(be.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(be.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),xe&&(Se=!1),Me&&(we=!0),Ge&&(_e=S({},P),Ee=[],!0===Ge.html&&(S(_e,R),S(Ee,k)),!0===Ge.svg&&(S(_e,D),S(Ee,U),S(Ee,H)),!0===Ge.svgFilters&&(S(_e,I),S(Ee,U),S(Ee,H)),!0===Ge.mathMl&&(S(_e,w),S(Ee,B),S(Ee,H))),e.ADD_TAGS&&(_e===Ne&&(_e=L(_e)),S(_e,e.ADD_TAGS,at)),e.ADD_ATTR&&(Ee===Te&&(Ee=L(Ee)),S(Ee,e.ADD_ATTR,at)),e.ADD_URI_SAFE_ATTR&&S(Xe,e.ADD_URI_SAFE_ATTR,at),e.FORBID_CONTENTS&&(Fe===ze&&(Fe=L(Fe)),S(Fe,e.FORBID_CONTENTS,at)),Be&&(_e["#text"]=!0),De&&S(_e,["html","head","body"]),_e.table&&(S(_e,["tbody"]),delete ye.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw y('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw y('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ee=e.TRUSTED_TYPES_POLICY,te=ee.createHTML("")}else void 0===ee&&(ee=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let i=null;const n="data-tt-policy-suffix";t&&t.hasAttribute(n)&&(i=t.getAttribute(n));const r="dompurify"+(i?"#"+i:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(W,s)),null!==ee&&"string"==typeof te&&(te=ee.createHTML(""));o&&o(e),ot=e}},ht=S({},[...D,...I,...O]),dt=S({},[...w,...M]),ut=function(e){m(n.removed,{element:e});try{Q(e).removeChild(e)}catch(t){V(e)}},mt=function(e,t){try{m(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){m(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(we||Me)try{ut(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},gt=function(e){let t=null,i=null;if(Oe)e=""+e;else{const t=f(e,/^[\r\n\t ]+/);i=t&&t[0]}"application/xhtml+xml"===nt&&Ke===qe&&(e=''+e+"");const n=ee?ee.createHTML(e):e;if(Ke===qe)try{t=(new z).parseFromString(n,nt)}catch(e){}if(!t||!t.documentElement){t=ie.createDocument(Ke,"template",null);try{t.documentElement.innerHTML=Je?te:n}catch(e){}}const a=t.body||t.documentElement;return e&&i&&a.insertBefore(r.createTextNode(i),a.childNodes[0]||null),Ke===qe?ae.call(t,De?"html":"body")[0]:De?t.documentElement:a},pt=function(e){return ne.call(e.ownerDocument||e,e,C.SHOW_ELEMENT|C.SHOW_COMMENT|C.SHOW_TEXT|C.SHOW_PROCESSING_INSTRUCTION|C.SHOW_CDATA_SECTION,null)},ft=function(e){return e instanceof F&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof G)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},_t=function(e){return"function"==typeof A&&e instanceof A};function Nt(e,t,i){d(e,(e=>{e.call(n,t,i,ot)}))}const Et=function(e){let t=null;if(Nt(se.beforeSanitizeElements,e,null),ft(e))return ut(e),!0;const i=at(e.nodeName);if(Nt(se.uponSanitizeElement,e,{tagName:i,allowedTags:_e}),e.hasChildNodes()&&!_t(e.firstElementChild)&&b(/<[/\w]/g,e.innerHTML)&&b(/<[/\w]/g,e.textContent))return ut(e),!0;if(7===e.nodeType)return ut(e),!0;if(Re&&8===e.nodeType&&b(/<[/\w]/g,e.data))return ut(e),!0;if(!_e[i]||ye[i]){if(!ye[i]&&bt(i)){if(be.tagNameCheck instanceof RegExp&&b(be.tagNameCheck,i))return!1;if(be.tagNameCheck instanceof Function&&be.tagNameCheck(i))return!1}if(Be&&!Fe[i]){const t=Q(e)||e.parentNode,i=Z(e)||e.childNodes;if(i&&t)for(let n=i.length-1;n>=0;--n){const r=Y(i[n],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,q(e))}}return ut(e),!0}return e instanceof v&&!function(e){let t=Q(e);t&&t.tagName||(t={namespaceURI:Ke,tagName:"template"});const i=g(e.tagName),n=g(t.tagName);return!!Ze[e.namespaceURI]&&(e.namespaceURI===$e?t.namespaceURI===qe?"svg"===i:t.namespaceURI===Ve?"svg"===i&&("annotation-xml"===n||et[n]):Boolean(ht[i]):e.namespaceURI===Ve?t.namespaceURI===qe?"math"===i:t.namespaceURI===$e?"math"===i&&tt[n]:Boolean(dt[i]):e.namespaceURI===qe?!(t.namespaceURI===$e&&!tt[n])&&!(t.namespaceURI===Ve&&!et[n])&&!dt[i]&&(it[i]||!ht[i]):!("application/xhtml+xml"!==nt||!Ze[e.namespaceURI]))}(e)?(ut(e),!0):"noscript"!==i&&"noembed"!==i&&"noframes"!==i||!b(/<\/no(script|embed|frames)/i,e.innerHTML)?(xe&&3===e.nodeType&&(t=e.textContent,d([le,ce,he],(e=>{t=_(t,e," ")})),e.textContent!==t&&(m(n.removed,{element:e.cloneNode()}),e.textContent=t)),Nt(se.afterSanitizeElements,e,null),!1):(ut(e),!0)},Tt=function(e,t,i){if(ke&&("id"===t||"name"===t)&&(i in r||i in st))return!1;if(Se&&!Ae[t]&&b(de,t));else if(ve&&b(ue,t));else if(!Ee[t]||Ae[t]){if(!(bt(e)&&(be.tagNameCheck instanceof RegExp&&b(be.tagNameCheck,e)||be.tagNameCheck instanceof Function&&be.tagNameCheck(e))&&(be.attributeNameCheck instanceof RegExp&&b(be.attributeNameCheck,t)||be.attributeNameCheck instanceof Function&&be.attributeNameCheck(t))||"is"===t&&be.allowCustomizedBuiltInElements&&(be.tagNameCheck instanceof RegExp&&b(be.tagNameCheck,i)||be.tagNameCheck instanceof Function&&be.tagNameCheck(i))))return!1}else if(Xe[t]);else if(b(fe,_(i,ge,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==N(i,"data:")||!We[e])if(Ce&&!b(me,_(i,ge,"")));else if(i)return!1;return!0},bt=function(e){return"annotation-xml"!==e&&f(e,pe)},yt=function(e){Nt(se.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||ft(e))return;const i={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Ee,forceKeepAttr:void 0};let r=t.length;for(;r--;){const a=t[r],{name:o,namespaceURI:s,value:l}=a,c=at(o);let h="value"===o?l:E(l);if(i.attrName=c,i.attrValue=h,i.keepAttr=!0,i.forceKeepAttr=void 0,Nt(se.uponSanitizeAttribute,e,i),h=i.attrValue,!Ue||"id"!==c&&"name"!==c||(mt(o,e),h="user-content-"+h),Re&&b(/((--!?|])>)|<\/(style|title)/i,h)){mt(o,e);continue}if(i.forceKeepAttr)continue;if(mt(o,e),!i.keepAttr)continue;if(!Le&&b(/\/>/i,h)){mt(o,e);continue}xe&&d([le,ce,he],(e=>{h=_(h,e," ")}));const m=at(e.nodeName);if(Tt(m,c,h)){if(ee&&"object"==typeof W&&"function"==typeof W.getAttributeType)if(s);else switch(W.getAttributeType(m,c)){case"TrustedHTML":h=ee.createHTML(h);break;case"TrustedScriptURL":h=ee.createScriptURL(h)}try{s?e.setAttributeNS(s,o,h):e.setAttribute(o,h),ft(e)?ut(e):u(n.removed)}catch(e){}}}Nt(se.afterSanitizeAttributes,e,null)},At=function e(t){let i=null;const n=pt(t);for(Nt(se.beforeSanitizeShadowDOM,t,null);i=n.nextNode();)Nt(se.uponSanitizeShadowNode,i,null),Et(i),yt(i),i.content instanceof c&&e(i.content);Nt(se.afterSanitizeShadowDOM,t,null)};return n.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=null,r=null,o=null,s=null;if(Je=!e,Je&&(e="\x3c!--\x3e"),"string"!=typeof e&&!_t(e)){if("function"!=typeof e.toString)throw y("toString is not a function");if("string"!=typeof(e=e.toString()))throw y("dirty is not a string, aborting")}if(!n.isSupported)return e;if(Ie||ct(t),n.removed=[],"string"==typeof e&&(He=!1),He){if(e.nodeName){const t=at(e.nodeName);if(!_e[t]||ye[t])throw y("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof A)i=gt("\x3c!----\x3e"),r=i.ownerDocument.importNode(e,!0),1===r.nodeType&&"BODY"===r.nodeName||"HTML"===r.nodeName?i=r:i.appendChild(r);else{if(!we&&!xe&&!De&&-1===e.indexOf("<"))return ee&&Pe?ee.createHTML(e):e;if(i=gt(e),!i)return we?null:Pe?te:""}i&&Oe&&ut(i.firstChild);const l=pt(He?e:i);for(;o=l.nextNode();)Et(o),yt(o),o.content instanceof c&&At(o.content);if(He)return e;if(we){if(Me)for(s=re.call(i.ownerDocument);i.firstChild;)s.appendChild(i.firstChild);else s=i;return(Ee.shadowroot||Ee.shadowrootmode)&&(s=oe.call(a,s,!0)),s}let h=De?i.outerHTML:i.innerHTML;return De&&_e["!doctype"]&&i.ownerDocument&&i.ownerDocument.doctype&&i.ownerDocument.doctype.name&&b($,i.ownerDocument.doctype.name)&&(h="\n"+h),xe&&d([le,ce,he],(e=>{h=_(h,e," ")})),ee&&Pe?ee.createHTML(h):h},n.setConfig=function(){ct(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Ie=!0},n.clearConfig=function(){ot=null,Ie=!1},n.isValidAttribute=function(e,t,i){ot||ct({});const n=at(e),r=at(t);return Tt(n,r,i)},n.addHook=function(e,t){"function"==typeof t&&m(se[e],t)},n.removeHook=function(e){return u(se[e])},n.removeHooks=function(e){se[e]=[]},n.removeAllHooks=function(){se={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},n}();e.exports=Z}},t={};function i(n){var r=t[n];if(void 0!==r)return r.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,i),a.exports}i.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return i.d(t,{a:t}),t},i.d=(e,t)=>{for(var n in t)i.o(t,n)&&!i.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";var e=i(396);function t(e){webkit.messageHandlers.readabilityMessageHandler.postMessage({Type:"StateChange",Value:e})}(0,e.isProbablyReaderable)(document)?t("Available"):t("Unavailable");var n=(new XMLSerializer).serializeToString(document);const r=i(454).sanitize(n,{WHOLE_DOCUMENT:!0});var a=(new DOMParser).parseFromString(r,"text/html");const o=new e.Readability(a,__READABILITY_OPTION__).parse();webkit.messageHandlers.readabilityMessageHandler.postMessage({Type:"ContentParsed",Value:JSON.stringify(o)})})()})(); -------------------------------------------------------------------------------- /Sources/Readability/Resources/ReadabilitySanitized.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! @license DOMPurify 3.2.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.3/LICENSE */ 2 | -------------------------------------------------------------------------------- /Sources/Readability/exported.swift: -------------------------------------------------------------------------------- 1 | @_exported import ReadabilityCore 2 | -------------------------------------------------------------------------------- /Sources/ReadabilityCore/ReadabilityMessageHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WebKit 3 | 4 | /// A message handler for receiving messages from injected JavaScript in the WKWebView. 5 | @MainActor 6 | package final class ReadabilityMessageHandler: NSObject, WKScriptMessageHandler { 7 | /// Modes that determine how the message handler processes content. 8 | package enum Mode { 9 | /// Generates reader HTML using the provided initial style. 10 | case generateReaderHTML(initialStyle: ReaderStyle) 11 | /// Returns the raw readability result. 12 | case generateReadabilityResult 13 | } 14 | 15 | /// Events emitted by the message handler. 16 | package enum Event { 17 | /// The readability content was parsed and reader HTML was generated. 18 | case contentParsedAndGeneratedHTML(html: String) 19 | /// The readability content was parsed. 20 | case contentParsed(readabilityResult: ReadabilityResult) 21 | /// The availability status of the reader changed. 22 | case availabilityChanged(availability: ReaderAvailability) 23 | } 24 | 25 | // The generator used to produce reader HTML from the readability result. 26 | private let readerContentGenerator: Generator 27 | private let mode: Mode 28 | 29 | /// A closure that is called when an event is received. 30 | package var eventHandler: (@MainActor (Event) -> Void)? 31 | 32 | package init(mode: Mode, readerContentGenerator: Generator) { 33 | self.mode = mode 34 | self.readerContentGenerator = readerContentGenerator 35 | } 36 | 37 | package func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { 38 | guard let message = message.body as? [String: Any], 39 | let typeString = message["Type"] as? String, 40 | let type = ReadabilityMessageType(rawValue: typeString), 41 | let value = message["Value"] 42 | else { 43 | return 44 | } 45 | 46 | switch type { 47 | case .stateChange: 48 | if let availability = ReaderAvailability(rawValue: value as? String ?? "") { 49 | eventHandler?(.availabilityChanged(availability: availability)) 50 | } 51 | case .contentParsed: 52 | Task.detached { [weak self, mode] in 53 | if let jsonString = value as? String, 54 | let jsonData = jsonString.data(using: .utf8), 55 | let result = try? JSONDecoder().decode(ReadabilityResult.self, from: jsonData) 56 | { 57 | switch mode { 58 | case let .generateReaderHTML(initialStyle): 59 | if let html = await self?.readerContentGenerator.generate(result, initialStyle: initialStyle) { 60 | await self?.eventHandler?(.contentParsedAndGeneratedHTML(html: html)) 61 | } 62 | case .generateReadabilityResult: 63 | await self?.eventHandler?(.contentParsed(readabilityResult: result)) 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | /// Subscribes to events emitted by the message handler. 71 | /// 72 | /// - Parameter operation: A closure to be invoked when an event occurs, or `nil` to unsubscribe. 73 | package func subscribeEvent(_ operation: (@MainActor (Event) -> Void)?) { 74 | eventHandler = operation 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/ReadabilityCore/ReadabilityMessageType.swift: -------------------------------------------------------------------------------- 1 | /// Represents the type of messages exchanged between the JavaScript and the native code. 2 | enum ReadabilityMessageType: String { 3 | /// Indicates a change in the reader state. 4 | case stateChange = "StateChange" 5 | /// Indicates that the content has been parsed. 6 | case contentParsed = "ContentParsed" 7 | } 8 | -------------------------------------------------------------------------------- /Sources/ReadabilityCore/ReadabilityResult.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A structure representing the result of parsing a web page using Readability. 4 | /// It contains metadata and content extracted from the web page. 5 | public struct ReadabilityResult: Decodable, Sendable { 6 | /// The title of the article. 7 | public let title: String 8 | /// The byline of the article, if available. 9 | public let byline: String? 10 | /// The main HTML content of the article. 11 | public let content: String 12 | /// The plain text content of the article. 13 | public let textContent: String 14 | /// The length of the article content. 15 | public let length: Int 16 | /// An excerpt from the article. 17 | public let excerpt: String 18 | /// The name of the site where the article originated. 19 | public let siteName: String? 20 | /// The language of the article. 21 | public let language: String 22 | /// The text direction (e.g., "ltr", "rtl") of the article, if available. 23 | public let direction: String? 24 | /// The published time of the article, if available. 25 | public let publishedTime: String? 26 | 27 | public enum CodingKeys: String, CodingKey, Sendable { 28 | case title 29 | case byline 30 | case content 31 | case textContent 32 | case length 33 | case excerpt 34 | case siteName 35 | case language = "lang" 36 | case direction = "dir" 37 | case publishedTime 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/ReadabilityCore/ReaderAvailability.swift: -------------------------------------------------------------------------------- 1 | /// An enumeration representing the availability status of the reader mode. 2 | public enum ReaderAvailability: String, Sendable { 3 | /// The reader mode is available. 4 | case available = "Available" 5 | /// The reader mode is unavailable. 6 | case unavailable = "Unavailable" 7 | } 8 | -------------------------------------------------------------------------------- /Sources/ReadabilityCore/ReaderContentGeneratable.swift: -------------------------------------------------------------------------------- 1 | /// A protocol that defines the ability to generate reader content (HTML) from a `ReadabilityResult` and an initial style. 2 | package protocol ReaderContentGeneratable: Sendable { 3 | /// Generates reader HTML content based on the provided readability result and initial style. 4 | /// 5 | /// - Parameters: 6 | /// - readabilityResult: The result of the readability parsing. 7 | /// - initialStyle: The initial style to apply to the reader content. 8 | /// - Returns: An optional `String` containing the generated HTML content. 9 | func generate( 10 | _ readabilityResult: ReadabilityResult, 11 | initialStyle: ReaderStyle 12 | ) async -> String? 13 | } 14 | -------------------------------------------------------------------------------- /Sources/ReadabilityCore/ReaderStyle.swift: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/ 4 | 5 | #if canImport(UIKit) 6 | import UIKit 7 | #elseif canImport(AppKit) 8 | import AppKit 9 | #endif 10 | 11 | /// A structure representing the style settings for the reader mode. 12 | public struct ReaderStyle: Sendable, Codable, Hashable { 13 | /// The theme to be applied in reader mode. 14 | public var theme: Theme 15 | /// The font size to be applied in reader mode. 16 | public var fontSize: FontSize 17 | 18 | /// Initializes a new `ReaderStyle` with the specified theme and font size. 19 | /// 20 | /// - Parameters: 21 | /// - theme: The theme to use (e.g., light, dark, sepia). 22 | /// - fontSize: The font size setting. 23 | public init(theme: Theme, fontSize: FontSize) { 24 | self.theme = theme 25 | self.fontSize = fontSize 26 | } 27 | 28 | /// An enumeration representing the available themes for reader mode. 29 | public enum Theme: String, Sendable, Codable, Hashable, CaseIterable { 30 | /// A light theme. 31 | case light 32 | /// A dark theme. 33 | case dark 34 | /// A sepia theme. 35 | case sepia 36 | } 37 | 38 | /// An enumeration representing the available font sizes for reader mode. 39 | public enum FontSize: Int, Sendable, Codable, Hashable, CaseIterable { 40 | /// The smallest font size. 41 | case size1 = 1 42 | case size2 = 2 43 | case size3 = 3 44 | case size4 = 4 45 | case size5 = 5 46 | case size6 = 6 47 | case size7 = 7 48 | case size8 = 8 49 | case size9 = 9 50 | case size10 = 10 51 | case size11 = 11 52 | case size12 = 12 53 | /// The largest font size. 54 | case size13 = 13 55 | 56 | /// Checks if the current font size is the smallest. 57 | public var isSmallest: Bool { 58 | self == FontSize.size1 59 | } 60 | 61 | /// Checks if the current font size is the largest. 62 | public var isLargest: Bool { 63 | self == FontSize.size13 64 | } 65 | 66 | /// Returns a smaller font size if available. 67 | /// 68 | /// - Returns: The next smaller font size, or the current size if already smallest. 69 | public func smaller() -> FontSize { 70 | if isSmallest { 71 | return self 72 | } else { 73 | return FontSize(rawValue: rawValue - 1)! 74 | } 75 | } 76 | 77 | /// Returns a larger font size if available. 78 | public func bigger() -> FontSize { 79 | if isLargest { 80 | return self 81 | } else { 82 | return FontSize(rawValue: rawValue + 1)! 83 | } 84 | } 85 | 86 | /// The default font size based on the user's preferred content size category. 87 | #if canImport(UIKit) 88 | @MainActor 89 | static var defaultSize: FontSize { 90 | switch UIApplication.shared.preferredContentSizeCategory { 91 | case .extraSmall: 92 | .size1 93 | case .small: 94 | .size2 95 | case .medium: 96 | .size3 97 | case .large: 98 | .size5 99 | case .extraLarge: 100 | .size7 101 | case .extraExtraLarge: 102 | .size9 103 | case .extraExtraExtraLarge: 104 | .size12 105 | default: 106 | .size5 107 | } 108 | } 109 | #endif 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/ReadabilityCore/ScriptLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An actor responsible for loading JavaScript and HTML resources from the bundle. 4 | package actor ScriptLoader { 5 | /// Resources available to be loaded by the ScriptLoader. 6 | package enum Resource { 7 | /// Script to be injected at document start. 8 | case atDocumentStart 9 | /// Script to be injected at document end. 10 | case atDocumentEnd 11 | /// HTML template for generating reader content. 12 | case readerHTML 13 | /// Basic Readability parsing script. 14 | case readabilityBasic 15 | /// Sanitized Readability parsing script. 16 | case readabilitySanitized 17 | 18 | /// The name of the resource file (without extension). 19 | var name: String { 20 | switch self { 21 | case .atDocumentStart: 22 | return "AtDocumentStart" 23 | case .atDocumentEnd: 24 | return "AtDocumentEnd" 25 | case .readerHTML: 26 | return "Reader" 27 | case .readabilityBasic: 28 | return "ReadabilityBasic" 29 | case .readabilitySanitized: 30 | return "ReadabilitySanitized" 31 | } 32 | } 33 | 34 | /// The file extension of the resource. 35 | var ext: String { 36 | switch self { 37 | case .atDocumentStart, .atDocumentEnd, .readabilityBasic, .readabilitySanitized: 38 | return "js" 39 | case .readerHTML: 40 | return "html" 41 | } 42 | } 43 | } 44 | 45 | // The bundle from which resources are loaded. 46 | private let bundle: Bundle 47 | 48 | /// Initializes a new `ScriptLoader` with the specified bundle. 49 | /// 50 | /// - Parameter bundle: The bundle containing the script resources. 51 | package init(bundle: Bundle) { 52 | self.bundle = bundle 53 | } 54 | 55 | /// Loads the content of the specified resource. 56 | /// 57 | /// - Parameter resource: The resource to load. 58 | /// - Returns: A `String` containing the contents of the resource. 59 | /// - Throws: An error if the resource cannot be found or read. 60 | package func load(_ resource: Resource) throws -> String { 61 | try load(forResource: resource.name, withExtension: resource.ext) 62 | } 63 | 64 | /// Loads the content for a given resource name and file extension. 65 | /// 66 | /// - Parameters: 67 | /// - name: The name of the resource. 68 | /// - ext: The file extension of the resource. 69 | /// - Returns: A `String` containing the resource's contents. 70 | /// - Throws: An error if the resource cannot be located or read. 71 | private func load(forResource name: String, withExtension ext: String) throws -> String { 72 | guard let url = bundle.url(forResource: name, withExtension: ext) else { 73 | throw Error.failedToCopyReadabilityScriptFromNodeModules 74 | } 75 | 76 | let readabilityScript = try String(contentsOf: url, encoding: .utf8) 77 | 78 | return readabilityScript 79 | } 80 | } 81 | 82 | extension ScriptLoader { 83 | /// Errors that can occur while loading script resources. 84 | enum Error: Swift.Error { 85 | /// Indicates failure to locate or read the Readability script from the bundle. 86 | case failedToCopyReadabilityScriptFromNodeModules 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/ReadabilityUI/Internal/ReaderContentGenerator.swift: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/ 4 | 5 | import Foundation 6 | import ReadabilityCore 7 | 8 | /// A content generator that creates reader HTML using a template and a readability result. 9 | /// Conforms to the `ReaderContentGeneratable` protocol. 10 | struct ReaderContentGenerator: ReaderContentGeneratable { 11 | private let encoder = { 12 | let encoder = JSONEncoder() 13 | return encoder 14 | }() 15 | 16 | private let scriptLoader = ScriptLoader(bundle: .module) 17 | 18 | /// Generates reader HTML content based on the provided `ReadabilityResult` and `ReaderStyle`. 19 | /// 20 | /// - Parameters: 21 | /// - readabilityResult: The result of the readability parsing. 22 | /// - initialStyle: The initial style settings to apply. 23 | /// - Returns: An optional `String` containing the generated reader HTML, or `nil` if generation fails. 24 | func generate( 25 | _ readabilityResult: ReadabilityResult, 26 | initialStyle: ReaderStyle 27 | ) async -> String? { 28 | // Load the HTML template and encode the reader style into JSON. 29 | guard let template = try? await scriptLoader.load(.readerHTML), 30 | let styleData = try? encoder.encode(initialStyle), 31 | let styleString = String(data: styleData, encoding: .utf8) 32 | else { return nil } 33 | 34 | // Replace placeholders in the template with actual content. 35 | return template.replacingOccurrences(of: "%READER-STYLE%", with: styleString) 36 | .replacingOccurrences(of: "%READER-TITLE%", with: readabilityResult.title) 37 | .replacingOccurrences(of: "%READER-BYLINE%", with: readabilityResult.byline ?? "") 38 | .replacingOccurrences(of: "%READER-CONTENT%", with: readabilityResult.content) 39 | .replacingOccurrences(of: "%READER-LANGUAGE%", with: readabilityResult.language) 40 | .replacingOccurrences(of: "%READER-DIRECTION%", with: readabilityResult.direction ?? "auto") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/ReadabilityUI/ReadabilityWebCoordinator.swift: -------------------------------------------------------------------------------- 1 | import ReadabilityCore 2 | import SwiftUI 3 | import WebKit 4 | 5 | /// A coordinator that manages a WKWebView configured for reader mode. 6 | /// It sets up the necessary scripts and message handlers to parse content and manage reader mode availability. 7 | @MainActor 8 | public final class ReadabilityWebCoordinator: ObservableObject { 9 | // A weak reference to the message handler that processes JavaScript messages. 10 | private weak var messageHandler: ReadabilityMessageHandler? 11 | // A weak reference to the WKWebView configuration. 12 | private weak var configuration: WKWebViewConfiguration? 13 | 14 | private let scriptLoader = ScriptLoader(bundle: .module) 15 | private let messageHandlerName = "readabilityMessageHandler" 16 | 17 | private var (_contentParsed, contentParsedContinuation) = AsyncStream.makeStream(of: String.self) 18 | private var (_availabilityChanged, availabilityChangedContinuation) = AsyncStream.makeStream(of: ReaderAvailability.self) 19 | 20 | /// An asynchronous stream that emits the generated reader HTML when the content is parsed. 21 | public var contentParsed: AsyncStream { 22 | _contentParsed 23 | } 24 | 25 | /// An asynchronous stream that emits updates to the reader mode availability status. 26 | public var availabilityChanged: AsyncStream { 27 | _availabilityChanged 28 | } 29 | 30 | /// The initial style to apply to the reader content. 31 | public let initialStyle: ReaderStyle 32 | 33 | /// Initializes a new `ReadabilityWebCoordinator` with the specified initial style. 34 | /// 35 | /// - Parameter initialStyle: The initial `ReaderStyle` to use. 36 | public init(initialStyle: ReaderStyle) { 37 | self.initialStyle = initialStyle 38 | } 39 | 40 | /// Creates and configures a `WKWebViewConfiguration` for reader mode. 41 | /// 42 | /// - Returns: A configured `WKWebViewConfiguration` with injected scripts and message handlers. 43 | /// - Throws: An error if script loading fails. 44 | public func createReadableWebViewConfiguration() async throws -> WKWebViewConfiguration { 45 | async let documentStartStringTask = scriptLoader.load(.atDocumentStart) 46 | async let documentEndStringTask = scriptLoader.load(.atDocumentEnd) 47 | 48 | let (documentStartString, documentEndString) = try await (documentStartStringTask, documentEndStringTask) 49 | 50 | let documentStartScript = WKUserScript( 51 | source: documentStartString, 52 | injectionTime: .atDocumentStart, 53 | forMainFrameOnly: true 54 | ) 55 | 56 | let documentEndScript = WKUserScript( 57 | source: documentEndString, 58 | injectionTime: .atDocumentEnd, 59 | forMainFrameOnly: true 60 | ) 61 | 62 | let configuration = WKWebViewConfiguration() 63 | let messageHandler = ReadabilityMessageHandler( 64 | mode: .generateReaderHTML(initialStyle: initialStyle), 65 | readerContentGenerator: ReaderContentGenerator() 66 | ) 67 | 68 | self.configuration = configuration 69 | self.messageHandler = messageHandler 70 | 71 | configuration.userContentController.addUserScript(documentStartScript) 72 | configuration.userContentController.addUserScript(documentEndScript) 73 | configuration.userContentController.add(messageHandler, name: messageHandlerName) 74 | 75 | messageHandler.subscribeEvent { [weak self] event in 76 | switch event { 77 | case let .availabilityChanged(availability): 78 | self?.availabilityChangedContinuation.yield(availability) 79 | case let .contentParsedAndGeneratedHTML(html: html): 80 | self?.contentParsedContinuation.yield(html) 81 | case .contentParsed: 82 | break 83 | } 84 | } 85 | 86 | return configuration 87 | } 88 | 89 | /// Invalidates the current configuration by removing all script message handlers and finishing the asynchronous streams. 90 | public func invalidate() { 91 | configuration?.userContentController.removeScriptMessageHandler(forName: messageHandlerName) 92 | configuration?.userContentController.removeAllUserScripts() 93 | contentParsedContinuation.finish() 94 | availabilityChangedContinuation.finish() 95 | } 96 | 97 | deinit { 98 | MainActor.assumeIsolated { 99 | invalidate() 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/ReadabilityUI/ReaderControllable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReadabilityCore 3 | import WebKit 4 | 5 | /// A protocol defining an interface for controlling a reader mode web view. 6 | /// Provides methods to evaluate JavaScript and manipulate the reader overlay. 7 | @MainActor 8 | public protocol ReaderControllable { 9 | /// Evaluates a JavaScript string in the context of the web view. 10 | /// 11 | /// - Parameter javascriptString: The JavaScript code to evaluate. 12 | /// - Returns: The result of the JavaScript evaluation. 13 | /// - Throws: An error if the evaluation fails. 14 | func evaluateJavaScript(_ javascriptString: String) async throws -> Any 15 | } 16 | 17 | public extension ReaderControllable { 18 | /// The JavaScript namespace for the Readability functions. 19 | private var namespace: String { 20 | "window.__swift_readability__" 21 | } 22 | 23 | /// Sets the reader style of the web view. 24 | /// 25 | /// - Parameter style: The `ReaderStyle` to apply. 26 | /// - Throws: An error if the JavaScript evaluation fails. 27 | func set(style: ReaderStyle) async throws { 28 | guard try await isReaderMode() else { 29 | throw ReaderControllableError.readerStyleChangeOnlyAllowedInReaderMode 30 | } 31 | let jsonData = try JSONEncoder().encode(style) 32 | let jsonString = String(data: jsonData, encoding: .utf8)! 33 | 34 | _ = try await evaluateJavaScript( 35 | "\(namespace).setStyle(\(jsonString));0" 36 | ) 37 | } 38 | 39 | /// Sets the reader theme of the web view. 40 | /// 41 | /// - Parameter theme: The `ReaderStyle.Theme` to apply. 42 | /// - Throws: An error if the JavaScript evaluation fails. 43 | func set(theme: ReaderStyle.Theme) async throws { 44 | guard try await isReaderMode() else { 45 | throw ReaderControllableError.readerStyleChangeOnlyAllowedInReaderMode 46 | } 47 | let jsonData = try JSONEncoder().encode(theme) 48 | let jsonString = String(data: jsonData, encoding: .utf8)! 49 | 50 | _ = try await evaluateJavaScript("\(namespace).setTheme(\(jsonString));0") 51 | } 52 | 53 | /// Sets the font size of the reader content. 54 | /// 55 | /// - Parameter fontSize: The `ReaderStyle.FontSize` to apply. 56 | /// - Throws: An error if the JavaScript evaluation fails. 57 | func set(fontSize: ReaderStyle.FontSize) async throws { 58 | guard try await isReaderMode() else { 59 | throw ReaderControllableError.readerStyleChangeOnlyAllowedInReaderMode 60 | } 61 | let jsonData = try JSONEncoder().encode(fontSize) 62 | let jsonString = String(data: jsonData, encoding: .utf8)! 63 | 64 | _ = try await evaluateJavaScript("\(namespace).setFontSize(\(jsonString));0") 65 | } 66 | 67 | /// Displays the reader content overlay with the specified HTML. 68 | /// 69 | /// - Parameter html: The HTML content to display. 70 | /// - Throws: An error if the JavaScript evaluation fails. 71 | func showReaderContent(with html: String) async throws { 72 | let escapedHTML = html.jsonEscaped 73 | _ = try await evaluateJavaScript("\(namespace).showReaderOverlay(\(escapedHTML));0") 74 | } 75 | 76 | /// Hides the reader content overlay. 77 | /// 78 | /// - Throws: An error if the JavaScript evaluation fails. 79 | func hideReaderContent() async throws { 80 | _ = try await evaluateJavaScript("\(namespace).hideReaderOverlay();0") 81 | } 82 | 83 | /// Checks whether the web view is currently in reader mode. 84 | /// 85 | /// - Returns: `true` if the web view is in reader mode, otherwise `false`. 86 | /// - Throws: An error if the JavaScript evaluation fails. 87 | func isReaderMode() async throws -> Bool { 88 | let isReaderMode = try await evaluateJavaScript("\(namespace).isReaderMode() ? 1 : 0") as? Int 89 | return isReaderMode == 1 ? true : false 90 | } 91 | } 92 | 93 | private extension String { 94 | var jsonEscaped: String { 95 | let data = try? JSONSerialization.data(withJSONObject: [self], options: []) 96 | if let data = data, 97 | let json = String(data: data, encoding: .utf8), 98 | json.first == "[", json.last == "]" 99 | { 100 | return String(json.dropFirst().dropLast()) 101 | } 102 | return self 103 | } 104 | } 105 | 106 | public enum ReaderControllableError: LocalizedError { 107 | case readerStyleChangeOnlyAllowedInReaderMode 108 | 109 | public var errorDescription: String? { 110 | switch self { 111 | case .readerStyleChangeOnlyAllowedInReaderMode: 112 | "ReaderStyle changes are only available when in Reader Mode." 113 | } 114 | } 115 | } 116 | 117 | extension WKWebView: ReaderControllable {} 118 | -------------------------------------------------------------------------------- /Sources/ReadabilityUI/Resources/AtDocumentEnd.js: -------------------------------------------------------------------------------- 1 | window.__swift_readability__.checkReadability(); 2 | window.__swift_readability__.configureReader(); 3 | -------------------------------------------------------------------------------- /Sources/ReadabilityUI/Resources/AtDocumentStart.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! @license DOMPurify 3.2.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.3/LICENSE */ 2 | -------------------------------------------------------------------------------- /Sources/ReadabilityUI/Resources/Reader.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | %READER-TITLE% 12 | 747 | 748 | 749 | 750 |
751 |

%READER-TITLE%

752 |
%READER-BYLINE%
753 |
754 | 755 |
756 | %READER-CONTENT% 757 |
758 | 759 | 760 | 761 | 762 | -------------------------------------------------------------------------------- /Sources/ReadabilityUI/exported.swift: -------------------------------------------------------------------------------- 1 | @_exported import ReadabilityCore 2 | -------------------------------------------------------------------------------- /Tests/ReadabilityTests/ReadabilityTests.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryu0118/swift-readability/5baf9d1fddc66fae9be962462532a27269882f93/Tests/ReadabilityTests/ReadabilityTests.swift -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | if ! command -v nest >/dev/null 2>&1; then 2 | curl -s https://raw.githubusercontent.com/mtj0928/nest/main/Scripts/install.sh | bash 3 | fi 4 | 5 | ~/.nest/bin/nest bootstrap nestfile.yaml 6 | 7 | npm install 8 | npm run build 9 | -------------------------------------------------------------------------------- /nestfile.yaml: -------------------------------------------------------------------------------- 1 | nestPath: ./.nest 2 | targets: 3 | - reference: nicklockwood/SwiftFormat 4 | version: 0.55.5 5 | assetName: swiftformat.artifactbundle.zip 6 | checksum: 2c6e8903b88ca94f621586a91617c89337f53460bb3db00e3de655f96895a1a8 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@mozilla/readability": "^0.5.0", 4 | "dompurify": "^3.2.3" 5 | }, 6 | "devDependencies": { 7 | "html-webpack-plugin": "^5.6.3", 8 | "raw-loader": "^4.0.2", 9 | "webpack": "^5.97.1", 10 | "webpack-cli": "^6.0.1" 11 | }, 12 | "scripts": { 13 | "build": "./node_modules/.bin/webpack -c webpack.config.js" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /webpack-resources/AtDocumentStart.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. 4 | */ 5 | 6 | "use strict"; 7 | import { isProbablyReaderable, Readability } from "@mozilla/readability"; 8 | 9 | // Debug flag to control logging. 10 | const DEBUG = false; 11 | 12 | // Variables to hold the readability result, current style, and original body style. 13 | let readabilityResult = null; 14 | let currentStyle = null; 15 | let originalBodyStyle = null; 16 | 17 | const themeColors = { 18 | light: { background: "#ffffff", color: "#15141a" }, 19 | dark: { background: "#333333", color: "#fbfbfe" }, 20 | sepia: { background: "#fff4de", color: "#15141a" } 21 | }; 22 | 23 | // Selector for block-level images in the content. 24 | const BLOCK_IMAGES_SELECTOR = 25 | ".content p > img:only-child, " + 26 | ".content p > a:only-child > img:only-child, " + 27 | ".content .wp-caption img, " + 28 | ".content figure img"; 29 | 30 | /** 31 | * Logs debug information if DEBUG is enabled. 32 | * @param {*} s - The message or object to log. 33 | */ 34 | function debug(s) { 35 | if (!DEBUG) { 36 | return; 37 | } 38 | console.log(s); 39 | } 40 | 41 | /** 42 | * Checks if the current document is readerable and initiates parsing if so. 43 | */ 44 | function checkReadability() { 45 | setTimeout(function() { 46 | if (!isProbablyReaderable(document)) { 47 | postStateChangedToUnavailable(); 48 | return; 49 | } 50 | 51 | if ((document.location.protocol === "http:" || document.location.protocol === "https:") && 52 | document.location.pathname !== "/") { 53 | // If a previous readability result exists, reuse it. 54 | if (readabilityResult && readabilityResult.content) { 55 | postStateChangedToAvailable(); 56 | postContentParsed(readabilityResult); 57 | return; 58 | } 59 | 60 | const uri = { 61 | spec: document.location.href, 62 | host: document.location.host, 63 | prePath: document.location.protocol + "//" + document.location.host, 64 | scheme: document.location.protocol.substr(0, document.location.protocol.indexOf(":")), 65 | pathBase: document.location.protocol + "//" + document.location.host + location.pathname.substr(0, location.pathname.lastIndexOf("/") + 1) 66 | }; 67 | 68 | const docStr = new XMLSerializer().serializeToString(document); 69 | if (docStr.indexOf(" -1) { 70 | postStateChangedToUnavailable(); 71 | return; 72 | } 73 | 74 | const DOMPurify = require('dompurify'); 75 | const clean = DOMPurify.sanitize(docStr, { WHOLE_DOCUMENT: true }); 76 | const doc = new DOMParser().parseFromString(clean, "text/html"); 77 | const readability = new Readability(uri, doc, { debug: DEBUG }); 78 | readabilityResult = readability.parse(); 79 | 80 | if (!readabilityResult) { 81 | postStateChangedToUnavailable(); 82 | return; 83 | } 84 | 85 | readabilityResult.title = escapeHTML(readabilityResult.title); 86 | readabilityResult.byline = escapeHTML(readabilityResult.byline); 87 | 88 | postStateChanged(readabilityResult !== null ? "Available" : "Unavailable"); 89 | postContentParsed(readabilityResult); 90 | return; 91 | } 92 | 93 | postStateChangedToUnavailable(); 94 | }, 100); 95 | } 96 | 97 | /** 98 | * Posts the parsed readability content to the native app. 99 | * @param {Object} readabilityResult - The result object from Readability. 100 | */ 101 | function postContentParsed(readabilityResult) { 102 | webkit.messageHandlers.readabilityMessageHandler.postMessage({ 103 | Type: "ContentParsed", 104 | Value: JSON.stringify(readabilityResult) 105 | }); 106 | } 107 | 108 | /** 109 | * Posts a state change message indicating the reader is available. 110 | */ 111 | function postStateChangedToAvailable() { 112 | postStateChanged("Available"); 113 | } 114 | 115 | /** 116 | * Posts a state change message indicating the reader is unavailable. 117 | */ 118 | function postStateChangedToUnavailable() { 119 | postStateChanged("Unavailable"); 120 | } 121 | 122 | /** 123 | * Sends a state change message to the native app if the page is not already in reader mode. 124 | * @param {string} value - The state value ("Available" or "Unavailable"). 125 | */ 126 | function postStateChanged(value) { 127 | if (!isCurrentPageReader()) { 128 | debug({ Type: "StateChange", Value: value }); 129 | webkit.messageHandlers.readabilityMessageHandler.postMessage({ 130 | Type: "StateChange", 131 | Value: value 132 | }); 133 | } 134 | } 135 | 136 | /** 137 | * Checks if the current page is already in reader mode. 138 | * @returns {boolean} True if the necessary reader elements are present, otherwise false. 139 | */ 140 | function isCurrentPageReader() { 141 | return document.getElementById("reader-content") && 142 | document.getElementById("reader-header") && 143 | document.getElementById("reader-title") && 144 | document.getElementById("reader-credits"); 145 | } 146 | 147 | /** 148 | * Updates the theme colors of the reader view based on the current style. 149 | */ 150 | function updateThemeColors() { 151 | if (currentStyle && currentStyle.theme && themeColors[currentStyle.theme]) { 152 | const colors = themeColors[currentStyle.theme]; 153 | 154 | const readerContainer = document.getElementById("reader-container"); 155 | if (readerContainer) { 156 | readerContainer.style.backgroundColor = colors.background; 157 | readerContainer.style.color = colors.color; 158 | } 159 | 160 | const overlay = document.getElementById("reader-overlay"); 161 | if (overlay) { 162 | overlay.style.backgroundColor = colors.background; 163 | overlay.style.color = colors.color; 164 | } 165 | 166 | document.body.style.backgroundColor = colors.background; 167 | document.body.style.color = colors.color; 168 | } 169 | } 170 | 171 | /** 172 | * Applies the provided style to the reader view. 173 | * @param {Object} style - An object containing theme and fontSize properties. 174 | */ 175 | function setStyle(style) { 176 | const readerRoot = document.getElementById("reader-container") || document.body; 177 | if (currentStyle && currentStyle.theme) { 178 | readerRoot.classList.remove(currentStyle.theme); 179 | document.documentElement.classList.remove(currentStyle.theme); 180 | } 181 | if (style && style.theme) { 182 | readerRoot.classList.add(style.theme); 183 | document.documentElement.classList.add(style.theme); 184 | } 185 | if (currentStyle && currentStyle.fontSize) { 186 | readerRoot.classList.remove("font-size" + currentStyle.fontSize); 187 | } 188 | if (style && style.fontSize) { 189 | readerRoot.classList.add("font-size" + style.fontSize); 190 | } 191 | currentStyle = style; 192 | updateThemeColors(); 193 | } 194 | 195 | /** 196 | * Sets the theme for the reader view. 197 | * @param {string} theme - The theme to apply (e.g., "light", "dark", "sepia"). 198 | */ 199 | function setTheme(theme) { 200 | const readerRoot = document.getElementById("reader-container") || document.body; 201 | if (currentStyle && currentStyle.theme) { 202 | readerRoot.classList.remove(currentStyle.theme); 203 | document.documentElement.classList.remove(currentStyle.theme); 204 | } 205 | currentStyle = currentStyle || {}; 206 | if (theme) { 207 | readerRoot.classList.add(theme); 208 | document.documentElement.classList.add(theme); 209 | currentStyle.theme = theme; 210 | } 211 | updateThemeColors(); 212 | } 213 | 214 | /** 215 | * Sets the font size for the reader view. 216 | * @param {number} fontSize - The font size value to apply. 217 | */ 218 | function setFontSize(fontSize) { 219 | const readerRoot = document.getElementById("reader-container") || document.body; 220 | if (currentStyle && currentStyle.fontSize) { 221 | readerRoot.classList.remove("font-size" + currentStyle.fontSize); 222 | } 223 | currentStyle = currentStyle || {}; 224 | if (fontSize) { 225 | readerRoot.classList.add("font-size" + fontSize); 226 | currentStyle.fontSize = fontSize; 227 | } 228 | updateThemeColors(); 229 | } 230 | 231 | /** 232 | * Updates margins for images within the reader content to ensure proper layout. 233 | */ 234 | function updateImageMargins() { 235 | const readerRoot = document.getElementById("reader-container") || document.body; 236 | const contentElement = readerRoot.querySelector("#reader-content"); 237 | if (!contentElement) { 238 | return; 239 | } 240 | 241 | const windowWidth = window.innerWidth; 242 | const contentWidth = contentElement.offsetWidth; 243 | const maxWidthStyle = windowWidth + "px !important"; 244 | 245 | const setImageMargins = function(img) { 246 | if (!img._originalWidth) { 247 | img._originalWidth = img.offsetWidth; 248 | } 249 | let imgWidth = img._originalWidth; 250 | if (imgWidth < contentWidth && imgWidth > windowWidth * 0.55) { 251 | imgWidth = windowWidth; 252 | } 253 | const sideMargin = Math.max((contentWidth - windowWidth) / 2, (contentWidth - imgWidth) / 2); 254 | const imageStyle = sideMargin + "px !important"; 255 | const widthStyle = imgWidth + "px !important"; 256 | const cssText = 257 | "max-width: " + maxWidthStyle + ";" + 258 | "width: " + widthStyle + ";" + 259 | "margin-left: " + imageStyle + ";" + 260 | "margin-right: " + imageStyle + ";"; 261 | img.style.cssText = cssText; 262 | }; 263 | 264 | const imgs = contentElement.querySelectorAll(BLOCK_IMAGES_SELECTOR); 265 | for (let i = imgs.length; --i >= 0;) { 266 | const img = imgs[i]; 267 | if (img.width > 0) { 268 | setImageMargins(img); 269 | } else { 270 | img.onload = function() { 271 | setImageMargins(img); 272 | }; 273 | } 274 | } 275 | } 276 | 277 | /** 278 | * Configures the reader view by applying the style and updating image margins. 279 | */ 280 | function configureReader() { 281 | const readerRoot = document.getElementById("reader-container"); 282 | if (!readerRoot) { 283 | return; 284 | } 285 | const dataStyle = readerRoot.getAttribute("data-readerstyle"); 286 | if (!dataStyle) { 287 | return; 288 | } 289 | const style = JSON.parse(dataStyle); 290 | setStyle(style); 291 | updateImageMargins(); 292 | } 293 | 294 | /** 295 | * Escapes special HTML characters in a string. 296 | * @param {string} string - The string to escape. 297 | * @returns {string} The escaped string. 298 | */ 299 | function escapeHTML(string) { 300 | if (typeof(string) !== 'string') { return ''; } 301 | return string 302 | .replace(/\&/g, "&") 303 | .replace(/\/g, ">") 305 | .replace(/\"/g, """) 306 | .replace(/\'/g, "'"); 307 | } 308 | 309 | /** 310 | * Displays the reader overlay with the provided HTML content. 311 | * Hides the original content and applies necessary styles. 312 | * @param {string} readerHTML - The HTML content to display in the reader overlay. 313 | */ 314 | function showReaderOverlay(readerHTML) { 315 | if (originalBodyStyle === null) { 316 | originalBodyStyle = document.body.getAttribute("style"); 317 | } 318 | 319 | let originalContainer = document.getElementById('original-content'); 320 | if (!originalContainer) { 321 | originalContainer = document.createElement('div'); 322 | originalContainer.id = 'original-content'; 323 | while (document.body.firstChild) { 324 | originalContainer.appendChild(document.body.firstChild); 325 | } 326 | document.body.appendChild(originalContainer); 327 | } 328 | originalContainer.style.display = 'none'; 329 | 330 | let overlay = document.getElementById('reader-overlay'); 331 | if (!overlay) { 332 | overlay = document.createElement('div'); 333 | overlay.id = 'reader-overlay'; 334 | Object.assign(overlay.style, { 335 | opacity: '0', 336 | transition: 'opacity 0.3s ease', 337 | top: '0', 338 | left: '0', 339 | width: '100%', 340 | height: '100%', 341 | overflow: 'auto' 342 | }); 343 | document.body.appendChild(overlay); 344 | } else { 345 | overlay.style.opacity = '0'; 346 | } 347 | 348 | const parser = new DOMParser(); 349 | const doc = parser.parseFromString(readerHTML, "text/html"); 350 | 351 | let styleContent = ""; 352 | const styleEl = doc.head.querySelector("style"); 353 | if (styleEl) { 354 | styleContent = styleEl.outerHTML; 355 | styleContent = styleContent.replace(/(^|\n)\s*body\s*\{/g, "$1#reader-container {") 356 | .replace(/(^|\n)\s*html\s*\{/g, "$1#reader-container {"); 357 | } 358 | const bodyContent = doc.body.innerHTML; 359 | const dataReaderStyle = doc.body.getAttribute("data-readerstyle") || ""; 360 | 361 | const container = document.createElement("div"); 362 | container.id = "reader-container"; 363 | if (dataReaderStyle) { 364 | container.setAttribute("data-readerstyle", dataReaderStyle); 365 | } 366 | container.innerHTML = styleContent + bodyContent; 367 | 368 | overlay.innerHTML = ""; 369 | overlay.appendChild(container); 370 | 371 | configureReader(); 372 | 373 | requestAnimationFrame(function() { 374 | updateThemeColors(); 375 | overlay.style.opacity = "1"; 376 | }); 377 | } 378 | 379 | /** 380 | * Hides the reader overlay and restores the original page content. 381 | */ 382 | function hideReaderOverlay() { 383 | const overlay = document.getElementById('reader-overlay'); 384 | if (overlay) { 385 | overlay.style.transition = 'opacity 0.3s ease'; 386 | void overlay.offsetWidth; 387 | overlay.style.opacity = '0'; 388 | overlay.addEventListener("transitionend", function(e) { 389 | if (overlay && overlay.parentNode) { 390 | overlay.parentNode.removeChild(overlay); 391 | } 392 | }, { once: true }); 393 | } 394 | const originalContainer = document.getElementById('original-content'); 395 | if (originalContainer) { 396 | originalContainer.style.display = ''; 397 | } 398 | if (currentStyle && currentStyle.theme) { 399 | document.documentElement.classList.remove(currentStyle.theme); 400 | } 401 | if (originalBodyStyle !== null) { 402 | document.body.setAttribute("style", originalBodyStyle); 403 | originalBodyStyle = null; 404 | } else { 405 | document.body.removeAttribute("style"); 406 | } 407 | } 408 | 409 | /** 410 | * Checks if the current page is in reader mode. 411 | * @returns {boolean} True if in reader mode, false otherwise. 412 | */ 413 | function isReaderMode() { 414 | return isCurrentPageReader(); 415 | } 416 | 417 | // Expose the Readability functions to the global namespace for Swift integration. 418 | Object.defineProperty(window, "__swift_readability__", { 419 | enumerable: false, 420 | configurable: false, 421 | writable: false, 422 | value: Object.freeze({ 423 | checkReadability: checkReadability, 424 | setStyle: setStyle, 425 | setTheme: setTheme, 426 | setFontSize: setFontSize, 427 | configureReader: configureReader, 428 | showReaderOverlay: showReaderOverlay, 429 | hideReaderOverlay: hideReaderOverlay, 430 | isReaderMode: isReaderMode 431 | }) 432 | }); 433 | 434 | // Configure the reader view on window load. 435 | window.addEventListener("load", function(event) { 436 | configureReader(); 437 | }); 438 | -------------------------------------------------------------------------------- /webpack-resources/ReadabilityBasic.js: -------------------------------------------------------------------------------- 1 | import { isProbablyReaderable, Readability } from "@mozilla/readability"; 2 | 3 | function postStateChanged(value) { 4 | webkit.messageHandlers.readabilityMessageHandler.postMessage({Type: "StateChange", Value: value}); 5 | } 6 | 7 | if(isProbablyReaderable(document)) { 8 | postStateChanged("Available") 9 | } else { 10 | postStateChanged("Unavailable") 11 | } 12 | 13 | var documentClone = document.cloneNode(true); 14 | const readabilityResult = new Readability( 15 | documentClone, 16 | __READABILITY_OPTION__ 17 | ).parse(); 18 | 19 | webkit.messageHandlers.readabilityMessageHandler.postMessage({Type: "ContentParsed", Value: JSON.stringify(readabilityResult)}); 20 | -------------------------------------------------------------------------------- /webpack-resources/ReadabilitySanitized.js: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/ 4 | 5 | import { isProbablyReaderable, Readability } from "@mozilla/readability"; 6 | 7 | function postStateChanged(value) { 8 | webkit.messageHandlers.readabilityMessageHandler.postMessage({Type: "StateChange", Value: value}); 9 | } 10 | 11 | if(isProbablyReaderable(document)) { 12 | postStateChanged("Available") 13 | } else { 14 | postStateChanged("Unavailable") 15 | } 16 | 17 | var docStr = new XMLSerializer().serializeToString(document); 18 | const DOMPurify = require('dompurify'); 19 | const clean = DOMPurify.sanitize(docStr, {WHOLE_DOCUMENT: true}); 20 | var doc = new DOMParser().parseFromString(clean, "text/html"); 21 | var readability = new Readability(doc, __READABILITY_OPTION__); 22 | const readabilityResult = readability.parse(); 23 | 24 | webkit.messageHandlers.readabilityMessageHandler.postMessage({Type: "ContentParsed", Value: JSON.stringify(readabilityResult)}); 25 | -------------------------------------------------------------------------------- /webpack-resources/Reader.css: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | html { 6 | -moz-text-size-adjust: none; 7 | -webkit-text-size-adjust: none; 8 | } 9 | 10 | body { 11 | padding: 2vw; 12 | transition-property: background-color, color; 13 | transition-duration: 0.4s; 14 | margin-left: auto; 15 | margin-right: auto; 16 | font-family: -apple-system, sans-serif; 17 | } 18 | 19 | .light { 20 | background-color: #ffffff; 21 | color: #15141a; 22 | } 23 | 24 | .dark { 25 | background-color: #333333; 26 | color: #fbfbfe; 27 | } 28 | 29 | .sepia { 30 | background-color: #fff4de; 31 | color: #15141a; 32 | } 33 | 34 | .message { 35 | margin-top: 40px; 36 | display: none; 37 | text-align: center; 38 | width: 100%; 39 | font-size: 16px; 40 | } 41 | 42 | #reader-header { 43 | text-align: start; 44 | } 45 | 46 | .domain, 47 | .credits { 48 | font-family: -apple-system, sans-serif; 49 | } 50 | 51 | .domain { 52 | margin-top: 10px; 53 | padding-bottom: 10px; 54 | color: #00acff !important; 55 | text-decoration: none; 56 | } 57 | 58 | .domain-border { 59 | margin-top: 15px; 60 | border-bottom: 1.5px solid #777777; 61 | width: 50%; 62 | } 63 | 64 | .header > h1 { 65 | font-size: 1.5em; 66 | font-weight: 700; 67 | line-height: 1.1em; 68 | width: 100%; 69 | margin: 0px; 70 | margin-top: 0px; 71 | margin-bottom: 16px; 72 | padding: 0px; 73 | } 74 | 75 | .header > .credits { 76 | padding: 0px; 77 | margin: 0px; 78 | margin-bottom: 24px; 79 | font-style: italic; 80 | } 81 | 82 | .font-size1 > .header > h1 { 83 | font-size: 24px; 84 | } 85 | 86 | .font-size2 > .header > h1 { 87 | font-size: 28px; 88 | } 89 | 90 | .font-size3 > .header > h1 { 91 | font-size: 32px; 92 | } 93 | 94 | .font-size4 > .header > h1 { 95 | font-size: 36px; 96 | } 97 | 98 | .font-size5 > .header > h1 { 99 | font-size: 40px; 100 | } 101 | 102 | .font-size6 > .header > h1 { 103 | font-size: 44px; 104 | } 105 | 106 | .font-size7 > .header > h1 { 107 | font-size: 48px; 108 | } 109 | 110 | .font-size8 > .header > h1 { 111 | font-size: 52px; 112 | } 113 | 114 | .font-size9 > .header > h1 { 115 | font-size: 56px; 116 | } 117 | 118 | .font-size10 > .header > h1 { 119 | font-size: 60px; 120 | } 121 | 122 | .font-size11 > .header > h1 { 123 | font-size: 64px; 124 | } 125 | 126 | .font-size12 > .header > h1 { 127 | font-size: 68px; 128 | } 129 | 130 | .font-size13 > .header > h1 { 131 | font-size: 72px; 132 | } 133 | 134 | /* This covers caption, domain, and credits 135 | texts in the reader UI */ 136 | 137 | .font-size1 > .content .wp-caption-text, 138 | .font-size1 > .content figcaption, 139 | .font-size1 > .header > .domain, 140 | .font-size1 > .header > .credits { 141 | font-size: 12px; 142 | } 143 | 144 | .font-size2 > .content .wp-caption-text, 145 | .font-size2 > .content figcaption, 146 | .font-size2 > .header > .domain, 147 | .font-size2 > .header > .credits { 148 | font-size: 14px; 149 | } 150 | 151 | .font-size3 > .content .wp-caption-text, 152 | .font-size3 > .content figcaption, 153 | .font-size3 > .header > .domain, 154 | .font-size3 > .header > .credits { 155 | font-size: 16px; 156 | } 157 | 158 | .font-size4 > .content .wp-caption-text, 159 | .font-size4 > .content figcaption, 160 | .font-size4 > .header > .domain, 161 | .font-size4 > .header > .credits { 162 | font-size: 18px; 163 | } 164 | 165 | .font-size5 > .content .wp-caption-text, 166 | .font-size5 > .content figcaption, 167 | .font-size5 > .header > .domain, 168 | .font-size5 > .header > .credits { 169 | font-size: 20px; 170 | } 171 | 172 | .font-size6 > .content .wp-caption-text, 173 | .font-size6 > .content figcaption, 174 | .font-size6 > .header > .domain, 175 | .font-size6 > .header > .credits { 176 | font-size: 22px; 177 | } 178 | 179 | .font-size7 > .content .wp-caption-text, 180 | .font-size7 > .content figcaption, 181 | .font-size7 > .header > .domain, 182 | .font-size7 > .header > .credits { 183 | font-size: 25px; 184 | } 185 | 186 | .font-size8 > .content .wp-caption-text, 187 | .font-size8 > .content figcaption, 188 | .font-size8 > .header > .domain, 189 | .font-size8 > .header > .credits { 190 | font-size: 28px; 191 | } 192 | 193 | .font-size9 > .content .wp-caption-text, 194 | .font-size9 > .content figcaption, 195 | .font-size9 > .header > .domain, 196 | .font-size9 > .header > .credits { 197 | font-size: 31px; 198 | } 199 | 200 | .font-size10 > .content .wp-caption-text, 201 | .font-size10 > .content figcaption, 202 | .font-size10 > .header > .domain, 203 | .font-size10 > .header > .credits { 204 | font-size: 35px; 205 | } 206 | 207 | .font-size11 > .content .wp-caption-text, 208 | .font-size11 > .content figcaption, 209 | .font-size11 > .header > .domain, 210 | .font-size11 > .header > .credits { 211 | font-size: 40px; 212 | } 213 | 214 | .font-size12 > .content .wp-caption-text, 215 | .font-size12 > .content figcaption, 216 | .font-size12 > .header > .domain, 217 | .font-size12 > .header > .credits { 218 | font-size: 45px; 219 | } 220 | 221 | .font-size13 > .content .wp-caption-text, 222 | .font-size13 > .content figcaption, 223 | .font-size13 > .header > .domain, 224 | .font-size13 > .header > .credits { 225 | font-size: 50px; 226 | } 227 | 228 | .content a { 229 | text-decoration: none !important; 230 | font-weight: normal; 231 | } 232 | 233 | .light > .content a, 234 | .light > .content a:visited, 235 | .light > .content a:hover, 236 | .light > .content a:active { 237 | color: #0060df !important; 238 | } 239 | 240 | .dark > .content a, 241 | .dark > .content a:visited, 242 | .dark > .content a:hover, 243 | .dark > .content a:active { 244 | color: #00ddff !important; 245 | } 246 | 247 | .sepia > .content a, 248 | .sepia > .content a:visited, 249 | .sepia > .content a:hover, 250 | .sepia > .content a:active { 251 | color: #00acff !important; 252 | } 253 | 254 | .content * { 255 | max-width: 100% !important; 256 | height: auto !important; 257 | } 258 | 259 | .content p { 260 | line-height: 1.4em !important; 261 | margin: 0px !important; 262 | margin-bottom: 20px !important; 263 | } 264 | 265 | /* Covers all images showing edge-to-edge using a 266 | an optional caption text */ 267 | .content .wp-caption, 268 | .content figure { 269 | display: block !important; 270 | width: 100% !important; 271 | margin: 0px !important; 272 | margin-bottom: 32px !important; 273 | } 274 | 275 | /* Images marked to be shown edge-to-edge with an 276 | optional captio ntext */ 277 | .content p > img:only-child, 278 | .content p > a:only-child > img:only-child, 279 | .content .wp-caption img, 280 | .content figure img { 281 | max-width: none !important; 282 | height: auto !important; 283 | display: block !important; 284 | margin-top: 0px !important; 285 | margin-bottom: 32px !important; 286 | } 287 | 288 | /* If image is place inside one of these blocks 289 | there's no need to add margin at the bottom */ 290 | .content .wp-caption img, 291 | .content figure img { 292 | margin-bottom: 0px !important; 293 | } 294 | 295 | /* Image caption text */ 296 | .content .caption, 297 | .content .wp-caption-text, 298 | .content figcaption { 299 | font-family: -apple-system, sans-serif; 300 | margin: 0px !important; 301 | padding-top: 4px !important; 302 | } 303 | 304 | .light > .content .caption, 305 | .light > .content .wp-caption-text, 306 | .light > .content figcaption { 307 | color: #898989; 308 | } 309 | 310 | .dark > .content .caption, 311 | .dark > .content .wp-caption-text, 312 | .dark > .content figcaption { 313 | color: #aaaaaa; 314 | } 315 | 316 | /* Ensure all pre-formatted code inside the reader content 317 | are properly wrapped inside content width */ 318 | .content code, 319 | .content pre { 320 | white-space: pre-wrap !important; 321 | margin-bottom: 20px !important; 322 | word-break: break-all; 323 | } 324 | 325 | .content blockquote { 326 | margin: 0px !important; 327 | margin-bottom: 20px !important; 328 | padding: 0px !important; 329 | -moz-padding-start: 16px !important; 330 | -webkit-padding-start: 16px !important; 331 | border: 0px !important; 332 | border-left: 2px solid !important; 333 | } 334 | 335 | .light > .content blockquote { 336 | color: #898989 !important; 337 | border-left-color: #d0d0d0 !important; 338 | } 339 | 340 | .dark > .content blockquote { 341 | color: #aaaaaa !important; 342 | border-left-color: #777777 !important; 343 | } 344 | 345 | .content ul, 346 | .content ol { 347 | margin: 0px !important; 348 | margin-bottom: 20px !important; 349 | padding: 0px !important; 350 | line-height: 1.5em; 351 | } 352 | 353 | .content ul { 354 | -moz-padding-start: 30px !important; 355 | -webkit-padding-start: 30px !important; 356 | list-style: disk !important; 357 | } 358 | 359 | .content ol { 360 | -moz-padding-start: 35px !important; 361 | -webkit-padding-start: 35px !important; 362 | list-style: decimal !important; 363 | } 364 | 365 | .font-size1-sample, 366 | .font-size1 > .content { 367 | font-size: 10px !important; 368 | } 369 | 370 | .font-size2-sample, 371 | .font-size2 > .content { 372 | font-size: 11px !important; 373 | } 374 | 375 | .font-size3-sample, 376 | .font-size3 > .content { 377 | font-size: 12px !important; 378 | } 379 | 380 | .font-size4-sample, 381 | .font-size4 > .content { 382 | font-size: 14px !important; 383 | } 384 | 385 | .font-size5-sample, 386 | .font-size5 > .content { 387 | font-size: 16px !important; 388 | } 389 | 390 | .font-size6-sample, 391 | .font-size6 > .content { 392 | font-size: 18px !important; 393 | } 394 | 395 | .font-size7-sample, 396 | .font-size7 > .content { 397 | font-size: 21px !important; 398 | } 399 | 400 | .font-size8-sample, 401 | .font-size8 > .content { 402 | font-size: 24px !important; 403 | } 404 | 405 | .font-size9-sample, 406 | .font-size9 > .content { 407 | font-size: 28px !important; 408 | } 409 | 410 | .font-size10-sample, 411 | .font-size10 > .content { 412 | font-size: 32px !important; 413 | } 414 | 415 | .font-size11-sample, 416 | .font-size11 > .content { 417 | font-size: 37px !important; 418 | } 419 | 420 | .font-size12-sample, 421 | .font-size12 > .content { 422 | font-size: 42px !important; 423 | } 424 | 425 | .font-size13-sample, 426 | .font-size13 > .content { 427 | font-size: 48px !important; 428 | } 429 | 430 | .toolbar { 431 | font-family: -apple-system, sans-serif; 432 | transition-property: visibility, opacity; 433 | transition-duration: 0.7s; 434 | visibility: visible; 435 | opacity: 1.0; 436 | position: fixed; 437 | width: 100%; 438 | bottom: 0px; 439 | left: 0px; 440 | margin: 0; 441 | padding: 0; 442 | list-style: none; 443 | background-color: #EBEBF0; 444 | -moz-user-select: none; 445 | } 446 | 447 | .toolbar-hidden { 448 | transition-property: visibility, opacity; 449 | transition-duration: 0.7s; 450 | visibility: hidden; 451 | opacity: 0.0; 452 | } 453 | 454 | .toolbar > * { 455 | float: right; 456 | width: 33%; 457 | } 458 | 459 | .button { 460 | color: white; 461 | display: block; 462 | background-position: center; 463 | background-size: 30px 24px; 464 | background-repeat: no-repeat; 465 | } 466 | 467 | .dropdown { 468 | text-align: center; 469 | display: inline-block; 470 | list-style: none; 471 | margin: 0px; 472 | padding: 0px; 473 | } 474 | 475 | .dropdown li { 476 | margin: 0px; 477 | padding: 0px; 478 | } 479 | 480 | .dropdown-toggle { 481 | margin: 0px; 482 | padding: 0px; 483 | } 484 | 485 | .dropdown-popup { 486 | text-align: start; 487 | position: absolute; 488 | left: 0px; 489 | z-index: 1000; 490 | float: left; 491 | background: #EBEBF0; 492 | margin-top: 12px; 493 | margin-bottom: 10px; 494 | padding-top: 4px; 495 | padding-bottom: 8px; 496 | font-size: 14px; 497 | box-shadow: 0px -1px 12px #333; 498 | border-radius: 3px; 499 | visibility: hidden; 500 | } 501 | 502 | .dropdown-popup > hr { 503 | width: 100%; 504 | height: 0px; 505 | border: 0px; 506 | border-top: 1px solid #B5B5B5; 507 | margin: 0; 508 | } 509 | 510 | .open > .dropdown-popup { 511 | margin-top: 0px; 512 | margin-bottom: 6px; 513 | bottom: 100%; 514 | visibility: visible; 515 | } 516 | 517 | .dropdown-arrow { 518 | position: absolute; 519 | width: 40px; 520 | height: 18px; 521 | bottom: -18px; 522 | background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-mdpi.png'); 523 | background-size: 40px 18px; 524 | background-position: center; 525 | display: block; 526 | } 527 | 528 | #font-type-buttons, 529 | .segmented-button { 530 | display: flex; 531 | flex-direction: row; 532 | list-style: none; 533 | padding: 10px 5px; 534 | white-space: nowrap; 535 | } 536 | 537 | #font-type-buttons > li, 538 | .segmented-button > li { 539 | width: 50px; /* combined with flex, this acts as a minimum width */ 540 | flex: 1 0 auto; 541 | text-align: center; 542 | line-height: 20px; 543 | } 544 | 545 | #font-type-buttons > li { 546 | padding: 10px 0; 547 | } 548 | 549 | .segmented-button > li { 550 | border-left: 1px solid #B5B5B5; 551 | } 552 | 553 | .segmented-button > li:first-child { 554 | border-left: 0px; 555 | } 556 | 557 | #font-type-buttons > li > a, 558 | .segmented-button > li > a { 559 | vertical-align: middle; 560 | text-decoration: none; 561 | color: black; 562 | } 563 | 564 | #font-type-buttons > li > a { 565 | display: inline-block; 566 | font-size: 48px; 567 | line-height: 50px; 568 | margin-bottom: 5px; 569 | border-bottom: 3px solid transparent; 570 | } 571 | 572 | .segmented-button > li > a { 573 | display: block; 574 | padding: 5px 0; 575 | font-family: -apple-system, sans-serif; 576 | font-weight: lighter; 577 | } 578 | 579 | #font-type-buttons > li > a:active, 580 | #font-type-buttons > li.selected > a { 581 | border-color: #ff9400; 582 | } 583 | 584 | .segmented-button > li > a:active, 585 | .segmented-button > li.selected > a { 586 | font-weight: bold; 587 | } 588 | 589 | #font-type-buttons > li > .sans-serif { 590 | font-weight: lighter; 591 | } 592 | 593 | #font-type-buttons > li > div { 594 | color: #666; 595 | font-size: 12px; 596 | } 597 | 598 | .toggle-button.on { 599 | background-image: url('chrome://browser/skin/images/reader-toggle-on-icon-mdpi.png'); 600 | } 601 | 602 | .toggle-button { 603 | background-image: url('chrome://browser/skin/images/reader-toggle-off-icon-mdpi.png'); 604 | } 605 | 606 | .share-button { 607 | background-image: url('chrome://browser/skin/images/reader-share-icon-mdpi.png'); 608 | } 609 | 610 | .style-button { 611 | background-image: url('chrome://browser/skin/images/reader-style-icon-mdpi.png'); 612 | } 613 | 614 | @media screen and (min-resolution: 1.25dppx) { 615 | .dropdown-arrow { 616 | background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-hdpi.png'); 617 | } 618 | 619 | .step-control > .plus-button { 620 | background-image: url('chrome://browser/skin/images/reader-plus-icon-hdpi.png'); 621 | } 622 | 623 | .step-control > .minus-button { 624 | background-image: url('chrome://browser/skin/images/reader-minus-icon-hdpi.png'); 625 | } 626 | 627 | .toggle-button.on { 628 | background-image: url('chrome://browser/skin/images/reader-toggle-on-icon-hdpi.png'); 629 | } 630 | 631 | .toggle-button { 632 | background-image: url('chrome://browser/skin/images/reader-toggle-off-icon-hdpi.png'); 633 | } 634 | 635 | .share-button { 636 | background-image: url('chrome://browser/skin/images/reader-share-icon-hdpi.png'); 637 | } 638 | 639 | .style-button { 640 | background-image: url('chrome://browser/skin/images/reader-style-icon-hdpi.png'); 641 | } 642 | } 643 | 644 | @media screen and (min-resolution: 2dppx) { 645 | .dropdown-arrow { 646 | background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-xhdpi.png'); 647 | } 648 | 649 | .step-control > .plus-button { 650 | background-image: url('chrome://browser/skin/images/reader-plus-icon-xhdpi.png'); 651 | } 652 | 653 | .step-control > .minus-button { 654 | background-image: url('chrome://browser/skin/images/reader-minus-icon-xhdpi.png'); 655 | } 656 | 657 | .toggle-button.on { 658 | background-image: url('chrome://browser/skin/images/reader-toggle-on-icon-xhdpi.png'); 659 | } 660 | 661 | .toggle-button { 662 | background-image: url('chrome://browser/skin/images/reader-toggle-off-icon-xhdpi.png'); 663 | } 664 | 665 | .share-button { 666 | background-image: url('chrome://browser/skin/images/reader-share-icon-xhdpi.png'); 667 | } 668 | 669 | .style-button { 670 | background-image: url('chrome://browser/skin/images/reader-style-icon-xhdpi.png'); 671 | } 672 | } 673 | 674 | @media screen and (orientation: portrait) { 675 | .button { 676 | height: 48px; 677 | } 678 | } 679 | 680 | @media screen and (orientation: landscape) { 681 | .button { 682 | height: 40px; 683 | } 684 | } 685 | 686 | @media screen and (min-width: 320px) { 687 | body { 688 | padding-left: 25px; 689 | padding-right: 25px; 690 | } 691 | } 692 | 693 | @media screen and (min-width: 640px) { 694 | body { 695 | padding-left: 50px; 696 | padding-right: 50px; 697 | } 698 | } 699 | 700 | @media screen and (min-width: 960px) { 701 | body { 702 | padding-left: 100px; 703 | padding-right: 100px; 704 | } 705 | 706 | .button { 707 | width: 56px; 708 | height: 56px; 709 | } 710 | 711 | .toolbar > * { 712 | width: 56px; 713 | } 714 | } 715 | 716 | .light #reader-overlay, 717 | .light #reader-container { 718 | background-color: #ffffff; 719 | color: #15141a; 720 | } 721 | 722 | .dark #reader-overlay, 723 | .dark #reader-container { 724 | background-color: #333333; 725 | color: #fbfbfe; 726 | } 727 | 728 | .sepia #reader-overlay, 729 | .sepia #reader-container { 730 | background-color: #fff4de; 731 | color: #15141a; 732 | } 733 | -------------------------------------------------------------------------------- /webpack-resources/Reader.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | %READER-TITLE% 12 | 15 | 16 | 17 | 18 |
19 |

%READER-TITLE%

20 |
%READER-BYLINE%
21 |
22 | 23 |
24 | %READER-CONTENT% 25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const fs = require('fs'); 5 | 6 | module.exports = { 7 | mode: "production", 8 | entry: { 9 | AtDocumentStart: "./webpack-resources/AtDocumentStart.js", 10 | ReadabilityBasic: "./webpack-resources/ReadabilityBasic.js", 11 | ReadabilitySanitized: "./webpack-resources/ReadabilitySanitized.js", 12 | }, 13 | output: { 14 | path: path.resolve(__dirname, 'Sources'), 15 | filename: (pathData) => { 16 | const chunkName = pathData.chunk.name; 17 | if (chunkName === 'AtDocumentStart') { 18 | return 'ReadabilityUI/Resources/[name].js'; 19 | } else { 20 | return 'Readability/Resources/[name].js'; 21 | } 22 | } 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.css$/, 28 | use: [ 29 | 'css-loader', 30 | 'raw-loader' 31 | ] 32 | } 33 | ] 34 | }, 35 | plugins: [ 36 | new HtmlWebpackPlugin({ 37 | template: './webpack-resources/Reader.html', 38 | filename: 'ReadabilityUI/Resources/Reader.html', 39 | inject: false, 40 | templateParameters: { 41 | css: fs.readFileSync('./webpack-resources/Reader.css', 'utf8') 42 | }, 43 | minify: false 44 | }), 45 | new webpack.DefinePlugin({ 46 | __READABILITY_OPTIONS__: JSON.stringify({ 47 | debug: false, 48 | maxElemsToParse: 0, 49 | nbTopCandidates: 5 50 | }) 51 | }) 52 | ] 53 | }; 54 | --------------------------------------------------------------------------------