summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAndrey Makarov <ph.makarov@gmail.com>2021-05-05 10:10:05 +0300
committerGitHub <noreply@github.com>2021-05-05 00:10:05 -0700
commit706562f661e49d06f63a1283525275fb70bb7073 (patch)
tree8e4827d6126c08ccc01337dc2847bb7cbb3abf97
parent00bb3c779e88d621c2b655aa5e1079bb0fc80a16 (diff)
downloadNim-706562f661e49d06f63a1283525275fb70bb7073.tar.gz
docgen: get rid of ropes (#17939)
-rw-r--r--compiler/docgen.nim336
1 files changed, 146 insertions, 190 deletions
diff --git a/compiler/docgen.nim b/compiler/docgen.nim
index 78fb88f9b..d04fbadd1 100644
--- a/compiler/docgen.nim
+++ b/compiler/docgen.nim
@@ -11,7 +11,7 @@
 # by knowing how the anchors are going to be named.
 
 import
-  ast, strutils, strtabs, options, msgs, os, ropes, idents,
+  ast, strutils, strtabs, options, msgs, os, idents,
   wordrecg, syntaxes, renderer, lexer, packages/docutils/rstast,
   packages/docutils/rst, packages/docutils/rstgen,
   json, xmltree, trees, types,
@@ -28,7 +28,7 @@ const
   DocColOffset = "## ".len  # assuming that a space was added after ##
 
 type
-  TSections = array[TSymKind, Rope]
+  TSections = array[TSymKind, string]
   ExampleGroup = ref object
     ## a group of runnableExamples with same rdoccmd
     rdoccmd: string ## from 1st arg in `runnableExamples(rdoccmd): body`
@@ -36,11 +36,11 @@ type
     code: string ## contains imports; each import contains `body`
     index: int ## group index
   TDocumentor = object of rstgen.RstGenerator
-    modDesc: Rope       # module description
+    modDesc: string       # module description
     module: PSym
-    modDeprecationMsg: Rope
+    modDeprecationMsg: string
     toc, toc2, section: TSections
-    tocTable: array[TSymKind, Table[string, Rope]]
+    tocTable: array[TSymKind, Table[string, string]]
     indexValFilename: string
     analytics: string  # Google Analytics javascript, "" if doesn't exist
     seenSymbols: StringTableRef # avoids duplicate symbol generation for HTML.
@@ -247,7 +247,8 @@ proc newDocumentor*(filename: AbsoluteFile; cache: IdentCache; conf: ConfigRef,
   result.destFile = getOutFile2(conf, presentationPath(conf, filename), outExt, false).string
   result.thisDir = result.destFile.AbsoluteFile.splitFile.dir
 
-template dispA(conf: ConfigRef; dest: var Rope, xml, tex: string, args: openArray[Rope]) =
+template dispA(conf: ConfigRef; dest: var string, xml, tex: string,
+               args: openArray[string]) =
   if conf.cmd != cmdRst2tex: dest.addf(xml, args)
   else: dest.addf(tex, args)
 
@@ -257,64 +258,6 @@ proc getVarIdx(varnames: openArray[string], id: string): int =
       return i
   result = -1
 
-proc ropeFormatNamedVars(conf: ConfigRef; frmt: FormatStr,
-                         varnames: openArray[string],
-                         varvalues: openArray[Rope]): Rope =
-  var i = 0
-  result = nil
-  var num = 0
-  while i < frmt.len:
-    if frmt[i] == '$':
-      inc(i)                  # skip '$'
-      case frmt[i]
-      of '#':
-        result.add(varvalues[num])
-        inc(num)
-        inc(i)
-      of '$':
-        result.add("$")
-        inc(i)
-      of '0'..'9':
-        var j = 0
-        while true:
-          j = (j * 10) + ord(frmt[i]) - ord('0')
-          inc(i)
-          if (i > frmt.len + 0 - 1) or not (frmt[i] in {'0'..'9'}): break
-        if j > high(varvalues) + 1:
-          rawMessage(conf, errGenerated, "Invalid format string; too many $s: " & frmt)
-        num = j
-        result.add(varvalues[j - 1])
-      of 'A'..'Z', 'a'..'z', '\x80'..'\xFF':
-        var id = ""
-        while true:
-          id.add(frmt[i])
-          inc(i)
-          if not (frmt[i] in {'A'..'Z', '_', 'a'..'z', '\x80'..'\xFF'}): break
-        var idx = getVarIdx(varnames, id)
-        if idx >= 0: result.add(varvalues[idx])
-        else: rawMessage(conf, errGenerated, "unknown substition variable: " & id)
-      of '{':
-        var id = ""
-        inc(i)
-        while i < frmt.len and frmt[i] != '}':
-          id.add(frmt[i])
-          inc(i)
-        if i >= frmt.len:
-          rawMessage(conf, errGenerated, "expected closing '}'")
-        else:
-          inc(i)                # skip }
-        # search for the variable:
-        let idx = getVarIdx(varnames, id)
-        if idx >= 0: result.add(varvalues[idx])
-        else: rawMessage(conf, errGenerated, "unknown substition variable: " & id)
-      else:
-        result.add("$")
-    var start = i
-    while i < frmt.len:
-      if frmt[i] != '$': inc(i)
-      else: break
-    if i - 1 >= start: result.add(substr(frmt, start, i - 1))
-
 proc genComment(d: PDoc, n: PNode): string =
   result = ""
   if n.comment.len > 0:
@@ -329,23 +272,23 @@ proc genComment(d: PDoc, n: PNode): string =
                             (var dummy: bool; dummy), d.options, d.conf),
                    result)
 
