/
mrchn
/
craft-ios
Обзор
Документация
Войти
/
mrchn
/
craft-ios
Код
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
modules/document-toolkit/ios/DocumentToolkitModule.swift
439 строк
11 KB
mrchn
added parseTags (Parse analog from Create), createDocument (Create analog)
05 июл 2026, 13:30
05 июл 2026, 13:30
ff75532
Код
Авторство
О чём код?
// @/modules/document-toolkit/ios/DocumentToolkitModule.swift import ExpoModulesCore import ZIPFoundation import UIKit import WebKit public class DocumentToolkitModule: Module { private var webView: WKWebView? private var currentDelegate: NavigationDelegate? public func definition() -> ModuleDefinition { Name("DocumentToolkit") AsyncFunction("convert") { ( docxPath: String, outputPath: String, promise: Promise ) in DispatchQueue.main.async { let docxUrl = URL(fileURLWithPath: docxPath) let outputUrl = URL(fileURLWithPath: outputPath) let webView = WKWebView(frame: CGRect( x: 0, y: 0, width: 816, height: 1056 )) self.webView = webView let delegate = NavigationDelegate( outputUrl: outputUrl, promise: promise ) { [weak self] in self?.webView = nil self?.currentDelegate = nil } self.currentDelegate = delegate webView.navigationDelegate = delegate webView.loadFileURL( docxUrl, allowingReadAccessTo: docxUrl.deletingLastPathComponent() ) } } AsyncFunction("readEntry") { ( zipPath: String, entryName: String, promise: Promise ) in let zipUrl = URL(fileURLWithPath: zipPath) guard let archive = Archive( url: zipUrl, accessMode: .read ) else { promise.reject( "E_ZIP_OPEN", "Cannot open archive at \(zipPath)" ) return } guard let entry = archive[entryName] else { promise.reject( "E_ENTRY_NOT_FOUND", "Entry \(entryName) not found in archive" ) return } var data = Data() do { _ = try archive.extract(entry) { chunk in data.append(chunk) } } catch { promise.reject( "E_EXTRACT_FAILED", error.localizedDescription ) return } guard let text = String( data: data, encoding: .utf8 ) else { promise.reject( "E_DECODE_FAILED", "Entry data is not valid UTF-8" ) return } promise.resolve(text) } AsyncFunction("repackWithEntry") { ( zipPath: String, entryName: String, newContent: String, outputPath: String, promise: Promise ) in let sourceUrl = URL(fileURLWithPath: zipPath) let outputUrl = URL(fileURLWithPath: outputPath) let fm = FileManager() do { if fm.fileExists(atPath: outputUrl.path) { try fm.removeItem(at: outputUrl) } try fm.copyItem(at: sourceUrl, to: outputUrl) } catch { promise.reject( "E_COPY_FAILED", error.localizedDescription ) return } guard let archive = Archive( url: outputUrl, accessMode: .update ) else { promise.reject( "E_ZIP_OPEN", "Cannot open archive at \(outputPath)" ) return } guard let newData = newContent.data(using: .utf8) else { promise.reject( "E_ENCODE_FAILED", "Could not encode new content as UTF-8" ) return } do { if let existingEntry = archive[entryName] { try archive.remove(existingEntry) } try archive.addEntry( with: entryName, type: .file, uncompressedSize: Int64(newData.count), provider: { position, size in let start = Int(position) let end = start + size return newData.subdata(in: start..<end) } ) } catch { try? fm.removeItem(at: outputUrl) promise.reject( "E_REPACK_FAILED", error.localizedDescription ) return } promise.resolve(outputUrl.absoluteString) } AsyncFunction("createDocument") { ( docxPath: String, data: [String: String], title: String, promise: Promise ) in let entryName = "word/document.xml" let selUrl = URL(fileURLWithPath: docxPath) let tmpUrl = selUrl.deletingLastPathComponent() .appendingPathComponent("tmp.docx") let pdfUrl = selUrl.deletingLastPathComponent() .appendingPathComponent("\(title).pdf") let fm = FileManager() for url in [tmpUrl, pdfUrl] { try? fm.removeItem(at: url) } guard let archive = Archive( url: selUrl, accessMode: .read ) else { promise.reject( "E_ZIP_OPEN", "Cannot open archive at \(docxPath)" ) return } guard let entry = archive[entryName] else { promise.reject( "E_ENTRY_NOT_FOUND", "Entry \(entryName) not found" ) return } var xmlData = Data() do { _ = try archive.extract(entry) { chunk in xmlData.append(chunk) } } catch { promise.reject( "E_EXTRACT_FAILED", error.localizedDescription ) return } guard var xml = String( data: xmlData, encoding: .utf8 ) else { promise.reject( "E_DECODE_FAILED", "Entry data is not valid UTF-8" ) return } for (key, value) in data { let escapedKey = NSRegularExpression.escapedPattern( for: key ) let tagPattern = "(?:<[^>]+>)*" let charsPattern = escapedKey.map { String($0) } .joined(separator: tagPattern) let pattern = "\\{\(tagPattern)\\{\(tagPattern)\\s*\(tagPattern)\(charsPattern)\\s*\(tagPattern)\\}\(tagPattern)\\}" let safeValue = value .replacingOccurrences(of: "&", with: "&") .replacingOccurrences(of: "<", with: "<") .replacingOccurrences(of: ">", with: ">") .replacingOccurrences(of: "\"", with: """) .replacingOccurrences(of: "'", with: "'") guard let regex = try? NSRegularExpression( pattern: pattern ) else { continue } let range = NSRange(xml.startIndex..., in: xml) xml = regex.stringByReplacingMatches( in: xml, range: range, withTemplate: safeValue ) } do { try fm.copyItem(at: selUrl, to: tmpUrl) } catch { promise.reject( "E_COPY_FAILED", error.localizedDescription ) return } guard let repackArchive = Archive( url: tmpUrl, accessMode: .update ) else { promise.reject( "E_ZIP_OPEN", "Cannot open archive at \(tmpUrl.path)" ) return } guard let newData = xml.data(using: .utf8) else { promise.reject( "E_ENCODE_FAILED", "Could not encode new content" ) return } do { if let existingEntry = repackArchive[entryName] { try repackArchive.remove(existingEntry) } try repackArchive.addEntry( with: entryName, type: .file, uncompressedSize: Int64(newData.count), provider: { position, size in let start = Int(position) return newData.subdata( in: start..<start + size ) } ) } catch { try? fm.removeItem(at: tmpUrl) promise.reject( "E_REPACK_FAILED", error.localizedDescription ) return } DispatchQueue.main.async { let webView = WKWebView( frame: CGRect(x: 0, y: 0, width: 816, height: 1056) ) self.webView = webView let delegate = NavigationDelegate( outputUrl: pdfUrl, promise: promise ) { [weak self] in self?.webView = nil self?.currentDelegate = nil DispatchQueue.main.async { guard let topVC = UIApplication .shared .connectedScenes .compactMap({($0 as? UIWindowScene)?.keyWindow?.rootViewController }) .first else { return } let activityVC = UIActivityViewController( activityItems: [pdfUrl], applicationActivities: nil ) activityVC.completionWithItemsHandler = { _, _, _, _ in try? fm.removeItem(at: tmpUrl) try? fm.removeItem(at: pdfUrl) } topVC.present(activityVC, animated: true) } } self.currentDelegate = delegate webView.navigationDelegate = delegate webView.loadFileURL( tmpUrl, allowingReadAccessTo: tmpUrl.deletingLastPathComponent() ) } } AsyncFunction("parseTags") { ( docxPath: String, promise: Promise ) in let entryName = "word/document.xml" let zipUrl = URL(fileURLWithPath: docxPath) guard let archive = Archive( url: zipUrl, accessMode: .read ) else { promise.reject( "E_ZIP_OPEN", "Cannot open archive at \(docxPath)" ) return } guard let entry = archive[entryName] else { promise.reject( "E_ENTRY_NOT_FOUND", "Entry \(entryName) not found" ) return } var data = Data() do { _ = try archive.extract(entry) { chunk in data.append(chunk) } } catch { promise.reject( "E_EXTRACT_FAILED", error.localizedDescription ) return } guard let xml = String( data: data, encoding: .utf8 ) else { promise.reject( "E_DECODE_FAILED", "Entry data is not valid UTF-8" ) return } let stripped = xml.replacingOccurrences( of: "<[^>]+>", with: "", options: .regularExpression ) guard let tagRegex = try? NSRegularExpression( pattern: "\\{\\{([^\\{\\}]+?)\\}\\}" ) else { promise.resolve([String]()) return } let range = NSRange( stripped.startIndex..., in: stripped ) let matches = tagRegex.matches( in: stripped, range: range ) var tags = [String]() var seen = Set<String>() for match in matches { guard let r = Range( match.range, in: stripped ) else { continue } let cleaned = String(stripped[r]) .replacingOccurrences( of: "[\\{\\}\\s\u{00a0}]", with: "", options: .regularExpression ) if !cleaned.isEmpty && !seen.contains(cleaned) { seen.insert(cleaned) tags.append(cleaned) } } promise.resolve(tags) } } } class NavigationDelegate: NSObject, WKNavigationDelegate { let outputUrl: URL let promise: Promise let onComplete: () -> Void init( outputUrl: URL, promise: Promise, onComplete: @escaping () -> Void ) { self.outputUrl = outputUrl self.promise = promise self.onComplete = onComplete } func webView( _ webView: WKWebView, didFinish navigation: WKNavigation! ) { DispatchQueue.main.asyncAfter( deadline: .now() + 0.5 ) { [weak self] in guard let self = self else { return } webView.createPDF( configuration: WKPDFConfiguration() ) { [weak self] result in guard let self = self else { return } switch result { case .success(let data): do { try data.write(to: self.outputUrl) self.promise.resolve( self.outputUrl.absoluteString ) } catch { self.promise.reject( "E_WRITE_FAILED", error.localizedDescription ) } case .failure(let error): self.promise.reject( "E_PDF_FAILED", error.localizedDescription ) } self.onComplete() } } } private func fail(_ message: String) { promise.reject("E_LOAD_FAILED", message) onComplete() } func webView( _ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error ) { fail(error.localizedDescription) } func webView( _ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error ) { fail(error.localizedDescription) } }