From 077b0d269d4fcd706b33445c0ff1784b9dbff95a Mon Sep 17 00:00:00 2001 From: Grant McSheffrey Date: Thu, 24 Aug 2023 03:58:29 -0400 Subject: [PATCH] Bugfix 1459 - Escape ~ character as markdown (#1561) * Add simple test for escaping markdown content in statuses * Add ~ as markdown character to be escaped in statuses The ~ isn't documented in the original markdown syntax docs but is commonly used (including by AttributedString) to surround text formatted with a strikethrough. --- .../Sources/Models/Alias/HTMLString.swift | 10 +++--- .../Tests/ModelsTests/HTMLStringTests.swift | 32 ++++++++++++++++--- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/Packages/Models/Sources/Models/Alias/HTMLString.swift b/Packages/Models/Sources/Models/Alias/HTMLString.swift index 7f922ac5..0f3f1e5b 100644 --- a/Packages/Models/Sources/Models/Alias/HTMLString.swift +++ b/Packages/Models/Sources/Models/Alias/HTMLString.swift @@ -37,10 +37,12 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable { if !alreadyDecoded { // https://daringfireball.net/projects/markdown/syntax - // Pre-escape \ ` _ * and [ as these are the only - // characters the markdown parser used picks up - // when it renders to attributed text - main_regex = try? NSRegularExpression(pattern: "([\\*\\`\\[\\\\])", options: .caseInsensitive) + // Pre-escape \ ` _ * ~ and [ as these are the only + // characters the markdown parser uses when it renders + // to attributed text. Note that ~ for strikethrough is + // not documented in the syntax docs but is used by + // AttributedString. + main_regex = try? NSRegularExpression(pattern: "([\\*\\`\\~\\[\\\\])", options: .caseInsensitive) // don't escape underscores that are between colons, they are most likely custom emoji underscore_regex = try? NSRegularExpression(pattern: "(?!\\B:[^:]*)(_)(?![^:]*:\\B)", options: .caseInsensitive) diff --git a/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift b/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift index 557eda6d..e1cff3f8 100644 --- a/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift +++ b/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift @@ -19,7 +19,7 @@ final class HTMLStringTests: XCTestCase { let extendedCharQuery = URL(string: "http://test.com/blah/city?name=京都市", encodePath: true) XCTAssertEqual("http://test.com/blah/city?name=%E4%BA%AC%E9%83%BD%E5%B8%82", extendedCharQuery?.absoluteString) - + // Double encoding will happen if you ask to encodePath on an already encoded string let alreadyEncodedPath = URL(string: "https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station", encodePath: true) XCTAssertEqual("https://en.wikipedia.org/wiki/Elbbr%25C3%25BCcken_station", alreadyEncodedPath?.absoluteString) @@ -27,7 +27,7 @@ final class HTMLStringTests: XCTestCase { func testHTMLStringInit() throws { let decoder = JSONDecoder() - + let basicContent = "\"

This is a test

\"" var htmlString = try decoder.decode(HTMLString.self, from: Data(basicContent.utf8)) XCTAssertEqual("This is a test", htmlString.asRawText) @@ -35,7 +35,7 @@ final class HTMLStringTests: XCTestCase { XCTAssertEqual("This is a test", htmlString.asMarkdown) XCTAssertEqual(0, htmlString.statusesURLs.count) XCTAssertEqual(0, htmlString.links.count) - + let basicLink = "\"

This is a test

\"" htmlString = try decoder.decode(HTMLString.self, from: Data(basicLink.utf8)) XCTAssertEqual("This is a test", htmlString.asRawText) @@ -45,7 +45,7 @@ final class HTMLStringTests: XCTestCase { XCTAssertEqual(1, htmlString.links.count) XCTAssertEqual("https://test.com", htmlString.links[0].url.absoluteString) XCTAssertEqual("test", htmlString.links[0].displayString) - + let extendedCharLink = "\"

This is a test

\"" htmlString = try decoder.decode(HTMLString.self, from: Data(extendedCharLink.utf8)) XCTAssertEqual("This is a test", htmlString.asRawText) @@ -55,7 +55,7 @@ final class HTMLStringTests: XCTestCase { XCTAssertEqual(1, htmlString.links.count) XCTAssertEqual("https://test.com/go%C3%9F%C3%AB%C3%B1a", htmlString.links[0].url.absoluteString) XCTAssertEqual("test", htmlString.links[0].displayString) - + let alreadyEncodedLink = "\"

This is a test

\"" htmlString = try decoder.decode(HTMLString.self, from: Data(alreadyEncodedLink.utf8)) XCTAssertEqual("This is a test", htmlString.asRawText) @@ -66,4 +66,26 @@ final class HTMLStringTests: XCTestCase { XCTAssertEqual("https://test.com/go%C3%9F%C3%AB%C3%B1a", htmlString.links[0].url.absoluteString) XCTAssertEqual("test", htmlString.links[0].displayString) } + + func testHTMLStringInit_markdownEscaping() throws { + let decoder = JSONDecoder() + + let stdMarkdownContent = "\"

This [*is*] `a`\\n**test**

\"" + var htmlString = try decoder.decode(HTMLString.self, from: Data(stdMarkdownContent.utf8)) + XCTAssertEqual("This [*is*] `a`\n**test**", htmlString.asRawText) + XCTAssertEqual("

This [*is*] `a`\n**test**

", htmlString.htmlValue) + XCTAssertEqual("This \\[\\*is\\*] \\`a\\` \\*\\*test\\*\\*", htmlString.asMarkdown) + + let underscoreContent = "\"

This _is_ an :emoji_maybe:

\"" + htmlString = try decoder.decode(HTMLString.self, from: Data(underscoreContent.utf8)) + XCTAssertEqual("This _is_ an :emoji_maybe:", htmlString.asRawText) + XCTAssertEqual("

This _is_ an :emoji_maybe:

", htmlString.htmlValue) + XCTAssertEqual("This \\_is\\_ an :emoji_maybe:", htmlString.asMarkdown) + + let strikeContent = "\"

This ~is~ a\\n`test`

\"" + htmlString = try decoder.decode(HTMLString.self, from: Data(strikeContent.utf8)) + XCTAssertEqual("This ~is~ a\n`test`", htmlString.asRawText) + XCTAssertEqual("

This ~is~ a\n`test`

", htmlString.htmlValue) + XCTAssertEqual("This \\~is\\~ a \\`test\\`", htmlString.asMarkdown) + } }