renderverbatim.nim 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import strutils
  2. from xmltree import addEscaped
  3. import ast, options, msgs
  4. import packages/docutils/highlite
  5. const isDebug = false
  6. when isDebug:
  7. import renderer
  8. import astalgo
  9. proc lastNodeRec(n: PNode): PNode =
  10. result = n
  11. while result.safeLen > 0: result = result[^1]
  12. proc isInIndentationBlock(src: string, indent: int): bool =
  13. #[
  14. we stop at the first de-indentation; there's an inherent ambiguity with non
  15. doc comments since they can have arbitrary indentation, so we just take the
  16. practical route and require a runnableExamples to keep its code (including non
  17. doc comments) to its indentation level.
  18. ]#
  19. for j in 0..<indent:
  20. if src.len <= j: return true
  21. if src[j] != ' ': return false
  22. return true
  23. type LineData = object
  24. ## keep track of which lines are starting inside a multiline doc comment.
  25. ## We purposefully avoid re-doing parsing which is already done (we get a PNode)
  26. ## so we don't worry about whether we're inside (nested) doc comments etc.
  27. ## But we sill need some logic to disambiguate different multiline styles.
  28. conf: ConfigRef
  29. lineFirst: int
  30. lines: seq[bool]
  31. ## lines[index] is true if line `lineFirst+index` starts inside a multiline string
  32. ## Using a HashSet (extra dependency) would simplify but not by much.
  33. proc tripleStrLitStartsAtNextLine(conf: ConfigRef, n: PNode): bool =
  34. # enabling TLineInfo.offsetA,offsetB would probably make this easier
  35. const tripleQuote = "\"\"\""
  36. let src = sourceLine(conf, n.info)
  37. let col = n.info.col
  38. doAssert src.continuesWith(tripleQuote, col) # sanity check
  39. var i = col + 3
  40. var onlySpace = true
  41. while true:
  42. if src.len <= i:
  43. doAssert src.len == i
  44. return onlySpace
  45. elif src.continuesWith(tripleQuote, i) and (src.len == i+3 or src[i+3] != '\"'):
  46. return false # triple lit is in 1 line
  47. elif src[i] != ' ': onlySpace = false
  48. i.inc
  49. proc visitMultilineStrings(ldata: var LineData, n: PNode) =
  50. var cline = ldata.lineFirst
  51. template setLine() =
  52. let index = cline - ldata.lineFirst
  53. if ldata.lines.len < index+1: ldata.lines.setLen index+1
  54. ldata.lines[index] = true
  55. case n.kind
  56. of nkTripleStrLit:
  57. # same logic should be applied for any multiline token
  58. # we could also consider nkCommentStmt but right now we just assume doc comments,
  59. # unlike triple string litterals, don't de-indent from runnableExamples.
  60. cline = n.info.line.int
  61. if tripleStrLitStartsAtNextLine(ldata.conf, n):
  62. cline.inc
  63. setLine()
  64. for ai in n.strVal:
  65. case ai
  66. of '\n':
  67. cline.inc
  68. setLine()
  69. else: discard
  70. else:
  71. for i in 0..<n.safeLen:
  72. visitMultilineStrings(ldata, n[i])
  73. proc startOfLineInsideTriple(ldata: LineData, line: int): bool =
  74. let index = line - ldata.lineFirst
  75. if index >= ldata.lines.len: false
  76. else: ldata.lines[index]
  77. proc extractRunnableExamplesSource*(conf: ConfigRef; n: PNode): string =
  78. ## TLineInfo.offsetA,offsetB would be cleaner but it's only enabled for nimpretty,
  79. ## we'd need to check performance impact to enable it for nimdoc.
  80. var first = n.lastSon.info
  81. if first.line == n[0].info.line:
  82. #[
  83. runnableExamples: assert true
  84. ]#
  85. discard
  86. else:
  87. #[
  88. runnableExamples:
  89. # non-doc comment that we want to capture even though `first` points to `assert true`
  90. assert true
  91. ]#
  92. first.line = n[0].info.line + 1
  93. let last = n.lastNodeRec.info
  94. var info = first
  95. var indent = info.col
  96. let numLines = numLines(conf, info.fileIndex).uint16
  97. var lastNonemptyPos = 0
  98. result = ""
  99. var ldata = LineData(lineFirst: first.line.int, conf: conf)
  100. visitMultilineStrings(ldata, n[^1])
  101. when isDebug:
  102. debug(n)
  103. for i in 0..<ldata.lines.len:
  104. echo (i+ldata.lineFirst, ldata.lines[i])
  105. for line in first.line..numLines: # bugfix, see `testNimDocTrailingExample`
  106. info.line = line
  107. let src = sourceLine(conf, info)
  108. let special = startOfLineInsideTriple(ldata, line.int)
  109. if line > last.line and not special and not isInIndentationBlock(src, indent):
  110. break
  111. if line > first.line: result.add "\n"
  112. if special:
  113. result.add src
  114. lastNonemptyPos = result.len
  115. elif src.len > indent:
  116. result.add src[indent..^1]
  117. lastNonemptyPos = result.len
  118. result.setLen lastNonemptyPos
  119. proc renderNimCode*(result: var string, code: string, isLatex = false) =
  120. var toknizr: GeneralTokenizer
  121. initGeneralTokenizer(toknizr, code)
  122. var buf = ""
  123. template append(kind, val) =
  124. buf.setLen 0
  125. buf.addEscaped(val)
  126. let class = tokenClassToStr[kind]
  127. if isLatex:
  128. result.addf "\\span$1{$2}", [class, buf]
  129. else:
  130. result.addf "<span class=\"$1\">$2</span>", [class, buf]
  131. while true:
  132. getNextToken(toknizr, langNim)
  133. case toknizr.kind
  134. of gtEof: break # End Of File (or string)
  135. else:
  136. # TODO: avoid alloc; maybe toOpenArray
  137. append(toknizr.kind, substr(code, toknizr.start, toknizr.length + toknizr.start - 1))