Skip to content

Commit

Permalink
Refactor to move implementation to lib/
Browse files Browse the repository at this point in the history
  • Loading branch information
wooorm committed Jan 19, 2023
1 parent 0407823 commit 691f44d
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 149 deletions.
155 changes: 6 additions & 149 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,154 +1,11 @@
/**
* @typedef {import('unist').Parent} UnistParent
* @typedef {import('nlcst').Root} Root
* @typedef {import('nlcst').Word} Word
* @typedef {import('nlcst').Content} Content
* @typedef {Root|Content} Node
* @typedef {Extract<Node, UnistParent>} Parent
* @typedef {import('nlcst-normalize').NormalizeOptions} NormalizeOptions
* @typedef {import('./lib/index.js').Handler} Handler
* @typedef {import('./lib/index.js').Options} Options
* @typedef {import('./lib/index.js').PhrasesList} PhrasesList
* @typedef {import('./lib/index.js').PhrasesMap} PhrasesMap
*
* @typedef {boolean} AllowApostrophes
* @typedef {NormalizeOptions & {allowLiterals?: boolean}} Options
* Configuration (optional).
* @typedef {Array<string>} PhrasesList
* List of phrases.
* @typedef {Record<string, unknown>} PhrasesMap
* Map where the keys are phrases.
* @typedef {(nodes: Array<Content>, index: number, parent: Parent, pattern: string) => void} Handler
* Function called when a pattern matches.
*
* @typedef {Options} SearchOptions
* @typedef {import('./lib/index.js').Options} SearchOptions
* Deprecated form of `Options`.
*/

import {visit} from 'unist-util-visit'
import {normalize} from 'nlcst-normalize'
import {isLiteral} from 'nlcst-is-literal'

const own = {}.hasOwnProperty

/**
* @param {Node} tree
* @param {PhrasesList|PhrasesMap} phrases
* @param {Handler} handler
* @param {AllowApostrophes|Options} [options=false]
*/
export function search(tree, phrases, handler, options) {
/** @type {Record<string, Array<string>>} */
const byWord = {'*': []}
let index = -1
/** @type {string} */
let key
/** @type {Options} */
let config

if (typeof options === 'boolean') {
config = options ? {allowApostrophes: true} : {}
} else {
config = options || {}
}

if (!tree || !tree.type) {
throw new Error('Expected node')
}

if (typeof phrases !== 'object') {
throw new TypeError('Expected object for phrases')
}

if (Array.isArray(phrases)) {
while (++index < phrases.length) {
handlePhrase(phrases[index])
}
} else {
for (key in phrases) {
if (own.call(phrases, key)) {
handlePhrase(key)
}
}
}

// Search the tree.
visit(tree, 'WordNode', (node, position, parent_) => {
const parent = /** @type {Parent} */ (parent_)

if (
!parent ||
position === null ||
(!config.allowLiterals && isLiteral(parent, position))
) {
return
}

const word = normalize(node, config)
const phrases = byWord['*'].concat(
own.call(byWord, word) ? byWord[word] : []
)
let index = -1

while (++index < phrases.length) {
const result = test(phrases[index], position, parent)

if (result) {
handler(result, position, parent, phrases[index])
}
}
})

/**
* Test a phrase.
*
* @param {string} phrase
* @param {number} position
* @param {Parent} parent
*/
function test(phrase, position, parent) {
const siblings = parent.children
const start = position
const expressions = phrase.split(' ').slice(1)
let index = -1

// Move one position forward.
position++

// Iterate over `expressions`.
while (++index < expressions.length) {
// Allow joining white-space.
while (position < siblings.length) {
if (siblings[position].type !== 'WhiteSpaceNode') break
position++
}

// Exit if there are no nodes left, if the current node is not a word, or
// if the current word does not match the search for value.
if (
!siblings[position] ||
siblings[position].type !== 'WordNode' ||
(expressions[index] !== '*' &&
normalize(expressions[index], config) !==
normalize(siblings[position], config))
) {
return
}

position++
}

return siblings.slice(start, position)
}

/**
* Handle a phrase.
*
* @param {string} phrase
*/
function handlePhrase(phrase) {
const firstWord = normalize(phrase.split(' ', 1)[0], config)

if (own.call(byWord, firstWord)) {
byWord[firstWord].push(phrase)
} else {
byWord[firstWord] = [phrase]
}
}
}
export {search} from './lib/index.js'
151 changes: 151 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* @typedef {import('unist').Parent} UnistParent
* @typedef {import('nlcst').Root} Root
* @typedef {import('nlcst').Word} Word
* @typedef {import('nlcst').Content} Content
* @typedef {Root|Content} Node
* @typedef {Extract<Node, UnistParent>} Parent
* @typedef {import('nlcst-normalize').NormalizeOptions} NormalizeOptions
*
* @typedef {boolean} AllowApostrophes
* @typedef {NormalizeOptions & {allowLiterals?: boolean}} Options
* Configuration (optional).
* @typedef {Array<string>} PhrasesList
* List of phrases.
* @typedef {Record<string, unknown>} PhrasesMap
* Map where the keys are phrases.
* @typedef {(nodes: Array<Content>, index: number, parent: Parent, pattern: string) => void} Handler
* Function called when a pattern matches.
*/

