diff --git a/Makefile b/Makefile index d17930776f..7cd8598370 100644 --- a/Makefile +++ b/Makefile @@ -300,6 +300,11 @@ test-v8 test-v8-intl test-v8-benchmarks test-v8-all: "$ git clone https://github.com/nodejs/node.git" endif +# Google Analytics ID used for tracking API docs page views, empty +# DOCS_ANALYTICS means no tracking scripts will be included in the +# generated .html files +DOCS_ANALYTICS ?= + apidoc_sources = $(wildcard doc/api/*.md) apidocs_html = $(apidoc_dirs) $(apiassets) $(addprefix out/,$(apidoc_sources:.md=.html)) apidocs_json = $(apidoc_dirs) $(apiassets) $(addprefix out/,$(apidoc_sources:.md=.json)) @@ -333,7 +338,8 @@ out/doc/api/%.json: doc/api/%.md [ -x $(NODE) ] && $(NODE) $(gen-json) || node $(gen-json) # check if ./node is actually set, else use user pre-installed binary -gen-html = tools/doc/generate.js --node-version=$(FULLVERSION) --format=html --template=doc/template.html $< > $@ +gen-html = tools/doc/generate.js --node-version=$(FULLVERSION) --format=html \ + --template=doc/template.html --analytics=$(DOCS_ANALYTICS) $< > $@ out/doc/api/%.html: doc/api/%.md @[ -e tools/doc/node_modules/js-yaml/package.json ] || \ [ -e tools/eslint/node_modules/js-yaml/package.json ] || \ @@ -587,7 +593,7 @@ ifeq ($(XZ), 0) ssh $(STAGINGSERVER) "touch nodejs/$(DISTTYPEDIR)/$(FULLVERSION)/node-$(FULLVERSION).tar.xz.done" endif -doc-upload: tar +doc-upload: doc ssh $(STAGINGSERVER) "mkdir -p nodejs/$(DISTTYPEDIR)/$(FULLVERSION)" chmod -R ug=rw-x+X,o=r+X out/doc/ scp -pr out/doc/ $(STAGINGSERVER):nodejs/$(DISTTYPEDIR)/$(FULLVERSION)/docs/ diff --git a/doc/api_assets/dnt_helper.js b/doc/api_assets/dnt_helper.js new file mode 100644 index 0000000000..f255d916c2 --- /dev/null +++ b/doc/api_assets/dnt_helper.js @@ -0,0 +1,49 @@ +/** + * http://schalkneethling.github.io/blog/2015/11/06/respect-user-choice-do-not-track/ + * https://github.com/schalkneethling/dnt-helper/blob/master/js/dnt-helper.js + * + * Returns true or false based on whether doNotTack is enabled. It also takes into account the + * anomalies, such as !bugzilla 887703, which effect versions of Fx 31 and lower. It also handles + * IE versions on Windows 7, 8 and 8.1, where the DNT implementation does not honor the spec. + * @see https://bugzilla.mozilla.org/show_bug.cgi?id=1217896 for more details + * @params {string} [dnt] - An optional mock doNotTrack string to ease unit testing. + * @params {string} [userAgent] - An optional mock userAgent string to ease unit testing. + * @returns {boolean} true if enabled else false + */ +function _dntEnabled(dnt, userAgent) { + + 'use strict'; + + // for old version of IE we need to use the msDoNotTrack property of navigator + // on newer versions, and newer platforms, this is doNotTrack but, on the window object + // Safari also exposes the property on the window object. + var dntStatus = dnt || navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack; + var ua = userAgent || navigator.userAgent; + + // List of Windows versions known to not implement DNT according to the standard. + var anomalousWinVersions = ['Windows NT 6.1', 'Windows NT 6.2', 'Windows NT 6.3']; + + var fxMatch = ua.match(/Firefox\/(\d+)/); + var ieRegEx = /MSIE|Trident/i; + var isIE = ieRegEx.test(ua); + // Matches from Windows up to the first occurance of ; un-greedily + // http://www.regexr.com/3c2el + var platform = ua.match(/Windows.+?(?=;)/g); + + // With old versions of IE, DNT did not exist so we simply return false; + if (isIE && typeof Array.prototype.indexOf !== 'function') { + return false; + } else if (fxMatch && parseInt(fxMatch[1], 10) < 32) { + // Can't say for sure if it is 1 or 0, due to Fx bug 887703 + dntStatus = 'Unspecified'; + } else if (isIE && platform && anomalousWinVersions.indexOf(platform.toString()) !== -1) { + // default is on, which does not honor the specification + dntStatus = 'Unspecified'; + } else { + // sets dntStatus to Disabled or Enabled based on the value returned by the browser. + // If dntStatus is undefined, it will be set to Unspecified + dntStatus = { '0': 'Disabled', '1': 'Enabled' }[dntStatus] || 'Unspecified'; + } + + return dntStatus === 'Enabled' ? true : false; +} diff --git a/doc/template.html b/doc/template.html index af680645d1..572197beff 100644 --- a/doc/template.html +++ b/doc/template.html @@ -45,5 +45,6 @@ + diff --git a/test/doctool/test-doctool-html.js b/test/doctool/test-doctool-html.js index e5c825aebb..442381b54d 100644 --- a/test/doctool/test-doctool-html.js +++ b/test/doctool/test-doctool-html.js @@ -72,11 +72,18 @@ const testData = [ '

