Generate item excerpts in the background, use lol-html

This commit is contained in:
Shadowfacts 2022-01-14 11:13:19 -05:00
parent 30ec5e54e0
commit f8026125cc
11 changed files with 390 additions and 28 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "lol-html"]
path = lol-html
url = https://github.com/cloudflare/lol-html.git

11
README.md Normal file
View File

@ -0,0 +1,11 @@
# Reader
In order to build reader you need the appropriate targets added to your Rust toolchain.
```sh
$ rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
```
x86_64-apple-ios is only necessary if you're on an Intel Mac, and aarch-64-apple-ios-sim if you're on Apple Silicon.
The Xcode build script will take care of actually building the Rust code.

View File

@ -15,6 +15,7 @@
D65B18BC27504FE7004A9448 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18BB27504FE7004A9448 /* Token.swift */; };
D65B18BE275051A1004A9448 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18BD275051A1004A9448 /* LocalData.swift */; };
D65B18C127505348004A9448 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18C027505348004A9448 /* HomeViewController.swift */; };
D68B303627907D9200E8B3FA /* ExcerptGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68B303527907D9200E8B3FA /* ExcerptGenerator.swift */; };
D6A8A33427766C2800CCEC72 /* PersistentContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A8A33327766C2800CCEC72 /* PersistentContainer.swift */; };
D6C687EC272CD27600874C10 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C687EB272CD27600874C10 /* AppDelegate.swift */; };
D6C687EE272CD27600874C10 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C687ED272CD27600874C10 /* SceneDelegate.swift */; };
@ -41,7 +42,6 @@
D6E2435F278B97240005E546 /* Group+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E2435B278B97240005E546 /* Group+CoreDataClass.swift */; };
D6E24360278B97240005E546 /* Group+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E2435C278B97240005E546 /* Group+CoreDataProperties.swift */; };
D6E24363278BA1410005E546 /* ItemCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E24361278BA1410005E546 /* ItemCollectionViewCell.swift */; };
D6E24367278BA2660005E546 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D6E24366278BA2660005E546 /* SwiftSoup */; };
D6E24369278BABB40005E546 /* UIColor+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E24368278BABB40005E546 /* UIColor+App.swift */; };
D6E2436B278BB1880005E546 /* HomeCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E2436A278BB1880005E546 /* HomeCollectionViewCell.swift */; };
D6E2436E278BD8160005E546 /* ReadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E2436D278BD8160005E546 /* ReadViewController.swift */; };
@ -98,6 +98,9 @@
D65B18BB27504FE7004A9448 /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = "<group>"; };
D65B18BD275051A1004A9448 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
D65B18C027505348004A9448 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = "<group>"; };
D68B3032278FDD1A00E8B3FA /* Reader-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Reader-Bridging-Header.h"; sourceTree = "<group>"; };
D68B303527907D9200E8B3FA /* ExcerptGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExcerptGenerator.swift; sourceTree = "<group>"; };
D68B3037279099FD00E8B3FA /* liblolhtml.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = liblolhtml.a; path = "lol-html/c-api/target/aarch64-apple-ios-sim/release/liblolhtml.a"; sourceTree = "<group>"; };
D6A8A33327766C2800CCEC72 /* PersistentContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentContainer.swift; sourceTree = "<group>"; };
D6C687E8272CD27600874C10 /* Reader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Reader.app; sourceTree = BUILT_PRODUCTS_DIR; };
D6C687EB272CD27600874C10 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -141,7 +144,6 @@
buildActionMask = 2147483647;
files = (
D6C68829272CD2BA00874C10 /* Fervor.framework in Frameworks */,
D6E24367278BA2660005E546 /* SwiftSoup in Frameworks */,
D6E24371278BE1250005E546 /* HTMLEntities in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -199,6 +201,14 @@
path = Home;
sourceTree = "<group>";
};
D68B302E278FDCE200E8B3FA /* Frameworks */ = {
isa = PBXGroup;
children = (
D68B3037279099FD00E8B3FA /* liblolhtml.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
D6A8A33527766E9300CCEC72 /* CoreData */ = {
isa = PBXGroup;
children = (
@ -222,6 +232,7 @@
D6C6880E272CD27700874C10 /* ReaderUITests */,
D6C68824272CD2BA00874C10 /* Fervor */,
D6C687E9272CD27600874C10 /* Products */,
D68B302E278FDCE200E8B3FA /* Frameworks */,
);
sourceTree = "<group>";
};
@ -239,12 +250,14 @@
D6C687EA272CD27600874C10 /* Reader */ = {
isa = PBXGroup;
children = (
D68B3032278FDD1A00E8B3FA /* Reader-Bridging-Header.h */,
D6C687EB272CD27600874C10 /* AppDelegate.swift */,
D6C687ED272CD27600874C10 /* SceneDelegate.swift */,
D65B18B527504920004A9448 /* FervorController.swift */,
D65B18BD275051A1004A9448 /* LocalData.swift */,
D6E24368278BABB40005E546 /* UIColor+App.swift */,
D6EB531E278E4A7500AD2E61 /* StretchyMenuInteraction.swift */,
D68B303527907D9200E8B3FA /* ExcerptGenerator.swift */,
D6A8A33527766E9300CCEC72 /* CoreData */,
D65B18AF2750468B004A9448 /* Screens */,
D6C687F7272CD27700874C10 /* Assets.xcassets */,
@ -325,6 +338,7 @@
isa = PBXNativeTarget;
buildConfigurationList = D6C68815272CD27700874C10 /* Build configuration list for PBXNativeTarget "Reader" */;
buildPhases = (
D68B303B2791D2A900E8B3FA /* Compile lol-html c-api */,
D6C687E4272CD27600874C10 /* Sources */,
D6C687E5272CD27600874C10 /* Frameworks */,
D6C687E6272CD27600874C10 /* Resources */,
@ -337,7 +351,6 @@
);
name = Reader;
packageProductDependencies = (
D6E24366278BA2660005E546 /* SwiftSoup */,
D6E24370278BE1250005E546 /* HTMLEntities */,
);
productName = Reader;
@ -435,7 +448,6 @@
);
mainGroup = D6C687DF272CD27600874C10;
packageReferences = (
D6E24365278BA2660005E546 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
D6E2436F278BE1250005E546 /* XCRemoteSwiftPackageReference "swift-html-entities" */,
);
productRefGroup = D6C687E9272CD27600874C10 /* Products */;
@ -484,6 +496,28 @@
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
D68B303B2791D2A900E8B3FA /* Compile lol-html c-api */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Compile lol-html c-api";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/bash;
shellScript = "pushd \"$PROJECT_DIR/lol-html/c-api/\"\n\nif [ \"$PLATFORM_NAME\" == \"iphonesimulator\" ]; then\n if [ \"$ARCHS\" == \"arm64\" ]; then\n export CARGO_TARGET=\"aarch64-apple-ios-sim\"\n elif [ \"$ARCHS\" == \"x86_64\" ]; then\n export CARGO_TARGET=\"x86_64-apple-ios\"\n else\n echo \"error: unknown value for \\$ARCHS\"\n fi\nelif [ \"$PLATFORM_NAME\" == \"iphoneos\" ]; then\n export CARGO_TARGET=\"aarch64-apple-ios\"\nfi\n\necho \"Building lol-html with CARGO_TARGET: $CARGO_TARGET\"\n\n~/.cargo/bin/cargo build --release --target $CARGO_TARGET\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
D6C687E4272CD27600874C10 /* Sources */ = {
isa = PBXSourcesBuildPhase;
@ -506,6 +540,7 @@
D6E24358278B96E40005E546 /* Feed+CoreDataProperties.swift in Sources */,
D65B18BE275051A1004A9448 /* LocalData.swift in Sources */,
D65B18B22750469D004A9448 /* LoginViewController.swift in Sources */,
D68B303627907D9200E8B3FA /* ExcerptGenerator.swift in Sources */,
D6E24363278BA1410005E546 /* ItemCollectionViewCell.swift in Sources */,
D6E2436E278BD8160005E546 /* ReadViewController.swift in Sources */,
D65B18C127505348004A9448 /* HomeViewController.swift in Sources */,
@ -630,6 +665,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
@ -685,6 +721,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
@ -705,6 +742,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZPBBSK8L8B;
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/lol-html/c-api/include/";
INFOPLIST_FILE = Reader/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
@ -715,10 +753,19 @@
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/lol-html/c-api/target/aarch64-apple-ios-sim/release",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/lol-html/c-api/target/aarch64-apple-ios/release/";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/lol-html/c-api/target/aarch64-apple-ios-sim/release/";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/lol-html/c-api/target/x86_64-apple-ios/release/";
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = "-llolhtml";
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.Reader;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Reader/Reader-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@ -734,6 +781,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZPBBSK8L8B;
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/lol-html/c-api/include/";
INFOPLIST_FILE = Reader/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
@ -744,10 +792,19 @@
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/lol-html/c-api/target/aarch64-apple-ios-sim/release",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/lol-html/c-api/target/aarch64-apple-ios/release/";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/lol-html/c-api/target/aarch64-apple-ios-sim/release/";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/lol-html/c-api/target/x86_64-apple-ios/release/";
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = "-llolhtml";
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.Reader;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Reader/Reader-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@ -762,6 +819,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZPBBSK8L8B;
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/lol-html/c-api/include/";
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.ReaderTests;
@ -782,6 +840,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZPBBSK8L8B;
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/lol-html/c-api/include/";
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.ReaderTests;
@ -945,14 +1004,6 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
D6E24365278BA2660005E546 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/scinfu/SwiftSoup";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 2.3.0;
};
};
D6E2436F278BE1250005E546 /* XCRemoteSwiftPackageReference "swift-html-entities" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Kitura/swift-html-entities.git";
@ -964,11 +1015,6 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
D6E24366278BA2660005E546 /* SwiftSoup */ = {
isa = XCSwiftPackageProductDependency;
package = D6E24365278BA2660005E546 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
productName = SwiftSoup;
};
D6E24370278BE1250005E546 /* HTMLEntities */ = {
isa = XCSwiftPackageProductDependency;
package = D6E2436F278BE1250005E546 /* XCRemoteSwiftPackageReference "swift-html-entities" */;

View File

@ -18,6 +18,8 @@ extension Item {
@NSManaged public var author: String?
@NSManaged public var content: String?
@NSManaged public var excerpt: String?
@NSManaged public var generatedExcerpt: Bool
@NSManaged public var id: String?
@NSManaged public var needsReadStateSync: Bool
@NSManaged public var published: Date?

View File

@ -16,6 +16,8 @@
<entity name="Item" representedClassName="Item" syncable="YES">
<attribute name="author" optional="YES" attributeType="String"/>
<attribute name="content" optional="YES" attributeType="String"/>
<attribute name="excerpt" optional="YES" attributeType="String"/>
<attribute name="generatedExcerpt" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="id" attributeType="String"/>
<attribute name="needsReadStateSync" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="published" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
@ -30,7 +32,7 @@
<elements>
<element name="Feed" positionX="-54" positionY="9" width="128" height="119"/>
<element name="Group" positionX="-63" positionY="-18" width="128" height="74"/>
<element name="Item" positionX="-45" positionY="63" width="128" height="164"/>
<element name="Item" positionX="-45" positionY="63" width="128" height="194"/>
<element name="SyncState" positionX="-63" positionY="90" width="128" height="44"/>
</elements>
</model>

View File

@ -0,0 +1,131 @@
//
// ExcerptGenerator.swift
// Reader
//
// Created by Shadowfacts on 1/13/22.
//
import Foundation
import OSLog
import CoreData
// public so that it can be imported in ReaderTests even when Reader is compiled in release mode (w/ testing disabled)
public struct ExcerptGenerator {
private init() {}
private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ExcerptGenerator")
static func generateAll(_ fervorController: FervorController) {
let req = Item.fetchRequest()
req.predicate = NSPredicate(format: "generatedExcerpt = NO")
req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
req.fetchBatchSize = 50
fervorController.persistentContainer.performBackgroundTask { ctx in
guard let items = try? ctx.fetch(req) else { return }
var count = 0
for item in items {
if let excerpt = excerpt(for: item) {
item.excerpt = excerpt
count += 1
if count % 50 == 0 {
logger.debug("Generated \(count, privacy: .public) excerpts")
}
}
item.generatedExcerpt = true
}
logger.log("Generated excerpts for \(count, privacy: .public) items")
if ctx.hasChanges {
do {
// get the updated objects now, because this set is empty after .save is called
let updated = ctx.updatedObjects
try ctx.save()
// make sure the view context has the newly added excerpts
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [
NSUpdatedObjectsKey: Array(updated)
], into: [fervorController.persistentContainer.viewContext])
} catch {
logger.error("Unable to save context: \(error.localizedDescription, privacy: .public)")
}
}
}
}
public static func excerpt(for item: Item) -> String? {
guard let content = item.content else {
return nil
}
return excerpt(from: content)
}
public static func excerpt(from html: String) -> String? {
var html = html
let builder = lol_html_rewriter_builder_new()!
let pSelector = lol_html_selector_parse("p", 1)!
var userData = UserData()
withUnsafeMutablePointer(to: &userData) { userDataPtr in
let rawPtr = UnsafeMutableRawPointer(userDataPtr)
let res = lol_html_rewriter_builder_add_element_content_handlers(builder, pSelector, elementHandler, rawPtr, nil, nil, textHandler, rawPtr)
guard res == 0 else {
lolHtmlError()
}
let memSettings = lol_html_memory_settings_t(preallocated_parsing_buffer_size: 1024, max_allowed_memory_usage: .max)
let rewriter = lol_html_rewriter_build(builder, "utf-8", 5, memSettings, outputSink, nil, true)
lol_html_rewriter_builder_free(builder)
lol_html_selector_free(pSelector)
guard let rewriter = rewriter else {
lolHtmlError()
}
_ = html.withUTF8 { buffer in
buffer.withMemoryRebound(to: CChar.self) { buffer in
lol_html_rewriter_write(rewriter, buffer.baseAddress!, buffer.count)
}
}
}
if userData.isInParagraph {
return userData.paragraphText.htmlUnescape().trimmingCharacters(in: .whitespacesAndNewlines)
// todo: steal css whitespace collapsing from tusker
} else {
return nil
}
}
private static func lolHtmlError() -> Never {
let lastError = lol_html_take_last_error()
let message = String(bytesNoCopy: UnsafeMutableRawPointer(mutating: lastError.data!), length: lastError.len, encoding: .utf8, freeWhenDone: false)
fatalError(message ?? "Unknown lol-html error")
}
}
private struct UserData {
var isInParagraph = false
var paragraphText = ""
}
private func elementHandler(element: OpaquePointer!, userData: UnsafeMutableRawPointer!) -> lol_html_rewriter_directive_t {
let userDataPtr = userData.assumingMemoryBound(to: UserData.self)
if userDataPtr.pointee.isInParagraph {
return LOL_HTML_STOP
} else {
let s = lol_html_element_tag_name_get(element)
let tagName = String(bytesNoCopy: UnsafeMutableRawPointer(mutating: s.data), length: s.len, encoding: .utf8, freeWhenDone: false)!
userDataPtr.pointee.isInParagraph = tagName == "p" || tagName == "P"
lol_html_str_free(s)
return LOL_HTML_CONTINUE
}
}
private func textHandler(chunk: OpaquePointer!, userData: UnsafeMutableRawPointer!) -> lol_html_rewriter_directive_t {
let userDataPtr = userData.assumingMemoryBound(to: UserData.self)
let s = lol_html_text_chunk_content_get(chunk)
let content = String(bytesNoCopy: UnsafeMutableRawPointer(mutating: s.data), length: s.len, encoding: .utf8, freeWhenDone: false)!
userDataPtr.pointee.paragraphText += content
return LOL_HTML_CONTINUE
}
private func outputSink(chunk: UnsafePointer<CChar>!, chunkLen: Int, userData: UnsafeMutableRawPointer!) {
// no-op
}

View File

@ -0,0 +1,13 @@
//
// Reader-Bridging-Header.h
// Reader
//
// Created by Shadowfacts on 1/12/22.
//
#ifndef Reader_Bridging_Header_h
#define Reader_Bridging_Header_h
#import "lol_html.h"
#endif /* Reader_Bridging_Header_h */

View File

@ -86,6 +86,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
} catch {
logger.error("Unable to sync from server: \(error.localizedDescription, privacy: .public)")
}
ExcerptGenerator.generateAll(fervorController)
}
}

View File

@ -6,7 +6,6 @@
//
import UIKit
import SwiftSoup
protocol ItemCollectionViewCellDelegate: AnyObject {
func itemCellSelected(cell: ItemCollectionViewCell, item: Item)
@ -74,13 +73,12 @@ class ItemCollectionViewCell: UICollectionViewListCell {
titleLabel.text = item.title
feedTitleLabel.text = item.feed!.title ?? item.feed!.url?.host
if let content = item.content {
let doc = try! SwiftSoup.parse(content)
contentLabel.text = try! doc.select("p").first()?.text()
if let excerpt = item.excerpt {
contentLabel.text = excerpt
contentLabel.isHidden = false
} else {
contentLabel.text = ""
contentLabel.isHidden = true
}
contentLabel.isHidden = contentLabel.text?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true
updateColors()
}

File diff suppressed because one or more lines are too long

1
lol-html Submodule

@ -0,0 +1 @@
Subproject commit f32bd14b229ed1088c25725cce242817ea2fe43a