diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js index 7ee18cf8f..c23e8d56f 100644 --- a/src/core/fetch/index.js +++ b/src/core/fetch/index.js @@ -47,12 +47,10 @@ export function fetchMixin (proto) { // Load main content last.then( (text, opt) => { - this._renderMain(text, opt) - loadSideAndNav() + this._renderMain(text, opt, loadSideAndNav) }, _ => { - this._renderMain(null) - loadSideAndNav() + this._renderMain(null, {}, loadSideAndNav) } ) diff --git a/src/core/render/compiler.js b/src/core/render/compiler.js index f632dd17a..65715619a 100644 --- a/src/core/render/compiler.js +++ b/src/core/render/compiler.js @@ -5,13 +5,12 @@ import { genTree } from './gen-tree' import { slugify } from './slugify' import { emojify } from './emojify' import { isAbsolutePath, getPath } from '../router/util' -import { isFn, merge, cached } from '../util/core' +import { isFn, merge, cached, isPrimitive } from '../util/core' import { get } from '../fetch/ajax' const cachedLinks = {} -let uid = 0 -function getAndRemoveConfig (str = '') { +export function getAndRemoveConfig (str = '') { const config = {} if (str) { @@ -25,62 +24,37 @@ function getAndRemoveConfig (str = '') { return { str, config } } + const compileMedia = { markdown (url) { - const id = `docsify-get-${uid++}` - - if (!process.env.SSR) { - get(url, false).then(text => { - document.getElementById(id).innerHTML = this.compile(text) - }) - - return `
` - } else { - return ` - ` + return { + url } }, iframe (url, title) { - return `` + return { + code: `` + } }, video (url, title) { - return `` + return { + code: `` + } }, audio (url, title) { - return `` + return { + code: `` + } }, code (url, title) { - const id = `docsify-get-${uid++}` - let ext = url.match(/\.(\w+)$/) + let lang = url.match(/\.(\w+)$/) - ext = title || (ext && ext[1]) - if (ext === 'md') ext = 'markdown' - - if (!process.env.SSR) { - get(url, false).then(text => { - document.getElementById(id).innerHTML = this.compile( - '```' + ext + '\n' + text.replace(/`/g, '@qm@') + '\n```\n' - ).replace(/@qm@/g, '`') - }) + lang = title || (lang && lang[1]) + if (lang === 'md') lang = 'markdown' - return `` - } else { - return ` - ` + return { + url, + lang } } } @@ -109,12 +83,18 @@ export class Compiler { compile = marked } + this._marked = compile this.compile = cached(text => { let html = '' if (!text) return text - html = compile(text) + if (isPrimitive(text)) { + html = compile(text) + } else { + html = compile.parser(text) + } + html = config.noEmoji ? html : emojify(html) slugify.clear() @@ -122,7 +102,40 @@ export class Compiler { }) } - matchNotCompileLink (link) { + compileEmbed (href, title) { + const { str, config } = getAndRemoveConfig(title) + let embed + title = str + + if (config.include) { + if (!isAbsolutePath(href)) { + href = getPath(this.contentBase, href) + } + + let media + if (config.type && (media = compileMedia[config.type])) { + embed = media.call(this, href, title) + embed.type = config.type + } else { + let type = 'code' + if (/\.(md|markdown)/.test(href)) { + type = 'markdown' + } else if (/\.html?/.test(href)) { + type = 'iframe' + } else if (/\.(mp4|ogg)/.test(href)) { + type = 'video' + } else if (/\.mp3/.test(href)) { + type = 'audio' + } + embed = compileMedia[type].call(this, href, title) + embed.type = type + } + + return embed + } + } + + _matchNotCompileLink (link) { const links = this.config.noCompileLinks || [] for (var i = 0; i < links.length; i++) { @@ -182,33 +195,9 @@ export class Compiler { const { str, config } = getAndRemoveConfig(title) title = str - if (config.include) { - if (!isAbsolutePath(href)) { - href = getPath(contentBase, href) - } - - let media - if (config.type && (media = compileMedia[config.type])) { - return media.call(_self, href, title) - } - - let type = 'code' - if (/\.(md|markdown)/.test(href)) { - type = 'markdown' - } else if (/\.html?/.test(href)) { - type = 'iframe' - } else if (/\.(mp4|ogg)/.test(href)) { - type = 'video' - } else if (/\.mp3/.test(href)) { - type = 'audio' - } - - return compileMedia[type].call(_self, href, title) - } - if ( !/:|(\/{2})/.test(href) && - !_self.matchNotCompileLink(href) && + !_self._matchNotCompileLink(href) && !config.ignore ) { if (href === _self.config.homepage) href = 'README' diff --git a/src/core/render/embed.js b/src/core/render/embed.js new file mode 100644 index 000000000..45f95e8a1 --- /dev/null +++ b/src/core/render/embed.js @@ -0,0 +1,84 @@ +import { get } from '../fetch/ajax' +import { merge } from '../util/core' + +const cached = {} + +function walkFetchEmbed ({ step = 0, embedTokens, compile }, cb) { + const token = embedTokens[step] + + if (!token) { + return cb({}) + } + + get(token.embed.url).then(text => { + let embedToken + + if (token.embed.type === 'markdown') { + embedToken = compile.lexer(text) + } else if (token.embed.type === 'code') { + embedToken = compile.lexer( + '```' + + token.embed.lang + + '\n' + + text.replace(/`/g, '@DOCSIFY_QM@') + + '\n```\n' + ) + } + cb({ token, embedToken }) + walkFetchEmbed({ step: ++step, compile, embedTokens }, cb) + }) +} + +export function prerenderEmbed ({ compiler, raw }, done) { + let hit + if ((hit = cached[raw])) { + return done(hit) + } + + const compile = compiler._marked + let tokens = compile.lexer(raw) + const embedTokens = [] + const linkRE = compile.InlineLexer.rules.link + const links = tokens.links + + tokens.forEach((token, index) => { + if (token.type === 'paragraph') { + token.text = token.text.replace( + new RegExp(linkRE, 'g'), + (src, filename, href, title) => { + const embed = compiler.compileEmbed(href, title) + + if (embed) { + if (embed.type === 'markdown' || embed.type === 'code') { + embedTokens.push({ + index, + embed + }) + } + return embed.code + } + + return src + } + ) + } + }) + + let moveIndex = 0 + walkFetchEmbed({ compile, embedTokens }, ({ embedToken, token }) => { + if (token) { + const index = token.index + moveIndex + + merge(links, embedToken.links) + + tokens = tokens + .slice(0, index) + .concat(embedToken, tokens.slice(index + 1)) + moveIndex += embedToken.length - 1 + } else { + cached[raw] = tokens.concat() + tokens.links = cached[raw].links = links + done(tokens) + } + }) +} diff --git a/src/core/render/index.js b/src/core/render/index.js index 382387dc6..6f832938c 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -9,6 +9,7 @@ import { getPath, isAbsolutePath } from '../router/util' import { isMobile, inBrowser } from '../util/env' import { isPrimitive } from '../util/core' import { scrollActiveSidebar, scroll2Top } from '../event/scroll' +import { prerenderEmbed } from './embed' function executeScript () { const script = dom @@ -119,18 +120,37 @@ export function renderMixin (proto) { getAndActive(this.router, 'nav') } - proto._renderMain = function (text, opt = {}) { + proto._renderMain = function (text, opt = {}, next) { if (!text) { return renderMain.call(this, text) } callHook(this, 'beforeEach', text, result => { - let html = this.isHTML ? result : this.compiler.compile(result) - if (opt.updatedAt) { - html = formatUpdated(html, opt.updatedAt, this.config.formatUpdated) - } + let html + const callback = () => { + if (opt.updatedAt) { + html = formatUpdated(html, opt.updatedAt, this.config.formatUpdated) + } - callHook(this, 'afterEach', html, text => renderMain.call(this, text)) + callHook(this, 'afterEach', html, text => renderMain.call(this, text)) + } + if (this.isHTML) { + html = this.result + callback() + next() + } else { + prerenderEmbed( + { + compiler: this.compiler, + raw: text + }, + tokens => { + html = this.compiler.compile(tokens) + callback() + next() + } + ) + } }) } diff --git a/src/core/util/core.js b/src/core/util/core.js index e3a520bca..f27322906 100644 --- a/src/core/util/core.js +++ b/src/core/util/core.js @@ -4,8 +4,9 @@ export function cached (fn) { const cache = Object.create(null) return function cachedFn (str) { - const hit = cache[str] - return hit || (cache[str] = fn(str)) + const key = isPrimitive(str) ? str : JSON.stringify(str) + const hit = cache[key] + return hit || (cache[key] = fn(str)) } }