Skip to content

Commit

Permalink
Switch to filter text/buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
Mottie committed Mar 23, 2020
1 parent 2fd08cb commit 5047ceb
Show file tree
Hide file tree
Showing 10 changed files with 532 additions and 388 deletions.
4 changes: 2 additions & 2 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1253,8 +1253,8 @@
"description": "Text for button to apply the selected action"
},
"bulkActionsTooltip": {
"message": "Click to open the filter, search and bulk actions panel",
"description": "Text for button to apply the selected action"
"message": "Bulk actions can be applied to selected styles in this column",
"description": "Select style for bulk action header tooltip"
},
"bulkActionsError": {
"message": "Choose at least one style",
Expand Down
224 changes: 168 additions & 56 deletions background/search-db.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,108 @@
(() => {
// toLocaleLowerCase cache, autocleared after 1 minute
const cache = new Map();
// top-level style properties to be searched
const PARTS = {
name: searchText,
url: searchText,
sourceCode: searchText,
sections: searchSections,
};

// Creates an array of intermediate words (2 letter minimum)
// 'usercss' => ["us", "use", "user", "userc", "usercs", "usercss"]
// this makes it so the user can type partial queries and not have the search
// constantly switching between using & ignoring the filter
const createPartials = id => id.split('').reduce((acc, _, index) => {
if (index > 0) {
acc.push(id.substring(0, index + 1));
}
return acc;
}, []);

const searchWithin = [{
id: 'code',
labels: createPartials('code'),
get: style => style.sections.map(section => section.code).join(' ')
}, {
id: 'usercss',
labels: [...createPartials('usercss'), ...createPartials('meta')],
get: style => JSON.stringify(style.usercssData || {})
// remove JSON structure; restore urls
.replace(/[[\]{},":]/g, ' ').replace(/\s\/\//g, '://')
}, {
id: 'name', // default
labels: createPartials('name'),
get: style => style.name
}];

const styleProps = [{
id: 'enabled',
labels: ['on', ...createPartials('enabled')],
check: style => style.enabled
}, {
id: 'disabled',
labels: ['off', ...createPartials('disabled')],
check: style => !style.enabled
}, {
id: 'local',
labels: createPartials('local'),
check: style => !style.updateUrl
}, {
id: 'external',
labels: createPartials('external'),
check: style => style.updateUrl
}, {
id: 'usercss',
labels: createPartials('usercss'),
check: style => style.usercssData
}, {
id: 'non usercss',
labels: ['original', ...createPartials('nonusercss')],
check: style => !style.usercssData
}];

const matchers = [{
id: 'url',
test: query => /url:\w+/i.test(query),
matches: query => {
const matchUrl = query.match(/url:([/.-_\w]+)/);
const result = matchUrl && matchUrl[1]
? styleManager.getStylesByUrl(matchUrl[1])
.then(result => result.map(r => r.data.id))
: [];
return {result};
},
}, {
id: 'regex',
test: query => {
const x = query.includes('/') && !query.includes('//') &&
/^\/(.+?)\/([gimsuy]*)$/.test(query);
// console.log('regex match?', query, x);
return x;
},
matches: () => ({regex: tryRegExp(RegExp.$1, RegExp.$2)})
}, {
id: 'props',
test: query => /is:/.test(query),
matches: query => {
const label = /is:(\w+)/g.exec(query);
return label && label[1]
? {prop: styleProps.find(p => p.labels.includes(label[1]))}
: {};
}
}, {
id: 'within',
test: query => /in:/.test(query),
matches: query => {
const label = /in:(\w+)/g.exec(query);
return label && label[1]
? {within: searchWithin.find(s => s.labels.includes(label[1]))}
: {};
}
}, {
id: 'default',
test: () => true,
matches: query => {
const word = query.startsWith('"') && query.endsWith('"')
? query.slice(1, -1)
: query;
return {word: word || query};
}
}];

/**
* @param params
Expand All @@ -19,77 +114,94 @@
* @returns {number[]} - array of matched styles ids
*/
API_METHODS.searchDB = ({query, ids}) => {
let rx, words, icase, matchUrl;
query = query.trim();
const parts = query.trim().split(/(".*?")|\s+/).filter(Boolean);

if (/^url:/i.test(query)) {
matchUrl = query.slice(query.indexOf(':') + 1).trim();
if (matchUrl) {
return styleManager.getStylesByUrl(matchUrl)
.then(results => results.map(r => r.data.id));
const searchFilters = {
words: [],
regex: null, // only last regex expression is used
results: [],
props: [],
within: [],
};

const searchText = (text, searchFilters) => {
if (searchFilters.regex) return searchFilters.regex.test(text);
for (let pass = 1; pass <= (searchFilters.icase ? 2 : 1); pass++) {
if (searchFilters.words.every(w => text.includes(w))) return true;
text = lower(text);
}
};

const searchProps = (style, searchFilters) => {
const x = searchFilters.props.every(prop => {
const y = prop.check(style)
// if (y) console.log('found prop', prop.id, style.id)
return y;
});
// if (x) console.log('found prop', style.id)
return x;
};

parts.forEach(part => {
matchers.some(matcher => {
if (matcher.test(part)) {
const {result, regex, word, prop, within} = matcher.matches(part || '');
if (result) searchFilters.results.push(result);
if (regex) searchFilters.regex = regex; // limited to a single regexp
if (word) searchFilters.words.push(word);
if (prop) searchFilters.props.push(prop);
if (within) searchFilters.within.push(within);
return true;
}
});
});
if (!searchFilters.within.length) {
searchFilters.within.push(...searchWithin.slice(-1));
}
if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) {
rx = tryRegExp(RegExp.$1, RegExp.$2);
}
if (!rx) {
words = query
.split(/(".*?")|\s+/)
.filter(Boolean)
.map(w => w.startsWith('"') && w.endsWith('"')
? w.slice(1, -1)
: w)
.filter(w => w.length > 1);
words = words.length ? words : [query];
icase = words.some(w => w === lower(w));

// console.log('matchers', searchFilters);
// url matches
if (searchFilters.results.length) {
return searchFilters.results;
}
searchFilters.icase = searchFilters.words.some(w => w === lower(w));
query = parts.join(' ').trim();

return styleManager.getAllStyles().then(styles => {
if (ids) {
const idSet = new Set(ids);
styles = styles.filter(s => idSet.has(s.id));
}

const results = [];
const propResults = [];
const hasProps = searchFilters.props.length > 0;
const noWords = searchFilters.words.length === 0;
for (const style of styles) {
const id = style.id;
if (!query || words && !words.length) {
if (noWords) {
// no query or only filters are matching -> show all styles
results.push(id);
continue;
}
for (const part in PARTS) {
const text = style[part];
if (text && PARTS[part](text, rx, words, icase)) {
} else {
const text = searchFilters.within.map(within => within.get(style)).join(' ');
if (searchText(text, searchFilters)) {
results.push(id);
break;
}
}
if (hasProps && searchProps(style, searchFilters) && results.includes(id)) {
propResults.push(id);
}
}
// results AND propResults
const finalResults = hasProps
? propResults.filter(id => results.includes(id))
: results;
if (cache.size) debounce(clearCache, 60e3);
return results;
// console.log('final', finalResults)
return finalResults;
});
};

function searchText(text, rx, words, icase) {
if (rx) return rx.test(text);
for (let pass = 1; pass <= (icase ? 2 : 1); pass++) {
if (words.every(w => text.includes(w))) return true;
text = lower(text);
}
}

function searchSections(sections, rx, words, icase) {
for (const section of sections) {
for (const prop in section) {
const value = section[prop];
if (typeof value === 'string') {
if (searchText(value, rx, words, icase)) return true;
} else if (Array.isArray(value)) {
if (value.some(str => searchText(str, rx, words, icase))) return true;
}
}
}
}

function lower(text) {
let result = cache.get(text);
if (result) return result;
Expand Down
Loading

0 comments on commit 5047ceb

Please sign in to comment.