source: trunk/src/jlatexeditor/bib/BibAssistant.java @ 1453

Last change on this file since 1453 was 1453, checked in by joerg, 5 years ago

fix

File size: 21.6 KB
Line 
1package jlatexeditor.bib;
2
3import de.endrullis.utils.Pair;
4import jlatexeditor.JLatexEditorJFrame;
5import jlatexeditor.addon.RenameElement;
6import org.jetbrains.annotations.Nullable;
7import sce.codehelper.CodeAssistant;
8import sce.codehelper.PatternPair;
9import sce.codehelper.SCEPopup;
10import sce.codehelper.WordWithPos;
11import sce.component.*;
12import sce.syntaxhighlighting.ParserStateStack;
13
14import javax.swing.*;
15import javax.swing.event.DocumentEvent;
16import javax.swing.event.DocumentListener;
17import java.awt.*;
18import java.util.ArrayList;
19import java.util.Collections;
20import java.util.HashSet;
21import java.util.List;
22
23/**
24 * Assists with bibtex files.
25 *
26 * @author Joerg Endrullis <joerg@endrullis.de>
27 */
28public class BibAssistant implements CodeAssistant, SCEPopup.ItemHandler {
29  private JLatexEditorJFrame jle;
30
31  private PatternPair authorPattern = new PatternPair("(?:^| and )(?:(?! and ) )*((?:(?! and ).)*?)", true, "(.*?)(?:$| and$| and )");
32  private PatternPair entryTypePattern = new PatternPair("^\\@([^\\{])*");
33  private PatternPair entryNamePattern = new PatternPair("^\\@[^\\{]+\\{ *([^,\\{]*)", true, "([^, ]*)(?:$|,| )");
34
35  private String[] keyOrder = new String[] {
36          "author", "title",
37          "booktitle",
38          "journal", "volume", "number",
39          "editor",
40          "pages",
41          "institution",
42          "publisher", "series",
43          "month", "year",
44          "edition",
45          "note",
46          "address",
47          "isbn","issn","doi","ee"
48  };
49
50  public BibAssistant(JLatexEditorJFrame jle) {
51    this.jle = jle;
52  }
53
54  public boolean assistAt(SCEPane pane) {
55    SCEDocument document = pane.getDocument();
56    SCEDocumentRow[] rows = document.getRowsModel().getRows();
57    int row = pane.getCaret().getRow();
58    int column = pane.getCaret().getColumn();
59
60    ParserStateStack stateStack = BibSyntaxHighlighting.parseRow(rows[row], column, document, rows, true);
61    BibParserState state = (BibParserState) stateStack.peek();
62
63    if(column == 0 && state.getState() == BibParserState.STATE_NOTHING) {
64      // refactor the whole bib
65      return false;
66    }
67
68    if(state.getState() != BibParserState.STATE_NOTHING) {
69      BibEntry entry = state.getEntry();
70
71      for(BibKeyValuePair keyValue : entry.getAllParameters().values()) {
72        for(WordWithPos value : keyValue.getValues()) {
73          if(value.contains(row, column)) {
74            String key = keyValue.getKey().word.toLowerCase();
75
76            // selection
77            SCEDocumentPosition selectionStart = document.getSelectionStart();
78            SCEDocumentPosition selectionEnd = document.getSelectionEnd();
79            if(document.hasSelection()
80                    && !selectionStart.equals(selectionEnd)
81                    && value.contains(selectionStart.getRow(), selectionStart.getColumn())
82                    && value.contains(selectionEnd.getRow(), selectionEnd.getColumn())) {
83
84              popup(pane, document, new WordWithPos(document.getSelectedText(), selectionStart, selectionEnd), "");
85              return true;
86            }
87
88            // author
89            if(key.equals("author") || key.equals("editor")) {
90              List<WordWithPos> params = authorPattern.find(pane, BibKeyValuePair.getInnerStart(value), BibKeyValuePair.getInnerEnd(value));
91              if(params != null) {
92                WordWithPos name = params.get(0);
93                String nameKey = getAuthorString(name.word);
94
95                popup(pane, document, name, nameKey);
96                return true;
97              }
98            }
99
100            // other values
101            SCEDocumentPosition start;
102            SCEDocumentPosition end;
103            if(value.word.startsWith("\"") || value.word.startsWith("{")) {
104              start = new SCEDocumentPosition(value.getStartPos().getRow(), value.getStartPos().getColumn()+1);
105              end = new SCEDocumentPosition(value.getEndPos().getRow(), value.getEndPos().getColumn()-1);
106            } else {
107              start = new SCEDocumentPosition(value.getStartPos().getRow(), value.getStartPos().getColumn());
108              end = new SCEDocumentPosition(value.getEndPos().getRow(), value.getEndPos().getColumn());
109            }
110            popup(pane, document, new WordWithPos(document.getText(start, end), start, end), "");
111            return true;
112          }
113        }
114      }
115    }
116
117    { // entry name correction
118      List<WordWithPos> params = entryNamePattern.find(pane);
119      if(params != null) {
120        BibEntry entry = state.getEntryByNr().get(state.getEntryNr());
121        if(entry == null) return false;
122
123        String canonicalEntryName = BibAssistant.getCanonicalEntryName(entry);
124        if(canonicalEntryName.equals(entry.getName())) return false;
125
126        RenameElement.renameBibRef(jle, entry.getName(), canonicalEntryName);
127
128        return true;
129      }
130
131    }
132
133    { // entry position correction
134      List<WordWithPos> params = entryTypePattern.find(pane);
135      if(params != null) {
136        BibEntry entry = state.getEntryByNr().get(state.getEntryNr());
137        if(entry == null) return false;
138
139        List<Object> list = new ArrayList<Object>();
140        list.add(new ReformatEntry(entry, pane));
141
142        SCEPosition end = new SCEDocumentPosition(document.getRowsModel().getRowsCount()-1, 0);
143        BibEntry nextEntry = state.getEntryByNr().get(state.getEntryNr()+1);
144        if(nextEntry != null) end = nextEntry.getStartPos();
145
146        list.add(new MoveEntry(state.getEntryNr(), entry, end, pane));
147
148        // open popup
149        pane.getPopup().openPopup(list, this);
150
151        return true;
152      }
153    }
154
155    return false;
156  }
157
158  private void popup(SCEPane pane, SCEDocument document, WordWithPos word, String key) {
159    document.setSelectionRange(word.getStartPos(), word.getEndPos(), true);
160    pane.repaint();
161
162    // find similar string
163    ArrayList<WeightedElement<BibEntry>> weightedEntries = new ArrayList<WeightedElement<BibEntry>>();
164
165    ArrayList<BibEntry> stringEntries = getEntries(document, "string");
166    for(BibEntry stringEntry : stringEntries) {
167      if(stringEntry.getName().isEmpty()) continue;
168      String stringValue = stringEntry.getAllParameters().get(stringEntry.getName().toLowerCase()).getValuesString();
169
170      int common = lcs(word.word, stringValue);
171      weightedEntries.add(new WeightedElement<BibEntry>(common, stringEntry));
172    }
173
174    Collections.sort(weightedEntries);
175
176    List<Object> list = new ArrayList<Object>();
177    list.add(new CreateStringAction(word.word, key, pane));
178
179    for(int itemNr = 0; itemNr < 3; itemNr++) {
180      if(weightedEntries.isEmpty()) break;
181      BibEntry entry = weightedEntries.remove(weightedEntries.size()-1).element;
182      list.add(new ReplaceByAction(word.word, entry.getName(), entry.getAllParameters().get(entry.getName().toLowerCase()).getValuesString(), pane));
183    }
184
185    // open popup
186    pane.getPopup().openPopup(list, this);
187  }
188
189  private ArrayList<BibEntry> getEntries(SCEDocument document, @Nullable String type) {
190    ArrayList<BibEntry> entries = new ArrayList<BibEntry>();
191
192    SCEDocumentRow[] rows = document.getRowsModel().getRows();
193
194    BibParserState state = (BibParserState) rows[document.getRowsModel().getRowsCount()-1].parserStateStack.peek();
195    int entriesCount = state.getEntryNr();
196    for(int entryNr = 0; entryNr < entriesCount; entryNr++) {
197      BibEntry entry = state.getEntryByNr().get(entryNr);
198      if(type != null && !entry.getType(false).equalsIgnoreCase(type)) continue;
199      entries.add(entry);
200    }
201
202    return entries;
203  }
204
205  public static Pair<String,String> getFirstAndLast(String name) {
206    name = name.replace('~', ' ');
207    String pname = "";
208    while(pname.length() != name.length()) {
209      pname = name;
210      name = name.replaceFirst("\\\\.","");
211    }
212    name = name.replaceAll("[^\\w\\d\\., ]", "");
213    name = name.replaceAll(" +"," ").trim();
214
215    String firstName = "", lastName = "";
216
217    int comma = name.indexOf(',');
218    if(comma >= 0) {
219      lastName = name.substring(0, comma);
220      firstName = name.substring(comma+1);
221    } else {
222      String split[] = name.split("[ ](?=[^ ]*$)");
223      if(split.length == 1) {
224        lastName = split[0];
225      } else {
226        firstName = split[0];
227        lastName = split[1];
228      }
229    }
230
231    return new Pair<String, String>(firstName.trim(), lastName.trim());
232  }
233
234  public static String[] getValues(BibKeyValuePair keyValue) {
235    if(keyValue == null) return new String[0];
236
237    ArrayList<WordWithPos> valuesList = keyValue.getValues();
238    String[] values = new String[keyValue.getValues().size()];
239    for(int nr = 0; nr < values.length; nr++) {
240      values[nr] = valuesList.get(nr).word;
241    }
242    return values;
243  }
244
245  public static String getCanonicalEntryName(BibEntry entry) {
246    if(entry.getType(true).equalsIgnoreCase("string")) return "";
247
248    BibKeyValuePair author = entry.getAllParameters().get("author");
249    String nameString = unfold(getValues(author)).replace('\n', ' ').trim();
250    if(nameString.equals("")) {
251      BibKeyValuePair editor = entry.getAllParameters().get("editor");
252      nameString = unfold(getValues(editor)).replace('\n', ' ').trim();
253    }
254    String year = unfold(getValues(entry.getAllParameters().get("year")));
255    return BibAssistant.getCanonicalEntryName(nameString.split(" and "), year);
256  }
257
258  public static String getCanonicalEntryName(String[] names, String year) {
259    StringBuilder builder = new StringBuilder();
260
261    for(String name : names) {
262      String last = getFirstAndLast(name).snd;
263      for(String prefix : new String[] {"van ", "von ", "de ", "der "}) {
264        if(last.startsWith(prefix)) last = last.substring(prefix.length());
265      }
266      last = last.toLowerCase().replaceAll("[^\\w]","");
267      if(last.length() > 4) last = last.substring(0,4);
268      builder.append(last).append(":");
269    }
270    builder.append(year);
271
272    return builder.toString().trim();
273  }
274
275  public static String unfold(String[] values) {
276    return unfold(values, new StringBuilder(), 8);
277  }
278
279  public static String unfold(String[] values, StringBuilder builder, int depth) {
280    if(depth < 0 || values == null) return "";
281
282    for(String value : values) {
283      if(value.startsWith("\"") || value.startsWith("{")) {
284        builder.append(value.substring(1,value.length()-1));
285      } else
286      if(value.isEmpty() || Character.isDigit(value.charAt(0))) {
287        builder.append(value);
288      } else {
289        String[] nvalues = BibSyntaxHighlighting.stringMap.get(value.toLowerCase());
290        if(nvalues != null) {
291          unfold(nvalues, builder, depth-1);
292        } else {
293          builder.append(value);
294        }
295      }
296    }
297
298    return builder.toString();
299  }
300
301  public static String getAuthorString(String name) {
302    Pair<String,String> fs = getFirstAndLast(name);
303    String firstName = fs.fst.toLowerCase();
304    String lastName = fs.snd.toLowerCase();
305
306    lastName = lastName.replaceAll(" ","");
307    lastName = lastName.replaceAll("\\W","");
308    firstName = firstName.replaceAll("(\\w)\\w*", "$1");
309    firstName = firstName.replaceAll("\\W","");
310
311    return lastName + "." + firstName;
312  }
313
314  /**
315   * Longest common substring: http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Longest_common_substring
316   */
317  private int lcs(String x, String y) {
318    if (x == null || y == null || x.length() == 0 || y.length() == 0) {
319      return 0;
320    }
321
322    int maxLen = 0;
323    int xlength = x.length();
324    int ylength = y.length();
325    int[][] table = new int[xlength][ylength];
326
327    for (int i = 0; i < xlength; i++) {
328      for (int j = 0; j < ylength; j++) {
329        if (x.charAt(i) == y.charAt(j)) {
330          if (i == 0 || j == 0) {
331            table[i][j] = 1;
332          }
333          else {
334            table[i][j] = table[i - 1][j - 1] + 1;
335          }
336          if (table[i][j] > maxLen) {
337            maxLen = table[i][j];
338          }
339        }
340      }
341    }
342
343    return maxLen;
344  }
345
346  private String oneLine(String text) {
347    text = text.replace('\n', ' ');
348    text = text.replaceAll(" +", " ");
349    return text;
350  }
351
352  private void replaceAll(SCEDocument document, String search, String replaceKey) {
353    ArrayList<BibEntry> entries = getEntries(document, null);
354    for(BibEntry entry : entries) {
355      // do not replace in strings, until we have some clever algorithm for deciding when
356      if(entry.getType(false).equalsIgnoreCase("string")) continue;
357
358      for(BibKeyValuePair keyValues : entry.getAllParameters().values()) {
359        boolean oneLine = keyValues.getKey().word.equalsIgnoreCase("author");
360        for(WordWithPos value : keyValues.getValues()) {
361          if(!value.word.startsWith("\"") && !value.word.startsWith("{")) continue;
362
363          replaceIn(document, value, oneLine, search, replaceKey);
364        }
365      }
366    }
367  }
368
369  private void replaceIn(SCEDocument document, WordWithPos value, boolean oneLine, String search, String replaceKey) {
370    String text = value.word;
371    if(oneLine) text = oneLine(text);
372
373    if(!text.contains(search)) return;
374
375    if(text.startsWith("\"") || text.startsWith("{")) {
376      text = text.substring(1,text.length()-1);
377    }
378
379    StringBuilder builder = new StringBuilder();
380    int index;
381    while((index = text.indexOf(search)) >= 0) {
382      String between = text.substring(0, index);
383      if(!between.isEmpty()) {
384        if(builder.length() != 0) builder.append(" # ");
385        builder.append("{").append(between).append("}");
386      }
387      if(builder.length() != 0) builder.append(" # ");
388      builder.append(replaceKey);
389      text = text.substring(index + search.length());
390    }
391    if(!text.isEmpty()) {
392      if(builder.length() != 0) builder.append(" # ");
393      builder.append("{").append(text).append("}");
394    }
395
396
397    document.replace(value.getStartPos(), value.getEndPos(), builder.toString());
398  }
399
400  private HashSet<String> getEntryNames(SCEDocument document) {
401    HashSet<String> names = new HashSet<String>();
402    ArrayList<BibEntry> stringEntries = getEntries(document, null);
403    for(BibEntry stringEntry : stringEntries) {
404      names.add(stringEntry.getName());
405    }
406    return names;
407  }
408
409  public void perform(Object item) {
410    final String forbiddenCharacters = "[ \\{\\}]";
411
412    if(item instanceof CreateStringAction) {
413      CreateStringAction action = (CreateStringAction) item;
414      SCEPane pane = action.getPane();
415      SCEDocument document = pane.getDocument();
416
417      final HashSet<String> names = getEntryNames(document);
418      final JTextField textField = new JTextField(action.getKey());
419      textField.getDocument().addDocumentListener(new DocumentListener() {
420        public void insertUpdate(DocumentEvent e) {
421          changedUpdate(e);
422        }
423
424        public void removeUpdate(DocumentEvent e) {
425          changedUpdate(e);
426        }
427
428        public void changedUpdate(DocumentEvent e) {
429          String string = textField.getText();
430          string = string.replaceAll(forbiddenCharacters,"");
431
432          if(names.contains(string) || string.isEmpty()) {
433            textField.setBackground(new Color(255, 204, 204));
434          } else {
435            textField.setBackground(Color.WHITE);
436          }
437        }
438      });
439      textField.setColumns(60);
440
441      int ok = JOptionPane.showConfirmDialog(JLatexEditorJFrame.getFrames()[0], textField, "Enter Entry Name", JOptionPane.OK_CANCEL_OPTION);
442      if(ok == JOptionPane.OK_OPTION && textField.getBackground().equals(Color.WHITE)) {
443        String key = textField.getText().replaceAll(forbiddenCharacters,"");
444
445        document.removeSelection();
446        pane.repaint();
447        pane.setFreezeCaret(true);
448        replaceAll(document, action.getString(), key);
449        document.insert("@string{" + key + " = {" + action.getString() + "}}\n", 0, 0);
450        pane.setFreezeCaret(false);
451      }
452    }
453
454    if(item instanceof ReplaceByAction) {
455      ReplaceByAction action = (ReplaceByAction) item;
456      SCEPane pane = action.getPane();
457      SCEDocument document = pane.getDocument();
458
459      document.removeSelection();
460      pane.repaint();
461      pane.setFreezeCaret(true);
462      replaceAll(document, action.getString(), action.getReplaceKey());
463      pane.setFreezeCaret(false);
464    }
465
466    if(item instanceof ReformatEntry) {
467      ReformatEntry action = (ReformatEntry) item;
468
469      BibEntry entry = action.getEntry();
470      SCEPosition start = entry.getStartPos();
471      SCEPosition end = entry.getEndPos();
472
473      SCEDocument document = action.getPane().getDocument();
474
475      ArrayList<String> keys = new ArrayList<String>(entry.getAllParameters().keySet());
476      Collections.sort(keys);
477
478      int maxLength = 0;
479      for(String key : keys) maxLength = Math.max(key.length(), maxLength);
480
481      StringBuilder builder = new StringBuilder();
482      builder.append("@").append(entry.getType(false).toLowerCase());
483      builder.append("{").append(entry.getName()).append(",\n");
484      for(String akey : keyOrder) {
485        if(!keys.remove(akey)) continue;
486        appendKeyValue(builder, entry, akey, maxLength);
487      }
488      for(String akey : keys) {
489        appendKeyValue(builder, entry, akey, maxLength);
490      }
491
492      document.replace(start, end, builder.toString());
493    }
494
495    if(item instanceof MoveEntry) {
496      MoveEntry action = (MoveEntry) item;
497
498      BibEntry entry = action.getEntry();
499      SCEPosition start = entry.getStartPos();
500      SCEPosition end = action.getEnd();
501
502      SCEDocument document = action.getPane().getDocument();
503
504      String text = document.getText(start, end);
505      document.remove(start, end);
506
507      String name = getCanonicalEntryName(entry);
508      BibParserState state = (BibParserState) document.getRowsModel().getRows()[0].parserStateStack.peek();
509      for(int entryNr = 0; entryNr < action.getEntryNr(); entryNr++) {
510        BibEntry otherEntry = state.getEntryByNr().get(entryNr);
511        String otherName = getCanonicalEntryName(otherEntry);
512        if(name.compareTo(otherName) < 0) {
513          document.insert(text, otherEntry.getStartPos().getRow(), 0);
514        }
515      }
516    }
517  }
518
519  private void appendKeyValue(StringBuilder builder, BibEntry entry, String akey, int maxLength) {
520    builder.append("  ").append(akey);
521    for(int spaceNr = 0; spaceNr < maxLength - akey.length(); spaceNr++) {
522      builder.append(' ');
523    }
524    builder.append(" = ");
525    boolean firstValue = true;
526    BibKeyValuePair values = entry.getAllParameters().get(akey);
527    for(WordWithPos value : values.getValues()) {
528      if(!firstValue) builder.append(" # ");
529
530      String v = value.word.replace('\n',' ').replaceAll(" +", " ");
531
532      if(akey.equals("pages")) v = v.replaceAll("([^-])-([^-])", "$1--$2");
533
534      if(v.startsWith("\"") || v.startsWith("{")) {
535        builder.append("{").append(v.substring(1,v.length()-1)).append("}");
536      } else
537      if(v.isEmpty() || Character.isDigit(v.charAt(0))) {
538        builder.append("{").append(v).append("}");
539      } else {
540        builder.append(v.toLowerCase());
541      }
542
543      firstValue = false;
544    }
545    builder.append(",\n");
546  }
547
548// inner class
549
550  class CreateStringAction {
551    private String string;
552    private String key;
553    private SCEPane pane;
554
555    CreateStringAction(String string, String key, SCEPane pane) {
556      this.string = string;
557      this.key = key;
558      this.pane = pane;
559    }
560
561    public String getString() {
562      return string;
563    }
564
565    public String getKey() {
566      return key;
567    }
568
569    public SCEPane getPane() {
570      return pane;
571    }
572
573    @Override
574    public String toString() {
575      return "Create Entry";
576    }
577  }
578
579  class ReplaceByAction {
580    private String string;
581    private String replaceKey;
582    private String replaceValue;
583    private SCEPane pane;
584
585    ReplaceByAction(String string, String replaceKey, String replaceValue, SCEPane pane) {
586      this.string = string;
587      this.replaceKey = replaceKey;
588      this.replaceValue = replaceValue;
589      this.pane = pane;
590    }
591
592    public String getString() {
593      return string;
594    }
595
596    public String getReplaceKey() {
597      return replaceKey;
598    }
599
600    public String getReplaceValue() {
601      return replaceValue;
602    }
603
604    public SCEPane getPane() {
605      return pane;
606    }
607
608    @Override
609    public String toString() {
610      return "Replace by: " + replaceValue;
611    }
612  }
613
614  class ReformatEntry {
615    private BibEntry entry;
616    private SCEPane pane;
617
618    ReformatEntry(BibEntry entry, SCEPane pane) {
619      this.entry = entry;
620      this.pane = pane;
621    }
622
623    public BibEntry getEntry() {
624      return entry;
625    }
626
627    public SCEPane getPane() {
628      return pane;
629    }
630
631    @Override
632    public String toString() {
633      return "Reformat Entry";
634    }
635  }
636
637  class MoveEntry {
638    private int entryNr;
639    private BibEntry entry;
640    private SCEPosition end;
641    private SCEPane pane;
642
643    MoveEntry(int entryNr, BibEntry entry, SCEPosition end, SCEPane pane) {
644      this.entryNr = entryNr;
645      this.entry = entry;
646      this.end = end;
647      this.pane = pane;
648    }
649
650    public int getEntryNr() {
651      return entryNr;
652    }
653
654    public BibEntry getEntry() {
655      return entry;
656    }
657
658    public SCEPosition getEnd() {
659      return end;
660    }
661
662    public SCEPane getPane() {
663      return pane;
664    }
665
666    @Override
667    public String toString() {
668      return "Move Entry";
669    }
670  }
671
672  class WeightedElement<E> implements Comparable<WeightedElement<E>> {
673    private double weight;
674    private E element;
675
676    WeightedElement(double weight, E element) {
677      this.weight = weight;
678      this.element = element;
679    }
680
681    public double getWeight() {
682      return weight;
683    }
684
685    public void setWeight(double weight) {
686      this.weight = weight;
687    }
688
689    public E getElement() {
690      return element;
691    }
692
693    public void setElement(E element) {
694      this.element = element;
695    }
696
697    public String toString() {
698      return element.toString();
699    }
700
701    @Override
702    public int compareTo(WeightedElement<E> o) {
703      return new Double(weight).compareTo(o.weight);
704    }
705  }
706}
Note: See TracBrowser for help on using the repository browser.