-proc genRecCommentAux(d: PDoc, n: PNode): Rope =
-  if n == nil: return nil
-  result = genComment(d, n).rope
-  if result == nil:
+proc genRecCommentAux(d: PDoc, n: PNode): string =
+  if n == nil: return ""
+  result = genComment(d, n)
+  if result == "":
     if n.kind in {nkStmtList, nkStmtListExpr, nkTypeDef, nkConstDef,
                   nkObjectTy, nkRefTy, nkPtrTy, nkAsgn, nkFastAsgn, nkHiddenStdConv}:
       # notin {nkEmpty..nkNilLit, nkEnumTy, nkTupleTy}:
       for i in 0..<n.len:
         result = genRecCommentAux(d, n[i])
-        if result != nil: return
+        if result != "": return
   else:
     n.comment = ""
 
-proc genRecComment(d: PDoc, n: PNode): Rope =
-  if n == nil: return nil
-  result = genComment(d, n).rope
-  if result == nil:
+proc genRecComment(d: PDoc, n: PNode): string =
+  if n == nil: return ""
+  result = genComment(d, n)
+  if result == "":
     if n.kind in {nkProcDef, nkFuncDef, nkMethodDef, nkIteratorDef,
                   nkMacroDef, nkTemplateDef, nkConverterDef}:
       result = genRecCommentAux(d, n[bodyPos])
@@ -377,15 +320,16 @@ proc externalDep(d: PDoc; module: PSym): string =
   else:
     result = extractFilename toFullPath(d.conf, FileIndex module.position)
 
-proc nodeToHighlightedHtml(d: PDoc; n: PNode; result: var Rope; renderFlags: TRenderFlags = {};
-                           procLink: Rope) =
+proc nodeToHighlightedHtml(d: PDoc; n: PNode; result: var string;
+                           renderFlags: TRenderFlags = {};
+                           procLink: string) =
   var r: TSrcGen
   var literal = ""
   initTokRender(r, n, renderFlags)
   var kind = tkEof
   var tokenPos = 0
   var procTokenPos = 0
-  template escLit(): untyped = rope(esc(d.target, literal))
+  template escLit(): untyped = esc(d.target, literal)
   while true:
     getNextTok(r, kind, literal)
     inc tokenPos
@@ -399,7 +343,7 @@ proc nodeToHighlightedHtml(d: PDoc; n: PNode; result: var Rope; renderFlags: TRe
       if kind in {tkProc, tkMethod, tkIterator, tkMacro, tkTemplate, tkFunc, tkConverter}:
         procTokenPos = tokenPos
       dispA(d.conf, result, "<span class=\"Keyword\">$1</span>", "\\spanKeyword{$1}",
-            [rope(literal)])
+            [literal])
     of tkOpr:
       dispA(d.conf, result, "<span class=\"Operator\">$1</span>", "\\spanOperator{$1}",
             [escLit])
