157 lines
4.9 KiB
JavaScript
157 lines
4.9 KiB
JavaScript
import config from '../config.js';
|
|
import { diacritics, commands, mathCommands, ligatures, fieldTypes } from '../input/constants.js';
|
|
const unicode = {};
|
|
for (const command in commands) {
|
|
unicode[commands[command]] = command;
|
|
}
|
|
for (const diacritic in diacritics) {
|
|
unicode[diacritics[diacritic]] = diacritic;
|
|
}
|
|
for (const ligature in ligatures) {
|
|
unicode[ligatures[ligature]] = ligature;
|
|
}
|
|
const mathUnicode = {};
|
|
for (const command in mathCommands) {
|
|
mathUnicode[mathCommands[command]] = command;
|
|
}
|
|
const UNSAFE_UNICODE = /[^a-zA-Z0-9\s!"#%&'()*+,\-./:;=?@[\]{}\u0300-\u0308\u030a-\u030c\u0332\u0323\u0327\u0328\u0361\u0326]/g;
|
|
const DIACRITIC_PATTERN = /.[\u0300-\u0308\u030a-\u030c\u0332\u0323\u0327\u0328\u0361\u0326]+/g;
|
|
const LONE_DIACRITIC_PATTERN = /[\u0300-\u0308\u030a-\u030c\u0332\u0323\u0327\u0328\u0361\u0326]/g;
|
|
const listDelimiters = {
|
|
separated: ',',
|
|
list: ' and '
|
|
};
|
|
const richTextMappings = {
|
|
i: '\\textit{',
|
|
b: '\\textbf{',
|
|
sc: '\\textsc{',
|
|
sup: '\\textsuperscript{',
|
|
sub: '\\textsubscript{',
|
|
'span style="font-variant:small-caps;"': '\\textsc{',
|
|
'span class="nocase"': '{'
|
|
};
|
|
function escapeCharacter(char) {
|
|
if (char in unicode) {
|
|
return unicode[char] in ligatures ? unicode[char] : `\\${unicode[char]}{}`;
|
|
} else if (char in mathUnicode) {
|
|
return `$\\${mathUnicode[char]}$`;
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
function escapeValue(value) {
|
|
if (!config.format.asciiOnly) {
|
|
return value;
|
|
}
|
|
return value.normalize('NFKD').replace(UNSAFE_UNICODE, char => escapeCharacter(char)).replace(DIACRITIC_PATTERN, match => Array.from(match).reduce((subject, diacritic) => `{\\${unicode[diacritic]} ${subject}}`)).replace(LONE_DIACRITIC_PATTERN, '');
|
|
}
|
|
function formatRichText(value) {
|
|
const closingTags = [];
|
|
let tokens = value.split(/<(\/?(?:i|b|sc|sup|sub|span)|span .*?)>/g);
|
|
tokens = tokens.map((token, index) => {
|
|
if (index % 2 === 0) {
|
|
return escapeValue(token);
|
|
} else if (token in richTextMappings) {
|
|
closingTags.push('/' + token.split(' ')[0]);
|
|
return richTextMappings[token];
|
|
} else if (token === closingTags[closingTags.length - 1]) {
|
|
closingTags.pop();
|
|
return '}';
|
|
} else {
|
|
return '';
|
|
}
|
|
});
|
|
return tokens.join('');
|
|
}
|
|
function formatName(name) {
|
|
if (name.family && !name.prefix && !name.given & !name.suffix) {
|
|
return name.family.includes(listDelimiters.list) ? name.family : `{${name.family}}`;
|
|
}
|
|
const parts = [''];
|
|
if (name.prefix && name.family) {
|
|
parts[0] += name.prefix + ' ';
|
|
}
|
|
if (name.family) {
|
|
parts[0] += name.family;
|
|
}
|
|
if (name.suffix) {
|
|
parts.push(name.suffix);
|
|
parts.push(name.given || '');
|
|
} else {
|
|
parts.push(name.given);
|
|
}
|
|
return escapeValue(parts.join(', ').trim());
|
|
}
|
|
function formatTitle(title) {
|
|
return formatRichText(title).split(/(:\s*)/).map((part, i) => i % 2 ? part : part.replace(/([^\\])\b([a-z]*[A-Z].*?)\b/g, '$1{$2}')).join('');
|
|
}
|
|
function formatSingleValue(value, valueType) {
|
|
switch (valueType) {
|
|
case 'title':
|
|
return formatTitle(value);
|
|
case 'literal':
|
|
return formatRichText(value.toString());
|
|
case 'name':
|
|
return formatName(value);
|
|
case 'verbatim':
|
|
case 'uri':
|
|
return value.toString();
|
|
default:
|
|
return escapeValue(value.toString());
|
|
}
|
|
}
|
|
function formatList(values, valueType, listType) {
|
|
const delimiter = listDelimiters[listType];
|
|
return values.map(value => {
|
|
const formatted = formatSingleValue(value, valueType);
|
|
return formatted.includes(delimiter) ? `{${formatted}}` : formatted;
|
|
}).join(delimiter);
|
|
}
|
|
function formatAnnotationValue(values) {
|
|
if (Array.isArray(values)) {
|
|
return values.map(value => escapeValue(value).replace(/([;,"])/g, '{$1}')).join(', ');
|
|
} else {
|
|
return '"' + escapeValue(values).replace(/(["])/g, '{$1}') + '"';
|
|
}
|
|
}
|
|
export function format(field, value) {
|
|
if (!(field in fieldTypes)) {
|
|
return formatSingleValue(value, 'verbatim');
|
|
}
|
|
const [listType, valueType] = fieldTypes[field];
|
|
if (listType in listDelimiters) {
|
|
return formatList(value, valueType, listType);
|
|
} else {
|
|
return formatSingleValue(value, valueType);
|
|
}
|
|
}
|
|
export function formatAnnotation(value) {
|
|
const annotations = [];
|
|
if (value.field) {
|
|
annotations.push('=' + formatAnnotationValue(value.field));
|
|
}
|
|
if (value.item) {
|
|
for (const [itemCount, itemValue] of Object.entries(value.item)) {
|
|
if (!itemValue) {
|
|
continue;
|
|
}
|
|
const i = parseInt(itemCount) + 1;
|
|
annotations.push(i + '=' + formatAnnotationValue(itemValue));
|
|
}
|
|
}
|
|
if (value.part) {
|
|
for (const [itemCount, itemValue] of Object.entries(value.part)) {
|
|
if (!itemValue) {
|
|
continue;
|
|
}
|
|
const i = parseInt(itemCount) + 1;
|
|
for (const part in itemValue) {
|
|
if (!itemValue[part]) {
|
|
continue;
|
|
}
|
|
annotations.push(i + ':' + part + '=' + formatAnnotationValue(itemValue[part]));
|
|
}
|
|
}
|
|
}
|
|
return annotations.join('; ');
|
|
} |