index.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import * as m from 'monaco-editor'
  2. import { compile, CompilerError } from '@vue/compiler-dom'
  3. import { compilerOptions, initOptions } from './options'
  4. import { watch } from '@vue/runtime-dom'
  5. import { SourceMapConsumer } from 'source-map'
  6. declare global {
  7. interface Window {
  8. monaco: typeof m
  9. _deps: any
  10. init: () => void
  11. }
  12. }
  13. window.init = () => {
  14. const monaco = window.monaco
  15. const persistedState = JSON.parse(
  16. decodeURIComponent(window.location.hash.slice(1)) ||
  17. localStorage.getItem('state') ||
  18. `{}`
  19. )
  20. Object.assign(compilerOptions, persistedState.options)
  21. let lastSuccessfulCode: string = `/* See console for error */`
  22. let lastSuccessfulMap: SourceMapConsumer | undefined = undefined
  23. function compileCode(source: string): string {
  24. console.clear()
  25. try {
  26. const errors: CompilerError[] = []
  27. const { code, ast, map } = compile(source, {
  28. filename: 'template.vue',
  29. ...compilerOptions,
  30. sourceMap: true,
  31. onError: err => {
  32. errors.push(err)
  33. }
  34. })
  35. monaco.editor.setModelMarkers(
  36. editor.getModel()!,
  37. `@vue/compiler-dom`,
  38. errors.filter(e => e.loc).map(formatError)
  39. )
  40. console.log(`AST: `, ast)
  41. lastSuccessfulCode = code + `\n\n// Check the console for the AST`
  42. lastSuccessfulMap = new window._deps['source-map'].SourceMapConsumer(map)
  43. lastSuccessfulMap!.computeColumnSpans()
  44. } catch (e) {
  45. console.error(e)
  46. }
  47. return lastSuccessfulCode
  48. }
  49. function formatError(err: CompilerError) {
  50. const loc = err.loc!
  51. return {
  52. severity: monaco.MarkerSeverity.Error,
  53. startLineNumber: loc.start.line,
  54. startColumn: loc.start.column,
  55. endLineNumber: loc.end.line,
  56. endColumn: loc.end.column,
  57. message: `Vue template compilation error: ${err.message}`,
  58. code: String(err.code)
  59. }
  60. }
  61. function reCompile() {
  62. const src = editor.getValue()
  63. // every time we re-compile, persist current state
  64. const state = JSON.stringify({
  65. src,
  66. options: compilerOptions
  67. })
  68. localStorage.setItem('state', state)
  69. window.location.hash = encodeURIComponent(state)
  70. const res = compileCode(src)
  71. if (res) {
  72. output.setValue(res)
  73. }
  74. }
  75. const sharedEditorOptions: m.editor.IEditorConstructionOptions = {
  76. theme: 'vs-dark',
  77. fontSize: 14,
  78. wordWrap: 'on',
  79. scrollBeyondLastLine: false,
  80. renderWhitespace: 'selection',
  81. contextmenu: false,
  82. minimap: {
  83. enabled: false
  84. }
  85. }
  86. const editor = monaco.editor.create(document.getElementById('source')!, {
  87. value: persistedState.src || `<div>Hello World!</div>`,
  88. language: 'html',
  89. ...sharedEditorOptions
  90. })
  91. editor.getModel()!.updateOptions({
  92. tabSize: 2
  93. })
  94. const output = monaco.editor.create(document.getElementById('output')!, {
  95. value: '',
  96. language: 'javascript',
  97. readOnly: true,
  98. ...sharedEditorOptions
  99. })
  100. output.getModel()!.updateOptions({
  101. tabSize: 2
  102. })
  103. // handle resize
  104. window.addEventListener('resize', () => {
  105. editor.layout()
  106. output.layout()
  107. })
  108. // update compile output when input changes
  109. editor.onDidChangeModelContent(debounce(reCompile))
  110. // highlight output code
  111. let prevOutputDecos: string[] = []
  112. function clearOutputDecos() {
  113. prevOutputDecos = output.deltaDecorations(prevOutputDecos, [])
  114. }
  115. editor.onDidChangeCursorPosition(
  116. debounce(e => {
  117. clearEditorDecos()
  118. if (lastSuccessfulMap) {
  119. const pos = lastSuccessfulMap.generatedPositionFor({
  120. source: 'template.vue',
  121. line: e.position.lineNumber,
  122. column: e.position.column - 1
  123. })
  124. if (pos.line != null && pos.column != null) {
  125. prevOutputDecos = output.deltaDecorations(prevOutputDecos, [
  126. {
  127. range: new monaco.Range(
  128. pos.line,
  129. pos.column + 1,
  130. pos.line,
  131. pos.lastColumn ? pos.lastColumn + 2 : pos.column + 2
  132. ),
  133. options: {
  134. inlineClassName: `highlight`
  135. }
  136. }
  137. ])
  138. output.revealPositionInCenter({
  139. lineNumber: pos.line,
  140. column: pos.column + 1
  141. })
  142. } else {
  143. clearOutputDecos()
  144. }
  145. }
  146. }, 100)
  147. )
  148. let previousEditorDecos: string[] = []
  149. function clearEditorDecos() {
  150. previousEditorDecos = editor.deltaDecorations(previousEditorDecos, [])
  151. }
  152. output.onDidChangeCursorPosition(
  153. debounce(e => {
  154. clearOutputDecos()
  155. if (lastSuccessfulMap) {
  156. const pos = lastSuccessfulMap.originalPositionFor({
  157. line: e.position.lineNumber,
  158. column: e.position.column - 1
  159. })
  160. if (
  161. pos.line != null &&
  162. pos.column != null &&
  163. !// ignore mock location
  164. (pos.line === 1 && pos.column === 0)
  165. ) {
  166. const translatedPos = {
  167. column: pos.column + 1,
  168. lineNumber: pos.line
  169. }
  170. previousEditorDecos = editor.deltaDecorations(previousEditorDecos, [
  171. {
  172. range: new monaco.Range(
  173. pos.line,
  174. pos.column + 1,
  175. pos.line,
  176. pos.column + 1
  177. ),
  178. options: {
  179. isWholeLine: true,
  180. className: `highlight`
  181. }
  182. }
  183. ])
  184. editor.revealPositionInCenter(translatedPos)
  185. } else {
  186. clearEditorDecos()
  187. }
  188. }
  189. }, 100)
  190. )
  191. initOptions()
  192. watch(reCompile)
  193. }
  194. function debounce<T extends (...args: any[]) => any>(
  195. fn: T,
  196. delay: number = 300
  197. ): T {
  198. let prevTimer: number | null = null
  199. return ((...args: any[]) => {
  200. if (prevTimer) {
  201. clearTimeout(prevTimer)
  202. }
  203. prevTimer = window.setTimeout(() => {
  204. fn(...args)
  205. prevTimer = null
  206. }, delay)
  207. }) as any
  208. }