Add support for annotation templates (#2359)

This commit is contained in:
Martynas Bagdonas 2022-02-18 21:38:36 +02:00 committed by GitHub
parent 7606c88e79
commit 5405da99db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 160 additions and 20 deletions

View file

@ -44,7 +44,7 @@ const DOWNLOADED_IMAGE_TYPE = [
];
// Schema version here has to be the same as in note-editor!
const SCHEMA_VERSION = 5;
const SCHEMA_VERSION = 6;
class EditorInstance {
constructor() {
@ -301,7 +301,7 @@ class EditorInstance {
walkFormat(doc.body);
return doc.body.innerHTML;
}
/**
* @param {Object[]} annotations JSON annotations
* @param {Boolean} skipEmbeddingItemData Do not add itemData to citation items
@ -325,6 +325,7 @@ class EditorInstance {
let citationHTML = '';
let imageHTML = '';
let highlightHTML = '';
let quotedHighlightHTML = '';
let commentHTML = '';
let storedAnnotation = {
@ -389,23 +390,40 @@ class EditorInstance {
// Text
if (annotation.text) {
let text = this._transformTextToHTML(annotation.text.trim());
highlightHTML = `<span class="highlight" data-annotation="${encodeURIComponent(JSON.stringify(storedAnnotation))}">“${text}”</span>`;
highlightHTML = `<span class="highlight" data-annotation="${encodeURIComponent(JSON.stringify(storedAnnotation))}">${text}</span>`;
quotedHighlightHTML = `<span class="highlight" data-annotation="${encodeURIComponent(JSON.stringify(storedAnnotation))}">${Zotero.getString('punctuation.openingQMark')}${text}${Zotero.getString('punctuation.closingQMark')}</span>`;
}
// Note
if (annotation.comment) {
let comment = this._transformTextToHTML(annotation.comment.trim());
// Move comment to the next line if it has multiple lines
commentHTML = (((highlightHTML || imageHTML || citationHTML) && comment.includes('<br')) ? '<br/>' : ' ') + comment;
commentHTML = this._transformTextToHTML(annotation.comment.trim());
}
if (citationHTML) {
// Move citation to the next line if highlight has multiple lines or is after image
citationHTML = ((highlightHTML && highlightHTML.includes('<br') || imageHTML) ? '<br>' : '') + citationHTML;
let template;
if (annotation.type === 'highlight') {
template = Zotero.Prefs.get('annotations.noteTemplates.highlight');
}
let otherHTML = [highlightHTML, citationHTML, commentHTML].filter(x => x).join(' ');
html += '<p>' + imageHTML + otherHTML + '</p>\n';
else if (annotation.type === 'note') {
template = Zotero.Prefs.get('annotations.noteTemplates.note');
}
else if (annotation.type === 'image') {
template = '<p>{{image}}<br/>{{citation}} {{comment}}</p>';
}
let vars = {
color: annotation.color,
highlight: (attrs) => attrs.quotes === 'true' ? quotedHighlightHTML : highlightHTML,
comment: commentHTML,
citation: citationHTML,
image: imageHTML,
tags: (attrs) => annotation.tags && annotation.tags.map(tag => tag.name).join(attrs.join || ' ')
};
let templateHTML = Zotero.Utilities.Internal.generateHTMLFromTemplate(template, vars);
// Remove some spaces at the end of paragraph
templateHTML = templateHTML.replace(/([\s]*)(<\/p)/g, '$2');
// Remove multiple spaces
templateHTML = templateHTML.replace(/\s\s+/g, ' ');
html += templateHTML;
}
return { html, citationItems: storedCitationItems };
}
@ -1464,9 +1482,15 @@ class EditorInstance {
jsonAnnotation.id = annotation.key;
jsonAnnotations.push(jsonAnnotation);
}
let html = `<h1>${Zotero.getString('pdfReader.annotations')}<br/>`
+ Zotero.getString('noteEditor.annotationsDateLine', new Date().toLocaleString())
+ `</h1>\n`;
let vars = {
title: Zotero.getString('pdfReader.annotations'),
date: new Date().toLocaleString()
};
let html = Zotero.Utilities.Internal.generateHTMLFromTemplate(Zotero.Prefs.get('annotations.noteTemplates.title'), vars);
// New line is needed for note title parser
html += '\n';
let { html: serializedHTML, citationItems } = await editorInstance._serializeAnnotations(jsonAnnotations, true);
html += serializedHTML;
citationItems = encodeURIComponent(JSON.stringify(citationItems));

View file

@ -2167,6 +2167,95 @@ Zotero.Utilities.Internal = {
this._addListener(event, () => resolve(), true);
});
};
},
/**
* A basic templating engine
*
* - 'if' statement does case-insensitive string comparison
* - Spaces around '==' are necessary in 'if' statement
*
* Vars example:
* {
* color: '#ff6666',
* highlight: '<span class="highlight">This is a highlight</span>,
* comment: 'This is a comment',
* citation: '<span class="citation">(Author, 1900)</citation>',
* image: '<img src="…"/>',
* tags: (attrs) => ['tag1', 'tag2'].map(tag => tag.name).join(attrs.join || ' ')
* }
*
* Template example:
* {{if color == '#ff6666'}}
* <h2>{{highlight}}</h2>
* {{elseif color == '#2ea8e5'}}
* {{if comment}}<p>{{comment}}:</p>{{endif}}<blockquote>{{highlight}}</blockquote><p>{{citation}}</p>
* {{else}}
* <p>{{highlight}} {{citation}} {{comment}} {{if tags}} #{{tags join=' #'}}{{endif}}</p>
* {{endif}}
*
* @param {String} template
* @param {Object} vars
* @returns {String} HTML
*/
generateHTMLFromTemplate: function (template, vars) {
let levels = [{ condition: true }];
let html = '';
let parts = template.split(/{{|}}/);
for (let i = 0; i < parts.length; i++) {
let part = parts[i];
let level = levels[levels.length - 1];
if (i % 2 === 1) {
let operator = part.split(' ').filter(x => x)[0];
// Get arguments that are used for 'if'
let args = [];
let match = part.match(/(["'][^"|^']+["']|[^\s"']+)/g);
if (match) {
args = match.map(x => x.replace(/['"](.*)['"]/, '$1')).slice(1);
}
if (operator === 'if') {
level = { condition: false, executed: false, parentCondition: levels[levels.length-1].condition };
levels.push(level);
}
if (['if', 'elseif'].includes(operator)) {
if (!level.executed) {
level.condition = level.parentCondition && (args[2] ? vars[args[0]].toLowerCase() == args[2].toLowerCase() : !!vars[args[0]]);
level.executed = level.condition;
}
else {
level.condition = false;
}
continue;
}
else if (operator === 'else') {
level.condition = level.parentCondition && !level.executed;
level.executed = level.condition;
continue;
}
else if (operator === 'endif') {
levels.pop();
continue;
}
if (level.condition) {
// Get attributes i.e. join=" #"
let attrsRegexp = new RegExp(/((\w*) *=+ *(['"])((\\\3|[^\3])*?)\3)|((\w*) *=+ *(\w*))/g);
let attrs = {};
while ((match = attrsRegexp.exec(part))) {
if (match[4]) {
attrs[match[2]] = match[4];
}
else {
attrs[match[7]] = match[8];
}
}
html += (typeof vars[operator] === 'function' ? vars[operator](attrs) : vars[operator]) || '';
}
}
else if (level.condition) {
html += part;
}
}
return html;
}
}

View file

@ -108,8 +108,8 @@ about.createdBy = %1$S is a project of %2$S and is developed by a [global commun
about.openSource = %S is [open-source software] and depends on many [exceptional open-source projects].
about.getInvolved = Want to help? [Get involved] today!
punctuation.openingQMark = "
punctuation.closingQMark = "
punctuation.openingQMark =
punctuation.closingQMark =
punctuation.colon = :
punctuation.ellipsis =

View file

@ -193,3 +193,8 @@ pref("extensions.zotero.translators.RIS.import.keepID", false);
// Retracted Items
pref("extensions.zotero.retractions.enabled", true);
pref("extensions.zotero.retractions.recentItems", "[]");
// Annotations
pref("extensions.zotero.annotations.noteTemplates.title", "<h1>{{title}}<br/>({{date}})</h1>");
pref("extensions.zotero.annotations.noteTemplates.highlight", "<p>{{highlight quotes='true'}} {{citation}} {{comment}}</p>");
pref("extensions.zotero.annotations.noteTemplates.note", "<p>{{citation}} {{comment}}</p>");

@ -1 +1 @@
Subproject commit 6ba8de80ff64558f7cc727355b69eaca0ae3dcad
Subproject commit a5ba3435011938a68799f6b169e2e89b574cfded

@ -1 +1 @@
Subproject commit 4f14537a31ed66152d99a949d554b8e7ef7f1260
Subproject commit c5b1bc4dfd88001f0e92ff82dc264f514eb00984

View file

@ -522,4 +522,26 @@ describe("Zotero.Utilities.Internal", function () {
});
});
});
describe("#generateHTMLFromTemplate()", function () {
it("should support variables with attributes", function () {
var vars = {
v1: '1',
v2: (pars) => pars.a1 + pars.a2 + pars.a3,
v3: () => undefined,
};
var template = `{{ v1}}{{v2 a1= 1 a2 =' 2' a3 = "3 "}}{{v3}}{{v4}}`;
var html = Zotero.Utilities.Internal.generateHTMLFromTemplate(template, vars);
assert.equal(html, '11 23 ');
});
it("should support nested 'if' statements", function () {
var vars = {
v1: '1',
v2: 'H',
};
var template = `{{if v1 == '1'}}yes1{{if x}}no{{elseif v2 == h }}yes2{{endif}}{{elseif v2 == 2}}no{{else}}no{{endif}} {{if v2 == 1}}not{{elseif x}}not{{else}}yes3{{ endif}}`;
var html = Zotero.Utilities.Internal.generateHTMLFromTemplate(template, vars);
assert.equal(html, 'yes1yes2 yes3');
});
});
})