Skip to content

Commit

Permalink
Merge pull request #1693 from planetary-social/hashtags
Browse files Browse the repository at this point in the history
Parse and publish hashtags in notes when posting
  • Loading branch information
joshuatbrown authored Dec 10, 2024
2 parents 3a8536b + 774ba36 commit ddbdb0c
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Release Notes
- Fixed display of mastodon usernames so it shows @username@server.instance rather than [email protected]
- Nos now publishes the hashtags it finds in your note when you post. This means it works the way you’ve always expected it to work. [#44](https://github.com/verse-pbc/issues/issues/44)

### Internal Changes

Expand Down
21 changes: 19 additions & 2 deletions Nos/Models/NoteParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,26 @@ struct NoteParser {
/// Parses attributed text generated when composing a note and returns
/// the content and tags.
func parse(attributedText: AttributedString) -> (String, [[String]]) {
cleanLinks(in: attributedText)
let (content, tags) = cleanLinks(in: attributedText)
let hashtags = hashtags(in: content)
return (content, tags + hashtags)
}


func hashtags(in content: String) -> [[String]] {
let pattern = "(?<=^|\\s)#([a-zA-Z0-9]{2,256})(?=\\s|[.,!?;:]|$)"
let regex = try! NSRegularExpression(pattern: pattern) // swiftlint:disable:this force_try
let matches = regex.matches(in: content, range: NSRange(content.startIndex..., in: content))

let hashtags = matches.map { match -> [String] in
if let range = Range(match.range(at: 1), in: content) {
return ["t", String(content[range].lowercased())]
}
return []
}

return hashtags
}

/// Parses the content and tags stored in a note and returns an attributed text with tagged entities replaced
/// with readable names.
func parse(content: String, tags: [[String]], context: NSManagedObjectContext) -> AttributedString {
Expand Down
95 changes: 94 additions & 1 deletion NosTests/Models/NoteParserTests+Parse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation
import UIKit
import XCTest

/// Collection of tests that exercise NoteParser.parse() function. This fubction
/// Collection of tests that exercise NoteParser.parse() function. This function
/// is the one Nos uses for converting editor generated text to note content
/// when publishing.
extension NoteParserTests {
Expand Down Expand Up @@ -69,6 +69,23 @@ extension NoteParserTests {
XCTAssertEqual(content, expected)
}

/// Example taken from [NIP-27](https://github.com/nostr-protocol/nips/blob/master/27.md)
func testMentionWithNpub() throws {
let mention = "@mattn"
let npub = "npub1937vv2nf06360qn9y8el6d8sevnndy7tuh5nzre4gj05xc32tnwqauhaj6"
let hex = "2c7cc62a697ea3a7826521f3fd34f0cb273693cbe5e9310f35449f43622a5cdc"
let link = "nostr:\(npub)"
let markdown = "hello [\(mention)](\(link))"
let attributedString = try AttributedString(markdown: markdown)
let (content, tags) = sut.parse(
attributedText: attributedString
)
let expectedContent = "hello nostr:\(npub)"
let expectedTags = [["p", hex]]
XCTAssertEqual(content, expectedContent)
XCTAssertEqual(tags, expectedTags)
}

@MainActor func testTwoMentionsWithEmojiBeforeAndAfter() throws {
// Arrange
let name = "🍐 mattn 🍐"
Expand All @@ -91,4 +108,80 @@ extension NoteParserTests {
// Assert
XCTAssertEqual(content, expected)
}

@MainActor func test_parse_returns_hashtag() throws {
// Arrange
let text = "#photography"

// Act
let expected = [["t", "photography"]]
let result = sut.hashtags(in: text)

// Assert
XCTAssertEqual(result, expected)
}

@MainActor func test_parse_returns_hashtag_lowercased() throws {
// Arrange
let text = "#DOGS"

// Act
let expected = [["t", "dogs"]]
let result = sut.hashtags(in: text)

// Assert
XCTAssertEqual(result, expected)
}

@MainActor func test_parse_returns_hashtag_without_punctuation() throws {
// Arrange
let text = "check out my #hashtag! #hello, #world."

// Act
let expected = [["t", "hashtag"], ["t", "hello"], ["t", "world"]]
let result = sut.hashtags(in: text)

// Assert
XCTAssertEqual(result, expected)
}

@MainActor func test_parse_returns_multiple_hashtags() throws {
// Arrange
let text = "#photography #birds #canada"

// Act
let expected = [["t", "photography"], ["t", "birds"], ["t", "canada"]]
let result = sut.hashtags(in: text)

// Assert
XCTAssertEqual(result, expected)
}

@MainActor func test_parse_returns_no_hashtags() throws {
// Arrange
let text = "example.com#photography"

// Act
let expected: [[String]] = []
let result = sut.hashtags(in: text)

// Assert
XCTAssertEqual(result, expected)
}

func test_parse_mention_and_hashtag() throws {
let mention = "@mattn"
let npub = "npub1937vv2nf06360qn9y8el6d8sevnndy7tuh5nzre4gj05xc32tnwqauhaj6"
let hex = "2c7cc62a697ea3a7826521f3fd34f0cb273693cbe5e9310f35449f43622a5cdc"
let link = "nostr:\(npub)"
let markdown = "hello [\(mention)](\(link)) #greetings #hi"
let attributedString = try AttributedString(markdown: markdown)
let (content, tags) = sut.parse(
attributedText: attributedString
)
let expectedContent = "hello nostr:\(npub) #greetings #hi"
let expectedTags = [["p", hex], ["t", "greetings"], ["t", "hi"]]
XCTAssertEqual(content, expectedContent)
XCTAssertEqual(tags, expectedTags)
}
}
19 changes: 1 addition & 18 deletions NosTests/Models/NoteParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,24 +93,7 @@ final class NoteParserTests: CoreDataTestCase {
XCTAssertEqual(links[safe: 0]?.key, nip05)
XCTAssertEqual(links[safe: 0]?.value, URL(string: webLink))
}

/// Example taken from [NIP-27](https://github.com/nostr-protocol/nips/blob/master/27.md)
func testMentionWithNPub() throws {
let mention = "@mattn"
let npub = "npub1937vv2nf06360qn9y8el6d8sevnndy7tuh5nzre4gj05xc32tnwqauhaj6"
let hex = "2c7cc62a697ea3a7826521f3fd34f0cb273693cbe5e9310f35449f43622a5cdc"
let link = "nostr:\(npub)"
let markdown = "hello [\(mention)](\(link))"
let attributedString = try AttributedString(markdown: markdown)
let (content, tags) = sut.parse(
attributedText: attributedString
)
let expectedContent = "hello nostr:\(npub)"
let expectedTags = [["p", hex]]
XCTAssertEqual(content, expectedContent)
XCTAssertEqual(tags, expectedTags)
}


@MainActor func testContentWithMixedMentions() throws {
let content = "hello nostr:npub1937vv2nf06360qn9y8el6d8sevnndy7tuh5nzre4gj05xc32tnwqauhaj6 and #[1]"
let displayName1 = "npub1937vv..."
Expand Down

0 comments on commit ddbdb0c

Please sign in to comment.