]> git.lyx.org Git - lyx.git/blob - src/xml.cpp
Correct Right Arrow key processing in Modules list
[lyx.git] / src / xml.cpp
1 /**
2  * \file xml.cpp
3  * This file is part of LyX, the document processor.
4  * License details can be found in the file COPYING.
5  *
6  * \author José Matos
7  * \author John Levon
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11
12 #include <config.h>
13
14 #include "xml.h"
15
16 #include "Buffer.h"
17 #include "BufferParams.h"
18 #include "Counters.h"
19 #include "Layout.h"
20 #include "OutputParams.h"
21 #include "Paragraph.h"
22 #include "Text.h"
23 #include "TextClass.h"
24
25 #include "support/convert.h"
26 #include "support/docstream.h"
27 #include "support/lassert.h"
28 #include "support/lstrings.h"
29 #include "support/textutils.h"
30
31 #include <atomic>
32 #include <map>
33 #include <functional>
34 #include <QThreadStorage>
35
36 using namespace std;
37 using namespace lyx::support;
38
39 namespace lyx {
40 namespace xml {
41
42
43 docstring escapeChar(char_type c, XMLStream::EscapeSettings e)
44 {
45         docstring str;
46         switch (e) { // For HTML: always ESCAPE_NONE. For XML: it depends, hence the parameter.
47         case XMLStream::ESCAPE_NONE:
48         case XMLStream::ESCAPE_COMMENTS:
49                 str += c;
50                 break;
51         case XMLStream::ESCAPE_ALL:
52                 if (c == '<') {
53                         str += "&lt;";
54                         break;
55                 } else if (c == '>') {
56                         str += "&gt;";
57                         break;
58                 }
59                 // fall through
60         case XMLStream::ESCAPE_AND:
61                 if (c == '&')
62                         str += "&amp;";
63                 else
64                         str     +=c ;
65                 break;
66         }
67         return str;
68 }
69
70
71 docstring escapeChar(char c, XMLStream::EscapeSettings e)
72 {
73         LATTEST(static_cast<unsigned char>(c) < 0x80);
74         return escapeChar(static_cast<char_type>(c), e);
75 }
76
77
78 docstring escapeString(docstring const & raw, XMLStream::EscapeSettings e)
79 {
80         docstring bin;
81         bin.reserve(raw.size() * 2); // crude approximation is sufficient
82         for (size_t i = 0; i != raw.size(); ++i) {
83                 char_type c = raw[i];
84                 if (e == XMLStream::ESCAPE_COMMENTS && c == '-' && i > 0 && raw[i - 1] == '-')
85                         bin += "&#45;";
86                 else
87                         bin += xml::escapeChar(c, e);
88         }
89
90         return bin;
91 }
92
93
94 docstring cleanAttr(docstring const & str)
95 {
96         docstring newname;
97         docstring::const_iterator it = str.begin();
98         docstring::const_iterator en = str.end();
99         for (; it != en; ++it) {
100                 char_type const c = *it;
101                 newname += isAlnumASCII(c) ? c : char_type('_');
102         }
103         return newname;
104 }
105
106
107 docstring StartTag::writeTag() const
108 {
109         docstring output = '<' + tag_;
110         if (!attr_.empty()) {
111                 docstring attributes = xml::escapeString(attr_, XMLStream::ESCAPE_NONE);
112                 attributes.erase(attributes.begin(), std::find_if(attributes.begin(), attributes.end(),
113                                                           [](int c) {return !std::isspace(c);}));
114                 if (!attributes.empty()) {
115                         output += ' ' + attributes;
116                 }
117         }
118         output += ">";
119         return output;
120 }
121
122
123 docstring StartTag::writeEndTag() const
124 {
125         return from_utf8("</") + tag_ + from_utf8(">");
126 }
127
128
129 bool StartTag::operator==(FontTag const &rhs) const
130 {
131         return rhs == *this;
132 }
133
134
135 docstring EndTag::writeEndTag() const
136 {
137         return from_utf8("</") + tag_ + from_utf8(">");
138 }
139
140
141 docstring CompTag::writeTag() const
142 {
143         docstring output = '<' + from_utf8(tag_);
144         if (!attr_.empty()) {
145                 // Erase the beginning of the attributes if it contains space characters: this function deals with that
146                 // automatically.
147                 docstring attributes = escapeString(from_utf8(attr_), XMLStream::ESCAPE_NONE);
148                 attributes.erase(attributes.begin(), std::find_if(attributes.begin(), attributes.end(),
149                                                           [](int c) {return !std::isspace(c);}));
150                 if (!attributes.empty()) {
151                         output += ' ' + attributes;
152                 }
153         }
154         output += " />";
155         return output;
156 }
157
158
159 bool FontTag::operator==(StartTag const & tag) const
160 {
161         FontTag const * const ftag = tag.asFontTag();
162         if (!ftag)
163                 return false;
164         return (font_type_ == ftag->font_type_);
165 }
166
167 } // namespace xml
168
169
170 void XMLStream::writeError(std::string const &s) const
171 {
172         LYXERR0(s);
173         os_ << from_utf8("<!-- Output Error: " + s + " -->\n");
174 }
175
176
177 void XMLStream::writeError(docstring const &s) const
178 {
179         LYXERR0(s);
180         os_ << from_utf8("<!-- Output Error: ") << s << from_utf8(" -->\n");
181 }
182
183
184 bool XMLStream::closeFontTags()
185 {
186         if (isTagPending(xml::parsep_tag))
187                 // we haven't had any content
188                 return true;
189
190         // this may be a useless check, since we ought at least to have
191         // the parsep_tag. but it can't hurt too much to be careful.
192         if (tag_stack_.empty())
193                 return true;
194
195         // first, we close any open font tags we can close
196         TagPtr *curtag = &tag_stack_.back();
197         while ((*curtag)->asFontTag()) {
198                 if (**curtag != xml::parsep_tag)
199                         os_ << (*curtag)->writeEndTag();
200                 tag_stack_.pop_back();
201                 // this shouldn't happen, since then the font tags
202                 // weren't in any other tag.
203 //              LASSERT(!tag_stack_.empty(), return true);
204                 if (tag_stack_.empty())
205                         return true;
206                 curtag = &tag_stack_.back();
207         }
208
209         if (**curtag == xml::parsep_tag)
210                 return true;
211
212         // so we've hit a non-font tag.
213         writeError("Tags still open in closeFontTags(). Probably not a problem,\n"
214                                            "but you might want to check these tags:");
215         TagDeque::const_reverse_iterator it = tag_stack_.rbegin();
216         TagDeque::const_reverse_iterator const en = tag_stack_.rend();
217         for (; it != en; ++it) {
218                 if (**it == xml::parsep_tag)
219                         break;
220                 writeError((*it)->tag_);
221         }
222         return false;
223 }
224
225
226 void XMLStream::startDivision(bool keep_empty)
227 {
228         pending_tags_.push_back(makeTagPtr(xml::StartTag(xml::parsep_tag)));
229         if (keep_empty)
230                 clearTagDeque();
231 }
232
233
234 void XMLStream::endDivision()
235 {
236         if (isTagPending(xml::parsep_tag)) {
237                 // this case is normal. it just means we didn't have content,
238                 // so the parsep_tag never got moved onto the tag stack.
239                 while (!pending_tags_.empty()) {
240                         // clear all pending tags up to and including the parsep tag.
241                         // note that we work from the back, because we want to get rid
242                         // of everything that hasn't been used.
243                         TagPtr const cur_tag = pending_tags_.back();
244                         pending_tags_.pop_back();
245                         if (*cur_tag == xml::parsep_tag)
246                                 break;
247                 }
248
249 #ifdef  XHTML_DEBUG
250                 dumpTagStack("EndDivision");
251 #endif
252
253                 return;
254         }
255
256         if (!isTagOpen(xml::parsep_tag)) {
257                 writeError("No division separation tag found in endDivision().");
258                 return;
259         }
260
261         // this case is also normal, if the parsep tag is the last one
262         // on the stack. otherwise, it's an error.
263         while (!tag_stack_.empty()) {
264                 TagPtr const cur_tag = tag_stack_.back();
265                 tag_stack_.pop_back();
266                 if (*cur_tag == xml::parsep_tag)
267                         break;
268                 writeError("Tag `" + cur_tag->tag_ + "' still open at end of paragraph. Closing.");
269                 os_ << cur_tag->writeEndTag();
270         }
271
272 #ifdef  XHTML_DEBUG
273         dumpTagStack("EndDivision");
274 #endif
275 }
276
277
278 void XMLStream::clearTagDeque()
279 {
280         while (!pending_tags_.empty()) {
281                 TagPtr const & tag = pending_tags_.front();
282                 if (*tag != xml::parsep_tag)
283                         // tabs?
284                         os_ << tag->writeTag();
285                 tag_stack_.push_back(tag);
286                 pending_tags_.pop_front();
287         }
288 }
289
290
291 XMLStream &XMLStream::operator<<(docstring const &d)
292 {
293         clearTagDeque();
294         os_ << xml::escapeString(d, escape_);
295         escape_ = ESCAPE_ALL;
296         return *this;
297 }
298
299
300 XMLStream &XMLStream::operator<<(const char *s)
301 {
302         clearTagDeque();
303         docstring const d = from_ascii(s);
304         os_ << xml::escapeString(d, escape_);
305         escape_ = ESCAPE_ALL;
306         return *this;
307 }
308
309
310 XMLStream &XMLStream::operator<<(char_type c)
311 {
312         clearTagDeque();
313         os_ << xml::escapeChar(c, escape_);
314         escape_ = ESCAPE_ALL;
315         return *this;
316 }
317
318
319 XMLStream &XMLStream::operator<<(char c)
320 {
321         clearTagDeque();
322         os_ << xml::escapeChar(c, escape_);
323         escape_ = ESCAPE_ALL;
324         return *this;
325 }
326
327
328 XMLStream &XMLStream::operator<<(int i)
329 {
330         clearTagDeque();
331         os_ << i;
332         escape_ = ESCAPE_ALL;
333         return *this;
334 }
335
336
337 XMLStream &XMLStream::operator<<(EscapeSettings e)
338 {
339         escape_ = e;
340         return *this;
341 }
342
343
344 XMLStream &XMLStream::operator<<(xml::StartTag const &tag)
345 {
346         if (tag.tag_.empty())
347                 return *this;
348         pending_tags_.push_back(makeTagPtr(tag));
349         if (tag.keepempty_)
350                 clearTagDeque();
351         return *this;
352 }
353
354
355 XMLStream &XMLStream::operator<<(xml::ParTag const &tag)
356 {
357         if (tag.tag_.empty())
358                 return *this;
359         pending_tags_.push_back(makeTagPtr(tag));
360         return *this;
361 }
362
363
364 XMLStream &XMLStream::operator<<(xml::CompTag const &tag)
365 {
366         if (tag.tag_.empty())
367                 return *this;
368         clearTagDeque();
369         os_ << tag.writeTag();
370         return *this;
371 }
372
373
374 XMLStream &XMLStream::operator<<(xml::FontTag const &tag)
375 {
376         if (tag.tag_.empty())
377                 return *this;
378         pending_tags_.push_back(makeTagPtr(tag));
379         return *this;
380 }
381
382
383 XMLStream &XMLStream::operator<<(xml::CR const &)
384 {
385         clearTagDeque();
386         os_ << from_ascii("\n");
387         return *this;
388 }
389
390
391 bool XMLStream::isTagOpen(xml::StartTag const &stag, int maxdepth) const
392 {
393         auto sit = tag_stack_.begin();
394         auto sen = tag_stack_.cend();
395         for (; sit != sen && maxdepth != 0; ++sit) {
396                 if (**sit == stag)
397                         return true;
398                 maxdepth -= 1;
399         }
400         return false;
401 }
402
403
404 bool XMLStream::isTagOpen(xml::EndTag const &etag, int maxdepth) const
405 {
406         auto sit = tag_stack_.begin();
407         auto sen = tag_stack_.cend();
408         for (; sit != sen && maxdepth != 0; ++sit) {
409                 if (etag == **sit)
410                         return true;
411                 maxdepth -= 1;
412         }
413         return false;
414 }
415
416
417 bool XMLStream::isTagPending(xml::StartTag const &stag, int maxdepth) const
418 {
419         auto sit = pending_tags_.begin();
420         auto sen = pending_tags_.cend();
421         for (; sit != sen && maxdepth != 0; ++sit) {
422                 if (**sit == stag)
423                         return true;
424                 maxdepth -= 1;
425         }
426         return false;
427 }
428
429
430 // this is complicated, because we want to make sure that
431 // everything is properly nested. the code ought to make
432 // sure of that, but we won't assert (yet) if we run into
433 // a problem. we'll just output error messages and try our
434 // best to make things work.
435 XMLStream &XMLStream::operator<<(xml::EndTag const &etag)
436 {
437         if (etag.tag_.empty())
438                 return *this;
439
440         // if this tag is pending, we can simply discard it.
441         if (!pending_tags_.empty()) {
442                 if (etag == *pending_tags_.back()) {
443                         // we have <tag></tag>, so we discard it and remove it
444                         // from the pending_tags_.
445                         pending_tags_.pop_back();
446                         return *this;
447                 }
448
449                 // there is a pending tag that isn't the one we are trying
450                 // to close.
451
452                 // is this tag itself pending?
453                 // non-const iterators because we may call erase().
454                 TagDeque::iterator dit = pending_tags_.begin();
455                 TagDeque::iterator const den = pending_tags_.end();
456                 for (; dit != den; ++dit) {
457                         if (etag == **dit) {
458                                 // it was pending, so we just erase it
459                                 writeError("Tried to close pending tag `" + to_utf8(etag.tag_)
460                                                    + "' when other tags were pending. Last pending tag is `"
461                                                    + to_utf8(pending_tags_.back()->writeTag())
462                                                    + "'. Tag discarded.");
463                                 pending_tags_.erase(dit);
464                                 return *this;
465                         }
466                 }
467                 // so etag isn't itself pending. is it even open?
468                 if (!isTagOpen(etag)) {
469                         writeError("Tried to close `" + to_utf8(etag.tag_)
470                                            + "' when tag was not open. Tag discarded.");
471                         return *this;
472                 }
473                 // ok, so etag is open.
474                 // our strategy will be as below: we will do what we need to
475                 // do to close this tag.
476                 string estr = "Closing tag `" + to_utf8(etag.tag_)
477                                           + "' when other tags are pending. Discarded pending tags:\n";
478                 for (dit = pending_tags_.begin(); dit != den; ++dit)
479                         estr += to_utf8(xml::escapeString((*dit)->writeTag(), XMLStream::ESCAPE_ALL)) + "\n";
480                 writeError(estr);
481                 // clear the pending tags...
482                 pending_tags_.clear();
483                 // ...and then just fall through.
484         }
485
486         // make sure there are tags to be closed
487         if (tag_stack_.empty()) {
488                 writeError("Tried to close `" + etag.tag_
489                                    + "' when no tags were open!");
490                 return *this;
491         }
492
493         // is the tag we are closing the last one we opened?
494         if (etag == *tag_stack_.back()) {
495                 // output it...
496                 os_ << etag.writeEndTag();
497                 // ...and forget about it
498                 tag_stack_.pop_back();
499                 return *this;
500         }
501
502         // we are trying to close a tag other than the one last opened.
503         // let's first see if this particular tag is still open somehow.
504         if (!isTagOpen(etag)) {
505                 writeError("Tried to close `" + etag.tag_
506                                    + "' when tag was not open. Tag discarded.");
507                 return *this;
508         }
509
510         // so the tag was opened, but other tags have been opened since
511         // and not yet closed.
512         // if it's a font tag, though...
513         if (etag.asFontTag()) {
514                 // it won't be a problem if the other tags open since this one
515                 // are also font tags.
516                 TagDeque::const_reverse_iterator rit = tag_stack_.rbegin();
517                 TagDeque::const_reverse_iterator ren = tag_stack_.rend();
518                 for (; rit != ren; ++rit) {
519                         if (etag == **rit)
520                                 break;
521                         if (!(*rit)->asFontTag()) {
522                                 // we'll just leave it and, presumably, have to close it later.
523                                 writeError("Unable to close font tag `" + etag.tag_
524                                                    + "' due to open non-font tag `" + (*rit)->tag_ + "'.");
525                                 return *this;
526                         }
527                 }
528
529                 // so we have e.g.:
530                 //    <em>this is <strong>bold
531                 // and are being asked to closed em. we want:
532                 //    <em>this is <strong>bold</strong></em><strong>
533                 // first, we close the intervening tags...
534                 TagPtr *curtag = &tag_stack_.back();
535                 // ...remembering them in a stack.
536                 TagDeque fontstack;
537                 while (etag != **curtag) {
538                         os_ << (*curtag)->writeEndTag();
539                         fontstack.push_back(*curtag);
540                         tag_stack_.pop_back();
541                         curtag = &tag_stack_.back();
542                 }
543                 os_ << etag.writeEndTag();
544                 tag_stack_.pop_back();
545
546                 // ...and restore the other tags.
547                 rit = fontstack.rbegin();
548                 ren = fontstack.rend();
549                 for (; rit != ren; ++rit)
550                         pending_tags_.push_back(*rit);
551                 return *this;
552         }
553
554         // it wasn't a font tag.
555         // so other tags were opened before this one and not properly closed.
556         // so we'll close them, too. that may cause other issues later, but it
557         // at least guarantees proper nesting.
558         writeError("Closing tag `" + etag.tag_
559                            + "' when other tags are open, namely:");
560         TagPtr *curtag = &tag_stack_.back();
561         while (etag != **curtag) {
562                 writeError((*curtag)->tag_);
563                 if (**curtag != xml::parsep_tag)
564                         os_ << (*curtag)->writeEndTag();
565                 tag_stack_.pop_back();
566                 curtag = &tag_stack_.back();
567         }
568         // curtag is now the one we actually want.
569         os_ << (*curtag)->writeEndTag();
570         tag_stack_.pop_back();
571
572         return *this;
573 }
574
575
576 docstring xml::uniqueID(docstring const & label)
577 {
578         // thread-safe
579         static atomic_uint seed(1000);
580         return label + convert<docstring>(++seed);
581 }
582
583
584 docstring xml::cleanID(docstring const & orig)
585 {
586         // The standard xml:id only allows letters,
587         // digits, '-' and '.' in a name.
588         // This routine replaces illegal characters by '-' or '.'
589         // and adds a number for uniqueness if need be.
590         docstring const allowed = from_ascii(".-_");
591
592         // Use a cache of already mangled names: the alterations may merge several IDs as one. This ensures that the IDs
593         // are not mixed up in the document.
594         typedef map<docstring, docstring> MangledMap;
595         static QThreadStorage<MangledMap> tMangledNames;
596         static QThreadStorage<int> tMangleID;
597
598         MangledMap & mangledNames = tMangledNames.localData();
599
600         // If the name is already known, just return it.
601         MangledMap::const_iterator const known = mangledNames.find(orig);
602         if (known != mangledNames.end())
603                 return known->second;
604
605         // Start creating the mangled name by iterating over the characters.
606         docstring content;
607         docstring::const_iterator it  = orig.begin();
608         docstring::const_iterator end = orig.end();
609
610         // Make sure it starts with a letter.
611         if (!isAlphaASCII(*it) && allowed.find(*it) >= allowed.size())
612                 content += "x";
613
614         // Do the mangling.
615         bool mangle = false; // Indicates whether the ID had to be changed, i.e. if ID no more ensured to be unique.
616         for (; it != end; ++it) {
617                 char_type c = *it;
618                 if (isAlphaASCII(c) || isDigitASCII(c) || c == '-' || c == '.'
619                       || allowed.find(c) < allowed.size())
620                         content += c;
621                 else if (c == '_' || c == ' ') {
622                         mangle = true;
623                         content += "-";
624                 }
625                 else if (c == ':' || c == ',' || c == ';' || c == '!') {
626                         mangle = true;
627                         content += ".";
628                 }
629                 else {
630                         mangle = true;
631                         content += "-";
632                 }
633         }
634
635         if (mangle) {
636                 int & mangleID = tMangleID.localData();
637                 content += "-" + convert<docstring>(mangleID++);
638         }
639
640         mangledNames[orig] = content;
641
642         return content;
643 }
644
645
646 void xml::openTag(odocstream & os, string const & name, string const & attribute)
647 {
648     // FIXME UNICODE
649     // This should be fixed in layout files later.
650     string param = subst(attribute, "<", "\"");
651     param = subst(param, ">", "\"");
652
653     // Note: we ignore the name if it empty or if it is a comment "<!-- -->" or
654     // if the name is *dummy*.
655     // We ignore dummy because dummy is not a valid docbook element and it is
656     // the internal name given to single paragraphs in the latex output.
657     // This allow us to simplify the code a lot and is a reasonable compromise.
658     if (!name.empty() && name != "!-- --" && name != "dummy") {
659         os << '<' << from_ascii(name);
660         if (!param.empty())
661             os << ' ' << from_ascii(param);
662         os << '>';
663     }
664 }
665
666
667 void xml::closeTag(odocstream & os, string const & name)
668 {
669     if (!name.empty() && name != "!-- --" && name != "dummy")
670         os << "</" << from_ascii(name) << '>';
671 }
672
673
674 void xml::openTag(Buffer const & buf, odocstream & os,
675                    OutputParams const & runparams, Paragraph const & par)
676 {
677     Layout const & style = par.layout();
678     string const & name = style.latexname();
679     string param = style.latexparam();
680     Counters & counters = buf.params().documentClass().counters();
681
682     string id = par.getID(buf, runparams);
683
684     string attribute;
685     if (!id.empty()) {
686         if (param.find('#') != string::npos) {
687             string::size_type pos = param.find("id=<");
688             string::size_type end = param.find(">");
689             if( pos != string::npos && end != string::npos)
690                 param.erase(pos, end-pos + 1);
691         }
692         attribute = id + ' ' + param;
693     } else {
694         if (param.find('#') != string::npos) {
695             // FIXME UNICODE
696             if (!style.counter.empty())
697                 // This uses InternalUpdate at the moment becuase xml output
698                 // does not do anything with tracked counters, and it would need
699                 // to track layouts if it did want to use them.
700                 counters.step(style.counter, InternalUpdate);
701             else
702                 counters.step(from_ascii(name), InternalUpdate);
703             int i = counters.value(from_ascii(name));
704             attribute = subst(param, "#", convert<string>(i));
705         } else {
706             attribute = param;
707         }
708     }
709     openTag(os, name, attribute);
710 }
711
712
713 void xml::closeTag(odocstream & os, Paragraph const & par)
714 {
715     Layout const & style = par.layout();
716     closeTag(os, style.latexname());
717 }
718
719
720 } // namespace lyx