citations service

This commit is contained in:
Artur
2025-11-26 18:19:19 +03:00
parent 059d3d5024
commit 5967cd34e2
6 changed files with 711 additions and 15 deletions

View File

@ -39,7 +39,8 @@
<script src="scripts/csl/styles/style-parser.js"></script>
<script src="scripts/csl/styles/storage.js"></script>
<script src="scripts/citation-doc-service.js"></script>
<script src="scripts/services/citation-service.js"></script>
<script src="scripts/services/translate-service.js"></script>
</head>
<body id="body" spellcheck="false" class="noselect">
<div id="loader" class="cssload-container display-none">

View File

@ -82,11 +82,14 @@ CslStylesManager.prototype.getLastUsedFormat = function () {
};
/**
* @returns {string}
* @returns {"endnotes" | "footnotes"}
*/
CslStylesManager.prototype.getLastUsedNotesStyle = function () {
let lastUsedNotesStyle = localStorage.getItem(this._lastNotesStyleKey);
if (lastUsedNotesStyle) {
if (
lastUsedNotesStyle === "footnotes" ||
lastUsedNotesStyle === "endnotes"
) {
return lastUsedNotesStyle;
}
return "footnotes";
@ -100,7 +103,7 @@ CslStylesManager.prototype.getLastUsedStyleId = function () {
if (lastUsedStyle) {
return lastUsedStyle;
}
return "ieee";
return "";
};
/**

View File

@ -0,0 +1,604 @@
// @ts-check
/// <reference path="../citeproc/citeproc_commonjs.js" />
/// <reference path="../zotero/zotero.js" />
/// <reference path="../csl/citation/storage.js" />
/// <reference path="../csl/citation/citation.js" />
/// <reference path="./translate-service.js" />
/// <reference path="./citation-doc-service.js" />
/// <reference path="../csl/styles/styles-manager.js" />
/**
* @param {CitationDocService} citationDocService
* @param {CslStylesManager} cslStylesManager
* @param {ZoteroSdk} sdk
*/
function CitationService(citationDocService, cslStylesManager, sdk) {
this.bibPlaceholder = "Please insert some citation into the document.";
this.citPrefixNew = "ZOTERO_ITEM";
this.citSuffixNew = "CSL_CITATION";
this.citPrefix = "ZOTERO_CITATION";
this.bibPrefixNew = "ZOTERO_BIBL";
this.bibPrefix = "ZOTERO_BIBLIOGRAPHY";
this.sdk = sdk;
this.cslStylesManager = cslStylesManager;
/** @type {CSL.Engine} */
this.formatter;
this.citationDocService = citationDocService;
}
CitationService.prototype = {
/**
* @param {{id: string, uris: string[]}} item
* @returns
*/
fillUrisFromId: function (item) {
const slashFirstIndex = item.id.indexOf("/") + 1;
const slashLastIndex = item.id.lastIndexOf("/") + 1;
const httpIndex = item.id.indexOf("http");
if (slashFirstIndex !== slashLastIndex && httpIndex === 0) {
if (!Object.hasOwnProperty.call(item, "uris")) {
item.uris = [];
}
item.uris.push(item.id);
}
if (slashLastIndex) item.id = item.id.substring(slashLastIndex);
return item;
},
/**
*
* @param {*} cslCitation
* @returns {Promise<Array<string|number>>}
*/
_formatInsertLink: function (cslCitation) {
const self = this;
let bUpdateItems = false;
/** @type {Array<string|number>} */
const keys = [];
/** @type {Array<InfoForCitationCluster>} */
const keysL = [];
return Promise.resolve()
.then(function () {
cslCitation
.getCitationItems()
.forEach(function (/** @type {CitationItem} */ item) {
if (!CSLCitationStorage.has(item.id)) {
bUpdateItems = true;
}
CSLCitationStorage.set(item.id, item);
keys.push(item.id);
keysL.push(item.getInfoForCitationCluster());
});
if (bUpdateItems) {
/** @type {string[]} */
var arrIds = [];
CSLCitationStorage.forEach(function (item, id) {
arrIds.push(id);
});
self.formatter.updateItems(arrIds);
}
})
.then(function () {
const fragment = document.createDocumentFragment();
const tempElement = document.createElement("div");
fragment.appendChild(tempElement);
// TODO может ещё очистить поиск (подумать над этим)
tempElement.innerHTML =
self.formatter.makeCitationCluster(keysL);
cslCitation.addPlainCitation(tempElement.innerText);
return self.citationDocService.addCitation(
tempElement.innerText,
JSON.stringify(cslCitation.toJSON())
);
})
.then(function () {
// TODO есть проблема, что в плагине мы индексы обновили, а вот в документе нет (по идее надо обновить и индексы в документе перед вставкой)
// но тогда у нас уедет селект и новое поле вставится не там, поэтому пока обновлять приходится в конце
// такая же проблем с вставкой библиографии (при обнолении индексов в плагине надо бы их обновлять и в документе тоже)
return self.updateCslItems(true, true, false);
})
.then(function () {
return keys;
});
},
/**
* @param {Array<any>} items
* @returns {Promise<CslJsonObjectItem[]>}
*/
_getSelectedInJsonFormat: function (items) {
var arrUsrItems = [];
/** @type {Object<string, string[]>} */
var arrGroupsItems = {};
for (var citationID in items) {
var item = items[citationID];
var userID = item["userID"];
const groupID = item["groupID"];
if (userID) {
arrUsrItems.push(item.id);
} else if (groupID) {
if (!arrGroupsItems[groupID]) {
arrGroupsItems[groupID] = [];
}
arrGroupsItems[groupID].push(item.id);
}
}
var promises = [];
if (arrUsrItems.length) {
promises.push(
this.sdk
.getItems(null, arrUsrItems, "json")
.then(function (res) {
var items = res.items || [];
return items;
})
.catch(function (err) {
console.error(err);
})
);
}
for (var groupID in arrGroupsItems) {
if (Object.hasOwnProperty.call(arrGroupsItems, groupID)) {
promises.push(
this.sdk
.getGroupItems(
null,
groupID,
arrGroupsItems[groupID],
"json"
)
.then(function (res) {
var items = res.items || [];
return items;
})
.catch(function (err) {
console.error(err);
})
);
}
}
return Promise.all(promises).then(function (res) {
/** @type {CslJsonObjectItem[]} */
var items = [];
res.forEach(function (resItems) {
if (
!Array.isArray(resItems) &&
Object.hasOwnProperty.call(resItems, "items")
) {
resItems = resItems.items;
}
items = items.concat(resItems);
});
return items;
});
},
/**
* @param {Array<any>} items
* @param {string} prefix
* @param {string} suffix
* @param {{locator: string, label: string} | null} locatorInfo
* @param {boolean} bOmitAuthor
* @returns {Promise<Array<string|number>>}
*/
insertSelectedCitations: function (
items,
prefix,
suffix,
locatorInfo,
bOmitAuthor
) {
const self = this;
var cslCitation = new CSLCitation(CSLCitationStorage.size, "");
for (var citationID in items) {
const item = items[citationID];
item["suppress-author"] = bOmitAuthor;
if (prefix !== "") {
item.prefix = prefix;
}
if (suffix !== "") {
item.suffix = suffix;
}
if (locatorInfo) {
item.locator = locatorInfo.locator;
item.label = locatorInfo.label;
}
cslCitation.fillFromObject(item);
}
return this._getSelectedInJsonFormat(items).then(function (items) {
items.forEach(function (item) {
cslCitation.fillFromObject(item);
});
return self._formatInsertLink(cslCitation);
});
},
/**
* @param {{id: string, uris: string[]}} item
* @returns
*/
/*synchronizeCSLItem: function (item) {
this.fillUrisFromId(item);
var cslItem = CSLCitationStorage.get(item.id);
if (!cslItem) {
return;
}
cslItem.fillFromObject(item);
},*/
/*synchronizeData: function () {
const self = this;
// form an array for request (one array for user and other for groups)
// todo now we should make full update (because when we make refresh, we check fields into the document). Fix it in new version (when we change refreshing and updating processes)
if (!CSLCitationStorage.size) return;
showLoader(true);
var bHasGroupsItems = false;
var arrUsrItems = [];
var arrGroupsItems = {};
CSLCitationStorage.forEach(function (citationItem, id) {
let index = CSLCitationStorage.getIndex(id);
let item = citationItem.toFlatJSON(index);
var userID = citationItem.getProperty("userID");
var groupID = citationItem.getProperty("groupID");
if (userID) {
arrUsrItems.push(citationItem.id);
} else if (groupID) {
if (!arrGroupsItems[groupID]) arrGroupsItems[groupID] = [];
arrGroupsItems[groupID].push(item.id);
}
});
const promises = [];
if (arrUsrItems.length) {
promises.push(this.sdk
.getItems(null, arrUsrItems)
.then(function (res) {
var items = (res.items ? res.items.items : []) || [];
items.forEach(function (item) {
self.synchronizeCSLItem(item);
});
}));
}
for (var groupID in arrGroupsItems) {
if (Object.hasOwnProperty.call(arrGroupsItems, groupID)) {
bHasGroupsItems = true;
promises.push(this.sdk
.getGroupItems(null, groupID, arrGroupsItems[groupID])
.then(function (res) {
var items = (res.items ? res.items.items : []) || [];
items.forEach(function (item) {
self.synchronizeCSLItem(item);
});
}));
}
}
Promise.all(promises).catch(function (err) {
console.error(err);
}).then(function () {
return self._updateAfterSync();
});
},*/
/**
* @param {boolean} bUpdateAll
* @param {boolean} bPastBib
* @returns {Promise<void>}
*/
_updateAllOrAddBib: function (bUpdateAll, bPastBib) {
const self = this;
return this.citationDocService
.getAddinZoteroFields()
.then(function (/** @type {CustomField[]} */ arrFields) {
if (!arrFields.length) {
return;
}
/** @type {CustomField[]} */
var updatedFields = [];
var bibField = null;
var bibFieldValue = " ";
const fragment = document.createDocumentFragment();
const tempElement = document.createElement("div");
fragment.appendChild(tempElement);
try {
var bibItems = new Array(CSLCitationStorage.size);
/** @type {false | any} */
var bibObject = self.formatter.makeBibliography();
// Sort bibliography items
for (var i = 0; i < bibObject[0].entry_ids.length; i++) {
var citationId = bibObject[0].entry_ids[i][0];
var citationIndex =
CSLCitationStorage.getIndex(citationId);
/** @type {string} */
var bibText = bibObject[1][i];
while (
bibText.indexOf("\n") !== bibText.lastIndexOf("\n")
) {
bibText = bibText.replace(/\n/, "");
}
// Check if bibliography item contains <sup> or <sub>
if (
/<sup[^>]*>|<\/sup>|<sub[^>]*>|<\/sub>/i.test(
bibText
)
) {
// Escape <sup> and <sub>
bibText = bibText
.replace(/<sup\b[^>]*>/gi, "&lt;sup&gt;")
.replace(/<\/sup>/gi, "&lt;/sup&gt;")
.replace(/<sub\b[^>]*>/gi, "&lt;sub&gt;")
.replace(/<\/sub>/gi, "&lt;/sub&gt;");
}
bibItems[citationIndex] = bibText;
}
tempElement.innerHTML = bibItems.join("");
} catch (e) {
if (
false ===
self.cslStylesManager.isLastUsedStyleContainBibliography()
) {
// style does not describe the bibliography
tempElement.textContent = "";
} else {
console.error(e);
throw "Failed to apply this style.";
}
}
var bibliography = tempElement.innerText;
arrFields.forEach(function (field) {
var citationObject;
var citationStartIndex = field.Value.indexOf("{");
var citationEndIndex = field.Value.lastIndexOf("}");
if (citationStartIndex !== -1) {
var citationString = field.Value.slice(
citationStartIndex,
citationEndIndex + 1
);
citationObject = JSON.parse(citationString);
}
var keysL = [];
var cslCitation;
if (
bUpdateAll &&
(field.Value.indexOf(self.citPrefixNew) !== -1 ||
field.Value.indexOf(self.citPrefix) !== -1)
) {
var citationID = ""; // old format
if (field.Value.indexOf(self.citPrefix) === -1) {
citationID = citationObject.citationID;
}
cslCitation = new CSLCitation(keysL.length, citationID);
cslCitation.fillFromObject(citationObject);
console.warn(cslCitation);
keysL = cslCitation.getInfoForCitationCluster();
tempElement.innerHTML =
self.formatter.makeCitationCluster(keysL);
field["Content"] = tempElement.innerText;
cslCitation.addPlainCitation(field["Content"]);
console.warn(cslCitation.toJSON());
if (cslCitation) {
field["Value"] =
self.citPrefixNew +
" " +
self.citSuffixNew +
JSON.stringify(cslCitation.toJSON());
}
updatedFields.push(field);
} else if (
field.Value.indexOf(self.bibPrefix) !== -1 ||
field.Value.indexOf(self.bibPrefixNew) !== -1
) {
bibField = field;
bibField["Content"] = bibliography;
if (
typeof citationObject === "object" &&
Object.keys(citationObject).length > 0
) {
bibFieldValue = JSON.stringify(citationObject);
}
}
});
if (bibField) {
updatedFields.push(bibField);
} else if (bPastBib) {
if (
self.cslStylesManager.isLastUsedStyleContainBibliography()
) {
return self.citationDocService
.addBibliography(bibliography, bibFieldValue)
.then(function () {
return updatedFields;
});
} else {
throw "The current bibliographic style does not describe the bibliography";
}
}
return updatedFields;
})
.then(function (
/** @type {CustomField[] | undefined} */ updatedFields
) {
if (updatedFields && updatedFields.length) {
return self.citationDocService.updateAddinFields(
updatedFields
);
}
});
},
/*_updateAfterSync: function () {
// todo now we should make full update (because when we make refresh, we check fields into the document). Fix it in new version (when we change refreshing and updating processes)
this._updateFormatter(true, false, true);
},*/
// onInit (1,0,0)
// Insert Citation (1,0,0)
// Insert Bibliography (1,1,1)
// Refresh (1,1,0)
/**
* @param {boolean} bUpdateFormatter
* @param {boolean} bUpdateAll
* @param {boolean} bPastBib
* @returns {Promise<void>}
*/
updateCslItems: function (bUpdateFormatter, bUpdateAll, bPastBib) {
CSLCitationStorage.clear();
const self = this;
return this.citationDocService
.getAddinZoteroFields()
.then(function (/** @type {CustomField[]} */ arrFields) {
var bibFieldValue = " ";
if (arrFields.length) {
var numOfItems = 0;
/** @type {CustomField | null} */
let bibField = arrFields.reduce(function (
/** @type {CustomField | null}*/ accumulator,
field
) {
var citationObject;
var citationStartIndex = field.Value.indexOf("{");
var citationEndIndex = field.Value.lastIndexOf("}");
if (
citationStartIndex !== -1 &&
citationEndIndex !== -1
) {
var citationString = field.Value.slice(
citationStartIndex,
citationEndIndex + 1
);
citationObject = JSON.parse(citationString);
}
if (
field.Value.indexOf(self.citPrefix) !== -1 ||
field.Value.indexOf(self.citPrefixNew) !== -1
) {
var citationID = ""; // old format
if (field.Value.indexOf(self.citPrefix) === -1) {
citationID = citationObject.citationID;
}
var cslCitation = new CSLCitation(
numOfItems,
citationID
);
numOfItems +=
cslCitation.fillFromObject(citationObject);
cslCitation
.getCitationItems()
.forEach(function (item) {
CSLCitationStorage.set(item.id, item);
});
} else if (
field.Value.indexOf(self.bibPrefix) !== -1 ||
field.Value.indexOf(self.bibPrefixNew) !== -1
) {
accumulator = field;
if (
typeof citationObject === "object" &&
Object.keys(citationObject).length > 0
) {
bibFieldValue = JSON.stringify(citationObject);
}
}
return accumulator;
},
null);
if (numOfItems) {
// sort?
} else if (bUpdateFormatter && bibField && bUpdateAll) {
// no need to find bib field again
bUpdateFormatter = false;
bibField["Content"] = translate(self.bibPlaceholder);
return self.citationDocService
.updateAddinFields([bibField])
.then(function () {
return bUpdateFormatter;
});
}
} else if (bUpdateFormatter && bPastBib) {
if (
self.cslStylesManager.isLastUsedStyleContainBibliography()
) {
return self.citationDocService
.addBibliography(
translate(self.bibPlaceholder),
bibFieldValue
)
.then(function () {
return bUpdateFormatter;
});
} else {
throw "The current bibliographic style does not describe the bibliography";
}
}
return bUpdateFormatter;
})
.then(function (bUpdateFormatter) {
if (bUpdateFormatter) return self._updateFormatter();
})
.then(function () {
if (bUpdateAll) {
return self._updateAllOrAddBib(bUpdateAll, bPastBib);
}
});
},
_updateFormatter: function () {
const self = this;
/** @type {string[]} */
const arrIds = [];
CSLCitationStorage.forEach(function (item, id) {
arrIds.push(id);
});
this.formatter = new CSL.Engine(
{
/** @param {string} id */
retrieveLocale: function (id) {
if (Object.hasOwnProperty.call(locales, id)) {
return locales[id];
}
return locales[selectedLocale];
},
/** @param {string} id */
retrieveItem: function (id) {
var item = CSLCitationStorage.get(id);
let index = CSLCitationStorage.getIndex(id);
if (!item) return null;
return item.toFlatJSON(index);
},
},
this.cslStylesManager.cached(
this.cslStylesManager.getLastUsedStyleId()
),
selectedLocale,
true
);
if (arrIds.length) {
this.formatter.updateItems(arrIds);
}
return;
},
};

View File

@ -0,0 +1,11 @@
// @ts-check
/// <reference path="../types-global.js" />
/**
* @param {string} message
* @returns {string}
*/
function translate(message) {
return window.Asc.plugin.tr(message);
}

View File

@ -1,5 +1,80 @@
// @ts-check
/**
* @typedef {Object} CslJsonObjectItem
* @property {CslJsonObjectData} data
* @property {string} key
* @property {CslJsonObjectLibrary} library
* @property {CslJsonObjectLinks} links
* @property {CslJsonObjectMeta} meta
* @property {number} version
*/
/**
* @typedef {Object} CslJsonObjectData
* @property {string} ISBN
* @property {string} abstractNote
* @property {string} accessDate
* @property {string} archive
* @property {string} archiveLocation
* @property {string} callNumber
* @property {Array<any>} collections
* @property {{creatorType: string, firstName: string, lastName: string}[]} creators
* @property {string} date
* @property {string} dateAdded
* @property {string} dateModified
* @property {string} edition
* @property {string} extra
* @property {string} itemType
* @property {string} key
* @property {string} language
* @property {string} libraryCatalog
* @property {string} numPages
* @property {string} numberOfVolumes
* @property {string} place
* @property {string} publisher
* @property {Object} relations
* @property {string} rights
* @property {string} series
* @property {string} seriesNumber
* @property {string} shortTitle
* @property {Array<string>} tags
* @property {string} title
* @property {string} url
* @property {number} version
* @property {string} volume
*/
/**
* @typedef {Object} CslJsonObjectLink
* @property {string} href
* @property {string} type
*/
/**
* @typedef {Object} CslJsonObjectMeta
* @property {{id:number,name:string,username:string,links:{alternate:CslJsonObjectLink}}} createdByUser
* @property {string} creatorSummary
* @property {number} numChildren
* @property {string} parsedDate
*/
/**
* @typedef {Object} CslJsonObjectLinks
* @property {CslJsonObjectLink} alternate
* @property {CslJsonObjectLink} self
*/
/**
* @typedef {Object} CslJsonObjectLibrary
* @property {number} id
* @property {{alternate:CslJsonObjectLink}} links
* @property {string} name
* @property {string} type
*/
/** ------------------------------------------------ */
/**
* @typedef {Object} AscSimpleRequestParams
* @property {string} url
@ -124,4 +199,4 @@ var Api = window.Api;
/**
* @typedef {Promise<FetchResponse>} FetchPromise
*/
*/

View File

@ -24,7 +24,7 @@
* @typedef {Object} ZoteroGroupInfo
* @property {number} id
* @property {number} version
* @property {{alternate: {href: string, type: string}, self: {href: string, type: string}}} meta
* @property {CslJsonObjectLinks} meta
* @property {{created: string, lastModified: string, numItems: number}} links
* @property {{name: string, description: string, id: number, owner: number, type: string}} data
*/
@ -46,6 +46,7 @@ const ZoteroSdk = function () {
// Constants
ZoteroSdk.prototype.ZOTERO_API_VERSION = "3";
ZoteroSdk.prototype.USER_AGENT = "AscDesktopEditor";
/** @type {"csljson"|"json"} */
ZoteroSdk.prototype.DEFAULT_FORMAT = "csljson";
ZoteroSdk.prototype.STORAGE_KEYS = {
USER_ID: "zoteroUserId",
@ -187,7 +188,7 @@ ZoteroSdk.prototype._parseLinkHeader = function (headerValue) {
* @param {Promise<AscSimpleResponse>} promise
* @param {function(any): void} resolve
* @param {function(any): void} reject
* @param {number} id
* @param {number|string} id
* @returns {Promise<void>}
*/
ZoteroSdk.prototype._parseDesktopItemsResponse = function (
@ -213,7 +214,7 @@ ZoteroSdk.prototype._parseDesktopItemsResponse = function (
* @param {Promise<FetchResponse>} promise
* @param {function(any): void} resolve
* @param {function(any): void} reject
* @param {number} id
* @param {number|string} id
* @returns {Promise<void>}
*/
ZoteroSdk.prototype._parseItemsResponse = function (
@ -233,7 +234,7 @@ ZoteroSdk.prototype._parseItemsResponse = function (
var links = self._parseLinkHeader(
response.headers.get("Link") || ""
);
/** @type {{items: any, id: number, next?: function(): Promise<void>}} */
/** @type {{items: any, id: number|string, next?: function(): Promise<void>}} */
var result = {
items: json,
id: id,
@ -262,7 +263,7 @@ ZoteroSdk.prototype._parseItemsResponse = function (
* @param {Promise<AscSimpleResponse | FetchResponse>} promise
* @param {function(any): void} resolve
* @param {function(any): void} reject
* @param {number} id
* @param {number|string} id
*/
ZoteroSdk.prototype._parseResponse = function (promise, resolve, reject, id) {
if (this._isOnlineAvailable) {
@ -283,9 +284,9 @@ ZoteroSdk.prototype._parseResponse = function (promise, resolve, reject, id) {
/**
* Get items from user library
* @param {string} search
* @param {string|null} search
* @param {string[]} itemsID
* @param {"csljson"|"json"} format
* @param {"csljson"|"json"} [format]
*/
ZoteroSdk.prototype.getItems = function (search, itemsID, format) {
var self = this;
@ -317,10 +318,10 @@ ZoteroSdk.prototype.getItems = function (search, itemsID, format) {
/**
* Get items from group library
* @param {string} search
* @param {number} groupId
* @param {string | null} search
* @param {number|string} groupId
* @param {string[]} itemsID
* @param {"csljson"|"json"} format
* @param {"csljson"|"json"} [format]
*
*/
ZoteroSdk.prototype.getGroupItems = function (
@ -330,6 +331,7 @@ ZoteroSdk.prototype.getGroupItems = function (
format
) {
var self = this;
format = format || self.DEFAULT_FORMAT;
return new Promise(function (resolve, reject) {