aho-corasick.js 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  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[] | Set<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. if (node) {
  44. node.fail = failNode?.children?.[key] || root;
  45. queue.push(node);
  46. }
  47. }
  48. idx++;
  49. }
  50. };
  51. /**
  52. * @param {string} key
  53. * @param {number} len
  54. */
  55. const put = (key, len) => {
  56. let node = root;
  57. const lastIdx = len - 1;
  58. node.count++;
  59. for (let idx = 0; idx < len; idx++) {
  60. const val = key[idx];
  61. const nextNode = node.children[val];
  62. if (nextNode) {
  63. nextNode.count++;
  64. node = nextNode;
  65. } else {
  66. const newNode = createNode(val, idx + 1);
  67. newNode.count = 1;
  68. node.children[val] = newNode;
  69. node = newNode;
  70. }
  71. if (lastIdx === idx && node.depth) {
  72. node.word = true;
  73. }
  74. }
  75. };
  76. keys.forEach(k => {
  77. put(k, k.length);
  78. });
  79. build();
  80. /**
  81. * @param {string} text
  82. * @returns {boolean}
  83. */
  84. const search = (text) => {
  85. let node = root;
  86. for (let i = 0, textLen = text.length; i < textLen; i++) {
  87. // const key = text.charAt(i);
  88. const key = text[i];
  89. while (node && !node?.children[key]) {
  90. node = node?.fail;
  91. }
  92. node = node?.children[key] || root;
  93. if (node.word) {
  94. return true;
  95. }
  96. }
  97. return false;
  98. };
  99. return {
  100. search
  101. };
  102. };
  103. module.exports = createKeywordFilter;