renderer.js 6.9 KB

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