]> git.lyx.org Git - lyx.git/blob - src/paragraph_pimpl.C
Change tracking:
[lyx.git] / src / paragraph_pimpl.C
1 /**
2  * \file paragraph_pimpl.C
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Lars Gullik Bjønnes
7  * \author Jean-Marc Lasgouttes
8  * \author John Levon
9  * \author André Pönitz
10  *
11  * Full author contact details are available in file CREDITS.
12  */
13
14 #include <config.h>
15
16 #include "paragraph_pimpl.h"
17 #include "paragraph.h"
18
19 #include "bufferparams.h"
20 #include "debug.h"
21 #include "encoding.h"
22 #include "language.h"
23 #include "LaTeXFeatures.h"
24 #include "LColor.h"
25 #include "lyxlength.h"
26 #include "lyxrc.h"
27 #include "outputparams.h"
28 #include "texrow.h"
29
30 #include <boost/next_prior.hpp>
31
32 using lyx::pos_type;
33
34 using std::endl;
35 using std::upper_bound;
36 using std::lower_bound;
37 using std::string;
38 using std::ostream;
39
40
41 // Initialization of the counter for the paragraph id's,
42 unsigned int Paragraph::Pimpl::paragraph_id = 0;
43
44 namespace {
45
46 struct special_phrase {
47         string phrase;
48         string macro;
49         bool builtin;
50 };
51
52 special_phrase special_phrases[] = {
53         { "LyX", "\\LyX{}", false },
54         { "TeX", "\\TeX{}", true },
55         { "LaTeX2e", "\\LaTeXe{}", true },
56         { "LaTeX", "\\LaTeX{}", true },
57 };
58
59 size_t const phrases_nr = sizeof(special_phrases)/sizeof(special_phrase);
60
61 } // namespace anon
62
63
64 Paragraph::Pimpl::Pimpl(Paragraph * owner)
65         : owner_(owner)
66 {
67         inset_owner = 0;
68         id_ = paragraph_id++;
69 }
70
71
72 Paragraph::Pimpl::Pimpl(Pimpl const & p, Paragraph * owner)
73         : params(p.params), owner_(owner)
74 {
75         inset_owner = p.inset_owner;
76         fontlist = p.fontlist;
77         id_ = paragraph_id++;
78
79         if (p.tracking())
80                 changes_.reset(new Changes(*p.changes_.get()));
81 }
82
83
84 void Paragraph::Pimpl::setContentsFromPar(Paragraph const & par)
85 {
86         owner_->text_ = par.text_;
87         if (par.pimpl_->tracking()) {
88                 changes_.reset(new Changes(*(par.pimpl_->changes_.get())));
89         }
90 }
91
92
93 bool Paragraph::Pimpl::isChanged(pos_type start, pos_type end) const
94 {
95         if (!tracking())
96                 return false;
97
98         return changes_->isChange(start, end);
99 }
100
101
102 bool Paragraph::Pimpl::isChangeEdited(pos_type start, pos_type end) const
103 {
104         if (!tracking())
105                 return false;
106
107         return changes_->isChangeEdited(start, end);
108 }
109
110
111 void Paragraph::Pimpl::setChangeType(pos_type pos, Change::Type type)
112 {
113         if (!tracking())
114                 return;
115
116         changes_->set(type, pos);
117 }
118
119
120 void Paragraph::Pimpl::setChange(pos_type pos, Change change)
121 {
122         if (!tracking())
123                 return;
124
125         changes_->set(change, pos);
126 }
127
128
129 Change const Paragraph::Pimpl::lookupChange(pos_type pos) const
130 {
131         if (!tracking())
132                 return Change(Change::UNCHANGED);
133
134         return changes_->lookup(pos);
135 }
136
137
138 void Paragraph::Pimpl::markErased(bool erased)
139 {
140         BOOST_ASSERT(tracking());
141
142         if (erased) {
143                 erase(0, size());
144         } else {
145                 for (pos_type i = 0; i < size(); ++i) {
146                         changes_->set(Change::UNCHANGED, i);
147                         if (owner_->isInset(i))
148                                 owner_->getInset(i)->markErased(false);
149                 }
150         }
151 }
152
153
154 void Paragraph::Pimpl::acceptChange(pos_type start, pos_type end)
155 {
156         if (!tracking())
157                 return;
158
159         if (!size()) {
160                 changes_.reset(new Changes(Change::UNCHANGED));
161                 return;
162         }
163
164         lyxerr[Debug::CHANGES] << "acceptchange" << endl;
165         pos_type i = start;
166
167         for (; i < end; ++i) {
168                 switch (lookupChange(i).type) {
169                         case Change::UNCHANGED:
170                                 break;
171
172                         case Change::INSERTED:
173                                 changes_->set(Change::UNCHANGED, i);
174                                 break;
175
176                         case Change::DELETED:
177                                 // Suppress access to nonexistent
178                                 // "end-of-paragraph char":
179                                 if (i < size()) {
180                                         eraseIntern(i);
181                                         changes_->erase(i);
182                                         --end;
183                                         --i;
184                                 }
185                                 break;
186                 }
187         }
188
189         lyxerr[Debug::CHANGES] << "endacceptchange" << endl;
190         changes_->reset(Change::UNCHANGED);
191 }
192
193
194 void Paragraph::Pimpl::rejectChange(pos_type start, pos_type end)
195 {
196         if (!tracking())
197                 return;
198
199         if (!size()) {
200                 changes_.reset(new Changes(Change::UNCHANGED));
201                 return;
202         }
203
204         pos_type i = start;
205
206         for (; i < end; ++i) {
207                 switch (lookupChange(i).type) {
208                         case Change::UNCHANGED:
209                                 break;
210
211                         case Change::INSERTED:
212                                 if (i < size()) {
213                                         eraseIntern(i);
214                                         changes_->erase(i);
215                                         --end;
216                                         --i;
217                                 }
218                                 break;
219
220                         case Change::DELETED:
221                                 changes_->set(Change::UNCHANGED, i);
222                                 // No real char at position size():
223                                 if (i < size() && owner_->isInset(i))
224                                         owner_->getInset(i)->markErased(false);
225                                 break;
226                 }
227         }
228         changes_->reset(Change::UNCHANGED);
229 }
230
231
232 Paragraph::value_type Paragraph::Pimpl::getChar(pos_type pos) const
233 {
234         return owner_->getChar(pos);
235 }
236
237
238 void Paragraph::Pimpl::insertChar(pos_type pos, value_type c, Change change)
239 {
240         BOOST_ASSERT(pos <= size());
241
242         if (tracking()) {
243                 changes_->record(change, pos);
244         }
245
246         // This is actually very common when parsing buffers (and
247         // maybe inserting ascii text)
248         if (pos == size()) {
249                 // when appending characters, no need to update tables
250                 owner_->text_.push_back(c);
251                 return;
252         }
253
254         owner_->text_.insert(owner_->text_.begin() + pos, c);
255
256         // Update the font table.
257         FontTable search_font(pos, LyXFont());
258         for (FontList::iterator it = lower_bound(fontlist.begin(),
259                                                       fontlist.end(),
260                                                       search_font, matchFT());
261              it != fontlist.end(); ++it)
262         {
263                 it->pos(it->pos() + 1);
264         }
265
266         // Update the insets
267         owner_->insetlist.increasePosAfterPos(pos);
268 }
269
270
271 void Paragraph::Pimpl::insertInset(pos_type pos,
272                                    InsetBase * inset, Change change)
273 {
274         BOOST_ASSERT(inset);
275         BOOST_ASSERT(pos <= size());
276
277         insertChar(pos, META_INSET, change);
278         BOOST_ASSERT(owner_->text_[pos] == META_INSET);
279
280         // Add a new entry in the insetlist.
281         owner_->insetlist.insert(inset, pos);
282 }
283
284
285 void Paragraph::Pimpl::eraseIntern(pos_type pos)
286 {
287         // if it is an inset, delete the inset entry
288         if (owner_->text_[pos] == Paragraph::META_INSET) {
289                 owner_->insetlist.erase(pos);
290         }
291
292         owner_->text_.erase(owner_->text_.begin() + pos);
293
294         // Erase entries in the tables.
295         FontTable search_font(pos, LyXFont());
296
297         FontList::iterator it =
298                 lower_bound(fontlist.begin(),
299                             fontlist.end(),
300                             search_font, matchFT());
301         if (it != fontlist.end() && it->pos() == pos &&
302             (pos == 0 ||
303              (it != fontlist.begin()
304               && boost::prior(it)->pos() == pos - 1))) {
305                 // If it is a multi-character font
306                 // entry, we just make it smaller
307                 // (see update below), otherwise we
308                 // should delete it.
309                 unsigned int const i = it - fontlist.begin();
310                 fontlist.erase(fontlist.begin() + i);
311                 it = fontlist.begin() + i;
312                 if (i > 0 && i < fontlist.size() &&
313                     fontlist[i - 1].font() == fontlist[i].font()) {
314                         fontlist.erase(fontlist.begin() + i - 1);
315                         it = fontlist.begin() + i - 1;
316                 }
317         }
318
319         // Update all other entries.
320         FontList::iterator fend = fontlist.end();
321         for (; it != fend; ++it)
322                 it->pos(it->pos() - 1);
323
324         // Update the insetlist.
325         owner_->insetlist.decreasePosAfterPos(pos);
326 }
327
328
329 bool Paragraph::Pimpl::erase(pos_type pos)
330 {
331         BOOST_ASSERT(pos <= size());
332
333         if (tracking()) {
334                 Change::Type changetype(changes_->lookup(pos).type);
335
336                 // only allow the actual removal if it was /new/ text
337                 if (changetype != Change::INSERTED) {
338                         changes_->record(Change(Change::DELETED), pos);
339                         if (pos < size() && owner_->isInset(pos))
340                                 owner_->getInset(pos)->markErased(true);
341                         return false;
342                 }
343         }
344
345         // Don't physically access nonexistent end-of-paragraph char
346         if (pos < size()) {
347                 eraseIntern(pos);
348                 return true;
349         }
350
351         return false;
352 }
353
354
355 int Paragraph::Pimpl::erase(pos_type start, pos_type end)
356 {
357         pos_type i = start;
358         for (pos_type count = end - start; count; --count) {
359                 if (!erase(i))
360                         ++i;
361         }
362         return end - i;
363 }
364
365
366 void Paragraph::Pimpl::simpleTeXBlanks(ostream & os, TexRow & texrow,
367                                        pos_type const i,
368                                        unsigned int & column,
369                                        LyXFont const & font,
370                                        LyXLayout const & style)
371 {
372         if (style.pass_thru)
373                 return;
374
375         if (column > lyxrc.ascii_linelen
376             && i
377             && getChar(i - 1) != ' '
378             && (i < size() - 1)
379             // same in FreeSpacing mode
380             && !owner_->isFreeSpacing()
381             // In typewriter mode, we want to avoid
382             // ! . ? : at the end of a line
383             && !(font.family() == LyXFont::TYPEWRITER_FAMILY
384                  && (getChar(i - 1) == '.'
385                      || getChar(i - 1) == '?'
386                      || getChar(i - 1) == ':'
387                      || getChar(i - 1) == '!'))) {
388                 os << '\n';
389                 texrow.newline();
390                 texrow.start(owner_->id(), i + 1);
391                 column = 0;
392         } else if (style.free_spacing) {
393                 os << '~';
394         } else {
395                 os << ' ';
396         }
397 }
398
399
400 bool Paragraph::Pimpl::isTextAt(string const & str, pos_type pos) const
401 {
402         pos_type const len = str.length();
403
404         // is the paragraph large enough?
405         if (pos + len > size())
406                 return false;
407
408         // does the wanted text start at point?
409         for (string::size_type i = 0; i < str.length(); ++i) {
410                 if (str[i] != owner_->text_[pos + i])
411                         return false;
412         }
413
414         // is there a font change in middle of the word?
415         FontList::const_iterator cit = fontlist.begin();
416         FontList::const_iterator end = fontlist.end();
417         for (; cit != end; ++cit) {
418                 if (cit->pos() >= pos)
419                         break;
420         }
421         if (cit != end && pos + len - 1 > cit->pos())
422                 return false;
423
424         return true;
425 }
426
427
428 void Paragraph::Pimpl::simpleTeXSpecialChars(Buffer const & buf,
429                                              BufferParams const & bparams,
430                                              ostream & os,
431                                              TexRow & texrow,
432                                              OutputParams const & runparams,
433                                              LyXFont & font,
434                                              LyXFont & running_font,
435                                              LyXFont & basefont,
436                                              LyXFont const & outerfont,
437                                              bool & open_font,
438                                              Change::Type & running_change,
439                                              LyXLayout const & style,
440                                              pos_type & i,
441                                              unsigned int & column,
442                                              value_type const c)
443 {
444         if (style.pass_thru) {
445                 if (c != Paragraph::META_INSET) {
446                         if (c != '\0')
447                                 os << c;
448                 } else {
449                         lyx::odocstringstream oss;
450                         owner_->getInset(i)->plaintext(buf, oss, runparams);
451                         // FIXME UNICODE
452                         os << lyx::to_utf8(oss.str());
453                 }
454                 return;
455         }
456
457         // Two major modes:  LaTeX or plain
458         // Handle here those cases common to both modes
459         // and then split to handle the two modes separately.
460         switch (c) {
461         case Paragraph::META_INSET: {
462                 InsetBase * inset = owner_->getInset(i);
463
464                 // FIXME: remove this check
465                 if (!inset)
466                         break;
467
468                 // FIXME: move this to InsetNewline::latex
469                 if (inset->lyxCode() == InsetBase::NEWLINE_CODE) {
470                         // newlines are handled differently here than
471                         // the default in simpleTeXSpecialChars().
472                         if (!style.newline_allowed) {
473                                 os << '\n';
474                         } else {
475                                 if (open_font) {
476                                         column += running_font.latexWriteEndChanges(os, basefont, basefont);
477                                         open_font = false;
478                                 }
479                                 basefont = owner_->getLayoutFont(bparams, outerfont);
480                                 running_font = basefont;
481
482                                 if (font.family() == LyXFont::TYPEWRITER_FAMILY)
483                                         os << '~';
484
485                                 if (runparams.moving_arg)
486                                         os << "\\protect ";
487
488                                 os << "\\\\\n";
489                         }
490                         texrow.newline();
491                         texrow.start(owner_->id(), i + 1);
492                         column = 0;
493                         break;
494                 }
495
496                 // output change tracking marks only if desired,
497                 // if dvipost is installed,
498                 // and with dvi/ps (other formats don't work)
499                 LaTeXFeatures features(buf, bparams, runparams);
500                 bool const output = bparams.outputChanges
501                         && runparams.flavor == OutputParams::LATEX
502                         && features.isAvailable("dvipost");
503
504                 if (inset->canTrackChanges()) {
505                         column += Changes::latexMarkChange(os, running_change,
506                                 Change::UNCHANGED, output);
507                         running_change = Change::UNCHANGED;
508                 }
509
510                 bool close = false;
511                 ostream::pos_type const len = os.tellp();
512
513                 if ((inset->lyxCode() == InsetBase::GRAPHICS_CODE
514                      || inset->lyxCode() == InsetBase::MATH_CODE
515                      || inset->lyxCode() == InsetBase::URL_CODE)
516                     && running_font.isRightToLeft()) {
517                         os << "\\L{";
518                         close = true;
519                 }
520
521 #ifdef WITH_WARNINGS
522 #warning Bug: we can have an empty font change here!
523 // if there has just been a font change, we are going to close it
524 // right now, which means stupid latex code like \textsf{}. AFAIK,
525 // this does not harm dvi output. A minor bug, thus (JMarc)
526 #endif
527                 // some insets cannot be inside a font change command
528                 if (open_font && inset->noFontChange()) {
529                         column +=running_font.
530                                 latexWriteEndChanges(os,
531                                                      basefont,
532                                                      basefont);
533                         open_font = false;
534                         basefont = owner_->getLayoutFont(bparams, outerfont);
535                         running_font = basefont;
536                 }
537
538                 int tmp = inset->latex(buf, os, runparams);
539
540                 if (close)
541                         os << '}';
542
543                 if (tmp) {
544                         for (int j = 0; j < tmp; ++j) {
545                                 texrow.newline();
546                         }
547                         texrow.start(owner_->id(), i + 1);
548                         column = 0;
549                 } else {
550                         column += os.tellp() - len;
551                 }
552         }
553         break;
554
555         default:
556                 // And now for the special cases within each mode
557
558                 switch (c) {
559                 case '\\':
560                         os << "\\textbackslash{}";
561                         column += 15;
562                         break;
563
564                 case '±': case '²': case '³':
565                 case '×': case '÷': case '¹':
566                 case '¬': case 'µ':
567                         if ((bparams.inputenc == "latin1" ||
568                              bparams.inputenc == "latin9") ||
569                             (bparams.inputenc == "auto" &&
570                              (font.language()->encoding()->latexName()
571                               == "latin1" ||
572                               font.language()->encoding()->latexName()
573                               == "latin9"))) {
574                                 os << "\\ensuremath{"
575                                    << c
576                                    << '}';
577                                 column += 13;
578                         } else {
579                                 os << c;
580                         }
581                         break;
582
583                 case '|': case '<': case '>':
584                         // In T1 encoding, these characters exist
585                         if (lyxrc.fontenc == "T1") {
586                                 os << c;
587                                 //... but we should avoid ligatures
588                                 if ((c == '>' || c == '<')
589                                     && i <= size() - 2
590                                     && getChar(i + 1) == c) {
591                                         //os << "\\textcompwordmark{}";
592                                         // Jean-Marc, have a look at
593                                         // this. I think this works
594                                         // equally well:
595                                         os << "\\,{}";
596                                         // Lgb
597                                         column += 19;
598                                 }
599                                 break;
600                         }
601                         // Typewriter font also has them
602                         if (font.family() == LyXFont::TYPEWRITER_FAMILY) {
603                                 os << c;
604                                 break;
605                         }
606                         // Otherwise, we use what LaTeX
607                         // provides us.
608                         switch (c) {
609                         case '<':
610                                 os << "\\textless{}";
611                                 column += 10;
612                                 break;
613                         case '>':
614                                 os << "\\textgreater{}";
615                                 column += 13;
616                                 break;
617                         case '|':
618                                 os << "\\textbar{}";
619                                 column += 9;
620                                 break;
621                         }
622                         break;
623
624                 case '-': // "--" in Typewriter mode -> "-{}-"
625                         if (i <= size() - 2
626                             && getChar(i + 1) == '-'
627                             && font.family() == LyXFont::TYPEWRITER_FAMILY) {
628                                 os << "-{}";
629                                 column += 2;
630                         } else {
631                                 os << '-';
632                         }
633                         break;
634
635                 case '\"':
636                         os << "\\char`\\\"{}";
637                         column += 9;
638                         break;
639
640                 case '£':
641                         if (bparams.inputenc == "default") {
642                                 os << "\\pounds{}";
643                                 column += 8;
644                         } else {
645                                 os << c;
646                         }
647                         break;
648
649                 case '$': case '&':
650                 case '%': case '#': case '{':
651                 case '}': case '_':
652                         os << '\\' << c;
653                         column += 1;
654                         break;
655
656                 case '~':
657                         os << "\\textasciitilde{}";
658                         column += 16;
659                         break;
660
661                 case '^':
662                         os << "\\textasciicircum{}";
663                         column += 17;
664                         break;
665
666                 case '*': case '[':
667                         // avoid being mistaken for optional arguments
668                         os << '{' << c << '}';
669                         column += 2;
670                         break;
671
672                 case ' ':
673                         // Blanks are printed before font switching.
674                         // Sure? I am not! (try nice-latex)
675                         // I am sure it's correct. LyX might be smarter
676                         // in the future, but for now, nothing wrong is
677                         // written. (Asger)
678                         break;
679
680                 default:
681
682                         // I assume this is hack treating typewriter as verbatim
683                         if (font.family() == LyXFont::TYPEWRITER_FAMILY) {
684                                 if (c != '\0') {
685                                         os << c;
686                                 }
687                                 break;
688                         }
689
690                         // LyX, LaTeX etc.
691
692                         // FIXME: if we have "LaTeX" with a font
693                         // change in the middle (before the 'T', then
694                         // the "TeX" part is still special cased.
695                         // Really we should only operate this on
696                         // "words" for some definition of word
697
698                         size_t pnr = 0;
699
700                         for (; pnr < phrases_nr; ++pnr) {
701                                 if (isTextAt(special_phrases[pnr].phrase, i)) {
702                                         os << special_phrases[pnr].macro;
703                                         i += special_phrases[pnr].phrase.length() - 1;
704                                         column += special_phrases[pnr].macro.length() - 1;
705                                         break;
706                                 }
707                         }
708
709                         if (pnr == phrases_nr && c != '\0') {
710                                 os << c;
711                         }
712                         break;
713                 }
714         }
715 }
716
717
718 void Paragraph::Pimpl::validate(LaTeXFeatures & features,
719                                 LyXLayout const & layout) const
720 {
721         BufferParams const & bparams = features.bufferParams();
722
723         // check the params.
724         if (!params.spacing().isDefault())
725                 features.require("setspace");
726
727         // then the layouts
728         features.useLayout(layout.name());
729
730         // then the fonts
731         Language const * doc_language = bparams.language;
732
733         FontList::const_iterator fcit = fontlist.begin();
734         FontList::const_iterator fend = fontlist.end();
735         for (; fcit != fend; ++fcit) {
736                 if (fcit->font().noun() == LyXFont::ON) {
737                         lyxerr[Debug::LATEX] << "font.noun: "
738                                              << fcit->font().noun()
739                                              << endl;
740                         features.require("noun");
741                         lyxerr[Debug::LATEX] << "Noun enabled. Font: "
742                                              << fcit->font().stateText(0)
743                                              << endl;
744                 }
745                 switch (fcit->font().color()) {
746                 case LColor::none:
747                 case LColor::inherit:
748                 case LColor::ignore:
749                         // probably we should put here all interface colors used for
750                         // font displaying! For now I just add this ones I know of (Jug)
751                 case LColor::latex:
752                 case LColor::note:
753                         break;
754                 default:
755                         features.require("color");
756                         lyxerr[Debug::LATEX] << "Color enabled. Font: "
757                                              << fcit->font().stateText(0)
758                                              << endl;
759                 }
760
761                 Language const * language = fcit->font().language();
762                 if (language->babel() != doc_language->babel() &&
763                     language != ignore_language &&
764                     language != latex_language)
765                 {
766                         features.useLanguage(language);
767                         lyxerr[Debug::LATEX] << "Found language "
768                                              << language->babel() << endl;
769                 }
770         }
771
772         if (!params.leftIndent().zero())
773                 features.require("ParagraphLeftIndent");
774
775         // then the insets
776         InsetList::const_iterator icit = owner_->insetlist.begin();
777         InsetList::const_iterator iend = owner_->insetlist.end();
778         for (; icit != iend; ++icit) {
779                 if (icit->inset) {
780                         icit->inset->validate(features);
781                         if (layout.needprotect &&
782                             icit->inset->lyxCode() == InsetBase::FOOT_CODE)
783                                 features.require("NeedLyXFootnoteCode");
784                 }
785         }
786
787         // then the contents
788         for (pos_type i = 0; i < size() ; ++i) {
789                 for (size_t pnr = 0; pnr < phrases_nr; ++pnr) {
790                         if (!special_phrases[pnr].builtin
791                             && isTextAt(special_phrases[pnr].phrase, i)) {
792                                 features.require(special_phrases[pnr].phrase);
793                                 break;
794                         }
795                 }
796         }
797 }