diff --git a/build/ssr.js b/build/ssr.js index 01fdd0518..032b3123a 100644 --- a/build/ssr.js +++ b/build/ssr.js @@ -1,35 +1,36 @@ -var rollup = require('rollup') -var buble = require('rollup-plugin-buble') -var async = require('rollup-plugin-async') -var replace = require('rollup-plugin-replace') +var rollup = require('rollup'); +// var buble = require('rollup-plugin-buble'); +// var async = require('rollup-plugin-async') +var replace = require('rollup-plugin-replace'); rollup .rollup({ input: 'packages/docsify-server-renderer/index.js', plugins: [ - async(), + // async(), replace({ __VERSION__: process.env.VERSION || require('../package.json').version, - 'process.env.SSR': true + 'process.env.SSR': true, }), - buble({ - transforms: { - generator: false - } - }) + // TODO restore this, for IE11. + // buble({ + // transforms: { + // generator: false, + // }, + // }), ], - onwarn: function () {} + onwarn: function() {}, }) - .then(function (bundle) { - var dest = 'packages/docsify-server-renderer/build.js' + .then(function(bundle) { + var dest = 'packages/docsify-server-renderer/build.js'; - console.log(dest) + console.log(dest); return bundle.write({ format: 'cjs', - file: dest - }) - }) - .catch(function (err) { - console.error(err) - process.exit(1) + file: dest, + }); }) + .catch(function(err) { + console.error(err); + process.exit(1); + }); diff --git a/package.json b/package.json index 8fa40d055..5431d0da6 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "pub": "sh build/release.sh", "postinstall": "opencollective-postinstall" }, - "husky": { + "husky-OFF": { "hooks": { "pre-commit": "lint-staged" } diff --git a/packages/docsify-server-renderer/default-template.html b/packages/docsify-server-renderer/default-template.html new file mode 100644 index 000000000..b6eaf01f8 --- /dev/null +++ b/packages/docsify-server-renderer/default-template.html @@ -0,0 +1,17 @@ + + + + + docsify + + + + + + + + + diff --git a/packages/docsify-server-renderer/default-template.js b/packages/docsify-server-renderer/default-template.js new file mode 100644 index 000000000..f1dc8a2d9 --- /dev/null +++ b/packages/docsify-server-renderer/default-template.js @@ -0,0 +1,8 @@ +import fs from 'fs'; +import path from 'path'; + +const tmplPath = path.resolve(__dirname, 'default-template.html'); + +export function getDefaultTemplate() { + return fs.readFileSync(tmplPath).toString(); +} diff --git a/packages/docsify-server-renderer/index.js b/packages/docsify-server-renderer/index.js index 605f2907c..f239a6ba3 100644 --- a/packages/docsify-server-renderer/index.js +++ b/packages/docsify-server-renderer/index.js @@ -9,35 +9,29 @@ import { Compiler } from '../../src/core/render/compiler'; import { isAbsolutePath } from '../../src/core/router/util'; import * as tpl from '../../src/core/render/tpl'; import { prerenderEmbed } from '../../src/core/render/embed'; +import { getDefaultTemplate } from './default-template'; + +export { getDefaultTemplate }; function cwd(...args) { return resolve(process.cwd(), ...args); } +// Borrowed from https://j11y.io/snippets/getting-a-fully-qualified-url. +function qualifyURL(url) { + // TODO this doesn't work in Node, passing in / results in /. It doesn't know the origin. Maybe we should update `location` globally first. + const img = document.createElement('img'); + img.src = url; // set string url + url = img.src; // get qualified url + img.src = ''; // prevent the server request + return url; +} + function isExternal(url) { - let match = url.match( - /^([^:\/?#]+:)?(?:\/\/([^\/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/ - ); - if ( - typeof match[1] === 'string' && - match[1].length > 0 && - match[1].toLowerCase() !== location.protocol - ) { - return true; - } - if ( - typeof match[2] === 'string' && - match[2].length > 0 && - match[2].replace( - new RegExp( - ':(' + { 'http:': 80, 'https:': 443 }[location.protocol] + ')?$' - ), - '' - ) !== location.host - ) { - return true; - } - return false; + url = qualifyURL(url); + // console.log('qualified URL:', url, location.origin); + url = new URL(url); + return url.origin !== location.origin; } function mainTpl(config) { @@ -59,12 +53,11 @@ function mainTpl(config) { } export default class Renderer { - constructor({ template, config, cache }) { + constructor({ template, config }) { this.html = template; this.config = config = Object.assign({}, config, { routerMode: 'history', }); - this.cache = cache; this.router = new AbstractHistory(config); this.compiler = new Compiler(config, this.router); @@ -208,4 +201,6 @@ export default class Renderer { } } +export { Renderer }; + Renderer.version = '__VERSION__'; diff --git a/server.js b/server.js index 2ba6c0439..e8f8d8f23 100644 --- a/server.js +++ b/server.js @@ -1,25 +1,25 @@ -const liveServer = require('live-server') -const isSSR = !!process.env.SSR -const middleware = [] +const liveServer = require('live-server'); +const isSSR = !!process.env.SSR; +const middleware = []; if (isSSR) { - const Renderer = require('./packages/docsify-server-renderer/build.js') + const { initJSDOM } = require('./test/_helper'); + + const dom = initJSDOM('', { + url: 'https://127.0.0.1:3000', + }); + + require = require('esm')(module /* , options */); + + const { + Renderer, + getDefaultTemplate, + } = require('./packages/docsify-server-renderer/index'); + + debugger; + const renderer = new Renderer({ - template: ` - - - - - docsify - - - - - - - - - `, + template: getDefaultTemplate(), config: { name: 'docsify', repo: 'docsifyjs/docsify', @@ -32,24 +32,24 @@ if (isSSR) { '/de-de/changelog': '/changelog', '/zh-cn/changelog': '/changelog', '/changelog': - 'https://raw.githubusercontent.com/docsifyjs/docsify/master/CHANGELOG' - } + 'https://raw.githubusercontent.com/docsifyjs/docsify/master/CHANGELOG', + }, }, - path: './' - }) + // path: './', // not used for anything? + }); middleware.push(function(req, res, next) { if (/\.(css|js)$/.test(req.url)) { - return next() + return next(); } - renderer.renderToString(req.url).then(html => res.end(html)) - }) + renderer.renderToString(req.url).then(html => res.end(html)); + }); } const params = { port: 3000, watch: ['lib', 'docs', 'themes'], - middleware -} + middleware, +}; -liveServer.start(params) +liveServer.start(params); diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js index 9d2769b84..0afef2a22 100644 --- a/src/core/fetch/index.js +++ b/src/core/fetch/index.js @@ -20,30 +20,19 @@ function loadNested(path, qs, file, next, vm, first) { ).then(next, _ => loadNested(path, qs, file, next, vm)); } -function isExternal(url) { - let match = url.match( - /^([^:\/?#]+:)?(?:\/\/([^\/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/ - ); - if ( - typeof match[1] === 'string' && - match[1].length > 0 && - match[1].toLowerCase() !== location.protocol - ) { - return true; - } - if ( - typeof match[2] === 'string' && - match[2].length > 0 && - match[2].replace( - new RegExp( - ':(' + { 'http:': 80, 'https:': 443 }[location.protocol] + ')?$' - ), - '' - ) !== location.host - ) { - return true; - } - return false; +// Borrowed from https://j11y.io/snippets/getting-a-fully-qualified-url. +function qualifyURL(url) { + const img = document.createElement('img'); + img.src = url; // set string url + url = img.src; // get qualified url + img.src = ''; // prevent the server request + return url; +} + +export function isExternal(url) { + url = qualifyURL(url); + url = new URL(url); + return url.origin !== location.origin; } export function fetchMixin(proto) { diff --git a/src/core/render/compiler.js b/src/core/render/compiler.js index 4eaf51215..e2c52b049 100644 --- a/src/core/render/compiler.js +++ b/src/core/render/compiler.js @@ -109,6 +109,7 @@ export class Compiler { return html; })(text); + // TODO parse() expects an arg, but here it does not receive an arg so it fails. const curFileName = this.router.parse().file; if (isCached) { diff --git a/src/core/render/index.js b/src/core/render/index.js index af1653486..3d65a5335 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-vars */ -import tinydate from 'tinydate'; import DOMPurify from 'dompurify'; +import tinydate from 'tinydate'; import * as dom from '../util/dom'; import cssVars from '../util/polyfill/css-vars'; import { callHook } from '../init/lifecycle'; @@ -9,6 +9,7 @@ import { getPath, isAbsolutePath } from '../router/util'; import { isMobile, inBrowser } from '../util/env'; import { isPrimitive } from '../util/core'; import { scrollActiveSidebar } from '../event/scroll'; +import { isExternal } from '../fetch'; import { Compiler } from './compiler'; import * as tpl from './tpl'; import { prerenderEmbed } from './embed'; @@ -259,10 +260,9 @@ export function initRender(vm) { if (config.logo) { const isBase64 = /^data:image/.test(config.logo); - const isExternal = /(?:http[s]?:)?\/\//.test(config.logo); const isRelative = /^\./.test(config.logo); - if (!isBase64 && !isExternal && !isRelative) { + if (!isBase64 && !isExternal(config.logo) && !isRelative) { config.logo = getPath(vm.router.getBasePath(), config.logo); } } diff --git a/test/_helper.js b/test/_helper.js index 39ee5bdb1..46dfbd571 100644 --- a/test/_helper.js +++ b/test/_helper.js @@ -21,8 +21,14 @@ function ready(callback) { module.exports.initJSDOM = initJSDOM; -/** @param {string} markup - The HTML document to initialize JSDOM with. */ -function initJSDOM(markup, options = {}) { +/** + * Creates a JSDOM instance and assigns the following variables to Node's + * `global`: window, document, navigator, location, XMLHttpRequest. + * + * @param {string} markup - The HTML document to initialize JSDOM with. + * @param {object} options - Options to pass to JSDOM. See https://github.com/jsdom/jsdom#customizing-jsdom + */ +function initJSDOM(markup = '', options = {}) { const dom = new JSDOM(markup, options); global.window = dom.window; diff --git a/test/unit/docsify.test.js b/test/unit/docsify.test.js index 8f095c849..42e20d03e 100644 --- a/test/unit/docsify.test.js +++ b/test/unit/docsify.test.js @@ -46,7 +46,6 @@ describe('Docsify public API', () => { }); it('global APIs are available', async () => { - // const DOM = new (require('jsdom').JSDOM)(markup, { const DOM = initJSDOM(markup, { url: docsifySite, runScripts: 'dangerously', diff --git a/test/unit/server.test.js b/test/unit/server.test.js new file mode 100644 index 000000000..183672fc8 --- /dev/null +++ b/test/unit/server.test.js @@ -0,0 +1,44 @@ +// @ts-check +/* eslint-disable no-global-assign */ +require = require('esm')( + module /* , options */ +); /* eslint-disable-line no-global-assign */ +const { expect } = require('chai'); +const { initJSDOM } = require('../_helper'); + +// const port = 9754; +// const docsifySite = 'http://127.0.0.1:' + port; + +initJSDOM(); + +const { + Renderer, + getDefaultTemplate, +} = require('../../packages/docsify-server-renderer/index'); + +describe('pacakges/docsify-server-render', function() { + it('renders content', async function() { + const renderer = new Renderer({ + template: getDefaultTemplate(), + config: { + name: 'docsify', + repo: 'docsifyjs/docsify', + // basePath: 'https://docsify.js.org/', + loadNavbar: true, + loadSidebar: true, + subMaxLevel: 3, + auto2top: true, + alias: { + '/de-de/changelog': '/changelog', + '/zh-cn/changelog': '/changelog', + '/changelog': + 'https://raw.githubusercontent.com/docsifyjs/docsify/master/CHANGELOG', + }, + }, + }); + + await renderer.renderToString('/changelog'); + + expect(renderer).to.be.an.instanceof(Renderer); + }); +});