import {visit} from 'unist-util-visit'
import {normalize} from 'nlcst-normalize'
import {isLiteral} from 'nlcst-is-literal'

const own = {}.hasOwnProperty

/**
* @param {Node} tree
* @param {PhrasesList|PhrasesMap} phrases
* @param {Handler} handler
* @param {AllowApostrophes|Options} [options=false]
*/
export function search(tree, phrases, handler, options) {
/** @type {Record<string, Array<string>>} */
const byWord = {'*': []}
let index = -1
/** @type {string} */
let key
/** @type {Options} */
let config

if (typeof options === 'boolean') {
config = options ? {allowApostrophes: true} : {}
} else {
config = options || {}
}

if (!tree || !tree.type) {
throw new Error('Expected node')
}

if (typeof phrases !== 'object') {
throw new TypeError('Expected object for phrases')
}

if (Array.isArray(phrases)) {
while (++index < phrases.length) {
handlePhrase(phrases[index])
}
} else {
for (key in phrases) {
if (own.call(phrases, key)) {
handlePhrase(key)
}
}
}

// Search the tree.
visit(tree, 'WordNode', (node, position, parent_) => {
const parent = /** @type {Parent} */ (parent_)

if (
!parent ||
position === null ||
(!config.allowLiterals && isLiteral(parent, position))
) {
return
}

const word = normalize(node, config)
const phrases = byWord['*'].concat(
own.call(byWord, word) ? byWord[word] : []
)
let index = -1

while (++index < phrases.length) {
const result = test(phrases[index], position, parent)

if (result) {
handler(result, position, parent, phrases[index])
}
}
})

/**
* Test a phrase.
*
* @param {string} phrase
* @param {number} position
* @param {Parent} parent
*/
function test(phrase, position, parent) {
const siblings = parent.children
const start = position
const expressions = phrase.split(' ').slice(1)
let index = -1

// Move one position forward.
position++

// Iterate over `expressions`.
while (++index < expressions.length) {
// Allow joining white-space.
while (position < siblings.length) {
if (siblings[position].type !== 'WhiteSpaceNode') break
position++
}

// Exit if there are no nodes left, if the current node is not a word, or
// if the current word does not match the search for value.
if (
!siblings[position] ||
siblings[position].type !== 'WordNode' ||
(expressions[index] !== '*' &&
normalize(expressions[index], config) !==
normalize(siblings[position], config))
) {
return
}

position++
}

return siblings.slice(start, position)
}

/**
* Handle a phrase.
*
* @param {string} phrase
*/
function handlePhrase(phrase) {
const firstWord = normalize(phrase.split(' ', 1)[0], config)

if (own.call(byWord, firstWord)) {
byWord[firstWord].push(phrase)
} else {
byWord[firstWord] = [phrase]
}
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"main": "index.js",
"types": "index.d.ts",
"files": [
"lib/",
"index.d.ts",
"index.js"
],
Expand Down

0 comments on commit 691f44d

Please sign in to comment.