]> git.lyx.org Git - lyx.git/blob - src/output_xhtml.cpp
dcc3f69c333c438a451452d17c60b38a1110564a
[lyx.git] / src / output_xhtml.cpp
1 /**
2  * \file output_xhtml.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Richard Heck
7  *
8  * This code is based upon output_docbook.cpp
9  *
10  * Full author contact details are available in file CREDITS.
11  */
12
13 #include <config.h>
14
15 #include "output_xhtml.h"
16
17 #include "Buffer.h"
18 #include "buffer_funcs.h"
19 #include "BufferParams.h"
20 #include "Counters.h"
21 #include "Font.h"
22 #include "Layout.h"
23 #include "OutputParams.h"
24 #include "Paragraph.h"
25 #include "ParagraphList.h"
26 #include "ParagraphParameters.h"
27 #include "xml.h"
28 #include "Text.h"
29 #include "TextClass.h"
30
31 #include "support/convert.h"
32 #include "support/debug.h"
33 #include "support/lassert.h"
34 #include "support/lstrings.h"
35 #include "support/textutils.h"
36
37 #include <vector>
38
39 // Uncomment to activate debugging code.
40 // #define XHTML_DEBUG
41
42 using namespace std;
43 using namespace lyx::support;
44
45 namespace lyx {
46
47
48 docstring const & fontToHtmlTag(xml::FontTypes type)
49 {
50     switch(type) {
51         case xml::FontTypes::FT_EMPH:
52             return from_utf8("em");
53         case xml::FontTypes::FT_BOLD:
54             return from_utf8("b");
55         case xml::FontTypes::FT_NOUN:
56             return from_utf8("dfn");
57         case xml::FontTypes::FT_UBAR:
58         case xml::FontTypes::FT_WAVE:
59         case xml::FontTypes::FT_DBAR:
60             return from_utf8("u");
61         case xml::FontTypes::FT_SOUT:
62         case xml::FontTypes::FT_XOUT:
63             return from_utf8("del");
64         case xml::FontTypes::FT_ITALIC:
65             return from_utf8("i");
66         case xml::FontTypes::FT_UPRIGHT:
67         case xml::FontTypes::FT_SLANTED:
68         case xml::FontTypes::FT_SMALLCAPS:
69         case xml::FontTypes::FT_ROMAN:
70         case xml::FontTypes::FT_SANS:
71         case xml::FontTypes::FT_TYPE:
72         case xml::FontTypes::FT_SIZE_TINY:
73         case xml::FontTypes::FT_SIZE_SCRIPT:
74         case xml::FontTypes::FT_SIZE_FOOTNOTE:
75         case xml::FontTypes::FT_SIZE_SMALL:
76         case xml::FontTypes::FT_SIZE_NORMAL:
77         case xml::FontTypes::FT_SIZE_LARGE:
78         case xml::FontTypes::FT_SIZE_LARGER:
79         case xml::FontTypes::FT_SIZE_LARGEST:
80         case xml::FontTypes::FT_SIZE_HUGE:
81         case xml::FontTypes::FT_SIZE_HUGER:
82         case xml::FontTypes::FT_SIZE_INCREASE:
83         case xml::FontTypes::FT_SIZE_DECREASE:
84             return from_utf8("span");
85     }
86     // kill warning
87     return docstring();
88 }
89
90
91 namespace {
92
93 string fontToAttribute(xml::FontTypes type)
94 {
95         switch(type) {
96         case xml::FontTypes::FT_EMPH:
97         case xml::FontTypes::FT_BOLD:
98                 return "";
99         case xml::FontTypes::FT_NOUN:
100                 return "class='lyxnoun'";
101         case xml::FontTypes::FT_UBAR:
102                 return "";
103         case xml::FontTypes::FT_DBAR:
104                 return "class='dline'";
105         case xml::FontTypes::FT_XOUT:
106         case xml::FontTypes::FT_SOUT:
107                 return "class='strikeout'";
108         case xml::FontTypes::FT_WAVE:
109                 return "class='wline'";
110         case xml::FontTypes::FT_ITALIC:
111                 return "";
112         case xml::FontTypes::FT_UPRIGHT:
113                 return "style='font-style:normal;'";
114         case xml::FontTypes::FT_SLANTED:
115                 return "style='font-style:oblique;'";
116         case xml::FontTypes::FT_SMALLCAPS:
117                 return "style='font-variant:small-caps;'";
118         case xml::FontTypes::FT_ROMAN:
119                 return "style='font-family:serif;'";
120         case xml::FontTypes::FT_SANS:
121                 return "style='font-family:sans-serif;'";
122         case xml::FontTypes::FT_TYPE:
123                 return "style='font-family:monospace;'";
124         case xml::FontTypes::FT_SIZE_TINY:
125         case xml::FontTypes::FT_SIZE_SCRIPT:
126         case xml::FontTypes::FT_SIZE_FOOTNOTE:
127                 return "style='font-size:x-small;'";
128         case xml::FontTypes::FT_SIZE_SMALL:
129                 return "style='font-size:small;'";
130         case xml::FontTypes::FT_SIZE_NORMAL:
131                 return "style='font-size:normal;'";
132         case xml::FontTypes::FT_SIZE_LARGE:
133                 return "style='font-size:large;'";
134         case xml::FontTypes::FT_SIZE_LARGER:
135         case xml::FontTypes::FT_SIZE_LARGEST:
136                 return "style='font-size:x-large;'";
137         case xml::FontTypes::FT_SIZE_HUGE:
138         case xml::FontTypes::FT_SIZE_HUGER:
139                 return "style='font-size:xx-large;'";
140         case xml::FontTypes::FT_SIZE_INCREASE:
141                 return "style='font-size:larger;'";
142         case xml::FontTypes::FT_SIZE_DECREASE:
143                 return "style='font-size:smaller;'";
144         }
145         // kill warning
146         return "";
147 }
148
149 } // end anonymous namespace
150
151
152 xml::FontTag xhtmlStartFontTag(xml::FontTypes type)
153 {
154         return xml::FontTag(fontToHtmlTag(type), from_utf8(fontToAttribute(type)), type);
155 }
156
157
158 xml::EndFontTag xhtmlEndFontTag(xml::FontTypes type)
159 {
160         return xml::EndFontTag(fontToHtmlTag(type), type);
161 }
162
163 namespace {
164
165 // convenience functions
166
167 inline void openParTag(XMLStream & xs, Layout const & lay,
168                        std::string parlabel)
169 {
170         xs << xml::ParTag(lay.htmltag(), lay.htmlattr(), parlabel);
171 }
172
173
174 void openParTag(XMLStream & xs, Layout const & lay,
175                 ParagraphParameters const & params,
176                 std::string parlabel)
177 {
178         // FIXME Are there other things we should handle here?
179         string const align = alignmentToCSS(params.align());
180         if (align.empty()) {
181                 openParTag(xs, lay, parlabel);
182                 return;
183         }
184         string attrs = lay.htmlattr() + " style='text-align: " + align + ";'";
185         xs << xml::ParTag(lay.htmltag(), attrs, parlabel);
186 }
187
188
189 inline void closeTag(XMLStream & xs, Layout const & lay)
190 {
191         xs << xml::EndTag(lay.htmltag());
192 }
193
194
195 inline void openLabelTag(XMLStream & xs, Layout const & lay)
196 {
197         xs << xml::StartTag(lay.htmllabeltag(), lay.htmllabelattr());
198 }
199
200
201 inline void closeLabelTag(XMLStream & xs, Layout const & lay)
202 {
203         xs << xml::EndTag(lay.htmllabeltag());
204 }
205
206
207 inline void openItemTag(XMLStream & xs, Layout const & lay)
208 {
209         xs << xml::StartTag(lay.htmlitemtag(), lay.htmlitemattr(), true);
210 }
211
212
213 void openItemTag(XMLStream & xs, Layout const & lay,
214              ParagraphParameters const & params)
215 {
216         // FIXME Are there other things we should handle here?
217         string const align = alignmentToCSS(params.align());
218         if (align.empty()) {
219                 openItemTag(xs, lay);
220                 return;
221         }
222         string attrs = lay.htmlattr() + " style='text-align: " + align + ";'";
223         xs << xml::StartTag(lay.htmlitemtag(), attrs);
224 }
225
226
227 inline void closeItemTag(XMLStream & xs, Layout const & lay)
228 {
229         xs << xml::EndTag(lay.htmlitemtag());
230 }
231
232 // end of convenience functions
233
234 ParagraphList::const_iterator findLastParagraph(
235         ParagraphList::const_iterator p,
236         ParagraphList::const_iterator const & pend)
237 {
238         for (++p; p != pend && p->layout().latextype == LATEX_PARAGRAPH; ++p)
239                 ;
240
241         return p;
242 }
243
244
245 ParagraphList::const_iterator findEndOfEnvironment(
246                 ParagraphList::const_iterator const & pstart,
247                 ParagraphList::const_iterator const & pend)
248 {
249         ParagraphList::const_iterator p = pstart;
250         Layout const & bstyle = p->layout();
251         size_t const depth = p->params().depth();
252         for (++p; p != pend; ++p) {
253                 Layout const & style = p->layout();
254                 // It shouldn't happen that e.g. a section command occurs inside
255                 // a quotation environment, at a higher depth, but as of 6/2009,
256                 // it can happen. We pretend that it's just at lowest depth.
257                 if (style.latextype == LATEX_COMMAND)
258                         return p;
259
260                 // If depth is down, we're done
261                 if (p->params().depth() < depth)
262                         return p;
263
264                 // If depth is up, we're not done
265                 if (p->params().depth() > depth)
266                         continue;
267
268                 // FIXME I am not sure about the first check.
269                 // Surely we *could* have different layouts that count as
270                 // LATEX_PARAGRAPH, right?
271                 if (style.latextype == LATEX_PARAGRAPH || style != bstyle)
272                         return p;
273         }
274         return pend;
275 }
276
277
278 ParagraphList::const_iterator makeParagraphs(Buffer const & buf,
279                                             XMLStream & xs,
280                                             OutputParams const & runparams,
281                                             Text const & text,
282                                             ParagraphList::const_iterator const & pbegin,
283                                             ParagraphList::const_iterator const & pend)
284 {
285         ParagraphList::const_iterator const begin = text.paragraphs().begin();
286         ParagraphList::const_iterator par = pbegin;
287         for (; par != pend; ++par) {
288                 Layout const & lay = par->layout();
289                 if (!lay.counter.empty())
290                         buf.masterBuffer()->params().
291                             documentClass().counters().step(lay.counter, OutputUpdate);
292
293                 // FIXME We should see if there's a label to be output and
294                 // do something with it.
295                 if (par != pbegin)
296                         xs << xml::CR();
297
298                 // We want to open the paragraph tag if:
299                 //   (i) the current layout permits multiple paragraphs
300                 //  (ii) we are either not already inside a paragraph (HTMLIsBlock) OR
301                 //       we are, but this is not the first paragraph
302                 //
303                 // But there is also a special case, and we first see whether we are in it.
304                 // We do not want to open the paragraph tag if this paragraph contains
305                 // only one item, and that item is "inline", i.e., not HTMLIsBlock (such
306                 // as a branch). On the other hand, if that single item has a font change
307                 // applied to it, then we still do need to open the paragraph.
308                 //
309                 // Obviously, this is very fragile. The main reason we need to do this is
310                 // because of branches, e.g., a branch that contains an entire new section.
311                 // We do not really want to wrap that whole thing in a <div>...</div>.
312                 bool special_case = false;
313                 Inset const * specinset = par->size() == 1 ? par->getInset(0) : nullptr;
314                 if (specinset && !specinset->getLayout().htmlisblock()) {
315                         Layout const & style = par->layout();
316                         FontInfo const first_font = style.labeltype == LABEL_MANUAL ?
317                                                 style.labelfont : style.font;
318                         FontInfo const our_font =
319                                 par->getFont(buf.masterBuffer()->params(), 0,
320                                        text.outerFont(distance(begin, par))).fontInfo();
321                         if (first_font == our_font)
322                                 special_case = true;
323                 }
324
325                 bool const open_par = runparams.html_make_pars
326                         && (!runparams.html_in_par || par != pbegin)
327                         && !special_case;
328
329                 // We want to issue the closing tag if either:
330                 //   (i)  We opened it, and either html_in_par is false,
331                 //        or we're not in the last paragraph, anyway.
332                 //   (ii) We didn't open it and html_in_par is true,
333                 //        but we are in the first par, and there is a next par.
334                 ParagraphList::const_iterator nextpar = par;
335                 ++nextpar;
336                 bool const close_par =
337                         (open_par && (!runparams.html_in_par || nextpar != pend))
338                         || (!open_par && runparams.html_in_par && par == pbegin && nextpar != pend);
339
340                 if (open_par) {
341                         // We do not issue the paragraph id if we are doing
342                         // this for the TOC (or some similar purpose)
343                         openParTag(xs, lay, par->params(),
344                                    runparams.for_toc ? "" : par->magicLabel());
345                 }
346
347                 docstring const deferred = par->simpleLyXHTMLOnePar(buf, xs,
348                         runparams, text.outerFont(distance(begin, par)),
349                         open_par, close_par);
350
351                 if (close_par) {
352                         closeTag(xs, lay);
353                         xs << xml::CR();
354                 }
355
356                 if (!deferred.empty()) {
357                         xs << XMLStream::ESCAPE_NONE << deferred << xml::CR();
358                 }
359         }
360         return pend;
361 }
362
363
364 ParagraphList::const_iterator makeBibliography(Buffer const & buf,
365                                 XMLStream & xs,
366                                 OutputParams const & runparams,
367                                 Text const & text,
368                                 ParagraphList::const_iterator const & pbegin,
369                                 ParagraphList::const_iterator const & pend)
370 {
371         // FIXME XHTML
372         // Use TextClass::htmlTOCLayout() to figure out how we should look.
373         xs << xml::StartTag("h2", "class='bibliography'")
374            << pbegin->layout().labelstring(false)
375            << xml::EndTag("h2")
376            << xml::CR()
377            << xml::StartTag("div", "class='bibliography'")
378            << xml::CR();
379         makeParagraphs(buf, xs, runparams, text, pbegin, pend);
380         xs << xml::EndTag("div");
381         return pend;
382 }
383
384
385 bool isNormalEnv(Layout const & lay)
386 {
387         return lay.latextype == LATEX_ENVIRONMENT
388             || lay.latextype == LATEX_BIB_ENVIRONMENT;
389 }
390
391
392 ParagraphList::const_iterator makeEnvironment(Buffer const & buf,
393                                               XMLStream & xs,
394                                               OutputParams const & runparams,
395                                               Text const & text,
396                                               ParagraphList::const_iterator const & pbegin,
397                                               ParagraphList::const_iterator const & pend)
398 {
399         ParagraphList::const_iterator const begin = text.paragraphs().begin();
400         ParagraphList::const_iterator par = pbegin;
401         Layout const & bstyle = par->layout();
402         depth_type const origdepth = pbegin->params().depth();
403
404         // open tag for this environment
405         openParTag(xs, bstyle, pbegin->magicLabel());
406         xs << xml::CR();
407
408         // we will on occasion need to remember a layout from before.
409         Layout const * lastlay = nullptr;
410
411         while (par != pend) {
412                 Layout const & style = par->layout();
413                 // the counter only gets stepped if we're in some kind of list,
414                 // or if it's the first time through.
415                 // note that enum, etc, are handled automatically.
416                 // FIXME There may be a bug here about user defined enumeration
417                 // types. If so, then we'll need to take the counter and add "i",
418                 // "ii", etc, as with enum.
419                 Counters & cnts = buf.masterBuffer()->params().documentClass().counters();
420                 docstring const & cntr = style.counter;
421                 if (!style.counter.empty()
422                     && (par == pbegin || !isNormalEnv(style))
423                                 && cnts.hasCounter(cntr)
424                 )
425                         cnts.step(cntr, OutputUpdate);
426                 ParagraphList::const_iterator send;
427
428                 switch (style.latextype) {
429                 case LATEX_ENVIRONMENT:
430                 case LATEX_LIST_ENVIRONMENT:
431                 case LATEX_ITEM_ENVIRONMENT: {
432                         // There are two possibilities in this case.
433                         // One is that we are still in the environment in which we
434                         // started---which we will be if the depth is the same.
435                         if (par->params().depth() == origdepth) {
436                                 LATTEST(bstyle == style);
437                                 if (lastlay != nullptr) {
438                                         closeItemTag(xs, *lastlay);
439                                         lastlay = nullptr;
440                                 }
441
442                                 // this will be positive, if we want to skip the
443                                 // initial word (if it's been taken for the label).
444                                 pos_type sep = 0;
445                                 bool const labelfirst = style.htmllabelfirst();
446                                 if (!labelfirst)
447                                         openItemTag(xs, style, par->params());
448
449                                 // label output
450                                 if (style.labeltype != LABEL_NO_LABEL &&
451                                     style.htmllabeltag() != "NONE") {
452                                         if (isNormalEnv(style)) {
453                                                 // in this case, we print the label only for the first
454                                                 // paragraph (as in a theorem).
455                                                 if (par == pbegin) {
456                                                         docstring const lbl =
457                                                                         pbegin->params().labelString();
458                                                         if (!lbl.empty()) {
459                                                                 openLabelTag(xs, style);
460                                                                 xs << lbl;
461                                                                 closeLabelTag(xs, style);
462                                                         }
463                                                         xs << xml::CR();
464                                                 }
465                                         } else { // some kind of list
466                                                 if (style.labeltype == LABEL_MANUAL) {
467                                                         openLabelTag(xs, style);
468                                                         sep = par->firstWordLyXHTML(xs, runparams);
469                                                         closeLabelTag(xs, style);
470                                                         xs << xml::CR();
471                                                 }
472                                                 else {
473                                                         openLabelTag(xs, style);
474                                                         xs << par->params().labelString();
475                                                         closeLabelTag(xs, style);
476                                                         xs << xml::CR();
477                                                 }
478                                         }
479                                 } // end label output
480
481                                 if (labelfirst)
482                                         openItemTag(xs, style, par->params());
483
484                                 docstring deferred = par->simpleLyXHTMLOnePar(buf, xs, runparams,
485                                         text.outerFont(distance(begin, par)), true, true, sep);
486                                 xs << XMLStream::ESCAPE_NONE << deferred;
487                                 ++par;
488
489                                 // We may not want to close the tag yet, in particular:
490                                 // If we're not at the end...
491                                 if (par != pend
492                                         //  and are doing items...
493                                          && !isNormalEnv(style)
494                                          // and if the depth has changed...
495                                          && par->params().depth() != origdepth) {
496                                          // then we'll save this layout for later, and close it when
497                                          // we get another item.
498                                         lastlay = &style;
499                                 } else
500                                         closeItemTag(xs, style);
501                                 xs << xml::CR();
502                         }
503                         // The other possibility is that the depth has increased, in which
504                         // case we need to recurse.
505                         else {
506                                 send = findEndOfEnvironment(par, pend);
507                                 par = makeEnvironment(buf, xs, runparams, text, par, send);
508                         }
509                         break;
510                 }
511                 case LATEX_PARAGRAPH:
512                         send = findLastParagraph(par, pend);
513                         par = makeParagraphs(buf, xs, runparams, text, par, send);
514                         break;
515                 // Shouldn't happen
516                 case LATEX_BIB_ENVIRONMENT:
517                         send = par;
518                         ++send;
519                         par = makeParagraphs(buf, xs, runparams, text, par, send);
520                         break;
521                 // Shouldn't happen
522                 case LATEX_COMMAND:
523                         ++par;
524                         break;
525                 }
526         }
527
528         if (lastlay != nullptr)
529                 closeItemTag(xs, *lastlay);
530         closeTag(xs, bstyle);
531         xs << xml::CR();
532         return pend;
533 }
534
535
536 void makeCommand(Buffer const & buf,
537                  XMLStream & xs,
538                  OutputParams const & runparams,
539                  Text const & text,
540                  ParagraphList::const_iterator const & pbegin)
541 {
542         Layout const & style = pbegin->layout();
543         if (!style.counter.empty())
544                 buf.masterBuffer()->params().
545                     documentClass().counters().step(style.counter, OutputUpdate);
546
547         bool const make_parid = !runparams.for_toc && runparams.html_make_pars;
548
549         openParTag(xs, style, pbegin->params(),
550                    make_parid ? pbegin->magicLabel() : "");
551
552         // Label around sectioning number:
553         // FIXME Probably need to account for LABEL_MANUAL
554         // FIXME Probably also need now to account for labels ABOVE and CENTERED.
555         if (style.labeltype != LABEL_NO_LABEL) {
556                 openLabelTag(xs, style);
557                 xs << pbegin->params().labelString();
558                 closeLabelTag(xs, style);
559                 // Otherwise the label might run together with the text
560                 xs << from_ascii(" ");
561         }
562
563         ParagraphList::const_iterator const begin = text.paragraphs().begin();
564         pbegin->simpleLyXHTMLOnePar(buf, xs, runparams,
565                         text.outerFont(distance(begin, pbegin)));
566         closeTag(xs, style);
567         xs << xml::CR();
568 }
569
570 } // end anonymous namespace
571
572
573 void xhtmlParagraphs(Text const & text,
574                        Buffer const & buf,
575                        XMLStream & xs,
576                        OutputParams const & runparams)
577 {
578         ParagraphList const & paragraphs = text.paragraphs();
579         if (runparams.par_begin == runparams.par_end) {
580                 runparams.par_begin = 0;
581                 runparams.par_end = paragraphs.size();
582         }
583         pit_type bpit = runparams.par_begin;
584         pit_type const epit = runparams.par_end;
585         LASSERT(bpit < epit,
586                 { xs << XMLStream::ESCAPE_NONE << "<!-- XHTML output error! -->\n"; return; });
587
588         OutputParams ourparams = runparams;
589         ParagraphList::const_iterator const pend =
590                 (epit == (int) paragraphs.size()) ?
591                         paragraphs.end() : paragraphs.iterator_at(epit);
592         while (bpit < epit) {
593                 ParagraphList::const_iterator par = paragraphs.iterator_at(bpit);
594                 if (par->params().startOfAppendix()) {
595                         // We want to reset the counter corresponding to toplevel sectioning
596                         Layout const & lay =
597                                 buf.masterBuffer()->params().documentClass().getTOCLayout();
598                         docstring const cnt = lay.counter;
599                         if (!cnt.empty()) {
600                                 Counters & cnts =
601                                         buf.masterBuffer()->params().documentClass().counters();
602                                 cnts.reset(cnt);
603                         }
604                 }
605                 Layout const & style = par->layout();
606                 ParagraphList::const_iterator const lastpar = par;
607                 ParagraphList::const_iterator send;
608
609                 switch (style.latextype) {
610                 case LATEX_COMMAND: {
611                         // The files with which we are working never have more than
612                         // one paragraph in a command structure.
613                         // FIXME
614                         // if (ourparams.html_in_par)
615                         //   fix it so we don't get sections inside standard, e.g.
616                         // note that we may then need to make runparams not const, so we
617                         // can communicate that back.
618                         // FIXME Maybe this fix should be in the routines themselves, in case
619                         // they are called from elsewhere.
620                         makeCommand(buf, xs, ourparams, text, par);
621                         ++par;
622                         break;
623                 }
624                 case LATEX_ENVIRONMENT:
625                 case LATEX_LIST_ENVIRONMENT:
626                 case LATEX_ITEM_ENVIRONMENT: {
627                         // FIXME Same fix here.
628                         send = findEndOfEnvironment(par, pend);
629                         par = makeEnvironment(buf, xs, ourparams, text, par, send);
630                         break;
631                 }
632                 case LATEX_BIB_ENVIRONMENT: {
633                         // FIXME Same fix here.
634                         send = findEndOfEnvironment(par, pend);
635                         par = makeBibliography(buf, xs, ourparams, text, par, send);
636                         break;
637                 }
638                 case LATEX_PARAGRAPH:
639                         send = findLastParagraph(par, pend);
640                         par = makeParagraphs(buf, xs, ourparams, text, par, send);
641                         break;
642                 }
643                 bpit += distance(lastpar, par);
644         }
645 }
646
647
648 string alignmentToCSS(LyXAlignment align)
649 {
650         switch (align) {
651         case LYX_ALIGN_BLOCK:
652                 // we are NOT going to use text-align: justify!!
653         case LYX_ALIGN_LEFT:
654                 return "left";
655         case LYX_ALIGN_RIGHT:
656                 return "right";
657         case LYX_ALIGN_CENTER:
658                 return "center";
659         default:
660                 break;
661         }
662         return "";
663 }
664
665 } // namespace lyx