@@ -418,7 +362,7 @@ proc nodeToHighlightedHtml(d: PDoc; n: PNode; result: var Rope; renderFlags: TRe
     of tkSymbol:
       let s = getTokSym(r)
       # -2 because of the whitespace in between:
-      if procTokenPos == tokenPos-2 and procLink != nil:
+      if procTokenPos == tokenPos-2 and procLink != "":
         dispA(d.conf, result, "<a href=\"#$2\"><span class=\"Identifier\">$1</span></a>",
               "\\spanIdentifier{$1}", [escLit, procLink])
       elif s != nil and s.kind in {skType, skVar, skLet, skConst} and
@@ -426,7 +370,7 @@ proc nodeToHighlightedHtml(d: PDoc; n: PNode; result: var Rope; renderFlags: TRe
            belongsToPackage(d.conf, s.owner) and d.target == outHtml:
         let external = externalDep(d, s.owner)
         result.addf "<a href=\"$1#$2\"><span class=\"Identifier\">$3</span></a>",
-          [rope changeFileExt(external, "html"), rope literal,
+          [changeFileExt(external, "html"), literal,
            escLit]
       else:
         dispA(d.conf, result, "<span class=\"Identifier\">$1</span>",
@@ -570,7 +514,9 @@ type RunnableState = enum
   rsRunnable
   rsDone
 
-proc getAllRunnableExamplesImpl(d: PDoc; n: PNode, dest: var Rope, state: RunnableState, topLevel: bool): RunnableState =
+proc getAllRunnableExamplesImpl(d: PDoc; n: PNode, dest: var string,
+                                state: RunnableState, topLevel: bool):
+                               RunnableState =
   ##[
   Simple state machine to tell whether we render runnableExamples and doc comments.
   This is to ensure that we can interleave runnableExamples and doc comments freely;
@@ -600,7 +546,7 @@ proc getAllRunnableExamplesImpl(d: PDoc; n: PNode, dest: var Rope, state: Runnab
         var msg = "Example:"
         if rdoccmd.len > 0: msg.add " cmd: " & rdoccmd
         dispA(d.conf, dest, "\n<p><strong class=\"examples_text\">$1</strong></p>\n",
-            "\n\\textbf{$1}\n", [msg.rope])
+            "\n\\textbf{$1}\n", [msg])
         inc d.listingCounter
         let id = $d.listingCounter
         dest.add(d.config.getOrDefault"doc.listing_start" % [id, "langNim", ""])
@@ -646,12 +592,12 @@ proc getRoutineBody(n: PNode): PNode =
     doAssert result.len == 2
     result = result[1]
 
-proc getAllRunnableExamples(d: PDoc, n: PNode, dest: var Rope) =
+proc getAllRunnableExamples(d: PDoc, n: PNode, dest: var string) =
   var n = n
   var state = rsStart
   template fn(n2, topLevel) =
     state = getAllRunnableExamplesImpl(d, n2, dest, state, topLevel)
-  dest.add genComment(d, n).rope
+  dest.add genComment(d, n)
   case n.kind
   of routineDefs:
     n = n.getRoutineBody
@@ -806,20 +752,18 @@ proc docstringSummary(rstText: string): string =
     result.delete(pos, last)
     result.add("…")
 
-proc genDeprecationMsg(d: PDoc, n: PNode): Rope =
+proc genDeprecationMsg(d: PDoc, n: PNode): string =
   ## Given a nkPragma wDeprecated node output a well-formatted section
   if n == nil: return
 
   case n.safeLen:
   of 0: # Deprecated w/o any message
-    result = ropeFormatNamedVars(d.conf,
-      getConfigVar(d.conf, "doc.deprecationmsg"), ["label", "message"],
-      [~"Deprecated", nil])
+    result = getConfigVar(d.conf, "doc.deprecationmsg") % [
+       "label" , "Deprecated", "message", ""]
   of 2: # Deprecated w/ a message
     if n[1].kind in {nkStrLit..nkTripleStrLit}:
-      result = ropeFormatNamedVars(d.conf,
-        getConfigVar(d.conf, "doc.deprecationmsg"), ["label", "message"],
-        [~"Deprecated:", rope(xmltree.escape(n[1].strVal))])
+      result = getConfigVar(d.conf, "doc.deprecationmsg") % [
+          "label", "Deprecated:", "message", xmltree.escape(n[1].strVal)]
   else:
     doAssert false
 
@@ -827,7 +771,7 @@ type DocFlags = enum
   kDefault
   kForceExport
 
-proc genSeeSrcRope(d: PDoc, path: string, line: int): Rope =
+proc genSeeSrc(d: PDoc, path: string, line: int): string =
   let docItemSeeSrc = getConfigVar(d.conf, "doc.item.seesrc")
   if docItemSeeSrc.len > 0:
     let path = relativeTo(AbsoluteFile path, AbsoluteDir getCurrentDir(), '/')
@@ -843,20 +787,19 @@ proc genSeeSrcRope(d: PDoc, path: string, line: int): Rope =
         else: "version-$1-$2" % [$NimMajor, $NimMinor]
       let commit = getConfigVar(d.conf, "git.commit", defaultBranch)
       let develBranch = getConfigVar(d.conf, "git.devel", "devel")
-      dispA(d.conf, result, "$1", "", [ropeFormatNamedVars(d.conf, docItemSeeSrc,
-          ["path", "line", "url", "commit", "devel"], [rope path.string,
-          rope($line), rope gitUrl, rope commit, rope develBranch])])
+      dispA(d.conf, result, "$1", "", [docItemSeeSrc % [
+          "path", path.string, "line", $line, "url", gitUrl,
+          "commit", commit, "devel", develBranch]])
 
 proc genItem(d: PDoc, n, nameNode: PNode, k: TSymKind, docFlags: DocFlags) =
   if (docFlags != kForceExport) and not isVisible(d, nameNode): return
   let
     name = getName(d, nameNode)
-    nameRope = name.rope
   var plainDocstring = getPlainDocstring(n) # call here before genRecComment!
-  var result: Rope = nil
+  var result = ""
   var literal, plainName = ""
   var kind = tkEof
-  var comm: Rope = nil
+  var comm = ""
   if n.kind in routineDefs:
     getAllRunnableExamples(d, n, comm)
   else:
@@ -878,35 +821,33 @@ proc genItem(d: PDoc, n, nameNode: PNode, k: TSymKind, docFlags: DocFlags) =
 
   inc(d.id)
   let
-    plainNameRope = rope(xmltree.escape(plainName.strip))
+    plainNameEsc = xmltree.escape(plainName.strip)
     cleanPlainSymbol = renderPlainSymbolName(nameNode)
     complexSymbol = complexName(k, n, cleanPlainSymbol)
-    plainSymbolRope = rope(cleanPlainSymbol)
-    plainSymbolEncRope = rope(encodeUrl(cleanPlainSymbol))
-    itemIDRope = rope(d.id)
+    plainSymbolEnc = encodeUrl(cleanPlainSymbol)
     symbolOrId = d.newUniquePlainSymbol(complexSymbol)
-    symbolOrIdRope = symbolOrId.rope
-    symbolOrIdEncRope = encodeUrl(symbolOrId).rope
-    deprecationMsgRope = genDeprecationMsg(d, pragmaNode)
+    symbolOrIdEnc = encodeUrl(symbolOrId)
+    deprecationMsg = genDeprecationMsg(d, pragmaNode)
 
   nodeToHighlightedHtml(d, n, result, {renderNoBody, renderNoComments,
-    renderDocComments, renderSyms}, symbolOrIdEncRope)
+    renderDocComments, renderSyms}, symbolOrIdEnc)
+
+  let seeSrc = genSeeSrc(d, toFullPath(d.conf, n.info), n.info.line.int)
 
-  let seeSrcRope = genSeeSrcRope(d, toFullPath(d.conf, n.info), n.info.line.int)
-  d.section[k].add(ropeFormatNamedVars(d.conf, getConfigVar(d.conf, "doc.item"),
-    ["name", "header", "desc", "itemID", "header_plain", "itemSym",
-      "itemSymOrID", "itemSymEnc", "itemSymOrIDEnc", "seeSrc", "deprecationMsg"],
-    [nameRope, result, comm, itemIDRope, plainNameRope, plainSymbolRope,
-      symbolOrIdRope, plainSymbolEncRope, symbolOrIdEncRope, seeSrcRope,
-      deprecationMsgRope]))
+  d.section[k].add(getConfigVar(d.conf, "doc.item") %
+    ["name", name, "header", result, "desc", comm, "itemID", $d.id,
+     "header_plain", plainNameEsc, "itemSym", cleanPlainSymbol,
+     "itemSymOrID", symbolOrId, "itemSymEnc", plainSymbolEnc,
+     "itemSymOrIDEnc", symbolOrIdEnc, "seeSrc", seeSrc,
+     "deprecationMsg", deprecationMsg])
 
   let external = d.destFile.AbsoluteFile.relativeTo(d.conf.outDir, '/').changeFileExt(HtmlExt).string
 
-  var attype: Rope
+  var attype = ""
   if k in routineKinds and nameNode.kind == nkSym:
     let att = attachToType(d, nameNode.sym)
     if att != nil:
-      attype = rope esc(d.target, att.name.s)
+      attype = esc(d.target, att.name.s)
   elif k == skType and nameNode.kind == nkSym and nameNode.sym.typ.kind in {tyEnum, tyBool}:
     let etyp = nameNode.sym.typ
     for e in etyp.n:
@@ -916,14 +857,15 @@ proc genItem(d: PDoc, n, nameNode: PNode, k: TSymKind, docFlags: DocFlags) =
       setIndexTerm(d[], external, symbolOrId, plain, nameNode.sym.name.s & '.' & plain,
         xmltree.escape(getPlainDocstring(e).docstringSummary))
 
-  d.toc[k].add(ropeFormatNamedVars(d.conf, getConfigVar(d.conf, "doc.item.toc"),
-    ["name", "header_plain", "itemSymOrIDEnc"],
-    [nameRope, plainNameRope, symbolOrIdEncRope]))
+  d.toc[k].add(getConfigVar(d.conf, "doc.item.toc") % [
+      "name", name, "header_plain", plainNameEsc,
+      "itemSymOrIDEnc", symbolOrIdEnc])
 
-  d.tocTable[k].mgetOrPut(cleanPlainSymbol, nil).add(ropeFormatNamedVars(
-    d.conf, getConfigVar(d.conf, "doc.item.tocTable"),
-    ["name", "header_plain", "itemSymOrID", "itemSymOrIDEnc"],
-    [nameRope, plainNameRope, rope(symbolOrId.replace(",", ",<wbr>")), symbolOrIdEncRope]))
+  d.tocTable[k].mgetOrPut(cleanPlainSymbol, "").add(
+    getConfigVar(d.conf, "doc.item.tocTable") % [
+      "name", name, "header_plain", plainNameEsc,
+      "itemSymOrID", symbolOrId.replace(",", ",<wbr>"),
+      "itemSymOrIDEnc", symbolOrIdEnc])
 
   # Ironically for types the complexSymbol is *cleaner* than the plainName
   # because it doesn't include object fields or documentation comments. So we
@@ -941,7 +883,7 @@ proc genJsonItem(d: PDoc, n, nameNode: PNode, k: TSymKind): JsonNode =
   if not isVisible(d, nameNode): return
   var
     name = getName(d, nameNode)
-    comm = $genRecComment(d, n)
+    comm = genRecComment(d, n)
     r: TSrcGen
   initTokRender(r, n, {renderNoBody, renderNoComments, renderDocComments})
   result = %{ "name": %name, "type": %($k), "line": %n.info.line.int,
@@ -997,35 +939,35 @@ proc traceDeps(d: PDoc, it: PNode) =
       traceDeps(d, a)
   elif it.kind == nkSym and belongsToPackage(d.conf, it.sym):
     let external = externalDep(d, it.sym)
-    if d.section[k] != nil: d.section[k].add(", ")
+    if d.section[k] != "": d.section[k].add(", ")
     dispA(d.conf, d.section[k],
           "<a class=\"reference external\" href=\"$2\">$1</a>",
-          "$1", [rope esc(d.target, external.prettyLink),
-          rope changeFileExt(external, "html")])
+          "$1", [esc(d.target, external.prettyLink),
+                 changeFileExt(external, "html")])
 
 proc exportSym(d: PDoc; s: PSym) =
   const k = exportSection
   if s.kind == skModule and belongsToPackage(d.conf, s):
     let external = externalDep(d, s)
-    if d.section[k] != nil: d.section[k].add(", ")
+    if d.section[k] != "": d.section[k].add(", ")
     dispA(d.conf, d.section[k],
           "<a class=\"reference external\" href=\"$2\">$1</a>",
-          "$1", [rope esc(d.target, external.prettyLink),
-          rope changeFileExt(external, "html")])
+          "$1", [esc(d.target, external.prettyLink),
+                 changeFileExt(external, "html")])
   elif s.kind != skModule and s.owner != nil:
     let module = originatingModule(s)
     if belongsToPackage(d.conf, module):
       let
         complexSymbol = complexName(s.kind, s.ast, s.name.s)
-        symbolOrIdRope = rope(d.newUniquePlainSymbol(complexSymbol))
+        symbolOrId = d.newUniquePlainSymbol(complexSymbol)
         external = externalDep(d, module)
-      if d.section[k] != nil: d.section[k].add(", ")
+      if d.section[k] != "": d.section[k].add(", ")
       # XXX proper anchor generation here
       dispA(d.conf, d.section[k],
             "<a href=\"$2#$3\"><span class=\"Identifier\">$1</span></a>",
-            "$1", [rope esc(d.target, s.name.s),
-            rope changeFileExt(external, "html"),
-            symbolOrIdRope])
+            "$1", [esc(d.target, s.name.s),
+                   changeFileExt(external, "html"),
+                   symbolOrId])
 
 proc documentNewEffect(cache: IdentCache; n: PNode): PNode =
   let s = n[namePos].sym
