plantuml.mjs 5.2 KB

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