Skip to content

Commit

Permalink
Merge pull request #889 from planetary-social/bugfix/detect-raw-domai…
Browse files Browse the repository at this point in the history
…n-urls

Fix URL detection of raw domain names
  • Loading branch information
joshuatbrown authored Feb 22, 2024
2 parents 5ea7bb3 + 5a4f25b commit 4d7c408
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Fixed URL detection of raw domain names, such as “nos.social” (without the “http” prefix).
- Fixed the sort order of gallery media to match the order in the note.
- While composing a note, a space is now automatically inserted after any mention of a user or note to ensure it’s formatted correctly.

Expand Down
3 changes: 3 additions & 0 deletions Nos.xcodeproj/xcshareddata/xcschemes/Nos.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
<Test
Identifier = "SocialGraphTests/testFollow()">
</Test>
<Test
Identifier = "SocialGraphTests/testTwoFollows()">
</Test>
</SkippedTests>
</TestableReference>
<TestableReference
Expand Down
16 changes: 9 additions & 7 deletions Nos/Extensions/String+Markdown.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,24 @@ extension String {
func extractURLs() -> (String, [URL]) {
var urls: [URL] = []
let mutableString = NSMutableString(string: self)
let regexPattern = "(\\s*)(https?://[^\\s]*)"

// The following pattern uses rules from the Domain Name System page on Wikipedia:
// https://en.wikipedia.org/wiki/Domain_Name_System#Domain_name_syntax,_internationalization
let regexPattern = "(\\s*)((https?://)?([a-zA-Z0-9][-a-zA-Z0-9]{0,62}\\.){1,127}[a-z]{2,63}[^\\s]*)"

do {
let regex = try NSRegularExpression(pattern: regexPattern, options: [])
let regex = try NSRegularExpression(pattern: regexPattern)
let range = NSRange(location: 0, length: mutableString.length)

let matches = regex.matches(in: self, options: [], range: range).reversed()
let matches = regex.matches(in: self, range: range).reversed()

for match in matches {
if let range = Range(match.range(at: 2), in: self), let url = URL(string: String(self[range])) {
urls.insert(url, at: 0) // maintain original order of links (we're looping in reverse)
// maintain original order of links by inserting at index 0 (we're looping in reverse)
urls.insert(url.addingSchemeIfNeeded(), at: 0)
let prettyURL = url.truncatedMarkdownLink
regex.replaceMatches(
in: mutableString,
options: [],
range: match.range,
range: match.range,
withTemplate: "$1\(prettyURL)"
)
}
Expand Down
25 changes: 19 additions & 6 deletions Nos/Extensions/URL+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@
import Foundation

extension URL {


/// Returns the URL with the scheme "https" if the URL doesn't have a scheme; othewise, returns self.
/// Using a URL with a scheme fixes an issue with opening link previews when there's no scheme.
/// - Returns: The URL with the scheme "https" if the URL doesn't have a scheme; otherwise, returns self.
func addingSchemeIfNeeded() -> URL {
guard scheme == nil else {
return self
}

return URL(string: "https://\(absoluteString)") ?? self
}

var isImage: Bool {
let imageExtensions = ["png", "jpg", "jpeg", "gif"]
return imageExtensions.contains(pathExtension)
Expand All @@ -23,18 +34,20 @@ extension URL {
}

var truncatedMarkdownLink: String {
guard var host = host() else {
return "[\(absoluteString)](\(absoluteString))"
let url = self.addingSchemeIfNeeded()

guard var host = url.host() else {
return "[\(url.absoluteString)](\(url.absoluteString))"
}

if host.hasPrefix("www.") {
host = String(host.dropFirst(4))
}

if path().isEmpty {
return "[\(host)](\(absoluteString))"
if url.path().isEmpty {
return "[\(host)](\(url.absoluteString))"
} else {
return "[\(host)...](\(absoluteString))"
return "[\(host)...](\(url.absoluteString))"
}
}
}
2 changes: 1 addition & 1 deletion Nos/Models/NoteParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ enum NoteParser {
}
}

/// Parses the content and tags stored in a note and returns an attributed text and list of URLs that can be used
/// Parses the content and tags stored in a note and returns an attributed string and list of URLs that can be used
/// to display the note in the UI.
static func parse(content: String, tags: [[String]], context: NSManagedObjectContext) -> (AttributedString, [URL]) {
var result = replaceTaggedNostrEntities(in: content, tags: tags, context: context)
Expand Down
56 changes: 47 additions & 9 deletions NosTests/Extensions/String+MarkdownTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,21 @@
import XCTest

class String_MarkdownTests: XCTestCase {
func testExtractURLsFromNote() throws {
/// Test this function that's not used anwhere.
/// Consider removing it after extracting all value from it. (that regex in it looks great)
func testFindAndReplaceUnformattedLinksWithNoURLScheme() throws {
// Arrange
let string = "One: https://nos.social and two: nostr1.com"
let expected = "One: [https://nos.social](https://nos.social) and two: [nostr1.com](https://nostr1.com)"

// Act
let result = try string.findAndReplaceUnformattedLinks(in: string)

// Assert
XCTAssertEqual(result, expected)
}

func testExtractURLs() throws {
// swiftlint:disable line_length
let string = "Classifieds incoming... 👀\n\nhttps://nostr.build/i/2170fa01a69bca5ad0334430ccb993e41bb47eb15a4b4dbdfbee45585f63d503.jpg"
let expectedString = "Classifieds incoming... 👀\n\n[nostr.build...](https://nostr.build/i/2170fa01a69bca5ad0334430ccb993e41bb47eb15a4b4dbdfbee45585f63d503.jpg)"
Expand All @@ -23,14 +37,26 @@ class String_MarkdownTests: XCTestCase {
XCTAssertEqual(actualURLs, expectedURLs)
}

func testExtractURLsFromNoteWithMultipleURLs() throws {
// swiftlint:disable line_length
let string = "Classifieds incoming... 👀\n\nhttps://nostr.build/i/2170fa01a69bca5ad0334430ccb993e41bb47eb15a4b4dbdfbee45585f63d503.jpg\n\nhttps://cdn.ymaws.com/nacfm.com/resource/resmgr/images/blog_photos/footprints.jpg"
let expectedString = "Classifieds incoming... 👀\n\n[nostr.build...](https://nostr.build/i/2170fa01a69bca5ad0334430ccb993e41bb47eb15a4b4dbdfbee45585f63d503.jpg)\n\n[cdn.ymaws.com...](https://cdn.ymaws.com/nacfm.com/resource/resmgr/images/blog_photos/footprints.jpg)"
// swiftlint:enable line_length
func testExtractURLsWithMultipleURLs() throws {
let string = """
A few links...
https://nostr.build/i/2170fa01a69bca5ad0334430ccb993e41bb47eb15a4b4dbdfbee45585f63d503.jpg
nos.social
www.nostr.com/get-started
"""
let expectedString = """
A few links...
[nostr.build...](https://nostr.build/i/2170fa01a69bca5ad0334430ccb993e41bb47eb15a4b4dbdfbee45585f63d503.jpg)
[nos.social](https://nos.social)
[nostr.com...](https://www.nostr.com/get-started)
"""

let expectedURLs = [
URL(string: "https://nostr.build/i/2170fa01a69bca5ad0334430ccb993e41bb47eb15a4b4dbdfbee45585f63d503.jpg")!,
URL(string: "https://cdn.ymaws.com/nacfm.com/resource/resmgr/images/blog_photos/footprints.jpg")!
URL(string: "https://nos.social")!,
URL(string: "https://www.nostr.com/get-started")
]

// Act
Expand All @@ -39,7 +65,19 @@ class String_MarkdownTests: XCTestCase {
XCTAssertEqual(actualURLs, expectedURLs)
}

func testExtractURLsFromImageNote() throws {
func testExtractURLsDoesNotInterpretAllDotsAsURLs() throws {
// Arrange
let string = "No links...and just some tricks for the extractor..to try to trip it up. ...Ready for It?"

// Act
let (actualString, actualURLs) = string.extractURLs()

// Assert
XCTAssertEqual(actualString, string)
XCTAssertTrue(actualURLs.isEmpty)
}

func testExtractURLsWithImage() throws {
let string = "Hello, world!https://cdn.ymaws.com/footprints.jpg"
let expectedString = "Hello, world![cdn.ymaws.com...](https://cdn.ymaws.com/footprints.jpg)"
let expectedURLs = [
Expand All @@ -52,7 +90,7 @@ class String_MarkdownTests: XCTestCase {
XCTAssertEqual(actualURLs, expectedURLs)
}

func testExtractURLsFromImageNoteWithExtraNewlines() throws {
func testExtractURLsWithImageWithExtraNewlines() throws {
let string = "https://cdn.ymaws.com/footprints.jpg\n\nHello, world!"
let expectedString = "[cdn.ymaws.com...](https://cdn.ymaws.com/footprints.jpg)\n\nHello, world!"
let expectedURLs = [
Expand Down
43 changes: 43 additions & 0 deletions NosTests/Fixtures/URLExtensionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,39 @@ import XCTest

final class URLExtensionTests: XCTestCase {

func test_addingSchemeIfNeeded_adds_scheme_when_there_is_no_scheme() {
let subject = URL(string: "nos.social")!
let expected = URL(string: "https://nos.social")!
let result = subject.addingSchemeIfNeeded()
XCTAssertEqual(result, expected)
}

func test_addingSchemeIfNeeded_adds_scheme_when_url_has_path() {
let subject = URL(string: "nos.social/about")!
let expected = URL(string: "https://nos.social/about")!
let result = subject.addingSchemeIfNeeded()
XCTAssertEqual(result, expected)
}

func test_addingSchemeIfNeeded_adds_scheme_when_url_has_subdomain() {
let subject = URL(string: "www.nos.social")!
let expected = URL(string: "https://www.nos.social")!
let result = subject.addingSchemeIfNeeded()
XCTAssertEqual(result, expected)
}

func test_addingSchemeIfNeeded_does_not_add_scheme_when_http_scheme_already_exists() {
let subject = URL(string: "http://nos.social")!
let result = subject.addingSchemeIfNeeded()
XCTAssertEqual(result, subject)
}

func test_addingSchemeIfNeeded_does_not_add_scheme_when_nostr_scheme_already_exists() {
let subject = URL(string: "nostr:npub1234")!
let result = subject.addingSchemeIfNeeded()
XCTAssertEqual(result, subject)
}

func testTruncatedMarkdownLink_withEmptyPathExtension() {
let url = URL(string: "https://subdomain.example.com")!
XCTAssertEqual(url.truncatedMarkdownLink, "[subdomain.example.com](https://subdomain.example.com)")
Expand All @@ -28,6 +61,16 @@ final class URLExtensionTests: XCTestCase {
let url = URL(string: "https://www.example.com")!
XCTAssertEqual(url.truncatedMarkdownLink, "[example.com](https://www.example.com)")
}

func testTruncatedMarkdownLink_noScheme_withWWW_removesWWW() {
let url = URL(string: "www.nostr.com/get-started")!
XCTAssertEqual(url.truncatedMarkdownLink, "[nostr.com...](https://www.nostr.com/get-started)")
}

func testTruncatedMarkdownLink_noScheme_withWWW_noPath_doesNotIncludeEllipsis() {
let url = URL(string: "www.nos.social")!
XCTAssertEqual(url.truncatedMarkdownLink, "[nos.social](https://www.nos.social)")
}

func testTruncatedMarkdownLink_withShortPath() {
let url = URL(string: "https://nips.be/1")!
Expand Down

0 comments on commit 4d7c408

Please sign in to comment.