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.

190 lines
5.3 KiB

  1. const pako = require('pako')
  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. var closeMarkerMatched = true
  64. for (i = 0; i < closeMarker.length; ++i) {
  65. if (closeMarker[i] !== state.src[start + i]) {
  66. closeMarkerMatched = false
  67. break
  68. }
  69. }
  70. if (!closeMarkerMatched) {
  71. continue
  72. }
  73. // make sure tail has spaces only
  74. if (state.skipSpaces(start + i) < max) {
  75. continue
  76. }
  77. // found!
  78. autoClosed = true
  79. break
  80. }
  81. const contents = state.src
  82. .split('\n')
  83. .slice(startLine + 1, nextLine)
  84. .join('\n')
  85. // We generate a token list for the alt property, to mimic what the image parser does.
  86. let altToken = []
  87. // Remove leading space if any.
  88. let alt = params ? params.slice(1) : 'uml diagram'
  89. state.md.inline.parse(
  90. alt,
  91. state.md,
  92. state.env,
  93. altToken
  94. )
  95. var zippedCode = encode64(pako.deflate('@startuml\n' + contents + '\n@enduml', { to: 'string' }))
  96. token = state.push('uml_diagram', 'img', 0)
  97. // alt is constructed from children. No point in populating it here.
  98. token.attrs = [ [ 'src', `${server}/${imageFormat}/${zippedCode}` ], [ 'alt', '' ], ['class', 'uml-diagram'] ]
  99. token.block = true
  100. token.children = altToken
  101. token.info = params
  102. token.map = [ startLine, nextLine ]
  103. token.markup = markup
  104. state.line = nextLine + (autoClosed ? 1 : 0)
  105. return true
  106. }, {
  107. alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
  108. })
  109. md.renderer.rules.uml_diagram = md.renderer.rules.image
  110. }, {
  111. openMarker: conf.openMarker,
  112. closeMarker: conf.closeMarker,
  113. imageFormat: conf.imageFormat,
  114. server: conf.server
  115. })
  116. }
  117. }
  118. function encode64 (data) {
  119. let r = ''
  120. for (let i = 0; i < data.length; i += 3) {
  121. if (i + 2 === data.length) {
  122. r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0)
  123. } else if (i + 1 === data.length) {
  124. r += append3bytes(data.charCodeAt(i), 0, 0)
  125. } else {
  126. r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), data.charCodeAt(i + 2))
  127. }
  128. }
  129. return r
  130. }
  131. function append3bytes (b1, b2, b3) {
  132. let c1 = b1 >> 2
  133. let c2 = ((b1 & 0x3) << 4) | (b2 >> 4)
  134. let c3 = ((b2 & 0xF) << 2) | (b3 >> 6)
  135. let c4 = b3 & 0x3F
  136. let r = ''
  137. r += encode6bit(c1 & 0x3F)
  138. r += encode6bit(c2 & 0x3F)
  139. r += encode6bit(c3 & 0x3F)
  140. r += encode6bit(c4 & 0x3F)
  141. return r
  142. }
  143. function encode6bit(raw) {
  144. let b = raw
  145. if (b < 10) {
  146. return String.fromCharCode(48 + b)
  147. }
  148. b -= 10
  149. if (b < 26) {
  150. return String.fromCharCode(65 + b)
  151. }
  152. b -= 26
  153. if (b < 26) {
  154. return String.fromCharCode(97 + b)
  155. }
  156. b -= 26
  157. if (b === 0) {
  158. return '-'
  159. }
  160. if (b === 1) {
  161. return '_'
  162. }
  163. return '?'
  164. }