blob: fdc1c8b07daec1555ea4632d7d21e6abd78924d2 [file] [log] [blame]
module.exports = exports = search
var npm = require("./npm.js")
, registry = npm.registry
search.usage = "npm search [some search terms ...]"
search.completion = function (opts, cb) {
var compl = {}
, partial = opts.partialWord
, ipartial = partial.toLowerCase()
, plen = partial.length
// get the batch of data that matches so far.
// this is an example of using npm.commands.search programmatically
// to fetch data that has been filtered by a set of arguments.
search(opts.conf.argv.remain.slice(2), true, function (er, data) {
if (er) return cb(er)
Object.keys(data).forEach(function (name) {
data[name].words.split(" ").forEach(function (w) {
if (w.toLowerCase().indexOf(ipartial) === 0) {
compl[partial + w.substr(plen)] = true
}
})
})
cb(null, Object.keys(compl))
})
}
function search (args, silent, staleness, cb) {
if (typeof cb !== "function") cb = staleness, staleness = 600
if (typeof cb !== "function") cb = silent, silent = false
var searchopts = npm.config.get("searchopts")
, searchexclude = npm.config.get("searchexclude")
if (typeof searchopts !== "string") searchopts = ""
searchopts = searchopts.split(/\s+/)
if (typeof searchexclude === "string") {
searchexclude = searchexclude.split(/\s+/)
} else searchexclude = []
var opts = searchopts.concat(args).map(function (s) {
return s.toLowerCase()
}).filter(function (s) { return s })
searchexclude = searchexclude.map(function (s) {
return s.toLowerCase()
})
getFilteredData( staleness, opts, searchexclude, function (er, data) {
// now data is the list of data that we want to show.
// prettify and print it, and then provide the raw
// data to the cb.
if (er || silent) return cb(er, data)
console.log(prettify(data, args))
cb(null, data)
})
}
function getFilteredData (staleness, args, notArgs, cb) {
registry.get( "/-/all", staleness, false
, true, function (er, data) {
if (er) return cb(er)
return cb(null, filter(data, args, notArgs))
})
}
function filter (data, args, notArgs) {
// data={<name>:{package data}}
return Object.keys(data).map(function (d) {
return data[d]
}).filter(function (d) {
return typeof d === "object"
}).map(stripData).map(getWords).filter(function (data) {
return filterWords(data, args, notArgs)
}).reduce(function (l, r) {
l[r.name] = r
return l
}, {})
}
function stripData (data) {
return { name: data.name
, description: npm.config.get("description") ? data.description : ""
, maintainers: (data.maintainers || []).map(function (m) {
return "=" + m.name
})
, url: !Object.keys(data.versions || {}).length ? data.url : null
, keywords: data.keywords || []
, version: Object.keys(data.versions || {})[0] || []
, time: data.time
&& data.time.modified
&& (new Date(data.time.modified).toISOString()
.split("T").join(" ")
.replace(/:[0-9]{2}\.[0-9]{3}Z$/, ""))
|| "(prehistoric)"
}
}
function getWords (data) {
data.words = [ data.name ]
.concat(data.description)
.concat(data.maintainers)
.concat(data.url && ("<" + data.url + ">"))
.concat(data.keywords)
.map(function (f) { return f && f.trim && f.trim() })
.filter(function (f) { return f })
.join(" ")
.toLowerCase()
return data
}
function filterWords (data, args, notArgs) {
var words = data.words
for (var i = 0, l = args.length; i < l; i ++) {
if (!match(words, args[i])) return false
}
for (var i = 0, l = notArgs.length; i < l; i ++) {
if (match(words, notArgs[i])) return false
}
return true
}
function match (words, arg) {
if (arg.charAt(0) === "/") {
arg = arg.replace(/\/$/, "")
arg = new RegExp(arg.substr(1, arg.length - 1))
return words.match(arg)
}
return words.indexOf(arg) !== -1
}
function prettify (data, args) {
try {
var tty = require("tty")
, stdout = process.stdout
, cols = !tty.isatty(stdout.fd) ? Infinity
: process.stdout.getWindowSize()[0]
cols = (cols == 0) ? Infinity : cols
} catch (ex) { cols = Infinity }
// name, desc, author, keywords
var longest = []
, spaces
, maxLen = npm.config.get("description")
? [20, 60, 20, 20, 10, Infinity]
: [20, 20, 20, 10, Infinity]
, headings = npm.config.get("description")
? ["NAME", "DESCRIPTION", "AUTHOR", "DATE", "VERSION", "KEYWORDS"]
: ["NAME", "AUTHOR", "DATE", "VERSION", "KEYWORDS"]
, lines
, searchsort = (npm.config.get("searchsort") || "NAME").toLowerCase()
, sortFields = { name: 0
, description: 1
, author: 2
, date: 3
, version: 4
, keywords: 5 }
, searchRev = searchsort.charAt(0) === "-"
, sortField = sortFields[searchsort.replace(/^\-+/, "")]
lines = Object.keys(data).map(function (d) {
return data[d]
}).map(function (data) {
// turn a pkg data into a string
// [name,who,desc,targets,keywords] tuple
// also set longest to the longest name
if (typeof data.keywords === "string") {
data.keywords = data.keywords.split(/[,\s]+/)
}
if (!Array.isArray(data.keywords)) data.keywords = []
var l = [ data.name
, data.description || ""
, data.maintainers.join(" ")
, data.time
, data.version || ""
, (data.keywords || []).join(" ")
]
l.forEach(function (s, i) {
var len = s.length
longest[i] = Math.min(maxLen[i] || Infinity
,Math.max(longest[i] || 0, len))
if (len > longest[i]) {
l._undent = l._undent || []
l._undent[i] = len - longest[i]
}
l[i] = ('' + l[i]).replace(/\s+/g, " ")
})
return l
}).sort(function (a, b) {
// a and b are "line" objects of [name, desc, maint, time, kw]
var aa = a[sortField].toLowerCase()
, bb = b[sortField].toLowerCase()
return aa === bb ? 0
: aa < bb ? (searchRev ? 1 : -1)
: (searchRev ? -1 : 1)
}).map(function (line) {
return line.map(function (s, i) {
spaces = spaces || longest.map(function (n) {
return new Array(n + 2).join(" ")
})
var len = s.length
if (line._undent && line._undent[i - 1]) {
len += line._undent[i - 1] - 1
}
return s + spaces[i].substr(len)
}).join(" ").substr(0, cols).trim()
}).map(function (line) {
// colorize!
args.forEach(function (arg, i) {
line = addColorMarker(line, arg, i)
})
return colorize(line).trim()
})
if (lines.length === 0) {
return "No match found for "+(args.map(JSON.stringify).join(" "))
}
// build the heading padded to the longest in each field
return headings.map(function (h, i) {
var space = Math.max(2, 3 + (longest[i] || 0) - h.length)
return h + (new Array(space).join(" "))
}).join("").substr(0, cols).trim() + "\n" + lines.join("\n")
}
var colors = [31, 33, 32, 36, 34, 35 ]
, cl = colors.length
function addColorMarker (str, arg, i) {
var m = i % cl + 1
, markStart = String.fromCharCode(m)
, markEnd = String.fromCharCode(0)
if (arg.charAt(0) === "/") {
//arg = arg.replace(/\/$/, "")
return str.replace( new RegExp(arg.substr(1, arg.length - 1), "gi")
, function (bit) { return markStart + bit + markEnd } )
}
// just a normal string, do the split/map thing
var pieces = str.toLowerCase().split(arg.toLowerCase())
, p = 0
return pieces.map(function (piece, i) {
piece = str.substr(p, piece.length)
var mark = markStart
+ str.substr(p+piece.length, arg.length)
+ markEnd
p += piece.length + arg.length
return piece + mark
}).join("")
}
function colorize (line) {
for (var i = 0; i < cl; i ++) {
var m = i + 1
var color = npm.color ? "\033["+colors[i]+"m" : ""
line = line.split(String.fromCharCode(m)).join(color)
}
var uncolor = npm.color ? "\033[0m" : ""
return line.split("\u0000").join(uncolor)
}