~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

The iOS app links the pulls and issues Markdown names, as the pages do

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:ios/at-links
ios/Knotless/Support/Markdown.swift+24−1
@@ -11,6 +11,18 @@ struct MarkdownView: View {
/// Its author's, to tick a task, which is by its number in `source`. Without
/// it the boxes only say how each stands.
var tick: ((Int, Bool) -> Void)?
/// The pulls and issues it names, read from `html`.
private let references: MarkdownReferences
@Environment(\.openRoute) private var openRoute
init(source: String, html: String? = nil, place: MarkdownPlace? = nil, tick: ((Int, Bool) -> Void)? = nil) {
self.source = source
self.html = html
self.place = place
self.tick = tick
references = html.map { MarkdownReferences(html: $0, source: source) } ?? MarkdownReferences()
}
enum Block: Hashable {
case heading(Int, String)
@@ -32,6 +44,17 @@ struct MarkdownView: View {
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.environment(\.openURL, OpenURLAction(handler: open))
}
/// A pull or issue of its repository opens here, as the app's own screen.
private func open(_ url: URL) -> OpenURLAction.Result {
guard let repo = place?.repo, let uri = MarkdownReferences.thread(url, in: repo) else {
return url.scheme == MarkdownReferences.scheme ? .discarded : .systemAction
}
guard let openRoute else { return .systemAction }
openRoute(.thread(repo, uri: uri))
return .handled
}
@ViewBuilder private func view(_ block: Block, _ colours: [String: String]) -> some View {
@@ -220,7 +243,7 @@ struct MarkdownView: View {
private func inline(_ text: String) -> Text {
let options = AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace)
if var marked = try? AttributedString(markdown: text, options: options) {
if var marked = try? AttributedString(markdown: references.linked(text), options: options) {
for run in marked.runs where run.inlinePresentationIntent?.contains(.code) == true {
marked[run.range].font = .callout.monospaced()
marked[run.range].backgroundColor = Color(.secondarySystemBackground)
ios/Knotless/Support/MarkdownFences.swift+1−1
@@ -14,7 +14,7 @@ enum MarkdownFences {
return out
}
private static func unescaped(_ text: String) -> String {
static func unescaped(_ text: String) -> String {
// `&` last, so that an escaped `<` stays one.
text.replacing("&lt;", with: "<").replacing("&gt;", with: ">")
.replacing("&quot;", with: "\"").replacing("&#39;", with: "'").replacing("&#x27;", with: "'")
ios/Knotless/Support/MarkdownReferences.swiftnew+85−0
@@ -0,0 +1,85 @@
import Foundation
/// The pulls and issues a Markdown text names by their `at://` URIs, as the
/// server links them: read from the HTML it made of the text, where each one
/// of the repository's is a link to its page, a bare one showing its title.
/// One the server didn't link, as another repository's, stays as written.
struct MarkdownReferences: Hashable, Sendable {
/// The title of each the server linked, by its URI.
var titles: [String: String] = [:]
/// Each the server linked, a bare one or a link's target.
var known: Set<String> = []
/// What a link to a pull or issue the app opens itself points at.
static let scheme = "knotless-thread"
init() {}
init(html: String, source: String) {
for found in html.matches(of: #/<a href="/[^/"]+/[^/"]+/(issue|pull)/(did:[^/"]+)/([^/"]+)">(.*?)</a>/#.dotMatchesNewlines()) {
let uri = "at://\(found.output.2)/dev.knotless.\(found.output.1)/\(found.output.3)"
known.insert(uri)
let title = MarkdownFences.unescaped(String(found.output.4).replacing(#/<[^>]*>/#, with: ""))
// Where the text links the URI itself, those are its own words, not the title.
if titles[uri] == nil, !source.contains("[\(title)](\(uri)") { titles[uri] = title }
}
}
/// `text` with each `at://` URI the server linked made a link the app opens:
/// a bare one showing its title, as the pages show it. Not in code.
func linked(_ text: String) -> String {
guard !known.isEmpty, text.contains("at://") else { return text }
var out = ""
var done = text.startIndex
var from = text.startIndex
while let found = text[from...].range(of: "at://") {
from = found.upperBound
let at = found.lowerBound
let before = text[..<at]
if let last = before.last, last.isLetter || last.isNumber { continue }
if before.filter({ $0 == "`" }).count % 2 == 1 { continue }
let rest = text[at...]
var end = rest.firstIndex { $0.isWhitespace || "<>()[]\"'`".contains($0) } ?? rest.endIndex
while end > at, ".,;:!?".contains(text[text.index(before: end)]) { end = text.index(before: end) }
let uri = String(text[at..<end])
guard known.contains(uri) else { continue }
if before.hasSuffix("](") {
out += text[done..<at] + Self.target(uri)
} else if let title = titles[uri] {
out += text[done..<at] + "[\(Self.escaped(title))](\(Self.target(uri)))"
} else {
continue
}
done = end
from = end
}
return out + text[done...]
}
/// The pull or issue of `repo` a link opens, if it's one: by the link this
/// makes of its URI, or its page on the site.
static func thread(_ url: URL, in repo: RepoRef) -> String? {
if url.scheme == scheme {
return url.absoluteString.dropFirst(scheme.count + 1).removingPercentEncoding
}
guard url.scheme == "https", url.host() == URL(string: MarkdownPlace.site)?.host() else { return nil }
let p = url.path(percentEncoded: false).split(separator: "/").map(String.init)
guard p.count == 5, [repo.handle, repo.did].contains(p[0]), p[1] == repo.name,
["issue", "pull"].contains(p[2]), p[3].hasPrefix("did:")
else { return nil }
return "at://\(p[3])/dev.knotless.\(p[2])/\(p[4])"
}
private static func target(_ uri: String) -> String {
"\(scheme):" + (uri.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? uri)
}
private static func escaped(_ title: String) -> String {
var out = ""
for c in title {
if "\\`*_[]<>!~".contains(c) { out.append("\\") }
out.append(c)
}
return out
}
}
ios/Knotless/App.swift+19−1
@@ -64,6 +64,18 @@ enum Route: Hashable {
case notificationSettings
}
/// Opens a screen in the stack it's asked from, for what can't be a
/// `NavigationLink`, as a link in some text.
struct OpenRoute {
let open: @MainActor (Route) -> Void
@MainActor func callAsFunction(_ route: Route) { open(route) }
}
extension EnvironmentValues {
@Entry var openRoute: OpenRoute?
}
extension View {
func routes() -> some View {
navigationDestination(for: Route.self) { route in
@@ -109,6 +121,7 @@ struct RootView: View {
@State private var homePath: [Route]
@State private var inboxPath: [Route] = []
@State private var profilePath: [Route] = []
@State private var explorePath: [Route] = []
@Environment(Account.self) private var account
private let pushes = Pushes.shared
@@ -133,6 +146,7 @@ struct RootView: View {
guard did == nil else { return }
homePath = []
inboxPath = []
explorePath = []
profilePath = []
}
.task(id: pushes.opened) {
@@ -147,15 +161,19 @@ struct RootView: View {
TabView(selection: $page) {
Tab("Home", image: "oct-home-fill", value: Page.home) {
NavigationStack(path: $homePath) { HomeView().routes() }
.environment(\.openRoute, OpenRoute { homePath.append($0) })
}
Tab("Inbox", image: "oct-inbox-fill", value: Page.inbox) {
NavigationStack(path: $inboxPath) { InboxView().routes() }
.environment(\.openRoute, OpenRoute { inboxPath.append($0) })
}
Tab("Explore", image: "oct-telescope-fill", value: Page.explore) {
NavigationStack { ExploreView().routes() }
NavigationStack(path: $explorePath) { ExploreView().routes() }
.environment(\.openRoute, OpenRoute { explorePath.append($0) })
}
Tab("Profile", image: "oct-person-fill", value: Page.profile) {
NavigationStack(path: $profilePath) { OwnProfileView().routes() }
.environment(\.openRoute, OpenRoute { profilePath.append($0) })
}
}
}
ios/KnotlessUITests/LookTests.swift+5−1
@@ -66,7 +66,11 @@ final class LookTests: XCTestCase {
}
private func take(_ step: Step, in app: XCUIApplication) {
if let label = step.tap {
if let label = step.tap, label.hasPrefix("@") {
let xy = label.dropFirst().split(separator: ",").compactMap { Double($0) }
app.coordinate(withNormalizedOffset: .zero).withOffset(CGVector(dx: xy[0], dy: xy[1])).tap()
sleep(2)
} else if let label = step.tap {
let match = NSPredicate(format: "label == %@ OR identifier == %@", label, label)
let found = app.descendants(matching: .any).matching(match).firstMatch
for _ in 0..<8 where !(found.exists && found.isHittable) { app.swipeUp() }
ios/look.mjs+3−1
@@ -16,7 +16,9 @@
// the file's JSON, and null takes it out
// --signed-out as no one; signed in as alice.test otherwise
// --tap <label> tap what has that label or accessibility
// identifier, scrolling to it, in turn
// identifier, scrolling to it, in turn; or
// @x,y, that point on the screen, as for a
// link in some text, which has no label
// --type <text> type into the first text box, in turn
// --swipe <up|down|left|right> swipe, in turn
// --top only the screen as it opens, not all of it