formattedTextRenderer.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import * as DOM from './dom.js';
  6. export function renderText(text, options = {}) {
  7. const element = createElement(options);
  8. element.textContent = text;
  9. return element;
  10. }
  11. export function renderFormattedText(formattedText, options = {}) {
  12. const element = createElement(options);
  13. _renderFormattedText(element, parseFormattedText(formattedText, !!options.renderCodeSegments), options.actionHandler, options.renderCodeSegments);
  14. return element;
  15. }
  16. export function createElement(options) {
  17. const tagName = options.inline ? 'span' : 'div';
  18. const element = document.createElement(tagName);
  19. if (options.className) {
  20. element.className = options.className;
  21. }
  22. return element;
  23. }
  24. class StringStream {
  25. constructor(source) {
  26. this.source = source;
  27. this.index = 0;
  28. }
  29. eos() {
  30. return this.index >= this.source.length;
  31. }
  32. next() {
  33. const next = this.peek();
  34. this.advance();
  35. return next;
  36. }
  37. peek() {
  38. return this.source[this.index];
  39. }
  40. advance() {
  41. this.index++;
  42. }
  43. }
  44. function _renderFormattedText(element, treeNode, actionHandler, renderCodeSegments) {
  45. let child;
  46. if (treeNode.type === 2 /* Text */) {
  47. child = document.createTextNode(treeNode.content || '');
  48. }
  49. else if (treeNode.type === 3 /* Bold */) {
  50. child = document.createElement('b');
  51. }
  52. else if (treeNode.type === 4 /* Italics */) {
  53. child = document.createElement('i');
  54. }
  55. else if (treeNode.type === 7 /* Code */ && renderCodeSegments) {
  56. child = document.createElement('code');
  57. }
  58. else if (treeNode.type === 5 /* Action */ && actionHandler) {
  59. const a = document.createElement('a');
  60. a.href = '#';
  61. actionHandler.disposables.add(DOM.addStandardDisposableListener(a, 'click', (event) => {
  62. actionHandler.callback(String(treeNode.index), event);
  63. }));
  64. child = a;
  65. }
  66. else if (treeNode.type === 8 /* NewLine */) {
  67. child = document.createElement('br');
  68. }
  69. else if (treeNode.type === 1 /* Root */) {
  70. child = element;
  71. }
  72. if (child && element !== child) {
  73. element.appendChild(child);
  74. }
  75. if (child && Array.isArray(treeNode.children)) {
  76. treeNode.children.forEach((nodeChild) => {
  77. _renderFormattedText(child, nodeChild, actionHandler, renderCodeSegments);
  78. });
  79. }
  80. }
  81. function parseFormattedText(content, parseCodeSegments) {
  82. const root = {
  83. type: 1 /* Root */,
  84. children: []
  85. };
  86. let actionViewItemIndex = 0;
  87. let current = root;
  88. const stack = [];
  89. const stream = new StringStream(content);
  90. while (!stream.eos()) {
  91. let next = stream.next();
  92. const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek(), parseCodeSegments) !== 0 /* Invalid */);
  93. if (isEscapedFormatType) {
  94. next = stream.next(); // unread the backslash if it escapes a format tag type
  95. }
  96. if (!isEscapedFormatType && isFormatTag(next, parseCodeSegments) && next === stream.peek()) {
  97. stream.advance();
  98. if (current.type === 2 /* Text */) {
  99. current = stack.pop();
  100. }
  101. const type = formatTagType(next, parseCodeSegments);
  102. if (current.type === type || (current.type === 5 /* Action */ && type === 6 /* ActionClose */)) {
  103. current = stack.pop();
  104. }
  105. else {
  106. const newCurrent = {
  107. type: type,
  108. children: []
  109. };
  110. if (type === 5 /* Action */) {
  111. newCurrent.index = actionViewItemIndex;
  112. actionViewItemIndex++;
  113. }
  114. current.children.push(newCurrent);
  115. stack.push(current);
  116. current = newCurrent;
  117. }
  118. }
  119. else if (next === '\n') {
  120. if (current.type === 2 /* Text */) {
  121. current = stack.pop();
  122. }
  123. current.children.push({
  124. type: 8 /* NewLine */
  125. });
  126. }
  127. else {
  128. if (current.type !== 2 /* Text */) {
  129. const textCurrent = {
  130. type: 2 /* Text */,
  131. content: next
  132. };
  133. current.children.push(textCurrent);
  134. stack.push(current);
  135. current = textCurrent;
  136. }
  137. else {
  138. current.content += next;
  139. }
  140. }
  141. }
  142. if (current.type === 2 /* Text */) {
  143. current = stack.pop();
  144. }
  145. if (stack.length) {
  146. // incorrectly formatted string literal
  147. }
  148. return root;
  149. }
  150. function isFormatTag(char, supportCodeSegments) {
  151. return formatTagType(char, supportCodeSegments) !== 0 /* Invalid */;
  152. }
  153. function formatTagType(char, supportCodeSegments) {
  154. switch (char) {
  155. case '*':
  156. return 3 /* Bold */;
  157. case '_':
  158. return 4 /* Italics */;
  159. case '[':
  160. return 5 /* Action */;
  161. case ']':
  162. return 6 /* ActionClose */;
  163. case '`':
  164. return supportCodeSegments ? 7 /* Code */ : 0 /* Invalid */;
  165. default:
  166. return 0 /* Invalid */;
  167. }
  168. }