Skip to content

Commit 5062997

Browse files
committed
Scaladoc: add Wiki code blocks to Md syntax
The idea here is that in the wild, the parts of Wiki syntax that are the most used are lists, code blocks, monospace text, and links. With this PR we now more-or-less support all of them in Markdown syntax, which means that one will be able to use it for old doc comments and get acceptable-looking output.
1 parent 0195dff commit 5062997

File tree

3 files changed

+179
-4
lines changed

3 files changed

+179
-4
lines changed

scaladoc-testcases/src/tests/tests.scala

+15
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,22 @@ package tests
2020
* ```scala
2121
* is.an("actual code block")
2222
* with.multiple("lines")
23+
* """
24+
* {{{
25+
* and an embedded Wiki code block for good measure
26+
* }}}
27+
* """
28+
* ```
29+
*
30+
* While
31+
* {{{
32+
* this.is("a Wiki code block")
33+
* """
2334
* ```
35+
* with an embedded Md code block for good measure
36+
* ```
37+
* """
38+
* }}}
2439
*
2540
* And this
2641
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package dotty.tools.scaladoc.parsers
2+
3+
import com.vladsch.flexmark.ast._
4+
import com.vladsch.flexmark.parser.Parser
5+
import com.vladsch.flexmark.parser.core._
6+
import com.vladsch.flexmark.parser.block._
7+
import com.vladsch.flexmark.util.ast.Block
8+
import com.vladsch.flexmark.util.ast.BlockContent
9+
import com.vladsch.flexmark.util.options.DataHolder
10+
import com.vladsch.flexmark.util.sequence.BasedSequence
11+
import com.vladsch.flexmark.util.sequence.SegmentedSequence
12+
13+
import java.{util => ju}
14+
import ju.regex.Matcher
15+
import ju.regex.Pattern
16+
17+
18+
/** Copied from FencedCodeBlockParser. */
19+
object WikiCodeBlockParser {
20+
private val OPENING_FENCE = Pattern.compile("^\\{{3}")
21+
private val CLOSING_FENCE = Pattern.compile("^(\\}{3})(?=[ \t]*$)$")
22+
23+
class Factory extends CustomBlockParserFactory {
24+
override def getAfterDependents =
25+
new ju.HashSet[Class[_ <: CustomBlockParserFactory]](ju.Arrays.asList(
26+
classOf[BlockQuoteParser.Factory],
27+
classOf[HeadingParser.Factory],
28+
//FencedCodeBlockParser.Factory.class,
29+
//HtmlBlockParser.Factory.class,
30+
//ThematicBreakParser.Factory.class,
31+
//ListBlockParser.Factory.class,
32+
//IndentedCodeBlockParser.Factory.class
33+
))
34+
35+
override def getBeforeDependents =
36+
new ju.HashSet[Class[_ <: CustomBlockParserFactory]](ju.Arrays.asList(
37+
//BlockQuoteParser.Factory.class,
38+
//HeadingParser.Factory.class,
39+
//FencedCodeBlockParser.Factory.class,
40+
classOf[HtmlBlockParser.Factory],
41+
classOf[ThematicBreakParser.Factory],
42+
classOf[ListBlockParser.Factory],
43+
classOf[IndentedCodeBlockParser.Factory],
44+
))
45+
46+
override def affectsGlobalScope = false
47+
48+
override def create(options: DataHolder) =
49+
new WikiCodeBlockParser.BlockFactory(options)
50+
}
51+
52+
private[WikiCodeBlockParser] class BlockFactory (val options: DataHolder)
53+
extends AbstractBlockParserFactory(options) {
54+
def tryStart(state: ParserState, matchedBlockParser: MatchedBlockParser): BlockStart = {
55+
val nextNonSpace = state.getNextNonSpaceIndex
56+
val line = state.getLine
57+
var matcher: Matcher = null
58+
if state.getIndent < 4 then {
59+
val trySequence = line.subSequence(nextNonSpace, line.length)
60+
matcher = OPENING_FENCE.matcher(trySequence)
61+
if matcher.find then {
62+
val fenceLength = matcher.group(0).length
63+
val blockParser =
64+
new WikiCodeBlockParser(state.getProperties, fenceLength, state.getIndent, nextNonSpace)
65+
blockParser.block.setOpeningMarker(trySequence.subSequence(0, fenceLength))
66+
return BlockStart.of(blockParser).atIndex(nextNonSpace + fenceLength)
67+
}
68+
}
69+
BlockStart.none
70+
}
71+
}
72+
}
73+
74+
/** Copied from FencedCodeBlockParser. */
75+
class WikiCodeBlockParser(
76+
options: DataHolder,
77+
var fenceLength: Int,
78+
private var fenceIndent: Int,
79+
private var fenceMarkerIndent: Int
80+
) extends AbstractBlockParser {
81+
82+
this.fenceMarkerIndent = fenceIndent + fenceMarkerIndent
83+
84+
final private val block = new FencedCodeBlock()
85+
private var content = new BlockContent
86+
private val codeContentBlock = options.get(Parser.FENCED_CODE_CONTENT_BLOCK)
87+
88+
def getBlock: Block = block
89+
def getFenceIndent: Int = fenceIndent
90+
def getFenceMarkerIndent: Int = fenceMarkerIndent
91+
def tryContinue(state: ParserState): BlockContinue = {
92+
val nextNonSpace = state.getNextNonSpaceIndex
93+
var newIndex = state.getIndex
94+
val line = state.getLine
95+
var matcher: Matcher = null
96+
val matches =
97+
state.getIndent <= 3
98+
&& nextNonSpace < line.length
99+
100+
if matches then {
101+
val trySequence = line.subSequence(nextNonSpace, line.length)
102+
matcher = WikiCodeBlockParser.CLOSING_FENCE.matcher(trySequence)
103+
if matcher.find then {
104+
val foundFenceLength = matcher.group(0).length
105+
if (foundFenceLength >= fenceLength) { // closing fence - we're at end of line, so we can finalize now
106+
block.setClosingMarker(trySequence.subSequence(0, foundFenceLength))
107+
return BlockContinue.finished
108+
}
109+
}
110+
}
111+
// skip optional spaces of fence indent
112+
var i = fenceIndent
113+
while ({
114+
i > 0 && newIndex < line.length && line.charAt(newIndex) == ' '
115+
}) do {
116+
newIndex += 1
117+
i -= 1
118+
}
119+
BlockContinue.atIndex(newIndex)
120+
}
121+
122+
override def addLine(state: ParserState, line: BasedSequence): Unit = {
123+
content.add(line, state.getIndent)
124+
}
125+
126+
override def isPropagatingLastBlankLine(lastMatchedBlockParser: BlockParser) = false
127+
128+
override def closeBlock(state: ParserState): Unit = { // first line, if not blank, has the info string
129+
val lines = content.getLines
130+
if (lines.size > 0) {
131+
val info = lines.get(0)
132+
if (!info.isBlank) block.setInfo(info.trim)
133+
val chars = content.getSpanningChars
134+
val spanningChars = chars.baseSubSequence(chars.getStartOffset, lines.get(0).getEndOffset)
135+
if (lines.size > 1) { // have more lines
136+
val segments = lines.subList(1, lines.size)
137+
block.setContent(spanningChars, segments)
138+
if this.codeContentBlock then {
139+
val codeBlock = new CodeBlock()
140+
codeBlock.setContent(segments)
141+
codeBlock.setCharsFromContent
142+
block.appendChild(codeBlock)
143+
} else {
144+
val codeBlock = new Text(SegmentedSequence.of(segments))
145+
block.appendChild(codeBlock)
146+
}
147+
}
148+
else block.setContent(spanningChars, BasedSequence.EMPTY_LIST)
149+
}
150+
else block.setContent(content)
151+
block.setCharsFromContent
152+
content = null
153+
}
154+
}
155+

scaladoc/src/dotty/tools/scaladoc/tasty/comments/MarkdownParser.scala

+9-4
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,19 @@ object MarkdownParser {
4444

4545
val markdownOptions: DataHolder = mkMarkdownOptions(Seq(WikiLinkExtension.create()))
4646

47-
private val parser = Parser.builder(markdownOptions).build()
48-
4947
val RENDERER = Formatter.builder(markdownOptions).build()
5048

5149
def parseToMarkdown(text: String, extensions: Extension*): mdu.Document =
50+
val options =
51+
if extensions.isEmpty then
52+
markdownOptions
53+
else
54+
mkMarkdownOptions(extensions)
55+
5256
val thisParser =
53-
if(extensions.isEmpty) parser
54-
else Parser.builder(mkMarkdownOptions(extensions)).build()
57+
Parser.builder(options)
58+
.customBlockParserFactory(parsers.WikiCodeBlockParser.Factory())
59+
.build()
5560

5661
// We need to remove safe tag markers as they break flexmark parsing
5762
thisParser.parse(text.replace(safeTagMarker.toString, "")).asInstanceOf[mdu.Document]

0 commit comments

Comments
 (0)