diff --git a/.eslintrc b/.eslintrc index a8f7cd0c9..3d14bbc2f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,10 @@ { "extends": "xo-space/browser", "rules": { - "semi": [2, "never"], + "semi": [ + 2, + "never" + ], "no-return-assign": "off", "no-unused-expressions": "off", "no-new-func": "off", @@ -10,11 +13,22 @@ "max-params": "off", "no-script-url": "off", "camelcase": "off", - "no-warning-comments": "off" + "object-curly-spacing": "off", + "no-warning-comments": "off", + "no-negated-condition": "off", + "eqeqeq": "warn", + "no-eq-null": "warn", + "max-statements-per-line": "warn" }, "globals": { "Docsify": true, "$docsify": true, "process": true + }, + "env": { + "browser": true, + "amd": true, + "node": true, + "jest": true } -} +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 87576c5a3..b97f8f1ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,6 @@ sudo: false language: node_js node_js: stable + +script: + - npm run lint diff --git a/docs/_media/example.js b/docs/_media/example.js index 7b6f668cc..8cad2d730 100644 --- a/docs/_media/example.js +++ b/docs/_media/example.js @@ -5,12 +5,12 @@ const PORT = 8080 /// [demo] const result = fetch(`${URL}:${PORT}`) - .then(function(response) { - return response.json(); + .then(function (response) { + return response.json() + }) + .then(function (myJson) { + console.log(JSON.stringify(myJson)) }) - .then(function(myJson) { - console.log(JSON.stringify(myJson)); - }); /// [demo] result.then(console.log).catch(console.error) diff --git a/package.json b/package.json index 9c0197143..c29c8dfff 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "serve:ssr": "cross-env SSR=1 node server", "dev": "run-p serve watch:*", "dev:ssr": "run-p serve:ssr watch:*", - "lint": "eslint {src,packages} --fix", + "lint": "eslint . --fix", "test": "mocha test/*/**", "css": "node build/css", "watch:css": "npm run css -- -o themes -w", @@ -48,8 +48,8 @@ }, "lint-staged": { "*.js": [ - "npm run lint", - "git add" + "npm run lint", + "git add" ] }, "dependencies": { @@ -98,4 +98,4 @@ "collective": { "url": "https://opencollective.com/docsify" } -} +} \ No newline at end of file diff --git a/packages/docsify-server-renderer/index.js b/packages/docsify-server-renderer/index.js index 21fb5c797..5b62effee 100644 --- a/packages/docsify-server-renderer/index.js +++ b/packages/docsify-server-renderer/index.js @@ -21,6 +21,7 @@ function mainTpl(config) { if (config.repo) { html += tpl.corner(config.repo) } + if (config.coverpage) { html += tpl.cover() } @@ -154,12 +155,14 @@ export default class Renderer { if (!res.ok) { throw Error() } + content = await res.text() this.lock = 0 } else { content = await readFileSync(filePath, 'utf8') this.lock = 0 } + return content } catch (e) { this.lock = this.lock || 0 diff --git a/src/core/config.js b/src/core/config.js index f77773726..9a3251e72 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -23,9 +23,9 @@ export default function () { ext: '.md', mergeNavbar: false, formatUpdated: '', - // this config for the links inside markdown + // This config for the links inside markdown externalLinkTarget: '_blank', - // this config for the corner + // This config for the corner cornerExternalLinkTarget: '_blank', externalLinkRel: 'noopener', routerMode: 'hash', @@ -55,15 +55,19 @@ export default function () { if (config.loadSidebar === true) { config.loadSidebar = '_sidebar' + config.ext } + if (config.loadNavbar === true) { config.loadNavbar = '_navbar' + config.ext } + if (config.coverpage === true) { config.coverpage = '_coverpage' + config.ext } + if (config.repo === true) { config.repo = '' } + if (config.name === true) { config.name = '' } diff --git a/src/core/event/scroll.js b/src/core/event/scroll.js index 3e9e33265..e1f80b77b 100644 --- a/src/core/event/scroll.js +++ b/src/core/event/scroll.js @@ -1,4 +1,4 @@ -import {isMobile} from '../util/env' +import { isMobile } from '../util/env' import * as dom from '../util/dom' import Tweezer from 'tweezer.js' @@ -12,6 +12,7 @@ function scrollTo(el) { if (scroller) { scroller.stop() } + enableScrollEvent = false scroller = new Tweezer({ start: window.pageYOffset, @@ -30,6 +31,7 @@ function highlight(path) { if (!enableScrollEvent) { return } + const sidebar = dom.getNode('.sidebar') const anchors = dom.findAll('.anchor') const wrap = dom.find(sidebar, '.sidebar-nav') @@ -45,14 +47,17 @@ function highlight(path) { if (!last) { last = node } + break } else { last = node } } + if (!last) { return } + const li = nav[getNavKey(decodeURIComponent(path), last.getAttribute('data-id'))] if (!li || li === active) { @@ -88,7 +93,7 @@ export function scrollActiveSidebar(router) { const sidebar = dom.getNode('.sidebar') let lis = [] - if (sidebar != null) { + if (sidebar !== null && sidebar !== undefined) { lis = dom.findAll(sidebar, 'li') } @@ -98,10 +103,11 @@ export function scrollActiveSidebar(router) { if (!a) { continue } + let href = a.getAttribute('href') if (href !== '/') { - const {query: {id}, path} = router.parse(href) + const { query: { id }, path } = router.parse(href) if (id) { href = getNavKey(path, id) } @@ -115,6 +121,7 @@ export function scrollActiveSidebar(router) { if (isMobile) { return } + const path = router.getCurrentPath() dom.off('scroll', () => highlight(path)) dom.on('scroll', () => highlight(path)) diff --git a/src/core/event/sidebar.js b/src/core/event/sidebar.js index 81c3003f5..8aa2bb8b8 100644 --- a/src/core/event/sidebar.js +++ b/src/core/event/sidebar.js @@ -1,17 +1,20 @@ -import {isMobile} from '../util/env' +import { isMobile } from '../util/env' import * as dom from '../util/dom' const title = dom.$.title /** * Toggle button + * @param {Element} el Button to be toggled + * @void */ export function btn(el) { const toggle = _ => dom.body.classList.toggle('close') el = dom.getNode(el) - if (el == null) { + if (el === null || el === undefined) { return } + dom.on(el, 'click', e => { e.stopPropagation() toggle() @@ -27,10 +30,11 @@ export function btn(el) { export function collapse(el) { el = dom.getNode(el) - if (el == null) { + if (el === null || el === undefined) { return } - dom.on(el, 'click', ({target}) => { + + dom.on(el, 'click', ({ target }) => { if ( target.nodeName === 'A' && target.nextSibling && @@ -46,6 +50,7 @@ export function sticky() { if (!cover) { return } + const coverHeight = cover.getBoundingClientRect().height if (window.pageYOffset >= coverHeight || cover.classList.contains('hidden')) { @@ -57,18 +62,19 @@ export function sticky() { /** * Get and active link - * @param {object} router - * @param {string|element} el - * @param {Boolean} isParent acitve parent - * @param {Boolean} autoTitle auto set title - * @return {element} + * @param {Object} router Router + * @param {String|Element} el Target element + * @param {Boolean} isParent Active parent + * @param {Boolean} autoTitle Automatically set title + * @return {Element} Active element */ export function getAndActive(router, el, isParent, autoTitle) { el = dom.getNode(el) let links = [] - if (el != null) { + if (el !== null && el !== undefined) { links = dom.findAll(el, 'a') } + const hash = decodeURI(router.toURL(router.getCurrentPath())) let target diff --git a/src/core/fetch/ajax.js b/src/core/fetch/ajax.js index 5911b7994..c70944a2e 100644 --- a/src/core/fetch/ajax.js +++ b/src/core/fetch/ajax.js @@ -1,23 +1,25 @@ import progressbar from '../render/progressbar' -import {noop, hasOwn} from '../util/core' +import { noop, hasOwn } from '../util/core' const cache = {} /** - * Simple ajax get - * @param {string} url - * @param {boolean} [hasBar=false] has progress bar - * @return { then(resolve, reject), abort } + * Ajax GET implmentation + * @param {string} url Resource URL + * @param {boolean} [hasBar=false] Has progress bar + * @param {String[]} headers Array of headers + * @return {Promise} Promise response */ export function get(url, hasBar = false, headers = {}) { const xhr = new XMLHttpRequest() const on = function () { xhr.addEventListener.apply(xhr, arguments) } + const cached = cache[url] if (cached) { - return {then: cb => cb(cached.content, cached.opt), abort: noop} + return { then: cb => cb(cached.content, cached.opt), abort: noop } } xhr.open('GET', url) @@ -26,6 +28,7 @@ export function get(url, hasBar = false, headers = {}) { xhr.setRequestHeader(i, headers[i]) } } + xhr.send() return { @@ -47,7 +50,7 @@ export function get(url, hasBar = false, headers = {}) { } on('error', error) - on('load', ({target}) => { + on('load', ({ target }) => { if (target.status >= 400) { error(target) } else { diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js index 17208df69..fb4f0d20a 100644 --- a/src/core/fetch/index.js +++ b/src/core/fetch/index.js @@ -1,8 +1,8 @@ -import {get} from './ajax' -import {callHook} from '../init/lifecycle' -import {getParentPath, stringifyQuery} from '../router/util' -import {noop} from '../util/core' -import {getAndActive} from '../event/sidebar' +import { get } from './ajax' +import { callHook } from '../init/lifecycle' +import { getParentPath, stringifyQuery } from '../router/util' +import { noop } from '../util/core' +import { getAndActive } from '../event/sidebar' function loadNested(path, qs, file, next, vm, first) { path = first ? path : path.replace(/\/$/, '') @@ -30,7 +30,7 @@ export function fetchMixin(proto) { } const get404Path = (path, config) => { - const {notFoundPage, ext} = config + const { notFoundPage, ext } = config const defaultPath = '_404' + (ext || '.md') let key let path404 @@ -75,9 +75,9 @@ export function fetchMixin(proto) { } proto._fetch = function (cb = noop) { - const {path, query} = this.route + const { path, query } = this.route const qs = stringifyQuery(query, ['id']) - const {loadNavbar, requestHeaders, loadSidebar} = this.config + const { loadNavbar, requestHeaders, loadSidebar } = this.config // Abort last request const file = this.router.getFile(path) @@ -112,7 +112,7 @@ export function fetchMixin(proto) { } proto._fetchCover = function () { - const {coverpage, requestHeaders} = this.config + const { coverpage, requestHeaders } = this.config const query = this.route.query const root = getParentPath(this.route.path) @@ -140,6 +140,7 @@ export function fetchMixin(proto) { } else { this._renderCover(null, coverOnly) } + return coverOnly } } @@ -163,7 +164,7 @@ export function fetchMixin(proto) { } proto._fetchFallbackPage = function (path, qs, cb = noop) { - const {requestHeaders, fallbackLanguages, loadSidebar} = this.config + const { requestHeaders, fallbackLanguages, loadSidebar } = this.config if (!fallbackLanguages) { return false @@ -174,6 +175,7 @@ export function fetchMixin(proto) { if (fallbackLanguages.indexOf(local) === -1) { return false } + const newPath = path.replace(new RegExp(`^/${local}`), '') const req = request(newPath + qs, true, requestHeaders) @@ -189,16 +191,17 @@ export function fetchMixin(proto) { return true } + /** * Load the 404 page - * @param path - * @param qs - * @param cb - * @returns {*} + * @param {String} path URL to be loaded + * @param {*} qs TODO: define + * @param {Function} cb Callback + * @returns {Boolean} True if the requested page is not found * @private */ proto._fetch404 = function (path, qs, cb = noop) { - const {loadSidebar, requestHeaders, notFoundPage} = this.config + const { loadSidebar, requestHeaders, notFoundPage } = this.config const fnLoadSideAndNav = this._loadSideAndNav(path, qs, loadSidebar, cb) if (notFoundPage) { @@ -217,7 +220,7 @@ export function fetchMixin(proto) { } export function initFetch(vm) { - const {loadSidebar} = vm.config + const { loadSidebar } = vm.config // Server-Side Rendering if (vm.rendered) { @@ -225,6 +228,7 @@ export function initFetch(vm) { if (loadSidebar && activeEl) { activeEl.parentNode.innerHTML += window.__SUB_SIDEBAR__ } + vm._bindEventOnRendered(activeEl) vm.$resetEvents() callHook(vm, 'doneEach') diff --git a/src/core/index.js b/src/core/index.js index ebb7fe3f2..cc00cc1f2 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -1,12 +1,15 @@ -import {initMixin} from './init' -import {routerMixin} from './router' -import {renderMixin} from './render' -import {fetchMixin} from './fetch' -import {eventMixin} from './event' +import { initMixin } from './init' +import { routerMixin } from './router' +import { renderMixin } from './render' +import { fetchMixin } from './fetch' +import { eventMixin } from './event' import initGlobalAPI from './global-api' /** * Fork https://github.com/bendrucker/document-ready/blob/master/index.js + * @param {Function} callback The callbacack to be called when the page is loaded + * @returns {Number|void} If the page is already laoded returns the result of the setTimeout callback, + * otherwise it only attaches the callback to the DOMContentLoaded event */ function ready(callback) { const state = document.readyState diff --git a/src/core/render/compiler.js b/src/core/render/compiler.js index 2f7ce1528..ff74711a3 100644 --- a/src/core/render/compiler.js +++ b/src/core/render/compiler.js @@ -1,11 +1,11 @@ import marked from 'marked' import Prism from 'prismjs' -import {helper as helperTpl, tree as treeTpl} from './tpl' -import {genTree} from './gen-tree' -import {slugify} from './slugify' -import {emojify} from './emojify' -import {isAbsolutePath, getPath, getParentPath} from '../router/util' -import {isFn, merge, cached, isPrimitive} from '../util/core' +import { helper as helperTpl, tree as treeTpl } from './tpl' +import { genTree } from './gen-tree' +import { slugify } from './slugify' +import { emojify } from './emojify' +import { isAbsolutePath, getPath, getParentPath } from '../router/util' +import { isFn, merge, cached, isPrimitive } from '../util/core' // See https://github.com/PrismJS/prism/pull/1367 import 'prismjs/components/prism-markup-templating' @@ -26,7 +26,7 @@ export function getAndRemoveConfig(str = '') { .trim() } - return {str, config} + return { str, config } } const compileMedia = { @@ -132,7 +132,7 @@ export class Compiler { } compileEmbed(href, title) { - const {str, config} = getAndRemoveConfig(title) + const { str, config } = getAndRemoveConfig(title) let embed title = str @@ -162,9 +162,11 @@ export class Compiler { } else if (/\.mp3/.test(href)) { type = 'audio' } + embed = compileMedia[type].call(this, href, title) embed.type = type } + embed.fragment = config.fragment return embed @@ -186,17 +188,20 @@ export class Compiler { _initRenderer() { const renderer = new marked.Renderer() - const {linkTarget, linkRel, router, contentBase} = this + const { linkTarget, linkRel, router, contentBase } = this const _self = this const origin = {} /** * Render anchor tag * @link https://github.com/markedjs/marked#overriding-renderer-methods + * @param {String} text Text content + * @param {Number} level Type of heading (h tag) + * @returns {String} Heading element */ origin.heading = renderer.heading = function (text, level) { - let {str, config} = getAndRemoveConfig(text) - const nextToc = {level, title: str} + let { str, config } = getAndRemoveConfig(text) + const nextToc = { level, title: str } if (/{docsify-ignore}/g.test(str)) { str = str.replace('{docsify-ignore}', '') @@ -211,12 +216,13 @@ export class Compiler { } const slug = slugify(config.id || str) - const url = router.toURL(router.getCurrentPath(), {id: slug}) + const url = router.toURL(router.getCurrentPath(), { id: slug }) nextToc.slug = url _self.toc.push(nextToc) return `${str}` } + // Highlight code origin.code = renderer.code = function (code, lang = '') { code = code.replace(/@DOCSIFY_QM@/g, '`') @@ -227,10 +233,11 @@ export class Compiler { return `
${hl}
` } + origin.link = renderer.link = function (href, title = '', text) { let attrs = '' - const {str, config} = getAndRemoveConfig(title) + const { str, config } = getAndRemoveConfig(title) title = str if ( @@ -241,6 +248,7 @@ export class Compiler { if (href === _self.config.homepage) { href = 'README' } + href = router.toURL(href, null, router.getCurrentPath()) } else { attrs += href.indexOf('mailto:') === 0 ? '' : ` target="${linkTarget}"` @@ -262,6 +270,7 @@ export class Compiler { return `${text}` } + origin.paragraph = renderer.paragraph = function (text) { let result if (/^!>/.test(text)) { @@ -271,13 +280,15 @@ export class Compiler { } else { result = `

