Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Ads/Linked.swift
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/// Read through ``Features``.
let LINKS_ADS = true
/// Read through ``Features``.
let ADVANCED_EDITING = false
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ once the version tag exists.
### Added

- The name of the open document is shown at the top, between the buttons.
- Editing tools under the bar: bold, italic, underline, strikethrough, text
colour, highlight, text size, undo and redo.
- A PDF can be marked up with highlights, lines and drawings.
- Presentations, Excel files and plain text files can be edited and saved.

### Changed

- The engine is odrcore 7.1.0, up from 6.13.0.
- Entering an edit keeps your place in the document.
- The pencil turns editing on and off, and a new save button saves without
leaving the edit.
- Lite edits text inside a paragraph. Formatting, paragraphs and PDF marks are
part of Pro.

### Fixed

Expand Down
2 changes: 2 additions & 0 deletions NoAds/Linked.swift
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/// Read through ``Features``.
let LINKS_ADS = false
/// Read through ``Features``.
let ADVANCED_EDITING = true
2 changes: 1 addition & 1 deletion OpenDocumentReader.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,7 @@
repositoryURL = "https://github.com/opendocument-app/OpenDocument.core.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 6.13.0;
minimumVersion = 7.1.0;
};
};
AD584FCD41577C8CDEE974AA /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

