Skip to content

Commit

Permalink
[gfm] Implement single/double tildes for strikethrough
Browse files Browse the repository at this point in the history
According to the GFM spec, strikethrough can be indicated
with either single or double tildes (~ / ~~).
Currently, the GFMFlavourDescriptor implementation only allows double tildes.

This commit implements single tilde strikethrough and adds/updates tests.
  • Loading branch information
clemairej committed Dec 10, 2024
1 parent 43c06cb commit aa4a6ae
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ open class GFMFlavourDescriptor(
override fun createHtmlGeneratingProviders(linkMap: LinkMap,
baseURI: URI?): Map<IElementType, GeneratingProvider> {
return super.createHtmlGeneratingProviders(linkMap, baseURI) + hashMapOf(
GFMElementTypes.STRIKETHROUGH to object : SimpleInlineTagProvider("span", 2, -2) {
GFMElementTypes.STRIKETHROUGH to object : EqualDelimiterTrimmingInlineTagProvider("span", GFMTokenTypes.TILDE) {
override fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
visitor.consumeTagOpen(node, tagName, "class=\"user-del\"")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.intellij.markdown.parser.sequentialparsers.SequentialParser
import org.intellij.markdown.parser.sequentialparsers.TokensCache
import org.intellij.markdown.parser.sequentialparsers.impl.EmphStrongDelimiterParser

class StrikeThroughDelimiterParser: DelimiterParser() {
class StrikeThroughDelimiterParser : DelimiterParser() {
override fun scan(tokens: TokensCache, iterator: TokensCache.Iterator, delimiters: MutableList<Info>): Int {
if (iterator.type != GFMTokenTypes.TILDE) {
return 0
Expand Down Expand Up @@ -40,25 +40,39 @@ class StrikeThroughDelimiterParser: DelimiterParser() {
delimiters: MutableList<Info>,
result: SequentialParser.ParsingResultBuilder
) {
var shouldSkipNext = false
for (index in delimiters.indices.reversed()) {
if (shouldSkipNext) {
shouldSkipNext = false
// Start at the end and move backward, matching tokens
var index = delimiters.size - 1

while (index > 0) {
// Find opening tilde
if (!delimiters[index].isOpeningTilde()) {
index -= 1
continue
}
val opener = delimiters[index]
if (opener.tokenType != GFMTokenTypes.TILDE || opener.closerIndex == -1) {
continue
var openerIndex = index
var closerIndex = delimiters[index].closerIndex

// Attempt to widen the matched delimiters
var delimitersMatched = 1
while (EmphStrongDelimiterParser.areAdjacentSameMarkers(delimiters, openerIndex, closerIndex)) {
openerIndex -= 1
closerIndex += 1
delimitersMatched += 1
}
shouldSkipNext = EmphStrongDelimiterParser.areAdjacentSameMarkers(delimiters, index, opener.closerIndex)
val closer = delimiters[opener.closerIndex]
if (shouldSkipNext) {
val node = SequentialParser.Node(
range = opener.position - 1..closer.position + 2,
type = GFMElementTypes.STRIKETHROUGH
)
result.withNode(node)

// If 3 or more delimiters are matched, ignore
if (delimitersMatched < 3) {
val opener = delimiters[openerIndex]
val closer = delimiters[closerIndex]

result.withNode(SequentialParser.Node(opener.position..closer.position + 1, GFMElementTypes.STRIKETHROUGH))
}

// Update index
index = openerIndex - 1
}
}
}

private fun DelimiterParser.Info.isOpeningTilde(): Boolean =
tokenType == GFMTokenTypes.TILDE && closerIndex != -1
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.intellij.markdown.html

import org.intellij.markdown.IElementType
import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.ast.*
Expand All @@ -8,7 +9,6 @@ import org.intellij.markdown.ast.impl.ListItemCompositeNode
import org.intellij.markdown.html.entities.EntityConverter
import org.intellij.markdown.lexer.Compat.assert
import org.intellij.markdown.parser.LinkMap
import kotlin.text.Regex

abstract class OpenCloseGeneratingProvider : GeneratingProvider {
abstract fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode)
Expand Down Expand Up @@ -53,7 +53,7 @@ open class SimpleTagProvider(val tagName: String) : OpenCloseGeneratingProvider(
}

open class SimpleInlineTagProvider(val tagName: String, val renderFrom: Int = 0, val renderTo: Int = 0)
: InlineHolderGeneratingProvider() {
: InlineHolderGeneratingProvider() {
override fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
visitor.consumeTagOpen(node, tagName)
}
Expand All @@ -67,6 +67,40 @@ open class SimpleInlineTagProvider(val tagName: String, val renderFrom: Int = 0,
}
}

/**
* Trims an equal number of delimiters from the start and end off the children.
* Meant to be used to trim surrounding tildes ('~') for strikethroughs.
*/
open class EqualDelimiterTrimmingInlineTagProvider(val tagName: String, val delimiterType: IElementType)
: InlineHolderGeneratingProvider() {
override fun openTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
visitor.consumeTagOpen(node, tagName)
}

override fun closeTag(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
visitor.consumeTagClose(tagName)
}

override fun childrenToRender(node: ASTNode): List<ASTNode> {
if (node.children.isEmpty()) return node.children

var left = 0
var right = node.children.size - 1

while (
node.children[left].type == delimiterType &&
node.children[right].type == delimiterType
) {
left += 1
right -= 1

if (left >= right) break
}

return node.children.subList(left, right + 1)
}
}

class CodeSpanGeneratingProvider: GeneratingProvider {
override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
val nodes = node.children.subList(1, node.children.size - 1)
Expand Down
27 changes: 22 additions & 5 deletions src/commonTest/kotlin/org/intellij/markdown/GfmSpecTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3001,16 +3001,34 @@ class GfmSpecTest : SpecTest(org.intellij.markdown.flavours.gfm.GFMFlavourDescri
// html = "<p><del>Hi</del> Hello, world!</p>\n"
//)

//@Test
//fun testStrikethroughExample491() = doTest(
// markdown = "~~Hi~~ Hello, world!\n",
// html = "<p><span class=\"user-del\">Hi</span> Hello, world!</p>\n"
//)

@Test
fun testStrikethroughExample491() = doTest(
markdown = "~~Hi~~ Hello, world!\n",
html = "<p><span class=\"user-del\">Hi</span> Hello, world!</p>\n"
markdown = "~~Hi~~ Hello, ~there~ world!\n",
html = "<p><span class=\"user-del\">Hi</span> Hello, <span class=\"user-del\">there</span> world!</p>\n"
)

@Test
fun testStrikethroughExample492() = doTest(
markdown = "This ~~has a\n\nnew paragraph~~.\n",
html = "<p>This ~~has a</p>\n<p>new paragraph~~.</p>\n"
markdown = "This ~~has a\n\nnew paragraph~~.\n",
html = "<p>This ~~has a</p>\n<p>new paragraph~~.</p>\n"
)

@Test
fun testStrikethroughExample493() = doTest(
markdown = "This will ~~~not~~~ strike.\n",
html = "<p>This will ~~~not~~~ strike.</p>\n"
)

@Test
fun testStrikethroughManyWords() = doTest(
markdown = "~~Two (so many) words~~\n",
html = "<p><span class=\"user-del\">Two (so many) words</span></p>\n"
)

@Test
Expand Down Expand Up @@ -4151,5 +4169,4 @@ class GfmSpecTest : SpecTest(org.intellij.markdown.flavours.gfm.GFMFlavourDescri
markdown = "Multiple spaces\n",
html = "<p>Multiple spaces</p>\n"
)

}
4 changes: 2 additions & 2 deletions src/fileBasedTest/resources/data/html/ruby17351.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,9 @@ Zookeeper is required for SolrCloud to maintain and synchronize the Solr server
1. `./zkcli.sh -cmd clear -z localhost:2181/kla_chroot /kla_chroot`


#### *~BETA~*
#### *\~BETA\~*
### Jena/Fuseki Local Persistence
#### *~BETA~*
#### *\~BETA\~*
The Knowtify Data Service requires a local data store to be configured so that local data can be stored, managed, and served up to clients. There are 2 possible configurations of the lcoal data store, both of which involve the jena TDB database.

1. Embedded (the database runs in-process to the Data Service). Use this in environments where you are certain there will only ever be a single instance of the Knowtify Data Service to server all of the users.
Expand Down

0 comments on commit aa4a6ae

Please sign in to comment.