171968849a99c68f935e2471fb14a6d5f39d99f244a34ee0a78548fee2025ba07540d8926a293bad5c94396a740e2e70ae07f7641b39791eb12441e170a4f1 10 KB

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