@@ -1130,9 +1072,9 @@ proc generateDoc*(d: PDoc, n, orig: PNode, docFlags: DocFlags = kDefault) =
   of nkExportExceptStmt: discard "transformed into nkExportStmt by semExportExcept"
   of nkFromStmt, nkImportExceptStmt: traceDeps(d, n[0])
   of nkCallKinds:
-    var comm: Rope = nil
+    var comm = ""
     getAllRunnableExamples(d, n, comm)
-    if comm != nil: d.modDesc.add(comm)
+    if comm != "": d.modDesc.add(comm)
   else: discard
 
 proc add(d: PDoc; j: JsonNode) =
@@ -1179,7 +1121,7 @@ proc generateJson*(d: PDoc, n: PNode, includeComments: bool = true) =
 proc genTagsItem(d: PDoc, n, nameNode: PNode, k: TSymKind): string =
   result = getName(d, nameNode) & "\n"
 
-proc generateTags*(d: PDoc, n: PNode, r: var Rope) =
+proc generateTags*(d: PDoc, n: PNode, r: var string) =
   case n.kind
   of nkCommentStmt:
     if startsWith(n.comment, "##"):
@@ -1224,41 +1166,41 @@ proc genSection(d: PDoc, kind: TSymKind, groupedToc = false) =
     "Imports", "Types", "Vars", "Lets", "Consts", "Vars", "Procs", "Funcs",
     "Methods", "Iterators", "Converters", "Macros", "Templates", "Exports"
   ]