I exist and am being linked to.

' + '' }, + { + file: path.join(common.fixturesDir, 'sample_document.md'), + html: '
  1. fish
  2. fish

  3. Redfish

  4. ' + + '
  5. Bluefish
', + analyticsId: 'UA-67020396-1' + }, ]; testData.forEach((item) => { // Normalize expected data by stripping whitespace const expected = item.html.replace(/\s/g, ''); + const includeAnalytics = typeof item.analyticsId !== 'undefined'; fs.readFile(item.file, 'utf8', common.mustCall((err, input) => { assert.ifError(err); @@ -89,6 +96,7 @@ testData.forEach((item) => { filename: 'foo', template: 'doc/template.html', nodeVersion: process.version, + analytics: item.analyticsId, }, common.mustCall((err, output) => { assert.ifError(err); @@ -97,6 +105,16 @@ testData.forEach((item) => { // Assert that the input stripped of all whitespace contains the // expected list assert.notStrictEqual(actual.indexOf(expected), -1); + + // Testing the insertion of Google Analytics script when + // an analytics id is provided. Should not be present by default + if (includeAnalytics) { + assert.notStrictEqual(actual.indexOf('google-analytics.com'), -1, + 'Google Analytics script was not present'); + } else { + assert.strictEqual(actual.indexOf('google-analytics.com'), -1, + 'Google Analytics script was present'); + } })); })); })); diff --git a/tools/doc/generate.js b/tools/doc/generate.js index 31b23c52a0..b7fcf0d4f9 100644 --- a/tools/doc/generate.js +++ b/tools/doc/generate.js @@ -11,16 +11,19 @@ let format = 'json'; let template = null; let inputFile = null; let nodeVersion = null; +let analytics = null; args.forEach(function(arg) { - if (!arg.match(/^--/)) { + if (!arg.startsWith('--')) { inputFile = arg; - } else if (arg.match(/^--format=/)) { + } else if (arg.startsWith('--format=')) { format = arg.replace(/^--format=/, ''); - } else if (arg.match(/^--template=/)) { + } else if (arg.startsWith('--template=')) { template = arg.replace(/^--template=/, ''); - } else if (arg.match(/^--node-version=/)) { + } else if (arg.startsWith('--node-version=')) { nodeVersion = arg.replace(/^--node-version=/, ''); + } else if (arg.startsWith('--analytics=')) { + analytics = arg.replace(/^--analytics=/, ''); } }); @@ -54,6 +57,7 @@ function next(er, input) { filename: inputFile, template: template, nodeVersion: nodeVersion, + analytics: analytics, }, function(er, html) { diff --git a/tools/doc/html.js b/tools/doc/html.js index c0790ffca5..4b1c0a0e69 100644 --- a/tools/doc/html.js +++ b/tools/doc/html.js @@ -67,6 +67,7 @@ function toHTML(opts, cb) { filename: opts.filename, template: template, nodeVersion: nodeVersion, + analytics: opts.analytics, }, cb); }); } @@ -128,6 +129,13 @@ function render(opts, cb) { gtocData.replace('class="nav-' + id, 'class="nav-' + id + ' active') ); + if (opts.analytics) { + template = template.replace( + '', + analyticsScript(opts.analytics) + ); + } + // content has to be the last thing we do with // the lexed tokens, because it's destructive. const content = marked.parser(lexed); @@ -137,6 +145,23 @@ function render(opts, cb) { }); } +function analyticsScript(analytics) { + return ` + + + `; +} + // handle general body-text replacements // for example, link man page references to the actual page function parseText(lexed) {