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