Page MenuHomeSoftware Heritage

D7490.id27490.diff
No OneTemporary

D7490.id27490.diff

diff --git a/.gitignore b/.gitignore
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,4 @@
cypress/junit/
cypress/downloads/
.eslintcache
+tree-sitter-swh_search_ql.wasm
diff --git a/Makefile.local b/Makefile.local
--- a/Makefile.local
+++ b/Makefile.local
@@ -5,12 +5,14 @@
SETTINGS_TEST ?= swh.web.settings.tests
SETTINGS_DEV ?= swh.web.settings.development
SETTINGS_PROD = swh.web.settings.production
+SWH_SEARCH_DIR := $(shell python -c "import os;from swh import search; print(os.path.dirname(search.__file__))")
yarn-install: package.json
$(YARN) install --frozen-lockfile
+ $(MAKE) build-ts-wasm
.PHONY: build-webpack-dev
-build-webpack-dev: yarn-install
+build-webpack-dev:
$(YARN) build-dev
.PHONY: build-webpack-test
@@ -123,3 +125,7 @@
# https://github.com/typeddjango/django-stubs/issues/166
check-mypy:
DJANGO_SETTINGS_MODULE=$(SETTINGS_DEV) $(MYPY) $(MYPYFLAGS) swh
+
+build-ts-wasm:
+ yarn run tree-sitter build-wasm $(SWH_SEARCH_DIR)/query_language
+ cp $(SWH_SEARCH_DIR)/query_language/tokens.js assets/tokens.js
diff --git a/assets/config/webpack.config.development.js b/assets/config/webpack.config.development.js
--- a/assets/config/webpack.config.development.js
+++ b/assets/config/webpack.config.development.js
@@ -149,10 +149,16 @@
alias: {
'pdfjs-dist': 'pdfjs-dist/build/pdf.min.js'
},
+ // for web-tree-sitter
+ fallback: {
+ 'path': false,
+ 'fs': false
+ },
// configure base paths for resolving modules with webpack
modules: [
'node_modules',
- path.resolve(__dirname, '../src')
+ path.resolve(__dirname, '../src'),
+ path.resolve(__dirname, '../../../swh-search/query_language/')
]
},
stats: 'errors-warnings',
@@ -212,6 +218,18 @@
}
}]
},
+ {
+ test: /\.wasm$/,
+ type: 'javascript/auto',
+ use: [{
+ loader: 'file-loader',
+ options: {
+ name: '[name].[ext]',
+ outputPath: 'js/'
+ }
+ }]
+ },
+
// expose jquery to the global context as $ and jQuery when importing it
{
test: require.resolve('jquery'),
@@ -370,6 +388,14 @@
{
from: path.resolve(nodeModules, 'mathjax/es5/output/chtml/fonts/woff-v2/**'),
to: path.resolve(__dirname, '../../static/fonts/[name][ext]')
+ },
+ {
+ from: path.resolve(__dirname, '../../tree-sitter-swh_search_ql.wasm'),
+ to: path.resolve(__dirname, '../../static/js/swh_ql.wasm')
+ },
+ {
+ from: path.resolve(__dirname, '../tokens.js'),
+ to: path.resolve(__dirname, '../../static/js/')
}
]
}),
diff --git a/assets/src/bundles/browse/origin-search.js b/assets/src/bundles/browse/origin-search.js
--- a/assets/src/bundles/browse/origin-search.js
+++ b/assets/src/bundles/browse/origin-search.js
@@ -6,6 +6,7 @@
*/
import {handleFetchError, errorMessageFromResponse, isArchivedOrigin} from 'utils/functions';
+import {initAutocomplete} from 'utils/search-ql-autocomplete';
const limit = 100;
const linksPrev = [];
@@ -195,6 +196,17 @@
export function initOriginSearch() {
$(document).ready(() => {
+ const inputBox = document.querySelector('#swh-origins-url-patterns');
+ const submitBtn = document.querySelector('#swh-search-submit');
+ const validQueryCallback = (isValid) => {
+ submitBtn.disabled = !isValid;
+ // if (!isValid)
+ // inputBox.classList.add('invalid');
+ // else
+ // inputBox.classList.remove('invalid');
+ };
+ initAutocomplete(inputBox, validQueryCallback);
+
$('#swh-search-origins').submit(event => {
event.preventDefault();
if (event.target.checkValidity()) {
diff --git a/assets/src/utils/autocomplete.css b/assets/src/utils/autocomplete.css
new file mode 100644
--- /dev/null
+++ b/assets/src/utils/autocomplete.css
@@ -0,0 +1,50 @@
+.autocomplete {
+ position: relative;
+ display: inline-block;
+}
+
+input.invalid {
+ outline: none !important;
+
+ /* border: 2px solid red; */
+}
+
+/* position the autocomplete items to be the same width as the container: */
+.autocomplete-items {
+ position: absolute;
+ border: 1px solid #d4d4d4;
+ width: 200px;
+ border-top: none;
+ z-index: 99998;
+ top: 100%;
+ left: 0;
+ right: 0;
+
+ /*
+ z-index: 99999; is taken by swh-top-bar
+ overflow-y: scroll;
+ */
+}
+
+.autocomplete-items div {
+ padding: 3px;
+ padding-left: 5px;
+ cursor: pointer;
+ background-color: #fff;
+
+ /*
+ font-size: 15px;
+ border-bottom: 1px solid #d4d4d4;
+ */
+}
+
+/* when hovering an item: */
+.autocomplete-items div:hover {
+ background-color: #e9e9e9;
+}
+
+/* when navigating through the items using the arrow keys: */
+.autocomplete-active {
+ background-color: #e20026 !important;
+ color: #fff;
+}
diff --git a/assets/src/utils/autocomplete.js b/assets/src/utils/autocomplete.js
new file mode 100644
--- /dev/null
+++ b/assets/src/utils/autocomplete.js
@@ -0,0 +1,148 @@
+import 'utils/autocomplete.css';
+
+export class Autocomplete {
+ constructor(params) {
+ const {inputBox, suggestions} = params;
+ this.inputBox = inputBox;
+ this.suggestions = suggestions;
+ this.currentIndex = -1;
+
+ this.autocompleteList = document.createElement('div');
+ this.autocompleteList.setAttribute('class', 'autocomplete-items');
+ this.inputBox.parentNode.appendChild(this.autocompleteList);
+
+ this.initListeners();
+ }
+
+ initListeners() {
+ this.inputBox.addEventListener('focus', this.updateLists.bind(this));
+ this.inputBox.addEventListener('input', this.updateLists.bind(this));
+
+ this.inputBox.addEventListener('keydown', (e) => {
+ if (e.keyCode === 40) { // down
+ e.preventDefault();
+ this.currentIndex++;
+ this.addActive();
+ } else if (e.keyCode === 38) { // up
+ e.preventDefault();
+ this.currentIndex--;
+ this.addActive();
+ } else if (e.keyCode === 13 || e.keyCode === 9) { // enter or tab
+ e.preventDefault();
+ if (this.currentIndex > -1) {
+ // Simulate a click on the "active" item:
+ if (this.autocompleteList) this.autocompleteList.children[this.currentIndex].click();
+ }
+ } else if (e.keyCode === 27) { // escape
+ e.preventDefault();
+ this.removeAllItems(e.target);
+ }
+ });
+
+ document.addEventListener('click', (e) => { this.removeAllItems(e.target); });
+ }
+
+ updateLists() {
+ const inputValue = this.inputBox.value;
+
+ const tokens = inputValue.split();
+ const lastToken = tokens[tokens.length - 1];
+ const lastChar = lastToken[lastToken.length - 1];
+
+ /* close any already open lists of autocompleted values */
+ this.removeAllItems();
+
+ this.currentIndex = -1;
+
+ const suggestions = this.suggestions.filter(s => (s.indexOf(lastToken) >= 0 || lastChar === ' '));
+
+ suggestions.slice(0, 10).forEach(suggestion => {
+ const itemDiv = document.createElement('div');
+ if (lastChar === ' ') {
+ itemDiv.innerHTML = suggestion;
+ } else {
+ const indexOfLastToken = suggestion.indexOf(lastToken);
+
+ itemDiv.innerHTML = suggestion.substr(0, indexOfLastToken) +
+ '<strong>' +
+ suggestion.substr(indexOfLastToken, lastToken.length) +
+ '</strong>' +
+ suggestion.substr(
+ indexOfLastToken + lastToken.length, suggestion.length - (lastToken.length - 2)
+ );
+
+ }
+
+ itemDiv.setAttribute('data-value', suggestion);
+ itemDiv.setAttribute('data-editable-suggestion', 'false');
+ itemDiv.setAttribute('title', 'Include repos with the provided term in their url (origin)');
+
+ const suggestionClick = (e) => {
+ const toInsert = e.target.getAttribute('data-value');
+ const isEditableSuggestion = e.target.getAttribute('data-editable-suggestion');
+
+ if (isEditableSuggestion === 'true') return;
+
+ const oldValue = this.inputBox.value;
+ const tokens = oldValue.split();
+ const lastToken = tokens[tokens.length - 1];
+ const lastChar = lastToken[lastToken.length - 1];
+
+ let newValue = '';
+
+ if (lastChar === ' ' || oldValue === '') {
+ newValue = oldValue + toInsert;
+ } else {
+ // const position = this.inputBox.selectionStart;
+ const queryWithoutLastToken = tokens.slice(0, tokens.length - 2).join(' ');
+ newValue = queryWithoutLastToken + ((queryWithoutLastToken !== '') ? ' ' : '') + toInsert;
+ }
+
+ this.inputBox.value = newValue;
+ this.inputBox.blur();
+ this.inputBox.focus();
+ // this.inputBox.dispatchEvent(new Event('input'))
+ };
+
+ itemDiv.addEventListener('click', suggestionClick.bind(this));
+
+ this.autocompleteList.appendChild(itemDiv);
+ });
+
+ if (suggestions?.length) {
+ // Select first element on each update
+ this.currentIndex = 0;
+ this.addActive();
+ }
+ }
+
+ addActive() {
+ // a function to classify an item as "active":
+ if (!this.autocompleteList) return false;
+ // start by removing the "active" class on all items:
+ const n = this.autocompleteList.childElementCount;
+ this.removeActive();
+ if (this.currentIndex >= n) this.currentIndex = 0;
+ if (this.currentIndex < 0) this.currentIndex = (n - 1);
+ // add class "autocomplete-active":
+ this.autocompleteList.children[this.currentIndex].classList.add('autocomplete-active');
+ }
+
+ removeActive() {
+ /* a function to remove the "active" class from all autocomplete items */
+ Array.from(this.autocompleteList.children).forEach(autocompleteItem => {
+ autocompleteItem.classList.remove('autocomplete-active');
+ });
+ }
+
+ removeAllItems(element) {
+ /*
+ close all autocomplete lists in the document,
+ except the one passed as an argument
+ */
+ if (element !== this.inputBox && this.autocompleteList) {
+ this.autocompleteList.innerHTML = '';
+ }
+ }
+
+}
diff --git a/assets/src/utils/search-ql-autocomplete.js b/assets/src/utils/search-ql-autocomplete.js
new file mode 100644
--- /dev/null
+++ b/assets/src/utils/search-ql-autocomplete.js
@@ -0,0 +1,224 @@
+import {staticAsset} from 'utils/functions';
+import 'web-tree-sitter/tree-sitter.wasm';
+import Parser from 'web-tree-sitter';
+import {Autocomplete} from 'utils/autocomplete.js';
+import {
+ fields, limitField, sortByField, // fields
+ sortByOptions, visitTypeOptions, // options
+ equalOp, containOp, rangeOp, choiceOp, // operators
+ AND, OR, TRUE, FALSE // special tokens
+} from '../../tokens.js';
+
+const filterNames = fields.concat(sortByField, limitField);
+
+const languageSyntax = [
+ {
+ category: 'patternFilter',
+ field: 'patternField',
+ operator: 'containOp',
+ value: 'patternVal',
+ suggestion: ['string', '"string"']
+ },
+ {
+ category: 'booleanFilter',
+ field: 'booleanField',
+ operator: 'equalOp',
+ value: 'booleanVal',
+ suggestion: [TRUE, FALSE]
+ },
+ {
+ category: 'numericFilter',
+ field: 'numericField',
+ operator: 'rangeOp',
+ value: 'numberVal',
+ suggestion: ['15']
+ },
+ {
+ category: 'boundedListFilter',
+ field: 'visitTypeField',
+ operator: 'equalOp',
+ value: 'visitTypeVal',
+ options: visitTypeOptions,
+ suggestion: ['[']
+ },
+ {
+ category: 'unboundedListFilter',
+ field: 'listField',
+ operator: 'choiceOp',
+ value: 'listVal',
+ options: ['string', '"string"'],
+ suggestion: ['[']
+ },
+ {
+ category: 'dateFilter',
+ field: 'dateField',
+ operator: 'rangeOp',
+ value: 'dateVal',
+ suggestion: ['2000-01-01', '2000-01-01T00:00Z']
+ },
+ {
+ category: 'sortBy',
+ field: 'sortByField',
+ operator: 'equalOp',
+ value: 'sortByVal',
+ options: sortByOptions,
+ suggestion: ['[']
+ },
+ {
+ category: 'limit',
+ field: 'limit',
+ operator: 'equalOp',
+ value: 'number',
+ suggestion: ['50']
+ }
+];
+
+const filterOperators = {equalOp, containOp, choiceOp, rangeOp};
+
+const findMissingNode = (node) => {
+ if (node.isMissing()) {
+ return node;
+ }
+ if (node.children.length > 0) {
+ for (let i = 0; i < node.children.length; i++) {
+ const missingNode = findMissingNode(node.children[i]);
+ if (missingNode !== null) { return missingNode; }
+ }
+ }
+
+ return null;
+};
+
+const isWrapperNode = (child, parent) => {
+ if (!child || !parent) return false;
+ if (parent.namedChildren.length === 1 && parent.type !== 'ERROR') return true;
+ return (
+ (child.startPosition.column === parent.startPosition.column) &&
+ (child.endPosition.column === parent.endPosition.column)
+ );
+};
+
+const isCategoryNode = (node) => {
+ if (!node || node === null) return false;
+ if (node.type === 'ERROR' || languageSyntax.filter(f => f.category === node.type).length > 0) { return true; }
+
+ return false;
+};
+
+const suggestNextNode = (tree, inputBox) => {
+ const cursor = inputBox.selectionStart - 1;
+ const query = inputBox.value;
+
+ let lastTokenIndex = cursor;
+ // let distFromLastToken = 0;
+ while (query[lastTokenIndex] === ' ') {
+ lastTokenIndex--;
+ // distFromLastToken++;
+ }
+
+ // if(query === "visit_type = []") debugger;
+
+ const lastTokenPosition = {row: 0, column: lastTokenIndex};
+ const lastTokenNode = tree.rootNode.descendantForPosition(lastTokenPosition, lastTokenPosition);
+
+ const missingNode = findMissingNode(tree.rootNode);
+
+ // Find last token node wrapper
+ let lastTokenNodeWrapper = lastTokenNode;
+ while (isWrapperNode(lastTokenNodeWrapper, lastTokenNodeWrapper.parent)) {
+ lastTokenNodeWrapper = lastTokenNodeWrapper.parent;
+ }
+
+ // Find last token node wrapper sibling
+ const lastTokenNodeWrapperSibling = lastTokenNodeWrapper.previousSibling;
+
+ // Find current filter category
+ let currentFilterCategory = lastTokenNode;
+ while (!isCategoryNode(currentFilterCategory)) {
+ currentFilterCategory = currentFilterCategory.parent;
+ }
+
+ console.log(lastTokenNode);
+ console.log(`LAST NODE: ${lastTokenNode.type}`);
+ console.log(`LAST NODE ANCESTOR: ${lastTokenNodeWrapper.type}`);
+ console.log(`LAST NODE ANCESTOR SIBLING: ${lastTokenNodeWrapperSibling?.type}`);
+ console.log(`LAST CATEGORY: ${currentFilterCategory.type}`);
+
+ // Suggest options for array valued filters
+ if ((lastTokenNode.type === ',' && lastTokenNodeWrapper.type.indexOf('Val') > 0) ||
+ (lastTokenNode.type === '[' && currentFilterCategory)
+ ) {
+ const filter = languageSyntax.filter(f => f.category === currentFilterCategory.type)[0];
+ console.log(filter.options);
+ return filter.options ?? [];
+ }
+ if (
+ (!tree.rootNode.hasError() && (lastTokenNodeWrapper.type.indexOf('Val') > 0)) ||
+ (lastTokenNode.type === ')' || lastTokenNode.type === ']')
+ ) {
+ // Suggest AND/OR
+ return [AND, OR];
+ }
+ if (missingNode && missingNode !== null) {
+ // Suggest missing nodes (Automatically suggested by Tree-sitter)
+ if (missingNode.type === ')') {
+ return [AND, OR, ')'];
+ } else if (missingNode.type === ']') {
+ return [',', ']'];
+ }
+ }
+
+ if (lastTokenNode.type === 'ERROR' ||
+ (lastTokenNode.type === '(') ||
+ ((lastTokenNode.type === AND || lastTokenNode.type === OR))
+ ) {
+ // Suggest field names
+ return filterNames.concat('(');
+ } else if (languageSyntax.map(f => f.field).includes(lastTokenNode.type)) {
+ // Suggest operators
+ const filter = languageSyntax.filter(f => f.field === lastTokenNode.type)[0];
+ return filterOperators[filter.operator];
+ } else if (lastTokenNode.type in filterOperators) {
+ // Suggest values
+ const filter = languageSyntax.filter(f => (
+ f.field === lastTokenNodeWrapperSibling.type
+ ))[0];
+ return filter.suggestion;
+ }
+
+ return [];
+};
+
+export const initAutocomplete = (inputBox, validQueryCallback) => {
+ Parser.init().then(async() => {
+ const parser = new Parser();
+ const swhSearchQL = await Parser.Language.load(staticAsset('js/swh_ql.wasm'));
+ parser.setLanguage(swhSearchQL);
+
+ const autocomplete = new Autocomplete(
+ {inputBox, suggestions: ['('].concat(filterNames)}
+ );
+
+ const getSuggestions = (e) => {
+ // if (e.keycode !== 32) // space
+ // return;
+ const tree = parser.parse(inputBox.value);
+
+ if (tree.rootNode.hasError()) {
+ validQueryCallback(false);
+ // inputBox.classList.add('invalid');
+ } else {
+ validQueryCallback(true);
+ // inputBox.classList.remove('invalid');
+ }
+
+ console.log(`input(${inputBox.value}) => ${tree.rootNode.toString()}`);
+
+ const suggestions = suggestNextNode(tree, inputBox);
+ // if (suggestions)
+ autocomplete.suggestions = suggestions; // .map(item => `${item} `);
+ };
+
+ inputBox.addEventListener('keydown', getSuggestions.bind(this));
+ });
+};
diff --git a/assets/src/utils/tokens.js b/assets/src/utils/tokens.js
new file mode 100644
--- /dev/null
+++ b/assets/src/utils/tokens.js
@@ -0,0 +1,109 @@
+// Copyright (C) 2021 The Software Heritage developers
+// See the AUTHORS file at the top-level directory of this distribution
+// License: GNU General Public License version 3, or any later version
+// See top-level LICENSE file for more information
+
+// Field tokens
+const visitTypeField = 'visit_type';
+const sortByField = 'sort_by';
+const limitField = 'limit';
+
+// Field categories
+const patternFields = ['origin', 'metadata'];
+const booleanFields = ['visited'];
+const numericFields = ['visits'];
+const boundedListFields = [visitTypeField];
+const listFields = ['language', 'license', 'keyword'];
+const dateFields = [
+ 'last_visit',
+ 'last_eventful_visit',
+ 'last_revision',
+ 'last_release',
+ 'created',
+ 'modified',
+ 'published'
+];
+
+const fields = [].concat(
+ patternFields,
+ booleanFields,
+ numericFields,
+ boundedListFields,
+ listFields,
+ dateFields
+);
+
+// Operators
+const equalOp = ['='];
+const containOp = [':'];
+const rangeOp = ['<', '<=', '=', '!=', '>=', '>'];
+const choiceOp = ['in', 'not in'];
+
+// Values
+const sortByOptions = [
+ 'visits',
+ 'last_visit',
+ 'last_eventful_visit',
+ 'last_revision',
+ 'last_release',
+ 'created',
+ 'modified',
+ 'published'
+];
+
+const visitTypeOptions = [
+ 'any',
+ 'bzr',
+ 'cran',
+ 'cvs',
+ 'deb',
+ 'deposit',
+ 'ftp',
+ 'hg',
+ 'git',
+ 'nixguix',
+ 'npm',
+ 'opam',
+ 'pypi',
+ 'svn',
+ 'tar'
+];
+
+// Extra tokens
+const OR = 'or';
+const AND = 'and';
+
+const TRUE = 'true';
+const FALSE = 'false';
+
+module.exports = {
+ // Field tokens
+ visitTypeField,
+ sortByField,
+ limitField,
+
+ // Field categories
+ patternFields,
+ booleanFields,
+ numericFields,
+ boundedListFields,
+ listFields,
+ dateFields,
+ fields,
+
+ // Operators
+ equalOp,
+ containOp,
+ rangeOp,
+ choiceOp,
+
+ // Values
+ sortByOptions,
+ visitTypeOptions,
+
+ // Extra tokens
+ OR,
+ AND,
+ TRUE,
+ FALSE
+};
diff --git a/assets/tokens.js b/assets/tokens.js
new file mode 100644
--- /dev/null
+++ b/assets/tokens.js
@@ -0,0 +1,109 @@
+// Copyright (C) 2021 The Software Heritage developers
+// See the AUTHORS file at the top-level directory of this distribution
+// License: GNU General Public License version 3, or any later version
+// See top-level LICENSE file for more information
+
+// Field tokens
+const visitTypeField = 'visit_type';
+const sortByField = 'sort_by';
+const limitField = 'limit';
+
+// Field categories
+const patternFields = ['origin', 'metadata'];
+const booleanFields = ['visited'];
+const numericFields = ['visits'];
+const boundedListFields = [visitTypeField];
+const listFields = ['language', 'license', 'keyword'];
+const dateFields = [
+ 'last_visit',
+ 'last_eventful_visit',
+ 'last_revision',
+ 'last_release',
+ 'created',
+ 'modified',
+ 'published'
+];
+
+const fields = [].concat(
+ patternFields,
+ booleanFields,
+ numericFields,
+ boundedListFields,
+ listFields,
+ dateFields
+);
+
+// Operators
+const equalOp = ['='];
+const containOp = [':'];
+const rangeOp = ['<', '<=', '=', '!=', '>=', '>'];
+const choiceOp = ['in', 'not in'];
+
+// Values
+const sortByOptions = [
+ 'visits',
+ 'last_visit',
+ 'last_eventful_visit',
+ 'last_revision',
+ 'last_release',
+ 'created',
+ 'modified',
+ 'published'
+];
+
+const visitTypeOptions = [
+ 'any',
+ 'bzr',
+ 'cran',
+ 'cvs',
+ 'deb',
+ 'deposit',
+ 'ftp',
+ 'hg',
+ 'git',
+ 'nixguix',
+ 'npm',
+ 'opam',
+ 'pypi',
+ 'svn',
+ 'tar'
+];
+
+// Extra tokens
+const OR = 'or';
+const AND = 'and';
+
+const TRUE = 'true';
+const FALSE = 'false';
+
+module.exports = {
+ // Field tokens
+ visitTypeField,
+ sortByField,
+ limitField,
+
+ // Field categories
+ patternFields,
+ booleanFields,
+ numericFields,
+ boundedListFields,
+ listFields,
+ dateFields,
+ fields,
+
+ // Operators
+ equalOp,
+ containOp,
+ rangeOp,
+ choiceOp,
+
+ // Values
+ sortByOptions,
+ visitTypeOptions,
+
+ // Extra tokens
+ OR,
+ AND,
+ TRUE,
+ FALSE
+};
diff --git a/mypy.ini b/mypy.ini
--- a/mypy.ini
+++ b/mypy.ini
@@ -18,6 +18,12 @@
[mypy-django_js_reverse.*]
ignore_missing_imports = True
+[mypy-django_plugin.*]
+no_implicit_reexport = False
+
+[mypy-drf_plugin.main]
+no_implicit_reexport = False
+
[mypy-htmlmin.*]
ignore_missing_imports = True
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -89,6 +89,7 @@
"typeface-alegreya": "^1.1.13",
"typeface-alegreya-sans": "^1.1.13",
"waypoints": "^4.0.1",
+ "web-tree-sitter": "^0.20.5",
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
@@ -121,6 +122,7 @@
"eslint-webpack-plugin": "^3.1.1",
"exports-loader": "^3.1.0",
"expose-loader": "^3.1.0",
+ "file-loader": "^6.2.0",
"imports-loader": "^3.1.1",
"istanbul-lib-coverage": "^3.2.0",
"json-stable-stringify": "^1.0.1",
@@ -149,6 +151,7 @@
"stylelint": "^14.6.1",
"stylelint-config-standard": "^25.0.0",
"terser-webpack-plugin": "^5.3.1",
+ "tree-sitter-cli": "^0.20.6",
"url-loader": "^4.1.1",
"webpack": "^5.72.0",
"webpack-bundle-tracker": "^1.5.0",
diff --git a/swh/web/settings/common.py b/swh/web/settings/common.py
--- a/swh/web/settings/common.py
+++ b/swh/web/settings/common.py
@@ -145,6 +145,7 @@
if not os.path.exists(STATIC_DIR):
# static folder location when developping swh-web
STATIC_DIR = os.path.join(PROJECT_DIR, "../../../static")
+
STATICFILES_DIRS = [STATIC_DIR]
INTERNAL_IPS = ["127.0.0.1"]
diff --git a/swh/web/templates/includes/origin-search-form.html b/swh/web/templates/includes/origin-search-form.html
--- a/swh/web/templates/includes/origin-search-form.html
+++ b/swh/web/templates/includes/origin-search-form.html
@@ -10,9 +10,9 @@
<input class="form-control"
placeholder="Enter a SWHID to resolve or string pattern(s) to search for in origin urls"
type="text" id="swh-origins-url-patterns"
- oninput="swh.webapp.validateSWHIDInput(this)" autofocus required>
+ oninput="swh.webapp.validateSWHIDInput(this)" autofocus required autocomplete="off">
<div class="input-group-append">
- <button class="btn btn-primary" type="submit"><i class="swh-search-icon mdi mdi-24px mdi-magnify" aria-hidden="true"></i></button>
+ <button class="btn btn-primary" type="submit" id="swh-search-submit"><i class="swh-search-icon mdi mdi-24px mdi-magnify" aria-hidden="true"></i></button>
</div>
<div class="invalid-feedback"></div>
</div>
@@ -48,7 +48,6 @@
search in metadata (instead of URL)
</label>
</div>
- {% if user.is_authenticated and user.is_staff or "swh.web.search_ql" in user.get_all_permissions %}
<div class="custom-control custom-checkbox swhid-option">
<input class="custom-control-input" value="option-use-ql" type="checkbox"
id="swh-search-use-ql">
@@ -60,7 +59,6 @@
</a>
</label>
</div>
- {% endif %}
</form>
<script>
diff --git a/yarn.lock b/yarn.lock
--- a/yarn.lock
+++ b/yarn.lock
@@ -4094,7 +4094,7 @@
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
-deep-equal@^1.0.0:
+deep-equal@^1.0.0, deep-equal@^1.0.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
@@ -5151,6 +5151,14 @@
dependencies:
flat-cache "^3.0.4"
+file-loader@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d"
+ integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==
+ dependencies:
+ loader-utils "^2.0.0"
+ schema-utils "^3.0.0"
+
filelist@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.1.tgz#f10d1a3ae86c1694808e8f20906f43d4c9132dbb"
@@ -9053,10 +9061,10 @@
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
-qs@6.9.7:
- version "6.9.7"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe"
- integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==
+qs@6.7.0:
+ version "6.7.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
+ integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
qs@~6.5.2:
version "6.5.2"
@@ -9390,14 +9398,13 @@
dependencies:
path-parse "^1.0.6"
-resolve@^1.1.4, resolve@^1.1.5, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.4.0, resolve@^1.9.0:
- version "1.22.0"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
- integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
+resolve@^1.1.4, resolve@^1.1.5, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.4.0, resolve@^1.9.0:
+ version "1.20.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
+ integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
dependencies:
- is-core-module "^2.8.1"
- path-parse "^1.0.7"
- supports-preserve-symlinks-flag "^1.0.0"
+ is-core-module "^2.2.0"
+ path-parse "^1.0.6"
restore-cursor@^3.1.0:
version "3.1.0"
@@ -9605,12 +9612,12 @@
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
-selfsigned@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.0.1.tgz#8b2df7fa56bf014d19b6007655fff209c0ef0a56"
- integrity sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==
+selfsigned@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.0.0.tgz#e927cd5377cbb0a1075302cff8df1042cc2bce5b"
+ integrity sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ==
dependencies:
- node-forge "^1"
+ node-forge "^1.2.0"
"semver@2 || 3 || 4 || 5", semver@^5.3.0:
version "5.7.1"
@@ -10572,10 +10579,10 @@
dependencies:
jquery ">=1.12.0"
-toidentifier@1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
- integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
+toidentifier@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
+ integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
tough-cookie@^4.0.0:
version "4.0.0"
@@ -10601,6 +10608,11 @@
dependencies:
punycode "^2.1.1"
+tree-sitter-cli@^0.20.6:
+ version "0.20.6"
+ resolved "https://registry.yarnpkg.com/tree-sitter-cli/-/tree-sitter-cli-0.20.6.tgz#2a7202190d7bd64e112b451f94573dbe40a04f04"
+ integrity sha512-tjbAeuGSMhco/EnsThjWkQbDIYMDmdkWsTPsa/NJAW7bjaki9P7oM9TkLxfdlnm4LXd1wR5wVSM2/RTLtZbm6A==
+
trim-newlines@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30"
@@ -11022,6 +11034,11 @@
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965"
integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==
+web-tree-sitter@^0.20.5:
+ version "0.20.5"
+ resolved "https://registry.yarnpkg.com/web-tree-sitter/-/web-tree-sitter-0.20.5.tgz#62c8ea29d94f6ef6f03ce9c68c860df011ee26c7"
+ integrity sha512-mpXlqIeEBE5Q71cnBnt8w6XKhIiKmllPECqsIFBtMvzcfCxA8+614iyMJXBCQo95Vs3y1zORLqiLJn25pYZ4Tw==
+
webidl-conversions@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"

File Metadata

Mime Type
text/plain
Expires
Jul 28 2024, 1:15 AM (11 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3224206

Event Timeline