PageRenderTime 75ms CodeModel.GetById 8ms app.highlight 60ms RepoModel.GetById 1ms app.codeStats 0ms

/BibLaTeX.js

https://code.google.com/p/zotero-biblatex-export/
JavaScript | 600 lines | 496 code | 57 blank | 47 comment | 169 complexity | 918e924dea911a8514ad9086d9007546 MD5 | raw file
  1{
  2    "translatorID":"ba4cd274-f24e-42cf-8ff2-ccfc603aacf3",
  3    "translatorType":2,
  4    "label":"BibLaTeX",
  5    "creator":"Simon Kornblith, Richard Karnesky and Anders Johansson",
  6    "target":"bib",
  7    "minVersion":"2.1.9",
  8    "maxVersion":"null",
  9    "priority":100,
 10    "inRepository":false,
 11    "configOptions":{"getCollections":"true"},
 12    "displayOptions": {
 13		"exportCharset": "UTF-8",
 14		"exportNotes": false,
 15		"exportFileData": false,
 16		"useJournalAbbreviation": false
 17    },
 18    "lastUpdated":"2013-01-04 18:00"
 19}
 20
 21
 22//%a = first author surname
 23//%y = year
 24//%t = first word of title
 25var citeKeyFormat = "%a_%t_%y";
 26
 27
 28var fieldMap = {
 29    location:"place",
 30    chapter:"chapter",
 31    edition:"edition",
 32    title:"title",
 33    volume:"volume",
 34    rights:"rights", //it's rights in zotero nowadays
 35    isbn:"ISBN",
 36    issn:"ISSN",
 37    url:"url",
 38    doi:"DOI",
 39    shorttitle:"shortTitle",
 40    abstract:"abstract",
 41    volumes:"numberOfVolumes",
 42    version:"version",
 43    eventtitle:"conferenceName",
 44    language:"language",
 45    issue:"issue",
 46    pages:"pages",
 47    pagetotal:"numPages"
 48};
 49//more conversions done below with special rules
 50
 51
 52//POTENTIAL ISSUES
 53//"programTitle", "bookTitle" //TODO, check!!
 54// 
 55//	accessDate:"accessDate", //only written on attached webpage snapshots by zo
 56//	journalAbbreviation:"journalAbbreviation", //not supported by bl
 57
 58//	country:"country", //TODO if patent, should be put into 'location' 
 59
 60
 61
 62var zotero2biblatexTypeMap = {
 63    "book":"book",
 64    "bookSection":"inbook",
 65    "journalArticle":"article",
 66    "magazineArticle":"article",
 67    "newspaperArticle":"article",
 68    "thesis":"thesis",
 69    "letter":"letter",
 70    "manuscript":"unpublished",
 71    "interview":"misc",
 72    "film":"movie",
 73    "artwork":"artwork",
 74    "webpage":"online",
 75    "conferencePaper":"inproceedings",
 76    "report":"report",
 77    "bill":"legislation",
 78    "case":"jurisdiction",
 79    "hearing":"jurisdiction",
 80    "patent":"patent",
 81    "statute":"legislation",
 82    "email":"letter",
 83    "map":"misc",
 84    "blogPost":"online",
 85    "instantMessage":"misc",
 86    "forumPost":"online",
 87    "audioRecording":"audio",
 88    "presentation":"unpublished",
 89    "videoRecording":"video",
 90    "tvBroadcast":"misc",
 91    "radioBroadcast":"misc",
 92    "podcast":"audio",
 93    "computerProgram":"software",
 94    "document":"misc",
 95    "encyclopediaArticle":"inreference",
 96    "dictionaryEntry":"inreference"
 97};
 98
 99
