Move javascript files to fit new style

This commit is contained in:
Patrick Jentsch
2023-11-13 12:59:36 +01:00
parent 660d7ebc99
commit d298b200dc
27 changed files with 30 additions and 30 deletions

View 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');
}
}
}
}

View 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, "&lt;").replace(/>/g, "&gt;")}</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];
}
}
}

View 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);
}
};

View File

@ -0,0 +1 @@
nopaque.corpus_analysis.cqi.api = {};

View 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();
}
};

View 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;

View 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
};

View File

@ -0,0 +1 @@
nopaque.corpus_analysis.cqi = {};

View 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;
}
};

View 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;
}
};

View File

@ -0,0 +1 @@
nopaque.corpus_analysis.cqi.models = {};

View 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);
}
};

View 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;
}
};

View 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
};

View File

@ -0,0 +1 @@
nopaque.corpus_analysis = {};

View File

@ -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');
}
}

View File

@ -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;
}
}

View 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),
};
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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;'>&hellip;</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;'>&hellip;</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];
}
}
}

View 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();
}
}