-  if d.section[kind] == nil: return
-  var title = sectionNames[kind].rope
-  d.section[kind] = ropeFormatNamedVars(d.conf, getConfigVar(d.conf, "doc.section"), [
-      "sectionid", "sectionTitle", "sectionTitleID", "content"], [
-      ord(kind).rope, title, rope(ord(kind) + 50), d.section[kind]])
+  if d.section[kind] == "": return
+  var title = sectionNames[kind]
+  d.section[kind] = getConfigVar(d.conf, "doc.section") % [
+      "sectionid", $ord(kind), "sectionTitle", title,
+      "sectionTitleID", $(ord(kind) + 50), "content", d.section[kind]]
 
   var tocSource = d.toc
   if groupedToc:
     for p in d.tocTable[kind].keys:
-      d.toc2[kind].add ropeFormatNamedVars(d.conf, getConfigVar(d.conf, "doc.section.toc2"), [
-          "sectionid", "sectionTitle", "sectionTitleID", "content", "plainName"], [
-          ord(kind).rope, title, rope(ord(kind) + 50), d.tocTable[kind][p], p.rope])
+      d.toc2[kind].add getConfigVar(d.conf, "doc.section.toc2") % [
+          "sectionid", $ord(kind), "sectionTitle", title,
+          "sectionTitleID", $(ord(kind) + 50),
+          "content", d.tocTable[kind][p], "plainName", p]
     tocSource = d.toc2
 