100var alwaysMap = {
101	"|":"{\\textbar}",
102	"<":"{\\textless}",
103	">":"{\\textgreater}",
104	"~":"{\\textasciitilde}",
105	"^":"{\\textasciicircum}",
106	"\\":"{\\textbackslash}",
107	"{" : "\\{",
108	"}" : "\\}"
109};
110
111
112
113
114// some fields are, in fact, macros.  If that is the case then we should not put the
115// data in the braces as it will cause the macros to not expand properly
116function writeField(field, value, isMacro, noEscape) {
117	if(!value && typeof value != "number") return;
118	value = value + ""; // convert integers to strings
119	Zotero.write(",\n\t" + field + " = ");
120	if (!isMacro) Zotero.write("{");
121	// url field is preserved, for use with \href and \url
122	// Other fields (DOI?) may need similar treatment
123	if (!noEscape && !isMacro && !(field == "url" || field == "doi" || field == "file" || field == "lccn")) {
124		//var titleCase = isTitleCase(value);	//figure this out before escaping all the characters
125		// I hope these are all the escape characters! (except for < > which are handled later)
126		value = value.replace(/[|\~\^\\\{\}]/g, mapEscape).replace(/[\#\$\%\&\_]/g, "\\$&");
127		//convert the HTML markup allowed in Zotero for rich text to TeX
128		value = mapHTMLmarkup(value);
129		//escape < > if mapHTMLmarkup did not convert some
130		value = value.replace(/[<>]/g, mapEscape);
131		
132		/*
133		if (field == "title" || field == "type" || field == "shorttitle" || field == "booktitle" || field == "series") {
134			if (!titleCase) {
135				//protect caps for everything but the first letter
136					value = value.replace(/(.)([A-Z]+)/g, "$1{$2}");
137			} else {	//protect all-caps vords and initials
138				value = value.replace(/([\s.->])([A-Z]+)(?=\.)/g, "$1{$2}");	//protect initials
139				if(value.toUpperCase() != value) value = value.replace(/([\s>])([A-Z]{2,})(?=[\.,\s<]|$)/g, "$1{$2}");
140			}
141		}
142		*/	
143			
144		// Case of words with uppercase characters in non-initial positions is preserved with braces.
145		// treat hyphen as whitespace for this purpose so that Large-scale etc. don't get enclosed
146		// treat curly bracket as whitespace because of mark-up immediately preceding word
147		// treat opening parentheses &brackets as whitespace
148		if (field != "pages") {
149			value = value.replace(/([^\s-\}\(\[]+[A-Z][^\s,]*)/g, "{$1}");
150		}
151	}
152	//we write utf8
153	//convert the HTML markup allowed in Zotero for rich text to TeX; excluding doi/url/file shouldn't be necessary, but better to be safe;
154	if (!((field == "url") || (field == "doi") || (field == "file"))) value = mapHTMLmarkup(value);
155	Zotero.write(value);
156	if (!isMacro) Zotero.write("}");
157}
158
159function mapHTMLmarkup(characters){
160	//converts the HTML markup allowed in Zotero for rich text to TeX
161	//since  < and > have already been escaped, we need this rather hideous code - I couldn't see a way around it though.
162	//italics and bold
163	characters = characters.replace(/\{\\textless\}i\{\\textgreater\}(((?!\{\\textless\}\/i{\\textgreater\}).)+)\{\\textless\}\/i{\\textgreater\}/, "\\textit{$1}").replace(/\{\\textless\}b\{\\textgreater\}(((?!\{\\textless\}\/b{\\textgreater\}).)+)\{\\textless\}\/b{\\textgreater\}/g, "\\textbf{$1}");
164	//sub and superscript
165	characters = characters.replace(/\{\\textless\}sup\{\\textgreater\}(((?!\{\\textless\}\/sup\{\\textgreater\}).)+)\{\\textless\}\/sup{\\textgreater\}/g, "\$^{\\textrm{$1}}\$").replace(/\{\\textless\}sub\{\\textgreater\}(((?!\{\\textless\}\/sub\{\\textgreater\}).)+)\{\\textless\}\/sub\{\\textgreater\}/g, "\$_{\\textrm{$1}}\$");
166	//two variants of small caps
167	characters = characters.replace(/\{\\textless\}span\sstyle=\"small\-caps\"\{\\textgreater\}(((?!\{\\textless\}\/span\{\\textgreater\}).)+)\{\\textless\}\/span{\\textgreater\}/g, "\\textsc{$1}").replace(/\{\\textless\}sc\{\\textgreater\}(((?!\{\\textless\}\/sc\{\\textgreater\}).)+)\{\\textless\}\/sc\{\\textgreater\}/g, "\\textsc{$1}");
168	return characters;
169}
170
171
172//Disable the isTitleCase function until we decide what to do with it.
173/* const skipWords = ["but", "or", "yet", "so", "for", "and", "nor",
174	"a", "an", "the", "at", "by", "from", "in", "into", "of", "on",
175	"to", "with", "up", "down", "as", "while", "aboard", "about",
176	"above", "across", "after", "against", "along", "amid", "among",
177	"anti", "around", "as", "before", "behind", "below", "beneath",
178	"beside", "besides", "between", "beyond", "but", "despite",
179	"down", "during", "except", "for", "inside", "like", "near",
180	"off", "onto", "over", "past", "per", "plus", "round", "save",
181	"since", "than", "through", "toward", "towards", "under",
182	"underneath", "unlike", "until", "upon", "versus", "via",
183	"within", "without"];
184
185function isTitleCase(string) {
186	const wordRE = /[\s[(]([^\s,\.:?!\])]+)/g;
187
188	var word;
189	while (word = wordRE.exec(string)) {
190		word = word[1];
191		if(word.search(/\d/) != -1	//ignore words with numbers (including just numbers)
192			|| skipWords.indexOf(word.toLowerCase()) != -1) {
193			continue;
194		}
195
196		if(word.toLowerCase() == word) return false;
197	}
198	return true;
199}
200*/
201
202function mapEscape(character) {
203	return alwaysMap[character];
204}
205
206
207// a little substitution function for BibTeX keys, where we don't want LaTeX 
208// escaping, but we do want to preserve the base characters
209
210function tidyAccents(s) {
211	var r=s.toLowerCase();
212
213	// XXX Remove conditional when we drop Zotero 2.1.x support
214	// This is supported in Zotero 3.0 and higher
215	if (ZU.removeDiacritics !== undefined)
216		r = ZU.removeDiacritics(r, true);
217	else {
218	// We fall back on the replacement list we used previously
219		r = r.replace(new RegExp("[ä]", 'g'),"ae");
220		r = r.replace(new RegExp("[ö]", 'g'),"oe");
221		r = r.replace(new RegExp("[ü]", 'g'),"ue");
222		r = r.replace(new RegExp("[àáâãå]", 'g'),"a");
223		r = r.replace(new RegExp("æ", 'g'),"ae");
224		r = r.replace(new RegExp("ç", 'g'),"c");
225		r = r.replace(new RegExp("[èéêë]", 'g'),"e");
226		r = r.replace(new RegExp("[ìíîï]", 'g'),"i");
227		r = r.replace(new RegExp("ñ", 'g'),"n");                            
228		r = r.replace(new RegExp("[òóôõ]", 'g'),"o");
229		r = r.replace(new RegExp("œ", 'g'),"oe");
230		r = r.replace(new RegExp("[ùúû]", 'g'),"u");
231		r = r.replace(new RegExp("[ýÿ]", 'g'),"y");
232	}
233
234	return r;
235};
236
237var numberRe = /^[0-9]+/;
238// Below is a list of words that should not appear as part of the citation key
239// in includes the indefinite articles of English, German, French and Spanish, as well as a small set of English prepositions whose 
240// force is more grammatical than lexical, i.e. which are likely to strike many as 'insignificant'.
241// The assumption is that most who want a title word in their key would prefer the first word of significance.
242var citeKeyTitleBannedRe = /\b(a|an|the|some|from|on|in|to|of|do|with|der|die|das|ein|eine|einer|eines|einem|einen|un|une|la|le|l\'|el|las|los|al|uno|una|unos|unas|de|des|del|d\')(\s+|\b)/g;
243var citeKeyConversionsRe = /%([a-zA-Z])/;
244var citeKeyCleanRe = /[^a-z0-9\!\$\&\*\+\-\.\/\:\;\<\>\?\[\]\^\_\`\|]+/g;
245
246var citeKeyConversions = {
247	"a":function (flags, item) {
248		if(item.creators && item.creators[0] && item.creators[0].lastName) {
249			return item.creators[0].lastName.toLowerCase().replace(/ /g,"_").replace(/,/g,"");
250		}
251		return "";
252	},
253	"t":function (flags, item) {
254		if (item["title"]) {
255			return item["title"].toLowerCase().replace(citeKeyTitleBannedRe, "").split(/\s+/g)[0];
256		}
257		return "";
258	},
259	"y":function (flags, item) {
260		if(item.date) {
261			var date = Zotero.Utilities.strToDate(item.date);
262			if(date.year && numberRe.test(date.year)) {
263				return date.year;
264			}
265		}
266		return "????";
267	}
268}
269
270
271function buildCiteKey (item,citekeys) {
272    var basekey = "";
273    var counter = 0;
274    citeKeyFormatRemaining = citeKeyFormat;
275    while (citeKeyConversionsRe.test(citeKeyFormatRemaining)) {
276        if (counter > 100) {
277            Zotero.debug("Pathological BibTeX format: " + citeKeyFormat);
278            break;
279        }
280        var m = citeKeyFormatRemaining.match(citeKeyConversionsRe);
281        if (m.index > 0) {
282            //add data before the conversion match to basekey
283            basekey = basekey + citeKeyFormatRemaining.substr(0, m.index);
284        }
285        var flags = ""; // for now
286        var f = citeKeyConversions[m[1]];
287        if (typeof(f) == "function") {
288            var value = f(flags, item);
289            Zotero.debug("Got value " + value + " for %" + m[1]);
290            //add conversion to basekey
291            basekey = basekey + value;
292        }
293        citeKeyFormatRemaining = citeKeyFormatRemaining.substr(m.index + m.length);
294        counter++;
295    }
296    if (citeKeyFormatRemaining.length > 0) {
297        basekey = basekey + citeKeyFormatRemaining;
298    }
299
300    // for now, remove any characters not explicitly known to be allowed;
301    // we might want to allow UTF-8 citation keys in the future, depending
302    // on implementation support.
303    //
304    // no matter what, we want to make sure we exclude
305    // " # % ' ( ) , = { } ~ and backslash
306    // however, we want to keep the base characters 
307
308    basekey = tidyAccents(basekey);
309    basekey = basekey.replace(citeKeyCleanRe, "");
310    var citekey = basekey;
311    var i = 0;
312    while(citekeys[citekey]) {
313        i++;
314        citekey = basekey + "-" + i;
315    }
316    citekeys[citekey] = true;
317    return citekey;
318}
319
320function doExport() {
321    //Zotero.write("% biblatex export generated by Zotero "+Zotero.Utilities.getVersion());
322    // to make sure the BOM gets ignored
323    Zotero.write("\n");
324    
325    var first = true;
326    var citekeys = new Object();
327    var item;
328    while(item = Zotero.nextItem()) {
329		//don't export standalone notes and attachments
330		if(item.itemType == "note" || item.itemType == "attachment") continue;
331
332	// determine type
333	var type = zotero2biblatexTypeMap[item.itemType];
334	if (typeof(type) == "function") { type = type(item); }
335	if(!type) type = "misc";
336	
337	// create a unique citation key
338	var citekey = buildCiteKey(item, citekeys);
339	
340	// write citation key (removed the comma)
341	Zotero.write((first ? "" : "\n\n") + "@"+type+"{"+citekey);
342	first = false;
343	
344	for(var field in fieldMap) {
345	    if(item[fieldMap[field]]) {
346		writeField(field, item[fieldMap[field]]);
347	    }
348	}
349
350	// Fields needing special treatment and not easily translatable via fieldMap
351	//e.g. where fieldname translation is dependent upon type, or special transformations
352	//has to be made
353
354	//all kinds of numbers (biblatex has additional support for journal number != issue, but zotero has not)
355	if(item.reportNumber || item.seriesNumber || item.patentNumber || item.billNumber || item.episodeNumber || item.number) {
356	    writeField("number", item.reportNumber || item.seriesNumber || item.patentNumber || item.billNumber || item.episodeNumber|| item.number);
357	}
358
359
360	if(item.publicationTitle) {
361	    if(item.itemType == "bookSection" || item.itemType == "conferencePaper") {
362		writeField("booktitle", item.publicationTitle); 
363	    } else if (item.itemType == "magazineArticle" || item.itemType == "newspaperArticle"){
364                writeField("journaltitle", item.publicationTitle);
365	    } else if (item.itemType == "journalArticle") {
366                if(Zotero.getOption("useJournalAbbreviation")){
367		    writeField("journal", item.journalAbbreviation);
368                } else {
369                    writeField("journaltitle", item.publicationTitle);
370                    writeField("shortjournal", item.journalAbbreviation);
371                }
372            }
373
374	   // else if (item.itemType == "website" || item.itemType == "forumPost" || item.itemType == "blogPost" || item.itemType == "tvBroadcast" || item.itemType == "radioBroadcast") {
375	//	writeField("titleaddon", item.publicationTitle);
376	// 
377		//do nothing as websiteTitle, forumTitle, blogTitle,
378		//programTitle seems
379		//to be just aliases for publicationTitle and already
380		//are correctly mapped below
381//	    }
382	    else { 
383		//writeField("journaltitle", item.publicationTitle);
384		//TODO, did we miss something
385	    }
386	}
387	
388
389    //TODO: check what happens to bookTitle, is that also an alias for publicationTitle?
390
391	
392	if(item.encyclopediaTitle || item.dictionaryTitle || item.proceedingsTitle) {
393	    writeField("booktitle",item.encyclopediaTitle || item.dictionaryTitle || item.proceedingsTitle);
394	}
395
396	if(item.websiteTitle || item.forumTitle || item.blogTitle || item.programTitle) {
397	    writeField("titleaddon", item.websiteTitle || item.forumTitle || item.blogTitle || item.programTitle);
398	}
399	
400	//don't really know if this is the best way
401	if(item.seriesTitle) {
402	    writeField("series",item.seriesTitle);
403	} else if(item.series) {
404	    writeField("series",item.series);
405	}
406
407	
408	if(item.publisher) {
409	    if(item.itemType == "thesis") {
410		writeField("school", item.publisher); //school is an acceptable alias in biblatex
411	    } else if(item.itemType =="report") {
412		writeField("institution", item.publisher);
413	    } else {
414		writeField("publisher", item.publisher);
415	    }
416	}
417	
418	//things concerning "type"
419	if(item.itemType == "letter"){
420	    if(item.letterType){
421		writeField("type",item.letterType);
422	    } else {
423		writeField("type","Letter"); //this isn't optimal, perhaps later versions of biblatex will add some suitable localization key
424	    }
425	} else if(item.itemType == "email"){
426	    writeField("type", "E-mail");
427	} else if(item.manuscriptType || item.thesisType || item.websiteType || item.presentationType || item.reportType || item.mapType) {
428	    writeField("type", item.manuscriptType || item.thesisType || item.websiteType || item.presentationType || item.reportType || item.mapType);
429	}
430
431	if(item.presentationType || item.manuscriptType){
432	    writeField("howpublished", item.presentationType || item.manuscriptType);
433	}
434
435	//case of specific eprint-archives in archive-fields
436	if(item.archive && item.archiveLocation) {
437	    if(item.archive == "arXiv" || item.archive == "arxiv") {
438		writeField("eprinttype", "arxiv");
439		writeField("eprint", item.archiveLocation);
440		if(item.callNumber) {//assume call number is used for arxiv class
441		    writeField("eprintclass", item.callNumber)
442		}
443	    } else if(item.archive = "JSTOR" || item.archive == "jstor") {
444		writeField("eprinttype", "jstor");
445		writeField("eprint", item.archiveLocation);
446	    } else if(item.archive = "PubMed" || item.archive == "pubmed") {
447		writeField("eprinttype", "pubmed");
448		writeField("eprint", item.archiveLocation);
449	    } else if(item.archive = "HDL" || item.archive == "hdl") {
450		writeField("eprinttype", "hdl");
451		writeField("eprint", item.archiveLocation);
452	    } else if(item.archive = "googlebooks" || item.archive == "Google Books") {
453		writeField("eprinttype", "googlebooks");
454		writeField("eprint", item.archiveLocation);
455	    }
456	}
457
458	
459	if(item.creators && item.creators.length) {
460	    // split creators into subcategories
461	    var author = "";
462	    var bookauthor = "";
463	    var commentator = "";
464	    var editor = "";
465	    var editora = "";
466	    var editorb = "";
467	    var holder = "";
468	    var translator = "";
469	    var noEscape = false;
470
471	    for each(var creator in item.creators) {
472		//var creatorString = creator.lastName;
473		
474		if (creator.firstName && creator.lastName) {
475		    creatorString = creator.lastName + ", " + creator.firstName;
476		//below to preserve possible corporate creators (biblatex 1.4a manual 2.3.3)
477		} else if (creator.fieldMode == true) { // fieldMode true, assume corporate author
478		    creatorString = "{" + creator.lastName + "}"; noEscape = true;
479		}
480
481		if (creator.creatorType == "author" || creator.creatorType == "interviewer" || creator.creatorType == "director" || creator.creatorType == "programmer" || creator.creatorType == "artist" || creator.creatorType == "podcaster" || creator.creatorType == "presenter") {
482		    author += " and "+creatorString;
483		} else if (creator.creatorType == "bookAuthor") {
484		    bookauthor += " and "+creatorString;
485		} else if (creator.creatorType == "commenter") {
486		    commentator += " and "+creatorString;
487		} else if (creator.creatorType == "editor") {
488		    editor += " and "+creatorString;
489		} else if (creator.creatorType == "inventor") {
490		    holder += " and "+creatorString;
491		} else if (creator.creatorType == "translator") {
492		    translator += " and "+creatorString;
493		} else if (creator.creatorType == "seriesEditor") {//let's call them redacors
494		    editorb = +" and "+creatorString;
495		} else {// the rest into editora with editoratype = collaborator
496		    editora += " and "+creatorString;
497		}
498	    }
499	    
500	    //remove first " and " string
501	    if(author) {
502		writeField("author", author.substr(5), false, noEscape);
503	    }
504	    if(bookauthor) {
505		writeField("bookauthor", bookauthor.substr(5), false, noEscape);
506	    }
507	    if(commentator) {
508		writeField("commentator", commenter.substr(5), false, noEscape); 
509	    }
510	    if(editor) {
511		writeField("editor", editor.substr(5), false, noEscape);
512	    }
513	    if(editora) {
514		writeField("editora", editora.substr(5), false, noEscape);
515		writeField("editoratype", "collaborator");
516	    }
517	    if(editorb) {
518		writeField("editorb", editorb.substr(5), false, noEscape);
519		writeField("editorbtype", "redactor");
520	    }
521	    if(holder) {
522		writeField("holder", holder.substr(5), false, noEscape);
523	    }
524	    if(translator) {
525		writeField("translator", translator.substr(5), false, noEscape);
526	    }
527
528
529	}
530
531	
532	if(item.accessDate){
533	    writeField("urldate",item.accessDate.substr(0,10));
534	}
535	//	} else if(item.dateAdded){
536	//		writeField("urldate",item.dateAdded.substr(0,10));
537	//	}
538
539	//TODO enable handling of date ranges when that's added to zotero
540	if(item.date) {
541	    writeField("date",item.date); //biblatex is smart, so this works (but not with ranges)
542	}
543	
544	
545	if(item.extra) {
546	    // this is for extracting fields not available in Zotero that one want's to use
547	    // in BibLaTex. Add the data in Zotero's 'extra' field e.g. like this and it
548	    // will be extracted:
549	    // biblatexdata[chapter=3;origyear=1909]
550	    if(item.extra.match("biblatexdata")){
551		var ex = item.extra.replace(/^.*biblatexdata\[|\].*$/g,"");
552		var blf = ex.split(";");
553		for each(var pair in blf){
554		    var ps=pair.split("=",2);
555		    writeField(ps[0],ps[1]);
556		}
557
558	    } else{
559		writeField("note", item.extra);
560	    }
561
562	}
563	
564	if(item.tags && item.tags.length) {
565	    var tagString = "";
566	    for each(var tag in item.tags) {
567		tagString += ", "+tag.tag;
568	    }
569	    writeField("keywords", tagString.substr(2));
570	}
571	
572	
573	if (item.notes && Zotero.getOption("exportNotes")) {
574	    for(var i in item.notes) {
575		var note = item.notes[i];
576		writeField("annote", Zotero.Utilities.unescapeHTML(note["note"]));
577	    }
578	}
579	
580	if(item.attachments) {
581	    var attachmentString = "";
582	    
583	    for(var i in item.attachments) {
584		var attachment = item.attachments[i];
585		if(Zotero.getOption("exportFileData") && attachment.saveFile) {
586		    attachment.saveFile(attachment.defaultPath, true);
587		    attachmentString += ";" + attachment.title + ":" + attachment.defaultPath + ":" + attachment.mimeType;
588		} else if(attachment.localPath) {
589		    attachmentString += ";" + attachment.title + ":" + attachment.localPath + ":" + attachment.mimeType;
590		}
591	    }
592	    
593	    if(attachmentString) {
594		writeField("file", attachmentString.substr(1));
595	    }
596	}
597	
598	Zotero.write("\n}");
599    }
600}