98 changes: 75 additions & 23 deletions OpenDocumentReader/CoreWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,19 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [
/// Whether odrcore saw only a container, so the page is a listing of what is inside it.
@objc private(set) var isArchive = false

/// Whether `backTranslate` has a document to apply an edit to. Only a
/// document that said it takes one is kept, so having it *is* the answer.
@objc var isEditable: Bool { lock.withLock { document != nil } }
/// Whether `save` has something to apply an edit to. Only a file that said
/// it takes one is kept, so having it *is* the answer.
@objc var isEditable: Bool { lock.withLock { document != nil || textFile != nil } }

/// Whether the file is a pdf that takes marks.
@objc var isAnnotatable: Bool { lock.withLock { pdfFile != nil } }

/// Whether the file is plain text, which takes typing but no formatting.
@objc var isPlainText: Bool { lock.withLock { textFile != nil } }

private var document: OdrCoreObjC.Document?
private var textFile: TextFile?
private var pdfFile: PdfFile?
private let lock = NSRecursiveLock()

/// The largest sheet region translated, as on OpenDocument.droid.
Expand All @@ -99,17 +107,19 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [

@objc func translate(
_ inputPath: String,
cache cachePath: String,
into outputPath: String,
with password: String?,
editable: Bool
editable: Bool,
scope: HtmlEditingScope
) throws {
lock.lock()
defer { lock.unlock() }

pageNames = []
pageURLs = []
document = nil
textFile = nil
pdfFile = nil
isArchive = false

let fileTypes = (try? DecodedFile.listFileTypes(path: inputPath)) ?? []
Expand Down Expand Up @@ -141,6 +151,9 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [
// decided from these
let config = HtmlConfig()
config.editable = editable
// how far an edit may reach: inside one paragraph, or across the
// document with formatting
config.editingScope = scope
// resource paths are resolved relative to an output directory, and in
// server mode there is none — odrcore rejects the combination
config.relativeResourcePaths = false
Expand All @@ -167,6 +180,8 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [

let documentType: DocumentType
let openedDocument: OdrCoreObjC.Document?
var openedTextFile: TextFile?
var openedPdfFile: PdfFile?
let service: HtmlService

if file.isDocumentFile {
Expand All @@ -177,15 +192,23 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [
// the document's own answer: a format odrcore renders but cannot write
// back would otherwise offer Edit and fail at the save
openedDocument = document.isEditable && document.isSavable ? document : nil
service = try HtmlTranslator.translate(
document: document, cachePath: cachePath, config: config)
service = try HtmlTranslator.translate(document: document, config: config)
} else {
// nothing to edit, and `.unknown` keeps the single view each of
// these has - `.spreadsheet` would ask for a tab per sheet
// `.unknown` keeps the single view each of these has -
// `.spreadsheet` would ask for a tab per sheet
documentType = .unknown
openedDocument = nil
service = try HtmlTranslator.translate(
file: file, cachePath: cachePath, config: config)

if file.isTextFile, let text = try? file.asTextFile(), text.isSavable {
openedTextFile = text
}
if file.isPdfFile, file.capabilities.annotate, let pdf = try? file.asPdfFile(),
pdf.isAnnotatable
{
openedPdfFile = pdf
}

service = try HtmlTranslator.translate(file: file, config: config)
}

let views = selectViews(service.views, documentType)
Expand All @@ -197,39 +220,68 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [
throw coreWrapperError(.unknown, "could not serve the translated document")
}

// only once nothing can throw any more: backTranslate must not be handed
// a document whose pages were never served
// only once nothing can throw any more: a save must not be handed a
// file whose pages were never served
self.document = openedDocument
self.textFile = openedTextFile
self.pdfFile = openedPdfFile

isArchive = file.isArchiveFile
pageNames = views.map(\.name)
pageURLs = views.map { base.appendingPathComponent($0.path) }
}

@objc func backTranslate(_ diff: String, into outputPath: String) throws {
/// The script the page hands its edits back through: the editor's log for
/// a document or a text file, the marks for a pdf.
@objc var editPayloadScript: String {
isAnnotatable ? "odr.annotation.getAnnotations()" : "odr.editing.getOperations()"
}

/// Writes the file with `payload` applied - the page's operations, or its
/// marks for a pdf.
@objc func save(_ payload: String, into outputPath: String) throws {
lock.lock()
defer { lock.unlock() }

guard let document else {
throw coreWrapperError(.unknown, "no document has been translated yet")
}

try HtmlTranslator.edit(document: document, diff: diff)

// odrcore streams the parts the edit did not touch out of the file it opened, and
// truncates the destination first - so saving onto the open document empties it
// odrcore streams the parts an edit did not touch out of the file it opened, and
// truncates the destination first - so saving onto the open file empties it
let output = URL(fileURLWithPath: outputPath)

let staging = try stagingDirectory(for: output)
defer { try? FileManager.default.removeItem(at: staging) }

let temporary = stagedFile(in: staging, for: output)

try document.save(to: temporary.path)
if let document {
if Self.holdsOperations(payload) {
try document.edit(operations: payload)
}
try document.save(to: temporary.path)
} else if let textFile {
try textFile.edit(operations: payload)
try textFile.save(to: temporary.path)
} else if let pdfFile {
try pdfFile.annotate(payload).write(to: temporary)
} else {
throw coreWrapperError(.unknown, "no editable file has been translated yet")
}

try moveIntoPlace(from: temporary, to: output)
}

/// Whether the envelope carries any operation: an empty one is a save of
/// the file as it is.
private static func holdsOperations(_ payload: String) -> Bool {
guard let data = payload.data(using: .utf8),
let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let ops = envelope["ops"] as? [Any]
else {
return true
}

return !ops.isEmpty
}

/// A directory on `output`'s own volume, because `replaceItemAt` cannot swap across one.
/// The caller has to delete it.
private func stagingDirectory(for output: URL) throws -> URL {
Expand Down
56 changes: 45 additions & 11 deletions OpenDocumentReader/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ protocol DocumentDelegate: AnyObject {
func documentLoadingStarted(_ doc: Document)
func documentLoadingCompleted(_ doc: Document)
func documentPagesChanged(_ doc: Document)
/// The edit mode was turned on. The page is the one already on screen.
func documentEditingStarted(_ doc: Document)
/// The edit mode was turned off, and the page on screen stays: it holds
/// nothing the file does not.
func documentEditingEnded(_ doc: Document)
}

enum DocumentError: Error {
Expand Down Expand Up @@ -39,19 +44,45 @@ class Document: UIDocument {
parse()
}
}
/// Entering an edit turns it on in the page. Leaving renders the file
/// again, unless ``endEdit(renderingAgain:)`` says not to.
public var edit = false {
didSet {
parse()
if edit {
notify { $0.documentEditingStarted(self) }
} else if rendersAgainOnLeave {
parse()
} else {
notify { $0.documentEditingEnded(self) }
}
}
}

private var rendersAgainOnLeave = true

/// Leaves the edit. Without `renderingAgain` only the mode goes off.
func endEdit(renderingAgain: Bool) {
rendersAgainOnLeave = renderingAgain
edit = false
rendersAgainOnLeave = true
}

/// Renders the file again and keeps the mode: what a save stays in.
func reload() {
parse()
}

public var webview: WKWebView?

public var isOdf = false
/// Whether the page is a listing of an archive - see `CoreWrapper.isArchive`.
public var isArchive = false
/// Whether the menu should offer to edit this one - see `CoreWrapper.isEditable`.
public var isEditable = false
/// Whether this is a pdf that takes marks - see `CoreWrapper.isAnnotatable`.
public var isAnnotatable = false
/// Whether this is plain text - see `CoreWrapper.isPlainText`.
public var isPlainText = false
private var wasPageCountAnnounced = false

override func load(fromContents contents: Any, ofType typeName: String?) throws {
Expand All @@ -66,19 +97,19 @@ class Document: UIDocument {
isOdf = false
isArchive = false
isEditable = false
isAnnotatable = false
isPlainText = false
result = nil
pageURLs = nil
notify { $0.documentUpdateContent(self) }

let temporaryDirectory = NSTemporaryDirectory()

do {
try coreWrapper.translate(
fileURL.path,
cache: temporaryDirectory,
into: temporaryDirectory,
into: NSTemporaryDirectory(),
with: password,
editable: edit
editable: true,
scope: Features.advancedEditing ? .document : .paragraph
)
} catch let error as NSError
where error.domain == CoreWrapperErrorDomain
Expand All @@ -96,6 +127,8 @@ class Document: UIDocument {
isOdf = true
isArchive = coreWrapper.isArchive
isEditable = coreWrapper.isEditable
isAnnotatable = coreWrapper.isAnnotatable
isPlainText = coreWrapper.isPlainText

loadProgress.completedUnitCount = loadProgress.totalUnitCount

Expand Down Expand Up @@ -152,12 +185,12 @@ class Document: UIDocument {
override func writeContents(
_ contents: Any, to url: URL, for saveOperation: UIDocument.SaveOperation, originalContentsURL: URL?
) throws {
let diff = try generateDiff()
let payload = try collectEdits()

// the document handle CoreWrapper holds is only valid together with the
// web view that produced the diff, so the edit stays on the main thread
// web view that produced the edits, so the save stays on the main thread
try onMainThread {
try coreWrapper.backTranslate(diff, into: url.path)
try coreWrapper.save(payload, into: url.path)
}
}

Expand All @@ -173,9 +206,10 @@ class Document: UIDocument {

/// Blocks the calling save thread until the web view has handed back the
/// edits the user made.
private func generateDiff() throws -> String {
private func collectEdits() throws -> String {
let semaphore = DispatchSemaphore(value: 0)
var result: Result<String, Error> = .failure(DocumentError.getHtml)
let script = coreWrapper.editPayloadScript

DispatchQueue.main.async {
guard let webview = self.webview else {
Expand All @@ -185,7 +219,7 @@ class Document: UIDocument {
return
}

webview.evaluateJavaScript("odr.generateDiff()") { value, error in
webview.evaluateJavaScript(script) { value, error in
defer { semaphore.signal() }

if let error {
Expand Down
Loading