aho-corasick.js 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. /**
  2. * @typedef {Object} Node
  3. * @prop {number} [depth = 0]
  4. * @prop {string} key
  5. * @prop {boolean} [word = false]
  6. * @prop {Record<string, Node>} [children={}]
  7. * @prop {Node} [fail]
  8. * @prop {number} [count=0]
  9. */
  10. /**
  11. * @param {string} key
  12. * @param {number} depth
  13. * @returns {Node}
  14. */
  15. const createNode = (key, depth = 0) => ({
  16. depth,
  17. key,
  18. word: false,
  19. children: {},
  20. fail: undefined,
  21. count: 0
  22. });
  23. /**
  24. * @param {string[]} keys
  25. */
  26. const createKeywordFilter = (keys) => {
  27. const root = createNode('root');
  28. const build = () => {
  29. /** @type {Node[]} */
  30. const queue = [];
  31. queue.push(root);
  32. let idx = 0;
  33. while (queue.length > idx) {
  34. const beginNode = queue[idx];
  35. const map = beginNode.children;
  36. // eslint-disable-next-line guard-for-in -- plain object
  37. for (const key in beginNode.children) {
  38. const node = map[key];
  39. let failNode = beginNode.fail;
  40. while (failNode && !failNode.children[key]) {
  41. failNode = failNode.fail;
  42. }
  43. node.fail = failNode?.children[key] || root;
  44. queue.push(node);
  45. }
  46. idx++;
  47. }
  48. };
  49. /**
  50. * @param {string} key
  51. * @param {number} len
  52. */
  53. const put = (key, len) => {
  54. let node = root;
  55. const lastIdx = len - 1;
  56. node.count++;
  57. for (let idx = 0; idx < len; idx++) {
  58. const val = key[idx];
  59. const nextNode = node.children[val];
  60. if (nextNode) {
  61. nextNode.count++;
  62. node = nextNode;
  63. } else {
  64. const newNode = createNode(val, idx + 1);
  65. newNode.count = 1;
  66. node.children[val] = newNode;
  67. node = newNode;
  68. }
  69. if (lastIdx === idx && node.depth) {
  70. node.word = true;
  71. }
  72. }
  73. };
  74. /**
  75. * @param {string} key
  76. */
  77. const add = (key) => {
  78. const len = key.length;
  79. put(key, len);
  80. build();
  81. return true;
  82. };
  83. for (let idx = 0; idx < keys.length; idx++) {
  84. add(keys[idx], false);
  85. }
  86. build();
  87. /**
  88. * @param {string} text
  89. * @returns {boolean}
  90. */
  91. const search = (text) => {
  92. let node = root;
  93. /** @type {string[]} */
  94. const fText = [];
  95. /** @type {string[]} */
  96. const oText = [];
  97. for (let i = 0, textLen = text.length; i < textLen; i++) {
  98. // const key = text.charAt(i);
  99. const key = text[i];
  100. while (node && !node?.children[key]) {
  101. node = node?.fail;
  102. }
  103. node = node?.children[key] || root;
  104. fText.push(key);
  105. oText.push(key);
  106. if (node.word) {
  107. return true;
  108. }
  109. }
  110. return false;
  111. };
  112. return {
  113. search
  114. };
  115. };
  116. module.exports = createKeywordFilter;