not really known
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.

292 lines
8.5 KiB

  1. import Delta from 'quill-delta';
  2. import { ClassAttributor, Scope } from 'parchment';
  3. import Inline from '../blots/inline';
  4. import Quill from '../core/quill';
  5. import Module from '../core/module';
  6. import { blockDelta } from '../blots/block';
  7. import BreakBlot from '../blots/break';
  8. import CursorBlot from '../blots/cursor';
  9. import TextBlot, { escapeText } from '../blots/text';
  10. import CodeBlock, { CodeBlockContainer } from '../formats/code';
  11. import { traverse } from './clipboard';
  12. const TokenAttributor = new ClassAttributor('code-token', 'hljs', {
  13. scope: Scope.INLINE,
  14. });
  15. class CodeToken extends Inline {
  16. static formats(node, scroll) {
  17. while (node != null && node !== scroll.domNode) {
  18. if (node.classList.contains(CodeBlock.className)) {
  19. return super.formats(node, scroll);
  20. }
  21. node = node.parentNode;
  22. }
  23. return undefined;
  24. }
  25. constructor(scroll, domNode, value) {
  26. super(scroll, domNode, value);
  27. TokenAttributor.add(this.domNode, value);
  28. }
  29. format(format, value) {
  30. if (format !== CodeToken.blotName) {
  31. super.format(format, value);
  32. } else if (value) {
  33. TokenAttributor.add(this.domNode, value);
  34. } else {
  35. TokenAttributor.remove(this.domNode);
  36. this.domNode.classList.remove(this.statics.className);
  37. }
  38. }
  39. optimize(...args) {
  40. super.optimize(...args);
  41. if (!TokenAttributor.value(this.domNode)) {
  42. this.unwrap();
  43. }
  44. }
  45. }
  46. CodeToken.blotName = 'code-token';
  47. CodeToken.className = 'ql-token';
  48. class SyntaxCodeBlock extends CodeBlock {
  49. static create(value) {
  50. const domNode = super.create(value);
  51. if (typeof value === 'string') {
  52. domNode.setAttribute('data-language', value);
  53. }
  54. return domNode;
  55. }
  56. static formats(domNode) {
  57. return domNode.getAttribute('data-language') || 'plain';
  58. }
  59. static register() {} // Syntax module will register
  60. format(name, value) {
  61. if (name === this.statics.blotName && value) {
  62. this.domNode.setAttribute('data-language', value);
  63. } else {
  64. super.format(name, value);
  65. }
  66. }
  67. replaceWith(name, value) {
  68. this.formatAt(0, this.length(), CodeToken.blotName, false);
  69. return super.replaceWith(name, value);
  70. }
  71. }
  72. class SyntaxCodeBlockContainer extends CodeBlockContainer {
  73. attach() {
  74. super.attach();
  75. this.forceNext = false;
  76. this.scroll.emitMount(this);
  77. }
  78. format(name, value) {
  79. if (name === SyntaxCodeBlock.blotName) {
  80. this.forceNext = true;
  81. this.children.forEach(child => {
  82. child.format(name, value);
  83. });
  84. }
  85. }
  86. formatAt(index, length, name, value) {
  87. if (name === SyntaxCodeBlock.blotName) {
  88. this.forceNext = true;
  89. }
  90. super.formatAt(index, length, name, value);
  91. }
  92. highlight(highlight, forced = false) {
  93. if (this.children.head == null) return;
  94. const nodes = Array.from(this.domNode.childNodes).filter(
  95. node => node !== this.uiNode,
  96. );
  97. const text = `${nodes.map(node => node.textContent).join('\n')}\n`;
  98. const language = SyntaxCodeBlock.formats(this.children.head.domNode);
  99. if (forced || this.forceNext || this.cachedText !== text) {
  100. if (text.trim().length > 0 || this.cachedText == null) {
  101. const oldDelta = this.children.reduce((delta, child) => {
  102. return delta.concat(blockDelta(child, false));
  103. }, new Delta());
  104. const delta = highlight(text, language);
  105. oldDelta.diff(delta).reduce((index, { retain, attributes }) => {
  106. // Should be all retains
  107. if (!retain) return index;
  108. if (attributes) {
  109. Object.keys(attributes).forEach(format => {
  110. if (
  111. [SyntaxCodeBlock.blotName, CodeToken.blotName].includes(format)
  112. ) {
  113. this.formatAt(index, retain, format, attributes[format]);
  114. }
  115. });
  116. }
  117. return index + retain;
  118. }, 0);
  119. }
  120. this.cachedText = text;
  121. this.forceNext = false;
  122. }
  123. }
  124. optimize(context) {
  125. super.optimize(context);
  126. if (
  127. this.parent != null &&
  128. this.children.head != null &&
  129. this.uiNode != null
  130. ) {
  131. const language = SyntaxCodeBlock.formats(this.children.head.domNode);
  132. if (language !== this.uiNode.value) {
  133. this.uiNode.value = language;
  134. }
  135. }
  136. }
  137. }
  138. SyntaxCodeBlockContainer.allowedChildren = [SyntaxCodeBlock];
  139. SyntaxCodeBlock.requiredContainer = SyntaxCodeBlockContainer;
  140. SyntaxCodeBlock.allowedChildren = [CodeToken, CursorBlot, TextBlot, BreakBlot];
  141. class Syntax extends Module {
  142. static register() {
  143. Quill.register(CodeToken, true);
  144. Quill.register(SyntaxCodeBlock, true);
  145. Quill.register(SyntaxCodeBlockContainer, true);
  146. }
  147. constructor(quill, options) {
  148. super(quill, options);
  149. if (this.options.hljs == null) {
  150. throw new Error(
  151. 'Syntax module requires highlight.js. Please include the library on the page before Quill.',
  152. );
  153. }
  154. this.highlightBlot = this.highlightBlot.bind(this);
  155. this.initListener();
  156. this.initTimer();
  157. }
  158. initListener() {
  159. this.quill.on(Quill.events.SCROLL_BLOT_MOUNT, blot => {
  160. if (!(blot instanceof SyntaxCodeBlockContainer)) return;
  161. const select = this.quill.root.ownerDocument.createElement('select');
  162. this.options.languages.forEach(({ key, label }) => {
  163. const option = select.ownerDocument.createElement('option');
  164. option.textContent = label;
  165. option.setAttribute('value', key);
  166. select.appendChild(option);
  167. });
  168. select.addEventListener('change', () => {
  169. blot.format(SyntaxCodeBlock.blotName, select.value);
  170. this.quill.root.focus(); // Prevent scrolling
  171. this.highlight(blot, true);
  172. });
  173. if (blot.uiNode == null) {
  174. blot.attachUI(select);
  175. if (blot.children.head) {
  176. select.value = SyntaxCodeBlock.formats(blot.children.head.domNode);
  177. }
  178. }
  179. });
  180. }
  181. initTimer() {
  182. let timer = null;
  183. this.quill.on(Quill.events.SCROLL_OPTIMIZE, () => {
  184. clearTimeout(timer);
  185. timer = setTimeout(() => {
  186. this.highlight();
  187. timer = null;
  188. }, this.options.interval);
  189. });
  190. }
  191. highlight(blot = null, force = false) {
  192. if (this.quill.selection.composing) return;
  193. this.quill.update(Quill.sources.USER);
  194. const range = this.quill.getSelection();
  195. const blots =
  196. blot == null
  197. ? this.quill.scroll.descendants(SyntaxCodeBlockContainer)
  198. : [blot];
  199. blots.forEach(container => {
  200. container.highlight(this.highlightBlot, force);
  201. });
  202. this.quill.update(Quill.sources.SILENT);
  203. if (range != null) {
  204. this.quill.setSelection(range, Quill.sources.SILENT);
  205. }
  206. }
  207. highlightBlot(text, language = 'plain') {
  208. if (language === 'plain') {
  209. return escapeText(text)
  210. .split('\n')
  211. .reduce((delta, line, i) => {
  212. if (i !== 0) {
  213. delta.insert('\n', { [CodeBlock.blotName]: language });
  214. }
  215. return delta.insert(line);
  216. }, new Delta());
  217. }
  218. const container = this.quill.root.ownerDocument.createElement('div');
  219. container.classList.add(CodeBlock.className);
  220. container.innerHTML = this.options.hljs.highlight(language, text).value;
  221. return traverse(
  222. this.quill.scroll,
  223. container,
  224. [
  225. (node, delta) => {
  226. const value = TokenAttributor.value(node);
  227. if (value) {
  228. return delta.compose(
  229. new Delta().retain(delta.length(), {
  230. [CodeToken.blotName]: value,
  231. }),
  232. );
  233. }
  234. return delta;
  235. },
  236. ],
  237. [
  238. (node, delta) => {
  239. return node.data.split('\n').reduce((memo, nodeText, i) => {
  240. if (i !== 0) memo.insert('\n', { [CodeBlock.blotName]: language });
  241. return memo.insert(nodeText);
  242. }, delta);
  243. },
  244. ],
  245. new WeakMap(),
  246. );
  247. }
  248. }
  249. Syntax.DEFAULTS = {
  250. hljs: (() => {
  251. return window.hljs;
  252. })(),
  253. interval: 1000,
  254. languages: [
  255. { key: 'plain', label: 'Plain' },
  256. { key: 'bash', label: 'Bash' },
  257. { key: 'cpp', label: 'C++' },
  258. { key: 'cs', label: 'C#' },
  259. { key: 'css', label: 'CSS' },
  260. { key: 'diff', label: 'Diff' },
  261. { key: 'xml', label: 'HTML/XML' },
  262. { key: 'java', label: 'Java' },
  263. { key: 'javascript', label: 'Javascript' },
  264. { key: 'markdown', label: 'Markdown' },
  265. { key: 'php', label: 'PHP' },
  266. { key: 'python', label: 'Python' },
  267. { key: 'ruby', label: 'Ruby' },
  268. { key: 'sql', label: 'SQL' },
  269. ],
  270. };
  271. export { SyntaxCodeBlock as CodeBlock, CodeToken, Syntax as default };