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