-  d.toc[kind] = ropeFormatNamedVars(d.conf, getConfigVar(d.conf, "doc.section.toc"), [
-      "sectionid", "sectionTitle", "sectionTitleID", "content"], [
-      ord(kind).rope, title, rope(ord(kind) + 50), tocSource[kind]])
+  d.toc[kind] = getConfigVar(d.conf, "doc.section.toc") % [
+      "sectionid", $ord(kind), "sectionTitle", title,
+      "sectionTitleID", $(ord(kind) + 50), "content", tocSource[kind]]
 
-proc relLink(outDir: AbsoluteDir, destFile: AbsoluteFile, linkto: RelativeFile): Rope =
-  rope($relativeTo(outDir / linkto, destFile.splitFile().dir, '/'))
+proc relLink(outDir: AbsoluteDir, destFile: AbsoluteFile, linkto: RelativeFile): string =
+  $relativeTo(outDir / linkto, destFile.splitFile().dir, '/')
 
-proc genOutFile(d: PDoc, groupedToc = false): Rope =
+proc genOutFile(d: PDoc, groupedToc = false): string =
   var
-    code, content: Rope
+    code, content: string
     title = ""
   var j = 0
-  var tmp = ""
-  renderTocEntries(d[], j, 1, tmp)
-  var toc = tmp.rope
+  var toc = ""
+  renderTocEntries(d[], j, 1, toc)
   for i in TSymKind:
     var shouldSort = i in routineKinds and groupedToc
     genSection(d, i, shouldSort)
     toc.add(d.toc[i])
