8
0

autolink.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module link/autolink
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import TextWatcher from '@ckeditor/ckeditor5-typing/src/textwatcher';
  10. import getLastTextLine from '@ckeditor/ckeditor5-typing/src/utils/getlasttextline';
  11. const MIN_LINK_LENGTH_WITH_SPACE_AT_END = 4; // Ie: "t.co " (length 5).
  12. const URL_REG_EXP = new RegExp(
  13. // Group 1: Line start or after a space.
  14. '(^|\\s)' + // Match .
  15. // Group 2: Full detected URL.
  16. '(' +
  17. // Group 3 + 4: Protocol + domain.
  18. '(([a-z]{3,9}:(?:\\/\\/)?)(?:[\\w]+)?[a-z0-9.-]+|(?:www\\.|[\\w]+)[a-z0-9.-]+)' +
  19. // Group 5: Optional path + query string + location.
  20. '((?:\\/[+~%/.\\w\\-_]*)?\\??(?:[-+=&;%@.\\w_]*)#?(?:[.!/\\\\\\w]*))?' +
  21. ')$', 'i' );
  22. const URL_GROUP_IN_MATCH = 2;
  23. // Simplified email test - should be run over previously found URL.
  24. const EMAIL_REG_EXP = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i;
  25. /**
  26. * The auto link plugin.
  27. *
  28. * @extends module:core/plugin~Plugin
  29. */
  30. export default class AutoLink extends Plugin {
  31. /**
  32. * @inheritDoc
  33. */
  34. static get pluginName() {
  35. return 'AutoLink';
  36. }
  37. /**
  38. * @inheritDoc
  39. */
  40. init() {
  41. const editor = this.editor;
  42. const selection = editor.model.document.selection;
  43. selection.on( 'change:range', () => {
  44. // Disable plugin when selection is inside a code block.
  45. this.isEnabled = !selection.anchor.parent.is( 'codeBlock' );
  46. } );
  47. const watcher = new TextWatcher( editor.model, text => {
  48. // 1. Detect "space" after a text with a potential link.
  49. if ( !isSingleSpaceAtTheEnd( text ) ) {
  50. return;
  51. }
  52. // 2. Check text before "space" or "enter".
  53. const url = getUrlAtTextEnd( text.substr( 0, text.length - 1 ) );
  54. if ( url ) {
  55. return { url };
  56. }
  57. } );
  58. const input = editor.plugins.get( 'Input' );
  59. watcher.on( 'matched:data', ( evt, data ) => {
  60. const { batch, range, url } = data;
  61. if ( !input.isInput( batch ) ) {
  62. return;
  63. }
  64. this._applyAutoLink( url, range );
  65. } );
  66. watcher.bind( 'isEnabled' ).to( this );
  67. }
  68. /**
  69. * @inheritDoc
  70. */
  71. afterInit() {
  72. this._enableEnterHandling();
  73. this._enableShiftEnterHandling();
  74. }
  75. _enableEnterHandling() {
  76. const editor = this.editor;
  77. const model = editor.model;
  78. const enterCommand = editor.commands.get( 'enter' );
  79. enterCommand.on( 'execute', () => {
  80. const position = model.document.selection.getFirstPosition();
  81. const rangeToCheck = model.createRange(
  82. model.createPositionAt( position.parent.previousSibling, 0 ),
  83. model.createPositionAt( position.parent.previousSibling, 'end' )
  84. );
  85. this._checkAndApplyAutoLinkOnRange( rangeToCheck );
  86. } );
  87. }
  88. _enableShiftEnterHandling() {
  89. const editor = this.editor;
  90. const model = editor.model;
  91. const shiftEnterCommand = editor.commands.get( 'shiftEnter' );
  92. shiftEnterCommand.on( 'execute', () => {
  93. const position = model.document.selection.getFirstPosition();
  94. const rangeToCheck = model.createRange(
  95. model.createPositionAt( position.parent, 0 ),
  96. position.getShiftedBy( -1 )
  97. );
  98. this._checkAndApplyAutoLinkOnRange( rangeToCheck );
  99. } );
  100. }
  101. _checkAndApplyAutoLinkOnRange( rangeToCheck ) {
  102. const { text, range } = getLastTextLine( rangeToCheck, this.editor.model );
  103. const url = getUrlAtTextEnd( text );
  104. if ( url ) {
  105. this._applyAutoLink( url, range, 0 );
  106. }
  107. }
  108. _applyAutoLink( linkHref, range, additionalOffset = 1 ) {
  109. // Enqueue change to make undo step.
  110. this.editor.model.enqueueChange( writer => {
  111. const linkRange = writer.createRange(
  112. range.end.getShiftedBy( -( additionalOffset + linkHref.length ) ),
  113. range.end.getShiftedBy( -additionalOffset )
  114. );
  115. const linkHrefValue = isEmail( linkHref ) ? `mailto://${ linkHref }` : linkHref;
  116. writer.setAttribute( 'linkHref', linkHrefValue, linkRange );
  117. } );
  118. }
  119. }
  120. function isSingleSpaceAtTheEnd( text ) {
  121. return text.length > MIN_LINK_LENGTH_WITH_SPACE_AT_END && text[ text.length - 1 ] === ' ' && text[ text.length - 2 ] !== ' ';
  122. }
  123. function getUrlAtTextEnd( text ) {
  124. const match = URL_REG_EXP.exec( text );
  125. return match ? match[ URL_GROUP_IN_MATCH ] : null;
  126. }
  127. function isEmail( linkHref ) {
  128. return EMAIL_REG_EXP.exec( linkHref );
  129. }