APIDocViewControllers.swift 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. //
  2. // APIDocRootViewController.swift
  3. // C Code Develop
  4. //
  5. // Created by xcbosa on 2022/6/20.
  6. // Copyright © 2022 xcbosa. All rights reserved.
  7. //
  8. import UIKit
  9. import FastLayout
  10. import WebKit
  11. public extension String {
  12. func apiDocLocalized(_ language: ElementDefs.SupportedLanguage = .unspecified) -> String {
  13. switch language {
  14. case .unspecified:
  15. return NSLocalizedString(self, tableName: "APIDocLocalizable", bundle: .main, value: "", comment: "")
  16. case .english:
  17. return Bundle(path: Bundle.main.path(forResource: "en", ofType: "lproj") ?? "")?.localizedString(forKey: self, value: "", table: "APIDocLocalizable") ?? ""
  18. case .chinese:
  19. return Bundle(path: Bundle.main.path(forResource: "zh-Hans", ofType: "lproj") ?? "")?.localizedString(forKey: self, value: "", table: "APIDocLocalizable") ?? ""
  20. }
  21. }
  22. }
  23. public extension Array where Element == String {
  24. func localized(_ language: ElementDefs.SupportedLanguage) -> String {
  25. var index = 0
  26. switch (language) {
  27. case .unspecified:
  28. if NSLocalizedString("UpdateAssets", comment: "en.rtf") == "zh.rtf" {
  29. index = 1
  30. } else {
  31. index = 0
  32. }
  33. break
  34. case .chinese:
  35. index = 1
  36. break
  37. case .english:
  38. index = 0
  39. break
  40. }
  41. return self[index]
  42. }
  43. }
  44. public class APIDocRootViewController: UISplitViewController {
  45. public var collectionModel: [APIDocFileCollectionModel] = []
  46. public var collectionModelChangeBlock: (() -> Void)?
  47. public private(set) var hasXmarkButton: Bool = false
  48. public private(set) var useNewSplitViewController: Bool
  49. public var legacyNavigationViewController: UINavigationController?
  50. public private(set) var didAPIDocLoaded = false
  51. private var executeWhenLoadedQueue = [() -> Void]()
  52. public func executeWhenLoaded(_ block: @escaping () -> Void) {
  53. if didAPIDocLoaded {
  54. block()
  55. } else {
  56. executeWhenLoadedQueue.append(block)
  57. }
  58. }
  59. public func provideRootCollectionModel() -> [APIDocFileCollectionModel] { collectionModel }
  60. public init(hasXmarkButton: Bool) {
  61. // if #available(iOS 14.0, *) {
  62. // self.useNewSplitViewController = true
  63. // super.init(style: .doubleColumn)
  64. // super.preferredSplitBehavior = .tile
  65. // } else {
  66. self.useNewSplitViewController = false
  67. super.init(nibName: nil, bundle: nil)
  68. // }
  69. super.preferredDisplayMode = .oneBesideSecondary
  70. super.primaryBackgroundStyle = .sidebar
  71. super.viewControllers = [ADLeftNavigationController(parent: self)]
  72. self.hasXmarkButton = hasXmarkButton
  73. }
  74. required init?(coder aDecoder: NSCoder) {
  75. fatalError("init(coder:) has not been implemented")
  76. }
  77. public func doFetching() {
  78. let fetchingAlert = UIAlertController(title: "APIDoc.FetchingAlert.Title".apiDocLocalized(),
  79. message: "APIDoc.FetchingAlert.Text".apiDocLocalized(),
  80. preferredStyle: .alert)
  81. fetchingAlert.regThemeManagerResponser()
  82. self.present(fetchingAlert, animated: true) {
  83. DispatchQueue.global(qos: .utility).async {
  84. self.collectionModel = APIDocFetcher.fetchAll(withGivenProjectContext: self.project)
  85. DispatchQueue.main.async {
  86. self.didAPIDocLoaded = true
  87. self.executeWhenLoadedQueue.forEach({ $0() })
  88. fetchingAlert.dismiss(animated: true) {
  89. self.collectionModelChangeBlock?()
  90. }
  91. }
  92. }
  93. }
  94. }
  95. public override func viewDidLoad() {
  96. self.regThemeManagerResponser()
  97. DispatchQueue.main.async {
  98. self.doFetching()
  99. }
  100. }
  101. public func openSecondary(viewController vc: UIViewController, replaceCurrentRightStack replace: Bool) {
  102. if useNewSplitViewController {
  103. if #available(iOS 14.0, *) {
  104. self.setViewController(vc, for: .secondary)
  105. self.show(.secondary)
  106. }
  107. } else {
  108. if replace {
  109. self.legacyNavigationViewController = nil
  110. }
  111. if let navVc = self.legacyNavigationViewController {
  112. self.legacyNavigationViewController?.pushViewController(vc, animated: true)
  113. ocIgnoreException {
  114. self.showDetailViewController(navVc, sender: nil)
  115. }
  116. } else {
  117. let navVc = UINavigationController(rootViewController: vc)
  118. self.legacyNavigationViewController = navVc
  119. self.showDetailViewController(navVc, sender: nil)
  120. }
  121. }
  122. }
  123. }
  124. extension APIDocRootViewController: AbstractWindow {
  125. public var filePath: String { "APIDoc.Title".apiDocLocalized() }
  126. public var type: AbstractWindowType { .draggableTools }
  127. }
  128. public class ADLeftNavigationController: UINavigationController {
  129. public var parentController: APIDocRootViewController
  130. public init(parent: APIDocRootViewController) {
  131. self.parentController = parent
  132. super.init(nibName: nil, bundle: nil)
  133. self.pushViewController(ADListViewController(parent: self), animated: false)
  134. }
  135. required init?(coder aDecoder: NSCoder) {
  136. fatalError("init(coder:) has not been implemented")
  137. }
  138. public override func viewDidLoad() {
  139. self.regThemeManagerResponser()
  140. self.navigationBar.prefersLargeTitles = true
  141. }
  142. }
  143. public class ADListViewController: UITableViewController {
  144. public var parentController: ADLeftNavigationController
  145. private lazy var resultViewController = ADSearchController(listViewController: self)
  146. public var collectionModel: [APIDocFileCollectionModel] {
  147. get { self.parentController.parentController.collectionModel }
  148. set { self.parentController.parentController.collectionModel = newValue }
  149. }
  150. public init(parent: ADLeftNavigationController) {
  151. self.parentController = parent
  152. super.init(style: .insetGrouped)
  153. }
  154. required init?(coder: NSCoder) {
  155. fatalError("init(coder:) has not been implemented")
  156. }
  157. private var btnExpandOrCollapse: UIBarButtonItem?
  158. public override func viewDidLoad() {
  159. self.regThemeManagerResponser()
  160. self.navigationItem.title = "APIDoc.Title".apiDocLocalized()
  161. self.navigationItem.hidesSearchBarWhenScrolling = false
  162. self.navigationItem.searchController = UISearchController(searchResultsController: resultViewController)
  163. self.navigationItem.searchController?.searchResultsUpdater = resultViewController
  164. self.navigationItem.largeTitleDisplayMode = .always
  165. self.navigationItem.leftBarButtonItems = []
  166. if parentController.parentController.hasXmarkButton {
  167. let btnXmark = UIBarButtonItem(image: UIImage(systemName: "xmark"), style: .plain, target: self, action: #selector(btnXmarkTouched(_:)))
  168. btnXmark.tintColor = .systemRed
  169. self.navigationItem.leftBarButtonItems?.append(btnXmark)
  170. }
  171. if !parentController.parentController.useNewSplitViewController && splitViewControllerIsIpadMode() {
  172. let btnExpandOrCollapse = UIBarButtonItem(image: UIImage(systemName: "sidebar.left"), style: .plain, target: self, action: #selector(btnSidebar(_:)))
  173. self.navigationItem.leftBarButtonItems?.append(btnExpandOrCollapse)
  174. self.btnExpandOrCollapse = btnExpandOrCollapse
  175. }
  176. self.navigationItem.rightBarButtonItems = [
  177. UIBarButtonItem(image: UIImage(systemName: "arrow.clockwise"), style: .plain, target: self, action: #selector(btnRefresh(_:)))
  178. ]
  179. self.tableView.register(ADFileCell.self, forCellReuseIdentifier: "ADFileCell")
  180. self.parentController.parentController.collectionModelChangeBlock = {
  181. [weak self] in
  182. self?.tableView.reloadData()
  183. }
  184. }
  185. @objc private func btnSidebar(_ any: Any) {
  186. let splitVc = self.parentController.parentController
  187. splitVc.preferredDisplayMode = splitVc.preferredDisplayMode == .oneBesideSecondary ? .oneOverSecondary : .oneBesideSecondary
  188. if splitVc.preferredDisplayMode == .oneBesideSecondary {
  189. self.btnExpandOrCollapse?.image = UIImage(systemName: "sidebar.left")
  190. } else {
  191. self.btnExpandOrCollapse?.image = UIImage(systemName: "rectangle")
  192. }
  193. }
  194. @objc private func btnXmarkTouched(_ any: Any) {
  195. self.parentController.parentController.dismiss(animated: true)
  196. }
  197. @objc private func btnRefresh(_ any: Any) {
  198. self.parentController.parentController.doFetching()
  199. }
  200. public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  201. return self.collectionModel[section].collectionTitle
  202. }
  203. public override func numberOfSections(in tableView: UITableView) -> Int {
  204. return self.collectionModel.count
  205. }
  206. public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  207. return self.collectionModel[section].files.count
  208. }
  209. public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  210. if let view = tableView.dequeueReusableCell(withIdentifier: "ADFileCell", for: indexPath) as? ADFileCell {
  211. view.fill(data: self.collectionModel[indexPath.section].files[indexPath.row])
  212. return view
  213. }
  214. return UITableViewCell()
  215. }
  216. public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  217. if indexPath.section >= 0 && indexPath.section < self.collectionModel.count {
  218. if indexPath.row >= 0 && indexPath.row < self.collectionModel[indexPath.section].files.count {
  219. let obj = self.collectionModel[indexPath.section].files[indexPath.row]
  220. openAPIDocObject(obj, replaceCurrentRightStack: true)
  221. }
  222. }
  223. }
  224. public func openAPIDocObject(_ apiDoc: APIDocMarkdownGenerable, replaceCurrentRightStack: Bool) {
  225. var data = apiDoc
  226. data.provideRootMarkdownBlock = {
  227. [weak self] in self?.collectionModel ?? []
  228. }
  229. let subVc = APIDocExplorerViewController(withAPIDocFile: data)
  230. self.parentController.parentController.openSecondary(viewController: subVc, replaceCurrentRightStack: replaceCurrentRightStack)
  231. }
  232. }
  233. public class ADSearchController: UITableViewController, UISearchResultsUpdating {
  234. public class SearchModel {
  235. public private(set) var fileModel: APIDocFileModel?
  236. public private(set) var elementModel: APIDocMemberModel?
  237. public init(withFile file: APIDocFileModel) {
  238. self.fileModel = file
  239. }
  240. public init(withElement element: APIDocMemberModel) {
  241. self.elementModel = element
  242. }
  243. public var title: String {
  244. if let fileModel = fileModel {
  245. return fileModel.fileName
  246. } else {
  247. return elementModel?.member.name ?? ""
  248. }
  249. }
  250. public var description: String {
  251. if let fileModel = fileModel {
  252. return fileModel.firstComment.getLocalizedComment(withKey: "filesummary")
  253. } else {
  254. return elementModel?.member.definition ?? ""
  255. }
  256. }
  257. public var renderCode: Bool {
  258. return elementModel != nil
  259. }
  260. }
  261. let sectionList = [
  262. "Header",
  263. ElementDefs.ElementType.typeDefinition.rawValue,
  264. ElementDefs.ElementType.structDefinition.rawValue,
  265. ElementDefs.ElementType.functionDefintion.rawValue,
  266. ElementDefs.ElementType.variableDefinition.rawValue,
  267. ElementDefs.ElementType.functionPointer.rawValue
  268. ]
  269. var model = [[SearchModel](),
  270. [SearchModel](),
  271. [SearchModel](),
  272. [SearchModel](),
  273. [SearchModel](),
  274. [SearchModel]()]
  275. public func updateSearchResults(for searchController: UISearchController) {
  276. guard let listViewController = self.listViewController else { return }
  277. self.listViewController?.tableView.isHidden = !(searchController.searchBar.text ?? "").isEmpty
  278. if let text = searchController.searchBar.text {
  279. let lowerText = text.lowercased()
  280. for id in 0..<model.count {
  281. model[id].removeAll()
  282. }
  283. for collection in listViewController.collectionModel {
  284. for file in collection.files {
  285. if file.fileName.lowercased().contains(lowerText) ||
  286. file.firstComment.getLocalizedComment(withKey: "filesummary").lowercased().contains(lowerText) {
  287. model[0].append(SearchModel(withFile: file))
  288. }
  289. for element in file.items {
  290. if element.name.lowercased().contains(lowerText) ||
  291. element.getLocalizedComment(withKey: nil).lowercased().contains(lowerText) {
  292. let elementModel = APIDocMemberModel(element, inFile: file.fileName, inCollection: collection.collectionStoragePath)
  293. let searchModel = SearchModel(withElement: elementModel)
  294. switch element.type {
  295. case .functionDefintion:
  296. model[3].append(searchModel)
  297. break
  298. case .structDefinition:
  299. model[2].append(searchModel)
  300. break
  301. case .typeDefinition:
  302. model[1].append(searchModel)
  303. break
  304. case .variableDefinition:
  305. model[4].append(searchModel)
  306. break
  307. case .anyToken:
  308. break
  309. case .functionPointer:
  310. model[5].append(searchModel)
  311. case .specialReserved:
  312. break
  313. default: break // TODO: union
  314. }
  315. }
  316. }
  317. }
  318. }
  319. self.tableView.reloadData()
  320. }
  321. }
  322. public weak var listViewController: ADListViewController?
  323. public init(listViewController: ADListViewController?) {
  324. self.listViewController = listViewController
  325. super.init(style: .insetGrouped)
  326. self.tableView.register(ADFileCell.self, forCellReuseIdentifier: "ADFileCell")
  327. }
  328. required init?(coder: NSCoder) {
  329. fatalError("init(coder:) has not been implemented")
  330. }
  331. public override func viewDidLoad() {
  332. self.regThemeManagerResponser()
  333. }
  334. public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  335. "APIDoc.\(sectionList[section])".apiDocLocalized()
  336. }
  337. public override func numberOfSections(in tableView: UITableView) -> Int {
  338. model.count
  339. }
  340. public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  341. return model[section].count
  342. }
  343. public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  344. if let cell = tableView.dequeueReusableCell(withIdentifier: "ADFileCell", for: indexPath) as? ADFileCell {
  345. cell.fill(data: model[indexPath.section][indexPath.row])
  346. return cell
  347. }
  348. return ADFileCell()
  349. }
  350. public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  351. let data = self.model[indexPath.section][indexPath.row]
  352. if let fileModel = data.fileModel {
  353. listViewController?.openAPIDocObject(fileModel, replaceCurrentRightStack: true)
  354. }
  355. if let elementModel = data.elementModel {
  356. listViewController?.openAPIDocObject(elementModel, replaceCurrentRightStack: false)
  357. }
  358. }
  359. }
  360. public class ADFileCell: UITableViewCell {
  361. public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
  362. super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
  363. }
  364. required init?(coder: NSCoder) {
  365. fatalError("init(coder:) has not been implemented")
  366. }
  367. public func fill(data: APIDocFileModel) {
  368. self.textLabel?.text = data.fileName
  369. self.textLabel?.font = .systemFont(ofSize: 18)
  370. self.detailTextLabel?.text = data.firstComment.getLocalizedComment(withKey: "filesummary")
  371. self.detailTextLabel?.numberOfLines = 3
  372. self.detailTextLabel?.font = .systemFont(ofSize: 12)
  373. }
  374. public func fill(data: ADSearchController.SearchModel) {
  375. self.textLabel?.text = data.title
  376. self.textLabel?.font = .systemFont(ofSize: 18)
  377. self.detailTextLabel?.text = "\(data.description)"
  378. self.detailTextLabel?.numberOfLines = 3
  379. self.detailTextLabel?.font = .systemFont(ofSize: 12)
  380. if data.renderCode {
  381. self.detailTextLabel?.text = " \(data.description)"
  382. let draw = CodeEditorDelegate(" \(data.description)", "snapshot.c", withProject: self.firstViewController()?.project)
  383. draw.updateHighlight_C_H(textView: self.detailTextLabel!)
  384. draw.deinitTimer()
  385. self.detailTextLabel?.attributedText = self.detailTextLabel?.attributedText?.attributedSubstring(from: NSRange(location: 1, length: data.description.count - 1))
  386. self.detailTextLabel?.font = UIFont(name: "Menlo", size: 12)
  387. }
  388. }
  389. }
  390. public class APIDocExplorerViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, UIScrollViewDelegate {
  391. public private(set) var apiDoc: APIDocMarkdownGenerable
  392. private var isPreviewLoaded: Bool = false
  393. public var openSecondaryBlock: ((APIDocExplorerViewController) -> Void)?
  394. var previewNotLoadedStoragedRequest: String? = nil
  395. public var inlineConstraints = [NSLayoutConstraint]()
  396. public init(withAPIDocFile file: APIDocMarkdownGenerable) {
  397. self.apiDoc = file
  398. super.init(nibName: nil, bundle: nil)
  399. }
  400. required init?(coder: NSCoder) {
  401. fatalError("init(coder:) has not been implemented")
  402. }
  403. public lazy var webView: WKWebView = {
  404. let view = WKWebView()
  405. view.loadFileURL(URL(fileURLWithPath: Bundle.main.resourcePath! + "/MarkdownView/index.html"), allowingReadAccessTo: URL(fileURLWithPath: Bundle.main.resourcePath! + "/MarkdownView"))
  406. view.uiDelegate = self
  407. view.navigationDelegate = self
  408. view.scrollView.delegate = self
  409. return view
  410. }()
  411. private lazy var webViewMaskView: UIView = {
  412. let view = UIView()
  413. view.backgroundColor = .systemBackground
  414. return view
  415. }()
  416. public override func viewDidLoad() {
  417. self.view.backgroundColor = .systemBackground
  418. self.view.addSubview(webView)
  419. webView.left == self.view.leftAnchor
  420. webView.top == self.view.topAnchor
  421. webView.right == self.view.rightAnchor
  422. webView.bottom == self.view.bottomAnchor
  423. self.view.addSubview(webViewMaskView)
  424. webViewMaskView.left == self.view.leftAnchor
  425. webViewMaskView.right == self.view.rightAnchor
  426. webViewMaskView.top == self.view.topAnchor
  427. webViewMaskView.bottom == self.view.bottomAnchor
  428. self.displayMarkdown(newCode: apiDoc.generateMarkdown())
  429. self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "square.and.arrow.up"), style: .plain, target: self, action: #selector(exportButtonClicked(_:)))
  430. }
  431. @objc private func exportButtonClicked(_ sourceItem: UIBarButtonItem) {
  432. let exportHtmlBlock = {
  433. let alert = UIAlertController(title: "APIDoc.ExportAlert.Title".apiDocLocalized(), message: "APIDoc.ExportAlert.Text".apiDocLocalized(), preferredStyle: .actionSheet)
  434. alert.popoverPresentationController?.barButtonItem = sourceItem
  435. alert.addAction(UIAlertAction(title: self.apiDoc.navigationItemTitle, style: .default, handler: {
  436. _ in
  437. self.submitWebView {
  438. APIDocGenerator(webView: self.webView).generateAsync(self.apiDoc, rootBlock: self.apiDoc.provideRootMarkdownBlock) {
  439. html in
  440. do {
  441. if !FileManager.default.fileExists(atPath: AppDelegate.documentsDirectory() + "/ExportTemp") {
  442. try FileManager.default.createDirectory(at: URL(fileURLWithPath: AppDelegate.documentsDirectory() + "/ExportTemp"), withIntermediateDirectories: true)
  443. }
  444. let exportTempFileURL = AppDelegate.documentsDirectory() + "/ExportTemp/" + self.apiDoc.navigationItemTitle + ".html"
  445. if FileManager.default.fileExists(atPath: exportTempFileURL) {
  446. try FileManager.default.removeItem(atPath: exportTempFileURL)
  447. }
  448. try html.write(toFile: exportTempFileURL, atomically: true, encoding: .utf8)
  449. self.presentSavePanel(url: URL(fileURLWithPath: exportTempFileURL), from: sourceItem) {
  450. try? FileManager.default.removeItem(at: $0)
  451. }
  452. }
  453. catch {
  454. let alert = UIAlertController(title: "APIDoc.Export.Error".apiDocLocalized(), message: error.localizedDescription, preferredStyle: .alert)
  455. alert.addAction(UIAlertAction(title: "APIDoc.Export.OK".apiDocLocalized(), style: .default))
  456. self.present(alert, animated: true)
  457. }
  458. }
  459. }
  460. }))
  461. for it in self.apiDoc.provideRootMarkdownBlock?().filter({ !$0.files.isEmpty }) ?? [] {
  462. #if !DEBUG
  463. if ["stdc", "cdenvc"].contains(it.collectionStoragePath) { continue }
  464. #endif
  465. alert.addAction(UIAlertAction(title: it.collectionTitle, style: .default, handler: {
  466. alert in
  467. self.submitWebView {
  468. var exportRoot = AppDelegate.documentsDirectory() + "/ExportTemp"
  469. do {
  470. let generator = APIDocGenerator(webView: self.webView)
  471. let group = DispatchGroup()
  472. if !FileManager.default.fileExists(atPath: exportRoot) {
  473. try FileManager.default.createDirectory(at: URL(fileURLWithPath: exportRoot), withIntermediateDirectories: true)
  474. }
  475. exportRoot += "/\(UUID())"
  476. try FileManager.default.createDirectory(at: URL(fileURLWithPath: exportRoot), withIntermediateDirectories: true)
  477. for file in it.files {
  478. group.enter()
  479. generator.generateAsync(file, rootBlock: self.apiDoc.provideRootMarkdownBlock, completion: {
  480. html in
  481. try? html.write(toFile: exportRoot + "/\(file.fileName).html", atomically: true, encoding: .utf8)
  482. group.leave()
  483. })
  484. for member in file.items {
  485. let member = APIDocMemberModel(member, inFile: file.fileName, inCollection: file.collectionPath)
  486. group.enter()
  487. generator.generateAsync(member, rootBlock: self.apiDoc.provideRootMarkdownBlock, completion: {
  488. html in
  489. if !FileManager.default.fileExists(atPath: exportRoot + "/\(file.fileName)") {
  490. try? FileManager.default.createDirectory(at: URL(fileURLWithPath: exportRoot + "/\(file.fileName)"), withIntermediateDirectories: true)
  491. }
  492. try? html.write(toFile: exportRoot + "/\(file.fileName)/\(member.member.name).html", atomically: true, encoding: .utf8)
  493. group.leave()
  494. })
  495. }
  496. }
  497. group.notify(queue: .main) {
  498. if SSZipArchive.createZipFile(atPath: exportRoot + "/\(it.collectionStoragePath).zip", withContentsOfDirectory: exportRoot) {
  499. self.presentSavePanel(url: URL(fileURLWithPath: exportRoot + "/\(it.collectionStoragePath).zip"), from: sourceItem) {
  500. url in
  501. try? FileManager.default.removeItem(atPath: exportRoot)
  502. }
  503. } else {
  504. try? FileManager.default.removeItem(atPath: exportRoot)
  505. let alert = UIAlertController(title: "APIDoc.Export.Error".apiDocLocalized(), message: "APIDoc.Export.ZipFail".apiDocLocalized(), preferredStyle: .alert)
  506. alert.addAction(UIAlertAction(title: "APIDoc.Export.OK".apiDocLocalized(), style: .default))
  507. self.present(alert, animated: true)
  508. }
  509. }
  510. }
  511. catch {
  512. try? FileManager.default.removeItem(atPath: exportRoot)
  513. let alert = UIAlertController(title: "APIDoc.Export.Error".apiDocLocalized(), message: error.localizedDescription, preferredStyle: .alert)
  514. alert.addAction(UIAlertAction(title: "APIDoc.Export.OK".apiDocLocalized(), style: .default))
  515. self.present(alert, animated: true)
  516. }
  517. }
  518. }))
  519. }
  520. alert.addAction(UIAlertAction(title: "APIDoc.ExportAlert.Cancel".apiDocLocalized(), style: .cancel))
  521. self.present(alert, animated: true)
  522. }
  523. let exportShare = {
  524. if let url = self.apiDoc.shareURL {
  525. self.presentSharePanel(url: url, from: sourceItem)
  526. }
  527. }
  528. if ["cdenvc", "stdc"].contains(self.apiDoc.collectionPath) {
  529. exportShare()
  530. } else {
  531. exportHtmlBlock()
  532. }
  533. }
  534. var beforeLoadingQueue = [(() -> Void)?]()
  535. private func submitWebView(action: @escaping () -> Void) {
  536. if isPreviewLoaded {
  537. action()
  538. } else {
  539. beforeLoadingQueue.append(action)
  540. }
  541. }
  542. public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  543. let theme = self.traitCollection.userInterfaceStyle == .dark ? "dark" : "light"
  544. submitWebView {
  545. self.webView.evaluateJavaScript("document.getElementById('x').style.backgroundColor = '\(self.view.backgroundColor?.resolvedColor(with: self.traitCollection).hexString ?? "#555555")'", completionHandler: nil)
  546. self.webView.evaluateJavaScript("setTheme(\"\(theme)\")", completionHandler: nil)
  547. self.displayMarkdown(newCode: self.apiDoc.generateMarkdown())
  548. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  549. self.webViewMaskView.isHidden = true
  550. }
  551. }
  552. }
  553. public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
  554. self.isPreviewLoaded = true
  555. let theme = self.traitCollection.userInterfaceStyle == .dark ? "dark" : "light"
  556. self.webView.evaluateJavaScript("setTheme(\"\(theme)\")", completionHandler: nil)
  557. if let storaged = self.previewNotLoadedStoragedRequest {
  558. self.previewNotLoadedStoragedRequest = nil
  559. self.displayMarkdown(newCode: storaged)
  560. }
  561. _ = beforeLoadingQueue.map({ $0?() })
  562. self.traitCollectionDidChange(self.traitCollection)
  563. }
  564. public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
  565. if !isPreviewLoaded {
  566. decisionHandler(.allow)
  567. return
  568. }
  569. decisionHandler(.cancel)
  570. if let path = navigationAction.request.url?.path {
  571. let pathComponents = path.split(separator: "/")
  572. if pathComponents.count == 3 {
  573. let collectionId = pathComponents[0]
  574. let fileName = pathComponents[1]
  575. let memberName = pathComponents[2]
  576. if ["cdenvc", "stdc"].contains(collectionId), memberName == "__article__" {
  577. let articleModel = ArticleDocModel(collectionPath: String(collectionId), filePath: String(fileName))
  578. articleModel.provideRootMarkdownBlock = apiDoc.provideRootMarkdownBlock
  579. let explorerVc = APIDocExplorerViewController(withAPIDocFile: articleModel)
  580. if let openSecondaryBlock = openSecondaryBlock {
  581. openSecondaryBlock(explorerVc)
  582. } else {
  583. (self.splitViewController as? APIDocRootViewController)?.openSecondary(viewController: explorerVc, replaceCurrentRightStack: false)
  584. }
  585. }
  586. else if let rootModel = apiDoc.provideRootMarkdownBlock?(),
  587. let collectionModel = rootModel.first(where: { $0.collectionStoragePath == collectionId }),
  588. let fileModel = collectionModel.files.first(where: { $0.fileName == fileName }),
  589. let defs = fileModel.items.first(where: { $0.name == memberName }) {
  590. let memberModel = APIDocMemberModel(defs, inFile: fileName.description, inCollection: collectionId.description)
  591. memberModel.provideRootMarkdownBlock = apiDoc.provideRootMarkdownBlock
  592. let explorerVc = APIDocExplorerViewController(withAPIDocFile: memberModel)
  593. if let openSecondaryBlock = openSecondaryBlock {
  594. openSecondaryBlock(explorerVc)
  595. } else {
  596. (self.splitViewController as? APIDocRootViewController)?.openSecondary(viewController: explorerVc, replaceCurrentRightStack: false)
  597. }
  598. }
  599. }
  600. if pathComponents.count == 2 {
  601. let collectionId = pathComponents[0]
  602. let fileName = pathComponents[1]
  603. if let rootModel = apiDoc.provideRootMarkdownBlock?(),
  604. let collectionModel = rootModel.first(where: { $0.collectionStoragePath == collectionId }),
  605. let fileModel = collectionModel.files.first(where: { $0.fileName == fileName }) {
  606. fileModel.provideRootMarkdownBlock = apiDoc.provideRootMarkdownBlock
  607. let explorerVc = APIDocExplorerViewController(withAPIDocFile: fileModel)
  608. if let openSecondaryBlock = openSecondaryBlock {
  609. openSecondaryBlock(explorerVc)
  610. } else {
  611. (self.splitViewController as? APIDocRootViewController)?.openSecondary(viewController: explorerVc, replaceCurrentRightStack: false)
  612. }
  613. }
  614. }
  615. }
  616. }
  617. func displayMarkdown(newCode: String) {
  618. if !isPreviewLoaded {
  619. self.previewNotLoadedStoragedRequest = newCode
  620. return
  621. }
  622. if Thread.isMainThread {
  623. self.webView.evaluateJavaScript("displayMarkdown(\(codeToJsStr(content: newCode)))", completionHandler: nil)
  624. } else {
  625. DispatchQueue.main.async {
  626. self.webView.evaluateJavaScript("displayMarkdown(\(codeToJsStr(content: newCode)))", completionHandler: nil)
  627. }
  628. }
  629. self.navigationItem.title = apiDoc.navigationItemTitle
  630. }
  631. public func scrollViewDidScroll(_ scrollView: UIScrollView) {
  632. scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y)
  633. }
  634. }