Developing a legal IDE based on Atom
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

245 lines
6.8 KiB

  1. const { TextEditor } = require('atom')
  2. const path = require('path')
  3. const createDOMPurify = require('dompurify')
  4. const emoji = require('emoji-images')
  5. const fs = require('fs-plus')
  6. let commonmark = null // Defer until used
  7. let renderer = null
  8. let cheerio = null
  9. let yamlFrontMatter = null
  10. const { scopeForFenceName } = require('./extension-helper')
  11. const { resourcePath } = atom.getLoadSettings()
  12. const packagePath = path.dirname(__dirname)
  13. const emojiFolder = path.join(
  14. path.dirname(require.resolve('emoji-images')),
  15. 'pngs'
  16. )
  17. exports.toDOMFragment = async function (text, filePath, grammar, callback) {
  18. if (text == null) {
  19. text = ''
  20. }
  21. const domFragment = render(text, filePath)
  22. await highlightCodeBlocks(domFragment, grammar, makeAtomEditorNonInteractive)
  23. return domFragment
  24. }
  25. exports.toHTML = async function (text, filePath, grammar) {
  26. if (text == null) {
  27. text = ''
  28. }
  29. const domFragment = render(text, filePath)
  30. const div = document.createElement('div')
  31. div.appendChild(domFragment)
  32. document.body.appendChild(div)
  33. await highlightCodeBlocks(div, grammar, convertAtomEditorToStandardElement)
  34. const result = div.innerHTML
  35. div.remove()
  36. return result
  37. }
  38. var render = function (text, filePath) {
  39. if (commonmark == null || yamlFrontMatter == null || cheerio == null) {
  40. commonmark = require('commonmark')
  41. yamlFrontMatter = require('yaml-front-matter')
  42. cheerio = require('cheerio')
  43. renderer = new commonmark.Renderer()
  44. renderer.listitem = function (text, isTask) {
  45. const listAttributes = isTask ? ' class="task-list-item"' : ''
  46. return `<li ${listAttributes}>${text}</li>\n`
  47. }
  48. }
  49. // commonmark.setOptions({
  50. // sanitize: false,
  51. // breaks: atom.config.get('markdown-preview.breakOnSingleNewline'),
  52. // renderer
  53. // })
  54. const { __content, ...vars } = yamlFrontMatter.loadFront(text)
  55. const parser = new commonmark.Parser()
  56. const writer = new commonmark.HtmlRenderer()
  57. const parsed = parser.parse(renderYamlTable(vars) + __content)
  58. let html = writer.render(parsed)
  59. // emoji-images is too aggressive, so replace images in monospace tags with the actual emoji text.
  60. const $ = cheerio.load(emoji(html, emojiFolder, 20))
  61. $('pre img').each((index, element) =>
  62. $(element).replaceWith($(element).attr('title'))
  63. )
  64. $('code img').each((index, element) =>
  65. $(element).replaceWith($(element).attr('title'))
  66. )
  67. html = $.html()
  68. html = createDOMPurify().sanitize(html, {
  69. ALLOW_UNKNOWN_PROTOCOLS: atom.config.get(
  70. 'markdown-preview.allowUnsafeProtocols'
  71. )
  72. })
  73. const template = document.createElement('template')
  74. template.innerHTML = html.trim()
  75. const fragment = template.content.cloneNode(true)
  76. resolveImagePaths(fragment, filePath)
  77. return fragment
  78. }
  79. function renderYamlTable (variables) {
  80. const entries = Object.entries(variables)
  81. if (!entries.length) {
  82. return ''
  83. }
  84. const markdownRows = [
  85. entries.map(entry => entry[0]),
  86. entries.map(entry => '--'),
  87. entries.map(entry => entry[1])
  88. ]
  89. return (
  90. markdownRows.map(row => '| ' + row.join(' | ') + ' |').join('\n') + '\n'
  91. )
  92. }
  93. var resolveImagePaths = function (element, filePath) {
  94. const [rootDirectory] = atom.project.relativizePath(filePath)
  95. const result = []
  96. for (const img of element.querySelectorAll('img')) {
  97. // We use the raw attribute instead of the .src property because the value
  98. // of the property seems to be transformed in some cases.
  99. let src
  100. if ((src = img.getAttribute('src'))) {
  101. if (src.match(/^(https?|atom):\/\//)) {
  102. continue
  103. }
  104. if (src.startsWith(process.resourcesPath)) {
  105. continue
  106. }
  107. if (src.startsWith(resourcePath)) {
  108. continue
  109. }
  110. if (src.startsWith(packagePath)) {
  111. continue
  112. }
  113. if (src[0] === '/') {
  114. if (!fs.isFileSync(src)) {
  115. if (rootDirectory) {
  116. result.push((img.src = path.join(rootDirectory, src.substring(1))))
  117. } else {
  118. result.push(undefined)
  119. }
  120. } else {
  121. result.push(undefined)
  122. }
  123. } else {
  124. result.push((img.src = path.resolve(path.dirname(filePath), src)))
  125. }
  126. } else {
  127. result.push(undefined)
  128. }
  129. }
  130. return result
  131. }
  132. var highlightCodeBlocks = function (domFragment, grammar, editorCallback) {
  133. let defaultLanguage, fontFamily
  134. if (
  135. (grammar != null ? grammar.scopeName : undefined) === 'source.litcoffee'
  136. ) {
  137. defaultLanguage = 'coffee'
  138. } else {
  139. defaultLanguage = 'text'
  140. }
  141. if ((fontFamily = atom.config.get('editor.fontFamily'))) {
  142. for (const codeElement of domFragment.querySelectorAll('code')) {
  143. codeElement.style.fontFamily = fontFamily
  144. }
  145. }
  146. const promises = []
  147. for (const preElement of domFragment.querySelectorAll('pre')) {
  148. const codeBlock =
  149. preElement.firstElementChild != null
  150. ? preElement.firstElementChild
  151. : preElement
  152. const className = codeBlock.getAttribute('class')
  153. const fenceName =
  154. className != null ? className.replace(/^language-/, '') : defaultLanguage
  155. const editor = new TextEditor({
  156. readonly: true,
  157. keyboardInputEnabled: false
  158. })
  159. const editorElement = editor.getElement()
  160. preElement.classList.add('editor-colors', `lang-${fenceName}`)
  161. editorElement.setUpdatedSynchronously(true)
  162. preElement.innerHTML = ''
  163. preElement.parentNode.insertBefore(editorElement, preElement)
  164. editor.setText(codeBlock.textContent.replace(/\r?\n$/, ''))
  165. atom.grammars.assignLanguageMode(editor, scopeForFenceName(fenceName))
  166. editor.setVisible(true)
  167. promises.push(editorCallback(editorElement, preElement))
  168. }
  169. return Promise.all(promises)
  170. }
  171. var makeAtomEditorNonInteractive = function (editorElement, preElement) {
  172. preElement.remove()
  173. editorElement.setAttributeNode(document.createAttribute('gutter-hidden')) // Hide gutter
  174. editorElement.removeAttribute('tabindex') // Make read-only
  175. // Remove line decorations from code blocks.
  176. for (const cursorLineDecoration of editorElement.getModel()
  177. .cursorLineDecorations) {
  178. cursorLineDecoration.destroy()
  179. }
  180. }
  181. var convertAtomEditorToStandardElement = (editorElement, preElement) => {
  182. return new Promise(function (resolve) {
  183. const editor = editorElement.getModel()
  184. const done = () =>
  185. editor.component.getNextUpdatePromise().then(function () {
  186. for (const line of editorElement.querySelectorAll(
  187. '.line:not(.dummy)'
  188. )) {
  189. const line2 = document.createElement('div')
  190. line2.className = 'line'
  191. line2.innerHTML = line.firstChild.innerHTML
  192. preElement.appendChild(line2)
  193. }
  194. editorElement.remove()
  195. resolve()
  196. })
  197. const languageMode = editor.getBuffer().getLanguageMode()
  198. if (languageMode.fullyTokenized || languageMode.tree) {
  199. done()
  200. } else {
  201. editor.onDidTokenize(done)
  202. }
  203. })
  204. }