nimweb.nim 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. #
  2. #
  3. # Nim Website Generator
  4. # (c) Copyright 2015 Andreas Rumpf
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. import
  10. os, strutils, times, parseopt, parsecfg, streams, strtabs, tables,
  11. re, htmlgen, macros, md5, osproc, parsecsv, algorithm
  12. from xmltree import escape
  13. type
  14. TKeyValPair = tuple[key, id, val: string]
  15. TConfigData = object of RootObj
  16. tabs, links: seq[TKeyValPair]
  17. doc, srcdoc, srcdoc2, webdoc, pdf: seq[string]
  18. authors, projectName, projectTitle, logo, infile, ticker: string
  19. vars: StringTableRef
  20. nimCompiler: string
  21. nimArgs: string
  22. gitURL: string
  23. docHTMLOutput: string
  24. webUploadOutput: string
  25. quotations: Table[string, tuple[quote, author: string]]
  26. numProcessors: int # Set by parallelBuild:n, only works for values > 0.
  27. gaId: string # google analytics ID, nil means analytics are disabled
  28. TRssItem = object
  29. year, month, day, title, url, content: string
  30. TAction = enum
  31. actAll, actOnlyWebsite, actPdf, actJson2, actOnlyDocs
  32. Sponsor = object
  33. logo: string
  34. name: string
  35. url: string
  36. thisMonth: int
  37. allTime: int
  38. since: string
  39. level: int
  40. var action: TAction
  41. proc initConfigData(c: var TConfigData) =
  42. c.tabs = @[]
  43. c.links = @[]
  44. c.doc = @[]
  45. c.srcdoc = @[]
  46. c.srcdoc2 = @[]
  47. c.webdoc = @[]
  48. c.pdf = @[]
  49. c.infile = ""
  50. c.nimArgs = "--hint[Conf]:off --hint[Path]:off --hint[Processing]:off -d:boot "
  51. c.gitURL = "https://github.com/nim-lang/Nim"
  52. c.docHTMLOutput = "doc/html"
  53. c.webUploadOutput = "web/upload"
  54. c.authors = ""
  55. c.projectTitle = ""
  56. c.projectName = ""
  57. c.logo = ""
  58. c.ticker = ""
  59. c.vars = newStringTable(modeStyleInsensitive)
  60. c.numProcessors = countProcessors()
  61. # Attempts to obtain the git current commit.
  62. when false:
  63. let (output, code) = execCmdEx("git log -n 1 --format=%H")
  64. if code == 0 and output.strip.len == 40:
  65. c.gitCommit = output.strip
  66. c.quotations = initTable[string, tuple[quote, author: string]]()
  67. include "website.nimf"
  68. # ------------------------- configuration file -------------------------------
  69. const
  70. version = "0.8"
  71. usage = "nimweb - Nim Website Generator Version " & version & """
  72. (c) 2015 Andreas Rumpf
  73. Usage:
  74. nimweb [options] ini-file[.ini] [compile_options]
  75. Options:
  76. -h, --help shows this help
  77. -v, --version shows the version
  78. -o, --output overrides output directory instead of default
  79. web/upload and doc/html
  80. --nimCompiler overrides nim compiler; default = bin/nim
  81. --var:name=value set the value of a variable
  82. --website only build the website, not the full documentation
  83. --pdf build the PDF version of the documentation
  84. --json2 build JSON of the documentation
  85. --onlyDocs build only the documentation
  86. --git.url override base url in generated doc links
  87. --git.commit override commit/branch in generated doc links 'source'
  88. --git.devel override devel branch in generated doc links 'edit'
  89. Compile_options:
  90. will be passed to the Nim compiler
  91. """
  92. rYearMonthDay = r"on\s+(\d{2})\/(\d{2})\/(\d{4})"
  93. rssUrl = "http://nim-lang.org/news.xml"
  94. rssNewsUrl = "http://nim-lang.org/news.html"
  95. activeSponsors = "web/sponsors.csv"
  96. inactiveSponsors = "web/inactive_sponsors.csv"
  97. validAnchorCharacters = Letters + Digits
  98. macro id(e: untyped): untyped =
  99. ## generates the rss xml ``id`` element.
  100. let e = callsite()
  101. result = xmlCheckedTag(e, "id")
  102. macro updated(e: varargs[untyped]): untyped =
  103. ## generates the rss xml ``updated`` element.
  104. let e = callsite()
  105. result = xmlCheckedTag(e, "updated")
  106. proc updatedDate(year, month, day: string): string =
  107. ## wrapper around the update macro with easy input.
  108. result = updated("$1-$2-$3T00:00:00Z" % [year,
  109. repeat("0", 2 - len(month)) & month,
  110. repeat("0", 2 - len(day)) & day])
  111. macro entry(e: varargs[untyped]): untyped =
  112. ## generates the rss xml ``entry`` element.
  113. let e = callsite()
  114. result = xmlCheckedTag(e, "entry")
  115. macro content(e: varargs[untyped]): untyped =
  116. ## generates the rss xml ``content`` element.
  117. let e = callsite()
  118. result = xmlCheckedTag(e, "content", reqAttr = "type")
  119. proc parseCmdLine(c: var TConfigData) =
  120. var p = initOptParser()
  121. while true:
  122. next(p)
  123. var kind = p.kind
  124. var key = p.key
  125. var val = p.val
  126. case kind
  127. of cmdArgument:
  128. c.infile = addFileExt(key, "ini")
  129. c.nimArgs.add(cmdLineRest(p))
  130. break
  131. of cmdLongOption, cmdShortOption:
  132. case normalize(key)
  133. of "help", "h":
  134. stdout.write(usage)
  135. quit(0)
  136. of "version", "v":
  137. stdout.write(version & "\n")
  138. quit(0)
  139. of "output", "o":
  140. c.webUploadOutput = val
  141. c.docHTMLOutput = val / "docs"
  142. of "nimcompiler":
  143. c.nimCompiler = val
  144. of "parallelbuild":
  145. try:
  146. let num = parseInt(val)
  147. if num != 0: c.numProcessors = num
  148. except ValueError:
  149. quit("invalid numeric value for --parallelBuild")
  150. of "var":
  151. var idx = val.find('=')
  152. if idx < 0: quit("invalid command line")
  153. c.vars[substr(val, 0, idx-1)] = substr(val, idx+1)
  154. of "website": action = actOnlyWebsite
  155. of "pdf": action = actPdf
  156. of "json2": action = actJson2
  157. of "onlydocs": action = actOnlyDocs
  158. of "googleanalytics":
  159. c.gaId = val
  160. c.nimArgs.add("--doc.googleAnalytics:" & val & " ")
  161. of "git.url":
  162. c.gitURL = val
  163. of "git.commit":
  164. c.nimArgs.add("--git.commit:" & val & " ")
  165. of "git.devel":
  166. c.nimArgs.add("--git.devel:" & val & " ")
  167. else:
  168. echo("Invalid argument '$1'" % [key])
  169. quit(usage)
  170. of cmdEnd: break
  171. if c.infile.len == 0: quit(usage)
  172. proc walkDirRecursively(s: var seq[string], root, ext: string) =
  173. for k, f in walkDir(root):
  174. case k
  175. of pcFile, pcLinkToFile:
  176. if cmpIgnoreCase(ext, splitFile(f).ext) == 0:
  177. add(s, f)
  178. of pcDir: walkDirRecursively(s, f, ext)
  179. of pcLinkToDir: discard
  180. proc addFiles(s: var seq[string], dir, ext: string, patterns: seq[string]) =
  181. for p in items(patterns):
  182. if fileExists(dir / addFileExt(p, ext)):
  183. s.add(dir / addFileExt(p, ext))
  184. if dirExists(dir / p):
  185. walkDirRecursively(s, dir / p, ext)
  186. proc parseIniFile(c: var TConfigData) =
  187. var
  188. p: CfgParser
  189. section: string # current section
  190. var input = newFileStream(c.infile, fmRead)
  191. if input == nil: quit("cannot open: " & c.infile)
  192. open(p, input, c.infile)
  193. while true:
  194. var k = next(p)
  195. case k.kind
  196. of cfgEof: break
  197. of cfgSectionStart:
  198. section = normalize(k.section)
  199. case section
  200. of "project", "links", "tabs", "ticker", "documentation", "var": discard
  201. else: echo("[Warning] Skipping unknown section: " & section)
  202. of cfgKeyValuePair:
  203. var v = k.value % c.vars
  204. c.vars[k.key] = v
  205. case section
  206. of "project":
  207. case normalize(k.key)
  208. of "name": c.projectName = v
  209. of "title": c.projectTitle = v
  210. of "logo": c.logo = v
  211. of "authors": c.authors = v
  212. else: quit(errorStr(p, "unknown variable: " & k.key))
  213. of "var": discard
  214. of "links":
  215. let valID = v.split(';')
  216. add(c.links, (k.key.replace('_', ' '), valID[1], valID[0]))
  217. of "tabs": add(c.tabs, (k.key, "", v))
  218. of "ticker": c.ticker = v
  219. of "documentation":
  220. case normalize(k.key)
  221. of "doc": addFiles(c.doc, "doc", ".rst", split(v, {';'}))
  222. of "pdf": addFiles(c.pdf, "doc", ".rst", split(v, {';'}))
  223. of "srcdoc": addFiles(c.srcdoc, "lib", ".nim", split(v, {';'}))
  224. of "srcdoc2": addFiles(c.srcdoc2, "lib", ".nim", split(v, {';'}))
  225. of "webdoc": addFiles(c.webdoc, "lib", ".nim", split(v, {';'}))
  226. of "parallelbuild":
  227. try:
  228. let num = parseInt(v)
  229. if num != 0: c.numProcessors = num
  230. except ValueError:
  231. quit("invalid numeric value for --parallelBuild in config")
  232. else: quit(errorStr(p, "unknown variable: " & k.key))
  233. of "quotations":
  234. let vSplit = v.split('-')
  235. doAssert vSplit.len == 2
  236. c.quotations[k.key.normalize] = (vSplit[0], vSplit[1])
  237. else: discard
  238. of cfgOption: quit(errorStr(p, "syntax error"))
  239. of cfgError: quit(errorStr(p, k.msg))
  240. close(p)
  241. if c.projectName.len == 0:
  242. c.projectName = changeFileExt(extractFilename(c.infile), "")
  243. # ------------------- main ----------------------------------------------------
  244. proc exe(f: string): string = return addFileExt(f, ExeExt)
  245. proc findNim(c: TConfigData): string =
  246. if c.nimCompiler.len > 0: return c.nimCompiler
  247. var nim = "nim".exe
  248. result = "bin" / nim
  249. if fileExists(result): return
  250. for dir in split(getEnv("PATH"), PathSep):
  251. if fileExists(dir / nim): return dir / nim
  252. # assume there is a symlink to the exe or something:
  253. return nim
  254. proc exec(cmd: string) =
  255. echo(cmd)
  256. let (outp, exitCode) = osproc.execCmdEx(cmd)
  257. if exitCode != 0: quit outp
  258. proc sexec(cmds: openarray[string]) =
  259. ## Serial queue wrapper around exec.
  260. for cmd in cmds: exec(cmd)
  261. proc mexec(cmds: openarray[string], processors: int) =
  262. ## Multiprocessor version of exec
  263. doAssert processors > 0, "nimweb needs at least one processor"
  264. if processors == 1:
  265. sexec(cmds)
  266. return
  267. let r = execProcesses(cmds, {poStdErrToStdOut, poParentStreams, poEchoCmd},
  268. n = processors)
  269. if r != 0:
  270. echo "external program failed, retrying serial work queue for logs!"
  271. sexec(cmds)
  272. proc buildDocSamples(c: var TConfigData, destPath: string) =
  273. ## Special case documentation sample proc.
  274. ##
  275. ## TODO: consider integrating into the existing generic documentation builders
  276. ## now that we have a single `doc` command.
  277. exec(findNim(c) & " doc $# -o:$# $#" %
  278. [c.nimArgs, destPath / "docgen_sample.html", "doc" / "docgen_sample.nim"])
  279. proc pathPart(d: string): string = splitFile(d).dir.replace('\\', '/')
  280. proc buildDoc(c: var TConfigData, destPath: string) =
  281. # call nim for the documentation:
  282. var
  283. commands = newSeq[string](len(c.doc) + len(c.srcdoc) + len(c.srcdoc2))
  284. i = 0
  285. for d in items(c.doc):
  286. commands[i] = findNim(c) & " rst2html $# --git.url:$# -o:$# --index:on $#" %
  287. [c.nimArgs, c.gitURL,
  288. destPath / changeFileExt(splitFile(d).name, "html"), d]
  289. i.inc
  290. for d in items(c.srcdoc):
  291. commands[i] = findNim(c) & " doc0 $# --git.url:$# -o:$# --index:on $#" %
  292. [c.nimArgs, c.gitURL,
  293. destPath / changeFileExt(splitFile(d).name, "html"), d]
  294. i.inc
  295. for d in items(c.srcdoc2):
  296. commands[i] = findNim(c) & " doc $# --git.url:$# -o:$# --index:on $#" %
  297. [c.nimArgs, c.gitURL,
  298. destPath / changeFileExt(splitFile(d).name, "html"), d]
  299. i.inc
  300. mexec(commands, c.numProcessors)
  301. exec(findNim(c) & " buildIndex -o:$1/theindex.html $1" % [destPath])
  302. proc buildPdfDoc(c: var TConfigData, destPath: string) =
  303. createDir(destPath)
  304. if os.execShellCmd("pdflatex -version") != 0:
  305. echo "pdflatex not found; no PDF documentation generated"
  306. else:
  307. const pdflatexcmd = "pdflatex -interaction=nonstopmode "
  308. for d in items(c.pdf):
  309. exec(findNim(c) & " rst2tex $# $#" % [c.nimArgs, d])
  310. # call LaTeX twice to get cross references right:
  311. exec(pdflatexcmd & changeFileExt(d, "tex"))
  312. exec(pdflatexcmd & changeFileExt(d, "tex"))
  313. # delete all the crappy temporary files:
  314. let pdf = splitFile(d).name & ".pdf"
  315. let dest = destPath / pdf
  316. removeFile(dest)
  317. moveFile(dest=dest, source=pdf)
  318. removeFile(changeFileExt(pdf, "aux"))
  319. if fileExists(changeFileExt(pdf, "toc")):
  320. removeFile(changeFileExt(pdf, "toc"))
  321. removeFile(changeFileExt(pdf, "log"))
  322. removeFile(changeFileExt(pdf, "out"))
  323. removeFile(changeFileExt(d, "tex"))
  324. proc buildAddDoc(c: var TConfigData, destPath: string) =
  325. # build additional documentation (without the index):
  326. var commands = newSeq[string](c.webdoc.len)
  327. for i, doc in pairs(c.webdoc):
  328. commands[i] = findNim(c) & " doc $# --git.url:$# -o:$# $#" %
  329. [c.nimArgs, c.gitURL,
  330. destPath / changeFileExt(splitFile(doc).name, "html"), doc]
  331. mexec(commands, c.numProcessors)
  332. proc parseNewsTitles(inputFilename: string): seq[TRssItem] =
  333. # Goes through each news file, returns its date/title.
  334. result = @[]
  335. var matches: array[3, string]
  336. let reYearMonthDay = re(rYearMonthDay)
  337. for kind, path in walkDir(inputFilename):
  338. let (dir, name, ext) = path.splitFile
  339. if ext == ".rst":
  340. let content = readFile(path)
  341. let title = content.splitLines()[0]
  342. let urlPath = "news/" & name & ".html"
  343. if content.find(reYearMonthDay, matches) >= 0:
  344. result.add(TRssItem(year: matches[2], month: matches[1], day: matches[0],
  345. title: title, url: "http://nim-lang.org/" & urlPath,
  346. content: content))
  347. result.reverse()
  348. proc genUUID(text: string): string =
  349. # Returns a valid RSS uuid, which is basically md5 with dashes and a prefix.
  350. result = getMD5(text)
  351. result.insert("-", 20)
  352. result.insert("-", 16)
  353. result.insert("-", 12)
  354. result.insert("-", 8)
  355. result.insert("urn:uuid:")
  356. proc genNewsLink(title: string): string =
  357. # Mangles a title string into an expected news.html anchor.
  358. result = title
  359. result.insert("Z")
  360. for i in 1..len(result)-1:
  361. let letter = result[i].toLowerAscii()
  362. if letter in validAnchorCharacters:
  363. result[i] = letter
  364. else:
  365. result[i] = '-'
  366. result.insert(rssNewsUrl & "#")
  367. proc generateRss(outputFilename: string, news: seq[TRssItem]) =
  368. # Given a list of rss items generates an rss overwriting destination.
  369. var
  370. output: File
  371. if not open(output, outputFilename, mode = fmWrite):
  372. quit("Could not write to $1 for rss generation" % [outputFilename])
  373. defer: output.close()
  374. output.write("""<?xml version="1.0" encoding="utf-8"?>
  375. <feed xmlns="http://www.w3.org/2005/Atom">
  376. """)
  377. output.write(title("Nim website news"))
  378. output.write(link(href = rssUrl, rel = "self"))
  379. output.write(link(href = rssNewsUrl))
  380. output.write(id(rssNewsUrl))
  381. let now = utc(getTime())
  382. output.write(updatedDate($now.year, $(int(now.month) + 1), $now.monthday))
  383. for rss in news:
  384. output.write(entry(
  385. title(xmltree.escape(rss.title)),
  386. id(genUUID(rss.title)),
  387. link(`type` = "text/html", rel = "alternate",
  388. href = rss.url),
  389. updatedDate(rss.year, rss.month, rss.day),
  390. "<author><name>Nim</name></author>",
  391. content(xmltree.escape(rss.content), `type` = "text")
  392. ))
  393. output.write("""</feed>""")
  394. proc buildNewsRss(c: var TConfigData, destPath: string) =
  395. # generates an xml feed from the web/news.rst file
  396. let
  397. srcFilename = "web" / "news"
  398. destFilename = destPath / changeFileExt(splitFile(srcFilename).name, "xml")
  399. generateRss(destFilename, parseNewsTitles(srcFilename))
  400. proc readSponsors(sponsorsFile: string): seq[Sponsor] =
  401. result = @[]
  402. var fileStream = newFileStream(sponsorsFile, fmRead)
  403. if fileStream == nil: quit("Cannot open sponsors.csv file: " & sponsorsFile)
  404. var parser: CsvParser
  405. open(parser, fileStream, sponsorsFile)
  406. discard readRow(parser) # Skip the header row.
  407. while readRow(parser):
  408. result.add(Sponsor(logo: parser.row[0], name: parser.row[1],
  409. url: parser.row[2], thisMonth: parser.row[3].parseInt,
  410. allTime: parser.row[4].parseInt,
  411. since: parser.row[5], level: parser.row[6].parseInt))
  412. parser.close()
  413. proc buildSponsors(c: var TConfigData, outputDir: string) =
  414. let sponsors = generateSponsorsPage(readSponsors(activeSponsors),
  415. readSponsors(inactiveSponsors))
  416. let outFile = outputDir / "sponsors.html"
  417. var f: File
  418. if open(f, outFile, fmWrite):
  419. writeLine(f, generateHtmlPage(c, "", "Our Sponsors", sponsors, ""))
  420. close(f)
  421. else:
  422. quit("[Error] Cannot write file: " & outFile)
  423. const
  424. cmdRst2Html = " rst2html --compileonly $1 -o:web/$2.temp web/$2.rst"
  425. proc buildPage(c: var TConfigData, file, title, rss: string, assetDir = "") =
  426. exec(findNim(c) & cmdRst2Html % [c.nimArgs, file])
  427. var temp = "web" / changeFileExt(file, "temp")
  428. var content: string
  429. try:
  430. content = readFile(temp)
  431. except IOError:
  432. quit("[Error] cannot open: " & temp)
  433. var f: File
  434. var outfile = c.webUploadOutput / "$#.html" % file
  435. if not dirExists(outfile.splitFile.dir):
  436. createDir(outfile.splitFile.dir)
  437. if open(f, outfile, fmWrite):
  438. writeLine(f, generateHTMLPage(c, file, title, content, rss, assetDir))
  439. close(f)
  440. else:
  441. quit("[Error] cannot write file: " & outfile)
  442. removeFile(temp)
  443. proc buildNews(c: var TConfigData, newsDir: string, outputDir: string) =
  444. for kind, path in walkDir(newsDir):
  445. let (dir, name, ext) = path.splitFile
  446. if ext == ".rst":
  447. let title = readFile(path).splitLines()[0]
  448. buildPage(c, tailDir(dir) / name, title, "", "../")
  449. else:
  450. echo("Skipping file in news directory: ", path)
  451. proc buildWebsite(c: var TConfigData) =
  452. if c.ticker.len > 0:
  453. try:
  454. c.ticker = readFile("web" / c.ticker)
  455. except IOError:
  456. quit("[Error] cannot open: " & c.ticker)
  457. for i in 0..c.tabs.len-1:
  458. var file = c.tabs[i].val
  459. let rss = if file in ["news", "index"]: extractFilename(rssUrl) else: ""
  460. if '.' in file: continue
  461. buildPage(c, file, if file == "question": "FAQ" else: file, rss)
  462. copyDir("web/assets", c.webUploadOutput / "assets")
  463. buildNewsRss(c, c.webUploadOutput)
  464. buildSponsors(c, c.webUploadOutput)
  465. buildNews(c, "web/news", c.webUploadOutput / "news")
  466. proc onlyDocs(c: var TConfigData) =
  467. createDir(c.docHTMLOutput)
  468. buildDocSamples(c, c.docHTMLOutput)
  469. buildDoc(c, c.docHTMLOutput)
  470. proc main(c: var TConfigData) =
  471. buildWebsite(c)
  472. let docup = c.webUploadOutput / NimVersion
  473. createDir(docup)
  474. buildAddDoc(c, docup)
  475. buildDocSamples(c, docup)
  476. buildDoc(c, docup)
  477. onlyDocs(c)
  478. proc json2(c: var TConfigData) =
  479. const destPath = "web/json2"
  480. var commands = newSeq[string](c.srcdoc2.len)
  481. var i = 0
  482. for d in items(c.srcdoc2):
  483. createDir(destPath / splitFile(d).dir)
  484. commands[i] = findNim(c) & " jsondoc $# --git.url:$# -o:$# --index:on $#" %
  485. [c.nimArgs, c.gitURL,
  486. destPath / changeFileExt(d, "json"), d]
  487. i.inc
  488. mexec(commands, c.numProcessors)
  489. var c: TConfigData
  490. initConfigData(c)
  491. parseCmdLine(c)
  492. parseIniFile(c)
  493. case action
  494. of actOnlyWebsite: buildWebsite(c)
  495. of actPdf: buildPdfDoc(c, "doc/pdf")
  496. of actOnlyDocs: onlyDocs(c)
  497. of actAll: main(c)
  498. of actJson2: json2(c)