Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse and publish hashtags in notes when posting #1693

Merged
merged 10 commits into from
Dec 10, 2024
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 @[email protected] 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
Loading