mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-06-15 18:40:40 +00:00
Move javascript files to fit new style
This commit is contained in:
119
app/static/js/corpus-analysis/app.js
Normal file
119
app/static/js/corpus-analysis/app.js
Normal file
@ -0,0 +1,119 @@
|
||||
nopaque.corpus_analysis.App = class App {
|
||||
constructor(corpusId) {
|
||||
this.corpusId = corpusId;
|
||||
|
||||
this.data = {};
|
||||
|
||||
// HTML elements
|
||||
this.elements = {
|
||||
container: document.querySelector('#corpus-analysis-container'),
|
||||
extensionCards: document.querySelector('#corpus-analysis-extension-cards'),
|
||||
extensionTabs: document.querySelector('#corpus-analysis-extension-tabs'),
|
||||
initModal: document.querySelector('#corpus-analysis-init-modal')
|
||||
};
|
||||
// Materialize elements
|
||||
this.elements.m = {
|
||||
extensionTabs: M.Tabs.init(this.elements.extensionTabs),
|
||||
initModal: M.Modal.init(this.elements.initModal, {dismissible: false})
|
||||
};
|
||||
|
||||
this.extensions = {};
|
||||
|
||||
this.settings = {};
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.disableActionElements();
|
||||
this.elements.m.initModal.open();
|
||||
|
||||
try {
|
||||
// Setup CQi over SocketIO connection and gather data from the CQPServer
|
||||
const statusTextElement = this.elements.initModal.querySelector('.status-text');
|
||||
statusTextElement.innerText = 'Creating CQi over SocketIO client...';
|
||||
const cqiClient = new nopaque.corpus_analysis.cqi.Client('/cqi_over_sio');
|
||||
statusTextElement.innerText += ' Done';
|
||||
statusTextElement.innerHTML = 'Waiting for the CQP server...';
|
||||
const response = await cqiClient.api.socket.emitWithAck('init', this.corpusId);
|
||||
if (response.code !== 200) {throw new Error();}
|
||||
statusTextElement.innerText += ' Done';
|
||||
statusTextElement.innerHTML = 'Connecting to the CQP server...';
|
||||
await cqiClient.connect('anonymous', '');
|
||||
statusTextElement.innerText += ' Done';
|
||||
statusTextElement.innerHTML = 'Building and receiving corpus data cache from the server (This may take a while)...';
|
||||
const cqiCorpus = await cqiClient.corpora.get(`NOPAQUE-${this.corpusId.toUpperCase()}`);
|
||||
statusTextElement.innerText += ' Done';
|
||||
// TODO: Don't do this hgere
|
||||
await cqiCorpus.updateDb();
|
||||
this.data.cqiClient = cqiClient;
|
||||
this.data.cqiCorpus = cqiCorpus;
|
||||
this.data.corpus = {o: cqiCorpus}; // legacy
|
||||
// Initialize extensions
|
||||
for (const extension of Object.values(this.extensions)) {
|
||||
statusTextElement.innerHTML = `Initializing ${extension.name} extension...`;
|
||||
await extension.init();
|
||||
statusTextElement.innerText += ' Done'
|
||||
}
|
||||
} catch (error) {
|
||||
let errorString = '';
|
||||
if ('code' in error && error.code !== undefined && error.code !== null) {
|
||||
errorString += `[${error.code}] `;
|
||||
}
|
||||
errorString += `${error.constructor.name}`;
|
||||
if ('description' in error && error.description !== undefined && error.description !== null) {
|
||||
errorString += `: ${error.description}`;
|
||||
}
|
||||
const errorsElement = this.elements.initModal.querySelector('.errors');
|
||||
const progressElement = this.elements.initModal.querySelector('.progress');
|
||||
errorsElement.innerText = errorString;
|
||||
errorsElement.classList.remove('hide');
|
||||
progressElement.classList.add('hide');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const extensionSelectorElement of this.elements.extensionCards.querySelectorAll('.extension-selector')) {
|
||||
extensionSelectorElement.addEventListener('click', () => {
|
||||
this.elements.m.extensionTabs.select(extensionSelectorElement.dataset.target);
|
||||
});
|
||||
}
|
||||
|
||||
this.enableActionElements();
|
||||
this.elements.m.initModal.close();
|
||||
}
|
||||
|
||||
registerExtension(extension) {
|
||||
if (extension.name in this.extensions) {return;}
|
||||
this.extensions[extension.name] = extension;
|
||||
}
|
||||
|
||||
disableActionElements() {
|
||||
const actionElements = this.elements.container.querySelectorAll('.corpus-analysis-action');
|
||||
for (const actionElement of actionElements) {
|
||||
switch(actionElement.nodeName) {
|
||||
case 'INPUT':
|
||||
actionElement.disabled = true;
|
||||
break;
|
||||
case 'SELECT':
|
||||
actionElement.parentNode.querySelector('input.select-dropdown').disabled = true;
|
||||
break;
|
||||
default:
|
||||
actionElement.classList.add('disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enableActionElements() {
|
||||
const actionElements = this.elements.container.querySelectorAll('.corpus-analysis-action');
|
||||
for (const actionElement of actionElements) {
|
||||
switch(actionElement.nodeName) {
|
||||
case 'INPUT':
|
||||
actionElement.disabled = false;
|
||||
break;
|
||||
case 'SELECT':
|
||||
actionElement.parentNode.querySelector('input.select-dropdown').disabled = false;
|
||||
break;
|
||||
default:
|
||||
actionElement.classList.remove('disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
584
app/static/js/corpus-analysis/concordance-extension.js
Normal file
584
app/static/js/corpus-analysis/concordance-extension.js
Normal file
@ -0,0 +1,584 @@
|
||||
nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
|
||||
name = 'Concordance';
|
||||
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
|
||||
this.data = {};
|
||||
|
||||
this.elements = {
|
||||
container: document.querySelector(`#corpus-analysis-concordance-container`),
|
||||
error: document.querySelector(`#corpus-analysis-concordance-error`),
|
||||
userInterfaceForm: document.querySelector(`#corpus-analysis-concordance-user-interface-form`),
|
||||
expertModeForm: document.querySelector(`#corpus-analysis-concordance-expert-mode-form`),
|
||||
queryBuilderForm: document.querySelector(`#corpus-analysis-concordance-query-builder-form`),
|
||||
progress: document.querySelector(`#corpus-analysis-concordance-progress`),
|
||||
subcorpusInfo: document.querySelector(`#corpus-analysis-concordance-subcorpus-info`),
|
||||
subcorpusActions: document.querySelector(`#corpus-analysis-concordance-subcorpus-actions`),
|
||||
subcorpusItems: document.querySelector(`#corpus-analysis-concordance-subcorpus-items`),
|
||||
subcorpusList: document.querySelector(`#corpus-analysis-concordance-subcorpus-list`),
|
||||
subcorpusPagination: document.querySelector(`#corpus-analysis-concordance-subcorpus-pagination`)
|
||||
};
|
||||
|
||||
this.settings = {
|
||||
context: parseInt(this.elements.userInterfaceForm['context'].value),
|
||||
perPage: parseInt(this.elements.userInterfaceForm['per-page'].value),
|
||||
selectedSubcorpus: undefined,
|
||||
textStyle: parseInt(this.elements.userInterfaceForm['text-style'].value),
|
||||
tokenRepresentation: this.elements.userInterfaceForm['token-representation'].value
|
||||
};
|
||||
|
||||
this.app.registerExtension(this);
|
||||
}
|
||||
|
||||
async submitForm(queryModeId) {
|
||||
this.app.disableActionElements();
|
||||
let queryBuilderQuery = nopaque.Utils.unescape(document.querySelector('#corpus-analysis-concordance-query-preview').innerHTML.trim());
|
||||
let expertModeQuery = this.elements.expertModeForm.query.value.trim();
|
||||
let query = queryModeId === 'corpus-analysis-concordance-expert-mode-form' ? expertModeQuery : queryBuilderQuery;
|
||||
let form = queryModeId === 'corpus-analysis-concordance-expert-mode-form' ? this.elements.expertModeForm : this.elements.queryBuilderForm;
|
||||
|
||||
let subcorpusName = form['subcorpus-name'].value;
|
||||
this.elements.error.innerText = '';
|
||||
this.elements.error.classList.add('hide');
|
||||
this.elements.progress.classList.remove('hide');
|
||||
try {
|
||||
const subcorpus = {};
|
||||
subcorpus.q = query;
|
||||
subcorpus.selectedItems = new Set();
|
||||
await this.data.corpus.o.query(subcorpusName, query);
|
||||
if (subcorpusName !== 'Last') {this.data.subcorpora.Last = subcorpus;}
|
||||
const cqiSubcorpus = await this.data.corpus.o.subcorpora.get(subcorpusName);
|
||||
subcorpus.o = cqiSubcorpus;
|
||||
const paginatedSubcorpus = await cqiSubcorpus.paginate(this.settings.context, 1, this.settings.perPage);
|
||||
subcorpus.p = paginatedSubcorpus;
|
||||
this.data.subcorpora[subcorpusName] = subcorpus;
|
||||
this.settings.selectedSubcorpus = subcorpusName;
|
||||
this.renderSubcorpusList();
|
||||
this.renderSubcorpusInfo();
|
||||
this.renderSubcorpusActions();
|
||||
this.renderSubcorpusItems();
|
||||
this.renderSubcorpusPagination();
|
||||
this.elements.progress.classList.add('hide');
|
||||
} catch (error) {
|
||||
let errorString = '';
|
||||
if ('code' in error) {errorString += `[${error.code}] `;}
|
||||
errorString += `${error.constructor.name}`;
|
||||
this.elements.error.innerText = errorString;
|
||||
this.elements.error.classList.remove('hide');
|
||||
app.flash(errorString, 'error');
|
||||
this.elements.progress.classList.add('hide');
|
||||
}
|
||||
this.app.enableActionElements();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Init data
|
||||
this.data.corpus = this.app.data.corpus;
|
||||
this.data.subcorpora = {};
|
||||
// Add event listeners
|
||||
this.elements.expertModeForm.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
this.submitForm(this.elements.expertModeForm.id);
|
||||
});
|
||||
this.elements.queryBuilderForm.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
this.submitForm(this.elements.queryBuilderForm.id);
|
||||
});
|
||||
this.elements.userInterfaceForm.addEventListener('change', (event) => {
|
||||
if (event.target === this.elements.userInterfaceForm['context']) {
|
||||
this.settings.context = parseInt(this.elements.userInterfaceForm['context'].value);
|
||||
this.submitForm();
|
||||
}
|
||||
if (event.target === this.elements.userInterfaceForm['per-page']) {
|
||||
this.settings.perPage = parseInt(this.elements.userInterfaceForm['per-page'].value);
|
||||
this.submitForm();
|
||||
}
|
||||
if (event.target === this.elements.userInterfaceForm['text-style']) {
|
||||
this.settings.textStyle = parseInt(this.elements.userInterfaceForm['text-style'].value);
|
||||
this.setTextStyle();
|
||||
}
|
||||
if (event.target === this.elements.userInterfaceForm['token-representation']) {
|
||||
this.settings.tokenRepresentation = this.elements.userInterfaceForm['token-representation'].value;
|
||||
this.setTokenRepresentation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearSubcorpusList() {
|
||||
this.elements.subcorpusList.innerHTML = '';
|
||||
this.elements.subcorpusList.classList.add('hide');
|
||||
}
|
||||
|
||||
renderSubcorpusList() {
|
||||
this.clearSubcorpusList();
|
||||
for (let subcorpusName in this.data.subcorpora) {
|
||||
this.elements.subcorpusList.innerHTML += `
|
||||
<a class="btn waves-effect waves-light subcorpus-selector" data-target="${subcorpusName}"><i class="material-icons left">bookmark</i>${subcorpusName}</a>
|
||||
`.trim();
|
||||
}
|
||||
for (let subcorpusSelectorElement of this.elements.subcorpusList.querySelectorAll('.subcorpus-selector')) {
|
||||
let subcorpusName = subcorpusSelectorElement.dataset.target;
|
||||
if (subcorpusName === this.settings.selectedSubcorpus) {
|
||||
subcorpusSelectorElement.classList.add('disabled');
|
||||
continue;
|
||||
}
|
||||
subcorpusSelectorElement.addEventListener('click', () => {
|
||||
this.settings.selectedSubcorpus = subcorpusName;
|
||||
this.elements.progress.classList.remove('hide');
|
||||
this.renderSubcorpusList();
|
||||
this.renderSubcorpusInfo();
|
||||
this.renderSubcorpusActions();
|
||||
this.renderSubcorpusActions();
|
||||
this.renderSubcorpusItems();
|
||||
this.renderSubcorpusPagination();
|
||||
this.elements.progress.classList.add('hide');
|
||||
});
|
||||
}
|
||||
this.elements.subcorpusList.classList.remove('hide');
|
||||
}
|
||||
|
||||
clearSubcorpusInfo() {
|
||||
this.elements.subcorpusInfo.innerHTML = '';
|
||||
this.elements.subcorpusInfo.classList.add('hide');
|
||||
}
|
||||
|
||||
renderSubcorpusInfo() {
|
||||
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
|
||||
this.clearSubcorpusInfo();
|
||||
this.elements.subcorpusInfo.innerHTML = `${subcorpus.p.total} matches found for <code>${subcorpus.q.replace(/</g, "<").replace(/>/g, ">")}</code>`;
|
||||
this.elements.subcorpusInfo.classList.remove('hide');
|
||||
}
|
||||
|
||||
clearSubcorpusActions() {
|
||||
for (let tooltippedElement of this.elements.subcorpusActions.querySelectorAll('.tooltipped')) {
|
||||
M.Tooltip.getInstance(tooltippedElement).destroy();
|
||||
}
|
||||
this.elements.subcorpusActions.innerHTML = '';
|
||||
}
|
||||
|
||||
renderSubcorpusActions() {
|
||||
this.clearSubcorpusActions();
|
||||
this.elements.subcorpusActions.innerHTML += `
|
||||
<a class="btn-floating btn-small tooltipped waves-effect waves-light corpus-analysis-action subcorpus-export-trigger" data-tooltip="Export subcorpus">
|
||||
<i class="material-icons">download</i>
|
||||
</a>
|
||||
<a class="btn-floating btn-small red tooltipped waves-effect waves-light corpus-analysis-action subcorpus-delete-trigger" data-tooltip="Delete subcorpus">
|
||||
<i class="material-icons">delete</i>
|
||||
</a>
|
||||
`.trim();
|
||||
M.Tooltip.init(this.elements.subcorpusActions.querySelectorAll('.tooltipped'));
|
||||
this.elements.subcorpusActions.querySelector('.subcorpus-export-trigger').addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
|
||||
let modalElementId = nopaque.Utils.generateElementId('export-subcorpus-modal-');
|
||||
let exportFormatSelectElementId = nopaque.Utils.generateElementId('export-format-select-');
|
||||
let exportSelectedMatchesOnlyCheckboxElementId = nopaque.Utils.generateElementId('export-selected-matches-only-checkbox-');
|
||||
let exportFileNameInputElementId = nopaque.Utils.generateElementId('export-file-name-input-');
|
||||
let modalElement = nopaque.Utils.HTMLToElement(
|
||||
`
|
||||
<div class="modal" id="${modalElementId}">
|
||||
<div class="modal-content">
|
||||
<h4>Export subcorpus "${subcorpus.o.name}"</h4>
|
||||
<br>
|
||||
<div class="row">
|
||||
<div class="input-field col s3">
|
||||
<select id="${exportFormatSelectElementId}">
|
||||
<option value="csv" selected>CSV</option>
|
||||
<option value="json">JSON</option>
|
||||
</select>
|
||||
<label>Export format</label>
|
||||
</div>
|
||||
<div class="input-field col s9">
|
||||
<input id="${exportFileNameInputElementId}" type="text" class="validate" value="export">
|
||||
<label class="active" for="${exportFileNameInputElementId}">Export filename without filename extension (.csv/.json/...)</label>
|
||||
</div>
|
||||
<p class="col s12">
|
||||
<label>
|
||||
<input id="${exportSelectedMatchesOnlyCheckboxElementId}" type="checkbox" ${subcorpus.selectedItems.size === 0 ? '' : 'checked'}>
|
||||
<span>Export selected matches only</span>
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn-flat modal-close waves-effect waves-light">Cancel</a>
|
||||
<a class="action-button btn modal-close waves-effect waves-light" data-action="export">Export</a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
document.querySelector('#modals').appendChild(modalElement);
|
||||
let exportFormatSelectElement = modalElement.querySelector(`#${exportFormatSelectElementId}`);
|
||||
let exportFormatSelect = M.FormSelect.init(exportFormatSelectElement);
|
||||
let exportSelectedMatchesOnlyCheckboxElement = modalElement.querySelector(`#${exportSelectedMatchesOnlyCheckboxElementId}`);
|
||||
let exportFileNameInputElement = modalElement.querySelector(`#${exportFileNameInputElementId}`);
|
||||
let exportButton = modalElement.querySelector('.action-button[data-action="export"]');
|
||||
let modal = M.Modal.init(
|
||||
modalElement,
|
||||
{
|
||||
dismissible: false,
|
||||
onCloseEnd: () => {
|
||||
exportFormatSelect.destroy();
|
||||
modal.destroy();
|
||||
modalElement.remove();
|
||||
}
|
||||
}
|
||||
);
|
||||
exportButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
this.app.disableActionElements();
|
||||
this.elements.progress.classList.remove('hide');
|
||||
let exportFormat = exportFormatSelectElement.value;
|
||||
let exportFileName = exportFileNameInputElement.value;
|
||||
let exportFileNameExtension = exportFormat === 'csv' ? 'csv' : 'json';
|
||||
let exportFileNameWithExtension = `${exportFileName}.${exportFileNameExtension}`;
|
||||
let exportSelectedMatchesOnly = exportSelectedMatchesOnlyCheckboxElement.checked;
|
||||
let promise;
|
||||
if (exportSelectedMatchesOnly) {
|
||||
if (subcorpus.selectedItems.size === 0) {
|
||||
this.elements.progress.classList.add('hide');
|
||||
this.app.enableActionElements();
|
||||
app.flash('No matches selected', 'error');
|
||||
return;
|
||||
}
|
||||
promise = subcorpus.o.partialExport([...subcorpus.selectedItems], 50);
|
||||
} else {
|
||||
promise = subcorpus.o.export(50);
|
||||
}
|
||||
promise.then(
|
||||
(data) => {
|
||||
let blob;
|
||||
if (exportFormat === 'csv') {
|
||||
let csvContent = 'sep=,\r\n';
|
||||
csvContent += '"#Match","Text title","Left context","KWIC","Right context"';
|
||||
for (let match of data.matches) {
|
||||
csvContent += '\r\n';
|
||||
csvContent += `"${match.num}"`;
|
||||
csvContent += ',';
|
||||
let textIds = new Set();
|
||||
for (let cpos = match.c[0]; cpos <= match.c[1]; cpos++) {
|
||||
textIds.add(data.cpos_lookup[cpos].text);
|
||||
}
|
||||
csvContent += '"' + [...textIds].map(x => data.text_lookup[x].title.replace('"', '""')).join(', ') + '"';
|
||||
csvContent += ',';
|
||||
if (match.lc !== null) {
|
||||
let lc_cpos_list = [];
|
||||
for (let cpos = match.lc[0]; cpos <= match.lc[1]; cpos++) {lc_cpos_list.push(cpos);}
|
||||
csvContent += '"' + lc_cpos_list.map(x => data.cpos_lookup[x].word.replace('"', '""')).join(' ') + '"';
|
||||
}
|
||||
csvContent += ',';
|
||||
let c_cpos_list = [];
|
||||
for (let cpos = match.c[0]; cpos <= match.c[1]; cpos++) {c_cpos_list.push(cpos);}
|
||||
csvContent += '"' + c_cpos_list.map(x => data.cpos_lookup[x].word.replace('"', '""')).join(' ') + '"';
|
||||
csvContent += ',';
|
||||
let rc_cpos_list = [];
|
||||
for (let cpos = match.rc[0]; cpos <= match.rc[1]; cpos++) {rc_cpos_list.push(cpos);}
|
||||
if (match.rc !== null) {
|
||||
csvContent += '"' + rc_cpos_list.map(x => data.cpos_lookup[x].word.replace('"', '""')).join(' ') + '"';
|
||||
}
|
||||
}
|
||||
blob = new Blob([csvContent], {type: 'text/csv;charset=utf-8;'});
|
||||
} else {
|
||||
blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json;charset=utf-8;'});
|
||||
}
|
||||
let url = URL.createObjectURL(blob);
|
||||
let pom = document.createElement('a');
|
||||
pom.href = url;
|
||||
pom.setAttribute('download', exportFileNameWithExtension);
|
||||
pom.click();
|
||||
this.elements.progress.classList.add('hide');
|
||||
this.app.enableActionElements();
|
||||
});
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
this.elements.subcorpusActions.querySelector('.subcorpus-delete-trigger').addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
|
||||
subcorpus.o.drop().then(
|
||||
(cQiStatus) => {
|
||||
app.flash(`${subcorpus.o.name} deleted`, 'corpus');
|
||||
delete this.data.subcorpora[subcorpus.o.name];
|
||||
this.settings.selectedSubcorpus = undefined;
|
||||
for (let subcorpusName in this.data.subcorpora) {
|
||||
this.settings.selectedSubcorpus = subcorpusName;
|
||||
break;
|
||||
}
|
||||
this.renderSubcorpusList();
|
||||
if (this.settings.selectedSubcorpus) {
|
||||
this.renderSubcorpusInfo();
|
||||
this.renderSubcorpusActions();
|
||||
this.renderSubcorpusItems();
|
||||
this.renderSubcorpusPagination();
|
||||
} else {
|
||||
this.clearSubcorpusInfo();
|
||||
this.clearSubcorpusActions();
|
||||
this.clearSubcorpusItems();
|
||||
this.clearSubcorpusPagination();
|
||||
}
|
||||
},
|
||||
(cqiError) => {
|
||||
let errorString = `${cqiError.code}: ${cqiError.constructor.name}`;
|
||||
app.flash(errorString, 'error');
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
clearSubcorpusItems() {
|
||||
// Destroy with .p-attr elements associated Materialize tooltips
|
||||
for (let pAttrElement of this.elements.subcorpusItems.querySelectorAll('.p-attr.tooltipped')) {
|
||||
M.Tooltip.getInstance(pAttrElement)?.destroy();
|
||||
}
|
||||
this.elements.subcorpusItems.innerHTML = `
|
||||
<tr class="show-if-only-child">
|
||||
<td colspan="100%">
|
||||
<p>
|
||||
<span class="card-title"><i class="left material-icons" style="font-size: inherit;">search</i>Nothing here...</span><br>
|
||||
No matches available.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
`.trim();
|
||||
}
|
||||
|
||||
renderSubcorpusItems() {
|
||||
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
|
||||
this.clearSubcorpusItems();
|
||||
for (let item of subcorpus.p.items) {
|
||||
let itemIsSelected = item.num in subcorpus.selectedItems;
|
||||
this.elements.subcorpusItems.innerHTML += `
|
||||
<tr class="item" data-id="${item.num}">
|
||||
<td class="num">${item.num}</td>
|
||||
<td class="text-title">${this.foo(...item.c)}</td>
|
||||
<td class="left-context">${item.lc ? this.cposRange2HTML(...item.lc) : ''}</td>
|
||||
<td class="kwic">${this.cposRange2HTML(...item.c)}</td>
|
||||
<td class="right-context">${item.rc ? this.cposRange2HTML(...item.rc) : ''}</td>
|
||||
<td class="actions right-align">
|
||||
<a class="btn-floating btn-small waves-effect waves-light corpus-analysis-action goto-reader-trigger">
|
||||
<i class="material-icons prefix">search</i>
|
||||
</a>
|
||||
<a class="btn-floating btn-small waves-effect waves-light corpus-analysis-action select-trigger ${itemIsSelected ? 'green' : ''}">
|
||||
<i class="material-icons prefix">${itemIsSelected ? 'check' : 'add'}</i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
`.trim();
|
||||
}
|
||||
this.setTextStyle();
|
||||
this.setTokenRepresentation();
|
||||
for (let gotoReaderTriggerElement of this.elements.subcorpusItems.querySelectorAll('.goto-reader-trigger')) {
|
||||
gotoReaderTriggerElement.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
let corpusAnalysisReader = this.app.extensions.Reader;
|
||||
let itemId = parseInt(gotoReaderTriggerElement.closest('.item').dataset.id);
|
||||
let item = undefined;
|
||||
for (let x of subcorpus.p.items) {if (x.num === itemId) {item = x;}}
|
||||
let page = Math.max(1, Math.ceil(item.c[0] / corpusAnalysisReader.settings.perPage));
|
||||
corpusAnalysisReader.page(page, () => {
|
||||
let range = new Range();
|
||||
let leftCpos = corpusAnalysisReader.data.corpus.p.items[0].includes(item.c[0]) ? item.c[0] : corpusAnalysisReader.data.corpus.p.items[0][0];
|
||||
let rightCpos = corpusAnalysisReader.data.corpus.p.items[0].includes(item.c[1]) ? item.c[1] : corpusAnalysisReader.data.corpus.p.items[0].at(-1);
|
||||
let leftElement = corpusAnalysisReader.elements.corpus.querySelector(`.p-attr[data-cpos="${leftCpos}"]`);
|
||||
let rightElement = corpusAnalysisReader.elements.corpus.querySelector(`.p-attr[data-cpos="${rightCpos}"]`);
|
||||
range.setStartBefore(leftElement);
|
||||
range.setEndAfter(rightElement);
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().addRange(range);
|
||||
});
|
||||
this.app.elements.m.extensionTabs.select(
|
||||
this.app.extensions.Reader.elements.container.id
|
||||
);
|
||||
});
|
||||
}
|
||||
for (let selectTriggerElement of this.elements.subcorpusItems.querySelectorAll('.select-trigger')) {
|
||||
selectTriggerElement.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
let itemElement = selectTriggerElement.closest('.item');
|
||||
let itemId = parseInt(itemElement.dataset.id);
|
||||
if (subcorpus.selectedItems.has(itemId)) {
|
||||
subcorpus.selectedItems.delete(itemId);
|
||||
selectTriggerElement.classList.remove('green');
|
||||
selectTriggerElement.querySelector('i').textContent = 'add';
|
||||
} else {
|
||||
subcorpus.selectedItems.add(itemId);
|
||||
selectTriggerElement.classList.add('green');
|
||||
selectTriggerElement.querySelector('i').textContent = 'check';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clearSubcorpusPagination() {
|
||||
this.elements.subcorpusPagination.innerHTML = '';
|
||||
this.elements.subcorpusPagination.classList.add('hide');
|
||||
}
|
||||
|
||||
renderSubcorpusPagination() {
|
||||
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
|
||||
this.clearSubcorpusPagination();
|
||||
if (subcorpus.p.pages === 0) {return;}
|
||||
this.elements.subcorpusPagination.innerHTML += `
|
||||
<li class="${subcorpus.p.page === 1 ? 'disabled' : 'waves-effect'}">
|
||||
<a class="corpus-analysis-action pagination-trigger" ${subcorpus.p.page === 1 ? '' : 'data-target="1"'}>
|
||||
<i class="material-icons">first_page</i>
|
||||
</a>
|
||||
</li>
|
||||
`.trim();
|
||||
this.elements.subcorpusPagination.innerHTML += `
|
||||
<li class="${subcorpus.p.has_prev ? 'waves-effect' : 'disabled'}">
|
||||
<a class="corpus-analysis-action pagination-trigger" ${subcorpus.p.has_prev ? 'data-target="' + subcorpus.p.prev_num + '"' : ''}>
|
||||
<i class="material-icons">chevron_left</i>
|
||||
</a>
|
||||
</li>
|
||||
`.trim();
|
||||
for (let i = 1; i <= subcorpus.p.pages; i++) {
|
||||
this.elements.subcorpusPagination.innerHTML += `
|
||||
<li class="${i === subcorpus.p.page ? 'active' : 'waves-effect'}">
|
||||
<a class="corpus-analysis-action pagination-trigger" ${i === subcorpus.p.page ? '' : 'data-target="' + i + '"'}>${i}</a>
|
||||
</li>
|
||||
`.trim();
|
||||
}
|
||||
this.elements.subcorpusPagination.innerHTML += `
|
||||
<li class="${subcorpus.p.has_next ? 'waves-effect' : 'disabled'}">
|
||||
<a class="corpus-analysis-action pagination-trigger" ${subcorpus.p.has_next ? 'data-target="' + subcorpus.p.next_num + '"' : ''}>
|
||||
<i class="material-icons">chevron_right</i>
|
||||
</a>
|
||||
</li>
|
||||
`.trim();
|
||||
this.elements.subcorpusPagination.innerHTML += `
|
||||
<li class="${subcorpus.p.page === subcorpus.p.pages ? 'disabled' : 'waves-effect'}">
|
||||
<a class="corpus-analysis-action pagination-trigger" ${subcorpus.p.page === subcorpus.p.pages ? '' : 'data-target="' + subcorpus.p.pages + '"'}>
|
||||
<i class="material-icons">last_page</i>
|
||||
</a>
|
||||
</li>
|
||||
`.trim();
|
||||
for (let paginationTriggerElement of this.elements.subcorpusPagination.querySelectorAll('.pagination-trigger[data-target]')) {
|
||||
paginationTriggerElement.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
this.app.disableActionElements();
|
||||
this.elements.progress.classList.remove('hide');
|
||||
let page = parseInt(paginationTriggerElement.dataset.target);
|
||||
subcorpus.o.paginate(this.settings.context, page, this.settings.perPage)
|
||||
.then(
|
||||
(paginatedSubcorpus) => {
|
||||
subcorpus.p = paginatedSubcorpus;
|
||||
this.renderSubcorpusItems();
|
||||
this.renderSubcorpusPagination();
|
||||
this.elements.progress.classList.add('hide');
|
||||
this.app.enableActionElements();
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
this.elements.subcorpusPagination.classList.remove('hide');
|
||||
}
|
||||
|
||||
foo(firstCpos, lastCpos) {
|
||||
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
|
||||
/* Returns a list of texts occuring in this cpos range */
|
||||
let textIds = new Set();
|
||||
for (let cpos = firstCpos; cpos <= lastCpos; cpos++) {
|
||||
textIds.add(subcorpus.p.lookups.cpos_lookup[cpos].text);
|
||||
}
|
||||
return [...textIds].map(x => subcorpus.p.lookups.text_lookup[x].title).join(', ');
|
||||
}
|
||||
|
||||
cposRange2HTML(firstCpos, lastCpos) {
|
||||
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
|
||||
let html = '';
|
||||
for (let cpos = firstCpos; cpos <= lastCpos; cpos++) {
|
||||
let prevPAttr = cpos > firstCpos ? subcorpus.p.lookups.cpos_lookup[cpos - 1] : null;
|
||||
let pAttr = subcorpus.p.lookups.cpos_lookup[cpos];
|
||||
let nextPAttr = cpos < lastCpos ? subcorpus.p.lookups.cpos_lookup[cpos + 1] : null;
|
||||
let isEntityStart = 'ent' in pAttr && pAttr.ent !== prevPAttr?.ent;
|
||||
let isEntityEnd = 'ent' in pAttr && pAttr.ent !== nextPAttr?.ent;
|
||||
// Add a space before pAttr
|
||||
if (cpos !== firstCpos || pAttr.simple_pos !== 'PUNCT') {html += ' ';}
|
||||
// Add entity start
|
||||
if (isEntityStart) {
|
||||
html += `<span class="s-attr" data-cpos="${cpos}" data-id="${pAttr.ent}" data-s-attr-type="ent" data-s-attr-ent-type="${subcorpus.p.lookups.ent_lookup[pAttr.ent].type}">`;
|
||||
}
|
||||
// Add pAttr
|
||||
html += `<span class="p-attr" data-cpos="${cpos}"></span>`;
|
||||
// Add entity end
|
||||
if (isEntityEnd) {
|
||||
html += ` <span class="black-text hide new white ent-indicator" data-badge-caption="">${subcorpus.p.lookups.ent_lookup[pAttr.ent].type}</span>`;
|
||||
html += '</span>';
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
setTextStyle() {
|
||||
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
|
||||
if (this.settings.textStyle >= 0) {
|
||||
// Destroy with .p-attr elements associated Materialize tooltips
|
||||
for (let pAttrElement of this.elements.subcorpusItems.querySelectorAll('.p-attr.tooltipped')) {
|
||||
M.Tooltip.getInstance(pAttrElement)?.destroy();
|
||||
}
|
||||
// Set basic styling on .p-attr elements
|
||||
for (let pAttrElement of this.elements.subcorpusItems.querySelectorAll('.p-attr')) {
|
||||
pAttrElement.setAttribute('class', 'p-attr');
|
||||
}
|
||||
// Set basic styling on .s-attr[data-type="ent"] elements
|
||||
for (let entElement of this.elements.subcorpusItems.querySelectorAll('.s-attr[data-s-attr-type="ent"]')) {
|
||||
entElement.querySelector('.ent-indicator').classList.add('hide');
|
||||
entElement.removeAttribute('style');
|
||||
entElement.setAttribute('class', 's-attr');
|
||||
}
|
||||
}
|
||||
if (this.settings.textStyle >= 1) {
|
||||
// Set advanced styling on .s-attr[data-type="ent"] elements
|
||||
for (let entElement of this.elements.subcorpusItems.querySelectorAll('.s-attr[data-s-attr-type="ent"]')) {
|
||||
entElement.classList.add('chip');
|
||||
entElement.querySelector('.ent-indicator').classList.remove('hide');
|
||||
}
|
||||
}
|
||||
if (this.settings.textStyle >= 2) {
|
||||
// Set advanced styling on .p-attr elements
|
||||
for (let pAttrElement of this.elements.subcorpusItems.querySelectorAll('.p-attr')) {
|
||||
pAttrElement.classList.add('chip', 'hoverable', 'tooltipped');
|
||||
let cpos = pAttrElement.dataset.cpos;
|
||||
let pAttr = subcorpus.p.lookups.cpos_lookup[cpos];
|
||||
let positionalPropertiesHTML = `
|
||||
<p class="left-align">
|
||||
<b>Positional properties</b><br>
|
||||
<span>Token: ${cpos}</span>
|
||||
`.trim();
|
||||
let structuralPropertiesHTML = `
|
||||
<p class="left-align">
|
||||
<b>Structural properties</b>
|
||||
`.trim();
|
||||
for (let [property, propertyValue] of Object.entries(pAttr)) {
|
||||
if (['lemma', 'ner', 'pos', 'simple_pos', 'word'].includes(property)) {
|
||||
if (propertyValue === 'None') {continue;}
|
||||
positionalPropertiesHTML += `<br><i class="material-icons" style="font-size: inherit;">subdirectory_arrow_right</i>${property}: ${propertyValue}`;
|
||||
} else {
|
||||
structuralPropertiesHTML += `<br><span>${property}: ${propertyValue}</span>`;
|
||||
if (!(`${property}_lookup` in subcorpus.p.lookups)) {continue;}
|
||||
for (let [subproperty, subpropertyValue] of Object.entries(subcorpus.p.lookups[`${property}_lookup`][propertyValue])) {
|
||||
if (subpropertyValue === 'NULL') {continue;}
|
||||
structuralPropertiesHTML += `<br><i class="material-icons" style="font-size: inherit;">subdirectory_arrow_right</i>${subproperty}: ${subpropertyValue}`
|
||||
}
|
||||
}
|
||||
}
|
||||
positionalPropertiesHTML += '</p>';
|
||||
structuralPropertiesHTML += '</p>';
|
||||
M.Tooltip.init(
|
||||
pAttrElement,
|
||||
{html: positionalPropertiesHTML + structuralPropertiesHTML}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTokenRepresentation() {
|
||||
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
|
||||
for (let pAttrElement of this.elements.subcorpusItems.querySelectorAll('.p-attr')) {
|
||||
let pAttr = subcorpus.p.lookups.cpos_lookup[pAttrElement.dataset.cpos];
|
||||
pAttrElement.innerText = pAttr[this.settings.tokenRepresentation];
|
||||
}
|
||||
}
|
||||
}
|
688
app/static/js/corpus-analysis/cqi/api/client.js
Normal file
688
app/static/js/corpus-analysis/cqi/api/client.js
Normal file
@ -0,0 +1,688 @@
|
||||
nopaque.corpus_analysis.cqi.api.Client = class Client {
|
||||
/**
|
||||
* @param {string} host
|
||||
* @param {number} [timeout=60] timeout
|
||||
* @param {string} [version=0.1] version
|
||||
*/
|
||||
constructor(host, timeout = 60, version = '0.1') {
|
||||
this.host = host;
|
||||
this.timeout = timeout * 1000; // convert seconds to milliseconds
|
||||
this.version = version;
|
||||
this.socket = io(
|
||||
this.host,
|
||||
{
|
||||
transports: ['websocket'],
|
||||
upgrade: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fn_name
|
||||
* @param {object} [fn_args={}]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async #request(fn_name, fn_args = {}) {
|
||||
// TODO: implement timeout
|
||||
let response = await this.socket.emitWithAck('exec', fn_name, fn_args);
|
||||
if (response.code === 200) {
|
||||
return response.payload;
|
||||
} else if (response.code === 500) {
|
||||
throw new Error(`[${response.code}] ${response.msg}`);
|
||||
} else if (response.code === 502) {
|
||||
if (response.payload.code in nopaque.corpus_analysis.cqi.errors.lookup) {
|
||||
throw new nopaque.corpus_analysis.cqi.errors.lookup[response.payload.code]();
|
||||
} else {
|
||||
throw new nopaque.corpus_analysis.cqi.errors.CQiError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusConnectOk>}
|
||||
*/
|
||||
async ctrl_connect(username, password) {
|
||||
const fn_name = 'ctrl_connect';
|
||||
const fn_args = {username: username, password: password};
|
||||
let payload = await this.#request(fn_name, fn_args);
|
||||
return new nopaque.corpus_analysis.cqi.status.lookup[payload.code]();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusByeOk>}
|
||||
*/
|
||||
async ctrl_bye() {
|
||||
const fn_name = 'ctrl_bye';
|
||||
let payload = await this.#request(fn_name);
|
||||
return new nopaque.corpus_analysis.cqi.status.lookup[payload.code]();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async ctrl_user_abort() {
|
||||
const fn_name = 'ctrl_user_abort';
|
||||
return await this.#request(fn_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusPingOk>}
|
||||
*/
|
||||
async ctrl_ping() {
|
||||
const fn_name = 'ctrl_ping';
|
||||
let payload = await this.#request(fn_name);
|
||||
return new nopaque.corpus_analysis.cqi.status.lookup[payload.code]();
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-text error message for the last general error reported
|
||||
* by the CQi server
|
||||
*
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async ctrl_last_general_error() {
|
||||
const fn_name = 'ctrl_last_general_error';
|
||||
return await this.#request(fn_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async ask_feature_cqi_1_0() {
|
||||
const fn_name = 'ask_feature_cqi_1_0';
|
||||
return await this.#request(fn_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async ask_feature_cl_2_3() {
|
||||
const fn_name = 'ask_feature_cl_2_3';
|
||||
return await this.#request(fn_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async ask_feature_cqp_2_3() {
|
||||
const fn_name = 'ask_feature_cqp_2_3';
|
||||
return await this.#request(fn_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async corpus_list_corpora() {
|
||||
const fn_name = 'corpus_list_corpora';
|
||||
return await this.#request(fn_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} corpus
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async corpus_charset(corpus) {
|
||||
const fn_name = 'corpus_charset';
|
||||
const fn_args = {corpus: corpus};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} corpus
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async corpus_properties(corpus) {
|
||||
const fn_name = 'corpus_properties';
|
||||
const fn_args = {corpus: corpus};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} corpus
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async corpus_positional_attributes(corpus) {
|
||||
const fn_name = 'corpus_positional_attributes';
|
||||
const fn_args = {corpus: corpus};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} corpus
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async corpus_structural_attributes(corpus) {
|
||||
const fn_name = 'corpus_structural_attributes';
|
||||
const fn_args = {corpus: corpus};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} corpus
|
||||
* @param {string} attribute
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async corpus_structural_attribute_has_values(corpus, attribute) {
|
||||
const fn_name = 'corpus_structural_attribute_has_values';
|
||||
const fn_args = {corpus: corpus, attribute: attribute};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} corpus
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async corpus_alignment_attributes(corpus) {
|
||||
const fn_name = 'corpus_alignment_attributes';
|
||||
const fn_args = {corpus: corpus};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* the full name of <corpus> as specified in its registry entry
|
||||
*
|
||||
* @param {string} corpus
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async corpus_full_name(corpus) {
|
||||
const fn_name = 'corpus_full_name';
|
||||
const fn_args = {corpus: corpus};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the contents of the .info file of <corpus> as a list of lines
|
||||
*
|
||||
* @param {string} corpus
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async corpus_info(corpus) {
|
||||
const fn_name = 'corpus_info';
|
||||
const fn_args = {corpus: corpus};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* try to unload a corpus and all its attributes from memory
|
||||
*
|
||||
* @param {string} corpus
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusOk>}
|
||||
*/
|
||||
async corpus_drop_corpus(corpus) {
|
||||
const fn_name = 'corpus_drop_corpus';
|
||||
const fn_args = {corpus: corpus};
|
||||
let payload = await this.#request(fn_name, fn_args);
|
||||
return new nopaque.corpus_analysis.cqi.status.lookup[payload.code]();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the size of <attribute>:
|
||||
* - number of tokens (positional)
|
||||
* - number of regions (structural)
|
||||
* - number of alignments (alignment)
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async cl_attribute_size(attribute) {
|
||||
const fn_name = 'cl_attribute_size';
|
||||
const fn_args = {attribute: attribute};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the number of entries in the lexicon of a positional attribute;
|
||||
*
|
||||
* valid lexicon IDs range from 0 .. (lexicon_size - 1)
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async cl_lexicon_size(attribute) {
|
||||
const fn_name = 'cl_lexicon_size';
|
||||
const fn_args = {attribute: attribute};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* unload attribute from memory
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusOk>}
|
||||
*/
|
||||
async cl_drop_attribute(attribute) {
|
||||
const fn_name = 'cl_drop_attribute';
|
||||
const fn_args = {attribute: attribute};
|
||||
let payload = await this.#request(fn_name, fn_args);
|
||||
return new nopaque.corpus_analysis.cqi.status.lookup[payload.code]();
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: simple (scalar) mappings are applied to lists (the returned list
|
||||
* has exactly the same length as the list passed as an argument)
|
||||
*/
|
||||
|
||||
/**
|
||||
* returns -1 for every string in <strings> that is not found in the lexicon
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {strings[]} string
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cl_str2id(attribute, strings) {
|
||||
const fn_name = 'cl_str2id';
|
||||
const fn_args = {attribute: attribute, strings: strings};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns "" for every ID in <id> that is out of range
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {number[]} id
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async cl_id2str(attribute, id) {
|
||||
const fn_name = 'cl_id2str';
|
||||
const fn_args = {attribute: attribute, id: id};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns 0 for every ID in <id> that is out of range
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {number[]} id
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cl_id2freq(attribute, id) {
|
||||
const fn_name = 'cl_id2freq';
|
||||
const fn_args = {attribute: attribute, id: id};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns -1 for every corpus position in <cpos> that is out of range
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {number[]} cpos
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cl_cpos2id(attribute, cpos) {
|
||||
const fn_name = 'cl_cpos2id';
|
||||
const fn_args = {attribute: attribute, cpos: cpos};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns "" for every corpus position in <cpos> that is out of range
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {number[]} cpos
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async cl_cpos2str(attribute, cpos) {
|
||||
const fn_name = 'cl_cpos2str';
|
||||
const fn_args = {attribute: attribute, cpos: cpos};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns -1 for every corpus position not inside a structure region
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {number[]} cpos
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cl_cpos2struc(attribute, cpos) {
|
||||
const fn_name = 'cl_cpos2struc';
|
||||
const fn_args = {attribute: attribute, cpos: cpos};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: temporary addition for the Euralex2000 tutorial, but should
|
||||
* probably be included in CQi specs
|
||||
*/
|
||||
|
||||
/**
|
||||
* returns left boundary of s-attribute region enclosing cpos,
|
||||
* -1 if not in region
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {number[]} cpos
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cl_cpos2lbound(attribute, cpos) {
|
||||
const fn_name = 'cl_cpos2lbound';
|
||||
const fn_args = {attribute: attribute, cpos: cpos};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns right boundary of s-attribute region enclosing cpos,
|
||||
* -1 if not in region
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {number[]} cpos
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cl_cpos2rbound(attribute, cpos) {
|
||||
const fn_name = 'cl_cpos2rbound';
|
||||
const fn_args = {attribute: attribute, cpos: cpos};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns -1 for every corpus position not inside an alignment
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {number[]} cpos
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cl_cpos2alg(attribute, cpos) {
|
||||
const fn_name = 'cl_cpos2alg';
|
||||
const fn_args = {attribute: attribute, cpos: cpos};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns annotated string values of structure regions in <strucs>;
|
||||
* "" if out of range
|
||||
*
|
||||
* check corpus_structural_attribute_has_values(<attribute>) first
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {number[]} strucs
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async cl_struc2str(attribute, strucs) {
|
||||
const fn_name = 'cl_struc2str';
|
||||
const fn_args = {attribute: attribute, strucs: strucs};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: the following mappings take a single argument and return multiple
|
||||
* values, including lists of arbitrary size
|
||||
*/
|
||||
|
||||
/**
|
||||
* returns all corpus positions where the given token occurs
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {number} id
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cl_id2cpos(attribute, id) {
|
||||
const fn_name = 'cl_id2cpos';
|
||||
const fn_args = {attribute: attribute, id: id};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns all corpus positions where one of the tokens in <id_list> occurs;
|
||||
* the returned list is sorted as a whole, not per token id
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {number[]} id_list
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cl_idlist2cpos(attribute, id_list) {
|
||||
const fn_name = 'cl_idlist2cpos';
|
||||
const fn_args = {attribute: attribute, id_list: id_list};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns lexicon IDs of all tokens that match <regex>;
|
||||
* the returned list may be empty (size 0);
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {string} regex
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cl_regex2id(attribute, regex) {
|
||||
const fn_name = 'cl_regex2id';
|
||||
const fn_args = {attribute: attribute, regex: regex};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns start and end corpus positions of structure region <struc>
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {number} struc
|
||||
* @returns {Promise<[number, number]>}
|
||||
*/
|
||||
async cl_struc2cpos(attribute, struc) {
|
||||
const fn_name = 'cl_struc2cpos';
|
||||
const fn_args = {attribute: attribute, struc: struc};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns (src_start, src_end, target_start, target_end)
|
||||
*
|
||||
* @param {string} attribute
|
||||
* @param {number} alg
|
||||
* @returns {Promise<[number, number, number, number]>}
|
||||
*/
|
||||
async alg2cpos(attribute, alg) {
|
||||
const fn_name = 'alg2cpos';
|
||||
const fn_args = {attribute: attribute, alg: alg};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* <query> must include the ';' character terminating the query.
|
||||
*
|
||||
* @param {string} mother_corpus
|
||||
* @param {string} subcorpus_name
|
||||
* @param {string} query
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusOk>}
|
||||
*/
|
||||
async cqp_query(mother_corpus, subcorpus_name, query) {
|
||||
const fn_name = 'cqp_query';
|
||||
const fn_args = {mother_corpus: mother_corpus, subcorpus_name: subcorpus_name, query: query};
|
||||
let payload = await this.#request(fn_name, fn_args);
|
||||
return new nopaque.corpus_analysis.cqi.status.lookup[payload.code]();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} corpus
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async cqp_list_subcorpora(corpus) {
|
||||
const fn_name = 'cqp_list_subcorpora';
|
||||
const fn_args = {corpus: corpus};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} subcorpus
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async cqp_subcorpus_size(subcorpus) {
|
||||
const fn_name = 'cqp_subcorpus_size';
|
||||
const fn_args = {subcorpus: subcorpus};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} subcorpus
|
||||
* @param {number} field
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async cqp_subcorpus_has_field(subcorpus, field) {
|
||||
const fn_name = 'cqp_subcorpus_has_field';
|
||||
const fn_args = {subcorpus: subcorpus, field: field};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the values of <field> for match ranges <first> .. <last>
|
||||
* in <subcorpus>. <field> is one of the nopaque.corpus_analysis.cqi.constants.FIELD_* constants.
|
||||
*
|
||||
* @param {string} subcorpus
|
||||
* @param {number} field
|
||||
* @param {number} first
|
||||
* @param {number} last
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cqp_dump_subcorpus(subcorpus, field, first, last) {
|
||||
const fn_name = 'cqp_dump_subcorpus';
|
||||
const fn_args = {subcorpus: subcorpus, field: field, first: first, last: last};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* delete a subcorpus from memory
|
||||
*
|
||||
* @param {string} subcorpus
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusOk>}
|
||||
*/
|
||||
async cqp_drop_subcorpus(subcorpus) {
|
||||
const fn_name = 'cqp_drop_subcorpus';
|
||||
const fn_args = {subcorpus: subcorpus};
|
||||
let payload = await this.#request(fn_name, fn_args);
|
||||
return new nopaque.corpus_analysis.cqi.status.lookup[payload.code]();
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: The following two functions are temporarily included for the
|
||||
* Euralex 2000 tutorial demo
|
||||
*/
|
||||
|
||||
/**
|
||||
* frequency distribution of single tokens
|
||||
*
|
||||
* returns <n> (id, frequency) pairs flattened into a list of size 2*<n>
|
||||
* field is one of
|
||||
* - nopaque.corpus_analysis.cqi.constants.FIELD_MATCH
|
||||
* - nopaque.corpus_analysis.cqi.constants.FIELD_TARGET
|
||||
* - nopaque.corpus_analysis.cqi.constants.FIELD_KEYWORD
|
||||
*
|
||||
* NB: pairs are sorted by frequency desc.
|
||||
*
|
||||
* @param {string} subcorpus
|
||||
* @param {number} cutoff
|
||||
* @param {number} field
|
||||
* @param {string} attribute
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cqp_fdist_1(subcorpus, cutoff, field, attribute) {
|
||||
const fn_name = 'cqp_fdist_1';
|
||||
const fn_args = {subcorpus: subcorpus, cutoff: cutoff, field: field, attribute: attribute};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* frequency distribution of pairs of tokens
|
||||
*
|
||||
* returns <n> (id1, id2, frequency) pairs flattened into a list of
|
||||
* size 3*<n>
|
||||
*
|
||||
* NB: triples are sorted by frequency desc.
|
||||
*
|
||||
* @param {string} subcorpus
|
||||
* @param {number} cutoff
|
||||
* @param {number} field1
|
||||
* @param {string} attribute1
|
||||
* @param {number} field2
|
||||
* @param {string} attribute2
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cqp_fdist_2(subcorpus, cutoff, field1, attribute1, field2, attribute2) {
|
||||
const fn_name = 'cqp_fdist_2';
|
||||
const fn_args = {subcorpus: subcorpus, cutoff: cutoff, field1: field1, attribute1: attribute1, field2: field2, attribute2: attribute2};
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**************************************************************************
|
||||
* NOTE: The following is not included in the CQi specification. *
|
||||
**************************************************************************/
|
||||
/**************************************************************************
|
||||
* Custom additions for nopaque *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* @param {string} corpus
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusOk>}
|
||||
*/
|
||||
async ext_corpus_update_db(corpus) {
|
||||
const fn_name = 'ext_corpus_update_db';
|
||||
const fn_args = {corpus: corpus};
|
||||
let payload = await this.#request(fn_name, fn_args);
|
||||
return new nopaque.corpus_analysis.cqi.status.lookup[payload.code]();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} corpus
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async ext_corpus_static_data(corpus) {
|
||||
const fn_name = 'ext_corpus_static_data';
|
||||
const fn_args = {corpus: corpus};
|
||||
let compressedEncodedData = await this.#request(fn_name, fn_args);
|
||||
let data = pako.inflate(compressedEncodedData, {to: 'string'});
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} corpus
|
||||
* @param {number=} page
|
||||
* @param {number=} per_page
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async ext_corpus_paginate_corpus(corpus, page, per_page) {
|
||||
const fn_name = 'ext_corpus_paginate_corpus';
|
||||
const fn_args = {corpus: corpus}
|
||||
if (page !== undefined) {fn_args.page = page;}
|
||||
if (per_page !== undefined) {fn_args.per_page = per_page;}
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} subcorpus
|
||||
* @param {number=} context
|
||||
* @param {number=} page
|
||||
* @param {number=} per_page
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async ext_cqp_paginate_subcorpus(subcorpus, context, page, per_page) {
|
||||
const fn_name = 'ext_cqp_paginate_subcorpus';
|
||||
const fn_args = {subcorpus: subcorpus}
|
||||
if (context !== undefined) {fn_args.context = context;}
|
||||
if (page !== undefined) {fn_args.page = page;}
|
||||
if (per_page !== undefined) {fn_args.per_page = per_page;}
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} subcorpus
|
||||
* @param {number[]} match_id_list
|
||||
* @param {number=} context
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async ext_cqp_partial_export_subcorpus(subcorpus, match_id_list, context) {
|
||||
const fn_name = 'ext_cqp_partial_export_subcorpus';
|
||||
const fn_args = {subcorpus: subcorpus, match_id_list: match_id_list};
|
||||
if (context !== undefined) {fn_args.context = context;}
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} subcorpus
|
||||
* @param {number=} context
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async ext_cqp_export_subcorpus(subcorpus, context) {
|
||||
const fn_name = 'ext_cqp_export_subcorpus';
|
||||
const fn_args = {subcorpus: subcorpus};
|
||||
if (context !== undefined) {fn_args.context = context;}
|
||||
return await this.#request(fn_name, fn_args);
|
||||
}
|
||||
};
|
1
app/static/js/corpus-analysis/cqi/api/index.js
Normal file
1
app/static/js/corpus-analysis/cqi/api/index.js
Normal file
@ -0,0 +1 @@
|
||||
nopaque.corpus_analysis.cqi.api = {};
|
57
app/static/js/corpus-analysis/cqi/client.js
Normal file
57
app/static/js/corpus-analysis/cqi/client.js
Normal file
@ -0,0 +1,57 @@
|
||||
nopaque.corpus_analysis.cqi.Client = class Client {
|
||||
/**
|
||||
* @param {string} host
|
||||
* @param {number} [timeout=60] timeout
|
||||
* @param {string} [version=0.1] version
|
||||
*/
|
||||
constructor(host, timeout = 60, version = '0.1') {
|
||||
/** @type {nopaque.corpus_analysis.cqi.api.Client} */
|
||||
this.api = new nopaque.corpus_analysis.cqi.api.Client(host, timeout, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {nopaque.corpus_analysis.cqi.models.corpora.CorpusCollection}
|
||||
*/
|
||||
get corpora() {
|
||||
return new nopaque.corpus_analysis.cqi.models.corpora.CorpusCollection(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusByeOk>}
|
||||
*/
|
||||
async bye() {
|
||||
return await this.api.ctrl_bye();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusConnectOk>}
|
||||
*/
|
||||
async connect(username, password) {
|
||||
return await this.api.ctrl_connect(username, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusPingOk>}
|
||||
*/
|
||||
async ping() {
|
||||
return await this.api.ctrl_ping();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async userAbort() {
|
||||
return await this.api.ctrl_user_abort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for "bye" method
|
||||
*
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusByeOk>}
|
||||
*/
|
||||
async disconnect() {
|
||||
return await this.api.ctrl_bye();
|
||||
}
|
||||
};
|
43
app/static/js/corpus-analysis/cqi/constants.js
Normal file
43
app/static/js/corpus-analysis/cqi/constants.js
Normal file
@ -0,0 +1,43 @@
|
||||
nopaque.corpus_analysis.cqi.constants = {};
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_KEYWORD = 9;
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_MATCH = 16;
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_MATCHEND = 17;
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_TARGET = 0;
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_TARGET_0 = 0;
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_TARGET_1 = 1;
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_TARGET_2 = 2;
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_TARGET_3 = 3;
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_TARGET_4 = 4;
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_TARGET_5 = 5;
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_TARGET_6 = 6;
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_TARGET_7 = 7;
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_TARGET_8 = 8;
|
||||
|
||||
/** @type {number} */
|
||||
nopaque.corpus_analysis.cqi.constants.FIELD_TARGET_9 = 9;
|
185
app/static/js/corpus-analysis/cqi/errors.js
Normal file
185
app/static/js/corpus-analysis/cqi/errors.js
Normal file
@ -0,0 +1,185 @@
|
||||
nopaque.corpus_analysis.cqi.errors = {};
|
||||
|
||||
|
||||
/**
|
||||
* A base class from which all other errors inherit.
|
||||
* If you want to catch all errors that the CQi package might throw,
|
||||
* catch this base error.
|
||||
*/
|
||||
nopaque.corpus_analysis.cqi.errors.CQiError = class CQiError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = undefined;
|
||||
this.description = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.Error = class Error extends nopaque.corpus_analysis.cqi.errors.CQiError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 2;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.ErrorGeneralError = class ErrorGeneralError extends nopaque.corpus_analysis.cqi.errors.Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 513;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.ErrorConnectRefused = class ErrorConnectRefused extends nopaque.corpus_analysis.cqi.errors.Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 514;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.ErrorUserAbort = class ErrorUserAbort extends nopaque.corpus_analysis.cqi.errors.Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 515;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.ErrorSyntaxError = class ErrorSyntaxError extends nopaque.corpus_analysis.cqi.errors.Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 516;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.CLError = class Error extends nopaque.corpus_analysis.cqi.errors.CQiError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 4;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.CLErrorNoSuchAttribute = class CLErrorNoSuchAttribute extends nopaque.corpus_analysis.cqi.errors.CLError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 1025;
|
||||
this.description = "CQi server couldn't open attribute";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.CLErrorWrongAttributeType = class CLErrorWrongAttributeType extends nopaque.corpus_analysis.cqi.errors.CLError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 1026;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.CLErrorOutOfRange = class CLErrorOutOfRange extends nopaque.corpus_analysis.cqi.errors.CLError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 1027;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.CLErrorRegex = class CLErrorRegex extends nopaque.corpus_analysis.cqi.errors.CLError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 1028;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.CLErrorCorpusAccess = class CLErrorCorpusAccess extends nopaque.corpus_analysis.cqi.errors.CLError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 1029;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.CLErrorOutOfMemory = class CLErrorOutOfMemory extends nopaque.corpus_analysis.cqi.errors.CLError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 1030;
|
||||
this.description = 'CQi server has run out of memory; try discarding some other corpora and/or subcorpora';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.CLErrorInternal = class CLErrorInternal extends nopaque.corpus_analysis.cqi.errors.CLError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 1031;
|
||||
this.description = "The classical 'please contact technical support' error";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.CQPError = class Error extends nopaque.corpus_analysis.cqi.errors.CQiError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 5;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.CQPErrorGeneral = class CQPErrorGeneral extends nopaque.corpus_analysis.cqi.errors.CQPError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 1281;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.CQPErrorNoSuchCorpus = class CQPErrorNoSuchCorpus extends nopaque.corpus_analysis.cqi.errors.CQPError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 1282;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.CQPErrorInvalidField = class CQPErrorInvalidField extends nopaque.corpus_analysis.cqi.errors.CQPError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 1283;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.CQPErrorOutOfRange = class CQPErrorOutOfRange extends nopaque.corpus_analysis.cqi.errors.CQPError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.code = 1284;
|
||||
this.description = 'A number is out of range';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.errors.lookup = {
|
||||
2: nopaque.corpus_analysis.cqi.errors.Error,
|
||||
513: nopaque.corpus_analysis.cqi.errors.ErrorGeneralError,
|
||||
514: nopaque.corpus_analysis.cqi.errors.ErrorConnectRefused,
|
||||
515: nopaque.corpus_analysis.cqi.errors.ErrorUserAbort,
|
||||
516: nopaque.corpus_analysis.cqi.errors.ErrorSyntaxError,
|
||||
4: nopaque.corpus_analysis.cqi.errors.CLError,
|
||||
1025: nopaque.corpus_analysis.cqi.errors.CLErrorNoSuchAttribute,
|
||||
1026: nopaque.corpus_analysis.cqi.errors.CLErrorWrongAttributeType,
|
||||
1027: nopaque.corpus_analysis.cqi.errors.CLErrorOutOfRange,
|
||||
1028: nopaque.corpus_analysis.cqi.errors.CLErrorRegex,
|
||||
1029: nopaque.corpus_analysis.cqi.errors.CLErrorCorpusAccess,
|
||||
1030: nopaque.corpus_analysis.cqi.errors.CLErrorOutOfMemory,
|
||||
1031: nopaque.corpus_analysis.cqi.errors.CLErrorInternal,
|
||||
5: nopaque.corpus_analysis.cqi.errors.CQPError,
|
||||
1281: nopaque.corpus_analysis.cqi.errors.CQPErrorGeneral,
|
||||
1282: nopaque.corpus_analysis.cqi.errors.CQPErrorNoSuchCorpus,
|
||||
1283: nopaque.corpus_analysis.cqi.errors.CQPErrorInvalidField,
|
||||
1284: nopaque.corpus_analysis.cqi.errors.CQPErrorOutOfRange
|
||||
};
|
1
app/static/js/corpus-analysis/cqi/index.js
Normal file
1
app/static/js/corpus-analysis/cqi/index.js
Normal file
@ -0,0 +1 @@
|
||||
nopaque.corpus_analysis.cqi = {};
|
289
app/static/js/corpus-analysis/cqi/models/attributes.js
Normal file
289
app/static/js/corpus-analysis/cqi/models/attributes.js
Normal file
@ -0,0 +1,289 @@
|
||||
nopaque.corpus_analysis.cqi.models.attributes = {};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.models.attributes.Attribute = class Attribute extends nopaque.corpus_analysis.cqi.models.resource.Model {
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get apiName() {
|
||||
return this.attrs.api_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get name() {
|
||||
return this.attrs.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
get size() {
|
||||
return this.attrs.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusOk>}
|
||||
*/
|
||||
async drop() {
|
||||
return await this.client.api.cl_drop_attribute(this.apiName);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.models.attributes.AttributeCollection = class AttributeCollection extends nopaque.corpus_analysis.cqi.models.resource.Collection {
|
||||
/** @type{typeof nopaque.corpus_analysis.cqi.models.attributes.Attribute} */
|
||||
static model = nopaque.corpus_analysis.cqi.models.attributes.Attribute;
|
||||
|
||||
/**
|
||||
* @param {nopaque.corpus_analysis.cqi.Client} client
|
||||
* @param {nopaque.corpus_analysis.cqi.models.corpora.Corpus} corpus
|
||||
*/
|
||||
constructor(client, corpus) {
|
||||
super(client);
|
||||
/** @type {nopaque.corpus_analysis.cqi.models.corpora.Corpus} */
|
||||
this.corpus = corpus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} attributeName
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async _get(attributeName) {
|
||||
/** @type{string} */
|
||||
let apiName = `${this.corpus.apiName}.${attributeName}`;
|
||||
return {
|
||||
api_name: apiName,
|
||||
name: attributeName,
|
||||
size: await this.client.api.cl_attribute_size(apiName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} attributeName
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.models.attributes.Attribute>}
|
||||
*/
|
||||
async get(attributeName) {
|
||||
return this.prepareModel(await this._get(attributeName));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.models.attributes.AlignmentAttribute = class AlignmentAttribute extends nopaque.corpus_analysis.cqi.models.attributes.Attribute {
|
||||
/**
|
||||
* @param {number} id
|
||||
* @returns {Promise<[number, number, number, number]>}
|
||||
*/
|
||||
async cposById(id) {
|
||||
return await this.client.api.cl_alg2cpos(this.apiName, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} cposList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async idsByCpos(cposList) {
|
||||
return await this.client.api.cl_cpos2alg(this.apiName, cposList);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.models.attributes.AlignmentAttributeCollection = class AlignmentAttributeCollection extends nopaque.corpus_analysis.cqi.models.attributes.AttributeCollection {
|
||||
/** @type{typeof nopaque.corpus_analysis.cqi.models.attributes.AlignmentAttribute} */
|
||||
static model = nopaque.corpus_analysis.cqi.models.attributes.AlignmentAttribute;
|
||||
|
||||
/**
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.models.attributes.AlignmentAttribute[]>}
|
||||
*/
|
||||
async list() {
|
||||
/** @type {string[]} */
|
||||
let alignmentAttributeNames = await this.client.api.corpus_alignment_attributes(this.corpus.apiName);
|
||||
/** @type {nopaque.corpus_analysis.cqi.models.attributes.AlignmentAttribute[]} */
|
||||
let alignmentAttributes = [];
|
||||
for (let alignmentAttributeName of alignmentAttributeNames) {
|
||||
alignmentAttributes.push(await this.get(alignmentAttributeName));
|
||||
}
|
||||
return alignmentAttributes;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.models.attributes.PositionalAttribute = class PositionalAttribute extends nopaque.corpus_analysis.cqi.models.attributes.Attribute {
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
get lexiconSize() {
|
||||
return this.attrs.lexicon_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cposById(id) {
|
||||
return await this.client.api.cl_id2cpos(this.apiName, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} idList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async cposByIds(idList) {
|
||||
return await this.client.api.cl_idlist2cpos(this.apiName, idList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} idList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async freqsByIds(idList) {
|
||||
return await this.client.api.cl_id2freq(this.apiName, idList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} cposList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async idsByCpos(cposList) {
|
||||
return await this.client.api.cl_cpos2id(this.apiName, cposList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} regex
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async idsByRegex(regex) {
|
||||
return await this.client.api.cl_regex2id(this.apiName, regex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} valueList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async idsByValues(valueList) {
|
||||
return await this.client.api.cl_str2id(this.apiName, valueList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} cposList
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async valuesByCpos(cposList) {
|
||||
return await this.client.api.cl_cpos2str(this.apiName, cposList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} idList
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async valuesByIds(idList) {
|
||||
return await this.client.api.cl_id2str(this.apiName, idList);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.models.attributes.PositionalAttributeCollection = class PositionalAttributeCollection extends nopaque.corpus_analysis.cqi.models.attributes.AttributeCollection {
|
||||
/** @type{typeof nopaque.corpus_analysis.cqi.models.attributes.PositionalAttribute} */
|
||||
static model = nopaque.corpus_analysis.cqi.models.attributes.PositionalAttribute;
|
||||
|
||||
/**
|
||||
* @param {string} positionalAttributeName
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async _get(positionalAttributeName) {
|
||||
let positionalAttribute = await super._get(positionalAttributeName);
|
||||
positionalAttribute.lexicon_size = await this.client.api.cl_lexicon_size(positionalAttribute.api_name);
|
||||
return positionalAttribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.models.attributes.PositionalAttribute[]>}
|
||||
*/
|
||||
async list() {
|
||||
let positionalAttributeNames = await this.client.api.corpus_positional_attributes(this.corpus.apiName);
|
||||
let positionalAttributes = [];
|
||||
for (let positionalAttributeName of positionalAttributeNames) {
|
||||
positionalAttributes.push(await this.get(positionalAttributeName));
|
||||
}
|
||||
return positionalAttributes;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.models.attributes.StructuralAttribute = class StructuralAttribute extends nopaque.corpus_analysis.cqi.models.attributes.Attribute {
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get hasValues() {
|
||||
return this.attrs.has_values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @returns {Promise<[number, number]>}
|
||||
*/
|
||||
async cposById(id) {
|
||||
return await this.client.api.cl_struc2cpos(this.apiName, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} cposList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async idsByCpos(cposList) {
|
||||
return await this.client.api.cl_cpos2struc(this.apiName, cposList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} cposList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async lboundByCpos(cposList) {
|
||||
return await this.client.api.cl_cpos2lbound(this.apiName, cposList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} cposList
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async rboundByCpos(cposList) {
|
||||
return await this.client.api.cl_cpos2rbound(this.apiName, cposList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} idList
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async valuesByIds(idList) {
|
||||
return await this.client.api.cl_struc2str(this.apiName, idList);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.models.attributes.StructuralAttributeCollection = class StructuralAttributeCollection extends nopaque.corpus_analysis.cqi.models.attributes.AttributeCollection {
|
||||
/** @type{typeof nopaque.corpus_analysis.cqi.models.attributes.StructuralAttribute} */
|
||||
static model = nopaque.corpus_analysis.cqi.models.attributes.StructuralAttribute;
|
||||
|
||||
/**
|
||||
* @param {string} structuralAttributeName
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async _get(structuralAttributeName) {
|
||||
let structuralAttribute = await super._get(structuralAttributeName);
|
||||
structuralAttribute.has_values = await this.client.api.cl_has_values(structuralAttribute.api_name);
|
||||
return structuralAttribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.models.attributes.StructuralAttribute[]>}
|
||||
*/
|
||||
async list() {
|
||||
let structuralAttributeNames = await this.client.api.corpus_structural_attributes(this.corpus.apiName);
|
||||
let structuralAttributes = [];
|
||||
for (let structuralAttributeName of structuralAttributeNames) {
|
||||
structuralAttributes.push(await this.get(structuralAttributeName));
|
||||
}
|
||||
return structuralAttributes;
|
||||
}
|
||||
};
|
166
app/static/js/corpus-analysis/cqi/models/corpora.js
Normal file
166
app/static/js/corpus-analysis/cqi/models/corpora.js
Normal file
@ -0,0 +1,166 @@
|
||||
nopaque.corpus_analysis.cqi.models.corpora = {};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.models.corpora.Corpus = class Corpus extends nopaque.corpus_analysis.cqi.models.resource.Model {
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get apiName() {
|
||||
return this.attrs.api_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get name() {
|
||||
return this.attrs.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
get size() {
|
||||
return this.attrs.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get charset() {
|
||||
return this.attrs.charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
get properties() {
|
||||
return this.attrs?.properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {nopaque.corpus_analysis.cqi.models.attributes.AlignmentAttributeCollection}
|
||||
*/
|
||||
get alignmentAttributes() {
|
||||
return new nopaque.corpus_analysis.cqi.models.attributes.AlignmentAttributeCollection(this.client, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {nopaque.corpus_analysis.cqi.models.attributes.PositionalAttributeCollection}
|
||||
*/
|
||||
get positionalAttributes() {
|
||||
return new nopaque.corpus_analysis.cqi.models.attributes.PositionalAttributeCollection(this.client, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {nopaque.corpus_analysis.cqi.models.attributes.StructuralAttributeCollection}
|
||||
*/
|
||||
get structuralAttributes() {
|
||||
return new nopaque.corpus_analysis.cqi.models.attributes.StructuralAttributeCollection(this.client, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {nopaque.corpus_analysis.cqi.models.subcorpora.SubcorpusCollection}
|
||||
*/
|
||||
get subcorpora() {
|
||||
return new nopaque.corpus_analysis.cqi.models.subcorpora.SubcorpusCollection(this.client, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusOk>}
|
||||
*/
|
||||
async drop() {
|
||||
return await this.client.api.corpus_drop_corpus(this.apiName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} subcorpusName
|
||||
* @param {string} query
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusOk>}
|
||||
*/
|
||||
async query(subcorpusName, query) {
|
||||
return await this.client.api.cqp_query(this.apiName, subcorpusName, query);
|
||||
}
|
||||
|
||||
/**************************************************************************
|
||||
* NOTE: The following is not included in the CQi specification. *
|
||||
**************************************************************************/
|
||||
/**************************************************************************
|
||||
* Custom additions for nopaque *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get staticData() {
|
||||
return this.attrs.static_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {nopaque.corpus_analysis.cqi.status.StatusOk}
|
||||
*/
|
||||
async updateDb() {
|
||||
return await this.client.api.ext_corpus_update_db(this.apiName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number=} page
|
||||
* @param {number=} per_page
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async paginate(page, per_page) {
|
||||
return await this.client.api.ext_corpus_paginate_corpus(this.apiName, page, per_page);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.models.corpora.CorpusCollection = class CorpusCollection extends nopaque.corpus_analysis.cqi.models.resource.Collection {
|
||||
/** @type {typeof nopaque.corpus_analysis.cqi.models.corpora.Corpus} */
|
||||
static model = nopaque.corpus_analysis.cqi.models.corpora.Corpus;
|
||||
|
||||
/**
|
||||
* @param {string} corpusName
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async _get(corpusName) {
|
||||
const returnValue = {
|
||||
api_name: corpusName,
|
||||
charset: await this.client.api.corpus_charset(corpusName),
|
||||
// full_name: await this.client.api.corpus_full_name(corpusName),
|
||||
// info: await this.client.api.corpus_info(corpusName),
|
||||
name: corpusName,
|
||||
properties: await this.client.api.corpus_properties(corpusName),
|
||||
size: await this.client.api.cl_attribute_size(`${corpusName}.word`)
|
||||
};
|
||||
|
||||
/************************************************************************
|
||||
* NOTE: The following is not included in the CQi specification. *
|
||||
************************************************************************/
|
||||
/************************************************************************
|
||||
* Custom additions for nopaque *
|
||||
************************************************************************/
|
||||
returnValue.static_data = await this.client.api.ext_corpus_static_data(corpusName);
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} corpusName
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.models.corpora.Corpus>}
|
||||
*/
|
||||
async get(corpusName) {
|
||||
return this.prepareModel(await this._get(corpusName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.models.corpora.Corpus[]>}
|
||||
*/
|
||||
async list() {
|
||||
/** @type {string[]} */
|
||||
let corpusNames = await this.client.api.corpus_list_corpora();
|
||||
/** @type {nopaque.corpus_analysis.cqi.models.corpora.Corpus[]} */
|
||||
let corpora = [];
|
||||
for (let corpusName of corpusNames) {
|
||||
corpora.push(await this.get(corpusName));
|
||||
}
|
||||
return corpora;
|
||||
}
|
||||
};
|
1
app/static/js/corpus-analysis/cqi/models/index.js
Normal file
1
app/static/js/corpus-analysis/cqi/models/index.js
Normal file
@ -0,0 +1 @@
|
||||
nopaque.corpus_analysis.cqi.models = {};
|
90
app/static/js/corpus-analysis/cqi/models/resource.js
Normal file
90
app/static/js/corpus-analysis/cqi/models/resource.js
Normal file
@ -0,0 +1,90 @@
|
||||
nopaque.corpus_analysis.cqi.models.resource = {};
|
||||
|
||||
|
||||
/**
|
||||
* A base class for representing a single object on the server.
|
||||
*/
|
||||
nopaque.corpus_analysis.cqi.models.resource.Model = class Model {
|
||||
/**
|
||||
* @param {object} attrs
|
||||
* @param {nopaque.corpus_analysis.cqi.CQiClient} client
|
||||
* @param {nopaque.corpus_analysis.cqi.models.resource.Collection} collection
|
||||
*/
|
||||
constructor(attrs, client, collection) {
|
||||
/**
|
||||
* A client pointing at the server that this object is on.
|
||||
*
|
||||
* @type {nopaque.corpus_analysis.cqi.CQiClient}
|
||||
*/
|
||||
this.client = client;
|
||||
/**
|
||||
* The collection that this model is part of.
|
||||
*
|
||||
* @type {nopaque.corpus_analysis.cqi.models.resource.Collection}
|
||||
*/
|
||||
this.collection = collection;
|
||||
/**
|
||||
* The raw representation of this object from the API
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
this.attrs = attrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get apiName() {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async reload() {
|
||||
this.attrs = await this.collection.get(this.apiName).attrs;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A base class for representing all objects of a particular type on the server.
|
||||
*/
|
||||
nopaque.corpus_analysis.cqi.models.resource.Collection = class Collection {
|
||||
/**
|
||||
* The type of object this collection represents, set by subclasses
|
||||
*
|
||||
* @type {typeof nopaque.corpus_analysis.cqi.models.resource.Model}
|
||||
*/
|
||||
static model;
|
||||
|
||||
/**
|
||||
* @param {nopaque.corpus_analysis.cqi.CQiClient} client
|
||||
*/
|
||||
constructor(client) {
|
||||
/**
|
||||
* A client pointing at the server that this object is on.
|
||||
*
|
||||
* @type {nopaque.corpus_analysis.cqi.CQiClient}
|
||||
*/
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async list() {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async get() {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a model from a set of attributes.
|
||||
*
|
||||
* @param {object} attrs
|
||||
* @returns {nopaque.corpus_analysis.cqi.models.resource.Model}
|
||||
*/
|
||||
prepareModel(attrs) {
|
||||
return new this.constructor.model(attrs, this.client, this);
|
||||
}
|
||||
};
|
189
app/static/js/corpus-analysis/cqi/models/subcorpora.js
Normal file
189
app/static/js/corpus-analysis/cqi/models/subcorpora.js
Normal file
@ -0,0 +1,189 @@
|
||||
nopaque.corpus_analysis.cqi.models.subcorpora = {};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.models.subcorpora.Subcorpus = class Subcorpus extends nopaque.corpus_analysis.cqi.models.resource.Model {
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get apiName() {
|
||||
return this.attrs.api_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object}
|
||||
*/
|
||||
get fields() {
|
||||
return this.attrs.fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
get name() {
|
||||
return this.attrs.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
get size() {
|
||||
return this.attrs.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.status.StatusOk>}
|
||||
*/
|
||||
async drop() {
|
||||
return await this.client.api.cqp_drop_subcorpus(this.apiName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} field
|
||||
* @param {number} first
|
||||
* @param {number} last
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async dump(field, first, last) {
|
||||
return await this.client.api.cqp_dump_subcorpus(
|
||||
this.apiName,
|
||||
field,
|
||||
first,
|
||||
last
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} cutoff
|
||||
* @param {number} field
|
||||
* @param {nopaque.corpus_analysis.cqi.models.attributes.PositionalAttribute} attribute
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async fdist1(cutoff, field, attribute) {
|
||||
return await this.client.api.cqp_fdist_1(
|
||||
this.apiName,
|
||||
cutoff,
|
||||
field,
|
||||
attribute.apiName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} cutoff
|
||||
* @param {number} field1
|
||||
* @param {nopaque.corpus_analysis.cqi.models.attributes.PositionalAttribute} attribute1
|
||||
* @param {number} field2
|
||||
* @param {nopaque.corpus_analysis.cqi.models.attributes.PositionalAttribute} attribute2
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
async fdist2(cutoff, field1, attribute1, field2, attribute2) {
|
||||
return await this.client.api.cqp_fdist_2(
|
||||
this.apiName,
|
||||
cutoff,
|
||||
field1,
|
||||
attribute1.apiName,
|
||||
field2,
|
||||
attribute2.apiName
|
||||
);
|
||||
}
|
||||
|
||||
/**************************************************************************
|
||||
* NOTE: The following is not included in the CQi specification. *
|
||||
**************************************************************************/
|
||||
/**************************************************************************
|
||||
* Custom additions for nopaque *
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* @param {number=} context
|
||||
* @param {number=} page
|
||||
* @param {number=} perPage
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async paginate(context, page, perPage) {
|
||||
return await this.client.api.ext_cqp_paginate_subcorpus(this.apiName, context, page, perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} matchIdList
|
||||
* @param {number=} context
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async partialExport(matchIdList, context) {
|
||||
return await this.client.api.ext_cqp_partial_export_subcorpus(this.apiName, matchIdList, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number=} context
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async export(context) {
|
||||
return await this.client.api.ext_cqp_export_subcorpus(this.apiName, context);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.models.subcorpora.SubcorpusCollection = class SubcorpusCollection extends nopaque.corpus_analysis.cqi.models.resource.Collection {
|
||||
/** @type {typeof nopaque.corpus_analysis.cqi.models.subcorpora.Subcorpus} */
|
||||
static model = nopaque.corpus_analysis.cqi.models.subcorpora.Subcorpus;
|
||||
|
||||
/**
|
||||
* @param {nopaque.corpus_analysis.cqi.CQiClient} client
|
||||
* @param {nopaque.corpus_analysis.cqi.models.corpora.Corpus} corpus
|
||||
*/
|
||||
constructor(client, corpus) {
|
||||
super(client);
|
||||
/** @type {nopaque.corpus_analysis.cqi.models.corpora.Corpus} */
|
||||
this.corpus = corpus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} subcorpusName
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async _get(subcorpusName) {
|
||||
/** @type {string} */
|
||||
let apiName = `${this.corpus.apiName}:${subcorpusName}`;
|
||||
/** @type {object} */
|
||||
let fields = {};
|
||||
if (await this.client.api.cqp_subcorpus_has_field(apiName, nopaque.corpus_analysis.cqi.constants.FIELD_MATCH)) {
|
||||
fields.match = nopaque.corpus_analysis.cqi.constants.FIELD_MATCH;
|
||||
}
|
||||
if (await this.client.api.cqp_subcorpus_has_field(apiName, nopaque.corpus_analysis.cqi.constants.FIELD_MATCHEND)) {
|
||||
fields.matchend = nopaque.corpus_analysis.cqi.constants.FIELD_MATCHEND
|
||||
}
|
||||
if (await this.client.api.cqp_subcorpus_has_field(apiName, nopaque.corpus_analysis.cqi.constants.FIELD_TARGET)) {
|
||||
fields.target = nopaque.corpus_analysis.cqi.constants.FIELD_TARGET
|
||||
}
|
||||
if (await this.client.api.cqp_subcorpus_has_field(apiName, nopaque.corpus_analysis.cqi.constants.FIELD_KEYWORD)) {
|
||||
fields.keyword = nopaque.corpus_analysis.cqi.constants.FIELD_KEYWORD
|
||||
}
|
||||
return {
|
||||
api_name: apiName,
|
||||
fields: fields,
|
||||
name: subcorpusName,
|
||||
size: await this.client.api.cqp_subcorpus_size(apiName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} subcorpusName
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.models.subcorpora.Subcorpus>}
|
||||
*/
|
||||
async get(subcorpusName) {
|
||||
return this.prepareModel(await this._get(subcorpusName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<nopaque.corpus_analysis.cqi.models.subcorpora.Subcorpus[]>}
|
||||
*/
|
||||
async list() {
|
||||
/** @type {string[]} */
|
||||
let subcorpusNames = await this.client.api.cqp_list_subcorpora(this.corpus.apiName);
|
||||
/** @type {nopaque.corpus_analysis.cqi.models.subcorpora.Subcorpus[]} */
|
||||
let subcorpora = [];
|
||||
for (let subcorpusName of subcorpusNames) {
|
||||
subcorpora.push(await this.get(subcorpusName));
|
||||
}
|
||||
return subcorpora;
|
||||
}
|
||||
};
|
51
app/static/js/corpus-analysis/cqi/status.js
Normal file
51
app/static/js/corpus-analysis/cqi/status.js
Normal file
@ -0,0 +1,51 @@
|
||||
nopaque.corpus_analysis.cqi.status = {};
|
||||
|
||||
|
||||
/**
|
||||
* A base class from which all other status inherit.
|
||||
*/
|
||||
nopaque.corpus_analysis.cqi.status.CQiStatus = class CQiStatus {
|
||||
constructor() {
|
||||
this.code = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.status.StatusOk = class StatusOk extends nopaque.corpus_analysis.cqi.status.CQiStatus {
|
||||
constructor() {
|
||||
super();
|
||||
this.code = 257;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.status.StatusConnectOk = class StatusConnectOk extends nopaque.corpus_analysis.cqi.status.CQiStatus {
|
||||
constructor() {
|
||||
super();
|
||||
this.code = 258;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.status.StatusByeOk = class StatusByeOk extends nopaque.corpus_analysis.cqi.status.CQiStatus {
|
||||
constructor() {
|
||||
super();
|
||||
this.code = 259;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.status.StatusPingOk = class StatusPingOk extends nopaque.corpus_analysis.cqi.status.CQiStatus {
|
||||
constructor() {
|
||||
super();
|
||||
this.code = 260;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
nopaque.corpus_analysis.cqi.status.lookup = {
|
||||
257: nopaque.corpus_analysis.cqi.status.StatusOk,
|
||||
258: nopaque.corpus_analysis.cqi.status.StatusConnectOk,
|
||||
259: nopaque.corpus_analysis.cqi.status.StatusByeOk,
|
||||
260: nopaque.corpus_analysis.cqi.status.StatusPingOk
|
||||
};
|
1
app/static/js/corpus-analysis/index.js
Normal file
1
app/static/js/corpus-analysis/index.js
Normal file
@ -0,0 +1 @@
|
||||
nopaque.corpus_analysis = {};
|
@ -0,0 +1,28 @@
|
||||
class ElementReferencesQueryBuilder {
|
||||
constructor() {
|
||||
// General Elements
|
||||
this.queryInputField = document.querySelector('#corpus-analysis-concordance-query-builder-input-field');
|
||||
this.queryChipElements = [];
|
||||
this.editingModusOn = false;
|
||||
this.editedQueryChipElementIndex = undefined;
|
||||
|
||||
// Structural Attribute Builder Elements
|
||||
this.structuralAttrModal = M.Modal.getInstance(document.querySelector('#corpus-analysis-concordance-structural-attr-modal'));
|
||||
this.englishEntTypeSelection = document.querySelector('#corpus-analysis-concordance-english-ent-type-selection');
|
||||
this.germanEntTypeSelection = document.querySelector('#corpus-analysis-concordance-german-ent-type-selection');
|
||||
this.textAnnotationSelection = document.querySelector('#corpus-analysis-concordance-text-annotation-options');
|
||||
this.textAnnotationInput = document.querySelector('#corpus-analysis-concordance-text-annotation-input');
|
||||
|
||||
// Token Attribute Builder Elements
|
||||
this.positionalAttrModal = M.Modal.getInstance(document.querySelector('#corpus-analysis-concordance-positional-attr-modal'));
|
||||
this.positionalAttrSelection = document.querySelector('#corpus-analysis-concordance-positional-attr-selection');
|
||||
this.tokenBuilderContent = document.querySelector('#corpus-analysis-concordance-token-builder-content');
|
||||
this.tokenQuery = document.querySelector('#corpus-analysis-concordance-token-query');
|
||||
this.tokenQueryTemplate = document.querySelector('#corpus-analysis-concordance-token-query-template');
|
||||
this.tokenSubmitButton = document.querySelector('#corpus-analysis-concordance-token-submit');
|
||||
this.noValueMessage = document.querySelector('#corpus-analysis-concordance-no-value-message');
|
||||
this.isTokenQueryInvalid = false;
|
||||
|
||||
this.ignoreCaseCheckbox = document.querySelector('#corpus-analysis-concordance-ignore-case-checkbox');
|
||||
}
|
||||
}
|
@ -0,0 +1,486 @@
|
||||
class GeneralQueryBuilderFunctions {
|
||||
name = 'General Query Builder Functions';
|
||||
|
||||
constructor(app, elements) {
|
||||
this.app = app;
|
||||
this.elements = elements;
|
||||
|
||||
this.incidenceModifierEventListeners();
|
||||
this.nAndMInputSubmitEventListeners();
|
||||
|
||||
let queryBuilderDisplay = document.querySelector("#corpus-analysis-concordance-query-builder-display");
|
||||
let expertModeDisplay = document.querySelector("#corpus-analysis-concordance-expert-mode-display");
|
||||
let expertModeSwitch = document.querySelector("#corpus-analysis-concordance-expert-mode-switch");
|
||||
|
||||
expertModeSwitch.addEventListener("change", () => {
|
||||
const isChecked = expertModeSwitch.checked;
|
||||
if (isChecked) {
|
||||
queryBuilderDisplay.classList.add("hide");
|
||||
expertModeDisplay.classList.remove("hide");
|
||||
this.switchToExpertModeParser();
|
||||
} else {
|
||||
queryBuilderDisplay.classList.remove("hide");
|
||||
expertModeDisplay.classList.add("hide");
|
||||
this.switchToQueryBuilderParser();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleClass(elements, className, action){
|
||||
elements.forEach(element => {
|
||||
document.querySelector(`[data-toggle-area="${element}"]`).classList[action](className);
|
||||
});
|
||||
}
|
||||
|
||||
resetQueryInputField() {
|
||||
this.elements.queryInputField.innerHTML = '';
|
||||
this.addPlaceholder();
|
||||
this.updateChipList();
|
||||
this.queryPreviewBuilder();
|
||||
}
|
||||
|
||||
updateChipList() {
|
||||
this.elements.queryChipElements = this.elements.queryInputField.querySelectorAll('.query-component');
|
||||
}
|
||||
|
||||
removePlaceholder() {
|
||||
let placeholder = this.elements.queryInputField.querySelector('#corpus-analysis-concordance-query-builder-input-field-placeholder');
|
||||
if (placeholder && this.elements.queryInputField !== undefined) {
|
||||
this.elements.queryInputField.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
addPlaceholder() {
|
||||
let placeholder = nopaque.Utils.HTMLToElement('<span id="corpus-analysis-concordance-query-builder-input-field-placeholder">Click on a button to add a query component</span>');
|
||||
this.elements.queryInputField.appendChild(placeholder);
|
||||
}
|
||||
|
||||
resetMaterializeSelection(selectionElements, value = "default") {
|
||||
selectionElements.forEach(selectionElement => {
|
||||
if (selectionElement.querySelector(`option[value=${value}]`) !== null) {
|
||||
selectionElement.querySelector(`option[value=${value}]`).selected = true;
|
||||
}
|
||||
let instance = M.FormSelect.getInstance(selectionElement);
|
||||
instance.destroy();
|
||||
M.FormSelect.init(selectionElement);
|
||||
})
|
||||
}
|
||||
|
||||
submitQueryChipElement(dataType = undefined, prettyQueryText = undefined, queryText = undefined, index = null, isClosingTag = false, isEditable = false) {
|
||||
if (this.elements.editingModusOn) {
|
||||
let editedQueryChipElement = this.elements.queryChipElements[this.elements.editedQueryChipElementIndex];
|
||||
editedQueryChipElement.dataset.type = dataType;
|
||||
editedQueryChipElement.dataset.query = queryText;
|
||||
editedQueryChipElement.firstChild.textContent = prettyQueryText;
|
||||
this.updateChipList();
|
||||
this.queryPreviewBuilder();
|
||||
} else {
|
||||
this.queryChipFactory(dataType, prettyQueryText, queryText, index, isClosingTag, isEditable);
|
||||
}
|
||||
}
|
||||
|
||||
queryChipFactory(dataType, prettyQueryText, queryText, index = null, isClosingTag = false, isEditable = false) {
|
||||
// Creates a new query chip element, adds Eventlisteners for selection, deletion and drag and drop and appends it to the query input field.
|
||||
queryText = nopaque.Utils.escape(queryText);
|
||||
prettyQueryText = nopaque.Utils.escape(prettyQueryText);
|
||||
let queryChipElement = nopaque.Utils.HTMLToElement(
|
||||
`
|
||||
<span class="chip query-component" data-type="${dataType}" data-query="${queryText}" draggable="true" data-closing-tag="${isClosingTag}">
|
||||
${prettyQueryText}${isEditable ? '<i class="material-icons chip-action-button" data-chip-action="edit" style="padding-left:5px; font-size:18px; cursor:pointer;">edit</i>': ''}
|
||||
${isClosingTag ? '<i class="material-icons chip-action-button" data-chip-action="lock" style="padding-top:5px; font-size:20px; cursor:pointer;">lock_open</i>' : '<i class="material-icons close chip-action-button" data-chip-action="delete">close</i>'}
|
||||
</span>
|
||||
`
|
||||
);
|
||||
this.actionListeners(queryChipElement);
|
||||
queryChipElement.addEventListener('dragstart', this.handleDragStart.bind(this, queryChipElement));
|
||||
queryChipElement.addEventListener('dragend', this.handleDragEnd);
|
||||
|
||||
// Ensures that metadata is always at the end of the query and if an index is given, inserts the query chip at the given index and if there is a closing tag, inserts the query chip before the closing tag.
|
||||
this.removePlaceholder();
|
||||
let lastChild = this.elements.queryInputField.lastChild;
|
||||
let isLastChildTextAnnotation = lastChild && lastChild.dataset.type === 'text-annotation';
|
||||
if (!index) {
|
||||
let closingTagElement = this.elements.queryInputField.querySelector('[data-closing-tag="true"]');
|
||||
if (closingTagElement) {
|
||||
index = Array.from(this.elements.queryInputField.children).indexOf(closingTagElement);
|
||||
}
|
||||
}
|
||||
if (dataType !== 'text-annotation' && index) {
|
||||
this.elements.queryInputField.insertBefore(queryChipElement, this.elements.queryChipElements[index]);
|
||||
} else if (dataType !== 'text-annotation' && isLastChildTextAnnotation) {
|
||||
this.elements.queryInputField.insertBefore(queryChipElement, lastChild);
|
||||
} else {
|
||||
this.elements.queryInputField.appendChild(queryChipElement);
|
||||
}
|
||||
|
||||
this.updateChipList();
|
||||
this.queryPreviewBuilder();
|
||||
}
|
||||
|
||||
actionListeners(queryChipElement) {
|
||||
let notQuantifiableDataTypes = ['start-sentence', 'end-sentence', 'start-entity', 'start-empty-entity', 'end-entity', 'token-incidence-modifier'];
|
||||
queryChipElement.addEventListener('click', (event) => {
|
||||
if (event.target.classList.contains('chip')) {
|
||||
if (!notQuantifiableDataTypes.includes(queryChipElement.dataset.type)) {
|
||||
this.selectChipElement(queryChipElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
let chipActionButtons = queryChipElement.querySelectorAll('.chip-action-button');
|
||||
// chipActionButtons.forEach(button => {
|
||||
for (let button of chipActionButtons) {
|
||||
button.addEventListener('click', (event) => {
|
||||
if (event.target.dataset.chipAction === 'delete') {
|
||||
this.deleteChipElement(queryChipElement);
|
||||
} else if (event.target.dataset.chipAction === 'edit') {
|
||||
this.editChipElement(queryChipElement);
|
||||
} else if (event.target.dataset.chipAction === 'lock') {
|
||||
this.lockClosingChipElement(queryChipElement);
|
||||
}
|
||||
});
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
editChipElement(queryChipElement) {
|
||||
this.elements.editingModusOn = true;
|
||||
this.elements.editedQueryChipElementIndex = Array.from(this.elements.queryInputField.children).indexOf(queryChipElement);
|
||||
switch (queryChipElement.dataset.type) {
|
||||
case 'start-entity':
|
||||
this.app.extensions.structuralAttributeBuilderFunctions.editStartEntityChipElement(queryChipElement);
|
||||
break;
|
||||
case 'text-annotation':
|
||||
this.app.extensions.structuralAttributeBuilderFunctions.editTextAnnotationChipElement(queryChipElement);
|
||||
break;
|
||||
case 'token':
|
||||
let queryElementsContent = this.app.extensions.tokenAttributeBuilderFunctions.prepareTokenQueryElementsContent(queryChipElement);
|
||||
this.app.extensions.tokenAttributeBuilderFunctions.editTokenChipElement(queryElementsContent);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lockClosingChipElement(queryChipElement) {
|
||||
queryChipElement.dataset.closingTag = 'false';
|
||||
let lockIcon = queryChipElement.querySelector('[data-chip-action="lock"]');
|
||||
lockIcon.textContent = 'lock';
|
||||
//TODO: Write unlock-Function?
|
||||
lockIcon.dataset.chipAction = 'unlock';
|
||||
}
|
||||
|
||||
deleteChipElement(attr) {
|
||||
let elementIndex = Array.from(this.elements.queryInputField.children).indexOf(attr);
|
||||
switch (attr.dataset.type) {
|
||||
case 'start-sentence':
|
||||
this.deletingClosingTagHandler(elementIndex, 'end-sentence');
|
||||
break;
|
||||
case 'start-entity':
|
||||
this.deletingClosingTagHandler(elementIndex, 'end-entity');
|
||||
break;
|
||||
case 'token':
|
||||
let nextElement = Array.from(this.elements.queryInputField.children)[elementIndex+1];
|
||||
if (nextElement !== undefined && nextElement.dataset.type === 'token-incidence-modifier') {
|
||||
this.deleteChipElement(nextElement);
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this.elements.queryInputField.removeChild(attr);
|
||||
if (this.elements.queryInputField.children.length === 0) {
|
||||
this.addPlaceholder();
|
||||
}
|
||||
this.updateChipList();
|
||||
this.queryPreviewBuilder();
|
||||
}
|
||||
|
||||
deletingClosingTagHandler(elementIndex, closingTagType) {
|
||||
let closingTags = this.elements.queryInputField.querySelectorAll(`[data-type="${closingTagType}"]`);
|
||||
for (let i = 0; i < closingTags.length; i++) {
|
||||
let closingTag = closingTags[i];
|
||||
|
||||
if (Array.from(this.elements.queryInputField.children).indexOf(closingTag) > elementIndex) {
|
||||
this.deleteChipElement(closingTag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleDragStart(queryChipElement, event) {
|
||||
// is called when a query chip is dragged. It creates a dropzone (in form of a chip) for the dragged chip and adds it to the query input field.
|
||||
let queryChips = this.elements.queryInputField.querySelectorAll('.query-component');
|
||||
if (queryChipElement.dataset.type === 'token-incidence-modifier') {
|
||||
queryChips = this.elements.queryInputField.querySelectorAll('.query-component[data-type="token"]');
|
||||
}
|
||||
setTimeout(() => {
|
||||
let targetChipElement = nopaque.Utils.HTMLToElement('<span class="chip drop-target">Drop here</span>');
|
||||
for (let element of queryChips) {
|
||||
if (element === this.elements.queryInputField.querySelectorAll('.query-component')[0]) {
|
||||
let secondTargetChipClone = targetChipElement.cloneNode(true);
|
||||
element.insertAdjacentElement('beforebegin', secondTargetChipClone);
|
||||
this.addDragDropListeners(secondTargetChipClone, queryChipElement);
|
||||
}
|
||||
if (element === queryChipElement || element.nextSibling === queryChipElement) {continue;}
|
||||
|
||||
let targetChipClone = targetChipElement.cloneNode(true);
|
||||
element.insertAdjacentElement('afterend', targetChipClone);
|
||||
|
||||
this.addDragDropListeners(targetChipClone, queryChipElement);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
handleDragEnd(event) {
|
||||
document.querySelectorAll('.drop-target').forEach(target => target.remove());
|
||||
}
|
||||
|
||||
addDragDropListeners(targetChipClone, queryChipElement) {
|
||||
targetChipClone.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
targetChipClone.addEventListener('dragenter', (event) => {
|
||||
event.preventDefault();
|
||||
event.target.style.borderStyle = 'solid dotted';
|
||||
});
|
||||
targetChipClone.addEventListener('dragleave', (event) => {
|
||||
event.preventDefault();
|
||||
event.target.style.borderStyle = 'hidden';
|
||||
});
|
||||
targetChipClone.addEventListener('drop', (event) => {
|
||||
let dropzone = event.target;
|
||||
dropzone.parentElement.replaceChild(queryChipElement, dropzone);
|
||||
this.updateChipList();
|
||||
this.queryPreviewBuilder();
|
||||
});
|
||||
}
|
||||
|
||||
queryPreviewBuilder() {
|
||||
// Builds the query preview in the form of pure CQL and displays it in the query preview field.
|
||||
let queryPreview = document.querySelector('#corpus-analysis-concordance-query-preview');
|
||||
let queryInputFieldContent = [];
|
||||
this.elements.queryChipElements.forEach(element => {
|
||||
let queryElement = element.dataset.query;
|
||||
if (queryElement !== undefined) {
|
||||
queryElement = nopaque.Utils.escape(queryElement);
|
||||
}
|
||||
queryInputFieldContent.push(queryElement);
|
||||
});
|
||||
|
||||
let queryString = queryInputFieldContent.join(' ');
|
||||
let replacements = {
|
||||
' +': '+',
|
||||
' *': '*',
|
||||
' ?': '?',
|
||||
' {': '{'
|
||||
};
|
||||
|
||||
for (let key in replacements) {
|
||||
queryString = queryString.replace(key, replacements[key]);
|
||||
}
|
||||
queryString += ';';
|
||||
|
||||
queryPreview.innerHTML = queryString;
|
||||
queryPreview.parentNode.classList.toggle('hide', queryString === ';');
|
||||
}
|
||||
|
||||
selectChipElement(attr) {
|
||||
document.querySelectorAll('.chip.teal').forEach(element => {
|
||||
if (element !== attr) {
|
||||
element.classList.remove('teal', 'lighten-2');
|
||||
this.toggleClass(['token-incidence-modifiers'], 'disabled', 'add');
|
||||
}
|
||||
});
|
||||
|
||||
this.toggleClass(['token-incidence-modifiers'], 'disabled', 'toggle');
|
||||
attr.classList.toggle('teal');
|
||||
attr.classList.toggle('lighten-5');
|
||||
}
|
||||
|
||||
tokenIncidenceModifierHandler(incidenceModifier, incidenceModifierPretty) {
|
||||
// Adds a token incidence modifier to the query input field.
|
||||
let selectedChip = this.elements.queryInputField.querySelector('.chip.teal');
|
||||
let selectedChipIndex = Array.from(this.elements.queryInputField.children).indexOf(selectedChip);
|
||||
this.submitQueryChipElement('token-incidence-modifier', incidenceModifierPretty, incidenceModifier, selectedChipIndex+1);
|
||||
this.selectChipElement(selectedChip);
|
||||
}
|
||||
|
||||
tokenNMSubmitHandler(modalId) {
|
||||
// Adds a token incidence modifier (exactly n or between n and m) to the query input field.
|
||||
let modal = document.querySelector(`#${modalId}`);
|
||||
let input_n = modal.querySelector('.n-m-input[data-value-type="n"]').value;
|
||||
let input_m = modal.querySelector('.n-m-input[data-value-type="m"]') || undefined;
|
||||
input_m = input_m !== undefined ? input_m.value : '';
|
||||
let input = `{${input_n}${input_m !== '' ? ',' : ''}${input_m}}`;
|
||||
let pretty_input = `between ${input_n} and ${input_m} (${input})`;
|
||||
if (input_m === '') {
|
||||
pretty_input = `exactly ${input_n} (${input})`;
|
||||
}
|
||||
|
||||
let instance = M.Modal.getInstance(modal);
|
||||
instance.close();
|
||||
|
||||
this.tokenIncidenceModifierHandler(input, pretty_input);
|
||||
}
|
||||
|
||||
incidenceModifierEventListeners() {
|
||||
// Eventlisteners for the incidence modifiers. There are two different types of incidence modifiers: token and character incidence modifiers.
|
||||
document.querySelectorAll('.incidence-modifier-selection').forEach(button => {
|
||||
let dropdownId = button.parentNode.parentNode.id;
|
||||
if (dropdownId === 'corpus-analysis-concordance-token-incidence-modifiers-dropdown') {
|
||||
button.addEventListener('click', () => this.tokenIncidenceModifierHandler(button.dataset.token, button.innerHTML));
|
||||
} else if (dropdownId === 'corpus-analysis-concordance-character-incidence-modifiers-dropdown') {
|
||||
button.addEventListener('click', () => this.characterIncidenceModifierHandler(button));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
nAndMInputSubmitEventListeners() {
|
||||
// Eventlisteners for the submit of n- and m-values of the incidence modifier modal for "exactly n" or "between n and m".
|
||||
document.querySelectorAll('.n-m-submit-button').forEach(button => {
|
||||
let modalId = button.dataset.modalId;
|
||||
if (modalId === 'corpus-analysis-concordance-exactly-n-token-modal' || modalId === 'corpus-analysis-concordance-between-nm-token-modal') {
|
||||
button.addEventListener('click', () => this.tokenNMSubmitHandler(modalId));
|
||||
} else if (modalId === 'corpus-analysis-concordance-exactly-n-character-modal' || modalId === 'corpus-analysis-concordance-between-nm-character-modal') {
|
||||
button.addEventListener('click', () => this.app.extensions.tokenAttributeBuilderFunctions.characterNMSubmitHandler(modalId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
switchToExpertModeParser() {
|
||||
let expertModeInputField = document.querySelector('#corpus-analysis-concordance-form-query');
|
||||
expertModeInputField.value = '';
|
||||
let queryBuilderInputFieldValue = nopaque.Utils.unescape(document.querySelector('#corpus-analysis-concordance-query-preview').innerHTML.trim());
|
||||
if (queryBuilderInputFieldValue !== "" && queryBuilderInputFieldValue !== ";") {
|
||||
expertModeInputField.value = queryBuilderInputFieldValue;
|
||||
}
|
||||
}
|
||||
|
||||
switchToQueryBuilderParser() {
|
||||
this.resetQueryInputField();
|
||||
let expertModeInputFieldValue = document.querySelector('#corpus-analysis-concordance-form-query').value;
|
||||
let chipElements = this.parseTextToChip(expertModeInputFieldValue);
|
||||
let closingTagElements = ['end-sentence', 'end-entity'];
|
||||
let editableElements = ['start-entity', 'text-annotation', 'token'];
|
||||
for (let chipElement of chipElements) {
|
||||
let isClosingTag = closingTagElements.includes(chipElement['type']);
|
||||
let isEditable = editableElements.includes(chipElement['type']);
|
||||
if (chipElement['query'] === '[]'){
|
||||
isEditable = false;
|
||||
}
|
||||
this.submitQueryChipElement(chipElement['type'], chipElement['pretty'], chipElement['query'], null, isClosingTag, isEditable);
|
||||
}
|
||||
}
|
||||
|
||||
parseTextToChip(query) {
|
||||
const parsingElementDict = {
|
||||
'<s>': {
|
||||
pretty: 'Sentence Start',
|
||||
type: 'start-sentence'
|
||||
},
|
||||
'<\/s>': {
|
||||
pretty: 'Sentence End',
|
||||
type: 'end-sentence'
|
||||
},
|
||||
'<ent>': {
|
||||
pretty: 'Entity Start',
|
||||
type: 'start-empty-entity'
|
||||
},
|
||||
'<ent_type="([A-Z]+)">': {
|
||||
pretty: '',
|
||||
type: 'start-entity'
|
||||
},
|
||||
'<\\\/ent(_type)?>': {
|
||||
pretty: 'Entity End',
|
||||
type: 'end-entity'
|
||||
},
|
||||
':: ?match\\.text_[A-Za-z]+="[^"]+"': {
|
||||
pretty: '',
|
||||
type: 'text-annotation'
|
||||
},
|
||||
'\\[(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?((\\&|\\|) ?(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?)*\\]': {
|
||||
pretty: '',
|
||||
type: 'token'
|
||||
},
|
||||
'\\[\\]': {
|
||||
pretty: 'Empty Token',
|
||||
type: 'token'
|
||||
},
|
||||
'(?<!\\[) ?\\+ ?(?![^\\]]\\])': {
|
||||
pretty: ' one or more (+)',
|
||||
type: 'token-incidence-modifier'
|
||||
},
|
||||
'(?<!\\[) ?\\* ?(?![^\\]]\\])': {
|
||||
pretty: 'zero or more (*)',
|
||||
type: 'token-incidence-modifier'
|
||||
},
|
||||
'(?<!\\[) ?\\? ?(?![^\\]]\\])': {
|
||||
pretty: 'zero or one (?)',
|
||||
type: 'token-incidence-modifier'
|
||||
},
|
||||
'(?<!\\[) ?\\{[0-9]+} ?(?![^\\]]\\])': {
|
||||
pretty: '',
|
||||
type: 'token-incidence-modifier'
|
||||
},
|
||||
'(?<!\\[) ?\\{[0-9]+(,[0-9]+)?} ?(?![^\\]]\\])': {
|
||||
pretty: '',
|
||||
type: 'token-incidence-modifier'
|
||||
}
|
||||
}
|
||||
|
||||
let chipElements = [];
|
||||
let regexPattern = Object.keys(parsingElementDict).map(pattern => `(${pattern})`).join('|');
|
||||
const regex = new RegExp(regexPattern, 'gi');
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(query)) !== null) {
|
||||
// this is necessary to avoid infinite loops with zero-width matches
|
||||
if (match.index === regex.lastIndex) {
|
||||
regex.lastIndex++;
|
||||
}
|
||||
let stringElement = match[0];
|
||||
for (let [pattern, chipElement] of Object.entries(parsingElementDict)) {
|
||||
const parsingRegex = new RegExp(pattern, 'gi');
|
||||
if (parsingRegex.exec(stringElement)) {
|
||||
// Creating the pretty text for the chip element
|
||||
let prettyText;
|
||||
switch (pattern) {
|
||||
case '<ent_type="([A-Z]+)">':
|
||||
prettyText = `Entity Type=${stringElement.replace(/<ent_type="|">/g, '')}`;
|
||||
break;
|
||||
case ':: ?match\\.text_[A-Za-z]+="[^"]+"':
|
||||
prettyText = stringElement.replace(/:: ?match\.text_|"|"/g, '');
|
||||
break;
|
||||
case '\\[(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?((\\&|\\|) ?(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?)*\\]':
|
||||
let doubleQuotes = /(word|lemma|pos|simple_pos)="[^"]+"/gi;
|
||||
let singleQuotes = /(word|lemma|pos|simple_pos)='[^']+'/gi;
|
||||
if (doubleQuotes.exec(stringElement)) {
|
||||
prettyText = stringElement.replace(/^\[|\]$|"/g, '');
|
||||
} else if (singleQuotes.exec(stringElement)) {
|
||||
prettyText = stringElement.replace(/^\[|\]$|'/g, '');
|
||||
}
|
||||
prettyText = prettyText.replace(/\&/g, ' and ').replace(/\|/g, ' or ');
|
||||
break;
|
||||
case '(?<!\\[) ?\\{[0-9]+} ?(?![^\\]]\\])':
|
||||
prettyText = `exactly ${stringElement.replace(/{|}/g, '')} (${stringElement})`;
|
||||
break;
|
||||
case '(?<!\\[) ?\\{[0-9]+(,[0-9]+)?} ?(?![^\\]]\\])':
|
||||
prettyText = `between${stringElement.replace(/{|}/g, ' ').replace(',', ' and ')}(${stringElement})`;
|
||||
break;
|
||||
default:
|
||||
prettyText = chipElement.pretty;
|
||||
break;
|
||||
}
|
||||
chipElements.push({
|
||||
type: chipElement.type,
|
||||
pretty: prettyText,
|
||||
query: stringElement
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chipElements;
|
||||
}
|
||||
}
|
||||
|
15
app/static/js/corpus-analysis/query-builder/index.js
Normal file
15
app/static/js/corpus-analysis/query-builder/index.js
Normal file
@ -0,0 +1,15 @@
|
||||
class ConcordanceQueryBuilder {
|
||||
|
||||
constructor() {
|
||||
|
||||
this.elements = new ElementReferencesQueryBuilder();
|
||||
|
||||
this.extensions = {
|
||||
generalFunctions: new GeneralQueryBuilderFunctions(this, this.elements),
|
||||
structuralAttributeBuilderFunctions: new StructuralAttributeBuilderFunctions(this, this.elements),
|
||||
tokenAttributeBuilderFunctions: new TokenAttributeBuilderFunctions(this, this.elements),
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
class StructuralAttributeBuilderFunctions {
|
||||
name = 'Token Attribute Builder Functions';
|
||||
|
||||
constructor(app, elements) {
|
||||
this.app = app;
|
||||
this.elements = elements;
|
||||
|
||||
this.structuralAttrModalEventlisteners();
|
||||
|
||||
document.querySelector('#corpus-analysis-concordance-text-annotation-submit').addEventListener('click', () => this.textAnnotationSubmitHandler());
|
||||
|
||||
this.elements.structuralAttrModal = M.Modal.init(
|
||||
document.querySelector('#corpus-analysis-concordance-structural-attr-modal'),
|
||||
{
|
||||
onCloseStart: () => {
|
||||
this.resetStructuralAttrModal();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
structuralAttrModalEventlisteners() {
|
||||
document.querySelectorAll('[data-structural-attr-modal-action-button]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
this.actionButtonInStrucAttrModalHandler(button.dataset.structuralAttrModalActionButton);
|
||||
});
|
||||
});
|
||||
document.querySelector('.ent-type-selection-action[data-ent-type="any"]').addEventListener('click', () => {
|
||||
this.app.extensions.generalFunctions.submitQueryChipElement('start-empty-entity', 'Entity Start', '<ent>');
|
||||
this.app.extensions.generalFunctions.submitQueryChipElement('end-entity', 'Entity End', '</ent>', null, true);
|
||||
this.elements.structuralAttrModal.close();
|
||||
});
|
||||
document.querySelector('.ent-type-selection-action[data-ent-type="english"]').addEventListener('change', (event) => {
|
||||
this.app.extensions.generalFunctions.submitQueryChipElement('start-entity', `Entity Type=${event.target.value}`, `<ent_type="${event.target.value}">`, null, false, true);
|
||||
if (!this.elements.editingModusOn) {
|
||||
this.app.extensions.generalFunctions.submitQueryChipElement('end-entity', 'Entity End', '</ent_type>', null, true);
|
||||
}
|
||||
this.elements.structuralAttrModal.close();
|
||||
});
|
||||
document.querySelector('.ent-type-selection-action[data-ent-type="german"]').addEventListener('change', (event) => {
|
||||
this.app.extensions.generalFunctions.submitQueryChipElement('start-entity', `Entity Type=${event.target.value}`, `<ent_type="${event.target.value}">`, null, false, true);
|
||||
if (!this.elements.editingModusOn) {
|
||||
this.app.extensions.generalFunctions.submitQueryChipElement('end-entity', 'Entity End', '</ent_type>', null, true);
|
||||
}
|
||||
this.elements.structuralAttrModal.close();
|
||||
});
|
||||
}
|
||||
|
||||
resetStructuralAttrModal() {
|
||||
this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.englishEntTypeSelection, this.elements.germanEntTypeSelection]);
|
||||
this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.textAnnotationSelection], 'address');
|
||||
this.elements.textAnnotationInput.value = '';
|
||||
|
||||
this.app.extensions.generalFunctions.toggleClass(['entity-builder', 'text-annotation-builder'], 'hide', 'add');
|
||||
this.toggleEditingAreaStructuralAttrModal('remove');
|
||||
this.elements.editingModusOn = false;
|
||||
this.elements.editedQueryChipElementIndex = undefined;
|
||||
}
|
||||
|
||||
actionButtonInStrucAttrModalHandler(action) {
|
||||
switch (action) {
|
||||
case 'sentence':
|
||||
this.app.extensions.generalFunctions.submitQueryChipElement('start-sentence', 'Sentence Start', '<s>');
|
||||
this.app.extensions.generalFunctions.submitQueryChipElement('end-sentence', 'Sentence End', '</s>', null, true);
|
||||
this.elements.structuralAttrModal.close();
|
||||
break;
|
||||
case 'entity':
|
||||
this.app.extensions.generalFunctions.toggleClass(['entity-builder'], 'hide', 'toggle');
|
||||
this.app.extensions.generalFunctions.toggleClass(['text-annotation-builder'], 'hide', 'add');
|
||||
break;
|
||||
case 'meta-data':
|
||||
this.app.extensions.generalFunctions.toggleClass(['text-annotation-builder'], 'hide', 'toggle');
|
||||
this.app.extensions.generalFunctions.toggleClass(['entity-builder'], 'hide', 'add');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
toggleEditingAreaStructuralAttrModal(action) {
|
||||
// If the user edits a query chip element, the corresponding editing area is displayed and the other areas are hidden or disabled.
|
||||
this.app.extensions.generalFunctions.toggleClass(['sentence-button', 'entity-button', 'text-annotation-button', 'any-type-entity-button'], 'disabled', action);
|
||||
}
|
||||
|
||||
textAnnotationSubmitHandler() {
|
||||
let noValueMetadataMessage = document.querySelector('#corpus-analysis-concordance-no-value-metadata-message');
|
||||
let textAnnotationSubmit = document.querySelector('#corpus-analysis-concordance-text-annotation-submit');
|
||||
let textAnnotationInput = document.querySelector('#corpus-analysis-concordance-text-annotation-input');
|
||||
let textAnnotationOptions = document.querySelector('#corpus-analysis-concordance-text-annotation-options');
|
||||
|
||||
if (textAnnotationInput.value === '') {
|
||||
textAnnotationSubmit.classList.add('red');
|
||||
noValueMetadataMessage.classList.remove('hide');
|
||||
setTimeout(() => {
|
||||
textAnnotationSubmit.classList.remove('red');
|
||||
}, 500);
|
||||
setTimeout(() => {
|
||||
noValueMetadataMessage.classList.add('hide');
|
||||
}, 3000);
|
||||
} else {
|
||||
let queryText = `:: match.text_${textAnnotationOptions.value}="${textAnnotationInput.value}"`;
|
||||
this.app.extensions.generalFunctions.submitQueryChipElement('text-annotation', `${textAnnotationOptions.value}=${textAnnotationInput.value}`, queryText, null, false, true);
|
||||
this.elements.structuralAttrModal.close();
|
||||
}
|
||||
}
|
||||
|
||||
editStartEntityChipElement(queryChipElement) {
|
||||
this.elements.structuralAttrModal.open();
|
||||
this.app.extensions.generalFunctions.toggleClass(['entity-builder'], 'hide', 'remove');
|
||||
this.toggleEditingAreaStructuralAttrModal('add');
|
||||
let entType = queryChipElement.dataset.query.replace(/<ent_type="|">/g, '');
|
||||
let isEnglishEntType = this.elements.englishEntTypeSelection.querySelector(`option[value=${entType}]`) !== null;
|
||||
let selection = isEnglishEntType ? this.elements.englishEntTypeSelection : this.elements.germanEntTypeSelection;
|
||||
this.app.extensions.generalFunctions.resetMaterializeSelection([selection], entType);
|
||||
}
|
||||
|
||||
editTextAnnotationChipElement(queryChipElement) {
|
||||
this.elements.structuralAttrModal.open();
|
||||
this.app.extensions.generalFunctions.toggleClass(['text-annotation-builder'], 'hide', 'remove');
|
||||
this.toggleEditingAreaStructuralAttrModal('add');
|
||||
let [textAnnotationSelection, textAnnotationContent] = queryChipElement.dataset.query
|
||||
.replace(/:: ?match\.text_|"|"/g, '')
|
||||
.split('=');
|
||||
this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.textAnnotationSelection], textAnnotationSelection);
|
||||
this.elements.textAnnotationInput.value = textAnnotationContent;
|
||||
}
|
||||
}
|
@ -0,0 +1,345 @@
|
||||
class TokenAttributeBuilderFunctions {
|
||||
name = 'Token Attribute Builder Functions';
|
||||
|
||||
constructor(app, elements) {
|
||||
this.app = app;
|
||||
this.elements = elements;
|
||||
|
||||
this.elements.positionalAttrSelection.addEventListener('change', () => {
|
||||
this.preparePositionalAttrModal();
|
||||
});
|
||||
|
||||
// Options for positional attribute selection
|
||||
document.querySelectorAll('.positional-attr-options-action-button[data-options-action]').forEach(button => {
|
||||
button.addEventListener('click', () => {this.actionButtonInOptionSectionHandler(button.dataset.optionsAction);});
|
||||
});
|
||||
|
||||
this.elements.tokenSubmitButton.addEventListener('click', () => {this.addTokenToQuery();});
|
||||
|
||||
this.elements.positionalAttrModal = M.Modal.init(
|
||||
document.querySelector('#corpus-analysis-concordance-positional-attr-modal'),
|
||||
{
|
||||
onOpenStart: () => {
|
||||
this.preparePositionalAttrModal();
|
||||
},
|
||||
onCloseStart: () => {
|
||||
this.resetPositionalAttrModal();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
resetPositionalAttrModal() {
|
||||
let originalSelectionList =
|
||||
`
|
||||
<option value="word" selected>word</option>
|
||||
<option value="lemma" >lemma</option>
|
||||
<option value="english-pos">english pos</option>
|
||||
<option value="german-pos">german pos</option>
|
||||
<option value="simple_pos">simple_pos</option>
|
||||
<option value="empty-token">empty token</option>
|
||||
`;
|
||||
this.elements.positionalAttrSelection.innerHTML = originalSelectionList;
|
||||
this.elements.tokenQuery.innerHTML = '';
|
||||
this.elements.tokenBuilderContent.innerHTML = '';
|
||||
this.app.extensions.generalFunctions.toggleClass(['input-field-options'], 'hide', 'remove');
|
||||
this.app.extensions.generalFunctions.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'add');
|
||||
this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.positionalAttrSelection], "word");
|
||||
this.elements.ignoreCaseCheckbox.checked = false;
|
||||
this.elements.editingModusOn = false;
|
||||
this.elements.editedQueryChipElementIndex = undefined;
|
||||
}
|
||||
|
||||
actionButtonInOptionSectionHandler(elem) {
|
||||
let input = this.tokenInputCheck(this.elements.tokenBuilderContent);
|
||||
switch (elem) {
|
||||
case 'option-group':
|
||||
input.value += '(option1|option2)';
|
||||
let firstIndex = input.value.indexOf('option1');
|
||||
let lastIndex = firstIndex + 'option1'.length;
|
||||
input.focus();
|
||||
input.setSelectionRange(firstIndex, lastIndex);
|
||||
break;
|
||||
case 'wildcard-char':
|
||||
input.value += '.';
|
||||
break;
|
||||
case 'and':
|
||||
this.conditionHandler('and');
|
||||
break;
|
||||
case 'or':
|
||||
this.conditionHandler('or');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this.optionToggleHandler();
|
||||
}
|
||||
|
||||
characterIncidenceModifierHandler(elem) {
|
||||
let input = this.tokenInputCheck(this.elements.tokenBuilderContent);
|
||||
input.value += elem.dataset.token;
|
||||
}
|
||||
|
||||
characterNMSubmitHandler(modalId) {
|
||||
let modal = document.querySelector(`#${modalId}`);
|
||||
let input_n = modal.querySelector('.n-m-input[data-value-type="n"]').value;
|
||||
let input_m = modal.querySelector('.n-m-input[data-value-type="m"]') || undefined;
|
||||
input_m = input_m !== undefined ? ',' + input_m.value : '';
|
||||
let input = `${input_n}${input_m}`;
|
||||
|
||||
let instance = M.Modal.getInstance(modal);
|
||||
instance.close();
|
||||
let tokenInput = this.tokenInputCheck(this.elements.tokenBuilderContent);
|
||||
tokenInput.value += '{' + input + '}';
|
||||
}
|
||||
|
||||
conditionHandler(conditionText) {
|
||||
let tokenQueryTemplateClone = this.elements.tokenQueryTemplate.content.cloneNode(true);
|
||||
tokenQueryTemplateClone.querySelector('.token-query-template-content').appendChild(this.elements.tokenBuilderContent.firstElementChild);
|
||||
let notSelectedButton = tokenQueryTemplateClone.querySelector(`[data-condition-pretty-text]:not([data-condition-pretty-text="${conditionText}"])`);
|
||||
let deleteButton = tokenQueryTemplateClone.querySelector(`[data-token-query-content-action="delete"]`);
|
||||
deleteButton.addEventListener('click', (event) => {
|
||||
this.deleteTokenQueryRow(event.target);
|
||||
});
|
||||
notSelectedButton.parentNode.removeChild(notSelectedButton);
|
||||
this.elements.tokenQuery.appendChild(tokenQueryTemplateClone);
|
||||
|
||||
// Deleting the options which do not make sense in the context of the condition like "word" AND "word". Also sets selection default.
|
||||
let selectionDefault = "word";
|
||||
let optionDeleteList = ['empty-token'];
|
||||
if (conditionText === 'and') {
|
||||
switch (this.elements.positionalAttrSelection.value) {
|
||||
case 'english-pos' || 'german-pos':
|
||||
optionDeleteList.push('english-pos', 'german-pos');
|
||||
break;
|
||||
default:
|
||||
optionDeleteList.push(this.elements.positionalAttrSelection.value);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
let originalSelectionList =
|
||||
`
|
||||
<option value="word" selected>word</option>
|
||||
<option value="lemma" >lemma</option>
|
||||
<option value="english-pos">english pos</option>
|
||||
<option value="german-pos">german pos</option>
|
||||
<option value="simple_pos">simple_pos</option>
|
||||
`;
|
||||
this.elements.positionalAttrSelection.innerHTML = originalSelectionList;
|
||||
M.FormSelect.init(this.elements.positionalAttrSelection);
|
||||
}
|
||||
let lastTokenQueryRow = this.elements.tokenQuery.lastElementChild;
|
||||
if(lastTokenQueryRow.querySelector('[data-kind-of-token="word"]') || lastTokenQueryRow.querySelector('[data-kind-of-token="lemma"]')) {
|
||||
this.appendIgnoreCaseCheckbox(lastTokenQueryRow.querySelector('.token-query-template-content'), this.elements.ignoreCaseCheckbox.checked);
|
||||
}
|
||||
this.elements.ignoreCaseCheckbox.checked = false;
|
||||
this.setTokenSelection(selectionDefault, optionDeleteList);
|
||||
}
|
||||
|
||||
deleteTokenQueryRow(deleteButton) {
|
||||
let deletedRow = deleteButton.closest('.row');
|
||||
let condition = deletedRow.querySelector('[data-condition-pretty-text]').dataset.conditionPrettyText;
|
||||
if (condition === 'and') {
|
||||
let kindOfToken = deletedRow.querySelector('[data-kind-of-token]').dataset.kindOfToken;
|
||||
switch (kindOfToken) {
|
||||
case 'english-pos' || 'german-pos':
|
||||
this.createOptionElementForPosAttrSelection('english-pos');
|
||||
this.createOptionElementForPosAttrSelection('german-pos');
|
||||
break;
|
||||
default:
|
||||
this.createOptionElementForPosAttrSelection(kindOfToken);
|
||||
break;
|
||||
}
|
||||
M.FormSelect.init(this.elements.positionalAttrSelection);
|
||||
}
|
||||
deletedRow.remove();
|
||||
}
|
||||
|
||||
createOptionElementForPosAttrSelection(kindOfToken) {
|
||||
let option = document.createElement('option');
|
||||
option.value = kindOfToken;
|
||||
option.text = kindOfToken;
|
||||
this.elements.positionalAttrSelection.appendChild(option);
|
||||
}
|
||||
|
||||
appendIgnoreCaseCheckbox(parentElement, checked = false) {
|
||||
let ignoreCaseCheckboxClone = document.querySelector('#ignore-case-checkbox-template').content.cloneNode(true);
|
||||
parentElement.appendChild(ignoreCaseCheckboxClone);
|
||||
M.Tooltip.init(parentElement.querySelectorAll('.tooltipped'));
|
||||
if (checked) {
|
||||
parentElement.querySelector('input[type="checkbox"]').checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
setTokenSelection(selection, optionDeleteList) {
|
||||
optionDeleteList.forEach(option => {
|
||||
if (this.elements.positionalAttrSelection.querySelector(`option[value=${option}]`) !== null) {
|
||||
this.elements.positionalAttrSelection.querySelector(`option[value=${option}]`).remove();
|
||||
}
|
||||
});
|
||||
|
||||
this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.positionalAttrSelection], selection);
|
||||
this.preparePositionalAttrModal();
|
||||
}
|
||||
|
||||
preparePositionalAttrModal() {
|
||||
let selection = this.elements.positionalAttrSelection.value;
|
||||
if (selection !== 'empty-token') {
|
||||
let selectionTemplate = document.querySelector(`.token-builder-section[data-token-builder-section="${selection}"]`);
|
||||
let selectionTemplateClone = selectionTemplate.content.cloneNode(true);
|
||||
|
||||
this.elements.tokenBuilderContent.innerHTML = '';
|
||||
this.elements.tokenBuilderContent.appendChild(selectionTemplateClone);
|
||||
if (this.elements.tokenBuilderContent.querySelector('select') !== null) {
|
||||
let selectElement = this.elements.tokenBuilderContent.querySelector('select');
|
||||
M.FormSelect.init(selectElement);
|
||||
selectElement.addEventListener('change', () => {this.optionToggleHandler();});
|
||||
} else {
|
||||
this.elements.tokenBuilderContent.querySelector('input').addEventListener('input', () => {this.optionToggleHandler();});
|
||||
}
|
||||
}
|
||||
this.optionToggleHandler();
|
||||
|
||||
if (selection === 'word' || selection === 'lemma') {
|
||||
this.app.extensions.generalFunctions.toggleClass(['input-field-options'], 'hide', 'remove');
|
||||
} else if (selection === 'empty-token'){
|
||||
this.addTokenToQuery();
|
||||
} else {
|
||||
this.app.extensions.generalFunctions.toggleClass(['input-field-options'], 'hide', 'add');
|
||||
}
|
||||
}
|
||||
|
||||
tokenInputCheck(elem) {
|
||||
return elem.querySelector('select') !== null ? elem.querySelector('select') : elem.querySelector('input');
|
||||
}
|
||||
|
||||
optionToggleHandler() {
|
||||
let input = this.tokenInputCheck(this.elements.tokenBuilderContent);
|
||||
if (input.value === '' && this.elements.editingModusOn === false) {
|
||||
this.app.extensions.generalFunctions.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'add');
|
||||
} else if (this.elements.positionalAttrSelection.querySelectorAll('option').length === 1) {
|
||||
this.app.extensions.generalFunctions.toggleClass(['and'], 'disabled', 'add');
|
||||
this.app.extensions.generalFunctions.toggleClass(['or'], 'disabled', 'remove');
|
||||
} else {
|
||||
this.app.extensions.generalFunctions.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'remove');
|
||||
}
|
||||
}
|
||||
|
||||
addTokenToQuery() {
|
||||
let tokenQueryPrettyText = '';
|
||||
let tokenQueryCQLText = '';
|
||||
let input;
|
||||
let kindOfToken = this.kindOfTokenCheck(this.elements.positionalAttrSelection.value);
|
||||
|
||||
// Takes all rows of the token query (if there is a query concatenation).
|
||||
// Adds their contents to tokenQueryPrettyText and tokenQueryCQLText, which will later be expanded with the current input field.
|
||||
let tokenQueryRows = this.elements.tokenQuery.querySelectorAll('.row');
|
||||
tokenQueryRows.forEach(row => {
|
||||
let ignoreCaseCheckbox = row.querySelector('input[type="checkbox"]');
|
||||
let c = ignoreCaseCheckbox !== null && ignoreCaseCheckbox.checked ? ' %c' : '';
|
||||
let tokenQueryRowInput = this.tokenInputCheck(row.querySelector('.token-query-template-content'));
|
||||
let tokenQueryKindOfToken = this.kindOfTokenCheck(tokenQueryRowInput.closest('.input-field').dataset.kindOfToken);
|
||||
let tokenConditionPrettyText = row.querySelector('[data-condition-pretty-text]').dataset.conditionPrettyText;
|
||||
let tokenConditionCQLText = row.querySelector('[data-condition-cql-text]').dataset.conditionCqlText;
|
||||
tokenQueryPrettyText += `${tokenQueryKindOfToken}=${tokenQueryRowInput.value}${c} ${tokenConditionPrettyText} `;
|
||||
tokenQueryCQLText += `${tokenQueryKindOfToken}="${tokenQueryRowInput.value}"${c} ${tokenConditionCQLText}`;
|
||||
});
|
||||
if (kindOfToken === 'empty-token') {
|
||||
tokenQueryPrettyText += 'empty token';
|
||||
} else {
|
||||
let c = this.elements.ignoreCaseCheckbox.checked ? ' %c' : '';
|
||||
input = this.tokenInputCheck(this.elements.tokenBuilderContent);
|
||||
tokenQueryPrettyText += `${kindOfToken}=${input.value}${c}`;
|
||||
tokenQueryCQLText += `${kindOfToken}="${input.value}"${c}`;
|
||||
}
|
||||
// isTokenQueryInvalid looks if a valid value is passed. If the input fields/dropdowns are empty (isTokenQueryInvalid === true), no token is added.
|
||||
if (this.elements.positionalAttrSelection.value !== 'empty-token' && input.value === '') {
|
||||
this.disableTokenSubmit();
|
||||
} else {
|
||||
tokenQueryCQLText = `[${tokenQueryCQLText}]`;
|
||||
this.app.extensions.generalFunctions.submitQueryChipElement('token', tokenQueryPrettyText, tokenQueryCQLText, null, false, kindOfToken === 'empty-token' ? false : true);
|
||||
this.elements.positionalAttrModal.close();
|
||||
}
|
||||
}
|
||||
|
||||
kindOfTokenCheck(kindOfToken) {
|
||||
return kindOfToken === 'english-pos' || kindOfToken === 'german-pos' ? 'pos' : kindOfToken;
|
||||
}
|
||||
|
||||
disableTokenSubmit() {
|
||||
this.elements.tokenSubmitButton.classList.add('red');
|
||||
this.elements.noValueMessage.classList.remove('hide');
|
||||
setTimeout(() => {
|
||||
this.elements.tokenSubmitButton.classList.remove('red');
|
||||
}, 500);
|
||||
setTimeout(() => {
|
||||
this.elements.noValueMessage.classList.add('hide');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
editTokenChipElement(queryElementsContent) {
|
||||
this.elements.positionalAttrModal.open();
|
||||
queryElementsContent.forEach((queryElement) => {
|
||||
this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.positionalAttrSelection], queryElement.tokenAttr);
|
||||
this.preparePositionalAttrModal();
|
||||
switch (queryElement.tokenAttr) {
|
||||
case 'word':
|
||||
case 'lemma':
|
||||
this.elements.tokenBuilderContent.querySelector('input').value = queryElement.tokenValue;
|
||||
break;
|
||||
case 'english-pos':
|
||||
// English-pos is selected by default. Then it is checked whether the passed token value occurs in the english-pos selection. If not, the selection is reseted and changed to german-pos.
|
||||
let selection = this.elements.tokenBuilderContent.querySelector('select');
|
||||
queryElement.tokenAttr = selection.querySelector(`option[value=${queryElement.tokenValue}]`) ? 'english-pos' : 'german-pos';
|
||||
this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.positionalAttrSelection], queryElement.tokenAttr);
|
||||
this.preparePositionalAttrModal();
|
||||
this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.tokenBuilderContent.querySelector('select')], queryElement.tokenValue);
|
||||
break;
|
||||
case 'simple_pos':
|
||||
this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.tokenBuilderContent.querySelector('select')], queryElement.tokenValue);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (queryElement.ignoreCase) {
|
||||
this.elements.ignoreCaseCheckbox.checked = true;
|
||||
}
|
||||
if (queryElement.condition !== undefined) {
|
||||
this.conditionHandler(queryElement.condition, true);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
prepareTokenQueryElementsContent(queryChipElement) {
|
||||
//this regex searches for word or lemma or pos or simple_pos="any string within single or double quotes" followed by one or no ignore case markers, followed by one or no condition characters.
|
||||
let regex = new RegExp('(word|lemma|pos|simple_pos)=(("[^"]+")|(\\\\u0027[^\\\\u0027]+\\\\u0027)) ?(%c)? ?(\\&|\\|)?', 'gm');
|
||||
let m;
|
||||
let queryElementsContent = [];
|
||||
while ((m = regex.exec(queryChipElement.dataset.query)) !== null) {
|
||||
// this is necessary to avoid infinite loops with zero-width matches
|
||||
if (m.index === regex.lastIndex) {
|
||||
regex.lastIndex++;
|
||||
}
|
||||
let tokenAttr = m[1];
|
||||
// Passes english-pos by default so that the template is added. In editTokenChipElement it is then checked whether it is english-pos or german-pos.
|
||||
if (tokenAttr === 'pos') {
|
||||
tokenAttr = 'english-pos';
|
||||
}
|
||||
let tokenValue = m[2].replace(/"|'/g, '');
|
||||
let ignoreCase = false;
|
||||
let condition = undefined;
|
||||
m.forEach((match) => {
|
||||
if (match === "%c") {
|
||||
ignoreCase = true;
|
||||
} else if (match === "&") {
|
||||
condition = "and";
|
||||
} else if (match === "|") {
|
||||
condition = "or";
|
||||
}
|
||||
});
|
||||
queryElementsContent.push({tokenAttr: tokenAttr, tokenValue: tokenValue, ignoreCase: ignoreCase, condition: condition});
|
||||
}
|
||||
return queryElementsContent;
|
||||
}
|
||||
|
||||
}
|
333
app/static/js/corpus-analysis/reader-extension.js
Normal file
333
app/static/js/corpus-analysis/reader-extension.js
Normal file
@ -0,0 +1,333 @@
|
||||
nopaque.corpus_analysis.ReaderExtension = class ReaderExtension {
|
||||
name = 'Reader';
|
||||
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
|
||||
this.data = {};
|
||||
|
||||
this.elements = {
|
||||
container: document.querySelector(`#corpus-analysis-reader-container`),
|
||||
corpus: document.querySelector(`#corpus-analysis-reader-corpus`),
|
||||
corpusPagination: document.querySelector(`#corpus-analysis-reader-corpus-pagination`),
|
||||
error: document.querySelector(`#corpus-analysis-reader-error`),
|
||||
progress: document.querySelector(`#corpus-analysis-reader-progress`),
|
||||
userInterfaceForm: document.querySelector(`#corpus-analysis-reader-user-interface-form`)
|
||||
};
|
||||
|
||||
this.settings = {
|
||||
perPage: parseInt(this.elements.userInterfaceForm['per-page'].value),
|
||||
textStyle: parseInt(this.elements.userInterfaceForm['text-style'].value),
|
||||
tokenRepresentation: this.elements.userInterfaceForm['token-representation'].value,
|
||||
pagination: {
|
||||
innerWindow: 5,
|
||||
outerWindow: 1
|
||||
}
|
||||
}
|
||||
|
||||
this.app.registerExtension(this);
|
||||
}
|
||||
|
||||
async submitForm() {
|
||||
this.app.disableActionElements();
|
||||
this.elements.error.innerText = '';
|
||||
this.elements.error.classList.add('hide');
|
||||
this.elements.progress.classList.remove('hide');
|
||||
try {
|
||||
const paginatedCorpus = await this.data.corpus.o.paginate(1, this.settings.perPage);
|
||||
this.data.corpus.p = paginatedCorpus;
|
||||
this.renderCorpus();
|
||||
this.renderCorpusPagination();
|
||||
this.elements.progress.classList.add('hide');
|
||||
} catch (error) {
|
||||
let errorString = '';
|
||||
if ('code' in error) {errorString += `[${error.code}] `;}
|
||||
errorString += `${error.constructor.name}`;
|
||||
if ('description' in error) {errorString += `: ${error.description}`;}
|
||||
this.elements.error.innerText = errorString;
|
||||
this.elements.error.classList.remove('hide');
|
||||
app.flash(errorString, 'error');
|
||||
this.elements.progress.classList.add('hide');
|
||||
}
|
||||
this.app.enableActionElements();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Init data
|
||||
this.data.corpus = this.app.data.corpus;
|
||||
// Add event listeners
|
||||
this.elements.userInterfaceForm.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
this.submitForm();
|
||||
});
|
||||
this.elements.userInterfaceForm.addEventListener('change', (event) => {
|
||||
if (event.target === this.elements.userInterfaceForm['per-page']) {
|
||||
this.settings.perPage = parseInt(this.elements.userInterfaceForm['per-page'].value);
|
||||
this.submitForm();
|
||||
}
|
||||
if (event.target === this.elements.userInterfaceForm['text-style']) {
|
||||
this.settings.textStyle = parseInt(this.elements.userInterfaceForm['text-style'].value);
|
||||
this.setTextStyle();
|
||||
}
|
||||
if (event.target === this.elements.userInterfaceForm['token-representation']) {
|
||||
this.settings.tokenRepresentation = this.elements.userInterfaceForm['token-representation'].value;
|
||||
this.setTokenRepresentation();
|
||||
}
|
||||
});
|
||||
// Load initial data
|
||||
await this.submitForm();
|
||||
}
|
||||
|
||||
clearCorpus() {
|
||||
// Destroy with .p-attr elements associated Materialize tooltips
|
||||
let pAttrElements = this.elements.corpus.querySelectorAll('.p-attr.tooltipped');
|
||||
for (let pAttrElement of pAttrElements) {
|
||||
M.Tooltip.getInstance(pAttrElement)?.destroy();
|
||||
}
|
||||
this.elements.corpus.innerHTML = `
|
||||
<p class="show-if-only-child">
|
||||
<span class="card-title"><i class="left material-icons" style="font-size: inherit;">search</i>Nothing here...</span><br>
|
||||
No text available.
|
||||
</p>
|
||||
`.trim();
|
||||
}
|
||||
|
||||
renderCorpus() {
|
||||
this.clearCorpus();
|
||||
let item = this.data.corpus.p.items[0];
|
||||
this.elements.corpus.innerHTML += `
|
||||
<p>${this.cposRange2HTML(item[0], item[item.length - 1])}</p>
|
||||
`.trim();
|
||||
this.setTextStyle();
|
||||
this.setTokenRepresentation();
|
||||
}
|
||||
|
||||
clearCorpusPagination() {
|
||||
this.elements.corpusPagination.innerHTML = '';
|
||||
this.elements.corpusPagination.classList.add('hide');
|
||||
}
|
||||
|
||||
renderCorpusPagination() {
|
||||
this.clearCorpusPagination();
|
||||
if (this.data.corpus.p.pages === 0) {return;}
|
||||
let pageElement;
|
||||
// First page button. Disables first page button if on first page
|
||||
pageElement = nopaque.Utils.HTMLToElement(
|
||||
`
|
||||
<li class="${this.data.corpus.p.page === 1 ? 'disabled' : 'waves-effect'}">
|
||||
<a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.page === 1 ? '' : 'data-target="1"'}>
|
||||
<i class="material-icons">first_page</i>
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
);
|
||||
this.elements.corpusPagination.appendChild(pageElement);
|
||||
// Previous page button. Disables previous page button if on first page
|
||||
pageElement = nopaque.Utils.HTMLToElement(
|
||||
`
|
||||
<li class="${this.data.corpus.p.has_prev ? 'waves-effect' : 'disabled'}">
|
||||
<a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.has_prev ? 'data-target="' + this.data.corpus.p.prev_num + '"' : ''}>
|
||||
<i class="material-icons">chevron_left</i>
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
);
|
||||
this.elements.corpusPagination.appendChild(pageElement);
|
||||
// First page as number. Hides first page button if on first page
|
||||
if (this.data.corpus.p.page > 6) {
|
||||
pageElement = nopaque.Utils.HTMLToElement(
|
||||
`
|
||||
<li class="waves-effect">
|
||||
<a class="corpus-analysis-action pagination-trigger" data-target="1">1</a>
|
||||
</li>
|
||||
`
|
||||
);
|
||||
this.elements.corpusPagination.appendChild(pageElement);
|
||||
pageElement = nopaque.Utils.HTMLToElement("<li style='margin-top: 5px;'>…</li>");
|
||||
this.elements.corpusPagination.appendChild(pageElement);
|
||||
}
|
||||
|
||||
// render page buttons (5 before and 5 after current page)
|
||||
for (let i = this.data.corpus.p.page - this.settings.pagination.innerWindow; i <= this.data.corpus.p.page; i++) {
|
||||
if (i <= 0) {continue;}
|
||||
pageElement = nopaque.Utils.HTMLToElement(
|
||||
`
|
||||
<li class="${i === this.data.corpus.p.page ? 'active' : 'waves-effect'}">
|
||||
<a class="corpus-analysis-action pagination-trigger" ${i === this.data.corpus.p.page ? '' : 'data-target="' + i + '"'}>${i}</a>
|
||||
</li>
|
||||
`
|
||||
);
|
||||
this.elements.corpusPagination.appendChild(pageElement);
|
||||
};
|
||||
for (let i = this.data.corpus.p.page +1; i <= this.data.corpus.p.page + this.settings.pagination.innerWindow; i++) {
|
||||
if (i > this.data.corpus.p.pages) {break;}
|
||||
pageElement = nopaque.Utils.HTMLToElement(
|
||||
`
|
||||
<li class="${i === this.data.corpus.p.page ? 'active' : 'waves-effect'}">
|
||||
<a class="corpus-analysis-action pagination-trigger" ${i === this.data.corpus.p.page ? '' : 'data-target="' + i + '"'}>${i}</a>
|
||||
</li>
|
||||
`
|
||||
);
|
||||
this.elements.corpusPagination.appendChild(pageElement);
|
||||
};
|
||||
// Last page as number. Hides last page button if on last page
|
||||
if (this.data.corpus.p.page < this.data.corpus.p.pages - 6) {
|
||||
pageElement = nopaque.Utils.HTMLToElement("<li style='margin-top: 5px;'>…</li>");
|
||||
this.elements.corpusPagination.appendChild(pageElement);
|
||||
pageElement = nopaque.Utils.HTMLToElement(
|
||||
`
|
||||
<li class="waves-effect">
|
||||
<a class="corpus-analysis-action pagination-trigger" data-target="${this.data.corpus.p.pages}">${this.data.corpus.p.pages}</a>
|
||||
</li>
|
||||
`
|
||||
);
|
||||
this.elements.corpusPagination.appendChild(pageElement);
|
||||
}
|
||||
// Next page button. Disables next page button if on last page
|
||||
pageElement = nopaque.Utils.HTMLToElement(
|
||||
`
|
||||
<li class="${this.data.corpus.p.has_next ? 'waves-effect' : 'disabled'}">
|
||||
<a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.has_next ? 'data-target="' + this.data.corpus.p.next_num + '"' : ''}>
|
||||
<i class="material-icons">chevron_right</i>
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
);
|
||||
this.elements.corpusPagination.appendChild(pageElement);
|
||||
// Last page button. Disables last page button if on last page
|
||||
pageElement = nopaque.Utils.HTMLToElement(
|
||||
`
|
||||
<li class="${this.data.corpus.p.page === this.data.corpus.p.pages ? 'disabled' : 'waves-effect'}">
|
||||
<a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.page === this.data.corpus.p.pages ? '' : 'data-target="' + this.data.corpus.p.pages + '"'}>
|
||||
<i class="material-icons">last_page</i>
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
);
|
||||
this.elements.corpusPagination.appendChild(pageElement);
|
||||
|
||||
for (let paginateTriggerElement of this.elements.corpusPagination.querySelectorAll('.pagination-trigger[data-target]')) {
|
||||
paginateTriggerElement.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
let page = parseInt(paginateTriggerElement.dataset.target);
|
||||
this.page(page);
|
||||
});
|
||||
}
|
||||
this.elements.corpusPagination.classList.remove('hide');
|
||||
}
|
||||
|
||||
cposRange2HTML(firstCpos, lastCpos) {
|
||||
let html = '';
|
||||
for (let cpos = firstCpos; cpos <= lastCpos; cpos++) {
|
||||
let prevPAttr = cpos > firstCpos ? this.data.corpus.p.lookups.cpos_lookup[cpos - 1] : null;
|
||||
let pAttr = this.data.corpus.p.lookups.cpos_lookup[cpos];
|
||||
let nextPAttr = cpos < lastCpos ? this.data.corpus.p.lookups.cpos_lookup[cpos + 1] : null;
|
||||
let isEntityStart = 'ent' in pAttr && pAttr.ent !== prevPAttr?.ent;
|
||||
let isEntityEnd = 'ent' in pAttr && pAttr.ent !== nextPAttr?.ent;
|
||||
// Add a space before pAttr
|
||||
if (cpos !== firstCpos || pAttr.simple_pos !== 'PUNCT') {html += ' ';}
|
||||
// Add entity start
|
||||
if (isEntityStart) {
|
||||
html += `<span class="s-attr" data-cpos="${cpos}" data-id="${pAttr.ent}" data-s-attr-type="ent" data-s-attr-ent-type="${this.data.corpus.p.lookups.ent_lookup[pAttr.ent].type}">`;
|
||||
}
|
||||
// Add pAttr
|
||||
html += `<span class="p-attr" data-cpos="${cpos}"></span>`;
|
||||
// Add entity end
|
||||
if (isEntityEnd) {
|
||||
html += ` <span class="badge black-text hide new white ent-indicator" data-badge-caption="">${this.data.corpus.p.lookups.ent_lookup[pAttr.ent].type}</span>`;
|
||||
html += '</span>';
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
page(pageNum, callback) {
|
||||
if (this.data.corpus.p.page === pageNum && typeof callback === 'function') {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
this.app.disableActionElements();
|
||||
window.scrollTo(top);
|
||||
this.elements.progress.classList.remove('hide');
|
||||
this.data.corpus.o.paginate(pageNum, this.settings.perPage)
|
||||
.then(
|
||||
(paginatedCorpus) => {
|
||||
this.data.corpus.p = paginatedCorpus;
|
||||
this.renderCorpus();
|
||||
this.renderCorpusPagination();
|
||||
this.elements.progress.classList.add('hide');
|
||||
this.app.enableActionElements();
|
||||
if (typeof callback === 'function') {callback();}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
setTextStyle() {
|
||||
if (this.settings.textStyle >= 0) {
|
||||
// Destroy with .p-attr elements associated Materialize tooltips
|
||||
for (let pAttrElement of this.elements.corpus.querySelectorAll('.p-attr.tooltipped')) {
|
||||
M.Tooltip.getInstance(pAttrElement)?.destroy();
|
||||
}
|
||||
// Set basic styling on .p-attr elements
|
||||
for (let pAttrElement of this.elements.corpus.querySelectorAll('.p-attr')) {
|
||||
pAttrElement.setAttribute('class', 'p-attr');
|
||||
}
|
||||
// Set basic styling on .s-attr[data-type="ent"] elements
|
||||
for (let entElement of this.elements.corpus.querySelectorAll('.s-attr[data-s-attr-type="ent"]')) {
|
||||
entElement.querySelector('.ent-indicator').classList.add('hide');
|
||||
entElement.removeAttribute('style');
|
||||
entElement.setAttribute('class', 's-attr');
|
||||
}
|
||||
}
|
||||
if (this.settings.textStyle >= 1) {
|
||||
// Set advanced styling on .s-attr[data-type="ent"] elements
|
||||
for (let entElement of this.elements.corpus.querySelectorAll('.s-attr[data-s-attr-type="ent"]')) {
|
||||
entElement.classList.add('chip');
|
||||
entElement.querySelector('.ent-indicator').classList.remove('hide');
|
||||
}
|
||||
}
|
||||
if (this.settings.textStyle >= 2) {
|
||||
// Set advanced styling on .p-attr elements
|
||||
for (let pAttrElement of this.elements.corpus.querySelectorAll('.p-attr')) {
|
||||
pAttrElement.classList.add('chip', 'hoverable', 'tooltipped');
|
||||
let cpos = pAttrElement.dataset.cpos;
|
||||
let pAttr = this.data.corpus.p.lookups.cpos_lookup[cpos];
|
||||
let positionalPropertiesHTML = `
|
||||
<p class="left-align">
|
||||
<b>Positional properties</b><br>
|
||||
<span>Token: ${cpos}</span>
|
||||
`.trim();
|
||||
let structuralPropertiesHTML = `
|
||||
<p class="left-align">
|
||||
<b>Structural properties</b>
|
||||
`.trim();
|
||||
for (let [property, propertyValue] of Object.entries(pAttr)) {
|
||||
if (['lemma', 'ner', 'pos', 'simple_pos', 'word'].includes(property)) {
|
||||
if (propertyValue === 'None') {continue;}
|
||||
positionalPropertiesHTML += `<br><i class="material-icons" style="font-size: inherit;">subdirectory_arrow_right</i>${property}: ${propertyValue}`;
|
||||
} else {
|
||||
structuralPropertiesHTML += `<br><span>${property}: ${propertyValue}</span>`;
|
||||
if (!(`${property}_lookup` in this.data.corpus.p.lookups)) {continue;}
|
||||
for (let [subproperty, subpropertyValue] of Object.entries(this.data.corpus.p.lookups[`${property}_lookup`][propertyValue])) {
|
||||
if (subpropertyValue === 'NULL') {continue;}
|
||||
structuralPropertiesHTML += `<br><i class="material-icons" style="font-size: inherit;">subdirectory_arrow_right</i>${subproperty}: ${subpropertyValue}`
|
||||
}
|
||||
}
|
||||
}
|
||||
positionalPropertiesHTML += '</p>';
|
||||
structuralPropertiesHTML += '</p>';
|
||||
M.Tooltip.init(
|
||||
pAttrElement,
|
||||
{html: positionalPropertiesHTML + structuralPropertiesHTML}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTokenRepresentation() {
|
||||
for (let pAttrElement of this.elements.corpus.querySelectorAll('.p-attr')) {
|
||||
let pAttr = this.data.corpus.p.lookups.cpos_lookup[pAttrElement.dataset.cpos];
|
||||
pAttrElement.innerText = pAttr[this.settings.tokenRepresentation];
|
||||
}
|
||||
}
|
||||
}
|
446
app/static/js/corpus-analysis/static-visualization-extension.js
Normal file
446
app/static/js/corpus-analysis/static-visualization-extension.js
Normal file
@ -0,0 +1,446 @@
|
||||
nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualizationExtension {
|
||||
name = 'Static Visualization (beta)';
|
||||
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
this.data = {
|
||||
stopwords: undefined,
|
||||
originalStopwords: {},
|
||||
stopwordCache: {},
|
||||
promises: {getStopwords: undefined},
|
||||
tokenSet: new Set()
|
||||
};
|
||||
|
||||
this.app.registerExtension(this);
|
||||
}
|
||||
|
||||
init() {
|
||||
// Init data
|
||||
this.data.corpus = this.app.data.corpus;
|
||||
this.renderGeneralCorpusInfo();
|
||||
this.renderTextInfoList();
|
||||
this.renderTextProportionsGraphic();
|
||||
this.renderTokenList();
|
||||
// this.renderFrequenciesGraphic();
|
||||
|
||||
// Add event listeners
|
||||
let frequenciesStopwordSettingModal = document.querySelector('#frequencies-stopwords-setting-modal');
|
||||
let frequenciesStopwordSettingModalButton = document.querySelector('#frequencies-stopwords-setting-modal-button');
|
||||
frequenciesStopwordSettingModalButton.addEventListener('click', () => {
|
||||
this.data.stopwordCache = structuredClone(this.data.stopwords);
|
||||
this.renderStopwordSettingsModal(this.data.stopwords);
|
||||
M.Modal.init(frequenciesStopwordSettingModal, {dismissible: false});
|
||||
});
|
||||
|
||||
let textProportionsGraphModeButtons = document.querySelectorAll('.text-proportions-graph-mode-button');
|
||||
textProportionsGraphModeButtons.forEach(graphModeButton => {
|
||||
graphModeButton.addEventListener('click', (event) => {
|
||||
textProportionsGraphModeButtons.forEach(btn => {
|
||||
btn.classList.remove('disabled');
|
||||
});
|
||||
event.target.closest('.text-proportions-graph-mode-button').classList.add('disabled');
|
||||
this.renderTextProportionsGraphic();
|
||||
});
|
||||
});
|
||||
|
||||
let frequenciesTokenCategoryDropdownElement = document.querySelector('[data-target="frequencies-token-category-dropdown"]');
|
||||
let frequenciesTokenCategoryDropdownListElement = document.querySelector("#frequencies-token-category-dropdown");
|
||||
frequenciesTokenCategoryDropdownListElement.addEventListener('click', (event) => {
|
||||
frequenciesTokenCategoryDropdownElement.firstChild.textContent = event.target.innerHTML;
|
||||
this.renderTokenList();
|
||||
});
|
||||
|
||||
let frequenciesGraphModeButtons = document.querySelectorAll('.frequencies-graph-mode-button');
|
||||
frequenciesGraphModeButtons.forEach(graphModeButton => {
|
||||
graphModeButton.addEventListener('click', (event) => {
|
||||
frequenciesGraphModeButtons.forEach(btn => {
|
||||
btn.classList.remove('disabled');
|
||||
});
|
||||
event.target.closest('.frequencies-graph-mode-button').classList.add('disabled');
|
||||
this.renderFrequenciesGraphic(this.data.tokenSet);
|
||||
});
|
||||
});
|
||||
|
||||
for (let actionButton of document.querySelectorAll('.frequencies-stopword-setting-modal-action-buttons')) {
|
||||
actionButton.addEventListener('click', (event) => {
|
||||
let action = event.target.closest('.frequencies-stopword-setting-modal-action-buttons').dataset.action;
|
||||
if (action === 'submit') {
|
||||
this.renderTokenList();
|
||||
} else if (action === 'cancel') {
|
||||
this.data.stopwords = structuredClone(this.data.stopwordCache);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getStopwords() {
|
||||
this.data.promises.getStopwords = new Promise((resolve, reject) => {
|
||||
nopaque.requests.corpora.entity.getStopwords()
|
||||
.then((response) => {
|
||||
response.json()
|
||||
.then((json) => {
|
||||
this.data.originalStopwords = structuredClone(json);
|
||||
this.data.stopwords = structuredClone(json);
|
||||
resolve(this.data.stopwords);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
return this.data.promises.getStopwords;
|
||||
}
|
||||
|
||||
renderGeneralCorpusInfo() {
|
||||
let corpusData = this.data.corpus.o.staticData;
|
||||
document.querySelector('.corpus-num-tokens').innerHTML = corpusData.corpus.bounds[1] - corpusData.corpus.bounds[0];
|
||||
document.querySelector('.corpus-num-s').innerHTML = corpusData.s_attrs.s.lexicon.length;
|
||||
document.querySelector('.corpus-num-unique-words').innerHTML = Object.entries(corpusData.corpus.freqs.word).length;
|
||||
document.querySelector('.corpus-num-unique-lemmas').innerHTML = Object.entries(corpusData.corpus.freqs.lemma).length;
|
||||
document.querySelector('.corpus-num-unique-pos').innerHTML = Object.entries(corpusData.corpus.freqs.pos).length;
|
||||
document.querySelector('.corpus-num-unique-simple-pos').innerHTML = Object.entries(corpusData.corpus.freqs.simple_pos).length;
|
||||
}
|
||||
|
||||
renderTextInfoList() {
|
||||
let corpusData = this.data.corpus.o.staticData;
|
||||
let corpusTextInfoListElement = document.querySelector('.corpus-text-info-list');
|
||||
let corpusTextInfoList = new nopaque.resource_lists.CorpusTextInfoList(corpusTextInfoListElement);
|
||||
let texts = corpusData.s_attrs.text.lexicon;
|
||||
let textData = [];
|
||||
for (let i = 0; i < Object.entries(texts).length; i++) {
|
||||
let resource = {
|
||||
title: corpusData.values.s_attrs.text[i].title,
|
||||
publishing_year: corpusData.values.s_attrs.text[i].publishing_year,
|
||||
// num_sentences: corpusData.s_attrs.text.lexicon[i].counts.s,
|
||||
num_tokens: corpusData.s_attrs.text.lexicon[i].bounds[1] - corpusData.s_attrs.text.lexicon[i].bounds[0],
|
||||
num_sentences: corpusData.s_attrs.s.lexicon.filter((s) => {
|
||||
return s.bounds[0] >= corpusData.s_attrs.text.lexicon[i].bounds[0] && s.bounds[1] <= corpusData.s_attrs.text.lexicon[i].bounds[1];
|
||||
}).length,
|
||||
num_unique_words: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.word).length,
|
||||
num_unique_lemmas: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.lemma).length,
|
||||
num_unique_pos: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.pos).length,
|
||||
num_unique_simple_pos: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.simple_pos).length
|
||||
};
|
||||
|
||||
textData.push(resource);
|
||||
}
|
||||
|
||||
corpusTextInfoList.add(textData);
|
||||
|
||||
let textCountChipElement = document.querySelector('.text-count-chip');
|
||||
textCountChipElement.innerHTML = `Text count: ${corpusData.s_attrs.text.lexicon.length}`;
|
||||
}
|
||||
|
||||
renderTextProportionsGraphic() {
|
||||
let corpusData = this.data.corpus.o.staticData;
|
||||
let textProportionsGraphicElement = document.querySelector('#text-proportions-graphic');
|
||||
let texts = Object.entries(corpusData.s_attrs.text.lexicon);
|
||||
let graphtype = document.querySelector('.text-proportions-graph-mode-button.disabled').dataset.graphType;
|
||||
let textProportionsTitleElement = document.querySelector('#text-proportions-title-element');
|
||||
|
||||
if (graphtype === 'bar') {
|
||||
textProportionsTitleElement.innerHTML = 'Bounds';
|
||||
} else if (graphtype === 'pie') {
|
||||
textProportionsTitleElement.innerHTML = 'Proportions';
|
||||
}
|
||||
|
||||
let graphData = this.createTextProportionsGraphData(texts, graphtype);
|
||||
let graphLayout = {
|
||||
barmode: graphtype === 'bar' ? 'relative' : '',
|
||||
type: graphtype,
|
||||
showgrid: false,
|
||||
height: 447,
|
||||
margin: {
|
||||
l: 10,
|
||||
r: 10,
|
||||
b: graphtype === 'bar' ? 80 : 10,
|
||||
t: graphtype === 'bar' ? 80 : 10,
|
||||
},
|
||||
legend: {
|
||||
"orientation": "h",
|
||||
font: {
|
||||
size: 10
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
rangemode: 'nonnegative',
|
||||
autorange: true
|
||||
},
|
||||
yaxis: {
|
||||
autorange: true,
|
||||
showticklabels: false
|
||||
}
|
||||
};
|
||||
let config = {
|
||||
responsive: true,
|
||||
modeBarButtonsToRemove: ['zoom2d', 'select2d', 'lasso2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'],
|
||||
displaylogo: false
|
||||
};
|
||||
|
||||
Plotly.newPlot(textProportionsGraphicElement, graphData, graphLayout, config);
|
||||
}
|
||||
|
||||
createTextProportionsGraphData(texts, graphtype) {
|
||||
let corpusData = this.data.corpus.o.staticData;
|
||||
let graphData = [];
|
||||
switch (graphtype) {
|
||||
case 'bar':
|
||||
for (let text of texts) {
|
||||
let textData = {
|
||||
type: 'bar',
|
||||
orientation: 'h',
|
||||
x: [text[1].bounds[1] - text[1].bounds[0]],
|
||||
y: [0.5],
|
||||
text: [`${text[1].bounds[0]} - ${text[1].bounds[1]}`],
|
||||
name: `${corpusData.values.s_attrs.text[text[0]].title} (${corpusData.values.s_attrs.text[text[0]].publishing_year})`,
|
||||
hovertemplate: `${text[1].bounds[0]} - ${text[1].bounds[1]}`,
|
||||
};
|
||||
graphData.push(textData);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
graphData = [
|
||||
{
|
||||
values: texts.map(text => text[1].bounds[1] - text[1].bounds[0]),
|
||||
labels: texts.map(text => `${corpusData.values.s_attrs.text[text[0]].title} (${corpusData.values.s_attrs.text[text[0]].publishing_year})`),
|
||||
type: graphtype
|
||||
}
|
||||
];
|
||||
break;
|
||||
}
|
||||
return graphData;
|
||||
}
|
||||
|
||||
async renderTokenList() {
|
||||
let corpusTokenListElement = document.querySelector('.corpus-token-list');
|
||||
let corpusTokenList = new nopaque.resource_lists.CorpusTokenList(corpusTokenListElement);
|
||||
let filteredData = this.filterData();
|
||||
let stopwords = this.data.stopwords;
|
||||
if (this.data.stopwords === undefined) {
|
||||
stopwords = await this.getStopwords();
|
||||
}
|
||||
stopwords = Object.values(stopwords).flat();
|
||||
let mostFrequent = Object.entries(filteredData)
|
||||
.sort((a, b) => b[1].count - a[1].count)
|
||||
.filter(item => !stopwords.includes(item[0].toLowerCase()))
|
||||
.slice(0, 4)
|
||||
.map(item => item[0])
|
||||
|
||||
let tokenData = [];
|
||||
Object.entries(filteredData).forEach(item => {
|
||||
let resource = {
|
||||
term: item[0],
|
||||
count: item[1].count,
|
||||
mostFrequent: mostFrequent.includes(item[0])
|
||||
};
|
||||
if (!Object.values(stopwords).includes(resource.term)) {
|
||||
tokenData.push(resource);
|
||||
}
|
||||
});
|
||||
corpusTokenList.add(tokenData);
|
||||
}
|
||||
|
||||
filterData() {
|
||||
let frequenciesTokenCategoryDropdownElement = document.querySelector('[data-target="frequencies-token-category-dropdown"]');
|
||||
let tokenCategory = frequenciesTokenCategoryDropdownElement.firstChild.textContent.toLowerCase();
|
||||
let corpusData = this.data.corpus.o.staticData;
|
||||
let filteredData = {};
|
||||
|
||||
for (let i = 0; i < Object.values(corpusData.corpus.freqs[tokenCategory]).length; i++) {
|
||||
let term = corpusData.values.p_attrs[tokenCategory][i].toLowerCase();
|
||||
let count = corpusData.corpus.freqs[tokenCategory][i];
|
||||
|
||||
if (filteredData[term]) {
|
||||
filteredData[term].count += count;
|
||||
filteredData[term].originalIds.push(i);
|
||||
} else {
|
||||
filteredData[term] = {
|
||||
count: count,
|
||||
originalIds: [i]
|
||||
};
|
||||
}
|
||||
}
|
||||
return filteredData;
|
||||
}
|
||||
|
||||
|
||||
renderFrequenciesGraphic(tokenSet) {
|
||||
this.data.tokenSet = tokenSet;
|
||||
let corpusData = this.data.corpus.o.staticData;
|
||||
let frequenciesTokenCategoryDropdownElement = document.querySelector('[data-target="frequencies-token-category-dropdown"]');
|
||||
let frequenciesGraphicElement = document.querySelector('#frequencies-graphic');
|
||||
let texts = Object.entries(corpusData.s_attrs.text.lexicon);
|
||||
let graphtype = document.querySelector('.frequencies-graph-mode-button.disabled').dataset.graphType;
|
||||
let tokenCategory = frequenciesTokenCategoryDropdownElement.firstChild.textContent.toLowerCase();
|
||||
|
||||
let graphData = this.createFrequenciesGraphData(tokenCategory, texts, graphtype, tokenSet);
|
||||
let graphLayout = {
|
||||
barmode: graphtype === 'bar' ? 'stack' : '',
|
||||
yaxis: {
|
||||
showticklabels: graphtype === 'markers' ? false : true
|
||||
},
|
||||
height: 627,
|
||||
margin: {
|
||||
l: 33
|
||||
}
|
||||
};
|
||||
let config = {
|
||||
responsive: true,
|
||||
modeBarButtonsToRemove: ['zoom2d', 'select2d', 'lasso2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'],
|
||||
displaylogo: false
|
||||
};
|
||||
Plotly.newPlot(frequenciesGraphicElement, graphData, graphLayout, config);
|
||||
}
|
||||
|
||||
createFrequenciesGraphData(tokenCategory, texts, graphtype, tokenSet) {
|
||||
let corpusData = this.data.corpus.o.staticData;
|
||||
let graphData = [];
|
||||
let filteredData = this.filterData();
|
||||
switch (graphtype) {
|
||||
case 'markers':
|
||||
for (let item of tokenSet) {
|
||||
let textTitles = texts.map(text => `${corpusData.values.s_attrs.text[text[0]].title} (${corpusData.values.s_attrs.text[text[0]].publishing_year})`);
|
||||
let tokenCountPerText = [];
|
||||
for (let originalId of filteredData[item].originalIds) {
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
tokenCountPerText[i] = (tokenCountPerText[i] || 0) + (texts[i][1].freqs[tokenCategory][originalId] || 0);
|
||||
}
|
||||
}
|
||||
let data = {
|
||||
x: textTitles,
|
||||
y: texts.map(text => item),
|
||||
name: item,
|
||||
text: texts.map(text => `${item}<br>${tokenCountPerText || 0}`),
|
||||
mode: 'markers',
|
||||
marker: {
|
||||
size: tokenCountPerText,
|
||||
sizeref: 0.4
|
||||
}
|
||||
};
|
||||
graphData.push(data);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
for (let item of tokenSet) {
|
||||
let textTitles = texts.map(text => `${corpusData.values.s_attrs.text[text[0]].title} (${corpusData.values.s_attrs.text[text[0]].publishing_year})`);
|
||||
let tokenCountPerText = [];
|
||||
for (let originalId of filteredData[item].originalIds) {
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
tokenCountPerText[i] = (tokenCountPerText[i] || 0) + (texts[i][1].freqs[tokenCategory][originalId] || 0);
|
||||
}
|
||||
}
|
||||
let data = {
|
||||
x: textTitles,
|
||||
y: tokenCountPerText,
|
||||
name: item,
|
||||
type: graphtype
|
||||
};
|
||||
graphData.push(data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return graphData;
|
||||
}
|
||||
|
||||
renderStopwordSettingsModal(stopwords) {
|
||||
let stopwordInputField = document.querySelector('#stopword-input-field');
|
||||
let userStopwordListContainer = document.querySelector('#user-stopword-list-container');
|
||||
let stopwordLanguageSelection = document.querySelector('#stopword-language-selection');
|
||||
let stopwordLanguageChipList = document.querySelector('#stopword-language-chip-list');
|
||||
let deleteLanguageStopwordListEntriesButton = document.querySelector('#delete-language-stopword-list-entries-button');
|
||||
let resetLanguageStopwordListEntriesButton = document.querySelector('#reset-language-stopword-list-entries-button');
|
||||
|
||||
stopwordLanguageChipList.innerHTML = '';
|
||||
userStopwordListContainer.innerHTML = '';
|
||||
stopwordInputField.value = '';
|
||||
|
||||
// Render stopword language selection. Set english as default language. Filter out user_stopwords.
|
||||
if (stopwordLanguageSelection.children.length === 0) {
|
||||
Object.keys(stopwords).forEach(language => {
|
||||
if (language !== 'user_stopwords') {
|
||||
let optionElement = nopaque.Utils.HTMLToElement(`<option value="${language}" ${language === 'english' ? 'selected' : ''}>${language}</option>`);
|
||||
stopwordLanguageSelection.appendChild(optionElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render user stopwords over input field.
|
||||
if (this.data.stopwords['user_stopwords'].length > 0) {
|
||||
for (let word of this.data.stopwords['user_stopwords']) {
|
||||
let chipElement = nopaque.Utils.HTMLToElement(`<div class="chip">${word}<i class="close material-icons">close</i></div>`);
|
||||
chipElement.addEventListener('click', (event) => {
|
||||
let removedListItem = event.target.closest('.chip').firstChild.textContent;
|
||||
this.data.stopwords['user_stopwords'] = structuredClone(this.data.stopwords['user_stopwords'].filter(item => item !== removedListItem));
|
||||
});
|
||||
userStopwordListContainer.appendChild(chipElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Render english stopwords as default ...
|
||||
let selectedLanguage = document.querySelector('#stopword-language-selection').value;
|
||||
this.renderStopwordLanguageChipList(selectedLanguage, stopwords[selectedLanguage]);
|
||||
|
||||
// ... or render selected language stopwords.
|
||||
stopwordLanguageSelection.addEventListener('change', (event) => {
|
||||
this.renderStopwordLanguageChipList(event.target.value, stopwords[event.target.value]);
|
||||
});
|
||||
|
||||
// Eventlistener for deleting all stopwords of a language.
|
||||
deleteLanguageStopwordListEntriesButton.addEventListener('click', (event) => {
|
||||
let selectedLanguage = stopwordLanguageSelection.value;
|
||||
this.data.stopwords[selectedLanguage] = [];
|
||||
stopwordLanguageChipList.innerHTML = '';
|
||||
this.buttonRendering();
|
||||
});
|
||||
|
||||
// Eventlistener for resetting all stopwords of a language to the original stopwords.
|
||||
resetLanguageStopwordListEntriesButton.addEventListener('click', () => {
|
||||
let selectedLanguage = stopwordLanguageSelection.value;
|
||||
this.data.stopwords[selectedLanguage] = structuredClone(this.data.originalStopwords[selectedLanguage]);
|
||||
this.renderStopwordLanguageChipList(selectedLanguage, this.data.stopwords[selectedLanguage]);
|
||||
});
|
||||
|
||||
// Initialize Materialize components.
|
||||
M.Chips.init(
|
||||
stopwordInputField,
|
||||
{
|
||||
placeholder: 'Add stopwords',
|
||||
onChipAdd: (event) => {
|
||||
for (let word of event[0].M_Chips.chipsData) {
|
||||
if (!this.data.stopwords['user_stopwords'].includes(word.tag.toLowerCase())) {
|
||||
this.data.stopwords['user_stopwords'].push(word.tag.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
M.FormSelect.init(stopwordLanguageSelection);
|
||||
|
||||
}
|
||||
|
||||
buttonRendering() {
|
||||
let deleteLanguageStopwordListEntriesButton = document.querySelector('#delete-language-stopword-list-entries-button');
|
||||
let resetLanguageStopwordListEntriesButton = document.querySelector('#reset-language-stopword-list-entries-button');
|
||||
let selectedLanguage = document.querySelector('#stopword-language-selection').value;
|
||||
let stopwordLength = this.data.stopwords[selectedLanguage].length;
|
||||
let originalStopwordListLength = this.data.originalStopwords[selectedLanguage].length;
|
||||
|
||||
deleteLanguageStopwordListEntriesButton.classList.toggle('disabled', stopwordLength === 0);
|
||||
resetLanguageStopwordListEntriesButton.classList.toggle('disabled', stopwordLength === originalStopwordListLength);
|
||||
}
|
||||
|
||||
renderStopwordLanguageChipList(language, stopwords) {
|
||||
let stopwordLanguageChipList = document.querySelector('#stopword-language-chip-list');
|
||||
stopwordLanguageChipList.innerHTML = '';
|
||||
for (let word of stopwords) {
|
||||
let chipElement = nopaque.Utils.HTMLToElement(`<div class="chip">${word}<i class="close material-icons">close</i></div>`);
|
||||
chipElement.addEventListener('click', (event) => {
|
||||
let removedListItem = event.target.closest('.chip').firstChild.textContent;
|
||||
this.data.stopwords[language] = structuredClone(this.data.stopwords[language].filter(item => item !== removedListItem));
|
||||
this.buttonRendering();
|
||||
});
|
||||
stopwordLanguageChipList.appendChild(chipElement);
|
||||
}
|
||||
this.buttonRendering();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user