948 lines
69 KiB
JavaScript
948 lines
69 KiB
JavaScript
|
import { visit } from 'unist-util-visit';
|
|||
|
import fetch from 'cross-fetch';
|
|||
|
import { parseFragment } from 'parse5';
|
|||
|
import { fromParse5 } from 'hast-util-from-parse5';
|
|||
|
|
|||
|
function _wrapRegExp() {
|
|||
|
_wrapRegExp = function (e, r) {
|
|||
|
return new BabelRegExp(e, void 0, r);
|
|||
|
};
|
|||
|
var e = RegExp.prototype,
|
|||
|
r = new WeakMap();
|
|||
|
function BabelRegExp(e, t, p) {
|
|||
|
var o = new RegExp(e, t);
|
|||
|
return r.set(o, p || r.get(e)), _setPrototypeOf(o, BabelRegExp.prototype);
|
|||
|
}
|
|||
|
function buildGroups(e, t) {
|
|||
|
var p = r.get(t);
|
|||
|
return Object.keys(p).reduce(function (r, t) {
|
|||
|
var o = p[t];
|
|||
|
if ("number" == typeof o) r[t] = e[o];else {
|
|||
|
for (var i = 0; void 0 === e[o[i]] && i + 1 < o.length;) i++;
|
|||
|
r[t] = e[o[i]];
|
|||
|
}
|
|||
|
return r;
|
|||
|
}, Object.create(null));
|
|||
|
}
|
|||
|
return _inherits(BabelRegExp, RegExp), BabelRegExp.prototype.exec = function (r) {
|
|||
|
var t = e.exec.call(this, r);
|
|||
|
if (t) {
|
|||
|
t.groups = buildGroups(t, this);
|
|||
|
var p = t.indices;
|
|||
|
p && (p.groups = buildGroups(p, this));
|
|||
|
}
|
|||
|
return t;
|
|||
|
}, BabelRegExp.prototype[Symbol.replace] = function (t, p) {
|
|||
|
if ("string" == typeof p) {
|
|||
|
var o = r.get(this);
|
|||
|
return e[Symbol.replace].call(this, t, p.replace(/\$<([^>]+)>/g, function (e, r) {
|
|||
|
var t = o[r];
|
|||
|
return "$" + (Array.isArray(t) ? t.join("$") : t);
|
|||
|
}));
|
|||
|
}
|
|||
|
if ("function" == typeof p) {
|
|||
|
var i = this;
|
|||
|
return e[Symbol.replace].call(this, t, function () {
|
|||
|
var e = arguments;
|
|||
|
return "object" != typeof e[e.length - 1] && (e = [].slice.call(e)).push(buildGroups(e, i)), p.apply(this, e);
|
|||
|
});
|
|||
|
}
|
|||
|
return e[Symbol.replace].call(this, t, p);
|
|||
|
}, _wrapRegExp.apply(this, arguments);
|
|||
|
}
|
|||
|
function _extends() {
|
|||
|
_extends = Object.assign ? Object.assign.bind() : function (target) {
|
|||
|
for (var i = 1; i < arguments.length; i++) {
|
|||
|
var source = arguments[i];
|
|||
|
for (var key in source) {
|
|||
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|||
|
target[key] = source[key];
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return target;
|
|||
|
};
|
|||
|
return _extends.apply(this, arguments);
|
|||
|
}
|
|||
|
function _inherits(subClass, superClass) {
|
|||
|
if (typeof superClass !== "function" && superClass !== null) {
|
|||
|
throw new TypeError("Super expression must either be null or a function");
|
|||
|
}
|
|||
|
subClass.prototype = Object.create(superClass && superClass.prototype, {
|
|||
|
constructor: {
|
|||
|
value: subClass,
|
|||
|
writable: true,
|
|||
|
configurable: true
|
|||
|
}
|
|||
|
});
|
|||
|
Object.defineProperty(subClass, "prototype", {
|
|||
|
writable: false
|
|||
|
});
|
|||
|
if (superClass) _setPrototypeOf(subClass, superClass);
|
|||
|
}
|
|||
|
function _setPrototypeOf(o, p) {
|
|||
|
_setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {
|
|||
|
o.__proto__ = p;
|
|||
|
return o;
|
|||
|
};
|
|||
|
return _setPrototypeOf(o, p);
|
|||
|
}
|
|||
|
|
|||
|
// Regex adapted from https://github.com/Zettlr/Zettlr/blob/develop/source/common/util/extract-citations.ts
|
|||
|
|
|||
|
/**
|
|||
|
* Citation detection: The first alternative matches "full" citations surrounded
|
|||
|
* by square brackets, whereas the second one matches in-text citations,
|
|||
|
* optionally with suffixes.
|
|||
|
*
|
|||
|
* * Group 1 matches regular "full" citations
|
|||
|
* * Group 2 matches in-text citations (not surrounded by brackets)
|
|||
|
* * Group 3 matches optional square-brackets suffixes to group 2 matches
|
|||
|
*
|
|||
|
* For more information, see https://pandoc.org/MANUAL.html#extension-citations
|
|||
|
*
|
|||
|
* @var {RegExp}
|
|||
|
*/
|
|||
|
const citationRE = /(?:\[((?:[\0-Z\\\^-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*@(?:[\0-Z\\\^-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)\])|(?<=[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]|^|(\x2D))(?:@((?:[0-9A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00
|
|||
|
|
|||
|
/**
|
|||
|
* I hate everything at this. This can match every single possible variation on
|
|||
|
* whatever the f*** you can possibly do within square brackets according to the
|
|||
|
* documentation. I opted for named groups for these because otherwise I have no
|
|||
|
* idea what I have been doing here.
|
|||
|
*
|
|||
|
* * Group prefix: Contains the prefix, ends with a dash if we should suppress the author
|
|||
|
* * Group citekey: Contains the actual citekey, can be surrounded in curly brackets
|
|||
|
* * Group explicitLocator: Contains an explicit locator statement. If given, we MUST ignore any form of locator in the suffix
|
|||
|
* * Group explicitLocatorInSuffix: Same as above, but not concatenated to the citekey
|
|||
|
* * Group suffix: Contains the suffix, but may start with a locator (if explicitLocator and explicitLocatorInSuffix are not given)
|
|||
|
*
|
|||
|
* @var {RegExp}
|
|||
|
*/
|
|||
|
const fullCitationRE = /*#__PURE__*/_wrapRegExp(/((?:[\0-\t\x0B\f\x0E-\u2027\u202A-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)?(?:@((?:[0-9A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-
|
|||
|
prefix: 1,
|
|||
|
citekey: 2,
|
|||
|
explicitLocator: 3,
|
|||
|
explicitLocatorInSuffix: 4,
|
|||
|
suffix: 5
|
|||
|
});
|
|||
|
|
|||
|
/**
|
|||
|
* This regular expression matches locator ranges, like the following:
|
|||
|
*
|
|||
|
* * 23-45, and further (here it matches up to, not including the comma)
|
|||
|
* * 45
|
|||
|
* * 15423
|
|||
|
* * 14235-12532
|
|||
|
* * 12-34, 23, 56
|
|||
|
* * 12, 23-14, 23
|
|||
|
* * 12, 54, 12-23
|
|||
|
* * 1, 1-4
|
|||
|
* * 3
|
|||
|
* * NEW NEW NEW: Now also matches Roman numerals as sometimes used in forewords!
|
|||
|
*
|
|||
|
* @var {RegExp}
|
|||
|
*/
|
|||
|
const locatorRE = /^(?:[\d, -]*\d|[ivxlcdm, -]*[ivxlcdm])/i;
|
|||
|
|
|||
|
/**
|
|||
|
* The locatorLabels have been sourced from the Citr library. Basically it's just
|
|||
|
* a map with valid CSL locator labels and an array of possible natural labels
|
|||
|
* which a user might want to write (instead of the standardized labels).
|
|||
|
*
|
|||
|
* @var {{ [key: string]: string[] }}}
|
|||
|
*/
|
|||
|
const locatorLabels = {
|
|||
|
book: ['Buch', 'Bücher', 'B.', 'book', 'books', 'bk.', 'bks.', 'livre', 'livres', 'liv.'],
|
|||
|
chapter: ['Kapitel', 'Kap.', 'chapter', 'chapters', 'chap.', 'chaps', 'chapitre', 'chapitres'],
|
|||
|
column: ['Spalte', 'Spalten', 'Sp.', 'column', 'columns', 'col.', 'cols', 'colonne', 'colonnes'],
|
|||
|
figure: ['Abbildung', 'Abbildungen', 'Abb.', 'figure', 'figures', 'fig.', 'figs'],
|
|||
|
folio: ['Blatt', 'Blätter', 'Fol.', 'folio', 'folios', 'fol.', 'fols', 'fᵒ', 'fᵒˢ'],
|
|||
|
issue: ['Nummer', 'Nummern', 'Nr.', 'number', 'numbers', 'no.', 'nos.', 'numéro', 'numéros', 'nᵒ', 'nᵒˢ'],
|
|||
|
line: ['Zeile', 'Zeilen', 'Z', 'line', 'lines', 'l.', 'll.', 'ligne', 'lignes'],
|
|||
|
note: ['Note', 'Noten', 'N.', 'note', 'notes', 'n.', 'nn.'],
|
|||
|
opus: ['Opus', 'Opera', 'op.', 'opus', 'opera', 'opp.'],
|
|||
|
page: ['Seite', 'Seiten', 'S.', 'page', 'pages', 'p.', 'pp.'],
|
|||
|
paragraph: ['Absatz', 'Absätze', 'Abs.', '¶', '¶¶', 'paragraph', 'paragraphs', 'para.', 'paras', 'paragraphe', 'paragraphes', 'paragr.'],
|
|||
|
part: ['Teil', 'Teile', 'part', 'parts', 'pt.', 'pts', 'partie', 'parties', 'part.'],
|
|||
|
section: ['Abschnitt', 'Abschnitte', 'Abschn.', '§', '§§', 'section', 'sections', 'sec.', 'secs', 'sect.'],
|
|||
|
'sub verbo': ['sub verbo', 'sub verbis', 's. v.', 's. vv.', 's.v.', 's.vv.'],
|
|||
|
verse: ['Vers', 'Verse', 'V.', 'verse', 'verses', 'v.', 'vv.', 'verset', 'versets'],
|
|||
|
volume: ['Band', 'Bände', 'Bd.', 'Bde.', 'volume', 'volumes', 'vol.', 'vols.']
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Parses a given citation string and return entries and isComposite flag required for cite-proc.
|
|||
|
* Adapted from https://github.com/Zettlr/Zettlr/blob/develop/source/common/util/extract-citations.ts
|
|||
|
*
|
|||
|
* @param {RegExpMatchArray} regexMatch Cite string in the form of '[@item]' or '@item'
|
|||
|
* @return {[CiteItem[], boolean]} [entries, isComposite]
|
|||
|
*/
|
|||
|
const parseCitation = regexMatch => {
|
|||
|
/** @type {CiteItem[]} */
|
|||
|
let entries = [];
|
|||
|
let isComposite = false;
|
|||
|
const fullCitation = regexMatch[1];
|
|||
|
const inTextSuppressAuthor = regexMatch[2];
|
|||
|
const inTextCitation = regexMatch[3];
|
|||
|
const optionalSuffix = regexMatch[4];
|
|||
|
if (fullCitation !== undefined) {
|
|||
|
// Handle citations in the form of [@item1; @item2]
|
|||
|
for (const citationPart of fullCitation.split(';')) {
|
|||
|
const match = fullCitationRE.exec(citationPart.trim());
|
|||
|
if (match === null) {
|
|||
|
continue; // Faulty citation
|
|||
|
}
|
|||
|
// Prefix is the portion before @ e.g. [see @item1] or an empty string
|
|||
|
// We explicitly cast groups since we have groups in our RegExp and as
|
|||
|
// such the groups object will be set.
|
|||
|
/** @type {CiteItem} */
|
|||
|
const thisCitation = {
|
|||
|
id: match.groups.citekey.replace(/{(.+)}/, '$1'),
|
|||
|
prefix: undefined,
|
|||
|
locator: undefined,
|
|||
|
label: 'page',
|
|||
|
'suppress-author': false,
|
|||
|
suffix: undefined
|
|||
|
};
|
|||
|
|
|||
|
// First, deal with the prefix. The speciality here is that it can
|
|||
|
// indicate if we should suppress the author.
|
|||
|
const rawPrefix = match.groups.prefix;
|
|||
|
if (rawPrefix !== undefined) {
|
|||
|
thisCitation['suppress-author'] = rawPrefix.trim().endsWith('-');
|
|||
|
if (thisCitation['suppress-author']) {
|
|||
|
thisCitation.prefix = rawPrefix.substring(0, rawPrefix.trim().length - 1).trim();
|
|||
|
} else {
|
|||
|
thisCitation.prefix = rawPrefix.trim();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Second, deal with the suffix. This one can be much more tricky than
|
|||
|
// the prefix. We have three alternatives where the locator may be
|
|||
|
// present: If we have an explicitLocator or an explicitLocatorInSuffix,
|
|||
|
// we should extract the locator from there and leave the actual suffix
|
|||
|
// untouched. Only if those two alternatives are not present, then we
|
|||
|
// have a look at the rawSuffix and extract a (potential) locator.
|
|||
|
const explicitLocator = match.groups.explicitLocator;
|
|||
|
const explicitLocatorInSuffix = match.groups.explicitLocatorInSuffix;
|
|||
|
const rawSuffix = match.groups.suffix;
|
|||
|
let suffixToParse;
|
|||
|
let containsLocator = true;
|
|||
|
if (explicitLocator === undefined && explicitLocatorInSuffix === undefined) {
|
|||
|
// Potential locator in rawSuffix. Only in this case should we overwrite
|
|||
|
// the suffix (hence the same if-condition below)
|
|||
|
suffixToParse = rawSuffix;
|
|||
|
containsLocator = false;
|
|||
|
} else if (explicitLocatorInSuffix !== undefined || explicitLocator !== undefined) {
|
|||
|
suffixToParse = explicitLocator !== undefined ? explicitLocator : explicitLocatorInSuffix;
|
|||
|
thisCitation.suffix = rawSuffix == null ? void 0 : rawSuffix.trim();
|
|||
|
}
|
|||
|
const {
|
|||
|
label,
|
|||
|
locator,
|
|||
|
suffix
|
|||
|
} = parseSuffix(suffixToParse, containsLocator);
|
|||
|
thisCitation.locator = locator;
|
|||
|
if (label !== undefined) {
|
|||
|
thisCitation.label = label;
|
|||
|
}
|
|||
|
if (explicitLocator === undefined && explicitLocatorInSuffix === undefined) {
|
|||
|
thisCitation.suffix = suffix;
|
|||
|
} else if (suffix !== undefined && thisCitation.locator !== undefined) {
|
|||
|
// If we're here, we should not change the suffix, but parseSuffix may
|
|||
|
// have put something into the suffix return. If we're here, that will
|
|||
|
// definitely be a part of the locator.
|
|||
|
thisCitation.locator += suffix;
|
|||
|
}
|
|||
|
entries.push(thisCitation);
|
|||
|
}
|
|||
|
} else {
|
|||
|
// We have an in-text citation, so we can take a shortcut
|
|||
|
isComposite = true;
|
|||
|
entries.push(_extends({
|
|||
|
prefix: undefined,
|
|||
|
id: inTextCitation.replace(/{(.+)}/, '$1'),
|
|||
|
'suppress-author': inTextSuppressAuthor !== undefined
|
|||
|
}, parseSuffix(optionalSuffix, false)));
|
|||
|
}
|
|||
|
return [entries, isComposite];
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* This takes a suffix and extracts optional label and locator from this. Pass
|
|||
|
* true for the containsLocator property to indicate to this function that what
|
|||
|
* it got was not a regular suffix with an optional locator, but an explicit
|
|||
|
* locator so it knows it just needs to look for an optional label.
|
|||
|
*
|
|||
|
* @param {string} suffix The suffix to parse
|
|||
|
* @param {boolean} containsLocator If true, forces parseSuffix to return a locator
|
|||
|
*
|
|||
|
* @return {CiteItemSuffix} An object containing three optional properties locator, label, or suffix.
|
|||
|
*/
|
|||
|
function parseSuffix(suffix, containsLocator) {
|
|||
|
/** @type {CiteItemSuffix} */
|
|||
|
const retValue = {
|
|||
|
locator: undefined,
|
|||
|
label: 'page',
|
|||
|
suffix: undefined
|
|||
|
};
|
|||
|
if (suffix === undefined) {
|
|||
|
return retValue;
|
|||
|
}
|
|||
|
|
|||
|
// Make sure the suffix does not start or end with spaces
|
|||
|
suffix = suffix.trim();
|
|||
|
|
|||
|
// If there is a label, the suffix must start with it
|
|||
|
for (const label in locatorLabels) {
|
|||
|
for (const natural of locatorLabels[label]) {
|
|||
|
if (suffix.toLowerCase().startsWith(natural.toLowerCase())) {
|
|||
|
retValue.label = label;
|
|||
|
if (containsLocator) {
|
|||
|
// The suffix actually is the full locator, we just had to extract
|
|||
|
// the label from it. There is no remaining suffix.
|
|||
|
retValue.locator = suffix.substr(natural.length).trim();
|
|||
|
} else {
|
|||
|
// The caller indicated that this is a regular suffix, so we must also
|
|||
|
// extract the locator from what is left after label extraction.
|
|||
|
retValue.suffix = suffix.substr(natural.length).trim();
|
|||
|
const match = locatorRE.exec(retValue.suffix);
|
|||
|
if (match !== null) {
|
|||
|
retValue.locator = match[0]; // Extract the full match
|
|||
|
retValue.suffix = retValue.suffix.substr(match[0].length).trim();
|
|||
|
}
|
|||
|
}
|
|||
|
return retValue; // Early exit
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// If we're here, there was no explicit label given, but the caller has indicated
|
|||
|
// that this suffix MUST contain a locator. This means that the whole suffix is
|
|||
|
// the locator.
|
|||
|
if (containsLocator) {
|
|||
|
retValue.locator = suffix;
|
|||
|
} else {
|
|||
|
// The caller has not indicated that the whole suffix is the locator, so it
|
|||
|
// can be at the beginning. We only accept simple page/number ranges here.
|
|||
|
// For everything, the user should please be more specific.
|
|||
|
const match = locatorRE.exec(suffix);
|
|||
|
if (match !== null) {
|
|||
|
retValue.locator = match[0]; // Full match is the locator
|
|||
|
retValue.suffix = suffix.substr(match[0].length).trim(); // The rest is the suffix.
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return retValue;
|
|||
|
}
|
|||
|
|
|||
|
const readFile = async path => {
|
|||
|
if (isValidHttpUrl(path)) {
|
|||
|
return fetch(path).then(response => response.text()).then(data => data);
|
|||
|
} else {
|
|||
|
{
|
|||
|
return import('fs').then(fs => fs.readFileSync(path, 'utf8'));
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Check if valid URL
|
|||
|
* https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url
|
|||
|
*
|
|||
|
* @param {string} str
|
|||
|
* @return {boolean}
|
|||
|
*/
|
|||
|
const isValidHttpUrl = str => {
|
|||
|
let url;
|
|||
|
try {
|
|||
|
url = new URL(str);
|
|||
|
} catch (_) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Get bibliography by merging options and vfile data
|
|||
|
*
|
|||
|
* @param {import('./generator.js').Options} options
|
|||
|
* @param {import('vfile').VFile} file
|
|||
|
*/
|
|||
|
const getBibliography = async (options, file) => {
|
|||
|
var _file$data;
|
|||
|
/** @type {string[]} */
|
|||
|
let bibliography = [];
|
|||
|
if (options.bibliography) {
|
|||
|
bibliography = typeof options.bibliography === 'string' ? [options.bibliography] : options.bibliography;
|
|||
|
// @ts-ignore
|
|||
|
} else if (file != null && (_file$data = file.data) != null && (_file$data = _file$data.frontmatter) != null && _file$data.bibliography) {
|
|||
|
// @ts-ignore
|
|||
|
bibliography = typeof file.data.frontmatter.bibliography === 'string' ? [file.data.frontmatter.bibliography] : file.data.frontmatter.bibliography;
|
|||
|
// If local path, get absolute path
|
|||
|
for (let i = 0; i < bibliography.length; i++) {
|
|||
|
if (!isValidHttpUrl(bibliography[i])) {
|
|||
|
{
|
|||
|
bibliography[i] = await import('path').then(path => path.join(options.path || file.cwd, bibliography[i]));
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return bibliography;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Load CSL - supports predefined name from config.templates.data or http, file path (nodejs)
|
|||
|
*
|
|||
|
* @param {*} Cite cite object from citation-js
|
|||
|
* @param {string} format CSL name e.g. apa or file path to CSL file
|
|||
|
* @param {string} root optional root path
|
|||
|
*/
|
|||
|
const loadCSL = async (Cite, format, root = '') => {
|
|||
|
const config = Cite.plugins.config.get('@csl');
|
|||
|
if (!Object.keys(config.templates.data).includes(format)) {
|
|||
|
const cslName = `customCSL-${Math.random().toString(36).slice(2, 7)}`;
|
|||
|
let cslPath = '';
|
|||
|
if (isValidHttpUrl(format)) cslPath = format;else {
|
|||
|
cslPath = await import('path').then(path => path.join(root, format));
|
|||
|
}
|
|||
|
try {
|
|||
|
config.templates.add(cslName, await readFile(cslPath));
|
|||
|
} catch (err) {
|
|||
|
throw new Error(`Input CSL option, ${format}, is invalid or is an unknown file.`);
|
|||
|
}
|
|||
|
return cslName;
|
|||
|
} else {
|
|||
|
return format;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Load locale - supports predefined name from config.locales.data or http, file path (nodejs)
|
|||
|
*
|
|||
|
* @param {*} Cite cite object from citation-js
|
|||
|
* @param {string} format locale name
|
|||
|
* @param {string} root optional root path
|
|||
|
*/
|
|||
|
const loadLocale = async (Cite, format, root = '') => {
|
|||
|
const config = Cite.plugins.config.get('@csl');
|
|||
|
if (!Object.keys(config.locales.data).includes(format)) {
|
|||
|
let localePath = '';
|
|||
|
if (isValidHttpUrl(format)) localePath = format;else {
|
|||
|
localePath = await import('path').then(path => path.join(root, format));
|
|||
|
}
|
|||
|
try {
|
|||
|
const file = await readFile(localePath);
|
|||
|
const xmlLangRe = /xml:lang="(.+)"/;
|
|||
|
const localeName = file.match(xmlLangRe)[1];
|
|||
|
config.locales.add(localeName, file);
|
|||
|
return localeName;
|
|||
|
} catch (err) {
|
|||
|
throw new Error(`Input locale option, ${format}, is invalid or is an unknown file.`);
|
|||
|
}
|
|||
|
} else {
|
|||
|
return format;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Get citation format
|
|||
|
*
|
|||
|
* @param {*} citeproc citeproc
|
|||
|
* @returns string
|
|||
|
*/
|
|||
|
const getCitationFormat = citeproc => {
|
|||
|
const info = citeproc.cslXml.dataObj.children[0];
|
|||
|
const node = info.children.find(x => x['attrs'] && x['attrs']['citation-format']);
|
|||
|
// citation-format takes 5 possible values
|
|||
|
// https://docs.citationstyles.org/en/stable/specification.html#toc-entry-14
|
|||
|
/** @type {'author-date' | 'author' | 'numeric' | 'note' | 'label'} */
|
|||
|
const citationFormat = node['attrs']['citation-format'];
|
|||
|
return citationFormat;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Get registry objects that matches a list of relevantIds
|
|||
|
* If sorted is false, retrieve registry item in the order of the given relevantIds
|
|||
|
*
|
|||
|
* @param {*} citeproc citeproc
|
|||
|
* @param {string[]} relevantIds
|
|||
|
* @param {boolean} sorted
|
|||
|
* @return {*} registry objects that matches Ids, in the correct order
|
|||
|
*/
|
|||
|
const getSortedRelevantRegistryItems = (citeproc, relevantIds, sorted) => {
|
|||
|
const res = [];
|
|||
|
if (sorted) {
|
|||
|
// If sorted follow registry order
|
|||
|
for (const item of citeproc.registry.reflist) {
|
|||
|
if (relevantIds.includes(item.id)) res.push(item);
|
|||
|
}
|
|||
|
} else {
|
|||
|
// Otherwise follow the relevantIds
|
|||
|
for (const id of relevantIds) {
|
|||
|
res.push(citeproc.registry.reflist.find(x => x.id === id));
|
|||
|
}
|
|||
|
}
|
|||
|
return res;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Split a string into two parts based on a given index position
|
|||
|
*
|
|||
|
* @param {string} str
|
|||
|
* @param {number} index
|
|||
|
* @return {string[]}
|
|||
|
*/
|
|||
|
const split = (str, index) => {
|
|||
|
return [str.slice(0, index), str.slice(index)];
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Check if two registry objects belong to the same author
|
|||
|
* Currently only checks on family name
|
|||
|
*
|
|||
|
* @param {*} item registry object
|
|||
|
* @param {*} item2 registry object
|
|||
|
* @return {boolean}
|
|||
|
*/
|
|||
|
const isSameAuthor = (item, item2) => {
|
|||
|
const authorList = item.ref.author;
|
|||
|
const authorList2 = item2.ref.author;
|
|||
|
if (authorList.length !== authorList2.length) return false;
|
|||
|
for (let i = 0; i < authorList.length; i++) {
|
|||
|
if (authorList[i].family !== authorList2[i].family) return false;
|
|||
|
}
|
|||
|
return true;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Convert HTML to HAST node
|
|||
|
*
|
|||
|
* @param {string} html
|
|||
|
*/
|
|||
|
const htmlToHast = html => {
|
|||
|
const p5ast = parseFragment(html);
|
|||
|
// @ts-ignore
|
|||
|
return fromParse5(p5ast).children[0];
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* @typedef {import('./types').CiteItem} CiteItem
|
|||
|
* @typedef {import('./types').Mode} Mode
|
|||
|
* @typedef {import('./types').Options} Options
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Generate citation using citeproc
|
|||
|
* This accounts for prev citations and additional properties
|
|||
|
*
|
|||
|
* @param {*} citeproc
|
|||
|
* @param {Mode} mode
|
|||
|
* @param {CiteItem[]} entries
|
|||
|
* @param {string} citationIdRoot
|
|||
|
* @param {number} citationId
|
|||
|
* @param {any[]} citationPre
|
|||
|
* @param {Options} options
|
|||
|
* @param {boolean} isComposite
|
|||
|
* @param {import('./types').CitationFormat} citationFormat
|
|||
|
* @return {[string, string]}
|
|||
|
*/
|
|||
|
const genCitation = (citeproc, mode, entries, citationIdRoot, citationId, citationPre, options, isComposite, citationFormat) => {
|
|||
|
const {
|
|||
|
inlineClass,
|
|||
|
linkCitations
|
|||
|
} = options;
|
|||
|
const key = `${citationIdRoot}-${citationId}`;
|
|||
|
const c = citeproc.processCitationCluster({
|
|||
|
citationID: key,
|
|||
|
citationItems: entries,
|
|||
|
properties: mode === 'in-text' ? {
|
|||
|
noteIndex: 0,
|
|||
|
mode: isComposite ? 'composite' : ''
|
|||
|
} : {
|
|||
|
noteIndex: citationId,
|
|||
|
mode: isComposite ? 'composite' : ''
|
|||
|
}
|
|||
|
}, citationPre.length > 0 ? citationPre : [], []);
|
|||
|
// c = [ { bibchange: true, citation_errors: [] }, [ [ 0, '(1)', 'CITATION-1' ] ]]
|
|||
|
|
|||
|
const citationText = c[1].find(x => x[2] === key)[1];
|
|||
|
const ids = `citation--${entries.map(x => x.id.toLowerCase()).join('--')}--${citationId}`;
|
|||
|
if (mode === 'note') {
|
|||
|
// Use cite-fn-{id} to denote footnote from citation, will clean it up later to follow gfm "user-content" format
|
|||
|
return [citationText, htmlToHast(`<span class="${(inlineClass != null ? inlineClass : []).join(' ')}" id=${ids}><sup><a href="#cite-fn-${citationId}" id="cite-fnref-${citationId}" data-footnote-ref aria-describedby="footnote-label">${citationId}</a></sup></span>`)];
|
|||
|
} else if (linkCitations && citationFormat === 'numeric') {
|
|||
|
// e.g. [1, 2]
|
|||
|
let i = 0;
|
|||
|
const refIds = entries.map(e => e.id);
|
|||
|
const output = citationText.replace(/\d+/g, function (d) {
|
|||
|
const url = `<a href="#bib-${refIds[i].toLowerCase()}">${d}</a>`;
|
|||
|
i++;
|
|||
|
return url;
|
|||
|
});
|
|||
|
return [citationText, htmlToHast(`<span class="${(inlineClass != null ? inlineClass : []).join(' ')}" id=${ids}>${output}</span>`)];
|
|||
|
} else if (linkCitations && citationFormat === 'author-date') {
|
|||
|
// E.g. (see Nash, 1950, pp. 12–13, 1951); (Nash, 1950; Xie, 2016)
|
|||
|
if (entries.length === 1) {
|
|||
|
// Do not link bracket
|
|||
|
const output = isComposite ? `<a href="#bib-${entries[0].id.toLowerCase()}">${citationText}</a>` : `${citationText.slice(0, 1)}<a href="#bib-${entries[0].id.toLowerCase()}">${citationText.slice(1, -1)}</a>${citationText.slice(-1)}`;
|
|||
|
return [citationText, htmlToHast(`<span class="${(inlineClass != null ? inlineClass : []).join(' ')}" id=${ids}>${output}</span>`)];
|
|||
|
} else {
|
|||
|
// Retrieve the items in the correct order and attach link each of them
|
|||
|
const refIds = entries.map(e => e.id);
|
|||
|
const results = getSortedRelevantRegistryItems(citeproc, refIds, citeproc.opt.sort_citations);
|
|||
|
const output = [];
|
|||
|
let str = citationText;
|
|||
|
for (const [i, item] of results.entries()) {
|
|||
|
// Need to compare author. If same just match on date.
|
|||
|
const id = item.id;
|
|||
|
let citeMatch = item.ambig;
|
|||
|
// If author is the same as the previous, some styles like apa collapse the author
|
|||
|
if (i > 0 && isSameAuthor(results[i - 1], item) && str.indexOf(citeMatch) === -1) {
|
|||
|
// Just match on year
|
|||
|
citeMatch = item.ref.issued.year.toString();
|
|||
|
}
|
|||
|
const startPos = str.indexOf(citeMatch);
|
|||
|
const [start, rest] = split(str, startPos);
|
|||
|
output.push(start); // Irrelevant parts
|
|||
|
const url = `<a href="#bib-${id.toLowerCase()}">${rest.substring(0, citeMatch.length)}</a>`;
|
|||
|
output.push(url);
|
|||
|
str = rest.substr(citeMatch.length);
|
|||
|
}
|
|||
|
output.push(str);
|
|||
|
return [citationText, htmlToHast(`<span class="${(inlineClass != null ? inlineClass : []).join(' ')}" id=${ids}>${output.join('')}</span>`)];
|
|||
|
}
|
|||
|
} else {
|
|||
|
return [citationText, htmlToHast(`<span class="${(inlineClass != null ? inlineClass : []).join(' ')}" id=${ids}>${citationText}</span>`)];
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Generate bibliography in html and convert it to hast
|
|||
|
*
|
|||
|
* @param {*} citeproc
|
|||
|
*/
|
|||
|
const genBiblioNode = citeproc => {
|
|||
|
const [params, bibBody] = citeproc.makeBibliography();
|
|||
|
const bibliography = '<div id="refs" class="references csl-bib-body">\n' + bibBody.join('') + '</div>';
|
|||
|
const biblioNode = htmlToHast(bibliography);
|
|||
|
|
|||
|
// Add citekey id to each bibliography entry.
|
|||
|
biblioNode.children.filter(node => {
|
|||
|
var _node$properties;
|
|||
|
return (_node$properties = node.properties) == null || (_node$properties = _node$properties.className) == null ? void 0 : _node$properties.includes('csl-entry');
|
|||
|
}).forEach((node, i) => {
|
|||
|
const citekey = params.entry_ids[i][0].toLowerCase();
|
|||
|
node.properties = node.properties || {};
|
|||
|
node.properties.id = 'bib-' + citekey;
|
|||
|
});
|
|||
|
return biblioNode;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* @typedef {import('hast').Element} Element
|
|||
|
* @typedef {import('hast').ElementContent} ElementContent
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Create new footnote section node based on footnoteArray mappings
|
|||
|
*
|
|||
|
* @param {{int: string}} citationDict
|
|||
|
* @param {{type: 'citation' | 'existing', oldId: string}[]} footnoteArray
|
|||
|
* @param {Element | undefined} footnoteSection
|
|||
|
* @return {Element}
|
|||
|
*/
|
|||
|
const genFootnoteSection = (citationDict, footnoteArray, footnoteSection) => {
|
|||
|
/** @type {Element} */
|
|||
|
const list = {
|
|||
|
type: 'element',
|
|||
|
tagName: 'ol',
|
|||
|
properties: {},
|
|||
|
children: [{
|
|||
|
type: 'text',
|
|||
|
value: '\n'
|
|||
|
}]
|
|||
|
};
|
|||
|
let oldFootnoteList;
|
|||
|
if (footnoteSection) {
|
|||
|
/** @type {Element} */ // @ts-ignore - for some reason, the type does not narrow even after filtering
|
|||
|
oldFootnoteList = footnoteSection.children.filter(n => n.type == "element").find(n => n.tagName === 'ol');
|
|||
|
}
|
|||
|
for (const [idx, item] of footnoteArray.entries()) {
|
|||
|
const {
|
|||
|
type,
|
|||
|
oldId
|
|||
|
} = item;
|
|||
|
if (type === 'citation') {
|
|||
|
list.children.push({
|
|||
|
type: 'element',
|
|||
|
tagName: 'li',
|
|||
|
properties: {
|
|||
|
id: `user-content-fn-${idx + 1}`
|
|||
|
},
|
|||
|
children: [{
|
|||
|
type: 'element',
|
|||
|
tagName: 'p',
|
|||
|
properties: {},
|
|||
|
children: [htmlToHast(`<span>${citationDict[oldId]}</span>`), {
|
|||
|
type: 'element',
|
|||
|
tagName: 'a',
|
|||
|
properties: {
|
|||
|
href: `#user-content-fnref-${idx + 1}`,
|
|||
|
dataFootnoteBackref: true,
|
|||
|
className: ['data-footnote-backref'],
|
|||
|
ariaLabel: 'Back to content'
|
|||
|
},
|
|||
|
children: [{
|
|||
|
type: 'text',
|
|||
|
value: '↩'
|
|||
|
}]
|
|||
|
}]
|
|||
|
}, {
|
|||
|
type: 'text',
|
|||
|
value: '\n'
|
|||
|
}]
|
|||
|
});
|
|||
|
} else if (type === 'existing') {
|
|||
|
// @ts-ignore
|
|||
|
const liNode = oldFootnoteList.children.find(n => n.tagName === 'li' && n.properties.id === `user-content-fn-${oldId}`);
|
|||
|
liNode.properties.id = `user-content-fn-${idx + 1}`;
|
|||
|
const aNode = liNode.children[1].children.find(n => n.tagName === 'a');
|
|||
|
aNode.properties.href = `#user-content-fnref-${idx + 1}`;
|
|||
|
list.children.push(liNode);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/** @type {Element} */
|
|||
|
const newfootnoteSection = {
|
|||
|
type: 'element',
|
|||
|
tagName: 'section',
|
|||
|
properties: {
|
|||
|
dataFootnotes: true,
|
|||
|
className: ['footnotes']
|
|||
|
},
|
|||
|
children: [{
|
|||
|
type: 'element',
|
|||
|
tagName: 'h2',
|
|||
|
properties: {
|
|||
|
className: ['sr-only'],
|
|||
|
id: 'footnote-label'
|
|||
|
},
|
|||
|
children: [{
|
|||
|
type: 'text',
|
|||
|
value: 'Footnotes'
|
|||
|
}]
|
|||
|
}, {
|
|||
|
type: 'text',
|
|||
|
value: '\n'
|
|||
|
}, list]
|
|||
|
};
|
|||
|
return newfootnoteSection;
|
|||
|
};
|
|||
|
|
|||
|
const defaultCiteFormat = 'apa';
|
|||
|
const permittedTags = ['div', 'p', 'span', 'li', 'td', 'th'];
|
|||
|
const idRoot = 'CITATION';
|
|||
|
|
|||
|
/**
|
|||
|
* Rehype plugin that formats citations in markdown documents and insert bibliography in html format
|
|||
|
*
|
|||
|
* [-@wadler1990] --> (1990)
|
|||
|
* [@hughes1989, sec 3.4] --> (Hughes 1989, sec 3.4)
|
|||
|
* [see @wadler1990; and @hughes1989, pp. 4] --> (see Wadler 1990 and Hughes 1989, pp. 4)
|
|||
|
*
|
|||
|
* @param {*} Cite cite object from citation-js configured with the required CSLs
|
|||
|
* @return {import('unified').Plugin<[Options?], Root>}
|
|||
|
*/
|
|||
|
const rehypeCitationGenerator = Cite => {
|
|||
|
return (options = {}) => {
|
|||
|
return async (tree, file) => {
|
|||
|
var _file$data, _options$inlineBibCla;
|
|||
|
/** @type {string[]} */
|
|||
|
let bibtexFile = [];
|
|||
|
/** @type {string} */ // @ts-ignore
|
|||
|
const inputCiteformat = options.csl || (file == null || (_file$data = file.data) == null || (_file$data = _file$data.frontmatter) == null ? void 0 : _file$data.csl) || defaultCiteFormat;
|
|||
|
const inputLang = options.lang || 'en-US';
|
|||
|
const config = Cite.plugins.config.get('@csl');
|
|||
|
const citeFormat = await loadCSL(Cite, inputCiteformat, options.path);
|
|||
|
const lang = await loadLocale(Cite, inputLang, options.path);
|
|||
|
let bibliography = await getBibliography(options, file);
|
|||
|
if (bibliography.length === 0) {
|
|||
|
return;
|
|||
|
}
|
|||
|
for (let i = 0; i < bibliography.length; i++) {
|
|||
|
if (isValidHttpUrl(bibliography[i])) {
|
|||
|
const response = await fetch(bibliography[i]);
|
|||
|
bibtexFile.push(await response.text());
|
|||
|
} else {
|
|||
|
{
|
|||
|
bibtexFile.push(await readFile(bibliography[i]));
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
const citations = new Cite(bibtexFile);
|
|||
|
const citationIds = citations.data.map(x => x.id);
|
|||
|
const citationPre = [];
|
|||
|
const citationDict = {};
|
|||
|
let citationId = 1;
|
|||
|
const citeproc = config.engine(citations.data, citeFormat, lang, 'html');
|
|||
|
/** @type {Mode} */
|
|||
|
const mode = citeproc.opt.xclass;
|
|||
|
const citationFormat = getCitationFormat(citeproc);
|
|||
|
visit(tree, 'text', (node, idx, parent) => {
|
|||
|
const match = node.value.match(citationRE);
|
|||
|
if (!match || 'tagName' in parent && !permittedTags.includes(parent.tagName)) return;
|
|||
|
let citeStartIdx = match.index;
|
|||
|
let citeEndIdx = match.index + match[0].length;
|
|||
|
// If we have an in-text citation and we should suppress the author, the
|
|||
|
// match.index does NOT include the positive lookbehind, so we have to manually
|
|||
|
// shift "from" to one before.
|
|||
|
if (match[2] !== undefined) {
|
|||
|
citeStartIdx--;
|
|||
|
}
|
|||
|
const newChildren = [];
|
|||
|
// if preceding string
|
|||
|
if (citeStartIdx !== 0) {
|
|||
|
// create a new child node
|
|||
|
newChildren.push({
|
|||
|
type: 'text',
|
|||
|
value: node.value.slice(0, citeStartIdx)
|
|||
|
});
|
|||
|
}
|
|||
|
const [entries, isComposite] = parseCitation(match);
|
|||
|
|
|||
|
// If id is not in citation file (e.g. route alias or js package), abort process
|
|||
|
for (const citeItem of entries) {
|
|||
|
if (!citationIds.includes(citeItem.id)) return;
|
|||
|
}
|
|||
|
const [citedText, citedTextNode] = genCitation(citeproc, mode, entries, idRoot, citationId, citationPre, options, isComposite, citationFormat);
|
|||
|
citationDict[citationId] = citedText;
|
|||
|
|
|||
|
// Prepare citationPre and citationId for the next cite instance
|
|||
|
citationPre.push([`${idRoot}-${citationId}`, 0]);
|
|||
|
citationId = citationId + 1;
|
|||
|
newChildren.push(citedTextNode);
|
|||
|
|
|||
|
// if trailing string
|
|||
|
if (citeEndIdx < node.value.length) {
|
|||
|
newChildren.push({
|
|||
|
type: 'text',
|
|||
|
value: node.value.slice(citeEndIdx)
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// insert into the parent
|
|||
|
// @ts-ignore
|
|||
|
parent.children = [...parent.children.slice(0, idx), ...newChildren, ...parent.children.slice(idx + 1)];
|
|||
|
});
|
|||
|
if (options.noCite) {
|
|||
|
citeproc.updateItems(options.noCite.map(x => x.replace('@', '')));
|
|||
|
}
|
|||
|
if (citeproc.registry.mylist.length >= 1 && (!options.suppressBibliography || ((_options$inlineBibCla = options.inlineBibClass) == null ? void 0 : _options$inlineBibCla.length) > 0)) {
|
|||
|
const biblioNode = genBiblioNode(citeproc);
|
|||
|
let bilioInserted = false;
|
|||
|
const biblioMap = {};
|
|||
|
biblioNode.children.filter(node => {
|
|||
|
var _node$properties;
|
|||
|
return (_node$properties = node.properties) == null || (_node$properties = _node$properties.className) == null ? void 0 : _node$properties.includes('csl-entry');
|
|||
|
}).forEach(node => {
|
|||
|
const citekey = node.properties.id.split('-').slice(1).join('-');
|
|||
|
biblioMap[citekey] = _extends({}, node);
|
|||
|
biblioMap[citekey].properties = {
|
|||
|
id: 'inlinebib-' + citekey
|
|||
|
};
|
|||
|
});
|
|||
|
|
|||
|
// Insert it at ^ref, if not found insert it as the last element of the tree
|
|||
|
visit(tree, 'element', (node, idx, parent) => {
|
|||
|
var _options$inlineBibCla2, _node$properties2;
|
|||
|
// Add inline bibliography
|
|||
|
if (((_options$inlineBibCla2 = options.inlineBibClass) == null ? void 0 : _options$inlineBibCla2.length) > 0 && (_node$properties2 = node.properties) != null && (_node$properties2 = _node$properties2.id) != null && _node$properties2.toString().startsWith('citation-')) {
|
|||
|
// id is citation--nash1951--nash1950--1
|
|||
|
const [, ...citekeys] = node.properties.id.toString().split('--');
|
|||
|
const citationID = citekeys.pop();
|
|||
|
|
|||
|
/** @type {Element} */
|
|||
|
const inlineBibNode = {
|
|||
|
type: 'element',
|
|||
|
tagName: 'div',
|
|||
|
properties: {
|
|||
|
className: options.inlineBibClass,
|
|||
|
id: `inlineBib--${citekeys.join('--')}--${citationID}`
|
|||
|
},
|
|||
|
children: citekeys.map(citekey => {
|
|||
|
const aBibNode = biblioMap[citekey];
|
|||
|
aBibNode.properties = {
|
|||
|
class: 'inline-entry',
|
|||
|
id: `inline--${citekey}--${citationID}`
|
|||
|
};
|
|||
|
return aBibNode;
|
|||
|
})
|
|||
|
};
|
|||
|
parent.children.push(inlineBibNode);
|
|||
|
}
|
|||
|
|
|||
|
// Add bibliography
|
|||
|
if (!options.suppressBibliography && (node.tagName === 'p' || node.tagName === 'div') && node.children.length >= 1 && node.children[0].type === 'text' && node.children[0].value === '[^ref]') {
|
|||
|
parent.children[idx] = biblioNode;
|
|||
|
bilioInserted = true;
|
|||
|
}
|
|||
|
});
|
|||
|
if (!options.suppressBibliography && !bilioInserted) {
|
|||
|
tree.children.push(biblioNode);
|
|||
|
}
|
|||
|
}
|
|||
|
let footnoteSection;
|
|||
|
visit(tree, 'element', (node, index, parent) => {
|
|||
|
if (node.tagName === 'section' && node.properties.dataFootnotes) {
|
|||
|
footnoteSection = node;
|
|||
|
parent.children.splice(index, 1);
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// Need to adjust footnote numbering based on existing ones already assigned
|
|||
|
// And insert them into the footnote section (if exists)
|
|||
|
// Footnote comes after bibliography
|
|||
|
if (mode === 'note' && Object.keys(citationDict).length > 0) {
|
|||
|
/** @type {{type: 'citation' | 'existing', oldId: string}[]} */
|
|||
|
let fnArray = [];
|
|||
|
let index = 1;
|
|||
|
visit(tree, 'element', node => {
|
|||
|
if (node.tagName === 'sup' && node.children[0].type === 'element') {
|
|||
|
let nextNode = node.children[0];
|
|||
|
if (nextNode.tagName === 'a') {
|
|||
|
/** @type {{href: string, id: string}} */ // @ts-ignore
|
|||
|
const {
|
|||
|
href,
|
|||
|
id
|
|||
|
} = nextNode.properties;
|
|||
|
if (href.includes('fn') && id.includes('fnref')) {
|
|||
|
const oldId = href.split('-').pop();
|
|||
|
fnArray.push({
|
|||
|
type: href.includes('cite') ? 'citation' : 'existing',
|
|||
|
oldId
|
|||
|
});
|
|||
|
// Update ref number
|
|||
|
nextNode.properties.href = `#user-content-fn-${index}`;
|
|||
|
nextNode.properties.id = `user-content-fnref-${index}`;
|
|||
|
// @ts-ignore
|
|||
|
nextNode.children[0].value = index.toString();
|
|||
|
index += 1;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
// @ts-ignore
|
|||
|
const newFootnoteSection = genFootnoteSection(citationDict, fnArray, footnoteSection);
|
|||
|
tree.children.push(newFootnoteSection);
|
|||
|
} else {
|
|||
|
if (footnoteSection) tree.children.push(footnoteSection);
|
|||
|
}
|
|||
|
};
|
|||
|
};
|
|||
|
};
|
|||
|
|
|||
|
export { rehypeCitationGenerator as default };
|
|||
|
//# sourceMappingURL=generator.mjs.map
|