3 * This file is part of LyX, the document processor.
4 * Licence details can be found in the file COPYING.
9 * Full author contact details are available in file CREDITS.
11 * Record changes in a paragraph.
19 #include "BufferParams.h"
21 #include "LaTeXFeatures.h"
22 #include "OutputParams.h"
23 #include "Paragraph.h"
24 #include "TocBackend.h"
26 #include "support/debug.h"
27 #include "support/gettext.h"
28 #include "support/lassert.h"
29 #include "support/lstrings.h"
31 #include "frontends/alert.h"
40 * Class Change has a changetime field that specifies the exact time at which
41 * a specific change was made. The change time is used as a guidance for the
42 * user while editing his document. Presently, it is not considered for LaTeX
44 * When merging two adjacent changes, the changetime is not considered,
45 * only the equality of the change type and author is checked (in method
46 * isSimilarTo(...)). If two changes are in fact merged (in method merge()),
47 * the later change time is preserved.
50 bool Change::isSimilarTo(Change const & change) const
52 if (type != change.type)
55 if (type == Change::UNCHANGED)
58 return author == change.author;
62 Color Change::color() const
64 Color color = Color_none;
67 color = Color_changedtextauthor1;
70 color = Color_changedtextauthor2;
73 color = Color_changedtextauthor3;
76 color = Color_changedtextauthor4;
79 color = Color_changedtextauthor5;
84 color.mergeColor = Color_deletedtextmodifier;
90 bool operator==(Change const & l, Change const & r)
95 // two changes of type UNCHANGED are always equal
96 if (l.type == Change::UNCHANGED)
99 return l.author == r.author && l.changetime == r.changetime;
103 bool operator!=(Change const & l, Change const & r)
109 bool operator==(Changes::Range const & r1, Changes::Range const & r2)
111 return r1.start == r2.start && r1.end == r2.end;
115 bool operator!=(Changes::Range const & r1, Changes::Range const & r2)
121 bool Changes::Range::intersects(Range const & r) const
123 return r.start < end && r.end > start; // end itself is not in the range!
127 void Changes::set(Change const & change, pos_type const pos)
129 set(change, pos, pos + 1);
133 void Changes::set(Change const & change, pos_type const start, pos_type const end)
135 if (change.type != Change::UNCHANGED) {
136 LYXERR(Debug::CHANGES, "setting change (type: " << change.type
137 << ", author: " << change.author
138 << ", time: " << long(change.changetime)
139 << ") in range (" << start << ", " << end << ")");
142 Range const newRange(start, end);
144 ChangeTable::iterator it = table_.begin();
146 for (; it != table_.end(); ) {
147 // current change starts like or follows new change
148 if (it->range.start >= start) {
152 // new change intersects with existing change
153 if (it->range.end > start) {
154 pos_type oldEnd = it->range.end;
155 it->range.end = start;
157 LYXERR(Debug::CHANGES, " cutting tail of type " << it->change.type
158 << " resulting in range (" << it->range.start << ", "
159 << it->range.end << ")");
163 LYXERR(Debug::CHANGES, " inserting tail in range ("
164 << end << ", " << oldEnd << ")");
165 it = table_.insert(it, ChangeRange((it-1)->change, Range(end, oldEnd)));
173 if (change.type != Change::UNCHANGED) {
174 LYXERR(Debug::CHANGES, " inserting change");
175 it = table_.insert(it, ChangeRange(change, Range(start, end)));
179 for (; it != table_.end(); ) {
180 // new change 'contains' existing change
181 if (newRange.contains(it->range)) {
182 LYXERR(Debug::CHANGES, " removing subrange ("
183 << it->range.start << ", " << it->range.end << ")");
184 it = table_.erase(it);
188 // new change precedes existing change
189 if (it->range.start >= end)
192 // new change intersects with existing change
193 it->range.start = end;
194 LYXERR(Debug::CHANGES, " cutting head of type "
195 << it->change.type << " resulting in range ("
196 << end << ", " << it->range.end << ")");
197 break; // no need for another iteration
204 void Changes::erase(pos_type const pos)
206 LYXERR(Debug::CHANGES, "Erasing change at position " << pos);
208 ChangeTable::iterator it = table_.begin();
209 ChangeTable::iterator end = table_.end();
211 for (; it != end; ++it) {
212 // range (pos,pos+x) becomes (pos,pos+x-1)
213 if (it->range.start > pos)
215 // range (pos-x,pos) stays (pos-x,pos)
216 if (it->range.end > pos)
224 void Changes::insert(Change const & change, lyx::pos_type pos)
226 if (change.type != Change::UNCHANGED) {
227 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
228 << " at position " << pos);
231 ChangeTable::iterator it = table_.begin();
232 ChangeTable::iterator end = table_.end();
234 for (; it != end; ++it) {
235 // range (pos,pos+x) becomes (pos+1,pos+x+1)
236 if (it->range.start >= pos)
239 // range (pos-x,pos) stays as it is
240 if (it->range.end > pos)
244 set(change, pos, pos + 1); // set will call merge
248 Change const & Changes::lookup(pos_type const pos) const
250 static Change const noChange = Change(Change::UNCHANGED);
252 ChangeTable::const_iterator it = table_.begin();
253 ChangeTable::const_iterator const end = table_.end();
255 for (; it != end; ++it) {
256 if (it->range.contains(pos))
264 bool Changes::isDeleted(pos_type start, pos_type end) const
266 ChangeTable::const_iterator it = table_.begin();
267 ChangeTable::const_iterator const itend = table_.end();
269 for (; it != itend; ++it) {
270 if (it->range.contains(Range(start, end))) {
271 LYXERR(Debug::CHANGES, "range ("
272 << start << ", " << end << ") fully contains ("
273 << it->range.start << ", " << it->range.end
274 << ") of type " << it->change.type);
275 return it->change.type == Change::DELETED;
282 bool Changes::isChanged(pos_type const start, pos_type const end) const
284 ChangeTable::const_iterator it = table_.begin();
285 ChangeTable::const_iterator const itend = table_.end();
287 for (; it != itend; ++it) {
288 if (it->range.intersects(Range(start, end))) {
289 LYXERR(Debug::CHANGES, "found intersection of range ("
290 << start << ", " << end << ") with ("
291 << it->range.start << ", " << it->range.end
292 << ") of type " << it->change.type);
300 void Changes::merge()
302 ChangeTable::iterator it = table_.begin();
304 while (it != table_.end()) {
305 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
306 << " and range (" << it->range.start << ", " << it->range.end
309 if (it->range.start == it->range.end) {
310 LYXERR(Debug::CHANGES, "removing empty range for pos "
319 if (it + 1 == table_.end())
322 if (it->change.isSimilarTo((it + 1)->change)
323 && it->range.end == (it + 1)->range.start) {
324 LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
325 << it->range.end << ") and (" << (it + 1)->range.start << ", "
326 << (it + 1)->range.end << ")");
328 (it + 1)->range.start = it->range.start;
329 (it + 1)->change.changetime = max(it->change.changetime,
330 (it + 1)->change.changetime);
344 docstring getLaTeXMarkup(docstring const & macro, docstring const & author,
345 docstring const & chgTime,
346 OutputParams const & runparams)
352 static docstring warned_author = docstring();
353 docstring uncodable_author = warned_author;
354 odocstringstream ods;
357 // convert utf8 author name to something representable
358 // in the current encoding
359 pair<docstring, docstring> author_latexed =
360 runparams.encoding->latexString(author, runparams.dryrun);
361 if (!author_latexed.second.empty()) {
362 LYXERR0("Omitting uncodable characters '"
363 << author_latexed.second
364 << "' in change author name!");
365 uncodable_author = author;
367 ods << author_latexed.first << "}{" << chgTime << "}{";
369 // warn user (once) if we found uncodable glyphs.
370 if (uncodable_author != warned_author) {
371 frontend::Alert::warning(_("Uncodable character in author name"),
372 support::bformat(_("The author name '%1$s',\n"
373 "used for change tracking, contains the following glyphs that\n"
374 "cannot be represented in the current encoding: %2$s.\n"
375 "These glyphs will be omitted in the exported LaTeX file.\n\n"
376 "Choose an appropriate document encoding (such as utf8)\n"
377 "or change the spelling of the author name."),
378 uncodable_author, author_latexed.second));
379 warned_author = uncodable_author;
388 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
389 Change const & oldChange, Change const & change,
390 OutputParams const & runparams)
392 if (!bparams.output_changes || oldChange == change)
397 if (oldChange.type != Change::UNCHANGED) {
398 // close \lyxadded or \lyxdeleted
401 if (oldChange.type == Change::DELETED)
402 --runparams.inulemcmd;
406 chgTime += asctime(gmtime(&change.changetime));
407 // remove trailing '\n'
408 chgTime.erase(chgTime.end() - 1);
411 if (change.type == Change::DELETED) {
412 macro_beg = from_ascii("\\lyxdeleted{");
413 ++runparams.inulemcmd;
415 else if (change.type == Change::INSERTED)
416 macro_beg = from_ascii("\\lyxadded{");
418 docstring str = getLaTeXMarkup(macro_beg,
419 bparams.authors().get(change.author).name(),
423 column += str.size();
429 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
430 Change const & old, Change const & change)
437 int const buffer_id = bparams.authors().get(change.author).bufferId();
439 switch (change.type) {
440 case Change::UNCHANGED:
441 os << "\n\\change_unchanged\n";
444 case Change::DELETED:
445 os << "\n\\change_deleted " << buffer_id
446 << " " << change.changetime << "\n";
449 case Change::INSERTED:
450 os << "\n\\change_inserted " << buffer_id
451 << " " << change.changetime << "\n";
457 void Changes::checkAuthors(AuthorList const & authorList)
459 ChangeTable::const_iterator it = table_.begin();
460 ChangeTable::const_iterator endit = table_.end();
461 for ( ; it != endit ; ++it)
462 if (it->change.type != Change::UNCHANGED)
463 authorList.get(it->change.author).setUsed(true);
467 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
468 bool output_active) const
473 Toc & change_list = buffer.tocBackend().toc("change");
474 AuthorList const & author_list = buffer.params().authors();
475 DocIterator dit = cdit;
477 ChangeTable::const_iterator it = table_.begin();
478 ChangeTable::const_iterator const itend = table_.end();
479 for (; it != itend; ++it) {
481 switch (it->change.type) {
482 case Change::UNCHANGED:
484 case Change::DELETED:
485 // 0x2702 is a scissors symbol in the Dingbats unicode group.
486 str.push_back(0x2702);
488 case Change::INSERTED:
489 // 0x270d is the hand writting symbol in the Dingbats unicode group.
490 str.push_back(0x270d); break;
492 dit.pos() = it->range.start;
493 Paragraph const & par = dit.paragraph();
494 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
495 if (it->range.end > par.size())
496 // the end of paragraph symbol from the Punctuation group
497 str.push_back(0x204B);
498 docstring const & author = author_list.get(it->change.author).name();
499 Toc::iterator it = change_list.item(0, author);
500 if (it == change_list.end()) {
501 change_list.push_back(TocItem(dit, 0, author, output_active));
502 change_list.push_back(TocItem(dit, 1, str, output_active,
503 support::wrapParas(str, 4)));
506 for (++it; it != change_list.end(); ++it) {
507 if (it->depth() == 0 && it->str() != author)
510 change_list.insert(it, TocItem(dit, 1, str, output_active,
511 support::wrapParas(str, 4)));