{"version":3,"file":"index.cjs","names":["Extension","Plugin","PluginKey"],"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":";;;;;;;;AAkEA,MAAa,iBAAiBA,aAAAA,UAAU,OAAqD;CAC3F,MAAM;CAEN,aAAa;EACX,OAAO;GACL,OAAO;GACP,UAAU;GACV,MAAM;GACN,cAAa,SAAQ,KAAK;GAC1B,cAAa,SAAQ,KAAK,MAAM,GAAG,CAAC,CAAC,QAAO,SAAQ,SAAS,EAAE,CAAC,CAAC;EACnE;CACF;CAEA,aAAa;EACX,OAAO;GACL,kBAAkB;GAClB,aAAa;EACf;CACF;CAEA,iBAAiB;EACf,KAAK,QAAQ,cAAa,YAAW;GACnC,MAAM,QAAA,YAAA,QAAA,YAAA,KAAA,IAAA,KAAA,IAAO,QAAS,SAAQ,KAAK,OAAO,MAAM;GAGhD,MAAA,YAAA,QAAA,YAAA,KAAA,IAAA,KAAA,IAFa,QAAS,SAAQ,KAAK,QAAQ,UAE9B,YAAY;IACvB,MAAM,OAAO,KAAK,YAAY,GAAG,KAAK,QAAQ,MAAM,KAAA,GAAW,GAAG;IAElE,OAAO,KAAK,QAAQ,YAAY,IAAI;GACtC;GAEA,OAAO,KAAK;EACd;EAEA,KAAK,QAAQ,SAAQ,YAAW;GAC9B,MAAM,QAAA,YAAA,QAAA,YAAA,KAAA,IAAA,KAAA,IAAO,QAAS,SAAQ,KAAK,OAAO,MAAM;GAChD,MAAM,OAAO,KAAK,YAAY,GAAG,KAAK,QAAQ,MAAM,KAAK,GAAG;GAE5D,OAAO,KAAK,QAAQ,YAAY,IAAI;EACtC;CACF;CAEA,wBAAwB;EACtB,IAAI,wBAAwB;EAE5B,OAAO,CACL,IAAIC,iBAAAA,OAAO;GACT,KAAK,IAAIC,iBAAAA,UAAU,gBAAgB;GACnC,oBAAoB,cAAc,UAAU,aAAa;IACvD,IAAI,uBACF;IAGF,MAAM,QAAQ,KAAK,QAAQ;IAC3B,MAAM,WAAW,KAAK,QAAQ;IAE9B,IAAI,UAAU,QAAQ,UAAU,KAAA,KAAa,UAAU,KAAK,aAAa,OAAO;KAC9E,wBAAwB;KACxB;IACF;IAEA,MAAM,qBAAqB,KAAK,QAAQ,WAAW,EAAE,MAAM,SAAS,IAAI,CAAC;IAEzE,IAAI,qBAAqB,OAAO;KAC9B,MAAM,OAAO,qBAAqB;KAClC,MAAM,OAAO;KACb,MAAM,KAAK;KAEX,QAAQ,KACN,sDAAsD,MAAM,gDAC9D;KACA,MAAM,KAAK,SAAS,GAAG,YAAY,MAAM,EAAE;KAC3C,wBAAwB;KACxB,OAAO;IACT;IAEA,wBAAwB;GAC1B;GACA,oBAAoB,aAAa,UAAU;IACzC,MAAM,QAAQ,KAAK,QAAQ;IAG3B,IAAI,CAAC,YAAY,cAAc,UAAU,KAAK,UAAU,QAAQ,UAAU,KAAA,GACxE,OAAO;IAGT,MAAM,UAAU,KAAK,QAAQ,WAAW,EAAE,MAAM,MAAM,IAAI,CAAC;IAC3D,MAAM,UAAU,KAAK,QAAQ,WAAW,EAAE,MAAM,YAAY,IAAI,CAAC;IAGjE,IAAI,WAAW,OACb,OAAO;IAIT,IAAI,UAAU,SAAS,UAAU,SAAS,WAAW,SACnD,OAAO;IAIT,IAAI,UAAU,SAAS,UAAU,SAAS,UAAU,SAClD,OAAO;IAMT,IAAI,CAHY,YAAY,QAAQ,OAGzB,GACT,OAAO;IAIT,MAAM,MAAM,YAAY,UAAU,MAAM;IAExC,MAAM,OAAO,OADA,UAAU;IAEvB,MAAM,KAAK;IAIX,YAAY,YAAY,MAAM,EAAE;IAQhC,IAFoB,KAAK,QAAQ,WAAW,EAAE,MAAM,YAAY,IAAI,CAEtD,IAAI,OAChB,OAAO;IAGT,OAAO;GACT;EACF,CAAC,CACH;CACF;AACF,CAAC"}