summary refs log tree commit diff stats
path: root/compiler/renderverbatim.nim
blob: 02d4058442fe19847af44a18797dc783383fb97b (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
152
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])

  result = ""
  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))
w">not defined(leanCompiler): setupJSgen(graph, module, idgen) else: nil of EvalPass, InterpreterPass: setupEvalGen(graph, module, idgen) of NirReplPass: setupNirReplGen(graph, module, idgen) of GenDependPass: setupDependPass(graph, module, idgen) of Docgen2Pass: when not defined(leanCompiler): openHtml(graph, module, idgen) else: nil of Docgen2TexPass: when not defined(leanCompiler): openTex(graph, module, idgen) else: nil of Docgen2JsonPass: when not defined(leanCompiler): openJson(graph, module, idgen) else: nil of SemPass: nil of NonePass: raiseAssert "use setPipeLinePass to set a proper PipelinePass" if stream == nil: let filename = toFullPathConsiderDirty(graph.config, fileIdx) s = llStreamOpen(filename, fmRead) if s == nil: rawMessage(graph.config, errCannotOpenFile, filename.string) return false graph.interactive = false else: s = stream graph.interactive = stream.kind == llsStdIn while true: syntaxes.openParser(p, fileIdx, s, graph.cache, graph.config) if not belongsToStdlib(graph, module) or (belongsToStdlib(graph, module) and module.name.s == "distros"): # XXX what about caching? no processing then? what if I change the # modules to include between compilation runs? we'd need to track that # in ROD files. I think we should enable this feature only # for the interactive mode. if module.name.s != "nimscriptapi": processImplicitImports graph, graph.config.implicitImports, nkImportStmt, module, ctx, bModule, idgen processImplicitImports graph, graph.config.implicitIncludes, nkIncludeStmt, module, ctx, bModule, idgen checkFirstLineIndentation(p) block processCode: if graph.stopCompile(): break processCode var n = parseTopLevelStmt(p) if n.kind == nkEmpty: break processCode # read everything, no streaming possible var sl = newNodeI(nkStmtList, n.info) sl.add n while true: var n = parseTopLevelStmt(p) if n.kind == nkEmpty: break sl.add n prePass(ctx, sl) if sfReorder in module.flags or codeReordering in graph.config.features: sl = reorder(graph, sl, module) if graph.pipelinePass != EvalPass: message(graph.config, sl.info, hintProcessingStmt, $idgen[]) var semNode = semWithPContext(ctx, sl) discard processPipeline(graph, semNode, bModule) closeParser(p) if s.kind != llsStdIn: break let finalNode = closePContext(graph, ctx, nil) case graph.pipelinePass of CgenPass: if bModule != nil: let m = BModule(bModule) finalCodegenActions(graph, m, finalNode) if graph.dispatchers.len > 0: let ctx = preparePContext(graph, module, idgen) for disp in getDispatchers(graph): let retTyp = disp.typ[0] if retTyp != nil: # TODO: properly semcheck the code of dispatcher? createTypeBoundOps(graph, ctx, retTyp, disp.ast.info, idgen) genProcAux(m, disp) discard closePContext(graph, ctx, nil) of JSgenPass: when not defined(leanCompiler): discard finalJSCodeGen(graph, bModule, finalNode) of EvalPass, InterpreterPass: discard interpreterCode(bModule, finalNode) of NirReplPass: discard runCode(bModule, finalNode) of NirPass: closeNirBackend(bModule, finalNode) of SemPass, GenDependPass: discard of Docgen2Pass, Docgen2TexPass: when not defined(leanCompiler): discard closeDoc(graph, bModule, finalNode) of Docgen2JsonPass: when not defined(leanCompiler): discard closeJson(graph, bModule, finalNode) of NonePass: raiseAssert "use setPipeLinePass to set a proper PipelinePass" if graph.config.backend notin {backendC, backendCpp, backendObjc}: # We only write rod files here if no C-like backend is active. # The C-like backends have been patched to support the IC mechanism. # They are responsible for closing the rod files. See `cbackend.nim`. closeRodFile(graph, module) result = true proc compilePipelineModule*(graph: ModuleGraph; fileIdx: FileIndex; flags: TSymFlags; fromModule: PSym = nil): PSym = var flags = flags if fileIdx == graph.config.projectMainIdx2: flags.incl sfMainModule result = graph.getModule(fileIdx) template processModuleAux(moduleStatus) = onProcessing(graph, fileIdx, moduleStatus, fromModule = fromModule) var s: PLLStream = nil if sfMainModule in flags: if graph.config.projectIsStdin: s = stdin.llStreamOpen elif graph.config.projectIsCmd: s = llStreamOpen(graph.config.cmdInput) discard processPipelineModule(graph, result, idGeneratorFromModule(result), s) if result == nil: var cachedModules: seq[FileIndex] = @[] result = moduleFromRodFile(graph, fileIdx, cachedModules) let filename = AbsoluteFile toFullPath(graph.config, fileIdx) if result == nil: result = newModule(graph, fileIdx) result.flags.incl flags registerModule(graph, result) processModuleAux("import") else: if sfSystemModule in flags: graph.systemModule = result if sfMainModule in flags and graph.config.cmd == cmdM: result.flags.incl flags registerModule(graph, result) processModuleAux("import") partialInitModule(result, graph, fileIdx, filename) for m in cachedModules: registerModuleById(graph, m) if sfMainModule in flags and graph.config.cmd == cmdM: discard else: replayStateChanges(graph.packed.pm[m.int].module, graph) replayGenericCacheInformation(graph, m.int) elif graph.isDirty(result): result.flags.excl sfDirty # reset module fields: initStrTables(graph, result) result.ast = nil processModuleAux("import(dirty)") graph.markClientsDirty(fileIdx) proc importPipelineModule(graph: ModuleGraph; s: PSym, fileIdx: FileIndex): PSym = # this is called by the semantic checking phase assert graph.config != nil result = compilePipelineModule(graph, fileIdx, {}, s) graph.addDep(s, fileIdx) # keep track of import relationships if graph.config.hcrOn: graph.importDeps.mgetOrPut(FileIndex(s.position), @[]).add(fileIdx) #if sfSystemModule in result.flags: # localError(result.info, errAttemptToRedefine, result.name.s) # restore the notes for outer module: graph.config.notes = if graph.config.belongsToProjectPackage(s) or isDefined(graph.config, "booting"): graph.config.mainPackageNotes else: graph.config.foreignPackageNotes proc connectPipelineCallbacks*(graph: ModuleGraph) = graph.includeFileCallback = modules.includeModule graph.importModuleCallback = importPipelineModule proc compilePipelineSystemModule*(graph: ModuleGraph) = if graph.systemModule == nil: connectPipelineCallbacks(graph) graph.config.m.systemFileIdx = fileInfoIdx(graph.config, graph.config.libpath / RelativeFile"system.nim") discard graph.compilePipelineModule(graph.config.m.systemFileIdx, {sfSystemModule}) proc compilePipelineProject*(graph: ModuleGraph; projectFileIdx = InvalidFileIdx) = connectPipelineCallbacks(graph) let conf = graph.config wantMainModule(conf) configComplete(graph) let systemFileIdx = fileInfoIdx(conf, conf.libpath / RelativeFile"system.nim") let projectFile = if projectFileIdx == InvalidFileIdx: conf.projectMainIdx else: projectFileIdx conf.projectMainIdx2 = projectFile let packSym = getPackage(graph, projectFile) graph.config.mainPackageId = packSym.getPackageId graph.importStack.add projectFile if projectFile == systemFileIdx: discard graph.compilePipelineModule(projectFile, {sfMainModule, sfSystemModule}) else: graph.compilePipelineSystemModule() discard graph.compilePipelineModule(projectFile, {sfMainModule})