Add support for annotation templates (#2359)
This commit is contained in:
parent
7606c88e79
commit
5405da99db
7 changed files with 160 additions and 20 deletions
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = …
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue