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)
351 static docstring warned_author = docstring();
352 docstring uncodable_author = warned_author;
353 odocstringstream ods;
356 // convert utf8 author name to something representable
357 // in the current encoding
358 pair<docstring, docstring> author_latexed =
359 runparams.encoding->latexString(author, runparams.dryrun);
360 if (!author_latexed.second.empty()) {
361 LYXERR0("Omitting uncodable characters '"
362 << author_latexed.second
363 << "' in change author name!");
364 uncodable_author = author;
366 ods << author_latexed.first << "}{" << chgTime << "}{";
368 // warn user (once) if we found uncodable glyphs.
369 if (uncodable_author != warned_author) {
370 frontend::Alert::warning(_("Uncodable character in author name"),
371 support::bformat(_("The author name '%1$s',\n"
372 "used for change tracking, contains the following glyphs that\n"
373 "cannot be represented in the current encoding: %2$s.\n"
374 "These glyphs will be omitted in the exported LaTeX file.\n\n"
375 "Choose an appropriate document encoding (such as utf8)\n"
376 "or change the spelling of the author name."),
377 uncodable_author, author_latexed.second));
378 warned_author = uncodable_author;
387 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
388 Change const & oldChange, Change const & change,
389 OutputParams const & runparams)
391 if (!bparams.outputChanges || oldChange == change)
396 if (oldChange.type != Change::UNCHANGED) {
397 // close \lyxadded or \lyxdeleted
403 chgTime += asctime(gmtime(&change.changetime));
404 // remove trailing '\n'
405 chgTime.erase(chgTime.end() - 1);
408 if (change.type == Change::DELETED)
409 macro_beg = from_ascii("\\lyxdeleted{");
410 else if (change.type == Change::INSERTED)
411 macro_beg = from_ascii("\\lyxadded{");
413 docstring str = getLaTeXMarkup(macro_beg,
414 bparams.authors().get(change.author).name(),
418 column += str.size();
424 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
425 Change const & old, Change const & change)
432 int const buffer_id = bparams.authors().get(change.author).bufferId();
434 switch (change.type) {
435 case Change::UNCHANGED:
436 os << "\n\\change_unchanged\n";
439 case Change::DELETED:
440 os << "\n\\change_deleted " << buffer_id
441 << " " << change.changetime << "\n";
444 case Change::INSERTED:
445 os << "\n\\change_inserted " << buffer_id
446 << " " << change.changetime << "\n";
452 void Changes::checkAuthors(AuthorList const & authorList)
454 ChangeTable::const_iterator it = table_.begin();
455 ChangeTable::const_iterator endit = table_.end();
456 for ( ; it != endit ; ++it)
457 if (it->change.type != Change::UNCHANGED)
458 authorList.get(it->change.author).setUsed(true);
462 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
463 bool output_active) const
468 Toc & change_list = buffer.tocBackend().toc("change");
469 AuthorList const & author_list = buffer.params().authors();
470 DocIterator dit = cdit;
472 ChangeTable::const_iterator it = table_.begin();
473 ChangeTable::const_iterator const itend = table_.end();
474 for (; it != itend; ++it) {
476 switch (it->change.type) {
477 case Change::UNCHANGED:
479 case Change::DELETED:
480 // 0x2702 is a scissors symbol in the Dingbats unicode group.
481 str.push_back(0x2702);
483 case Change::INSERTED:
484 // 0x270d is the hand writting symbol in the Dingbats unicode group.
485 str.push_back(0x270d); break;
487 dit.pos() = it->range.start;
488 Paragraph const & par = dit.paragraph();
489 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
490 if (it->range.end > par.size())
491 // the end of paragraph symbol from the Punctuation group
492 str.push_back(0x204B);
493 docstring const & author = author_list.get(it->change.author).name();
494 Toc::iterator it = change_list.item(0, author);
495 if (it == change_list.end()) {
496 change_list.push_back(TocItem(dit, 0, author, output_active));
497 change_list.push_back(TocItem(dit, 1, str, output_active,
498 support::wrapParas(str, 4)));
501 for (++it; it != change_list.end(); ++it) {
502 if (it->depth() == 0 && it->str() != author)
505 change_list.insert(it, TocItem(dit, 1, str, output_active,
506 support::wrapParas(str, 4)));