syntax.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  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 && 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. html(index, length) {
  125. const [codeBlock] = this.children.find(index);
  126. const language = codeBlock
  127. ? SyntaxCodeBlock.formats(codeBlock.domNode)
  128. : 'plain';
  129. return `<pre data-language="${language}">\n${this.code(
  130. index,
  131. length,
  132. )}\n</pre>`;
  133. }
  134. optimize(context) {
  135. super.optimize(context);
  136. if (
  137. this.parent != null &&
  138. this.children.head != null &&
  139. this.uiNode != null
  140. ) {
  141. const language = SyntaxCodeBlock.formats(this.children.head.domNode);
  142. if (language !== this.uiNode.value) {
  143. this.uiNode.value = language;
  144. }
  145. }
  146. }
  147. }
  148. SyntaxCodeBlockContainer.allowedChildren = [SyntaxCodeBlock];
  149. SyntaxCodeBlock.requiredContainer = SyntaxCodeBlockContainer;
  150. SyntaxCodeBlock.allowedChildren = [CodeToken, CursorBlot, TextBlot, BreakBlot];
  151. class Syntax extends Module {
  152. static register(options) {
  153. Quill.register(CodeToken, { ...options, overwrite: true });
  154. Quill.register(SyntaxCodeBlock, { ...options, overwrite: true });
  155. Quill.register(SyntaxCodeBlockContainer, { ...options, overwrite: true });
  156. }
  157. constructor(quill, options) {
  158. super(quill, options);
  159. if (this.options.hljs == null) {
  160. throw new Error(
  161. 'Syntax module requires highlight.js. Please include the library on the page before Quill.',
  162. );
  163. }
  164. this.languages = this.options.languages.reduce((memo, { key }) => {
  165. memo[key] = true;
  166. return memo;
  167. }, {});
  168. this.highlightBlot = this.highlightBlot.bind(this);
  169. this.initListener();
  170. this.initTimer();
  171. }
  172. initListener() {
  173. this.quill.on(Quill.events.SCROLL_BLOT_MOUNT, blot => {
  174. if (!(blot instanceof SyntaxCodeBlockContainer)) return;
  175. const select = this.quill.root.ownerDocument.createElement('select');
  176. this.options.languages.forEach(({ key, label }) => {
  177. const option = select.ownerDocument.createElement('option');
  178. option.textContent = label;
  179. option.setAttribute('value', key);
  180. select.appendChild(option);
  181. });
  182. select.addEventListener('change', () => {
  183. blot.format(SyntaxCodeBlock.blotName, select.value);
  184. this.quill.root.focus(); // Prevent scrolling
  185. this.highlight(blot, true);
  186. });
  187. if (blot.uiNode == null) {
  188. blot.attachUI(select);
  189. if (blot.children.head) {
  190. select.value = SyntaxCodeBlock.formats(blot.children.head.domNode);
  191. }
  192. }
  193. });
  194. }
  195. initTimer() {
  196. let timer = null;
  197. this.quill.on(Quill.events.SCROLL_OPTIMIZE, () => {
  198. clearTimeout(timer);
  199. timer = setTimeout(() => {
  200. this.highlight();
  201. timer = null;
  202. }, this.options.interval);
  203. });
  204. }
  205. highlight(blot = null, force = false) {
  206. if (this.quill.selection.composing) return;
  207. this.quill.update(Quill.sources.USER);
  208. const range = this.quill.getSelection();
  209. const blots =
  210. blot == null
  211. ? this.quill.scroll.descendants(SyntaxCodeBlockContainer)
  212. : [blot];
  213. blots.forEach(container => {
  214. container.highlight(this.highlightBlot, force);
  215. });
  216. this.quill.update(Quill.sources.SILENT);
  217. if (range != null) {
  218. this.quill.setSelection(range, Quill.sources.SILENT);
  219. }
  220. }
  221. highlightBlot(text, language = 'plain') {
  222. language = this.languages[language] ? language : 'plain';
  223. if (language === 'plain') {
  224. return escapeText(text)
  225. .split('\n')
  226. .reduce((delta, line, i) => {
  227. if (i !== 0) {
  228. delta.insert('\n', { [CodeBlock.blotName]: language });
  229. }
  230. return delta.insert(line);
  231. }, new Delta());
  232. }
  233. const container = this.quill.root.ownerDocument.createElement('div');
  234. container.classList.add(CodeBlock.className);
  235. container.innerHTML = this.options.hljs.highlight(language, text).value;
  236. return traverse(
  237. this.quill.scroll,
  238. container,
  239. [
  240. (node, delta) => {
  241. const value = TokenAttributor.value(node);
  242. if (value) {
  243. return delta.compose(
  244. new Delta().retain(delta.length(), {
  245. [CodeToken.blotName]: value,
  246. }),
  247. );
  248. }
  249. return delta;
  250. },
  251. ],
  252. [
  253. (node, delta) => {
  254. return node.data.split('\n').reduce((memo, nodeText, i) => {
  255. if (i !== 0) memo.insert('\n', { [CodeBlock.blotName]: language });
  256. return memo.insert(nodeText);
  257. }, delta);
  258. },
  259. ],
  260. new WeakMap(),
  261. );
  262. }
  263. }
  264. Syntax.DEFAULTS = {
  265. hljs: (() => {
  266. return window.hljs;
  267. })(),
  268. interval: 1000,
  269. languages: [
  270. { key: 'plain', label: 'Plain' },
  271. { key: 'bash', label: 'Bash' },
  272. { key: 'cpp', label: 'C++' },
  273. { key: 'cs', label: 'C#' },
  274. { key: 'css', label: 'CSS' },
  275. { key: 'diff', label: 'Diff' },
  276. { key: 'xml', label: 'HTML/XML' },
  277. { key: 'java', label: 'Java' },
  278. { key: 'javascript', label: 'Javascript' },
  279. { key: 'markdown', label: 'Markdown' },
  280. { key: 'php', label: 'PHP' },
  281. { key: 'python', label: 'Python' },
  282. { key: 'ruby', label: 'Ruby' },
  283. { key: 'sql', label: 'SQL' },
  284. ],
  285. };
  286. export { SyntaxCodeBlock as CodeBlock, CodeToken, Syntax as default };