${text}

` } + return result } + origin.image = renderer.image = function (href, title, text) { let url = href let attrs = '' - const {str, config} = getAndRemoveConfig(title) + const { str, config } = getAndRemoveConfig(title) title = str if (config['no-zoom']) { @@ -304,6 +315,7 @@ export class Compiler { return `${text}` } + origin.list = renderer.list = function (body, ordered, start) { const isTaskList = /
  • /.test(body.split('class="task-list"')[0]) const isStartReq = start && start > 1 @@ -315,6 +327,7 @@ export class Compiler { return `<${tag} ${tagAttrs}>${body}` } + origin.listitem = renderer.listitem = function (text) { const isTaskItem = /^(]*>)/.test(text) const html = isTaskItem ? `
  • ` : `
  • ${text}
  • ` @@ -329,9 +342,12 @@ export class Compiler { /** * Compile sidebar + * @param {String} text Text content + * @param {Number} level Type of heading (h tag) + * @returns {String} Sidebar element */ sidebar(text, level) { - const {toc} = this + const { toc } = this const currentPath = this.router.getCurrentPath() let html = '' @@ -346,9 +362,11 @@ export class Compiler { for (let j = i; deletedHeaderLevel < toc[j].level && j < toc.length; j++) { toc.splice(j, 1) && j-- && i++ } + i-- } } + const tree = this.cacheTree[currentPath] || genTree(toc, level) html = treeTpl(tree, '') this.cacheTree[currentPath] = tree @@ -359,14 +377,17 @@ export class Compiler { /** * Compile sub sidebar + * @param {Number} level Type of heading (h tag) + * @returns {String} Sub-sidebar element */ subSidebar(level) { if (!level) { this.toc = [] return } + const currentPath = this.router.getCurrentPath() - const {cacheTree, toc} = this + const { cacheTree, toc } = this toc[0] && toc[0].ignoreAllSubs && toc.splice(0) toc[0] && toc[0].level === 1 && toc.shift() @@ -374,6 +395,7 @@ export class Compiler { for (let i = 0; i < toc.length; i++) { toc[i].ignoreSubHeading && toc.splice(i, 1) && i-- } + const tree = cacheTree[currentPath] || genTree(toc, level) cacheTree[currentPath] = tree @@ -387,6 +409,8 @@ export class Compiler { /** * Compile cover page + * @param {Text} text Text content + * @returns {String} Cover page */ cover(text) { const cacheToc = this.toc.slice() diff --git a/src/core/render/embed.js b/src/core/render/embed.js index 821837488..eefa07bda 100644 --- a/src/core/render/embed.js +++ b/src/core/render/embed.js @@ -1,9 +1,9 @@ -import {get} from '../fetch/ajax' -import {merge} from '../util/core' +import { get } from '../fetch/ajax' +import { merge } from '../util/core' const cached = {} -function walkFetchEmbed({embedTokens, compile, fetch}, cb) { +function walkFetchEmbed({ embedTokens, compile, fetch }, cb) { let token let step = 0 let count = 1 @@ -23,26 +23,28 @@ function walkFetchEmbed({embedTokens, compile, fetch}, cb) { if (token.embed.fragment) { const fragment = token.embed.fragment const pattern = new RegExp(`(?:###|\\/\\/\\/)\\s*\\[${fragment}\\]([\\s\\S]*)(?:###|\\/\\/\\/)\\s*\\[${fragment}\\]`) - text = ((text.match(pattern) || [])[1] || '').trim() + text = ((text.match(pattern) || [])[1] || '').trim() } + embedToken = compile.lexer( '```' + - token.embed.lang + - '\n' + - text.replace(/`/g, '@DOCSIFY_QM@') + - '\n```\n' + token.embed.lang + + '\n' + + text.replace(/`/g, '@DOCSIFY_QM@') + + '\n```\n' ) } else if (token.embed.type === 'mermaid') { embedToken = [ - {type: 'html', text: `
    \n${text}\n
    `} + { type: 'html', text: `
    \n${text}\n
    ` } ] embedToken.links = {} } else { - embedToken = [{type: 'html', text}] + embedToken = [{ type: 'html', text }] embedToken.links = {} } } - cb({token, embedToken}) + + cb({ token, embedToken }) if (++count >= step) { cb({}) } @@ -61,7 +63,7 @@ function walkFetchEmbed({embedTokens, compile, fetch}, cb) { } } -export function prerenderEmbed({compiler, raw = '', fetch}, done) { +export function prerenderEmbed({ compiler, raw = '', fetch }, done) { let hit = cached[raw] if (hit) { const copy = hit.slice() @@ -96,7 +98,7 @@ export function prerenderEmbed({compiler, raw = '', fetch}, done) { }) let moveIndex = 0 - walkFetchEmbed({compile, embedTokens, fetch}, ({embedToken, token}) => { + walkFetchEmbed({ compile, embedTokens, fetch }, ({ embedToken, token }) => { if (token) { const index = token.index + moveIndex diff --git a/src/core/render/gen-tree.js b/src/core/render/gen-tree.js index 81ba4bd22..24dffe70c 100644 --- a/src/core/render/gen-tree.js +++ b/src/core/render/gen-tree.js @@ -1,9 +1,9 @@ /** * Gen toc tree * @link https://github.com/killercup/grock/blob/5280ae63e16c5739e9233d9009bc235ed7d79a50/styles/solarized/assets/js/behavior.coffee#L54-L81 - * @param {Array} toc - * @param {Number} maxLevel - * @return {Array} + * @param {Array} toc List of TOC elements + * @param {Number} maxLevel Deep level + * @return {Array} Headlines */ export function genTree(toc, maxLevel) { const headlines = [] @@ -16,11 +16,13 @@ export function genTree(toc, maxLevel) { if (level > maxLevel) { return } + if (last[len]) { last[len].children = (last[len].children || []).concat(headline) } else { headlines.push(headline) } + last[level] = headline }) diff --git a/src/core/render/index.js b/src/core/render/index.js index b4d16006d..5df3c4cd5 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -18,6 +18,7 @@ function executeScript() { if (!script) { return false } + const code = script.innerText.trim() if (!code) { return false @@ -102,6 +103,7 @@ export function renderMixin(proto) { // Reset toc this.compiler.subSidebar() } + // Bind event this._bindEventOnRendered(activeEl) } @@ -145,6 +147,7 @@ export function renderMixin(proto) { callHook(this, 'afterEach', html, text => renderMain.call(this, text)) } + if (this.isHTML) { html = this.result = text callback() @@ -173,6 +176,7 @@ export function renderMixin(proto) { dom.toggleClass(el, 'remove', 'show') return } + dom.toggleClass(el, 'add', 'show') let html = this.coverIsHTML ? text : this.compiler.cover(text) @@ -191,10 +195,12 @@ export function renderMixin(proto) { if (!isAbsolutePath(m[1])) { path = getPath(this.router.getBasePath(), m[1]) } + el.style.backgroundImage = `url(${path})` el.style.backgroundSize = 'cover' el.style.backgroundPosition = 'center center' } + html = html.replace(m[0], '') } @@ -228,6 +234,7 @@ export function initRender(vm) { if (config.repo) { html += tpl.corner(config.repo, config.cornerExternalLinkTarge) } + if (config.coverpage) { html += tpl.cover() } @@ -271,6 +278,7 @@ export function initRender(vm) { // Polyfll cssVars(config.themeColor) } + vm._updateRender() dom.toggleClass(dom.body, 'ready') } diff --git a/src/core/render/progressbar.js b/src/core/render/progressbar.js index df1546469..61a2aae22 100644 --- a/src/core/render/progressbar.js +++ b/src/core/render/progressbar.js @@ -13,6 +13,7 @@ function init() { dom.appendTo(dom.body, div) barEl = div } + /** * Render progress bar */ diff --git a/src/core/render/tpl.js b/src/core/render/tpl.js index 9e3fab126..41f63a75b 100644 --- a/src/core/render/tpl.js +++ b/src/core/render/tpl.js @@ -1,8 +1,9 @@ import { isMobile } from '../util/env' /** * Render github corner - * @param {Object} data - * @return {String} + * @param {Object} data URL for the View Source on Github link + * @param {String} cornerExternalLinkTarge value of the target attribute of the link + * @return {String} SVG element as string */ export function corner(data, cornerExternalLinkTarge) { if (!data) { @@ -29,7 +30,9 @@ export function corner(data, cornerExternalLinkTarge) { } /** - * Render main content + * Renders main content + * @param {Object} config Configuration object + * @returns {String} HTML of the main content */ export function main(config) { const name = config.name ? config.name : '' @@ -41,11 +44,11 @@ export function main(config) { '' + '' + '' @@ -60,6 +63,7 @@ export function main(config) { /** * Cover Page + * @returns {String} Cover page */ export function cover() { const SL = ', 100%, 85%' @@ -78,9 +82,9 @@ export function cover() { /** * Render tree - * @param {Array} tree - * @param {String} tpl - * @return {String} + * @param {Array} toc Array of TOC section links + * @param {String} tpl TPL list + * @return {String} Rendered tree */ export function tree(toc, tpl = '
      {inner}
    ') { if (!toc || !toc.length) { diff --git a/src/core/router/history/base.js b/src/core/router/history/base.js index 7ea763d8d..4c570df1c 100644 --- a/src/core/router/history/base.js +++ b/src/core/router/history/base.js @@ -81,6 +81,7 @@ export class History { const currentDir = currentRoute.substring(0, currentRoute.lastIndexOf('/') + 1) return cleanPath(resolvePath(currentDir + path)) } + return cleanPath('/' + path) } } diff --git a/src/core/router/history/hash.js b/src/core/router/history/hash.js index 2674d5d0d..4e1b1a361 100644 --- a/src/core/router/history/hash.js +++ b/src/core/router/history/hash.js @@ -1,7 +1,7 @@ -import {History} from './base' -import {noop} from '../../util/core' -import {on} from '../../util/dom' -import {parseQuery, cleanPath, replaceSlug} from '../util' +import { History } from './base' +import { noop } from '../../util/core' +import { on } from '../../util/dom' +import { parseQuery, cleanPath, replaceSlug } from '../util' function replaceHash(path) { const i = location.href.indexOf('#') @@ -41,12 +41,13 @@ export class HashHistory extends History { if (path.charAt(0) === '/') { return replaceHash(path) } + replaceHash('/' + path) } /** * Parse the url - * @param {string} [path=location.herf] + * @param {string} [path=location.herf] URL to be parsed * @return {object} { path, query } */ parse(path = location.href) { diff --git a/src/core/router/history/html5.js b/src/core/router/history/html5.js index 04c53919b..a3bda1391 100644 --- a/src/core/router/history/html5.js +++ b/src/core/router/history/html5.js @@ -1,7 +1,7 @@ -import {History} from './base' -import {noop} from '../../util/core' -import {on} from '../../util/dom' -import {parseQuery, getPath} from '../util' +import { History } from './base' +import { noop } from '../../util/core' +import { on } from '../../util/dom' +import { parseQuery, getPath } from '../util' export class HTML5History extends History { constructor(config) { @@ -27,7 +27,7 @@ export class HTML5History extends History { if (el.tagName === 'A' && !/_blank/.test(el.target)) { e.preventDefault() const url = el.href - window.history.pushState({key: url}, '', url) + window.history.pushState({ key: url }, '', url) cb() } }) @@ -37,7 +37,7 @@ export class HTML5History extends History { /** * Parse the url - * @param {string} [path=location.href] + * @param {string} [path=location.href] URL to be parsed * @return {object} { path, query } */ parse(path = location.href) { diff --git a/src/core/router/util.js b/src/core/router/util.js index 2ed88c58f..8b3ce7b4a 100644 --- a/src/core/router/util.js +++ b/src/core/router/util.js @@ -1,4 +1,4 @@ -import {cached} from '../util/core' +import { cached } from '../util/core' const decode = decodeURIComponent const encode = encodeURIComponent @@ -29,6 +29,7 @@ export function stringifyQuery(obj, ignores = []) { if (ignores.indexOf(key) > -1) { continue } + qs.push( obj[key] ? `${encode(key)}=${encode(obj[key])}`.toLowerCase() : @@ -44,9 +45,12 @@ export const isAbsolutePath = cached(path => { }) export const getParentPath = cached(path => { - return /\/$/g.test(path) ? - path : - (path = path.match(/(\S*\/)[^/]+$/)) ? path[1] : '' + if (/\/$/g.test(path)) { + return path + } + + const matchingParts = path.match(/(\S*\/)[^/]+$/) + return matchingParts ? matchingParts[1] : '' }) export const cleanPath = cached(path => { @@ -64,6 +68,7 @@ export const resolvePath = cached(path => { resolved.push(segment) } } + return '/' + resolved.join('/') }) diff --git a/src/core/util/core.js b/src/core/util/core.js index 0df54c9c3..2d4f20c9b 100644 --- a/src/core/util/core.js +++ b/src/core/util/core.js @@ -1,6 +1,9 @@ /** * Create a cached version of a pure function. + * @param {*} fn The function call to be cached + * @void */ + export function cached(fn) { const cache = Object.create(null) return function (str) { @@ -21,6 +24,8 @@ export const hasOwn = Object.prototype.hasOwnProperty /** * Simple Object.assign polyfill + * @param {Object} to The object to be merged with + * @returns {Object} The merged object */ export const merge = Object.assign || @@ -40,18 +45,23 @@ export const merge = /** * Check if value is primitive + * @param {*} value Checks if a value is primitive + * @returns {Boolean} Result of the check */ export function isPrimitive(value) { return typeof value === 'string' || typeof value === 'number' } /** - * Perform no operation. + * Performs no operation. + * @void */ -export function noop() {} +export function noop() { } /** * Check if value is function + * @param {*} obj Any javascript object + * @returns {Boolean} True if the passed-in value is a function */ export function isFn(obj) { return typeof obj === 'function' diff --git a/src/core/util/dom.js b/src/core/util/dom.js index 388927d11..3d529bba5 100644 --- a/src/core/util/dom.js +++ b/src/core/util/dom.js @@ -1,19 +1,20 @@ -import {isFn} from '../util/core' -import {inBrowser} from './env' +import { isFn } from '../util/core' +import { inBrowser } from './env' const cacheNode = {} /** * Get Node - * @param {String|Element} el - * @param {Boolean} noCache - * @return {Element} + * @param {String|Element} el A DOM element + * @param {Boolean} noCache Flag to use or not use the cache + * @return {Element} The found node element */ export function getNode(el, noCache = false) { if (typeof el === 'string') { if (typeof window.Vue !== 'undefined') { return find(el) } + el = noCache ? find(el) : cacheNode[el] || (cacheNode[el] = find(el)) } @@ -27,7 +28,10 @@ export const body = inBrowser && $.body export const head = inBrowser && $.head /** - * Find element + * Find elements + * @param {String|Element} el The root element where to perform the search from + * @param {Element} node The query + * @returns {Element} The found DOM element * @example * find('nav') => document.querySelector('nav') * find(nav, 'a') => nav.querySelector('a') @@ -38,6 +42,9 @@ export function find(el, node) { /** * Find all elements + * @param {String|Element} el The root element where to perform the search from + * @param {Element} node The query + * @returns {Array} An array of DOM elements * @example * findAll('a') => [].slice.call(document.querySelectorAll('a')) * findAll(nav, 'a') => [].slice.call(nav.querySelectorAll('a')) @@ -53,6 +60,7 @@ export function create(node, tpl) { if (tpl) { node.innerHTML = tpl } + return node } @@ -78,7 +86,10 @@ export function off(el, type, handler) { /** * Toggle class - * + * @param {String|Element} el The element that needs the class to be toggled + * @param {Element} type The type of action to be performed on the classList (toggle by default) + * @param {String} val Name of the class to be toggled + * @void * @example * toggleClass(el, 'active') => el.classList.toggle('active') * toggleClass(el, 'add', 'active') => el.classList.add('active') diff --git a/src/plugins/ga.js b/src/plugins/ga.js index 2d6419bbf..b327d9b5d 100644 --- a/src/plugins/ga.js +++ b/src/plugins/ga.js @@ -13,6 +13,7 @@ function init(id) { function () { (window.ga.q = window.ga.q || []).push(arguments) } + window.ga.l = Number(new Date()) window.ga('create', id, 'auto') } diff --git a/src/plugins/matomo.js b/src/plugins/matomo.js index 7b61cba7c..a27de2a06 100644 --- a/src/plugins/matomo.js +++ b/src/plugins/matomo.js @@ -9,10 +9,10 @@ function init(options) { window._paq = window._paq || [] window._paq.push(['trackPageView']) window._paq.push(['enableLinkTracking']) - setTimeout(function() { + setTimeout(function () { appendScript(options) window._paq.push(['setTrackerUrl', options.host + '/matomo.php']) - window._paq.push(['setSiteId', options.id + '']) + window._paq.push(['setSiteId', String(options.id)]) }, 0) } @@ -20,7 +20,8 @@ function collect() { if (!window._paq) { init($docsify.matomo) } - window._paq.push(['setCustomUrl', window.location.hash.substr(1)]) + + window._paq.push(['setCustomUrl', window.location.hash.substr(1)]) window._paq.push(['setDocumentTitle', document.title]) window._paq.push(['trackPageView']) } diff --git a/src/plugins/search/component.js b/src/plugins/search/component.js index 4daf5fd3e..37f964e9c 100644 --- a/src/plugins/search/component.js +++ b/src/plugins/search/component.js @@ -139,8 +139,10 @@ function doSearch(value) { $sidebarNav.classList.remove('hide') $appName.classList.remove('hide') } + return } + const matchs = search(value) let html = '' @@ -193,6 +195,7 @@ function updatePlaceholder(text, path) { if (!$input) { return } + if (typeof text === 'string') { $input.placeholder = text } else { diff --git a/src/plugins/search/search.js b/src/plugins/search/search.js index 2c9febfbc..73dbdb2c0 100644 --- a/src/plugins/search/search.js +++ b/src/plugins/search/search.js @@ -8,6 +8,7 @@ const LOCAL_STORAGE = { function resolveExpireKey(namespace) { return namespace ? `${LOCAL_STORAGE.EXPIRE_KEY}/${namespace}` : LOCAL_STORAGE.EXPIRE_KEY } + function resolveIndexKey(namespace) { return namespace ? `${LOCAL_STORAGE.INDEX_KEY}/${namespace}` : LOCAL_STORAGE.INDEX_KEY } @@ -58,14 +59,15 @@ export function genIndex(path, content = '', router, depth) { tokens.forEach(token => { if (token.type === 'heading' && token.depth <= depth) { - slug = router.toURL(path, {id: slugify(token.text)}) - index[slug] = {slug, title: token.text, body: ''} + slug = router.toURL(path, { id: slugify(token.text) }) + index[slug] = { slug, title: token.text, body: '' } } else { if (!slug) { return } + if (!index[slug]) { - index[slug] = {slug, title: '', body: ''} + index[slug] = { slug, title: '', body: '' } } else if (index[slug].body) { index[slug].body += '\n' + (token.text || '') } else { @@ -78,8 +80,8 @@ export function genIndex(path, content = '', router, depth) { } /** - * @param {String} query - * @returns {Array} + * @param {String} query Search query + * @returns {Array} Array of results */ export function search(query) { const matchingResults = [] @@ -103,12 +105,12 @@ export function search(query) { const postUrl = post.slug || '' if (postTitle) { - keywords.forEach( keyword => { + keywords.forEach(keyword => { // From https://github.com/sindresorhus/escape-string-regexp const regEx = new RegExp( keyword.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'), 'gi' - ); + ) let indexTitle = -1 let indexContent = -1 @@ -116,7 +118,7 @@ export function search(query) { indexContent = postContent ? postContent.search(regEx) : -1 if (indexTitle >= 0 || indexContent >= 0) { - matchesScore += indexTitle >= 0 ? 3 : indexContent >= 0 ? 2 : 0; + matchesScore += indexTitle >= 0 ? 3 : indexContent >= 0 ? 2 : 0 if (indexContent < 0) { indexContent = 0 } @@ -155,7 +157,7 @@ export function search(query) { } } - return matchingResults.sort((r1, r2) => r2.score - r1.score); + return matchingResults.sort((r1, r2) => r2.score - r1.score) } export function init(config, vm) { diff --git a/test/_helper.js b/test/_helper.js index 4b4abce89..b638cd819 100644 --- a/test/_helper.js +++ b/test/_helper.js @@ -1,10 +1,10 @@ -// load ES6 modules in Node.js on the fly -require = require('esm')(module/*, options*/) +// Load ES6 modules in Node.js on the fly +require = require('esm')(module/* , options */) /* eslint-disable-line no-global-assign */ const path = require('path') -const {expect} = require('chai') +const { expect } = require('chai') -const {JSDOM} = require('jsdom') +const { JSDOM } = require('jsdom') function ready(callback) { const state = document.readyState @@ -15,9 +15,10 @@ function ready(callback) { document.addEventListener('DOMContentLoaded', callback) } -module.exports.init = function(fixture = 'default', config = {}, markup) { - if (markup == null) { - markup = ` + +module.exports.init = function (fixture = 'default', config = {}, markup = null) { + if (markup === null || markup === undefined) { + markup = ` @@ -27,61 +28,63 @@ module.exports.init = function(fixture = 'default', config = {}, markup) { ` - } - const rootPath = path.join(__dirname, 'fixtures', fixture) - - const dom = new JSDOM(markup) - dom.reconfigure({ url: 'file:///' + rootPath }) - - global.window = dom.window - global.document = dom.window.document - global.navigator = dom.window.navigator - global.location = dom.window.location - global.XMLHttpRequest = dom.window.XMLHttpRequest - - // mimic src/core/index.js but for Node.js - function Docsify() { - this._init() - } - - const proto = Docsify.prototype - - const {initMixin} = require('../src/core/init') - const {routerMixin} = require('../src/core//router') - const {renderMixin} = require('../src/core//render') - const {fetchMixin} = require('../src/core/fetch') - const {eventMixin} = require('../src/core//event') - - initMixin(proto) - routerMixin(proto) - renderMixin(proto) - fetchMixin(proto) - eventMixin(proto) - - const NOT_INIT_PATTERN = '' - - return new Promise((resolve, reject) => { - ready(() => { - const docsify = new Docsify() - // NOTE: I was not able to get it working with a callback, but polling works usually at the first time - const id = setInterval(() => { - if (dom.window.document.body.innerHTML.indexOf(NOT_INIT_PATTERN) == -1) { - clearInterval(id) - return resolve({ - docsify: docsify, - dom: dom - }) - } - }, 10) - }) - - }) + } + + const rootPath = path.join(__dirname, 'fixtures', fixture) + + const dom = new JSDOM(markup) + dom.reconfigure({ url: 'file:///' + rootPath }) + + global.window = dom.window + global.document = dom.window.document + global.navigator = dom.window.navigator + global.location = dom.window.location + global.XMLHttpRequest = dom.window.XMLHttpRequest + + // Mimic src/core/index.js but for Node.js + function Docsify() { + this._init() + } + + const proto = Docsify.prototype + + const { initMixin } = require('../src/core/init') + const { routerMixin } = require('../src/core//router') + const { renderMixin } = require('../src/core//render') + const { fetchMixin } = require('../src/core/fetch') + const { eventMixin } = require('../src/core//event') + + initMixin(proto) + routerMixin(proto) + renderMixin(proto) + fetchMixin(proto) + eventMixin(proto) + + const NOT_INIT_PATTERN = '' + + return new Promise(resolve => { + ready(() => { + const docsify = new Docsify() + // NOTE: I was not able to get it working with a callback, but polling works usually at the first time + const id = setInterval(() => { + if (dom.window.document.body.innerHTML.indexOf(NOT_INIT_PATTERN) === -1) { + clearInterval(id) + return resolve({ + docsify: docsify, + dom: dom + }) + } + }, 10) + }) + }) } -module.exports.expectSameDom = function(actual, expected) { - const WHITESPACES_BETWEEN_TAGS = />(\s\s+)(\s\s+) **Time** is money, my friend!') + expect(output).equal('

    Time is money, my friend!

    ') + }) -describe('render', function() { - it('important content (tips)', async function() { - const {docsify, dom} = await init() - const output = docsify.compiler.compile('!> **Time** is money, my friend!') - expect(output).equal('

    Time is money, my friend!

    ') - }) - - describe('lists', function() { - it('as unordered task list', async function() { - const {docsify, dom} = await init() - const output = docsify.compiler.compile(` + describe('lists', function () { + it('as unordered task list', async function () { + const { docsify } = await init() + const output = docsify.compiler.compile(` - [x] Task 1 - [ ] Task 2 - [ ] Task 3`) - expect(output, `
      + expect(output, `
      `) - }) + }) - it('as ordered task list', async function() { - const {docsify, dom} = await init() - const output = docsify.compiler.compile(` + it('as ordered task list', async function () { + const { docsify } = await init() + const output = docsify.compiler.compile(` 1. [ ] Task 1 2. [x] Task 2`) - expectSameDom(output, `
        + expectSameDom(output, `
        `) - }) + }) - it('normal unordered', async function() { - const {docsify, dom} = await init() - const output = docsify.compiler.compile(` + it('normal unordered', async function () { + const { docsify } = await init() + const output = docsify.compiler.compile(` - [linktext](link) - just text`) - expectSameDom(output, `
          + expectSameDom(output, ``) - }) + }) - it('unordered with custom start', async function() { - const {docsify, dom} = await init() - const output = docsify.compiler.compile(` + it('unordered with custom start', async function () { + const { docsify } = await init() + const output = docsify.compiler.compile(` 1. first 2. second text 3. third`) - expectSameDom(output, `
            + expectSameDom(output, `
            1. first
            2. second
            @@ -64,17 +62,17 @@ text
            1. third
            `) - }) + }) - it('nested', async function() { - const {docsify, dom} = await init() - const output = docsify.compiler.compile(` + it('nested', async function () { + const { docsify } = await init() + const output = docsify.compiler.compile(` - 1 - 2 - 2 a - 2 b - 3`) - expectSameDom(output, `
              + expectSameDom(output, `
              • 1
              • 2
                • 2 a
                • @@ -83,7 +81,6 @@ text
                • 3
                `) - }) - }) - + }) + }) }) diff --git a/test/unit/util.js b/test/unit/util.js index 1e65dafe0..f4d70e8d7 100644 --- a/test/unit/util.js +++ b/test/unit/util.js @@ -1,30 +1,29 @@ -/* eslint-env node, chai, mocha */ -require = require('esm')(module/*, options*/) -const {expect} = require('chai') -const {resolvePath} = require('../../src/core/router/util') - -describe('router/util', function () { - it('resolvePath', async function () { - // WHEN - const result = resolvePath('hello.md') - - // THEN - expect(result).equal('/hello.md') - }) - - it('resolvePath with dot', async function () { - // WHEN - const result = resolvePath('./hello.md') - - // THEN - expect(result).equal('/hello.md') - }) - - it('resolvePath with two dots', async function () { - // WHEN - const result = resolvePath('test/../hello.md') - - // THEN - expect(result).equal('/hello.md') - }) -}) +require = require('esm')(module/* , options */) /* eslint-disable-line no-global-assign */ +const { expect } = require('chai') +const { resolvePath } = require('../../src/core/router/util') + +describe('router/util', function () { + it('resolvePath', async function () { + // WHEN + const result = resolvePath('hello.md') + + // THEN + expect(result).equal('/hello.md') + }) + + it('resolvePath with dot', async function () { + // WHEN + const result = resolvePath('./hello.md') + + // THEN + expect(result).equal('/hello.md') + }) + + it('resolvePath with two dots', async function () { + // WHEN + const result = resolvePath('test/../hello.md') + + // THEN + expect(result).equal('/hello.md') + }) +})