-  if toc != nil:
-    toc = ropeFormatNamedVars(d.conf, getConfigVar(d.conf, "doc.toc"), ["content"], [toc])
+  if toc != "":
+    toc = getConfigVar(d.conf, "doc.toc") % ["content", toc]
   for i in TSymKind: code.add(d.section[i])
 
   # Extract the title. Non API modules generate an entry in the index table.
@@ -1269,10 +1211,10 @@ proc genOutFile(d: PDoc, groupedToc = false): Rope =
   else:
     # Modules get an automatic title for the HTML, but no entry in the index.
     title = canonicalImport(d.conf, AbsoluteFile d.filename)
-  var subtitle = "".rope
+  var subtitle = ""
   if d.meta[metaSubtitle] != "":
     dispA(d.conf, subtitle, "<h2 class=\"subtitle\">$1</h2>",
-        "\\\\\\vspace{0.5em}\\large $1", [d.meta[metaSubtitle].rope])
+        "\\\\\\vspace{0.5em}\\large $1", [d.meta[metaSubtitle]])
 
   var groupsection = getConfigVar(d.conf, "doc.body_toc_groupsection")
   let bodyname = if d.hasToc and not d.isPureRst:
@@ -1280,20 +1222,27 @@ proc genOutFile(d: PDoc, groupedToc = false): Rope =
                    "doc.body_toc_group"
                  elif d.hasToc: "doc.body_toc"
                  else: "doc.body_no_toc"
-  let seeSrcRope = genSeeSrcRope(d, d.filename, 1)
-  content = ropeFormatNamedVars(d.conf, getConfigVar(d.conf, bodyname), ["title", "subtitle",
-      "tableofcontents", "moduledesc", "date", "time", "content", "deprecationMsg", "theindexhref", "body_toc_groupsection", "seeSrc"],
-      [title.rope, subtitle, toc, d.modDesc, rope(getDateStr()),
-      rope(getClockStr()), code, d.modDeprecationMsg, relLink(d.conf.outDir, d.destFile.AbsoluteFile, theindexFname.RelativeFile), groupsection.rope, seeSrcRope])
+  let seeSrc = genSeeSrc(d, d.filename, 1)
+  content = getConfigVar(d.conf, bodyname) % [
+      "title", title, "subtitle", subtitle,
+      "tableofcontents", toc, "moduledesc", d.modDesc, "date", getDateStr(),
+      "time", getClockStr(), "content", code,
+      "deprecationMsg", d.modDeprecationMsg,
+      "theindexhref", relLink(d.conf.outDir, d.destFile.AbsoluteFile,
+                              theindexFname.RelativeFile),
+      "body_toc_groupsection", groupsection, "seeSrc", seeSrc]
   if optCompileOnly notin d.conf.globalOptions:
     # XXX what is this hack doing here? 'optCompileOnly' means raw output!?
-    code = ropeFormatNamedVars(d.conf, getConfigVar(d.conf, "doc.file"), [
-        "nimdoccss", "dochackjs",  "title", "subtitle", "tableofcontents", "moduledesc", "date", "time",
-        "content", "author", "version", "analytics", "deprecationMsg"],
-        [relLink(d.conf.outDir, d.destFile.AbsoluteFile, nimdocOutCss.RelativeFile),
-        relLink(d.conf.outDir, d.destFile.AbsoluteFile, docHackJsFname.RelativeFile),
-        title.rope, subtitle, toc, d.modDesc, rope(getDateStr()), rope(getClockStr()),
-        content, d.meta[metaAuthor].rope, d.meta[metaVersion].rope, d.analytics.rope, d.modDeprecationMsg])
+    code = getConfigVar(d.conf, "doc.file") % [
+        "nimdoccss", relLink(d.conf.outDir, d.destFile.AbsoluteFile,
+                             nimdocOutCss.RelativeFile),
+        "dochackjs", relLink(d.conf.outDir, d.destFile.AbsoluteFile,
+                             docHackJsFname.RelativeFile),
+        "title", title, "subtitle", subtitle, "tableofcontents", toc,
+        "moduledesc", d.modDesc, "date", getDateStr(), "time", getClockStr(),
+        "content", content, "author", d.meta[metaAuthor],
+        "version", d.meta[metaVersion], "analytics", d.analytics,
+        "deprecationMsg", d.modDeprecationMsg]
   else:
     code = content
   result = code
@@ -1316,17 +1265,19 @@ proc writeOutput*(d: PDoc, useWarning = false, groupedToc = false) =
   runAllExamples(d)
   var content = genOutFile(d, groupedToc)
   if optStdout in d.conf.globalOptions:
