Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions markdown/shared/src/main/scala/zio/blocks/docs/HtmlRenderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,17 @@ object HtmlRenderer {
case None => s"""<a href="${escape(url)}">${renderInlines(text)}</a>"""
}

case WikiLink(url, text) =>
text match {
case Some(text) => s"""<a href="${escape(url)}">$text}</a>"""
case None => s"""<a href="${escape(url)}">$url</a>"""
}
case Inline.WikiLink(url, text) =>
text match {
case Some(text) => s"""<a href="${escape(url)}">$text}</a>"""
case None => s"""<a href="${escape(url)}">$url</a>"""
}

case Image(alt, url, titleOpt) =>
titleOpt match {
case Some(title) => s"""<img src="${escape(url)}" alt="${escape(alt)}" title="${escape(title)}">"""
Expand Down
24 changes: 23 additions & 1 deletion markdown/shared/src/main/scala/zio/blocks/docs/Inline.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package zio.blocks.docs
import zio.blocks.chunk.Chunk

/**
* An inline markdown element.
* An inline Markdown element.
*
* Inline elements are the content within block elements, such as text,
* emphasis, links, and code spans.
Expand Down Expand Up @@ -80,6 +80,17 @@ object Inline {
*/
final case class Link(text: Chunk[Inline], url: String, title: Option[String]) extends Inline

/**
* A wiki link.
*
* @param url
* Wiki link URL
*
* @param text
* Wiki link text
*/
final case class WikiLink(url: String, text: Option[String]) extends Inline

/**
* An image.
*
Expand Down Expand Up @@ -169,6 +180,17 @@ final case class Strikethrough(content: Chunk[Inline]) extends Inline
*/
final case class Link(text: Chunk[Inline], url: String, title: Option[String]) extends Inline

/**
* A wiki link.
*
* @param url
* Wiki link URL
*
* @param text
* Wiki link text
*/
final case class WikiLink(url: String, text: Option[String]) extends Inline

/**
* An image.
*
Expand Down
34 changes: 29 additions & 5 deletions markdown/shared/src/main/scala/zio/blocks/docs/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,16 @@ object Parser {
* $${err.line}: $${err.message}") } }}}
*/
def parse(input: String): Either[ParseError, Doc] = {
val state = new ParserState(input)
val state = new ParserState(input, ParserConfig.default)
state.parseDocument()
}

private class ParserState(input: String) {
def parse(input: String, config: ParserConfig): Either[ParseError, Doc] = {
val state = new ParserState(input, config)
state.parseDocument()
}

private class ParserState(input: String, config: ParserConfig) {
private val lines: Array[String] = input.split("\n", -1)
private var lineIndex: Int = 0

Expand Down Expand Up @@ -212,7 +217,7 @@ object Parser {
}

val innerInput = quoteLines.result().toList.mkString("\n")
val innerState = new ParserState(innerInput)
val innerState = new ParserState(innerInput, config)
innerState.parseBlocks().map(blocks => BlockQuote(blocks))
}

Expand Down Expand Up @@ -254,7 +259,7 @@ object Parser {
}

val itemText = continuationLines.result().toList.mkString("\n")
val innerState = new ParserState(itemText)
val innerState = new ParserState(itemText, config)
val innerBlocks = innerState.parseBlocks().getOrElse(Chunk.empty)

val finalBlocks =
Expand Down Expand Up @@ -312,7 +317,7 @@ object Parser {
}

val itemText = continuationLines.result().toList.mkString("\n")
val innerState = new ParserState(itemText)
val innerState = new ParserState(itemText, config)
val innerBlocks = innerState.parseBlocks().getOrElse(Chunk.empty)

val finalBlocks =
Expand Down Expand Up @@ -553,6 +558,12 @@ object Parser {
result += Text("`")
pos += 1
}
} else if (config.processWikiLinks && remaining.startsWith("[[") && remaining.substring(2).contains("]]")) {
val closeBrackets = remaining.indexOf("]]", 2)
if (closeBrackets > 2) {
result += parseWikiLink(remaining.substring(2, closeBrackets))
pos += closeBrackets + 2
}
} else if (remaining.startsWith("![")) {
val closeBracket = remaining.indexOf(']', 2)
if (closeBracket > 2 && remaining.length > closeBracket + 1 && remaining(closeBracket + 1) == '(') {
Expand Down Expand Up @@ -652,6 +663,19 @@ object Parser {
}
}

private def parseWikiLink(body: String): WikiLink = {
val trimmed = body.trim
val pipeIdx = trimmed.indexOf('|')
if (pipeIdx > 0) {
WikiLink(
trimmed.substring(0, pipeIdx).trim,
Some(trimmed.substring(pipeIdx + 1).trim)
)
} else {
WikiLink(trimmed, None)
}
}

private def findNextSpecial(text: String): Int = {
var i = 0
while (i < text.length) {
Expand Down
31 changes: 31 additions & 0 deletions markdown/shared/src/main/scala/zio/blocks/docs/ParserConfig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package zio.blocks.docs

/**
* Configuration for [[Parser]].
*
* @param processWikiLinks
* If true, process wiki links
*/
final case class ParserConfig(
processWikiLinks: Boolean = false
)

object ParserConfig {
val default: ParserConfig = ParserConfig()
}
7 changes: 7 additions & 0 deletions markdown/shared/src/main/scala/zio/blocks/docs/Renderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,13 @@ object Renderer {
val titleStr = titleOpt.map(t => s""" "$t"""").getOrElse("")
s"![${alt}]($url$titleStr)"

case WikiLink(url, text) =>
val textStr = text.map(text => s" | $text").getOrElse("")
s"[[$url$textStr]]"
case Inline.WikiLink(url, text) =>
val textStr = text.map(text => s" | $text").getOrElse("")
s"[[$url$textStr]]"

case HtmlInline(content) =>
content
case Inline.HtmlInline(content) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,17 @@ object TerminalRenderer {
case Inline.Image(alt, url, _) =>
s"[Image: $alt] ($url)"

case WikiLink(url, text) =>
text match {
case None => s"$Blue$Underline$url$Reset"
case Some(text) => s"$Blue$Underline$text$Reset ($url)"
}
case Inline.WikiLink(url, text) =>
text match {
case None => s"$Blue$Underline$url$Reset"
case Some(text) => s"$Blue$Underline$text$Reset ($url)"
}

case HtmlInline(html) =>
html
case Inline.HtmlInline(html) =>
Expand Down
19 changes: 19 additions & 0 deletions markdown/shared/src/test/scala/zio/blocks/docs/ParserSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,25 @@ object ParserSpec extends MarkdownBaseSpec {
link.title == Some("title")
)
},
test("parses wiki link") {
val result = Parser.parse("[[url]]", ParserConfig.default.copy(processWikiLinks = true))
val para = result.toOption.get.blocks.head.asInstanceOf[Paragraph]
println(result.toOption.get.blocks.head)
val link = para.content.head.asInstanceOf[WikiLink]
assertTrue(
link.url == "url",
link.text.isEmpty
)
},
test("parses wiki link with text") {
val result = Parser.parse("[[url | text]]", ParserConfig.default.copy(processWikiLinks = true))
val para = result.toOption.get.blocks.head.asInstanceOf[Paragraph]
val link = para.content.head.asInstanceOf[WikiLink]
assertTrue(
link.url == "url",
link.text == Some("text")
)
},
test("parses image") {
val result = Parser.parse("![alt](url)")
val para = result.toOption.get.blocks.head.asInstanceOf[Paragraph]
Expand Down
16 changes: 16 additions & 0 deletions markdown/shared/src/test/scala/zio/blocks/docs/RendererSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,14 @@ object RendererSpec extends MarkdownBaseSpec {
val doc = Doc(Chunk(Paragraph(Chunk(Link(Chunk(Text("text")), "url", Some("title"))))))
assertTrue(Renderer.render(doc) == "[text](url \"title\")\n\n")
},
test("renders wiki link") {
val doc = Doc(Chunk(Paragraph(Chunk(WikiLink("a wiki link", None)))))
assertTrue(Renderer.render(doc) == "[[a wiki link]]\n\n")
},
test("renders wiki link with text") {
val doc = Doc(Chunk(Paragraph(Chunk(WikiLink("a wiki link", Some("text"))))))
assertTrue(Renderer.render(doc) == "[[a wiki link | text]]\n\n")
},
test("renders image without title") {
val doc = Doc(Chunk(Paragraph(Chunk(Image("alt", "url", None)))))
assertTrue(Renderer.render(doc) == "![alt](url)\n\n")
Expand Down Expand Up @@ -333,6 +341,14 @@ object RendererSpec extends MarkdownBaseSpec {
)
assertTrue(Renderer.render(doc) == "[text](url \"title\")\n\n")
},
test("renders Inline.WikiLink") {
val doc = Doc(Chunk(Paragraph(Chunk(WikiLink("a wiki link", None)))))
assertTrue(Renderer.render(doc) == "[[a wiki link]]\n\n")
},
test("renders Inline.WikiLink with text") {
val doc = Doc(Chunk(Paragraph(Chunk(WikiLink("a wiki link", Some("text"))))))
assertTrue(Renderer.render(doc) == "[[a wiki link | text]]\n\n")
},
test("renders Inline.Image without title") {
val doc = Doc(Chunk(Paragraph(Chunk(Inline.Image("alt", "url", None)))))
assertTrue(Renderer.render(doc) == "![alt](url)\n\n")
Expand Down
14 changes: 14 additions & 0 deletions markdown/shared/src/test/scala/zio/blocks/docs/RoundTripSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,20 @@ object RoundTripSpec extends MarkdownBaseSpec {
val reparsed = rendered.flatMap(Parser.parse)
assertTrue(parsed == reparsed)
},
test("wiki link round-trips") {
val input = "[[url]]\n\n"
val parsed = Parser.parse(input)
val rendered = parsed.map(Renderer.render)
val reparsed = rendered.flatMap(Parser.parse)
assertTrue(parsed == reparsed)
},
test("wiki link with text round-trips") {
val input = "[[url | text]]\n\n"
val parsed = Parser.parse(input)
val rendered = parsed.map(Renderer.render)
val reparsed = rendered.flatMap(Parser.parse)
assertTrue(parsed == reparsed)
},
test("image round-trips") {
val input = "![alt](url)\n\n"
val parsed = Parser.parse(input)
Expand Down
Loading