fix: elaborate and render blockquotes in Verso docstrings (#13670)
This PR adds support for blockquotes to Verso docstrings, which had been missing before. It also substantially improves the robustness of Verso->Markdown rendering of docstrings, especially the handling of blockquote line prefixes.
This commit is contained in:
parent
36a54dbe9c
commit
5d5642107d
6 changed files with 999 additions and 247 deletions
|
|
@ -37,7 +37,9 @@ instance : Repr ElabInline where
|
|||
|
||||
instance : Doc.MarkdownInline ElabInline where
|
||||
-- TODO extensibility
|
||||
toMarkdown go _i content := content.forM go
|
||||
toMarkdown go _i content := do
|
||||
let parts ← content.mapM go
|
||||
return Doc.joinInlines parts
|
||||
|
||||
|
||||
/--
|
||||
|
|
@ -57,7 +59,9 @@ instance : Repr ElabBlock where
|
|||
|
||||
-- TODO extensible toMarkdown
|
||||
instance : Doc.MarkdownBlock ElabInline ElabBlock where
|
||||
toMarkdown _goI goB _b content := content.forM goB
|
||||
toMarkdown _goI goB _b content := do
|
||||
let parts ← content.mapM goB
|
||||
return Doc.joinBlocks parts
|
||||
|
||||
structure VersoDocString where
|
||||
text : Array (Doc.Block ElabInline ElabBlock)
|
||||
|
|
@ -176,10 +180,9 @@ def findSimpleDocString? (env : Environment) (declName : Name) (includeBuiltin :
|
|||
where
|
||||
toMarkdown : VersoDocString → String
|
||||
| .mk bs ps => Doc.MarkdownM.run' do
|
||||
for b in bs do
|
||||
Doc.ToMarkdown.toMarkdown b
|
||||
for p in ps do
|
||||
Doc.ToMarkdown.toMarkdown p
|
||||
let blockLines ← bs.mapM Doc.ToMarkdown.toMarkdown
|
||||
let partLines ← ps.mapM Doc.ToMarkdown.toMarkdown
|
||||
return Doc.joinBlocks (blockLines ++ partLines)
|
||||
|
||||
|
||||
structure ModuleDoc where
|
||||
|
|
@ -273,19 +276,13 @@ def addPart (snippet : Snippet) (level : Nat) (range : DeclarationRange) (part :
|
|||
|
||||
end VersoModuleDocs.Snippet
|
||||
|
||||
open Lean Doc ToMarkdown MarkdownM in
|
||||
open Lean Doc ToMarkdown in
|
||||
instance : ToMarkdown VersoModuleDocs.Snippet where
|
||||
toMarkdown
|
||||
| {text, sections, ..} => do
|
||||
text.forM toMarkdown
|
||||
endBlock
|
||||
for (level, _, part) in sections do
|
||||
push ("".pushn '#' (level + 1))
|
||||
push " "
|
||||
for i in part.title do toMarkdown i
|
||||
endBlock
|
||||
for b in part.content do toMarkdown b
|
||||
endBlock
|
||||
let textBlocks ← text.mapM toMarkdown
|
||||
let sectionBlocks ← sections.mapM fun (level, _, part) => partMarkdown level part
|
||||
return joinBlocks (textBlocks ++ sectionBlocks)
|
||||
|
||||
structure VersoModuleDocs where
|
||||
snippets : PersistentArray VersoModuleDocs.Snippet := {}
|
||||
|
|
|
|||
|
|
@ -18,152 +18,207 @@ set_option linter.missingDocs true
|
|||
|
||||
namespace Lean.Doc
|
||||
|
||||
/--
|
||||
A monad for accumulating footnotes while rendering Markdown. Footnote labels are paired with their
|
||||
already-rendered bodies, in order of first reference. `MarkdownM.run'` flushes them at the end of
|
||||
the document.
|
||||
-/
|
||||
public abbrev MarkdownM := StateM (Array (String × String))
|
||||
|
||||
namespace MarkdownM
|
||||
|
||||
/--
|
||||
The surrounding context of Markdown that's being generated, in order to prevent nestings that
|
||||
Markdown doesn't allow.
|
||||
Tracks which inline delimiters are already open so that nested `*` / `**` / `[…](…)` don't emit
|
||||
redundant openers and closers.
|
||||
-/
|
||||
public structure Context where
|
||||
/-- The current code is inside emphasis. -/
|
||||
public structure InlineCtx where
|
||||
/-- The current code is inside emphasis (`*…*`). -/
|
||||
inEmph : Bool := false
|
||||
/-- The current code is inside strong emphasis. -/
|
||||
/-- The current code is inside strong emphasis (`**…**`). -/
|
||||
inBold : Bool := false
|
||||
/-- The current code is inside a link. -/
|
||||
/-- The current code is inside a link's display text. -/
|
||||
inLink : Bool := false
|
||||
/-- The prefix that should be added to each line (typically for indentation). -/
|
||||
linePrefix : String := ""
|
||||
|
||||
/-- The state of a Markdown generation task. -/
|
||||
public structure State where
|
||||
/-- The blocks prior to the one being generated. -/
|
||||
priorBlocks : String := ""
|
||||
/-- The block being generated. -/
|
||||
currentBlock : String := ""
|
||||
/-- Footnotes -/
|
||||
footnotes : Array (String × String) := #[]
|
||||
|
||||
def combineBlocks (prior current : String) :=
|
||||
if prior.isEmpty then current
|
||||
else if current.isEmpty then prior
|
||||
else if prior.endsWith "\n\n" then prior ++ current
|
||||
else if prior.endsWith "\n" then prior ++ "\n" ++ current
|
||||
else prior ++ "\n\n" ++ current
|
||||
|
||||
def State.endBlock (state : State) : State :=
|
||||
{ state with
|
||||
priorBlocks :=
|
||||
combineBlocks state.priorBlocks state.currentBlock ++
|
||||
(if state.footnotes.isEmpty then ""
|
||||
else state.footnotes.foldl (init := "\n\n") fun s (n, txt) => s ++ s!"[^{n}]:{txt}\n\n"),
|
||||
currentBlock := "",
|
||||
footnotes := #[]
|
||||
}
|
||||
|
||||
def State.render (state : State) : String :=
|
||||
state.endBlock.priorBlocks
|
||||
|
||||
def State.push (state : State) (txt : String) : State :=
|
||||
{ state with currentBlock := state.currentBlock ++ txt }
|
||||
|
||||
def State.endsWith (state : State) (txt : String) : Bool :=
|
||||
state.currentBlock.endsWith txt || (state.currentBlock.isEmpty && state.priorBlocks.endsWith txt)
|
||||
deriving Inhabited
|
||||
|
||||
end MarkdownM
|
||||
|
||||
open MarkdownM in
|
||||
/--
|
||||
The monad for generating Markdown output.
|
||||
Renders an action that produces an array of lines into a single Markdown string, appending any
|
||||
accumulated footnotes after the main body.
|
||||
-/
|
||||
public abbrev MarkdownM := ReaderT Context (StateM State)
|
||||
public def MarkdownM.run' (act : MarkdownM (Array String)) : String :=
|
||||
let (lines, footnotes) := act #[]
|
||||
let main := "\n".intercalate lines.toList
|
||||
if footnotes.isEmpty then
|
||||
main
|
||||
else
|
||||
let foots := footnotes.toList.map fun (n, t) => s!"[^{n}]:{t}"
|
||||
main ++ "\n\n" ++ "\n\n".intercalate foots
|
||||
|
||||
/--
|
||||
Generates Markdown, rendering the result from the final state.
|
||||
Drops trailing ASCII spaces (`' '`) from a string. Used to compute the "empty line" form of a
|
||||
per-line prefix (`"> "` becomes `">"`) so that prefixed empty lines don't carry trailing whitespace
|
||||
into the rendered output.
|
||||
-/
|
||||
public def MarkdownM.run (act : MarkdownM α) (context : Context := {}) (state : State := {}) : (α × String) :=
|
||||
let (val, state) := act context state
|
||||
(val, state.render)
|
||||
def trimEndSpaces (s : String) : String :=
|
||||
s.dropEndWhile ' ' |>.copy
|
||||
|
||||
/--
|
||||
Generates Markdown, rendering the result from the final state, without producing a value.
|
||||
Applies a uniform prefix to every line. Empty lines receive the trimmed prefix to avoid trailing
|
||||
whitespace (which denotes a hard line break).
|
||||
-/
|
||||
public def MarkdownM.run' (act : MarkdownM Unit) (context : Context := {}) (state : State := {}) : String :=
|
||||
act.run context state |>.2
|
||||
public def prefixLines (p : String) (lines : Array String) : Array String :=
|
||||
let pTrim := trimEndSpaces p
|
||||
lines.map fun l => if l.isEmpty then pTrim else p ++ l
|
||||
|
||||
/--
|
||||
Adds a string to the current Markdown output.
|
||||
Applies one prefix to the first line and a different prefix to subsequent lines. Used for list items,
|
||||
where the first line gets `"* "` / `"1. "` and the continuation lines get `" "` / `" "`.
|
||||
-/
|
||||
public def MarkdownM.push (txt : String) : MarkdownM Unit := modify (·.push txt)
|
||||
public def prefixListLines (head rest : String) (lines : Array String) : Array String :=
|
||||
let headTrim := trimEndSpaces head
|
||||
let restTrim := trimEndSpaces rest
|
||||
lines.mapIdx fun i l =>
|
||||
let (p, pTrim) := if i = 0 then (head, headTrim) else (rest, restTrim)
|
||||
if l.isEmpty then pTrim else p ++ l
|
||||
|
||||
/--
|
||||
Checks whether the current output ends with the given string.
|
||||
Concatenates an array of "block" line arrays into one line array, with a single empty separator line
|
||||
between adjacent non-empty blocks. Empty input blocks are skipped.
|
||||
-/
|
||||
public def MarkdownM.endsWith (txt : String) : MarkdownM Bool := do
|
||||
return (← get).endsWith txt
|
||||
public def joinBlocks (blocks : Array (Array String)) : Array String :=
|
||||
blocks.foldl (init := #[]) fun acc b =>
|
||||
if b.isEmpty then acc
|
||||
else if acc.isEmpty then b
|
||||
else acc.push "" ++ b
|
||||
|
||||
/--
|
||||
Terminates the current block.
|
||||
Concatenates two strings that are the result of rendering consecutive inliens, inserting a U+200B
|
||||
zero-width space when both concatenated ends are backticks. Markdown has no syntactic way to place
|
||||
two code spans consecutively, so the zero-width space disambiguates them without introducing visible
|
||||
whitespace.
|
||||
-/
|
||||
public def MarkdownM.endBlock : MarkdownM Unit := modify (·.endBlock)
|
||||
def glueInlineBoundary (l r : String) : String :=
|
||||
if l.endsWith "`" ∧ r.startsWith "`" then l ++ "" ++ r
|
||||
else l ++ r
|
||||
|
||||
/--
|
||||
Increases the indentation level by one.
|
||||
Concatenates a array of lines, where each line is an array of rendered inlines. The last line of one
|
||||
piece is glued onto the first line of the next via `glueInlineBoundary` (so `.text "a"` followed by
|
||||
`.text "b"` yields one line `"ab"`, but two adjacent `.code` spans get a U+200B between them).
|
||||
-/
|
||||
public def MarkdownM.indent: MarkdownM α → MarkdownM α :=
|
||||
withReader fun st => { st with linePrefix := st.linePrefix ++ " " }
|
||||
public def joinInlines (parts : Array (Array String)) : Array String :=
|
||||
parts.foldl (init := #[]) fun acc next =>
|
||||
if h : next.size = 0 then acc
|
||||
else if h' : acc.size = 0 then next
|
||||
else
|
||||
let lastIdx := acc.size - 1
|
||||
have : 0 < acc.size := Nat.ne_zero_iff_zero_lt.mp h'
|
||||
have : 0 < next.size := Nat.ne_zero_iff_zero_lt.mp h
|
||||
have : lastIdx < acc.size := Nat.sub_one_lt h'
|
||||
let glued := glueInlineBoundary (acc[lastIdx]) next[0]
|
||||
acc.set lastIdx glued ++ next.drop 1
|
||||
|
||||
/--
|
||||
A means of transforming values to Markdown representations.
|
||||
A means of transforming values into Markdown lines.
|
||||
-/
|
||||
public class ToMarkdown (α : Type u) where
|
||||
/--
|
||||
A function that transforms an `α` into a Markdown representation.
|
||||
Render an `α` to its Markdown lines. The lines should not contain literal `\n`
|
||||
characters; line breaks are encoded by the array structure.
|
||||
-/
|
||||
toMarkdown : α → MarkdownM Unit
|
||||
toMarkdown : α → MarkdownM (Array String)
|
||||
|
||||
/--
|
||||
A way to transform inline elements extended with `i` into Markdown.
|
||||
A way to transform inline elements extended with `i` into Markdown lines.
|
||||
-/
|
||||
public class MarkdownInline (i : Type u) where
|
||||
/--
|
||||
A function that transforms an `i` and its contents into Markdown, given a way to transform the
|
||||
contents.
|
||||
Render the `i` and its contents to Markdown lines, given a recursive renderer
|
||||
for ordinary inline content.
|
||||
-/
|
||||
toMarkdown : (Inline i → MarkdownM Unit) → i → Array (Inline i) → MarkdownM Unit
|
||||
toMarkdown :
|
||||
(Inline i → MarkdownM (Array String)) → i → Array (Inline i) →
|
||||
MarkdownM (Array String)
|
||||
|
||||
public instance : MarkdownInline Empty where
|
||||
toMarkdown := nofun
|
||||
|
||||
/--
|
||||
A way to transform block elements extended with `b` that contain inline elements extended with `i`
|
||||
into Markdown.
|
||||
A way to transform block elements extended with `b` (containing inline elements
|
||||
extended with `i`) into Markdown lines.
|
||||
-/
|
||||
public class MarkdownBlock (i : Type u) (b : Type v) where
|
||||
/--
|
||||
A function that transforms a `b` and its contents into Markdown, given a way to transform the
|
||||
contents.
|
||||
Render the `b` and its contents to Markdown lines, given recursive renderers
|
||||
for inline and block content.
|
||||
-/
|
||||
toMarkdown :
|
||||
(Inline i → MarkdownM Unit) → (Block i b → MarkdownM Unit) →
|
||||
b → Array (Block i b) → MarkdownM Unit
|
||||
(Inline i → MarkdownM (Array String)) → (Block i b → MarkdownM (Array String)) →
|
||||
b → Array (Block i b) → MarkdownM (Array String)
|
||||
|
||||
public instance : MarkdownBlock i Empty where
|
||||
toMarkdown := nofun
|
||||
|
||||
/--
|
||||
Checks whether `c` needs escaping in a context that definitely cannot start a block. `next?` is the
|
||||
following character, used for the `![…]` image-syntax check.
|
||||
-/
|
||||
def midLineSpecial (c : Char) (next? : Option Char) : Bool :=
|
||||
match c with
|
||||
| '!' => next? == some '['
|
||||
| _ => "*_`<[]{}()#".any c
|
||||
|
||||
/--
|
||||
Checks whether `c` needs escaping in a context that could potentially start a block. In these
|
||||
contexts, potential list and blockquote markers need escaping.
|
||||
-/
|
||||
def markerPrefixSpecial (prev? : Option Char) (c : Char) (next? : Option Char) : Bool :=
|
||||
let endsMarker := next? == some ' ' || next?.isNone
|
||||
match c with
|
||||
| '>' => true
|
||||
| '-' | '+' => endsMarker
|
||||
| '.' => prev?.any (·.isDigit) && endsMarker
|
||||
| _ => midLineSpecial c next?
|
||||
|
||||
/--
|
||||
Backslash-escapes Markdown-significant punctuation in `s` so it renders as literal text. `s` must
|
||||
be a single line.
|
||||
-/
|
||||
def escape (s : String) : String := Id.run do
|
||||
let mut s' := ""
|
||||
let mut iter := s.startPos
|
||||
let mut prev? : Option Char := none
|
||||
-- First, escape everything that could start a block, until that's definitely impossible.
|
||||
while h : ¬iter.IsAtEnd do
|
||||
let c := iter.get h
|
||||
iter := iter.next h
|
||||
if isSpecial c then
|
||||
let nextIter := iter.next h
|
||||
let next? : Option Char :=
|
||||
if h' : ¬nextIter.IsAtEnd then some (nextIter.get h') else none
|
||||
if markerPrefixSpecial prev? c next? then
|
||||
s' := s'.push '\\'
|
||||
s' := s'.push c
|
||||
prev? := some c
|
||||
iter := nextIter
|
||||
unless c.isDigit || "> -+. \t".any c do
|
||||
break
|
||||
-- Now escape more conservatively. prev? is no longer used.
|
||||
while h : ¬iter.IsAtEnd do
|
||||
let c := iter.get h
|
||||
let nextIter := iter.next h
|
||||
let next? : Option Char :=
|
||||
if h' : ¬nextIter.IsAtEnd then some (nextIter.get h') else none
|
||||
if midLineSpecial c next? then
|
||||
s' := s'.push '\\'
|
||||
s' := s'.push c
|
||||
iter := nextIter
|
||||
return s'
|
||||
where
|
||||
isSpecial c := "*_`-+.!<>[]{}()#".any (· == c)
|
||||
|
||||
def quoteCode (str : String) : String := Id.run do
|
||||
/--
|
||||
Length of the longest unbroken run of `` ` `` characters in `str`.
|
||||
-/
|
||||
def longestBacktickRun (str : String) : Nat := Id.run do
|
||||
let mut longest := 0
|
||||
let mut current := 0
|
||||
let mut iter := str.startPos
|
||||
|
|
@ -175,10 +230,44 @@ def quoteCode (str : String) : String := Id.run do
|
|||
else
|
||||
longest := max longest current
|
||||
current := 0
|
||||
let backticks := "".pushn '`' (max longest current + 1)
|
||||
return max longest current
|
||||
|
||||
/--
|
||||
A run of `` ` `` characters one longer than any run inside `str` (with a minimum of `atLeast`),
|
||||
suitable for use as a code block fence or a delimiter for inline code.
|
||||
-/
|
||||
def fenceFor (atLeast : Nat) (str : String) : String :=
|
||||
"".pushn '`' (max atLeast (longestBacktickRun str) + 1)
|
||||
|
||||
def quoteCode (str : String) : String :=
|
||||
let backticks := fenceFor 0 str
|
||||
let str := if str.startsWith "`" || str.endsWith "`" then " " ++ str ++ " " else str
|
||||
backticks ++ str ++ backticks
|
||||
|
||||
/--
|
||||
Splits a string into an array of lines on `'\n'`, retaining empty trailing/leading lines (so
|
||||
`"a\nb\n"` yields `#["a", "b", ""]`). Used to convert code blocks into the line-array representation
|
||||
used by the rest of the renderer.
|
||||
-/
|
||||
def splitNewlines (str : String) : Array String :=
|
||||
str.split '\n' |>.map (·.copy) |>.toArray
|
||||
|
||||
/--
|
||||
Produces a fenced code block as a line array (with no surrounding blank lines), with a sufficiently
|
||||
long fence.
|
||||
-/
|
||||
def codeBlockLines (str : String) : Array String := Id.run do
|
||||
let fence := fenceFor 2 str
|
||||
let body := splitNewlines str
|
||||
-- Drop a trailing empty so the closing fence isn't preceded by a blank line.
|
||||
let body := if body.size > 0 && body.back!.isEmpty then body.pop else body
|
||||
#[fence] ++ body ++ #[fence]
|
||||
|
||||
/--
|
||||
Splits off the leading run of whitespace from an inline tree, returning the whitespace as a plain
|
||||
string and the remainder as a new inline. This allows emphasis to be applied to strings that start
|
||||
or end with whitespace, taking the flanking rules of Markdown into account.
|
||||
-/
|
||||
partial def trimLeft (inline : Inline i) : (String × Inline i) := go [inline]
|
||||
where
|
||||
go : List (Inline i) → String × Inline i
|
||||
|
|
@ -194,168 +283,151 @@ where
|
|||
| .concat xs :: more => go (xs.toList ++ more)
|
||||
| here :: more => ("", here ++ .concat more.toArray)
|
||||
|
||||
partial def trimRight (inline : Inline i) : (Inline i × String) := go [inline]
|
||||
/--
|
||||
Splits off the trailing run of whitespace from an inline tree, returning the remainder as a new
|
||||
inline and the whitespace as a plain string. This allows emphasis to be applied to strings that
|
||||
start or end with whitespace, taking the flanking rules of Markdown into account.
|
||||
-/
|
||||
partial def trimRight (inline : Inline i) : (Inline i × String) := go #[inline]
|
||||
where
|
||||
go : List (Inline i) → Inline i × String
|
||||
| [] => (.empty, "")
|
||||
| .text s :: more =>
|
||||
if s.all Char.isWhitespace then
|
||||
let (pre, post) := go more
|
||||
(pre, post ++ s)
|
||||
else
|
||||
let s1 := s.takeEndWhile Char.isWhitespace |>.copy
|
||||
let s2 := s.dropEnd s1.length |>.copy
|
||||
(.concat more.toArray.reverse ++ .text s2, s1)
|
||||
| .concat xs :: more => go (xs.reverse.toList ++ more)
|
||||
| here :: more => (.concat more.toArray.reverse ++ here, "")
|
||||
go (xs : Array (Inline i)) : Inline i × String :=
|
||||
if h : xs.size = 0 then (.empty, "")
|
||||
else
|
||||
match xs.back (Nat.ne_zero_iff_zero_lt.mp h) with
|
||||
| .text s =>
|
||||
if s.all Char.isWhitespace then
|
||||
let (pre, post) := go xs.pop
|
||||
(pre, post ++ s)
|
||||
else
|
||||
let pos := s.skipSuffixWhile Char.isWhitespace
|
||||
let ws := s.sliceFrom pos
|
||||
let nonWs := s.sliceTo pos
|
||||
(.concat (xs.pop.push (.text nonWs.copy)), ws.copy)
|
||||
| .concat ys => go (xs.pop ++ ys)
|
||||
| _ => (.concat xs, "")
|
||||
|
||||
|
||||
/--
|
||||
Strips leading and trailing whitespace from `inline`, returning the leading whitespace, the stripped
|
||||
middle (still an `Inline`), and the trailing whitespace, in order. The caller can then re-emit the
|
||||
leading and trailing whitespace *outside* of emphasis.
|
||||
-/
|
||||
def trim (inline : Inline i) : (String × Inline i × String) :=
|
||||
let (pre, more) := trimLeft inline
|
||||
let (mid, post) := trimRight more
|
||||
(pre, mid, post)
|
||||
|
||||
open MarkdownM in
|
||||
partial def inlineMarkdown [MarkdownInline i] : Inline i → MarkdownM Unit
|
||||
| .text s =>
|
||||
push (escape s)
|
||||
| .linebreak s => do
|
||||
push <| s.replace "\n" ("\n" ++ (← read).linePrefix )
|
||||
| .emph xs => do
|
||||
/--
|
||||
Renders an `Inline i` to its Markdown lines. The `InlineCtx` argument tracks which inline delimiters
|
||||
are already open so that nested `*` / `**` / `[…](…)` don't emit redundant openers and closers. The
|
||||
top level is represented by `{}`.
|
||||
-/
|
||||
partial def inlineMarkdown [MarkdownInline i] :
|
||||
InlineCtx → Inline i → MarkdownM (Array String)
|
||||
| _, .text s => return #[escape s]
|
||||
| _, .linebreak _ => return #["", ""]
|
||||
| ctx, .emph xs => do
|
||||
let (pre, mid, post) := trim (.concat xs)
|
||||
push pre
|
||||
unless (← read).inEmph do
|
||||
push "*"
|
||||
withReader (fun ρ => { ρ with inEmph := true }) do
|
||||
inlineMarkdown mid
|
||||
unless (← read).inEmph do
|
||||
push "*"
|
||||
push post
|
||||
| .bold xs => do
|
||||
let inner ← inlineMarkdown { ctx with inEmph := true } mid
|
||||
let mut pieces : Array (Array String) := #[]
|
||||
if !pre.isEmpty then pieces := pieces.push #[pre]
|
||||
if !ctx.inEmph then pieces := pieces.push #["*"]
|
||||
pieces := pieces.push inner
|
||||
if !ctx.inEmph then pieces := pieces.push #["*"]
|
||||
if !post.isEmpty then pieces := pieces.push #[post]
|
||||
return joinInlines pieces
|
||||
| ctx, .bold xs => do
|
||||
let (pre, mid, post) := trim (.concat xs)
|
||||
push pre
|
||||
unless (← read).inBold do
|
||||
push "**"
|
||||
withReader (fun ρ => { ρ with inEmph := true }) do
|
||||
inlineMarkdown mid
|
||||
unless (← read).inBold do
|
||||
push "**"
|
||||
push post
|
||||
| .concat xs =>
|
||||
for i in xs do inlineMarkdown i
|
||||
| .link content url => do
|
||||
if (← read).inLink then
|
||||
for i in content do inlineMarkdown i
|
||||
let inner ← inlineMarkdown { ctx with inBold := true } mid
|
||||
let mut pieces : Array (Array String) := #[]
|
||||
if !pre.isEmpty then pieces := pieces.push #[pre]
|
||||
if !ctx.inBold then pieces := pieces.push #["**"]
|
||||
pieces := pieces.push inner
|
||||
if !ctx.inBold then pieces := pieces.push #["**"]
|
||||
if !post.isEmpty then pieces := pieces.push #[post]
|
||||
return joinInlines pieces
|
||||
| ctx, .concat xs => do
|
||||
let parts ← xs.mapM (inlineMarkdown ctx)
|
||||
return joinInlines parts
|
||||
| ctx, .link content url => do
|
||||
if ctx.inLink then
|
||||
let parts ← content.mapM (inlineMarkdown ctx)
|
||||
return joinInlines parts
|
||||
else
|
||||
push "["
|
||||
for i in content do inlineMarkdown i
|
||||
push "]("
|
||||
push url
|
||||
push ")"
|
||||
| .image alt url =>
|
||||
push s!""
|
||||
| .footnote name content => do
|
||||
push s!"[ˆ^{name}]"
|
||||
let footnoteContent := (content.forM inlineMarkdown) {} {} |>.2.render
|
||||
modify fun st => { st with footnotes := st.footnotes.push (name, footnoteContent) }
|
||||
| .code str => do
|
||||
if (← endsWith "`") then
|
||||
-- Markdown has no reasonable way to put one code element after another. This is a zero-width
|
||||
-- space to work around this syntactic limitation:
|
||||
push ""
|
||||
push (quoteCode str)
|
||||
| .math .display m => push s!"$${m}$$"
|
||||
| .math .inline m => push s!"${m}$"
|
||||
| .other container content => do
|
||||
MarkdownInline.toMarkdown inlineMarkdown container content
|
||||
let inner ← inlineMarkdown { ctx with inLink := true } (.concat content)
|
||||
return joinInlines #[#["["], inner, #["](" ++ url ++ ")"]]
|
||||
| _, .image alt url => return #[s!""]
|
||||
| ctx, .footnote name content => do
|
||||
let parts ← content.mapM (inlineMarkdown ctx)
|
||||
let footnoteContent := "\n".intercalate (joinInlines parts).toList
|
||||
modify (·.push (name, footnoteContent))
|
||||
return #[s!"[^{name}]"]
|
||||
| _, .code str => return #[quoteCode str]
|
||||
| _, .math .display m => return #[s!"$${m}$$"]
|
||||
| _, .math .inline m => return #[s!"${m}$"]
|
||||
| ctx, .other container content =>
|
||||
MarkdownInline.toMarkdown (inlineMarkdown ctx) container content
|
||||
|
||||
public instance [MarkdownInline i] : ToMarkdown (Inline i) where
|
||||
toMarkdown inline := private inlineMarkdown inline
|
||||
toMarkdown := private inlineMarkdown {}
|
||||
|
||||
def quoteCodeBlock (indent : Nat) (str : String) : String := Id.run do
|
||||
let mut longest := 2
|
||||
let mut current := 0
|
||||
let mut iter := str.startPos
|
||||
let mut out := ""
|
||||
while h : ¬iter.IsAtEnd do
|
||||
let c := iter.get h
|
||||
iter := iter.next h
|
||||
if c == '`' then
|
||||
current := current + 1
|
||||
else
|
||||
longest := max longest current
|
||||
current := 0
|
||||
out := out.push c
|
||||
if c == '\n' then
|
||||
out := out.pushn ' ' indent
|
||||
out := if out.endsWith "\n" then out else out.push '\n'
|
||||
let backticks := "" |>.pushn ' ' indent |>.pushn '`' (max longest current + 1)
|
||||
backticks ++ "\n" ++ out ++ backticks ++ "\n"
|
||||
|
||||
open MarkdownM in
|
||||
partial def blockMarkdown [MarkdownInline i] [MarkdownBlock i b] : Block i b → MarkdownM Unit
|
||||
| .para xs => do
|
||||
for i in xs do
|
||||
ToMarkdown.toMarkdown i
|
||||
endBlock
|
||||
| .concat bs =>
|
||||
for b in bs do
|
||||
blockMarkdown b
|
||||
partial def blockMarkdown [MarkdownInline i] [MarkdownBlock i b] :
|
||||
Block i b → MarkdownM (Array String)
|
||||
| .para xs => inlineMarkdown {} (.concat xs)
|
||||
| .concat bs => do
|
||||
let parts ← bs.mapM blockMarkdown
|
||||
return joinBlocks parts
|
||||
| .blockquote bs => do
|
||||
withReader (fun ρ => { ρ with linePrefix := ρ.linePrefix ++ "> " })
|
||||
for b in bs do
|
||||
blockMarkdown b
|
||||
endBlock
|
||||
let parts ← bs.mapM blockMarkdown
|
||||
return prefixLines "> " (joinBlocks parts)
|
||||
| .ul items => do
|
||||
for item in items do
|
||||
push <| (← read).linePrefix ++ "* "
|
||||
withReader (fun ρ => { ρ with linePrefix := ρ.linePrefix ++ " " }) do
|
||||
for b in item.contents do
|
||||
blockMarkdown b
|
||||
endBlock
|
||||
let rendered ← items.mapM fun item => do
|
||||
let inner ← item.contents.mapM blockMarkdown
|
||||
return prefixListLines "* " " " (joinBlocks inner)
|
||||
return joinBlocks rendered
|
||||
| .ol start items => do
|
||||
let mut out : Array (Array String) := #[]
|
||||
let mut n := max 1 start.toNat
|
||||
for item in items do
|
||||
push <| (← read).linePrefix ++ s!"{n}. "
|
||||
withReader (fun ρ => { ρ with linePrefix := ρ.linePrefix ++ " " }) do
|
||||
for b in item.contents do
|
||||
blockMarkdown b
|
||||
let head := s!"{n}. "
|
||||
let cont := "".pushn ' ' head.length
|
||||
let inner ← item.contents.mapM blockMarkdown
|
||||
out := out.push (prefixListLines head cont (joinBlocks inner))
|
||||
n := n + 1
|
||||
endBlock
|
||||
return joinBlocks out
|
||||
| .dl items => do
|
||||
for item in items do
|
||||
push <| (← read).linePrefix ++ "* "
|
||||
withReader (fun ρ => { ρ with linePrefix := ρ.linePrefix ++ " " }) do
|
||||
inlineMarkdown (.bold item.term)
|
||||
inlineMarkdown (.text ": " : Inline i)
|
||||
push "\n"
|
||||
push (← read).linePrefix
|
||||
blockMarkdown (.concat item.desc)
|
||||
endBlock
|
||||
| .code str => do
|
||||
unless (← get).currentBlock.isEmpty || (← get).currentBlock.endsWith "\n" do
|
||||
push "\n"
|
||||
push <| quoteCodeBlock (← read).linePrefix.length str
|
||||
endBlock
|
||||
let rendered ← items.mapM fun item => do
|
||||
let term ← inlineMarkdown {} (.bold item.term)
|
||||
let desc ← item.desc.mapM blockMarkdown
|
||||
let termWithColon := joinInlines #[term, #[":"]]
|
||||
-- Single-block descriptions are attached directly to the term. Multi-block descriptions are
|
||||
-- separated from the term with a blank line so the term doesn't fuse into the
|
||||
-- first description paragraph.
|
||||
let body :=
|
||||
if item.desc.size ≤ 1 then termWithColon ++ joinBlocks desc
|
||||
else joinBlocks (#[termWithColon] ++ desc)
|
||||
return prefixListLines "* " " " body
|
||||
return joinBlocks rendered
|
||||
| .code str => return codeBlockLines str
|
||||
| .other container content =>
|
||||
MarkdownBlock.toMarkdown (i := i) (b := b) inlineMarkdown blockMarkdown container content
|
||||
|
||||
MarkdownBlock.toMarkdown (i := i) (b := b) (inlineMarkdown {}) blockMarkdown container content
|
||||
|
||||
public instance [MarkdownInline i] [MarkdownBlock i b] : ToMarkdown (Block i b) where
|
||||
toMarkdown block := private blockMarkdown block
|
||||
toMarkdown := private blockMarkdown
|
||||
|
||||
open MarkdownM in
|
||||
open ToMarkdown in
|
||||
partial def partMarkdown [MarkdownInline i] [MarkdownBlock i b] (level : Nat) (part : Part i b p) : MarkdownM Unit := do
|
||||
push ("".pushn '#' (level + 1))
|
||||
push " "
|
||||
for i in part.title do
|
||||
toMarkdown i
|
||||
endBlock
|
||||
for b in part.content do
|
||||
toMarkdown b
|
||||
endBlock
|
||||
for p in part.subParts do
|
||||
partMarkdown (level + 1) p
|
||||
/--
|
||||
Renders a `Part` (a logical document section) at the given heading level. Headings use ATX-style
|
||||
(`#`, `##`, …) with level `0` rendering as `#`.
|
||||
-/
|
||||
public partial def partMarkdown [MarkdownInline i] [MarkdownBlock i b] (level : Nat)
|
||||
(part : Part i b p) : MarkdownM (Array String) := do
|
||||
let titleParts ← part.title.mapM ToMarkdown.toMarkdown
|
||||
let header := "".pushn '#' (level + 1) ++ " "
|
||||
let titleLine := joinInlines (#[#[header]] ++ titleParts)
|
||||
let contentBlocks ← part.content.mapM ToMarkdown.toMarkdown
|
||||
let subPartBlocks ← part.subParts.mapM (partMarkdown (level + 1))
|
||||
return joinBlocks (#[titleLine] ++ contentBlocks ++ subPartBlocks)
|
||||
|
||||
public instance [MarkdownInline i] [MarkdownBlock i b] : ToMarkdown (Part i b p) where
|
||||
toMarkdown part := private partMarkdown 0 part
|
||||
toMarkdown part := partMarkdown 0 part
|
||||
|
|
|
|||
|
|
@ -1448,6 +1448,8 @@ public partial def elabBlock (stx : TSyntax `block) : DocM (Block ElabInline Ela
|
|||
match stx with
|
||||
| `(block|para[$inls*]) =>
|
||||
.para <$> inls.mapM elabInline
|
||||
| `(block| > $blocks*) =>
|
||||
.blockquote <$> blocks.mapM elabBlock
|
||||
| `(block|ul{$[* $itemss*]*}) =>
|
||||
.ul <$> itemss.mapM fun items =>
|
||||
.mk <$> items.mapM elabBlock
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ def topLevel := 1
|
|||
/--
|
||||
info: #[]
|
||||
#[{ title := #[Lean.Doc.Inline.text "Top-level header"],
|
||||
titleString := "Top\\-level header",
|
||||
titleString := "Top-level header",
|
||||
metadata := none,
|
||||
content := #[Lean.Doc.Block.para #[Lean.Doc.Inline.text "Content.", Lean.Doc.Inline.linebreak "\n"]],
|
||||
subParts := #[] }]
|
||||
|
|
|
|||
|
|
@ -18,10 +18,9 @@ open Lean Elab Command Term
|
|||
|
||||
private def toMarkdown : VersoDocString → String
|
||||
| .mk bs ps => Doc.MarkdownM.run' do
|
||||
for b in bs do
|
||||
Doc.ToMarkdown.toMarkdown b
|
||||
for p in ps do
|
||||
Doc.ToMarkdown.toMarkdown p
|
||||
let blockLines ← bs.mapM Doc.ToMarkdown.toMarkdown
|
||||
let partLines ← ps.mapM Doc.ToMarkdown.toMarkdown
|
||||
return Doc.joinBlocks (blockLines ++ partLines)
|
||||
|
||||
private def manualRw (md : String) : String := md.replace manualRoot "$MANUAL_ROOT/"
|
||||
|
||||
|
|
@ -158,7 +157,7 @@ Because {assert}`assertTests 2 4 = 6`, it must certainly be addition.
|
|||
-/
|
||||
def assertTests (x y : Nat) := x + y
|
||||
|
||||
/-- info: Because `assertTests 2 4 = 6`, it must certainly be addition\. -/
|
||||
/-- info: Because `assertTests 2 4 = 6`, it must certainly be addition. -/
|
||||
#guard_msgs in
|
||||
#verso_to_markdown assertTests
|
||||
|
||||
|
|
@ -169,7 +168,7 @@ The attribute {attr}`simp` registers a simp lemma. Use {attr}`@[simp]`.
|
|||
-/
|
||||
def attrTests (x y : Nat) := x + y
|
||||
|
||||
/-- info: The attribute `simp` registers a simp lemma\. Use `@[simp]`\. -/
|
||||
/-- info: The attribute `simp` registers a simp lemma. Use `@[simp]`. -/
|
||||
#guard_msgs in
|
||||
#verso_to_markdown attrTests
|
||||
|
||||
|
|
@ -208,7 +207,7 @@ def givenInstanceTests (x y : Nat) : Nat := x - y
|
|||
info: ⏎
|
||||
Invisible: ⏎
|
||||
|
||||
There is an `addInst : Add β` and an `inferInstance : Add α`, and `x + y + z`\.
|
||||
There is an `addInst : Add β` and an `inferInstance : Add α`, and `x + y + z`.
|
||||
|
||||
Visible: `Add γ`&`addInst : OfNat γ 5`
|
||||
|
||||
|
|
@ -222,13 +221,13 @@ info: Invisible: ⏎
|
|||
|
||||
Visible:
|
||||
|
||||
* For `n`, `givenTests n n = n - n`\.
|
||||
* For `n`, `givenTests n n = n - n`.
|
||||
|
||||
* For `n`, `k : Nat`, `givenTests n k = n - k`\.
|
||||
* For `n`, `k : Nat`, `givenTests n k = n - k`.
|
||||
|
||||
* For `n`, `k`, `givenTests n k = n - k`\.
|
||||
* For `n`, `k`, `givenTests n k = n - k`.
|
||||
|
||||
* For `k`, `givenTests m k = m - k`\.
|
||||
* For `k`, `givenTests m k = m - k`.
|
||||
-/
|
||||
#guard_msgs in
|
||||
#verso_to_markdown givenTests
|
||||
|
|
|
|||
682
tests/elab/versoDocMarkdownRendering.lean
Normal file
682
tests/elab/versoDocMarkdownRendering.lean
Normal file
|
|
@ -0,0 +1,682 @@
|
|||
import Lean.Elab.Command
|
||||
import Lean.DocString.Markdown
|
||||
|
||||
/-!
|
||||
This test ensures that the rendering of all Verso docstrings to Markdown is sensible.
|
||||
|
||||
This rendering is used when showing Verso docstrings over LSP.
|
||||
-/
|
||||
|
||||
open Lean Elab Command Term Doc
|
||||
|
||||
private def render (b : Block Empty Empty) : String :=
|
||||
MarkdownM.run' (ToMarkdown.toMarkdown b)
|
||||
|
||||
/--
|
||||
Renders `b` to Markdown and prints it preceded by a blank line, so the rendered output reads cleanly
|
||||
inside `#guard_msgs` expected output. The leading blank line keeps the first rendered line visually
|
||||
separated from the `info:` marker.
|
||||
-/
|
||||
private def showMd (b : Block Empty Empty) : IO Unit := do
|
||||
IO.println ""
|
||||
IO.println (render b)
|
||||
|
||||
/-! Blockquote with one paragraph -/
|
||||
|
||||
/--
|
||||
info:
|
||||
> A
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.blockquote #[.para #[.text "A"]])
|
||||
|
||||
/-! Blockquote with two paragraphs -/
|
||||
|
||||
/--
|
||||
info:
|
||||
> A
|
||||
>
|
||||
> B
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.blockquote #[.para #[.text "A"], .para #[.text "B"]])
|
||||
|
||||
/-! Nested blockquote -/
|
||||
|
||||
/--
|
||||
info:
|
||||
> > X
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.blockquote #[.blockquote #[.para #[.text "X"]]])
|
||||
|
||||
/-! Blockquote with inline linebreak -/
|
||||
|
||||
/--
|
||||
info:
|
||||
> A
|
||||
> B
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.blockquote #[.para #[.text "A", .linebreak "\n", .text "B"]])
|
||||
|
||||
/-! Blockquote inside an unordered list item -/
|
||||
|
||||
/--
|
||||
info:
|
||||
* > X
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.ul #[⟨#[.blockquote #[.para #[.text "X"]]]⟩])
|
||||
|
||||
/--
|
||||
info:
|
||||
* > X
|
||||
> Y
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.ul #[⟨#[.blockquote #[.para #[.text "X", .linebreak "\n", .text "Y"]]]⟩])
|
||||
|
||||
/--
|
||||
info:
|
||||
* > X
|
||||
|
||||
> Y
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.ul #[⟨#[.blockquote #[.para #[.text "X"]], .blockquote #[.para #[.text "Y"]]]⟩])
|
||||
|
||||
|
||||
/-! Blockquote of two paragraphs inside an unordered list item -/
|
||||
|
||||
/--
|
||||
info:
|
||||
* > A
|
||||
>
|
||||
> B
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.ul #[⟨#[.blockquote #[.para #[.text "A"], .para #[.text "B"]]]⟩])
|
||||
|
||||
/-! Blockquote of a fenced code block -/
|
||||
|
||||
/--
|
||||
info:
|
||||
> ```
|
||||
> x = 1
|
||||
> ```
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.blockquote #[.code "x = 1"])
|
||||
|
||||
/-! Blockquote → ordered list → blockquotes (multi-line; one item has two paragraphs) -/
|
||||
|
||||
/--
|
||||
info:
|
||||
> 1. > First line of item 1
|
||||
> >
|
||||
> > Second paragraph of item 1
|
||||
>
|
||||
> 2. > Item 2 line 1
|
||||
> > Item 2 line 2
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd <|
|
||||
.blockquote #[
|
||||
.ol 1 #[
|
||||
⟨#[.blockquote #[
|
||||
.para #[.text "First line of item 1"],
|
||||
.para #[.text "Second paragraph of item 1"]]]⟩,
|
||||
⟨#[.blockquote #[
|
||||
.para #[.text "Item 2 line 1", .linebreak "\n", .text "Item 2 line 2"]]]⟩
|
||||
]
|
||||
]
|
||||
|
||||
/-! List with two paragraphs -/
|
||||
|
||||
/--
|
||||
info:
|
||||
* X
|
||||
|
||||
Y
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.ul #[⟨#[.para #[.text "X"], .para #[.text "Y"]]⟩])
|
||||
|
||||
/-! An ordered list with ≥ 10 items keeps continuation prefix width consistent -/
|
||||
|
||||
/--
|
||||
info:
|
||||
10. X
|
||||
|
||||
Y
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.ol 10 #[⟨#[.para #[.text "X"], .para #[.text "Y"]]⟩])
|
||||
|
||||
/--
|
||||
info:
|
||||
1. X
|
||||
|
||||
Y
|
||||
|
||||
2. X
|
||||
|
||||
Y
|
||||
|
||||
3. X
|
||||
|
||||
Y
|
||||
|
||||
4. X
|
||||
|
||||
Y
|
||||
|
||||
5. X
|
||||
|
||||
Y
|
||||
|
||||
6. X
|
||||
|
||||
Y
|
||||
|
||||
7. X
|
||||
|
||||
Y
|
||||
|
||||
8. X
|
||||
|
||||
Y
|
||||
|
||||
9. X
|
||||
|
||||
Y
|
||||
|
||||
10. X
|
||||
|
||||
Y
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.ol 1 <| List.replicate 10 ⟨#[.para #[.text "X"], .para #[.text "Y"]]⟩ |>.toArray)
|
||||
|
||||
/-! Nested definition list (a `.dl` inside another `.dl`'s description) -/
|
||||
|
||||
/--
|
||||
info:
|
||||
* **outer**:
|
||||
* **inner**:
|
||||
value
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd <|
|
||||
.dl #[⟨#[.text "outer"], #[
|
||||
.dl #[⟨#[.text "inner"], #[.para #[.text "value"]]⟩]
|
||||
]⟩]
|
||||
|
||||
/-! Definition list inside a blockquote -/
|
||||
|
||||
/--
|
||||
info:
|
||||
> * **k**:
|
||||
> v
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd <|
|
||||
.blockquote #[
|
||||
.dl #[⟨#[.text "k"], #[.para #[.text "v"]]⟩]
|
||||
]
|
||||
|
||||
/-!
|
||||
Definition list with a multi-paragraph descripti
|
||||
|
||||
The term is separated from the description by a blank line so a CommonMark
|
||||
parser keeps `**k**:` as its own paragraph instead of fusing it with the first
|
||||
description paragraph (the latter would produce `<p>**k**: v1</p>`).
|
||||
-/
|
||||
|
||||
/--
|
||||
info:
|
||||
* **k**:
|
||||
|
||||
v1
|
||||
|
||||
v2
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd <|
|
||||
.dl #[⟨#[.text "k"], #[
|
||||
.para #[.text "v1"],
|
||||
.para #[.text "v2"]
|
||||
]⟩]
|
||||
|
||||
/-! Definition list with a description that mixes a paragraph and a code block -/
|
||||
|
||||
/--
|
||||
info:
|
||||
* **fn**:
|
||||
|
||||
Computes the answer.
|
||||
|
||||
```
|
||||
def f x := x
|
||||
```
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd <|
|
||||
.dl #[⟨#[.text "fn"], #[
|
||||
.para #[.text "Computes the answer."],
|
||||
.code "def f x := x"
|
||||
]⟩]
|
||||
|
||||
/-!
|
||||
Definition list whose single-block description is itself a code blo
|
||||
|
||||
A code fence interrupts a paragraph cleanly in CommonMark, so the renderer
|
||||
keeps the compact (term-glued) form for single-block descriptions even when
|
||||
the block isn't a paragraph.
|
||||
-/
|
||||
|
||||
/--
|
||||
info:
|
||||
* **fn**:
|
||||
```
|
||||
def f x := x
|
||||
```
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd <|
|
||||
.dl #[⟨#[.text "fn"], #[.code "def f x := x"]⟩]
|
||||
|
||||
/-!
|
||||
## Inline constructor covera
|
||||
|
||||
Each `Inline` constructor (`text`, `emph`, `bold`, `code`, `math` (`inline`,
|
||||
`display`), `linebreak`, `link`, `footnote`, `image`, `concat`, `other`) is
|
||||
exercised below. The `other` case requires a non-`Empty` extension type and is
|
||||
left to `versoDocMarkdown.lean`.
|
||||
-/
|
||||
|
||||
/-! `.text`: plain text is rendered verbatim, with Markdown specials escaped -/
|
||||
|
||||
/--
|
||||
info:
|
||||
hello \(world\)
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.text "hello (world)"])
|
||||
|
||||
/-! `.text`: end-of-sentence periods are *not* escaped (only digit-`.` is, to avoid forming an ol marker) -/
|
||||
|
||||
/--
|
||||
info:
|
||||
A sentence. Another sentence.
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.text "A sentence. Another sentence."])
|
||||
|
||||
/--
|
||||
info:
|
||||
Step 1. do the thing.
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.text "Step 1. do the thing."])
|
||||
|
||||
/-! `.text`: `-`/`+` are only escaped at the start of a line followed by a space -/
|
||||
|
||||
/--
|
||||
info:
|
||||
\- bullet
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.text "- bullet"])
|
||||
|
||||
/--
|
||||
info:
|
||||
co-author and 1+1
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.text "co-author and 1+1"])
|
||||
|
||||
/-! `.text`: `!` is only escaped when followed by `[` (to avoid forming image syntax) -/
|
||||
|
||||
/--
|
||||
info:
|
||||
hello! world \!\[not an image\]
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.text "hello! world ![not an image]"])
|
||||
|
||||
/-! `.emph`: emphasis is wrapped in `*…*` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
*italic*
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.emph #[.text "italic"]])
|
||||
|
||||
/-! `.bold`: strong emphasis is wrapped in `**…**` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
**bold**
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.bold #[.text "bold"]])
|
||||
|
||||
/-!
|
||||
`.code`: inline code is wrapped in backticks; adjacent code spans get a U+200B separat
|
||||
|
||||
The two adjacent code spans here would otherwise butt against each other; the
|
||||
zero-width space (U+200B) keeps them syntactically distinct.
|
||||
-/
|
||||
|
||||
/--
|
||||
info:
|
||||
`x``y`
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.code "x", .code "y"])
|
||||
|
||||
/-! `.math .inline`: inline math is wrapped in `$…$` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
$x^2$
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.math .inline "x^2"])
|
||||
|
||||
/-! `.math .display`: display math is wrapped in `$$…$$` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
$$x^2$$
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.math .display "x^2"])
|
||||
|
||||
/-! `.linebreak`: a hard line break inside a paragraph -/
|
||||
|
||||
/--
|
||||
info:
|
||||
left
|
||||
right
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.text "left", .linebreak "\n", .text "right"])
|
||||
|
||||
/-! `.link`: a link rendered as `[text](url)` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
[click](https://example.com)
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.link #[.text "click"] "https://example.com"])
|
||||
|
||||
/-! `.footnote`: a `[^name]` reference plus a `[^name]:body` flushed at the end -/
|
||||
|
||||
/--
|
||||
info:
|
||||
see [^fn1]
|
||||
|
||||
[^fn1]:the explanation
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.text "see ", .footnote "fn1" #[.text "the explanation"]])
|
||||
|
||||
/-! `.image`: an inline image rendered as `` -/
|
||||
|
||||
/--
|
||||
info:
|
||||

|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.image "cat" "cat.png"])
|
||||
|
||||
/-! `.concat`: an inline `concat` aggregates adjacent inlines on one line -/
|
||||
|
||||
/--
|
||||
info:
|
||||
ab
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.concat #[.text "a", .text "b"]])
|
||||
|
||||
/-!
|
||||
## Inline nestin
|
||||
|
||||
Combinations of `.emph`, `.bold`, `.link`, and `.image`. The renderer must avoid
|
||||
double-emitting `*` / `**` markers when they're already open, and the `.link`
|
||||
case must avoid emitting nested `[…](…)` when already inside a link.
|
||||
-/
|
||||
|
||||
/-! Emph inside bold renders as `***…***` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
***x***
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.bold #[.emph #[.text "x"]]])
|
||||
|
||||
/-! Bold inside emph renders as `***…***` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
***x***
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.emph #[.bold #[.text "x"]]])
|
||||
|
||||
/-! Emph inside emph: the inner `*` markers are suppressed -/
|
||||
|
||||
/--
|
||||
info:
|
||||
*x*
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.emph #[.emph #[.text "x"]]])
|
||||
|
||||
/-! Bold inside bold: the inner `**` markers are suppressed -/
|
||||
|
||||
/--
|
||||
info:
|
||||
**x**
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.bold #[.bold #[.text "x"]]])
|
||||
|
||||
/-!
|
||||
Emph with internal whitespace: leading/trailing spaces are hoisted outside the `*` marke
|
||||
|
||||
CommonMark refuses to parse `* x *` as emphasis (the markers can't be padded).
|
||||
The renderer peels the whitespace out so the emphasized middle has tight markers.
|
||||
-/
|
||||
|
||||
/--
|
||||
info:
|
||||
a *x* b
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.text "a", .emph #[.text " x "], .text "b"])
|
||||
|
||||
/-! Bold containing inline code: `**…`x`…**` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
**a `x` b**
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.bold #[.text "a ", .code "x", .text " b"]])
|
||||
|
||||
/-! Emph inside a link: `[*x*](url)` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
[*x*](https://example.com)
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.link #[.emph #[.text "x"]] "https://example.com"])
|
||||
|
||||
/-! Bold inside a link: `[**x**](url)` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
[**x**](https://example.com)
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.link #[.bold #[.text "x"]] "https://example.com"])
|
||||
|
||||
/-! Inline code inside a link -/
|
||||
|
||||
/--
|
||||
info:
|
||||
[`x`](https://example.com)
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.link #[.code "x"] "https://example.com"])
|
||||
|
||||
/-! Link inside emph: `*[x](url)*` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
*[x](https://example.com)*
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.emph #[.link #[.text "x"] "https://example.com"]])
|
||||
|
||||
/-! Link inside bold: `**[x](url)**` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
**[x](https://example.com)**
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.bold #[.link #[.text "x"] "https://example.com"]])
|
||||
|
||||
/-!
|
||||
Nested links: the inner link's URL is dropped and its text rendered inli
|
||||
|
||||
CommonMark does not allow nested `[…](…)`, so the renderer flattens the inner
|
||||
link to its display text only.
|
||||
-/
|
||||
|
||||
/--
|
||||
info:
|
||||
[outer x inner](https://outer.example.com)
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd <|
|
||||
.para #[.link #[
|
||||
.text "outer ",
|
||||
.link #[.text "x"] "https://inner.example.com",
|
||||
.text " inner"
|
||||
] "https://outer.example.com"]
|
||||
|
||||
/-! Linked image: `[](url)` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
[](https://example.com)
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.link #[.image "cat" "cat.png"] "https://example.com"])
|
||||
|
||||
/-! Image alt text is escaped: Markdown specials in `alt` get backslash-escaped -/
|
||||
|
||||
/--
|
||||
info:
|
||||

|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.image "a (b) *c*" "cat.png"])
|
||||
|
||||
/-! Image inside bold: `****` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
****
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.bold #[.image "cat" "cat.png"]])
|
||||
|
||||
/-! Image inside emph: `**` -/
|
||||
|
||||
/--
|
||||
info:
|
||||
**
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.para #[.emph #[.image "cat" "cat.png"]])
|
||||
|
||||
/-!
|
||||
## Block constructor covera
|
||||
|
||||
`Block` constructors covered above: `para`, `code`, `ul`, `ol`, `dl`, `blockquote`.
|
||||
The `concat` case is exercised below. `other` requires a non-`Empty` extension and
|
||||
is left to `versoDocMarkdown.lean`.
|
||||
-/
|
||||
|
||||
/-! `.concat`: a block `concat` joins adjacent blocks with a blank line between -/
|
||||
|
||||
/--
|
||||
info:
|
||||
A
|
||||
|
||||
B
|
||||
-/
|
||||
#guard_msgs in
|
||||
#eval show CommandElabM Unit from do
|
||||
showMd (.concat #[.para #[.text "A"], .para #[.text "B"]])
|
||||
Loading…
Add table
Reference in a new issue