-    writeRope(stdout, content)
+    write(stdout, content)
   else:
     template outfile: untyped = d.destFile.AbsoluteFile
     #let outfile = getOutFile2(d.conf, shortenDir(d.conf, filename), outExt)
     let dir = outfile.splitFile.dir
     createDir(dir)
     updateOutfile(d, outfile)
-    if not writeRope(content, outfile):
+    try:
+      writeFile(outfile, content)
+    except IOError:
       rawMessage(d.conf, if useWarning: warnCannotOpenFile else: errCannotOpenFile,
         outfile.string)
-    elif not d.wroteSupportFiles: # nimdoc.css + dochack.js
+    if not d.wroteSupportFiles: # nimdoc.css + dochack.js
       let nimr = $d.conf.getPrefixDir()
       copyFile(docCss.interp(nimr = nimr), $d.conf.outDir / nimdocOutCss)
       if optGenIndex in d.conf.globalOptions:
@@ -1386,7 +1337,7 @@ proc commandRstAux(cache: IdentCache, conf: ConfigRef;
                      conf)
   var modDesc = newStringOfCap(30_000)
   renderRstToOut(d[], rst, modDesc)
-  d.modDesc = rope(modDesc)
+  d.modDesc = modDesc
   writeOutput(d)
   generateIndex(d)
 
@@ -1407,14 +1358,16 @@ proc commandJson*(cache: IdentCache, conf: ConfigRef) =
   d.hasToc = true
   generateJson(d, ast)
   let json = d.jArray
-  let content = rope(pretty(json))
+  let content = pretty(json)
 
   if optStdout in d.conf.globalOptions:
-    writeRope(stdout, content)
+    write(stdout, content)
   else:
     #echo getOutFile(gProjectFull, JsonExt)
     let filename = getOutFile(conf, RelativeFile conf.projectName, JsonExt)
-    if not writeRope(content, filename):
+    try:
+      writeFile(filename, content)
+    except:
       rawMessage(conf, errCannotOpenFile, filename.string)
 
 proc commandTags*(cache: IdentCache, conf: ConfigRef) =
@@ -1427,33 +1380,36 @@ proc commandTags*(cache: IdentCache, conf: ConfigRef) =
                warnUser, "the ':test:' attribute is not supported by this backend")
   d.hasToc = true
   var
-    content: Rope
+    content = ""
   generateTags(d, ast, content)
 
   if optStdout in d.conf.globalOptions:
-    writeRope(stdout, content)
+    write(stdout, content)
   else:
     #echo getOutFile(gProjectFull, TagsExt)
     let filename = getOutFile(conf, RelativeFile conf.projectName, TagsExt)
-    if not writeRope(content, filename):
+    try:
+      writeFile(filename, content)
+    except:
       rawMessage(conf, errCannotOpenFile, filename.string)
 
 proc commandBuildIndex*(conf: ConfigRef, dir: string, outFile = RelativeFile"") =
-  var content = mergeIndexes(dir).rope
+  var content = mergeIndexes(dir)
 
   var outFile = outFile
   if outFile.isEmpty: outFile = theindexFname.RelativeFile.changeFileExt("")
   let filename = getOutFile(conf, outFile, HtmlExt)
 
-  let code = ropeFormatNamedVars(conf, getConfigVar(conf, "doc.file"), [
-      "nimdoccss", "dochackjs",
-      "title", "subtitle", "tableofcontents", "moduledesc", "date", "time",
-      "content", "author", "version", "analytics"],
-      [relLink(conf.outDir, filename, nimdocOutCss.RelativeFile),
-      relLink(conf.outDir, filename, docHackJsFname.RelativeFile),
-      rope"Index", rope"", nil, nil, rope(getDateStr()),
-      rope(getClockStr()), content, nil, nil, nil])
+  let code = getConfigVar(conf, "doc.file") % [
+      "nimdoccss", relLink(conf.outDir, filename, nimdocOutCss.RelativeFile),
+      "dochackjs", relLink(conf.outDir, filename, docHackJsFname.RelativeFile),
+      "title", "Index",
+      "subtitle", "", "tableofcontents", "", "moduledesc", "",
+      "date", getDateStr(), "time", getClockStr(),
+      "content", content, "author", "", "version", "", "analytics", ""]
   # no analytics because context is not available
 
-  if not writeRope(code, filename):
+  try:
+    writeFile(filename, code)
+  except:
     rawMessage(conf, errCannotOpenFile, filename.string)