renderer.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. const zlib = require('zlib')
  2. // ------------------------------------
  3. // Markdown - PlantUML Preprocessor
  4. // ------------------------------------
  5. module.exports = {
  6. init (mdinst, conf) {
  7. mdinst.use((md, opts) => {
  8. const openMarker = opts.openMarker || '```plantuml'
  9. const openChar = openMarker.charCodeAt(0)
  10. const closeMarker = opts.closeMarker || '```'
  11. const closeChar = closeMarker.charCodeAt(0)
  12. const imageFormat = opts.imageFormat || 'svg'
  13. const server = opts.server || 'https://plantuml.requarks.io'
  14. md.block.ruler.before('fence', 'uml_diagram', (state, startLine, endLine, silent) => {
  15. let nextLine
  16. let markup
  17. let params
  18. let token
  19. let i
  20. let autoClosed = false
  21. let start = state.bMarks[startLine] + state.tShift[startLine]
  22. let max = state.eMarks[startLine]
  23. // Check out the first character quickly,
  24. // this should filter out most of non-uml blocks
  25. //
  26. if (openChar !== state.src.charCodeAt(start)) { return false }
  27. // Check out the rest of the marker string
  28. //
  29. for (i = 0; i < openMarker.length; ++i) {
  30. if (openMarker[i] !== state.src[start + i]) { return false }
  31. }
  32. markup = state.src.slice(start, start + i)
  33. params = state.src.slice(start + i, max)
  34. // Since start is found, we can report success here in validation mode
  35. //
  36. if (silent) { return true }
  37. // Search for the end of the block
  38. //
  39. nextLine = startLine
  40. for (;;) {
  41. nextLine++
  42. if (nextLine >= endLine) {
  43. // unclosed block should be autoclosed by end of document.
  44. // also block seems to be autoclosed by end of parent
  45. break
  46. }
  47. start = state.bMarks[nextLine] + state.tShift[nextLine]
  48. max = state.eMarks[nextLine]
  49. if (start < max && state.sCount[nextLine] < state.blkIndent) {
  50. // non-empty line with negative indent should stop the list:
  51. // - ```
  52. // test
  53. break
  54. }
  55. if (closeChar !== state.src.charCodeAt(start)) {
  56. // didn't find the closing fence
  57. continue
  58. }
  59. if (state.sCount[nextLine] > state.sCount[startLine]) {
  60. // closing fence should not be indented with respect of opening fence
  61. continue
  62. }
  63. const i = closeMarker.findIndex(item => item !== state.src[start + i])
  64. const closeMarkerMatched = i !== -1
  65. if (!closeMarkerMatched) {
  66. continue
  67. }
  68. // make sure tail has spaces only
  69. if (state.skipSpaces(start + i) < max) {
  70. continue
  71. }
  72. // found!
  73. autoClosed = true
  74. break
  75. }
  76. const contents = state.src
  77. .split('\n')
  78. .slice(startLine + 1, nextLine)
  79. .join('\n')
  80. // We generate a token list for the alt property, to mimic what the image parser does.
  81. let altToken = []
  82. // Remove leading space if any.
  83. let alt = params ? params.slice(1) : 'uml diagram'
  84. state.md.inline.parse(
  85. alt,
  86. state.md,
  87. state.env,
  88. altToken
  89. )
  90. const zippedCode = encode64(zlib.deflateRawSync('@startuml\n' + contents + '\n@enduml').toString('binary'))
  91. token = state.push('uml_diagram', 'img', 0)
  92. // alt is constructed from children. No point in populating it here.
  93. token.attrs = [ [ 'src', `${server}/${imageFormat}/${zippedCode}` ], [ 'alt', '' ], ['class', 'uml-diagram prefetch-candidate'] ]
  94. token.block = true
  95. token.children = altToken
  96. token.info = params
  97. token.map = [ startLine, nextLine ]
  98. token.markup = markup
  99. state.line = nextLine + (autoClosed ? 1 : 0)
  100. return true
  101. }, {
  102. alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
  103. })
  104. md.renderer.rules.uml_diagram = md.renderer.rules.image
  105. }, {
  106. openMarker: conf.openMarker,
  107. closeMarker: conf.closeMarker,
  108. imageFormat: conf.imageFormat,
  109. server: conf.server
  110. })
  111. }
  112. }
  113. function encode64 (data) {
  114. let r = ''
  115. for (let i = 0; i < data.length; i += 3) {
  116. if (i + 2 === data.length) {
  117. r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0)
  118. } else if (i + 1 === data.length) {
  119. r += append3bytes(data.charCodeAt(i), 0, 0)
  120. } else {
  121. r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), data.charCodeAt(i + 2))
  122. }
  123. }
  124. return r
  125. }
  126. function append3bytes (b1, b2, b3) {
  127. let c1 = b1 >> 2
  128. let c2 = ((b1 & 0x3) << 4) | (b2 >> 4)
  129. let c3 = ((b2 & 0xF) << 2) | (b3 >> 6)
  130. let c4 = b3 & 0x3F
  131. let r = ''
  132. r += encode6bit(c1 & 0x3F)
  133. r += encode6bit(c2 & 0x3F)
  134. r += encode6bit(c3 & 0x3F)
  135. r += encode6bit(c4 & 0x3F)
  136. return r
  137. }
  138. function encode6bit(raw) {
  139. let b = raw
  140. if (b < 10) {
  141. return String.fromCharCode(48 + b)
  142. }
  143. b -= 10
  144. if (b < 26) {
  145. return String.fromCharCode(65 + b)
  146. }
  147. b -= 26
  148. if (b < 26) {
  149. return String.fromCharCode(97 + b)
  150. }
  151. b -= 26
  152. if (b === 0) {
  153. return '-'
  154. }
  155. if (b === 1) {
  156. return '_'
  157. }
  158. return '?'
  159. }