renderer.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. const _ = require('lodash')
  2. const cheerio = require('cheerio')
  3. const uslug = require('uslug')
  4. const pageHelper = require('../../../helpers/page')
  5. const URL = require('url').URL
  6. /* global WIKI */
  7. module.exports = {
  8. async render() {
  9. const $ = cheerio.load(this.input)
  10. if ($.root().children().length < 1) {
  11. return ''
  12. }
  13. for (let child of this.children) {
  14. const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`)
  15. renderer.init($, child.config)
  16. }
  17. // --------------------------------
  18. // Detect internal / external links
  19. // --------------------------------
  20. let internalRefs = []
  21. const reservedPrefixes = /^\/[a-z]\//gi
  22. const exactReservedPaths = /^\/[a-z]$/gi
  23. const isHostSet = WIKI.config.host.length > 7 && WIKI.config.host !== 'http://'
  24. if (!isHostSet) {
  25. WIKI.logger.warn('Host is not set. You must set the Site Host under General in the Administration Area!')
  26. }
  27. $('a').each((i, elm) => {
  28. let href = $(elm).attr('href')
  29. // -> Ignore empty / anchor links
  30. if (!href || href.length < 1 || href.indexOf('#') === 0 || href.indexOf('mailto:') === 0) {
  31. return
  32. }
  33. // -> Strip host from local links
  34. if (isHostSet && href.indexOf(WIKI.config.host) === 0) {
  35. href = href.replace(WIKI.config.host, '')
  36. }
  37. // -> Assign local / external tag
  38. if (href.indexOf('://') < 0) {
  39. // -> Remove trailing slash
  40. if (_.endsWith('/')) {
  41. href = href.slice(0, -1)
  42. }
  43. // -> Check for system prefix
  44. if (reservedPrefixes.test(href) || exactReservedPaths.test(href)) {
  45. $(elm).addClass(`is-system-link`)
  46. } else if (href.indexOf('.') >= 0) {
  47. $(elm).addClass(`is-asset-link`)
  48. } else {
  49. let pagePath = null
  50. // -> Add locale prefix if using namespacing
  51. if (WIKI.config.lang.namespacing) {
  52. // -> Reformat paths
  53. if (href.indexOf('/') !== 0) {
  54. href = (this.page.path === 'home') ? `/${this.page.localeCode}/${href}` : `/${this.page.localeCode}/${this.page.path}/${href}`
  55. } else if (href.charAt(3) !== '/') {
  56. href = `/${this.page.localeCode}${href}`
  57. }
  58. try {
  59. const parsedUrl = new URL(`http://x${href}`)
  60. pagePath = pageHelper.parsePath(parsedUrl.pathname)
  61. } catch (err) {
  62. return
  63. }
  64. } else {
  65. // -> Reformat paths
  66. if (href.indexOf('/') !== 0) {
  67. href = (this.page.path === 'home') ? `/${href}` : `/${this.page.path}/${href}`
  68. }
  69. try {
  70. const parsedUrl = new URL(`http://x${href}`)
  71. pagePath = pageHelper.parsePath(parsedUrl.pathname)
  72. } catch (err) {
  73. return
  74. }
  75. }
  76. // -> Save internal references
  77. internalRefs.push({
  78. localeCode: pagePath.locale,
  79. path: pagePath.path
  80. })
  81. $(elm).addClass(`is-internal-link`)
  82. }
  83. } else {
  84. $(elm).addClass(`is-external-link`)
  85. }
  86. // -> Update element
  87. $(elm).attr('href', href)
  88. })
  89. // --------------------------------
  90. // Detect internal link states
  91. // --------------------------------
  92. const pastLinks = await this.page.$relatedQuery('links')
  93. if (internalRefs.length > 0) {
  94. // -> Find matching pages
  95. const results = await WIKI.models.pages.query().column('id', 'path', 'localeCode').where(builder => {
  96. internalRefs.forEach((ref, idx) => {
  97. if (idx < 1) {
  98. builder.where(ref)
  99. } else {
  100. builder.orWhere(ref)
  101. }
  102. })
  103. })
  104. // -> Apply tag to internal links for found pages
  105. $('a.is-internal-link').each((i, elm) => {
  106. const href = $(elm).attr('href')
  107. let hrefObj = {}
  108. try {
  109. const parsedUrl = new URL(`http://x${href}`)
  110. hrefObj = pageHelper.parsePath(parsedUrl.pathname)
  111. } catch (err) {
  112. return
  113. }
  114. if (_.some(results, r => {
  115. return r.localeCode === hrefObj.locale && r.path === hrefObj.path
  116. })) {
  117. $(elm).addClass(`is-valid-page`)
  118. } else {
  119. $(elm).addClass(`is-invalid-page`)
  120. }
  121. })
  122. // -> Add missing links
  123. const missingLinks = _.differenceWith(internalRefs, pastLinks, (nLink, pLink) => {
  124. return nLink.localeCode === pLink.localeCode && nLink.path === pLink.path
  125. })
  126. if (missingLinks.length > 0) {
  127. if (WIKI.config.db.type === 'postgres') {
  128. await WIKI.models.pageLinks.query().insert(missingLinks.map(lnk => ({
  129. pageId: this.page.id,
  130. path: lnk.path,
  131. localeCode: lnk.localeCode
  132. })))
  133. } else {
  134. for (const lnk of missingLinks) {
  135. await WIKI.models.pageLinks.query().insert({
  136. pageId: this.page.id,
  137. path: lnk.path,
  138. localeCode: lnk.localeCode
  139. })
  140. }
  141. }
  142. }
  143. }
  144. // -> Remove outdated links
  145. if (pastLinks) {
  146. const outdatedLinks = _.differenceWith(pastLinks, internalRefs, (nLink, pLink) => {
  147. return nLink.localeCode === pLink.localeCode && nLink.path === pLink.path
  148. })
  149. if (outdatedLinks.length > 0) {
  150. await WIKI.models.pageLinks.query().delete().whereIn('id', _.map(outdatedLinks, 'id'))
  151. }
  152. }
  153. // --------------------------------
  154. // Add header handles
  155. // --------------------------------
  156. let headers = []
  157. $('h1,h2,h3,h4,h5,h6').each((i, elm) => {
  158. if ($(elm).attr('id')) {
  159. return
  160. }
  161. let headerSlug = uslug($(elm).text())
  162. // -> Cannot start with a number (CSS selector limitation)
  163. if (headerSlug.match(/^\d/)) {
  164. headerSlug = `h-${headerSlug}`
  165. }
  166. // -> Make sure header is unique
  167. if (headers.indexOf(headerSlug) >= 0) {
  168. let isUnique = false
  169. let hIdx = 1
  170. while (!isUnique) {
  171. const headerSlugTry = `${headerSlug}-${hIdx}`
  172. if (headers.indexOf(headerSlugTry) < 0) {
  173. isUnique = true
  174. headerSlug = headerSlugTry
  175. }
  176. hIdx++
  177. }
  178. }
  179. // -> Add anchor
  180. $(elm).attr('id', headerSlug).addClass('toc-header')
  181. $(elm).prepend(`<a class="toc-anchor" href="#${headerSlug}">&#xB6;</a> `)
  182. headers.push(headerSlug)
  183. })
  184. return $.html('body').replace('<body>', '').replace('</body>', '')
  185. }
  186. }