{"version":3,"file":"index.js","sources":["../src/character-count.ts"],"sourcesContent":["import { Extension } from '@tiptap/core'\nimport { 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 * 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\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      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\n          if (limit === null || limit === undefined || limit === 0) {\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(`[CharacterCount] Initial content exceeded limit of ${limit} characters. Content was automatically trimmed.`)\n            const tr = newState.tr.deleteRange(from, to)\n\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"],"names":[],"mappings":"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"}