text: /**
   * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
   * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
   */
  
  /**
   * @module link/linkediting
   */
  
  import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  import LinkCommand from './linkcommand';
  import UnlinkCommand from './unlinkcommand';
  import { createLinkElement, ensureSafeUrl, getLocalizedDecorators, normalizeDecorators } from './utils';
  import AutomaticDecorators from './utils/automaticdecorators';
  import ManualDecorator from './utils/manualdecorator';
  import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute';
  import findLinkRange from './findlinkrange';
  import '../theme/link.css';
  
  const HIGHLIGHT_CLASS = 'ck-link_selected';
  const DECORATOR_AUTOMATIC = 'automatic';
  const DECORATOR_MANUAL = 'manual';
  const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
  
  /**
   * The link engine feature.
   *
   * It introduces the `linkHref="url"` attribute in the model which renders to the view as a `<a href="url">` element
   * as well as `'link'` and `'unlink'` commands.
   *
   * @extends module:core/plugin~Plugin
   */
  export default class LinkEditing extends Plugin {
    /**
     * @inheritDoc
     */
    static get pluginName() {
      return 'LinkEditing';
    }
  
    /**
     * @inheritDoc
     */
    constructor(editor) {
      super(editor);
  
      editor.config.define('link', {
        addTargetToExternalLinks: false,
      });
    }
  
    /**
     * @inheritDoc
     */
    init() {
      const editor = this.editor;
      const locale = editor.locale;
      const { fileIcons, fileIconStyles } = this.editor.config.get('link');
  
      // Allow link attribute on all inline nodes.
      editor.model.schema.extend('$text', { allowAttributes: 'linkHref' });
      editor.model.schema.register('fileIcon', {
        inheritAllFrom: '$text',
        allowAttributes: [...Object.keys(fileIconStyles), 'fileExtension'],
        isObject: true,
  			isBlock: true,
      });
  
      // fileExtension conversions
      editor.conversion.elementToElement({
        model: 'fileIcon',
        view: 'i',
      });
  
      editor.conversion.attributeToAttribute({
        view: Object.entries(fileIcons).reduce((result, [extension, className]) => {
          result[extension] = {
            key: 'class',
            value: className,
          };
  
          return result;
        }, {}),
        model: {
          key: 'fileExtension',
          values: Object.keys(fileIcons),
        },
      });
  
      // linkHref conversions
      editor.conversion.for('dataDowncast').attributeToElement({ model: 'linkHref', view: createLinkElement });
      editor.conversion.for('editingDowncast').attributeToElement({
        model: 'linkHref',
        view: (href, writer) => {
          return createLinkElement(ensureSafeUrl(href), writer);
        },
      });
  
      editor.conversion.for('upcast').elementToAttribute({
        view: {
          name: 'a',
          attributes: {
            href: true,
          },
        },
        model: {
          key: 'linkHref',
          value: viewElement => viewElement.getAttribute('href'),
        },
      });
  
      // Create linking commands.
      editor.commands.add('link', new LinkCommand(editor));
      editor.commands.add('unlink', new UnlinkCommand(editor));
  
      const linkDecorators = getLocalizedDecorators(editor.t, normalizeDecorators(editor.config.get('link.decorators')));
  
      this._enableAutomaticDecorators(linkDecorators.filter(item => item.mode === DECORATOR_AUTOMATIC));
      this._enableManualDecorators(linkDecorators.filter(item => item.mode === DECORATOR_MANUAL));
  
      // Enable two-step caret movement for `linkHref` attribute.
      bindTwoStepCaretToAttribute({
        view: editor.editing.view,
        model: editor.model,
        emitter: this,
        attribute: 'linkHref',
        locale,
      });
  
      // Setup highlight over selected link.
      this._setupLinkHighlight();
  
      // Change the attributes of the selection in certain situations after the link was inserted into the document.
      this._enableInsertContentSelectionAttributesFixer();
    }
  
    /**
     * Processes an array of configured {@link module:link/link~LinkDecoratorAutomaticDefinition automatic decorators}
     * and registers a {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher}
     * for each one of them. Downcast dispatchers are obtained using the
     * {@link module:link/utils~AutomaticDecorators#getDispatcher} method.
     *
     * **Note**: This method also activates the automatic external link decorator if enabled with
     * {@link module:link/link~LinkConfig#addTargetToExternalLinks `config.link.addTargetToExternalLinks`}.
     *
     * @private
     * @param {Array.<module:link/link~LinkDecoratorAutomaticDefinition>} automaticDecoratorDefinitions
     */
    _enableAutomaticDecorators(automaticDecoratorDefinitions) {
      const editor = this.editor;
      const automaticDecorators = new AutomaticDecorators();
  
      // Adds a default decorator for external links.
      if (editor.config.get('link.addTargetToExternalLinks')) {
        automaticDecorators.add({
          id: 'linkIsExternal',
          mode: DECORATOR_AUTOMATIC,
          callback: url => EXTERNAL_LINKS_REGEXP.test(url),
          attributes: {
            target: '_blank',
            rel: 'noopener noreferrer',
          },
        });
      }
  
      automaticDecorators.add(automaticDecoratorDefinitions);
  
      if (automaticDecorators.length) {
        editor.conversion.for('downcast').add(automaticDecorators.getDispatcher());
      }
    }
  
    /**
     * Processes an array of configured {@link module:link/link~LinkDecoratorManualDefinition manual decorators},
     * transforms them into {@link module:link/utils~ManualDecorator} instances and stores them in the
     * {@link module:link/linkcommand~LinkCommand#manualDecorators} collection (a model for manual decorators state).
     *
     * Also registers an {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement attribute-to-element}
     * converter for each manual decorator and extends the {@link module:engine/model/schema~Schema model's schema}
     * with adequate model attributes.
     *
     * @private
     * @param {Array.<module:link/link~LinkDecoratorManualDefinition>} manualDecoratorDefinitions
     */
    _enableManualDecorators(manualDecoratorDefinitions) {
      if (!manualDecoratorDefinitions.length) {
        return;
      }
  
      const editor = this.editor;
      const command = editor.commands.get('link');
      const manualDecorators = command.manualDecorators;
  
      manualDecoratorDefinitions.forEach(decorator => {
        editor.model.schema.extend('$text', { allowAttributes: decorator.id });
  
        // Keeps reference to manual decorator to decode its name to attributes during downcast.
        manualDecorators.add(new ManualDecorator(decorator));
  
        editor.conversion.for('downcast').attributeToElement({
          model: decorator.id,
          view: (manualDecoratorName, writer) => {
            if (manualDecoratorName) {
              const attributes = manualDecorators.get(decorator.id).attributes;
              const element = writer.createAttributeElement('a', attributes, { priority: 5 });
              writer.setCustomProperty('link', true, element);
  
              return element;
            }
          },
        });
  
        editor.conversion.for('upcast').elementToAttribute({
          view: {
            name: 'a',
            attributes: manualDecorators.get(decorator.id).attributes,
          },
          model: {
            key: decorator.id,
          },
        });
      });
    }
  
    /**
     * Adds a visual highlight style to a link in which the selection is anchored.
     * Together with two-step caret movement, they indicate that the user is typing inside the link.
     *
     * Highlight is turned on by adding the `.ck-link_selected` class to the link in the view:
     *
     * * The class is removed before the conversion has started, as callbacks added with the `'highest'` priority
     * to {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} events.
     * * The class is added in the view post fixer, after other changes in the model tree were converted to the view.
     *
     * This way, adding and removing the highlight does not interfere with conversion.
     *
     * @private
     */
    _setupLinkHighlight() {
      const editor = this.editor;
      const view = editor.editing.view;
      const highlightedLinks = new Set();
  
      // Adding the class.
      view.document.registerPostFixer(writer => {
        const selection = editor.model.document.selection;
        let changed = false;
  
        if (selection.hasAttribute('linkHref')) {
          const modelRange = findLinkRange(
            selection.getFirstPosition(),
            selection.getAttribute('linkHref'),
            editor.model
          );
          const viewRange = editor.editing.mapper.toViewRange(modelRange);
  
          // There might be multiple `a` elements in the `viewRange`, for example, when the `a` element is
          // broken by a UIElement.
          for (const item of viewRange.getItems()) {
            if (item.is('a') && !item.hasClass(HIGHLIGHT_CLASS)) {
              writer.addClass(HIGHLIGHT_CLASS, item);
              highlightedLinks.add(item);
              changed = true;
            }
          }
        }
  
        return changed;
      });
  
      // Removing the class.
      editor.conversion.for('editingDowncast').add(dispatcher => {
        // Make sure the highlight is removed on every possible event, before conversion is started.
        dispatcher.on('insert', removeHighlight, { priority: 'highest' });
        dispatcher.on('remove', removeHighlight, { priority: 'highest' });
        dispatcher.on('attribute', removeHighlight, { priority: 'highest' });
        dispatcher.on('selection', removeHighlight, { priority: 'highest' });
  
        function removeHighlight() {
          view.change(writer => {
            for (const item of highlightedLinks.values()) {
              writer.removeClass(HIGHLIGHT_CLASS, item);
              highlightedLinks.delete(item);
            }
          });
        }
      });
    }
  
    /**
     * Starts listening to {@link module:engine/model/model~Model#event:insertContent} and corrects the model
     * selection attributes if the selection is at the end of a link after inserting the content.
     *
     * The purpose of this action is to improve the overall UX because the user is no longer "trapped" by the
     * `linkHref` attribute of the selection and they can type a "clean" (`linkHref`–less) text right away.
     *
     * See https://github.com/ckeditor/ckeditor5/issues/6053.
     *
     * @private
     */
    _enableInsertContentSelectionAttributesFixer() {
      const editor = this.editor;
      const model = editor.model;
      const selection = model.document.selection;
  
      model.on(
        'insertContent',
        () => {
          const nodeBefore = selection.anchor.nodeBefore;
          const nodeAfter = selection.anchor.nodeAfter;
  
          // NOTE: ↰ and ↱ represent the gravity of the selection.
  
          // The only truly valid case is:
          //
          //		                                 ↰
          //		...<$text linkHref="foo">INSERTED[]</$text>
          //
          // If the selection is not "trapped" by the `linkHref` attribute after inserting, there's nothing
          // to fix there.
          if (!selection.hasAttribute('linkHref')) {
            return;
          }
  
          // Filter out the following case where a link with the same href (e.g. <a href="foo">INSERTED</a>) is inserted
          // in the middle of an existing link:
          //
          // Before insertion:
          //		                       ↰
          //		<$text linkHref="foo">l[]ink</$text>
          //
          // Expected after insertion:
          //		                               ↰
          //		<$text linkHref="foo">lINSERTED[]ink</$text>
          //
          if (!nodeBefore) {
            return;
          }
  
          // Filter out the following case where the selection has the "linkHref" attribute because the
          // gravity is overridden and some text with another attribute (e.g. <b>INSERTED</b>) is inserted:
          //
          // Before insertion:
          //
          //		                       ↱
          //		<$text linkHref="foo">[]link</$text>
          //
          // Expected after insertion:
          //
          //		                                                          ↱
          //		<$text bold="true">INSERTED</$text><$text linkHref="foo">[]link</$text>
          //
          if (!nodeBefore.hasAttribute('linkHref')) {
            return;
          }
  
          // Filter out the following case where a link is a inserted in the middle (or before) another link
          // (different URLs, so they will not merge). In this (let's say weird) case, we can leave the selection
          // attributes as they are because the user will end up writing in one link or another anyway.
          //
          // Before insertion:
          //
          //		                       ↰
          //		<$text linkHref="foo">l[]ink</$text>
          //
          // Expected after insertion:
          //
          //		                                                             ↰
          //		<$text linkHref="foo">l</$text><$text linkHref="bar">INSERTED[]</$text><$text linkHref="foo">ink</$text>
          //
          if (nodeAfter && nodeAfter.hasAttribute('linkHref')) {
            return;
          }
  
          // Make the selection free of link-related model attributes.
          // All link-related model attributes start with "link". That includes not only "linkHref"
          // but also all decorator attributes (they have dynamic names).
          model.change(writer => {
            [...model.document.selection.getAttributeKeys()]
              .filter(name => name.startsWith('link'))
              .forEach(name => writer.removeSelectionAttribute(name));
          });
        },
        { priority: 'low' }
      );
    }
  }
  