123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- const _ = require('lodash')
- const cheerio = require('cheerio')
- const uslug = require('uslug')
- const pageHelper = require('../../../helpers/page')
- const URL = require('url').URL
- const mustacheRegExp = /(\{|{?){2}(.+?)(\}|}?){2}/i
- /* global WIKI */
- module.exports = {
- async render() {
- const $ = cheerio.load(this.input, {
- decodeEntities: true
- })
- if ($.root().children().length < 1) {
- return ''
- }
- // --------------------------------
- // STEP: PRE
- // --------------------------------
- for (let child of _.reject(this.children, ['step', 'post'])) {
- const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`)
- await renderer.init($, child.config)
- }
- // --------------------------------
- // Detect internal / external links
- // --------------------------------
- let internalRefs = []
- const reservedPrefixes = /^\/[a-z]\//i
- const exactReservedPaths = /^\/[a-z]$/i
- const isHostSet = WIKI.config.host.length > 7 && WIKI.config.host !== 'http://'
- if (!isHostSet) {
- WIKI.logger.warn('Host is not set. You must set the Site Host under General in the Administration Area!')
- }
- $('a').each((i, elm) => {
- let href = $(elm).attr('href')
- // -> Ignore empty / anchor links, e-mail addresses, and telephone numbers
- if (!href || href.length < 1 || href.indexOf('#') === 0 ||
- href.indexOf('mailto:') === 0 || href.indexOf('tel:') === 0) {
- return
- }
- // -> Strip host from local links
- if (isHostSet && href.indexOf(`${WIKI.config.host}/`) === 0) {
- href = href.replace(WIKI.config.host, '')
- }
- // -> Assign local / external tag
- if (href.indexOf('://') < 0) {
- // -> Remove trailing slash
- if (_.endsWith('/')) {
- href = href.slice(0, -1)
- }
- // -> Check for system prefix
- if (reservedPrefixes.test(href) || exactReservedPaths.test(href)) {
- $(elm).addClass(`is-system-link`)
- } else if (href.indexOf('.') >= 0) {
- $(elm).addClass(`is-asset-link`)
- } else {
- let pagePath = null
- // -> Add locale prefix if using namespacing
- if (WIKI.config.lang.namespacing) {
- // -> Reformat paths
- if (href.indexOf('/') !== 0) {
- if (this.config.absoluteLinks) {
- href = `/${this.page.localeCode}/${href}`
- } else {
- href = (this.page.path === 'home') ? `/${this.page.localeCode}/${href}` : `/${this.page.localeCode}/${this.page.path}/${href}`
- }
- } else if (href.charAt(3) !== '/') {
- href = `/${this.page.localeCode}${href}`
- }
- try {
- const parsedUrl = new URL(`http://x${href}`)
- pagePath = pageHelper.parsePath(parsedUrl.pathname)
- } catch (err) {
- return
- }
- } else {
- // -> Reformat paths
- if (href.indexOf('/') !== 0) {
- if (this.config.absoluteLinks) {
- href = `/${href}`
- } else {
- href = (this.page.path === 'home') ? `/${href}` : `/${this.page.path}/${href}`
- }
- }
- try {
- const parsedUrl = new URL(`http://x${href}`)
- pagePath = pageHelper.parsePath(parsedUrl.pathname)
- } catch (err) {
- return
- }
- }
- // -> Save internal references
- internalRefs.push({
- localeCode: pagePath.locale,
- path: pagePath.path
- })
- $(elm).addClass(`is-internal-link`)
- }
- } else {
- $(elm).addClass(`is-external-link`)
- if (this.config.openExternalLinkNewTab) {
- $(elm).attr('target', '_blank')
- $(elm).attr('rel', this.config.relAttributeExternalLink)
- }
- }
- // -> Update element
- $(elm).attr('href', href)
- })
- // --------------------------------
- // Detect internal link states
- // --------------------------------
- const pastLinks = await this.page.$relatedQuery('links')
- if (internalRefs.length > 0) {
- // -> Find matching pages
- const results = await WIKI.models.pages.query().column('id', 'path', 'localeCode').where(builder => {
- internalRefs.forEach((ref, idx) => {
- if (idx < 1) {
- builder.where(ref)
- } else {
- builder.orWhere(ref)
- }
- })
- })
- // -> Apply tag to internal links for found pages
- $('a.is-internal-link').each((i, elm) => {
- const href = $(elm).attr('href')
- let hrefObj = {}
- try {
- const parsedUrl = new URL(`http://x${href}`)
- hrefObj = pageHelper.parsePath(parsedUrl.pathname)
- } catch (err) {
- return
- }
- if (_.some(results, r => {
- return r.localeCode === hrefObj.locale && r.path === hrefObj.path
- })) {
- $(elm).addClass(`is-valid-page`)
- } else {
- $(elm).addClass(`is-invalid-page`)
- }
- })
- // -> Add missing links
- const missingLinks = _.differenceWith(internalRefs, pastLinks, (nLink, pLink) => {
- return nLink.localeCode === pLink.localeCode && nLink.path === pLink.path
- })
- if (missingLinks.length > 0) {
- if (WIKI.config.db.type === 'postgres') {
- await WIKI.models.pageLinks.query().insert(missingLinks.map(lnk => ({
- pageId: this.page.id,
- path: lnk.path,
- localeCode: lnk.localeCode
- })))
- } else {
- for (const lnk of missingLinks) {
- await WIKI.models.pageLinks.query().insert({
- pageId: this.page.id,
- path: lnk.path,
- localeCode: lnk.localeCode
- })
- }
- }
- }
- }
- // -> Remove outdated links
- if (pastLinks) {
- const outdatedLinks = _.differenceWith(pastLinks, internalRefs, (nLink, pLink) => {
- return nLink.localeCode === pLink.localeCode && nLink.path === pLink.path
- })
- if (outdatedLinks.length > 0) {
- await WIKI.models.pageLinks.query().delete().whereIn('id', _.map(outdatedLinks, 'id'))
- }
- }
- // --------------------------------
- // Add header handles
- // --------------------------------
- let headers = []
- $('h1,h2,h3,h4,h5,h6').each((i, elm) => {
- let headerSlug = uslug($(elm).text())
- // -> If custom ID is defined, try to use that instead
- if ($(elm).attr('id')) {
- headerSlug = $(elm).attr('id')
- }
- // -> Cannot start with a number (CSS selector limitation)
- if (headerSlug.match(/^\d/)) {
- headerSlug = `h-${headerSlug}`
- }
- // -> Make sure header is unique
- if (headers.indexOf(headerSlug) >= 0) {
- let isUnique = false
- let hIdx = 1
- while (!isUnique) {
- const headerSlugTry = `${headerSlug}-${hIdx}`
- if (headers.indexOf(headerSlugTry) < 0) {
- isUnique = true
- headerSlug = headerSlugTry
- }
- hIdx++
- }
- }
- // -> Add anchor
- $(elm).attr('id', headerSlug).addClass('toc-header')
- $(elm).prepend(`<a class="toc-anchor" href="#${headerSlug}">¶</a> `)
- headers.push(headerSlug)
- })
- // --------------------------------
- // Wrap non-empty root text nodes
- // --------------------------------
- $('body').contents().toArray().forEach(item => {
- if (item && item.type === 'text' && item.parent.name === 'body' && item.data !== `\n` && item.data !== `\r`) {
- $(item).wrap('<div></div>')
- }
- })
- // --------------------------------
- // Escape mustache expresions
- // --------------------------------
- function iterateMustacheNode (node) {
- const list = $(node).contents().toArray()
- list.forEach(item => {
- if (item && item.type === 'text') {
- const rawText = $(item).text().replace(/\r?\n|\r/g, '')
- if (mustacheRegExp.test(rawText)) {
- $(item).parent().attr('v-pre', true)
- }
- } else {
- iterateMustacheNode(item)
- }
- })
- }
- iterateMustacheNode($.root())
- $('pre').each((idx, elm) => {
- $(elm).attr('v-pre', true)
- })
- // --------------------------------
- // STEP: POST
- // --------------------------------
- let output = decodeEscape($.html('body').replace('<body>', '').replace('</body>', ''))
- for (let child of _.sortBy(_.filter(this.children, ['step', 'post']), ['order'])) {
- const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`)
- output = await renderer.init(output, child.config)
- }
- return output
- }
- }
- function decodeEscape (string) {
- return string.replace(/&#x([0-9a-f]{1,6});/ig, (entity, code) => {
- code = parseInt(code, 16)
- // Don't unescape ASCII characters, assuming they're encoded for a good reason
- if (code < 0x80) return entity
- return String.fromCodePoint(code)
- })
- }
|