diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt index 77092cadb2..6293f8c4f4 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt @@ -33,6 +33,7 @@ data class MetaTag( } object MetaTagsParser { + private val TAG_NAME = Regex("""[0-9a-zA-Z]+""") private val NON_ATTR_NAME_CHARS = setOf(Char(0x0), '"', '\'', '>', '/') private val NON_UNQUOTED_ATTR_VALUE_CHARS = setOf('"', '\'', '=', '>', '<', '`') @@ -81,10 +82,42 @@ object MetaTagsParser { this.skipWhile { it.isWhitespace() } } + private fun skipComment() { + val end = input.indexOf("-->", p) + p = if (end < 0) input.length else end + 3 + } + + private fun skipToTagEnd() { + skipWhile { it != '>' } + if (!exhausted()) consume() + } + + /** Leaves [p] on the ``, `` and `` are not element markup, so the + // attribute-quote tracking below must not run over them. A comment holding an odd + // number of quote characters -- an apostrophe in "we don't", a lone `"` -- would + // otherwise leave the scanner inside a phantom quoted attribute value and make it + // swallow every tag that follows, until the next matching quote character. That is + // enough to hide a page's whole `` block from the preview. + if (peek() == '!' || peek() == '?') { + if (input.startsWith("!--", p)) { + skipComment() + } else { + skipToTagEnd() + } + return null + } // read tag name val isEnd = peek() == '/' @@ -105,7 +138,7 @@ object MetaTagsParser { val c = consume() when { // `/>` out of quote -> end of tag - quote == null && c == '/' && peek() == '>' -> { + quote == null && c == '/' && !exhausted() && peek() == '>' -> { consume() break } @@ -129,11 +162,20 @@ object MetaTagsParser { val attrsEnd = p - 1 val name = input.slice(nameStart.. + | + | + | + | + """.trimMargin() + + val metaTags = MetaTagsParser.parse(input).toList() + + assertEquals(2, metaTags.size) + assertEquals("Brainstorm", metaTags[0].attr("content")) + assertEquals("https://example.com/og-image.png", metaTags[1].attr("content")) + } + + @Test + fun metaTagsInsideCommentsAreNotParsed() { + val input = + """ + | + | + | + | + """.trimMargin() + + val metaTags = MetaTagsParser.parse(input).toList() + + assertEquals(1, metaTags.size) + assertEquals("Real Title", metaTags[0].attr("content")) + } + + @Test + fun scriptBodyDoesNotSwallowFollowingMetaTags() { + val input = + """ + | + | + | + | + """.trimMargin() + + val metaTags = MetaTagsParser.parse(input).toList() + + assertEquals(1, metaTags.size) + assertEquals("Real Title", metaTags[0].attr("content")) + } + + @Test + fun styleBodyDoesNotSwallowFollowingMetaTags() { + val input = + """ + | + | + | + | + """.trimMargin() + + val metaTags = MetaTagsParser.parse(input).toList() + + assertEquals(1, metaTags.size) + assertEquals("Real Title", metaTags[0].attr("content")) + } + + @Test + fun doctypeAndProcessingInstructionsAreSkipped() { + val input = + """ + | + | + | + | + | + """.trimMargin() + + val metaTags = MetaTagsParser.parse(input).toList() + + assertEquals(1, metaTags.size) + assertEquals("Real Title", metaTags[0].attr("content")) + } + + @Test + fun unterminatedCommentEndsTheDocument() { + val input = + """ + | + | + | + | + | Brainstorm - Web of Trust for Nostr + | + | + | + | + |
+ | + """.trimMargin() + + val info = OpenGraphParser().extractUrlInfo(MetaTagsParser.parse(input)) + + assertEquals("Brainstorm - Your Network. Your Rules.", info.title) + assertEquals("The decentralized Web of Trust layer for Nostr.", info.description) + assertEquals("https://brainstorm.nosfabrica.com/og-image.png", info.image) + } +}