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