{"version":3,"sources":["../../src/character-count/character-count.ts"],"sourcesContent":["import { Extension } from '@tiptap/core'\nimport type { Node as ProseMirrorNode } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nexport interface CharacterCountOptions {\n  /**\n   * The maximum number of characters that should be allowed. Defaults to `0`.\n   * @default null\n   * @example 180\n   */\n  limit: number | null | undefined\n  /**\n   * The mode by which the size is calculated. If set to `textSize`, the textContent of the document is used.\n   * If set to `nodeSize`, the nodeSize of the document is used.\n   * @default 'textSize'\n   * @example 'textSize'\n   */\n  mode: 'textSize' | 'nodeSize'\n  /**\n   * Sets whether the content will be automatically trimmed when programatically setting content over the limit.\n   * If set to false, the user will be able to trim the text manually.\n   * @default true\n   * @example false\n   */\n  autoTrim?: boolean\n  /**\n   * The text counter function to use. Defaults to a simple character count.\n   * @default (text) => text.length\n   * @example (text) => [...new Intl.Segmenter().segment(text)].length\n   */\n  textCounter: (text: string) => number\n  /**\n   * The word counter function to use. Defaults to a simple word count.\n   * @default (text) => text.split(' ').filter(word => word !== '').length\n   * @example (text) => text.split(/\\s+/).filter(word => word !== '').length\n   */\n  wordCounter: (text: string) => number\n}\n\nexport interface CharacterCountStorage {\n  /**\n   * Get the number of characters for the current document.\n   * @param options The options for the character count. (optional)\n   * @param options.node The node to get the characters from. Defaults to the current document.\n   * @param options.mode The mode by which the size is calculated. If set to `textSize`, the textContent of the document is used.\n   */\n  characters: (options?: { node?: ProseMirrorNode; mode?: 'textSize' | 'nodeSize' }) => number\n\n  /**\n   * Get the number of words for the current document.\n   * @param options The options for the character count. (optional)\n   * @param options.node The node to get the words from. Defaults to the current document.\n   */\n  words: (options?: { node?: ProseMirrorNode }) => number\n}\n\ndeclare module '@tiptap/core' {\n  interface Storage {\n    characterCount: CharacterCountStorage\n  }\n}\n\n/**\n * This extension allows you to count the characters and words of your document.\n * @see https://tiptap.dev/api/extensions/character-count\n */\nexport const CharacterCount = Extension.create<CharacterCountOptions, CharacterCountStorage>({\n  name: 'characterCount',\n\n  addOptions() {\n    return {\n      limit: null,\n      autoTrim: true,\n      mode: 'textSize',\n      textCounter: text => text.length,\n      wordCounter: text => text.split(' ').filter(word => word !== '').length,\n    }\n  },\n\n  addStorage() {\n    return {\n      characters: () => 0,\n      words: () => 0,\n    }\n  },\n\n  onBeforeCreate() {\n    this.storage.characters = options => {\n      const node = options?.node || this.editor.state.doc\n      const mode = options?.mode || this.options.mode\n\n      if (mode === 'textSize') {\n        const text = node.textBetween(0, node.content.size, undefined, ' ')\n\n        return this.options.textCounter(text)\n      }\n\n      return node.nodeSize\n    }\n\n    this.storage.words = options => {\n      const node = options?.node || this.editor.state.doc\n      const text = node.textBetween(0, node.content.size, ' ', ' ')\n\n      return this.options.wordCounter(text)\n    }\n  },\n\n  addProseMirrorPlugins() {\n    let initialEvaluationDone = false\n\n    return [\n      new Plugin({\n        key: new PluginKey('characterCount'),\n        appendTransaction: (transactions, oldState, newState) => {\n          if (initialEvaluationDone) {\n            return\n          }\n\n          const limit = this.options.limit\n          const autoTrim = this.options.autoTrim\n\n          if (limit === null || limit === undefined || limit === 0 || autoTrim === false) {\n            initialEvaluationDone = true\n            return\n          }\n\n          const initialContentSize = this.storage.characters({ node: newState.doc })\n\n          if (initialContentSize > limit) {\n            const over = initialContentSize - limit\n            const from = 0\n            const to = over\n\n            console.warn(\n              `[CharacterCount] Initial content exceeded limit of ${limit} characters. Content was automatically trimmed.`,\n            )\n            const tr = newState.tr.deleteRange(from, to)\n            initialEvaluationDone = true\n            return tr\n          }\n\n          initialEvaluationDone = true\n        },\n        filterTransaction: (transaction, state) => {\n          const limit = this.options.limit\n\n          // Nothing has changed or no limit is defined. Ignore it.\n          if (!transaction.docChanged || limit === 0 || limit === null || limit === undefined) {\n            return true\n          }\n\n          const oldSize = this.storage.characters({ node: state.doc })\n          const newSize = this.storage.characters({ node: transaction.doc })\n\n          // Everything is in the limit. Good.\n          if (newSize <= limit) {\n            return true\n          }\n\n          // The limit has already been exceeded but will be reduced.\n          if (oldSize > limit && newSize > limit && newSize <= oldSize) {\n            return true\n          }\n\n          // The limit has already been exceeded and will be increased further.\n          if (oldSize > limit && newSize > limit && newSize > oldSize) {\n            return false\n          }\n\n          const isPaste = transaction.getMeta('paste')\n\n          // Block all exceeding transactions that were not pasted.\n          if (!isPaste) {\n            return false\n          }\n\n          // For pasted content, we try to remove the exceeding content.\n          const pos = transaction.selection.$head.pos\n          const over = newSize - limit\n          const from = pos - over\n          const to = pos\n\n          // It’s probably a bad idea to mutate transactions within `filterTransaction`\n          // but for now this is working fine.\n          transaction.deleteRange(from, to)\n\n          // In some situations, the limit will continue to be exceeded after trimming.\n          // This happens e.g. when truncating within a complex node (e.g. table)\n          // and ProseMirror has to close this node again.\n          // If this is the case, we prevent the transaction completely.\n          const updatedSize = this.storage.characters({ node: transaction.doc })\n\n          if (updatedSize > limit) {\n            return false\n          }\n\n          return true\n        },\n      }),\n    ]\n  },\n})\n"],"mappings":"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","names":[]}