summary refs log tree commit diff stats
path: root/compiler/renderverbatim.nim
blob: d20ee15496427f757df36a668510781d37e17969 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import strutils
from xmltree import addEscaped

import ast, options, msgs
import packages/docutils/highlite

const isDebug = false
when isDebug:
  import renderer
  import astalgo

proc lastNodeRec(n: PNode): PNode =
  result = n
  while result.safeLen > 0: result = result[^1]

proc isInIndentationBlock(src: string, indent: int): bool =
  #[
  we stop at the first de-indentation; there's an inherent ambiguity with non
  doc comments since they can have arbitrary indentation, so we just take the
  practical route and require a runnableExamples to keep its code (including non
  doc comments) to its indentation level.
  ]#
  for j in 0..<indent:
    if src.len <= j: return true
    if src[j] != ' ': return false
  return true

type LineData = object
  ## keep track of which lines are starting inside a multiline doc comment.
  ## We purposefully avoid re-doing parsing which is already done (we get a PNode)
  ## so we don't worry about whether we're inside (nested) doc comments etc.
  ## But we sill need some logic to disambiguate different multiline styles.
  conf: ConfigRef
  lineFirst: int
  lines: seq[bool]
    ## lines[index] is true if line `lineFirst+index` starts inside a multiline string
    ## Using a HashSet (extra dependency) would simplify but not by much.

proc tripleStrLitStartsAtNextLine(conf: ConfigRef, n: PNode): bool =
  # enabling TLineInfo.offsetA,offsetB would probably make this easier
  const tripleQuote = "\"\"\""
  let src = sourceLine(conf, n.info)
  let col = n.info.col
  doAssert src.continuesWith(tripleQuote, col) # sanity check
  var i = col + 3
  var onlySpace = true
  while true:
    if src.len <= i:
      doAssert src.len == i
      return onlySpace
    elif src.continuesWith(tripleQuote, i) and (src.len == i+3 or src[i+3] != '\"'):
      return false # triple lit is in 1 line
    elif src[i] != ' ': onlySpace = false
    i.inc

proc visitMultilineStrings(ldata: var LineData, n: PNode) =
  var cline = ldata.lineFirst

  template setLine() =
    let index = cline - ldata.lineFirst
    if ldata.lines.len < index+1: ldata.lines.setLen index+1
    ldata.lines[index] = true

  case n.kind
  of nkTripleStrLit:
    # same logic should be applied for any multiline token
    # we could also consider nkCommentStmt but right now we just assume doc comments,
    # unlike triple string litterals, don't de-indent from runnableExamples.
    cline = n.info.line.int
    if tripleStrLitStartsAtNextLine(ldata.conf, n):
      cline.inc
      setLine()
    for ai in n.strVal:
      case ai
      of '\n':
        cline.inc
        setLine()
      else: discard
  else:
    for i in 0..<n.safeLen:
      visitMultilineStrings(ldata, n[i])

proc startOfLineInsideTriple(ldata: LineData, line: int): bool =
  let index = line - ldata.lineFirst
  if index >= ldata.lines.len: false
  else: ldata.lines[index]

proc extractRunnableExamplesSource*(conf: ConfigRef; n: PNode): string =
  ## TLineInfo.offsetA,offsetB would be cleaner but it's only enabled for nimpretty,
  ## we'd need to check performance impact to enable it for nimdoc.
  var first = n.lastSon.info
  if first.line == n[0].info.line:
    #[
    runnableExamples: assert true
    ]#
    discard
  else:
    #[
    runnableExamples:
      # non-doc comment that we want to capture even though `first` points to `assert true`
      assert true
    ]#
    first.line = n[0].info.line + 1

  let last = n.lastNodeRec.info
  var info = first
  var indent = info.col
  let numLines = numLines(conf, info.fileIndex).uint16
  var lastNonemptyPos = 0

  var ldata = LineData(lineFirst: first.line.int, conf: conf)
  visitMultilineStrings(ldata, n[^1])
  when isDebug:
    debug(n)
    for i in 0..<ldata.lines.len:
      echo (i+ldata.lineFirst, ldata.lines[i])

  for line in first.line..numLines: # bugfix, see `testNimDocTrailingExample`
    info.line = line
    let src = sourceLine(conf, info)
    let special = startOfLineInsideTriple(ldata, line.int)
    if line > last.line and not special and not isInIndentationBlock(src, indent):
      break
    if line > first.line: result.add "\n"
    if special:
      result.add src
      lastNonemptyPos = result.len
    elif src.len > indent:
      result.add src[indent..^1]
      lastNonemptyPos = result.len
  result.setLen lastNonemptyPos

proc renderNimCode*(result: var string, code: string, isLatex = false) =
  var toknizr: GeneralTokenizer
  initGeneralTokenizer(toknizr, code)
  var buf = ""
  template append(kind, val) =
    buf.setLen 0
    buf.addEscaped(val)
    let class = tokenClassToStr[kind]
    if isLatex:
      result.addf "\\span$1{$2}", [class, buf]
    else:
      result.addf  "<span class=\"$1\">$2</span>", [class, buf]
  while true:
    getNextToken(toknizr, langNim)
    case toknizr.kind
    of gtEof: break  # End Of File (or string)
    else:
      # TODO: avoid alloc; maybe toOpenArray
      append(toknizr.kind, substr(code, toknizr.start, toknizr.length + toknizr.start - 1))