123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557 |
- #
- #
- # Nim Website Generator
- # (c) Copyright 2015 Andreas Rumpf
- #
- # See the file "copying.txt", included in this
- # distribution, for details about the copyright.
- #
- import
- os, strutils, times, parseopt, parsecfg, streams, strtabs, tables,
- re, htmlgen, macros, md5, osproc, parsecsv, algorithm
- from xmltree import escape
- type
- TKeyValPair = tuple[key, id, val: string]
- TConfigData = object of RootObj
- tabs, links: seq[TKeyValPair]
- doc, srcdoc, srcdoc2, webdoc, pdf: seq[string]
- authors, projectName, projectTitle, logo, infile, ticker: string
- vars: StringTableRef
- nimCompiler: string
- nimArgs: string
- gitURL: string
- docHTMLOutput: string
- webUploadOutput: string
- quotations: Table[string, tuple[quote, author: string]]
- numProcessors: int # Set by parallelBuild:n, only works for values > 0.
- gaId: string # google analytics ID, nil means analytics are disabled
- TRssItem = object
- year, month, day, title, url, content: string
- TAction = enum
- actAll, actOnlyWebsite, actPdf, actJson2, actOnlyDocs
- Sponsor = object
- logo: string
- name: string
- url: string
- thisMonth: int
- allTime: int
- since: string
- level: int
- var action: TAction
- proc initConfigData(c: var TConfigData) =
- c.tabs = @[]
- c.links = @[]
- c.doc = @[]
- c.srcdoc = @[]
- c.srcdoc2 = @[]
- c.webdoc = @[]
- c.pdf = @[]
- c.infile = ""
- c.nimArgs = "--hint:Conf:off --hint:Path:off --hint:Processing:off -d:boot "
- c.gitURL = "https://github.com/nim-lang/Nim"
- c.docHTMLOutput = "doc/html"
- c.webUploadOutput = "web/upload"
- c.authors = ""
- c.projectTitle = ""
- c.projectName = ""
- c.logo = ""
- c.ticker = ""
- c.vars = newStringTable(modeStyleInsensitive)
- c.numProcessors = countProcessors()
- # Attempts to obtain the git current commit.
- when false:
- let (output, code) = execCmdEx("git log -n 1 --format=%H")
- if code == 0 and output.strip.len == 40:
- c.gitCommit = output.strip
- c.quotations = initTable[string, tuple[quote, author: string]]()
- include "website.nimf"
- # ------------------------- configuration file -------------------------------
- const
- version = "0.8"
- usage = "nimweb - Nim Website Generator Version " & version & """
- (c) 2015 Andreas Rumpf
- Usage:
- nimweb [options] ini-file[.ini] [compile_options]
- Options:
- -h, --help shows this help
- -v, --version shows the version
- -o, --output overrides output directory instead of default
- web/upload and doc/html
- --nimCompiler overrides nim compiler; default = bin/nim
- --var:name=value set the value of a variable
- --website only build the website, not the full documentation
- --pdf build the PDF version of the documentation
- --json2 build JSON of the documentation
- --onlyDocs build only the documentation
- --git.url override base url in generated doc links
- --git.commit override commit/branch in generated doc links 'source'
- --git.devel override devel branch in generated doc links 'edit'
- Compile_options:
- will be passed to the Nim compiler
- """
- rYearMonthDay = r"on\s+(\d{2})\/(\d{2})\/(\d{4})"
- rssUrl = "http://nim-lang.org/news.xml"
- rssNewsUrl = "http://nim-lang.org/news.html"
- activeSponsors = "web/sponsors.csv"
- inactiveSponsors = "web/inactive_sponsors.csv"
- validAnchorCharacters = Letters + Digits
- macro id(e: untyped): untyped =
- ## generates the rss xml ``id`` element.
- let e = callsite()
- result = xmlCheckedTag(e, "id")
- macro updated(e: varargs[untyped]): untyped =
- ## generates the rss xml ``updated`` element.
- let e = callsite()
- result = xmlCheckedTag(e, "updated")
- proc updatedDate(year, month, day: string): string =
- ## wrapper around the update macro with easy input.
- result = updated("$1-$2-$3T00:00:00Z" % [year,
- repeat("0", 2 - len(month)) & month,
- repeat("0", 2 - len(day)) & day])
- macro entry(e: varargs[untyped]): untyped =
- ## generates the rss xml ``entry`` element.
- let e = callsite()
- result = xmlCheckedTag(e, "entry")
- macro content(e: varargs[untyped]): untyped =
- ## generates the rss xml ``content`` element.
- let e = callsite()
- result = xmlCheckedTag(e, "content", reqAttr = "type")
- proc parseCmdLine(c: var TConfigData) =
- var p = initOptParser()
- while true:
- next(p)
- var kind = p.kind
- var key = p.key
- var val = p.val
- case kind
- of cmdArgument:
- c.infile = addFileExt(key, "ini")
- c.nimArgs.add(cmdLineRest(p))
- break
- of cmdLongOption, cmdShortOption:
- case normalize(key)
- of "help", "h":
- stdout.write(usage)
- quit(0)
- of "version", "v":
- stdout.write(version & "\n")
- quit(0)
- of "output", "o":
- c.webUploadOutput = val
- c.docHTMLOutput = val / "docs"
- of "nimcompiler":
- c.nimCompiler = val
- of "parallelbuild":
- try:
- let num = parseInt(val)
- if num != 0: c.numProcessors = num
- except ValueError:
- quit("invalid numeric value for --parallelBuild")
- of "var":
- var idx = val.find('=')
- if idx < 0: quit("invalid command line")
- c.vars[substr(val, 0, idx-1)] = substr(val, idx+1)
- of "website": action = actOnlyWebsite
- of "pdf": action = actPdf
- of "json2": action = actJson2
- of "onlydocs": action = actOnlyDocs
- of "googleanalytics":
- c.gaId = val
- c.nimArgs.add("--doc.googleAnalytics:" & val & " ")
- of "git.url":
- c.gitURL = val
- of "git.commit":
- c.nimArgs.add("--git.commit:" & val & " ")
- of "git.devel":
- c.nimArgs.add("--git.devel:" & val & " ")
- else:
- echo("Invalid argument '$1'" % [key])
- quit(usage)
- of cmdEnd: break
- if c.infile.len == 0: quit(usage)
- proc walkDirRecursively(s: var seq[string], root, ext: string) =
- for k, f in walkDir(root):
- case k
- of pcFile, pcLinkToFile:
- if cmpIgnoreCase(ext, splitFile(f).ext) == 0:
- add(s, f)
- of pcDir: walkDirRecursively(s, f, ext)
- of pcLinkToDir: discard
- proc addFiles(s: var seq[string], dir, ext: string, patterns: seq[string]) =
- for p in items(patterns):
- if fileExists(dir / addFileExt(p, ext)):
- s.add(dir / addFileExt(p, ext))
- if dirExists(dir / p):
- walkDirRecursively(s, dir / p, ext)
- proc parseIniFile(c: var TConfigData) =
- var
- p: CfgParser
- section: string # current section
- var input = newFileStream(c.infile, fmRead)
- if input == nil: quit("cannot open: " & c.infile)
- open(p, input, c.infile)
- while true:
- var k = next(p)
- case k.kind
- of cfgEof: break
- of cfgSectionStart:
- section = normalize(k.section)
- case section
- of "project", "links", "tabs", "ticker", "documentation", "var": discard
- else: echo("[Warning] Skipping unknown section: " & section)
- of cfgKeyValuePair:
- var v = k.value % c.vars
- c.vars[k.key] = v
- case section
- of "project":
- case normalize(k.key)
- of "name": c.projectName = v
- of "title": c.projectTitle = v
- of "logo": c.logo = v
- of "authors": c.authors = v
- else: quit(errorStr(p, "unknown variable: " & k.key))
- of "var": discard
- of "links":
- let valID = v.split(';')
- add(c.links, (k.key.replace('_', ' '), valID[1], valID[0]))
- of "tabs": add(c.tabs, (k.key, "", v))
- of "ticker": c.ticker = v
- of "documentation":
- case normalize(k.key)
- of "doc": addFiles(c.doc, "doc", ".rst", split(v, {';'}))
- of "pdf": addFiles(c.pdf, "doc", ".rst", split(v, {';'}))
- of "srcdoc": addFiles(c.srcdoc, "lib", ".nim", split(v, {';'}))
- of "srcdoc2": addFiles(c.srcdoc2, "lib", ".nim", split(v, {';'}))
- of "webdoc": addFiles(c.webdoc, "lib", ".nim", split(v, {';'}))
- of "parallelbuild":
- try:
- let num = parseInt(v)
- if num != 0: c.numProcessors = num
- except ValueError:
- quit("invalid numeric value for --parallelBuild in config")
- else: quit(errorStr(p, "unknown variable: " & k.key))
- of "quotations":
- let vSplit = v.split('-')
- doAssert vSplit.len == 2
- c.quotations[k.key.normalize] = (vSplit[0], vSplit[1])
- else: discard
- of cfgOption: quit(errorStr(p, "syntax error"))
- of cfgError: quit(errorStr(p, k.msg))
- close(p)
- if c.projectName.len == 0:
- c.projectName = changeFileExt(extractFilename(c.infile), "")
- # ------------------- main ----------------------------------------------------
- proc exe(f: string): string = return addFileExt(f, ExeExt)
- proc findNim(c: TConfigData): string =
- if c.nimCompiler.len > 0: return c.nimCompiler
- var nim = "nim".exe
- result = "bin" / nim
- if fileExists(result): return
- for dir in split(getEnv("PATH"), PathSep):
- if fileExists(dir / nim): return dir / nim
- # assume there is a symlink to the exe or something:
- return nim
- proc exec(cmd: string) =
- echo(cmd)
- let (outp, exitCode) = osproc.execCmdEx(cmd)
- if exitCode != 0: quit outp
- proc sexec(cmds: openarray[string]) =
- ## Serial queue wrapper around exec.
- for cmd in cmds: exec(cmd)
- proc mexec(cmds: openarray[string], processors: int) =
- ## Multiprocessor version of exec
- doAssert processors > 0, "nimweb needs at least one processor"
- if processors == 1:
- sexec(cmds)
- return
- let r = execProcesses(cmds, {poStdErrToStdOut, poParentStreams, poEchoCmd},
- n = processors)
- if r != 0:
- echo "external program failed, retrying serial work queue for logs!"
- sexec(cmds)
- proc buildDocSamples(c: var TConfigData, destPath: string) =
- ## Special case documentation sample proc.
- ##
- ## TODO: consider integrating into the existing generic documentation builders
- ## now that we have a single `doc` command.
- exec(findNim(c) & " doc $# -o:$# $#" %
- [c.nimArgs, destPath / "docgen_sample.html", "doc" / "docgen_sample.nim"])
- proc pathPart(d: string): string = splitFile(d).dir.replace('\\', '/')
- proc buildDoc(c: var TConfigData, destPath: string) =
- # call nim for the documentation:
- var
- commands = newSeq[string](len(c.doc) + len(c.srcdoc) + len(c.srcdoc2))
- i = 0
- for d in items(c.doc):
- commands[i] = findNim(c) & " rst2html $# --git.url:$# -o:$# --index:on $#" %
- [c.nimArgs, c.gitURL,
- destPath / changeFileExt(splitFile(d).name, "html"), d]
- i.inc
- for d in items(c.srcdoc):
- commands[i] = findNim(c) & " doc0 $# --git.url:$# -o:$# --index:on $#" %
- [c.nimArgs, c.gitURL,
- destPath / changeFileExt(splitFile(d).name, "html"), d]
- i.inc
- for d in items(c.srcdoc2):
- commands[i] = findNim(c) & " doc $# --git.url:$# -o:$# --index:on $#" %
- [c.nimArgs, c.gitURL,
- destPath / changeFileExt(splitFile(d).name, "html"), d]
- i.inc
- mexec(commands, c.numProcessors)
- exec(findNim(c) & " buildIndex -o:$1/theindex.html $1" % [destPath])
- proc buildPdfDoc(c: var TConfigData, destPath: string) =
- createDir(destPath)
- if os.execShellCmd("pdflatex -version") != 0:
- echo "pdflatex not found; no PDF documentation generated"
- else:
- const pdflatexcmd = "pdflatex -interaction=nonstopmode "
- for d in items(c.pdf):
- exec(findNim(c) & " rst2tex $# $#" % [c.nimArgs, d])
- # call LaTeX twice to get cross references right:
- exec(pdflatexcmd & changeFileExt(d, "tex"))
- exec(pdflatexcmd & changeFileExt(d, "tex"))
- # delete all the crappy temporary files:
- let pdf = splitFile(d).name & ".pdf"
- let dest = destPath / pdf
- removeFile(dest)
- moveFile(dest=dest, source=pdf)
- removeFile(changeFileExt(pdf, "aux"))
- if fileExists(changeFileExt(pdf, "toc")):
- removeFile(changeFileExt(pdf, "toc"))
- removeFile(changeFileExt(pdf, "log"))
- removeFile(changeFileExt(pdf, "out"))
- removeFile(changeFileExt(d, "tex"))
- proc buildAddDoc(c: var TConfigData, destPath: string) =
- # build additional documentation (without the index):
- var commands = newSeq[string](c.webdoc.len)
- for i, doc in pairs(c.webdoc):
- commands[i] = findNim(c) & " doc $# --git.url:$# -o:$# $#" %
- [c.nimArgs, c.gitURL,
- destPath / changeFileExt(splitFile(doc).name, "html"), doc]
- mexec(commands, c.numProcessors)
- proc parseNewsTitles(inputFilename: string): seq[TRssItem] =
- # Goes through each news file, returns its date/title.
- result = @[]
- var matches: array[3, string]
- let reYearMonthDay = re(rYearMonthDay)
- for kind, path in walkDir(inputFilename):
- let (dir, name, ext) = path.splitFile
- if ext == ".rst":
- let content = readFile(path)
- let title = content.splitLines()[0]
- let urlPath = "news/" & name & ".html"
- if content.find(reYearMonthDay, matches) >= 0:
- result.add(TRssItem(year: matches[2], month: matches[1], day: matches[0],
- title: title, url: "http://nim-lang.org/" & urlPath,
- content: content))
- result.reverse()
- proc genUUID(text: string): string =
- # Returns a valid RSS uuid, which is basically md5 with dashes and a prefix.
- result = getMD5(text)
- result.insert("-", 20)
- result.insert("-", 16)
- result.insert("-", 12)
- result.insert("-", 8)
- result.insert("urn:uuid:")
- proc genNewsLink(title: string): string =
- # Mangles a title string into an expected news.html anchor.
- result = title
- result.insert("Z")
- for i in 1..len(result)-1:
- let letter = result[i].toLowerAscii()
- if letter in validAnchorCharacters:
- result[i] = letter
- else:
- result[i] = '-'
- result.insert(rssNewsUrl & "#")
- proc generateRss(outputFilename: string, news: seq[TRssItem]) =
- # Given a list of rss items generates an rss overwriting destination.
- var
- output: File
- if not open(output, outputFilename, mode = fmWrite):
- quit("Could not write to $1 for rss generation" % [outputFilename])
- defer: output.close()
- output.write("""<?xml version="1.0" encoding="utf-8"?>
- <feed xmlns="http://www.w3.org/2005/Atom">
- """)
- output.write(title("Nim website news"))
- output.write(link(href = rssUrl, rel = "self"))
- output.write(link(href = rssNewsUrl))
- output.write(id(rssNewsUrl))
- let now = utc(getTime())
- output.write(updatedDate($now.year, $(int(now.month) + 1), $now.monthday))
- for rss in news:
- output.write(entry(
- title(xmltree.escape(rss.title)),
- id(genUUID(rss.title)),
- link(`type` = "text/html", rel = "alternate",
- href = rss.url),
- updatedDate(rss.year, rss.month, rss.day),
- "<author><name>Nim</name></author>",
- content(xmltree.escape(rss.content), `type` = "text")
- ))
- output.write("""</feed>""")
- proc buildNewsRss(c: var TConfigData, destPath: string) =
- # generates an xml feed from the web/news.rst file
- let
- srcFilename = "web" / "news"
- destFilename = destPath / changeFileExt(splitFile(srcFilename).name, "xml")
- generateRss(destFilename, parseNewsTitles(srcFilename))
- proc readSponsors(sponsorsFile: string): seq[Sponsor] =
- result = @[]
- var fileStream = newFileStream(sponsorsFile, fmRead)
- if fileStream == nil: quit("Cannot open sponsors.csv file: " & sponsorsFile)
- var parser: CsvParser
- open(parser, fileStream, sponsorsFile)
- discard readRow(parser) # Skip the header row.
- while readRow(parser):
- result.add(Sponsor(logo: parser.row[0], name: parser.row[1],
- url: parser.row[2], thisMonth: parser.row[3].parseInt,
- allTime: parser.row[4].parseInt,
- since: parser.row[5], level: parser.row[6].parseInt))
- parser.close()
- proc buildSponsors(c: var TConfigData, outputDir: string) =
- let sponsors = generateSponsorsPage(readSponsors(activeSponsors),
- readSponsors(inactiveSponsors))
- let outFile = outputDir / "sponsors.html"
- var f: File
- if open(f, outFile, fmWrite):
- writeLine(f, generateHtmlPage(c, "", "Our Sponsors", sponsors, ""))
- close(f)
- else:
- quit("[Error] Cannot write file: " & outFile)
- const
- cmdRst2Html = " rst2html --compileonly $1 -o:web/$2.temp web/$2.rst"
- proc buildPage(c: var TConfigData, file, title, rss: string, assetDir = "") =
- exec(findNim(c) & cmdRst2Html % [c.nimArgs, file])
- var temp = "web" / changeFileExt(file, "temp")
- var content: string
- try:
- content = readFile(temp)
- except IOError:
- quit("[Error] cannot open: " & temp)
- var f: File
- var outfile = c.webUploadOutput / "$#.html" % file
- if not dirExists(outfile.splitFile.dir):
- createDir(outfile.splitFile.dir)
- if open(f, outfile, fmWrite):
- writeLine(f, generateHTMLPage(c, file, title, content, rss, assetDir))
- close(f)
- else:
- quit("[Error] cannot write file: " & outfile)
- removeFile(temp)
- proc buildNews(c: var TConfigData, newsDir: string, outputDir: string) =
- for kind, path in walkDir(newsDir):
- let (dir, name, ext) = path.splitFile
- if ext == ".rst":
- let title = readFile(path).splitLines()[0]
- buildPage(c, tailDir(dir) / name, title, "", "../")
- else:
- echo("Skipping file in news directory: ", path)
- proc buildWebsite(c: var TConfigData) =
- if c.ticker.len > 0:
- try:
- c.ticker = readFile("web" / c.ticker)
- except IOError:
- quit("[Error] cannot open: " & c.ticker)
- for i in 0..c.tabs.len-1:
- var file = c.tabs[i].val
- let rss = if file in ["news", "index"]: extractFilename(rssUrl) else: ""
- if '.' in file: continue
- buildPage(c, file, if file == "question": "FAQ" else: file, rss)
- copyDir("web/assets", c.webUploadOutput / "assets")
- buildNewsRss(c, c.webUploadOutput)
- buildSponsors(c, c.webUploadOutput)
- buildNews(c, "web/news", c.webUploadOutput / "news")
- proc onlyDocs(c: var TConfigData) =
- createDir(c.docHTMLOutput)
- buildDocSamples(c, c.docHTMLOutput)
- buildDoc(c, c.docHTMLOutput)
- proc main(c: var TConfigData) =
- buildWebsite(c)
- let docup = c.webUploadOutput / NimVersion
- createDir(docup)
- buildAddDoc(c, docup)
- buildDocSamples(c, docup)
- buildDoc(c, docup)
- onlyDocs(c)
- proc json2(c: var TConfigData) =
- const destPath = "web/json2"
- var commands = newSeq[string](c.srcdoc2.len)
- var i = 0
- for d in items(c.srcdoc2):
- createDir(destPath / splitFile(d).dir)
- commands[i] = findNim(c) & " jsondoc $# --git.url:$# -o:$# --index:on $#" %
- [c.nimArgs, c.gitURL,
- destPath / changeFileExt(d, "json"), d]
- i.inc
- mexec(commands, c.numProcessors)
- var c: TConfigData
- initConfigData(c)
- parseCmdLine(c)
- parseIniFile(c)
- case action
- of actOnlyWebsite: buildWebsite(c)
- of actPdf: buildPdfDoc(c, "doc/pdf")
- of actOnlyDocs: onlyDocs(c)
- of actAll: main(c)
- of actJson2: json2(c)
|