Tiny markdown parser, makes tutorial screens editing much simpler
This commit is contained in:
68
main/Common Classes/TinyMarkdown.swift
Normal file
68
main/Common Classes/TinyMarkdown.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
import UIKit
|
||||
|
||||
struct TinyMarkdown {
|
||||
/// Load markdown file and run through a (very) simple parser (see below).
|
||||
/// - Parameters:
|
||||
/// - filename: Will automatically append `.md` extension
|
||||
/// - replacements: Replace a single occurrence of search string with an attributed replacement.
|
||||
static func load(_ filename: String, replacements: [String : NSMutableAttributedString] = [:]) -> UITextView {
|
||||
let url = Bundle.main.url(forResource: filename, withExtension: "md")!
|
||||
let str = NSMutableAttributedString(withMarkdown: try! String(contentsOf: url))
|
||||
for (key, val) in replacements {
|
||||
guard let r = str.string.range(of: key) else {
|
||||
QLog.Debug("WARN: markdown key '\(key)' does not exist in \(filename)")
|
||||
continue
|
||||
}
|
||||
str.replaceCharacters(in: NSRange(r, in: str.string), with: val)
|
||||
}
|
||||
return QuickUI.text(attributed: str)
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
/// Supports only: `#h1`, `##h2`, `###h3`, `_italic_`, `__bold__`, `___boldItalic___`
|
||||
convenience init(withMarkdown content: String) {
|
||||
self.init()
|
||||
let emph = try! NSRegularExpression(pattern: #"(?<=(^|\W))(_{1,3})(\S|\S.*?\S)\2"#, options: [])
|
||||
beginEditing()
|
||||
content.enumerateLines { (line, _) in
|
||||
if line.starts(with: "#") {
|
||||
var h = 0
|
||||
for char in line {
|
||||
if char == "#" { h += 1 }
|
||||
else { break }
|
||||
}
|
||||
var line = line
|
||||
line.removeFirst(h)
|
||||
line = line.trimmingCharacters(in: CharacterSet(charactersIn: " "))
|
||||
switch h {
|
||||
case 1: self.h1(line + "\n")
|
||||
case 2: self.h2(line + "\n")
|
||||
default: self.h3(line + "\n")
|
||||
}
|
||||
} else {
|
||||
let nsline = line as NSString
|
||||
let range = NSRange(location: 0, length: nsline.length)
|
||||
var i = 0
|
||||
for x in emph.matches(in: line, options: [], range: range) {
|
||||
let r = x.range
|
||||
self.normal(nsline.substring(from: i, to: r.location))
|
||||
i = r.upperBound
|
||||
let before = nsline.substring(with: r)
|
||||
let after = before.trimmingCharacters(in: CharacterSet(charactersIn: "_"))
|
||||
switch (before.count - after.count) / 2 {
|
||||
case 1: self.italic(after)
|
||||
case 2: self.bold(after)
|
||||
default: self.boldItalic(after)
|
||||
}
|
||||
}
|
||||
if i < range.length {
|
||||
self.normal(nsline.substring(from: i, to: range.length) + "\n")
|
||||
} else {
|
||||
self.normal("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
endEditing()
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ extension UIFont {
|
||||
}
|
||||
func bold() -> UIFont { withTraits(traits: .traitBold) }
|
||||
func italic() -> UIFont { withTraits(traits: .traitItalic) }
|
||||
func boldItalic() -> UIFont { withTraits(traits: [.traitBold, .traitItalic]) }
|
||||
func monoSpace() -> UIFont {
|
||||
let traits = fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any] ?? [:]
|
||||
let weight = (traits[.weight] as? CGFloat) ?? UIFont.Weight.regular.rawValue
|
||||
@@ -13,24 +14,29 @@ extension UIFont {
|
||||
}
|
||||
}
|
||||
|
||||
extension NSAttributedString {
|
||||
static func image(_ img: UIImage) -> Self {
|
||||
extension NSMutableAttributedString {
|
||||
convenience init(image: UIImage, centered: Bool = false) {
|
||||
self.init()
|
||||
let att = NSTextAttachment()
|
||||
att.image = img
|
||||
return self.init(attachment: att)
|
||||
att.image = image
|
||||
append(.init(attachment: att))
|
||||
if centered {
|
||||
let ps = NSMutableParagraphStyle()
|
||||
ps.alignment = .center
|
||||
addAttribute(.paragraphStyle, value: ps, range: .init(location: 0, length: length))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
static private var def: UIFont = .preferredFont(forTextStyle: .body)
|
||||
@discardableResult func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) }
|
||||
@discardableResult func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) }
|
||||
@discardableResult func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) }
|
||||
@discardableResult func boldItalic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).boldItalic()) }
|
||||
|
||||
func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) }
|
||||
func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) }
|
||||
func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) }
|
||||
|
||||
func h1(_ str: String) -> Self { normal(str, .title1) }
|
||||
func h2(_ str: String) -> Self { normal(str, .title2) }
|
||||
func h3(_ str: String) -> Self { normal(str, .title3) }
|
||||
@discardableResult func h1(_ str: String) -> Self { normal(str, .title1) }
|
||||
@discardableResult func h2(_ str: String) -> Self { normal(str, .title2) }
|
||||
@discardableResult func h3(_ str: String) -> Self { normal(str, .title3) }
|
||||
|
||||
private func append(_ str: String, withFont: UIFont) -> Self {
|
||||
append(NSAttributedString(string: str, attributes: [
|
||||
@@ -39,13 +45,4 @@ extension NSMutableAttributedString {
|
||||
]))
|
||||
return self
|
||||
}
|
||||
|
||||
func centered(_ content: NSAttributedString) -> Self {
|
||||
let before = length
|
||||
append(content)
|
||||
let ps = NSMutableParagraphStyle()
|
||||
ps.alignment = .center
|
||||
addAttribute(.paragraphStyle, value: ps, range: .init(location: before, length: content.length))
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,3 +47,9 @@ private var listOfSLDs: [String : [String : Bool]] = {
|
||||
}
|
||||
return res
|
||||
}()
|
||||
|
||||
extension NSString {
|
||||
func substring(from: Int, to: Int) -> String {
|
||||
substring(with: NSRange(location: from, length: to - from))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,49 +126,15 @@ class VCRecordings: UIViewController, UINavigationControllerDelegate {
|
||||
|
||||
@IBAction private func showInfo(_ sender: UIButton) {
|
||||
let x = TutorialSheet()
|
||||
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
|
||||
.h1("How to record?\n")
|
||||
.normal("\nThere are two types: specific app recordings and general background activity. " +
|
||||
"The former are usually 3 – 5 minutes long, the latter need to be at least an hour long.")
|
||||
.h2("\n\nApp recording\n")
|
||||
.normal("Before you begin make sure that you quit all running applications and wait a few seconds. " +
|
||||
"Tap on the 'App' recording button and switch to the application you'd like to inspect. " +
|
||||
"Use the App as you would normally. Try to get to all corners and functionality the App provides. " +
|
||||
"When you feel that you have captured enough content, come back to ").italic("AppCheck").normal(" and stop the recording.")
|
||||
.h2("\n\nBackground recording\n")
|
||||
.normal("Will answer one simple question: What communications happen while you aren't using your device. " +
|
||||
"You should solely start a background recording when you know you aren't going to use your device in the near future. " +
|
||||
"For example, before you go to bed.\n" +
|
||||
"As soon as you start using your device, you should stop the recording to avoid distorting the results.")
|
||||
.h2("\n\nFinish\n")
|
||||
.normal("Upon completion you will find your recording in the section below. " +
|
||||
"You can review your results and remove any user specific information if necessary.\n")
|
||||
))
|
||||
x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-recording-howto"))
|
||||
x.buttonTitleDone = "Close"
|
||||
x.present()
|
||||
}
|
||||
|
||||
@objc private func showTutorial() {
|
||||
let x = TutorialSheet()
|
||||
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
|
||||
.h1("What are Recordings?\n")
|
||||
.normal("\nSimilar to the default logging, recordings will intercept every request and log it for later review. " +
|
||||
"App recordings are usually 3 – 5 minutes long and cover a single application. " +
|
||||
"You can utilize recordings for App analysis or to get a ground truth on background traffic." +
|
||||
"\n\n" +
|
||||
"Optionally, you can help us by providing your app specific recordings. " +
|
||||
"Together with your findings we can create a community driven privacy monitor. " +
|
||||
"The research results will help you and others avoid Apps that unnecessarily share data with third-party providers.")
|
||||
))
|
||||
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
|
||||
.h1("Share results\n")
|
||||
.normal("\nThis step is completely ").bold("optional").normal(". " +
|
||||
"You can choose to share your results with us. " +
|
||||
"We can compare similar applications and suggest privacy friendly alternatives. " +
|
||||
"Together with other likeminded individuals we can increase the awareness for privacy friendly design." +
|
||||
"\n\n" +
|
||||
"Thank you very much.")
|
||||
))
|
||||
x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-recording-1"))
|
||||
x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-recording-2"))
|
||||
x.buttonTitleDone = "Got it"
|
||||
x.present {
|
||||
Prefs.DidShowTutorial.Recordings = true
|
||||
|
||||
@@ -132,21 +132,9 @@ extension VCCoOccurrence {
|
||||
}()
|
||||
|
||||
let x = TutorialSheet()
|
||||
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
|
||||
.h3("Co-Occurrence")
|
||||
.normal(" allows you to find requests that happen often at the same time as the selected domain. " +
|
||||
"Hence it will give you a hint what Apps might be involved in the activity." +
|
||||
"\n\nHow do you interpret these results? Lets look at an example:\n\n")
|
||||
.centered(.image(sampleCell))
|
||||
.normal("\n\nThe domain ").bold("example.org").normal(" had ").bold("14").normal(" requests with an ").italic("average time divergence").normal(" of ").bold("0.71 seconds").normal(". " +
|
||||
"That is, these 14 domain calls happend, on average, less then a second before or after the original request of the selected domain." +
|
||||
"\n\nClose temporal proximity and high occurrence counts are both indicators for domain correlation. " +
|
||||
"Results are sorted by a ranking index (").bold("9.").normal(") which strikes a balance between the two. " +
|
||||
"Preferring entries with higher counts as well as low time divergence.")
|
||||
.italic("\n\nTip: ").normal("As a visual guide you can look for the colored bar beside each value. " +
|
||||
"The larger the bar, the greater the correlation.")
|
||||
))
|
||||
|
||||
x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-cooccurrence", replacements: [
|
||||
"<IMG>" : .init(image: sampleCell, centered: true)
|
||||
]))
|
||||
x.present(in: self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,27 +34,8 @@ class TBCMain: UITabBarController {
|
||||
|
||||
@objc private func showWelcomeMessage() {
|
||||
let x = TutorialSheet()
|
||||
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
|
||||
.h1("Welcome\n")
|
||||
.normal("\nAppCheck helps you identify which applications communicate with third parties. " +
|
||||
"It does so by logging network requests. " +
|
||||
"AppCheck learns only the destination addresses, not the actual data that is exchanged." +
|
||||
"\n\n" +
|
||||
"Your data belongs to you. " +
|
||||
"Therefore, monitoring and analysis take place on your device only. " +
|
||||
"The app does not share any data with us or any other third-party. " +
|
||||
"Unless you choose to.")
|
||||
))
|
||||
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
|
||||
.h1("How it works\n")
|
||||
.normal("\nAppCheck creates a local VPN tunnel to intercept all network connections. " +
|
||||
"For each connection AppCheck looks into the DNS headers only, namely the domain names. " +
|
||||
"\n" +
|
||||
"These domain names are logged in the background while the VPN is active. " +
|
||||
"That means, AppCheck does not have to be active in the foreground. " +
|
||||
"You can close the app and come back later to see the results."
|
||||
)
|
||||
))
|
||||
x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-welcome-1"))
|
||||
x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-welcome-2"))
|
||||
x.present {
|
||||
Prefs.DidShowTutorial.Welcome = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user