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