trst.nim 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. discard """
  2. output: '''
  3. [Suite] RST parsing
  4. [Suite] RST indentation
  5. [Suite] Warnings
  6. [Suite] RST include directive
  7. [Suite] RST escaping
  8. [Suite] RST inline markup
  9. '''
  10. """
  11. # tests for rst module
  12. import ../../lib/packages/docutils/rstgen
  13. import ../../lib/packages/docutils/rst
  14. import ../../lib/packages/docutils/rstast
  15. import unittest, strutils
  16. import std/private/miscdollars
  17. import os
  18. proc toAst(input: string,
  19. rstOptions: RstParseOptions = {roPreferMarkdown, roSupportMarkdown, roNimFile},
  20. error: ref string = nil,
  21. warnings: ref seq[string] = nil): string =
  22. ## If `error` is nil then no errors should be generated.
  23. ## The same goes for `warnings`.
  24. proc testMsgHandler(filename: string, line, col: int, msgkind: MsgKind,
  25. arg: string) =
  26. let mc = msgkind.whichMsgClass
  27. let a = $msgkind % arg
  28. var message: string
  29. toLocation(message, filename, line, col + ColRstOffset)
  30. message.add " $1: $2" % [$mc, a]
  31. if mc == mcError:
  32. if error == nil:
  33. raise newException(EParseError, "[unexpected error] " & message)
  34. error[] = message
  35. # we check only first error because subsequent ones may be meaningless
  36. raise newException(EParseError, "")
  37. else:
  38. doAssert warnings != nil, "unexpected RST warning '" & message & "'"
  39. warnings[].add message
  40. try:
  41. const filen = "input"
  42. proc myFindFile(filename: string): string =
  43. # we don't find any files in online mode:
  44. result = ""
  45. var (rst, _, _) = rstParse(input, filen, line=LineRstInit, column=ColRstInit,
  46. rstOptions, myFindFile, testMsgHandler)
  47. result = renderRstToStr(rst)
  48. except EParseError as e:
  49. if e.msg != "":
  50. result = e.msg
  51. suite "RST parsing":
  52. test "option list has priority over definition list":
  53. check(dedent"""
  54. --defusages
  55. file
  56. -o set
  57. """.toAst ==
  58. dedent"""
  59. rnOptionList
  60. rnOptionListItem order=1
  61. rnOptionGroup
  62. rnLeaf '--'
  63. rnLeaf 'defusages'
  64. rnDescription
  65. rnInner
  66. rnLeaf 'file'
  67. rnOptionListItem order=2
  68. rnOptionGroup
  69. rnLeaf '-'
  70. rnLeaf 'o'
  71. rnDescription
  72. rnLeaf 'set'
  73. """)
  74. test "items of 1 option list can be separated by blank lines":
  75. check(dedent"""
  76. -a desc1
  77. -b desc2
  78. """.toAst ==
  79. dedent"""
  80. rnOptionList
  81. rnOptionListItem order=1
  82. rnOptionGroup
  83. rnLeaf '-'
  84. rnLeaf 'a'
  85. rnDescription
  86. rnLeaf 'desc1'
  87. rnOptionListItem order=2
  88. rnOptionGroup
  89. rnLeaf '-'
  90. rnLeaf 'b'
  91. rnDescription
  92. rnLeaf 'desc2'
  93. """)
  94. test "option list has priority over definition list":
  95. check(dedent"""
  96. defName
  97. defBody
  98. -b desc2
  99. """.toAst ==
  100. dedent"""
  101. rnInner
  102. rnDefList
  103. rnDefItem
  104. rnDefName
  105. rnLeaf 'defName'
  106. rnDefBody
  107. rnInner
  108. rnLeaf 'defBody'
  109. rnOptionList
  110. rnOptionListItem order=1
  111. rnOptionGroup
  112. rnLeaf '-'
  113. rnLeaf 'b'
  114. rnDescription
  115. rnLeaf 'desc2'
  116. """)
  117. test "RST comment":
  118. check(dedent"""
  119. .. comment1
  120. comment2
  121. someParagraph""".toAst ==
  122. dedent"""
  123. rnLeaf 'someParagraph'
  124. """)
  125. check(dedent"""
  126. ..
  127. comment1
  128. comment2
  129. someParagraph""".toAst ==
  130. dedent"""
  131. rnLeaf 'someParagraph'
  132. """)
  133. test "check that additional line right after .. ends comment":
  134. check(dedent"""
  135. ..
  136. notAcomment1
  137. notAcomment2
  138. someParagraph""".toAst ==
  139. dedent"""
  140. rnInner
  141. rnBlockQuote
  142. rnInner
  143. rnLeaf 'notAcomment1'
  144. rnLeaf ' '
  145. rnLeaf 'notAcomment2'
  146. rnParagraph
  147. rnLeaf 'someParagraph'
  148. """)
  149. test "but blank lines after 2nd non-empty line don't end the comment":
  150. check(dedent"""
  151. ..
  152. comment1
  153. comment2
  154. someParagraph""".toAst ==
  155. dedent"""
  156. rnLeaf 'someParagraph'
  157. """)
  158. test "using .. as separator b/w directives and block quotes":
  159. check(dedent"""
  160. .. note:: someNote
  161. ..
  162. someBlockQuote""".toAst ==
  163. dedent"""
  164. rnInner
  165. rnAdmonition adType=note
  166. [nil]
  167. [nil]
  168. rnLeaf 'someNote'
  169. rnBlockQuote
  170. rnInner
  171. rnLeaf 'someBlockQuote'
  172. """)
  173. test "no redundant blank lines in literal blocks":
  174. check(dedent"""
  175. Check::
  176. code
  177. """.toAst ==
  178. dedent"""
  179. rnInner
  180. rnLeaf 'Check'
  181. rnLeaf ':'
  182. rnLiteralBlock
  183. rnLeaf 'code'
  184. """)
  185. suite "RST indentation":
  186. test "nested bullet lists":
  187. let input = dedent """
  188. * - bullet1
  189. - bullet2
  190. * - bullet3
  191. - bullet4
  192. """
  193. let output = input.toAst
  194. check(output == dedent"""
  195. rnBulletList
  196. rnBulletItem
  197. rnBulletList
  198. rnBulletItem
  199. rnInner
  200. rnLeaf 'bullet1'
  201. rnBulletItem
  202. rnInner
  203. rnLeaf 'bullet2'
  204. rnBulletItem
  205. rnBulletList
  206. rnBulletItem
  207. rnInner
  208. rnLeaf 'bullet3'
  209. rnBulletItem
  210. rnInner
  211. rnLeaf 'bullet4'
  212. """)
  213. test "nested markup blocks":
  214. let input = dedent"""
  215. #) .. Hint:: .. Error:: none
  216. #) .. Warning:: term0
  217. Definition0
  218. #) some
  219. paragraph1
  220. #) term1
  221. Definition1
  222. term2
  223. Definition2
  224. """
  225. check(input.toAst == dedent"""
  226. rnEnumList labelFmt=1)
  227. rnEnumItem
  228. rnAdmonition adType=hint
  229. [nil]
  230. [nil]
  231. rnAdmonition adType=error
  232. [nil]
  233. [nil]
  234. rnLeaf 'none'
  235. rnEnumItem
  236. rnAdmonition adType=warning
  237. [nil]
  238. [nil]
  239. rnDefList
  240. rnDefItem
  241. rnDefName
  242. rnLeaf 'term0'
  243. rnDefBody
  244. rnInner
  245. rnLeaf 'Definition0'
  246. rnEnumItem
  247. rnInner
  248. rnLeaf 'some'
  249. rnLeaf ' '
  250. rnLeaf 'paragraph1'
  251. rnEnumItem
  252. rnDefList
  253. rnDefItem
  254. rnDefName
  255. rnLeaf 'term1'
  256. rnDefBody
  257. rnInner
  258. rnLeaf 'Definition1'
  259. rnDefItem
  260. rnDefName
  261. rnLeaf 'term2'
  262. rnDefBody
  263. rnInner
  264. rnLeaf 'Definition2'
  265. """)
  266. test "code-block parsing":
  267. let input1 = dedent"""
  268. .. code-block:: nim
  269. :test: "nim c $1"
  270. template additive(typ: typedesc) =
  271. discard
  272. """
  273. let input2 = dedent"""
  274. .. code-block:: nim
  275. :test: "nim c $1"
  276. template additive(typ: typedesc) =
  277. discard
  278. """
  279. let input3 = dedent"""
  280. .. code-block:: nim
  281. :test: "nim c $1"
  282. template additive(typ: typedesc) =
  283. discard
  284. """
  285. let inputWrong = dedent"""
  286. .. code-block:: nim
  287. :test: "nim c $1"
  288. template additive(typ: typedesc) =
  289. discard
  290. """
  291. let ast = dedent"""
  292. rnCodeBlock
  293. rnDirArg
  294. rnLeaf 'nim'
  295. rnFieldList
  296. rnField
  297. rnFieldName
  298. rnLeaf 'test'
  299. rnFieldBody
  300. rnInner
  301. rnLeaf '"'
  302. rnLeaf 'nim'
  303. rnLeaf ' '
  304. rnLeaf 'c'
  305. rnLeaf ' '
  306. rnLeaf '$'
  307. rnLeaf '1'
  308. rnLeaf '"'
  309. rnField
  310. rnFieldName
  311. rnLeaf 'default-language'
  312. rnFieldBody
  313. rnLeaf 'Nim'
  314. rnLiteralBlock
  315. rnLeaf 'template additive(typ: typedesc) =
  316. discard'
  317. """
  318. check input1.toAst == ast
  319. check input2.toAst == ast
  320. check input3.toAst == ast
  321. # "template..." should be parsed as a definition list attached to ":test:":
  322. check inputWrong.toAst != ast
  323. suite "Warnings":
  324. test "warnings for broken footnotes/links/substitutions":
  325. let input = dedent"""
  326. firstParagraph
  327. footnoteRef [som]_
  328. link `a broken Link`_
  329. substitution |undefined subst|
  330. link short.link_
  331. lastParagraph
  332. """
  333. var warnings = new seq[string]
  334. let output = input.toAst(warnings=warnings)
  335. check(warnings[] == @[
  336. "input(3, 14) Warning: broken link 'citation-som'",
  337. "input(5, 7) Warning: broken link 'a-broken-link'",
  338. "input(7, 15) Warning: unknown substitution 'undefined subst'",
  339. "input(9, 6) Warning: broken link 'shortdotlink'"
  340. ])
  341. test "With include directive and blank lines at the beginning":
  342. "other.rst".writeFile(dedent"""
  343. firstParagraph
  344. here brokenLink_""")
  345. let input = ".. include:: other.rst"
  346. var warnings = new seq[string]
  347. let output = input.toAst(warnings=warnings)
  348. check warnings[] == @["other.rst(5, 6) Warning: broken link 'brokenlink'"]
  349. check(output == dedent"""
  350. rnInner
  351. rnParagraph
  352. rnLeaf 'firstParagraph'
  353. rnParagraph
  354. rnLeaf 'here'
  355. rnLeaf ' '
  356. rnRef
  357. rnLeaf 'brokenLink'
  358. """)
  359. removeFile("other.rst")
  360. suite "RST include directive":
  361. test "Include whole":
  362. "other.rst".writeFile("**test1**")
  363. let input = ".. include:: other.rst"
  364. doAssert "<strong>test1</strong>" == rstTohtml(input, {}, defaultConfig())
  365. removeFile("other.rst")
  366. test "Include starting from":
  367. "other.rst".writeFile("""
  368. And this should **NOT** be visible in `docs.html`
  369. OtherStart
  370. *Visible*
  371. """)
  372. let input = """
  373. .. include:: other.rst
  374. :start-after: OtherStart
  375. """
  376. check "<em>Visible</em>" == rstTohtml(input, {}, defaultConfig())
  377. removeFile("other.rst")
  378. test "Include everything before":
  379. "other.rst".writeFile("""
  380. *Visible*
  381. OtherEnd
  382. And this should **NOT** be visible in `docs.html`
  383. """)
  384. let input = """
  385. .. include:: other.rst
  386. :end-before: OtherEnd
  387. """
  388. doAssert "<em>Visible</em>" == rstTohtml(input, {}, defaultConfig())
  389. removeFile("other.rst")
  390. test "Include everything between":
  391. "other.rst".writeFile("""
  392. And this should **NOT** be visible in `docs.html`
  393. OtherStart
  394. *Visible*
  395. OtherEnd
  396. And this should **NOT** be visible in `docs.html`
  397. """)
  398. let input = """
  399. .. include:: other.rst
  400. :start-after: OtherStart
  401. :end-before: OtherEnd
  402. """
  403. check "<em>Visible</em>" == rstTohtml(input, {}, defaultConfig())
  404. removeFile("other.rst")
  405. test "Ignore premature ending string":
  406. "other.rst".writeFile("""
  407. OtherEnd
  408. And this should **NOT** be visible in `docs.html`
  409. OtherStart
  410. *Visible*
  411. OtherEnd
  412. And this should **NOT** be visible in `docs.html`
  413. """)
  414. let input = """
  415. .. include:: other.rst
  416. :start-after: OtherStart
  417. :end-before: OtherEnd
  418. """
  419. doAssert "<em>Visible</em>" == rstTohtml(input, {}, defaultConfig())
  420. removeFile("other.rst")
  421. suite "RST escaping":
  422. test "backspaces":
  423. check("""\ this""".toAst == dedent"""
  424. rnLeaf 'this'
  425. """)
  426. check("""\\ this""".toAst == dedent"""
  427. rnInner
  428. rnLeaf '\'
  429. rnLeaf ' '
  430. rnLeaf 'this'
  431. """)
  432. check("""\\\ this""".toAst == dedent"""
  433. rnInner
  434. rnLeaf '\'
  435. rnLeaf 'this'
  436. """)
  437. check("""\\\\ this""".toAst == dedent"""
  438. rnInner
  439. rnLeaf '\'
  440. rnLeaf '\'
  441. rnLeaf ' '
  442. rnLeaf 'this'
  443. """)
  444. suite "RST inline markup":
  445. test "* and ** surrounded by spaces are not inline markup":
  446. check("a * b * c ** d ** e".toAst == dedent"""
  447. rnInner
  448. rnLeaf 'a'
  449. rnLeaf ' '
  450. rnLeaf '*'
  451. rnLeaf ' '
  452. rnLeaf 'b'
  453. rnLeaf ' '
  454. rnLeaf '*'
  455. rnLeaf ' '
  456. rnLeaf 'c'
  457. rnLeaf ' '
  458. rnLeaf '**'
  459. rnLeaf ' '
  460. rnLeaf 'd'
  461. rnLeaf ' '
  462. rnLeaf '**'
  463. rnLeaf ' '
  464. rnLeaf 'e'
  465. """)
  466. test "end-string has repeating symbols":
  467. check("*emphasis content****".toAst == dedent"""
  468. rnEmphasis
  469. rnLeaf 'emphasis'
  470. rnLeaf ' '
  471. rnLeaf 'content'
  472. rnLeaf '***'
  473. """)
  474. check("""*emphasis content\****""".toAst == dedent"""
  475. rnEmphasis
  476. rnLeaf 'emphasis'
  477. rnLeaf ' '
  478. rnLeaf 'content'
  479. rnLeaf '*'
  480. rnLeaf '**'
  481. """) # exact configuration of leafs with * is not really essential,
  482. # only total number of * is essential
  483. check("**strong content****".toAst == dedent"""
  484. rnStrongEmphasis
  485. rnLeaf 'strong'
  486. rnLeaf ' '
  487. rnLeaf 'content'
  488. rnLeaf '**'
  489. """)
  490. check("""**strong content*\****""".toAst == dedent"""
  491. rnStrongEmphasis
  492. rnLeaf 'strong'
  493. rnLeaf ' '
  494. rnLeaf 'content'
  495. rnLeaf '*'
  496. rnLeaf '*'
  497. rnLeaf '*'
  498. """)
  499. check("``lit content`````".toAst == dedent"""
  500. rnInlineLiteral
  501. rnLeaf 'lit'
  502. rnLeaf ' '
  503. rnLeaf 'content'
  504. rnLeaf '```'
  505. """)
  506. test "interpreted text parsing: code fragments":
  507. check(dedent"""
  508. .. default-role:: option
  509. `--gc:refc`""".toAst ==
  510. dedent"""
  511. rnInner
  512. rnDefaultRole
  513. rnDirArg
  514. rnLeaf 'option'
  515. [nil]
  516. [nil]
  517. rnParagraph
  518. rnCodeFragment
  519. rnInner
  520. rnLeaf '--'
  521. rnLeaf 'gc'
  522. rnLeaf ':'
  523. rnLeaf 'refc'
  524. rnLeaf 'option'
  525. """)
  526. test """interpreted text can be ended with \` """:
  527. let output = (".. default-role:: literal\n" & """`\``""").toAst
  528. check(output.endsWith """
  529. rnParagraph
  530. rnInlineLiteral
  531. rnLeaf '`'""" & "\n")
  532. let output2 = """`\``""".toAst
  533. check(output2 == dedent"""
  534. rnInlineCode
  535. rnDirArg
  536. rnLeaf 'nim'
  537. [nil]
  538. rnLiteralBlock
  539. rnLeaf '`'
  540. """)
  541. let output3 = """`proc \`+\``""".toAst
  542. check(output3 == dedent"""
  543. rnInlineCode
  544. rnDirArg
  545. rnLeaf 'nim'
  546. [nil]
  547. rnLiteralBlock
  548. rnLeaf 'proc `+`'
  549. """)
  550. check("""`\\`""".toAst ==
  551. dedent"""
  552. rnInlineCode
  553. rnDirArg
  554. rnLeaf 'nim'
  555. [nil]
  556. rnLiteralBlock
  557. rnLeaf '\\'
  558. """)
  559. test "Markdown-style code/backtick":
  560. # no whitespace is required before `
  561. check("`try`...`except`".toAst ==
  562. dedent"""
  563. rnInner
  564. rnInlineCode
  565. rnDirArg
  566. rnLeaf 'nim'
  567. [nil]
  568. rnLiteralBlock
  569. rnLeaf 'try'
  570. rnLeaf '...'
  571. rnInlineCode
  572. rnDirArg
  573. rnLeaf 'nim'
  574. [nil]
  575. rnLiteralBlock
  576. rnLeaf 'except'
  577. """)
  578. test """inline literals can contain \ anywhere""":
  579. check("""``\``""".toAst == dedent"""
  580. rnInlineLiteral
  581. rnLeaf '\'
  582. """)
  583. check("""``\\``""".toAst == dedent"""
  584. rnInlineLiteral
  585. rnLeaf '\'
  586. rnLeaf '\'
  587. """)
  588. check("""``\```""".toAst == dedent"""
  589. rnInlineLiteral
  590. rnLeaf '\'
  591. rnLeaf '`'
  592. """)
  593. check("""``\\```""".toAst == dedent"""
  594. rnInlineLiteral
  595. rnLeaf '\'
  596. rnLeaf '\'
  597. rnLeaf '`'
  598. """)
  599. check("""``\````""".toAst == dedent"""
  600. rnInlineLiteral
  601. rnLeaf '\'
  602. rnLeaf '`'
  603. rnLeaf '`'
  604. """)
  605. test "references with _ at the end":
  606. check(dedent"""
  607. .. _lnk: https
  608. lnk_""".toAst ==
  609. dedent"""
  610. rnHyperlink
  611. rnInner
  612. rnLeaf 'lnk'
  613. rnInner
  614. rnLeaf 'https'
  615. """)
  616. test "not a hyper link":
  617. check(dedent"""
  618. .. _lnk: https
  619. lnk___""".toAst ==
  620. dedent"""
  621. rnInner
  622. rnLeaf 'lnk'
  623. rnLeaf '___'
  624. """)
  625. test "no punctuation in the end of a standalone URI is allowed":
  626. check(dedent"""
  627. [see (http://no.org)], end""".toAst ==
  628. dedent"""
  629. rnInner
  630. rnLeaf '['
  631. rnLeaf 'see'
  632. rnLeaf ' '
  633. rnLeaf '('
  634. rnStandaloneHyperlink
  635. rnLeaf 'http://no.org'
  636. rnLeaf ')'
  637. rnLeaf ']'
  638. rnLeaf ','
  639. rnLeaf ' '
  640. rnLeaf 'end'
  641. """)
  642. # but `/` at the end is OK
  643. check(
  644. dedent"""
  645. See http://no.org/ end""".toAst ==
  646. dedent"""
  647. rnInner
  648. rnLeaf 'See'
  649. rnLeaf ' '
  650. rnStandaloneHyperlink
  651. rnLeaf 'http://no.org/'
  652. rnLeaf ' '
  653. rnLeaf 'end'
  654. """)
  655. # a more complex URL with some made-up ending '&='.
  656. # Github Markdown would include final &= and
  657. # so would rst2html.py in contradiction with RST spec.
  658. check(
  659. dedent"""
  660. See https://www.google.com/url?sa=t&source=web&cd=&cad=rja&url=https%3A%2F%2Fnim-lang.github.io%2FNim%2Frst.html%23features&usg=AO&= end""".toAst ==
  661. dedent"""
  662. rnInner
  663. rnLeaf 'See'
  664. rnLeaf ' '
  665. rnStandaloneHyperlink
  666. rnLeaf 'https://www.google.com/url?sa=t&source=web&cd=&cad=rja&url=https%3A%2F%2Fnim-lang.github.io%2FNim%2Frst.html%23features&usg=AO'
  667. rnLeaf '&'
  668. rnLeaf '='
  669. rnLeaf ' '
  670. rnLeaf 'end'
  671. """)
  672. test "URL with balanced parentheses (Markdown rule)":
  673. # 2 balanced parens, 1 unbalanced:
  674. check(dedent"""
  675. https://en.wikipedia.org/wiki/APL_((programming_language)))""".toAst ==
  676. dedent"""
  677. rnInner
  678. rnStandaloneHyperlink
  679. rnLeaf 'https://en.wikipedia.org/wiki/APL_((programming_language))'
  680. rnLeaf ')'
  681. """)
  682. # the same for Markdown-style link:
  683. check(dedent"""
  684. [foo [bar]](https://en.wikipedia.org/wiki/APL_((programming_language))))""".toAst ==
  685. dedent"""
  686. rnInner
  687. rnHyperlink
  688. rnLeaf 'foo [bar]'
  689. rnLeaf 'https://en.wikipedia.org/wiki/APL_((programming_language))'
  690. rnLeaf ')'
  691. """)
  692. # unbalanced (here behavior is more RST-like actually):
  693. check(dedent"""
  694. https://en.wikipedia.org/wiki/APL_(programming_language(""".toAst ==
  695. dedent"""
  696. rnInner
  697. rnStandaloneHyperlink
  698. rnLeaf 'https://en.wikipedia.org/wiki/APL_(programming_language'
  699. rnLeaf '('
  700. """)
  701. # unbalanced [, but still acceptable:
  702. check(dedent"""
  703. [my {link example](http://example.com/bracket_(symbol_[))""".toAst ==
  704. dedent"""
  705. rnHyperlink
  706. rnLeaf 'my {link example'
  707. rnLeaf 'http://example.com/bracket_(symbol_[)'
  708. """)