attention.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. /**
  2. * @import {
  3. * Code,
  4. * Construct,
  5. * Event,
  6. * Point,
  7. * Resolver,
  8. * State,
  9. * TokenizeContext,
  10. * Tokenizer,
  11. * Token
  12. * } from 'micromark-util-types'
  13. */
  14. import { push, splice } from 'micromark-util-chunked';
  15. import { classifyCharacter } from 'micromark-util-classify-character';
  16. import { resolveAll } from 'micromark-util-resolve-all';
  17. /** @type {Construct} */
  18. export const attention = {
  19. name: 'attention',
  20. resolveAll: resolveAllAttention,
  21. tokenize: tokenizeAttention
  22. };
  23. /**
  24. * Take all events and resolve attention to emphasis or strong.
  25. *
  26. * @type {Resolver}
  27. */
  28. // eslint-disable-next-line complexity
  29. function resolveAllAttention(events, context) {
  30. let index = -1;
  31. /** @type {number} */
  32. let open;
  33. /** @type {Token} */
  34. let group;
  35. /** @type {Token} */
  36. let text;
  37. /** @type {Token} */
  38. let openingSequence;
  39. /** @type {Token} */
  40. let closingSequence;
  41. /** @type {number} */
  42. let use;
  43. /** @type {Array<Event>} */
  44. let nextEvents;
  45. /** @type {number} */
  46. let offset;
  47. // Walk through all events.
  48. //
  49. // Note: performance of this is fine on an mb of normal markdown, but it’s
  50. // a bottleneck for malicious stuff.
  51. while (++index < events.length) {
  52. // Find a token that can close.
  53. if (events[index][0] === 'enter' && events[index][1].type === 'attentionSequence' && events[index][1]._close) {
  54. open = index;
  55. // Now walk back to find an opener.
  56. while (open--) {
  57. // Find a token that can open the closer.
  58. if (events[open][0] === 'exit' && events[open][1].type === 'attentionSequence' && events[open][1]._open &&
  59. // If the markers are the same:
  60. context.sliceSerialize(events[open][1]).charCodeAt(0) === context.sliceSerialize(events[index][1]).charCodeAt(0)) {
  61. // If the opening can close or the closing can open,
  62. // and the close size *is not* a multiple of three,
  63. // but the sum of the opening and closing size *is* multiple of three,
  64. // then don’t match.
  65. if ((events[open][1]._close || events[index][1]._open) && (events[index][1].end.offset - events[index][1].start.offset) % 3 && !((events[open][1].end.offset - events[open][1].start.offset + events[index][1].end.offset - events[index][1].start.offset) % 3)) {
  66. continue;
  67. }
  68. // Number of markers to use from the sequence.
  69. use = events[open][1].end.offset - events[open][1].start.offset > 1 && events[index][1].end.offset - events[index][1].start.offset > 1 ? 2 : 1;
  70. const start = {
  71. ...events[open][1].end
  72. };
  73. const end = {
  74. ...events[index][1].start
  75. };
  76. movePoint(start, -use);
  77. movePoint(end, use);
  78. openingSequence = {
  79. type: use > 1 ? "strongSequence" : "emphasisSequence",
  80. start,
  81. end: {
  82. ...events[open][1].end
  83. }
  84. };
  85. closingSequence = {
  86. type: use > 1 ? "strongSequence" : "emphasisSequence",
  87. start: {
  88. ...events[index][1].start
  89. },
  90. end
  91. };
  92. text = {
  93. type: use > 1 ? "strongText" : "emphasisText",
  94. start: {
  95. ...events[open][1].end
  96. },
  97. end: {
  98. ...events[index][1].start
  99. }
  100. };
  101. group = {
  102. type: use > 1 ? "strong" : "emphasis",
  103. start: {
  104. ...openingSequence.start
  105. },
  106. end: {
  107. ...closingSequence.end
  108. }
  109. };
  110. events[open][1].end = {
  111. ...openingSequence.start
  112. };
  113. events[index][1].start = {
  114. ...closingSequence.end
  115. };
  116. nextEvents = [];
  117. // If there are more markers in the opening, add them before.
  118. if (events[open][1].end.offset - events[open][1].start.offset) {
  119. nextEvents = push(nextEvents, [['enter', events[open][1], context], ['exit', events[open][1], context]]);
  120. }
  121. // Opening.
  122. nextEvents = push(nextEvents, [['enter', group, context], ['enter', openingSequence, context], ['exit', openingSequence, context], ['enter', text, context]]);
  123. // Always populated by defaults.
  124. // Between.
  125. nextEvents = push(nextEvents, resolveAll(context.parser.constructs.insideSpan.null, events.slice(open + 1, index), context));
  126. // Closing.
  127. nextEvents = push(nextEvents, [['exit', text, context], ['enter', closingSequence, context], ['exit', closingSequence, context], ['exit', group, context]]);
  128. // If there are more markers in the closing, add them after.
  129. if (events[index][1].end.offset - events[index][1].start.offset) {
  130. offset = 2;
  131. nextEvents = push(nextEvents, [['enter', events[index][1], context], ['exit', events[index][1], context]]);
  132. } else {
  133. offset = 0;
  134. }
  135. splice(events, open - 1, index - open + 3, nextEvents);
  136. index = open + nextEvents.length - offset - 2;
  137. break;
  138. }
  139. }
  140. }
  141. }
  142. // Remove remaining sequences.
  143. index = -1;
  144. while (++index < events.length) {
  145. if (events[index][1].type === 'attentionSequence') {
  146. events[index][1].type = 'data';
  147. }
  148. }
  149. return events;
  150. }
  151. /**
  152. * @this {TokenizeContext}
  153. * Context.
  154. * @type {Tokenizer}
  155. */
  156. function tokenizeAttention(effects, ok) {
  157. const attentionMarkers = this.parser.constructs.attentionMarkers.null;
  158. const previous = this.previous;
  159. const before = classifyCharacter(previous);
  160. /** @type {NonNullable<Code>} */
  161. let marker;
  162. return start;
  163. /**
  164. * Before a sequence.
  165. *
  166. * ```markdown
  167. * > | **
  168. * ^
  169. * ```
  170. *
  171. * @type {State}
  172. */
  173. function start(code) {
  174. marker = code;
  175. effects.enter('attentionSequence');
  176. return inside(code);
  177. }
  178. /**
  179. * In a sequence.
  180. *
  181. * ```markdown
  182. * > | **
  183. * ^^
  184. * ```
  185. *
  186. * @type {State}
  187. */
  188. function inside(code) {
  189. if (code === marker) {
  190. effects.consume(code);
  191. return inside;
  192. }
  193. const token = effects.exit('attentionSequence');
  194. // To do: next major: move this to resolver, just like `markdown-rs`.
  195. const after = classifyCharacter(code);
  196. // Always populated by defaults.
  197. const open = !after || after === 2 && before || attentionMarkers.includes(code);
  198. const close = !before || before === 2 && after || attentionMarkers.includes(previous);
  199. token._open = Boolean(marker === 42 ? open : open && (before || !close));
  200. token._close = Boolean(marker === 42 ? close : close && (after || !open));
  201. return ok(code);
  202. }
  203. }
  204. /**
  205. * Move a point a bit.
  206. *
  207. * Note: `move` only works inside lines! It’s not possible to move past other
  208. * chunks (replacement characters, tabs, or line endings).
  209. *
  210. * @param {Point} point
  211. * Point.
  212. * @param {number} offset
  213. * Amount to move.
  214. * @returns {undefined}
  215. * Nothing.
  216. */
  217. function movePoint(point, offset) {
  218. point.column += offset;
  219. point.offset += offset;
  220. point._bufferIndex += offset;
  221. }