detach-code-blocks.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import { BasicInterceptor } from '@growi/core';
  2. import loggerFactory from '~/utils/logger';
  3. class DetachCodeBlockUtil {
  4. static createReplaceStr(replaceId) {
  5. return `<pre class="detached-code-block">${replaceId}</pre>`;
  6. }
  7. }
  8. /**
  9. * The interceptor that detach code blocks
  10. */
  11. export class DetachCodeBlockInterceptor extends BasicInterceptor {
  12. constructor(crowi) {
  13. super();
  14. this.logger = loggerFactory('growi:interceptor:DetachCodeBlockInterceptor');
  15. this.crowi = crowi;
  16. this.crowiForJquery = crowi.getCrowiForJquery();
  17. }
  18. /**
  19. * @inheritdoc
  20. */
  21. isInterceptWhen(contextName) {
  22. return /^prePreProcess|prePostProcess$/.test(contextName);
  23. }
  24. getTargetKey(contextName) {
  25. if (contextName === 'prePreProcess') {
  26. return 'markdown';
  27. }
  28. if (contextName === 'prePostProcess') {
  29. return 'parsedHTML';
  30. }
  31. }
  32. /**
  33. * @inheritdoc
  34. */
  35. process(contextName, ...args) {
  36. this.logger.debug(`processing: 'contextName'=${contextName}`);
  37. const context = Object.assign(args[0]); // clone
  38. const targetKey = this.getTargetKey(contextName);
  39. context.dcbContextMap = {};
  40. // see: https://regex101.com/r/8PAEcC/5
  41. // eslint-disable-next-line max-len
  42. context[targetKey] = context[targetKey].replace(/(^(```|~~~)(.|[\r\n])*?(```|~~~)$)|(`[^\r\n]*?`)|(<pre>(.|[\r\n])*?<\/pre>)|(<pre\s[^>]*>(.|[\r\n])*?<\/pre>)/gm, (all) => {
  43. // create ID
  44. const replaceId = `dcb-${this.createRandomStr(8)}`;
  45. this.logger.debug(`'replaceId'=${replaceId} : `, all);
  46. // register to context
  47. const dcbContext = {};
  48. dcbContext.content = all;
  49. dcbContext.substituteContent = DetachCodeBlockUtil.createReplaceStr(replaceId);
  50. context.dcbContextMap[replaceId] = dcbContext;
  51. // return substituteContent
  52. return dcbContext.substituteContent;
  53. });
  54. // resolve
  55. return Promise.resolve(context);
  56. }
  57. /**
  58. * @see http://qiita.com/ryounagaoka/items/4736c225bdd86a74d59c
  59. *
  60. * @param {number} length
  61. * @return random strings
  62. */
  63. createRandomStr(length) {
  64. const bag = 'abcdefghijklmnopqrstuvwxyz0123456789';
  65. let generated = '';
  66. for (let i = 0; i < length; i++) {
  67. generated += bag[Math.floor(Math.random() * bag.length)];
  68. }
  69. return generated;
  70. }
  71. }
  72. /**
  73. * The interceptor that restore detached code blocks
  74. */
  75. export class RestoreCodeBlockInterceptor extends BasicInterceptor {
  76. constructor(crowi) {
  77. super();
  78. this.logger = loggerFactory('growi:interceptor:DetachCodeBlockInterceptor');
  79. this.crowi = crowi;
  80. this.crowiForJquery = crowi.getCrowiForJquery();
  81. }
  82. /**
  83. * @inheritdoc
  84. */
  85. isInterceptWhen(contextName) {
  86. return /^postPreProcess|preRenderHtml|preRenderPreviewHtml|preRenderCommentHtml|preRenderCommentPreviewHtml$/.test(contextName);
  87. }
  88. getTargetKey(contextName) {
  89. if (contextName === 'postPreProcess') {
  90. return 'markdown';
  91. }
  92. if (contextName === 'preRenderHtml' || contextName === 'preRenderPreviewHtml'
  93. || contextName === 'preRenderCommentHtml' || contextName === 'preRenderCommentPreviewHtml') {
  94. return 'parsedHTML';
  95. }
  96. }
  97. /**
  98. * @inheritdoc
  99. */
  100. process(contextName, ...args) {
  101. this.logger.debug(`processing: 'contextName'=${contextName}`);
  102. const context = Object.assign(args[0]); // clone
  103. const targetKey = this.getTargetKey(contextName);
  104. // forEach keys of dcbContextMap
  105. Object.keys(context.dcbContextMap).forEach((replaceId) => {
  106. // get context object from context
  107. const dcbContext = context.dcbContextMap[replaceId];
  108. // replace it with content by using getter function so that the doller sign does not work
  109. // see: https://github.com/weseek/growi/issues/285
  110. context[targetKey] = context[targetKey].replace(dcbContext.substituteContent, () => { return dcbContext.content });
  111. });
  112. // resolve
  113. return Promise.resolve(context);
  114. }
  115. }