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.
8 * This code is based upon output_docbook.cpp
10 * Full author contact details are available in file CREDITS.
15 #include "output_xhtml.h"
18 #include "buffer_funcs.h"
19 #include "BufferParams.h"
23 #include "OutputParams.h"
24 #include "Paragraph.h"
25 #include "ParagraphList.h"
26 #include "ParagraphParameters.h"
29 #include "TextClass.h"
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"
39 // Uncomment to activate debugging code.
40 // #define XHTML_DEBUG
43 using namespace lyx::support;
48 docstring fontToHtmlTag(xml::FontTypes 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");
91 docstring fontToHtmlAttribute(xml::FontTypes type)
94 case xml::FontTypes::FT_EMPH:
95 case xml::FontTypes::FT_BOLD:
96 return from_ascii("");
97 case xml::FontTypes::FT_NOUN:
98 return from_ascii("class='lyxnoun'");
99 case xml::FontTypes::FT_UBAR:
100 return from_ascii("");
101 case xml::FontTypes::FT_DBAR:
102 return from_ascii("class='dline'");
103 case xml::FontTypes::FT_XOUT:
104 case xml::FontTypes::FT_SOUT:
105 return from_ascii("class='strikeout'");
106 case xml::FontTypes::FT_WAVE:
107 return from_ascii("class='wline'");
108 case xml::FontTypes::FT_ITALIC:
109 return from_ascii("");
110 case xml::FontTypes::FT_UPRIGHT:
111 return from_ascii("style='font-style:normal;'");
112 case xml::FontTypes::FT_SLANTED:
113 return from_ascii("style='font-style:oblique;'");
114 case xml::FontTypes::FT_SMALLCAPS:
115 return from_ascii("style='font-variant:small-caps;'");
116 case xml::FontTypes::FT_ROMAN:
117 return from_ascii("style='font-family:serif;'");
118 case xml::FontTypes::FT_SANS:
119 return from_ascii("style='font-family:sans-serif;'");
120 case xml::FontTypes::FT_TYPE:
121 return from_ascii("style='font-family:monospace;'");
122 case xml::FontTypes::FT_SIZE_TINY:
123 case xml::FontTypes::FT_SIZE_SCRIPT:
124 case xml::FontTypes::FT_SIZE_FOOTNOTE:
125 return from_ascii("style='font-size:x-small;'");
126 case xml::FontTypes::FT_SIZE_SMALL:
127 return from_ascii("style='font-size:small;'");
128 case xml::FontTypes::FT_SIZE_NORMAL:
129 return from_ascii("style='font-size:normal;'");
130 case xml::FontTypes::FT_SIZE_LARGE:
131 return from_ascii("style='font-size:large;'");
132 case xml::FontTypes::FT_SIZE_LARGER:
133 case xml::FontTypes::FT_SIZE_LARGEST:
134 return from_ascii("style='font-size:x-large;'");
135 case xml::FontTypes::FT_SIZE_HUGE:
136 case xml::FontTypes::FT_SIZE_HUGER:
137 return from_ascii("style='font-size:xx-large;'");
138 case xml::FontTypes::FT_SIZE_INCREASE:
139 return from_ascii("style='font-size:larger;'");
140 case xml::FontTypes::FT_SIZE_DECREASE:
141 return from_ascii("style='font-size:smaller;'");
144 return from_ascii("");
148 xml::FontTag xhtmlStartFontTag(xml::FontTypes type)
150 return xml::FontTag(fontToHtmlTag(type), fontToHtmlAttribute(type), type);
154 xml::EndFontTag xhtmlEndFontTag(xml::FontTypes type)
156 return xml::EndFontTag(fontToHtmlTag(type), type);
161 // convenience functions
163 inline void openParTag(XMLStream & xs, Layout const & lay,
164 const std::string & parlabel)
166 string attrs = lay.htmlattr();
167 if (!parlabel.empty())
168 attrs += " id='" + parlabel + "'";
169 xs << xml::ParTag(lay.htmltag(), attrs);
173 void openParTag(XMLStream & xs, Layout const & lay,
174 ParagraphParameters const & params,
175 const std::string & parlabel)
177 // FIXME Are there other things we should handle here?
178 string const align = alignmentToCSS(params.align());
180 openParTag(xs, lay, parlabel);
183 string attrs = lay.htmlattr() + " style='text-align: " + align + ";'";
184 if (!parlabel.empty())
185 attrs += " id='" + parlabel + "'";
186 xs << xml::ParTag(lay.htmltag(), attrs);
190 inline void closeTag(XMLStream & xs, Layout const & lay)
192 xs << xml::EndTag(lay.htmltag());
196 inline void openLabelTag(XMLStream & xs, Layout const & lay)
198 xs << xml::StartTag(lay.htmllabeltag(), lay.htmllabelattr());
202 inline void closeLabelTag(XMLStream & xs, Layout const & lay)
204 xs << xml::EndTag(lay.htmllabeltag());
208 inline void openItemTag(XMLStream & xs, Layout const & lay)
210 xs << xml::StartTag(lay.htmlitemtag(), lay.htmlitemattr(), true);
214 void openItemTag(XMLStream & xs, Layout const & lay,
215 ParagraphParameters const & params)
217 // FIXME Are there other things we should handle here?
218 string const align = alignmentToCSS(params.align());
220 openItemTag(xs, lay);
223 string attrs = lay.htmlattr() + " style='text-align: " + align + ";'";
224 xs << xml::StartTag(lay.htmlitemtag(), attrs);
228 inline void closeItemTag(XMLStream & xs, Layout const & lay)
230 xs << xml::EndTag(lay.htmlitemtag());
233 // end of convenience functions
235 ParagraphList::const_iterator findLastParagraph(
236 ParagraphList::const_iterator p,
237 ParagraphList::const_iterator const & pend)
239 for (++p; p != pend && p->layout().latextype == LATEX_PARAGRAPH; ++p)
246 ParagraphList::const_iterator findEndOfEnvironment(
247 ParagraphList::const_iterator const & pstart,
248 ParagraphList::const_iterator const & pend)
250 ParagraphList::const_iterator p = pstart;
251 Layout const & bstyle = p->layout();
252 size_t const depth = p->params().depth();
253 for (++p; p != pend; ++p) {
254 Layout const & style = p->layout();
255 // It shouldn't happen that e.g. a section command occurs inside
256 // a quotation environment, at a higher depth, but as of 6/2009,
257 // it can happen. We pretend that it's just at lowest depth.
258 if (style.latextype == LATEX_COMMAND)
261 // If depth is down, we're done
262 if (p->params().depth() < depth)
265 // If depth is up, we're not done
266 if (p->params().depth() > depth)
269 // FIXME I am not sure about the first check.
270 // Surely we *could* have different layouts that count as
271 // LATEX_PARAGRAPH, right?
272 if (style.latextype == LATEX_PARAGRAPH || style != bstyle)
279 ParagraphList::const_iterator makeParagraphs(Buffer const & buf,
281 OutputParams const & runparams,
283 ParagraphList::const_iterator const & pbegin,
284 ParagraphList::const_iterator const & pend)
286 ParagraphList::const_iterator const begin = text.paragraphs().begin();
287 ParagraphList::const_iterator par = pbegin;
288 for (; par != pend; ++par) {
289 Layout const & lay = par->layout();
290 if (!lay.counter.empty())
291 buf.masterBuffer()->params().
292 documentClass().counters().step(lay.counter, OutputUpdate);
294 // FIXME We should see if there's a label to be output and
295 // do something with it.
299 // We want to open the paragraph tag if:
300 // (i) the current layout permits multiple paragraphs
301 // (ii) we are either not already inside a paragraph (HTMLIsBlock) OR
302 // we are, but this is not the first paragraph
304 // But there is also a special case, and we first see whether we are in it.
305 // We do not want to open the paragraph tag if this paragraph contains
306 // only one item, and that item is "inline", i.e., not HTMLIsBlock (such
307 // as a branch). On the other hand, if that single item has a font change
308 // applied to it, then we still do need to open the paragraph.
310 // Obviously, this is very fragile. The main reason we need to do this is
311 // because of branches, e.g., a branch that contains an entire new section.
312 // We do not really want to wrap that whole thing in a <div>...</div>.
313 bool special_case = false;
314 Inset const * specinset = par->size() == 1 ? par->getInset(0) : nullptr;
315 if (specinset && !specinset->getLayout().htmlisblock()) {
316 Layout const & style = par->layout();
317 FontInfo const first_font = style.labeltype == LABEL_MANUAL ?
318 style.labelfont : style.font;
319 FontInfo const our_font =
320 par->getFont(buf.masterBuffer()->params(), 0,
321 text.outerFont(distance(begin, par))).fontInfo();
322 if (first_font == our_font)
326 bool const open_par = runparams.html_make_pars
327 && (!runparams.html_in_par || par != pbegin)
330 // We want to issue the closing tag if either:
331 // (i) We opened it, and either html_in_par is false,
332 // or we're not in the last paragraph, anyway.
333 // (ii) We didn't open it and html_in_par is true,
334 // but we are in the first par, and there is a next par.
335 ParagraphList::const_iterator nextpar = par;
337 bool const close_par =
338 (open_par && (!runparams.html_in_par || nextpar != pend))
339 || (!open_par && runparams.html_in_par && par == pbegin && nextpar != pend);
342 // We do not issue the paragraph id if we are doing
343 // this for the TOC (or some similar purpose)
344 openParTag(xs, lay, par->params(),
345 runparams.for_toc ? "" : par->magicLabel());
348 docstring const deferred = par->simpleLyXHTMLOnePar(buf, xs,
349 runparams, text.outerFont(distance(begin, par)),
350 open_par, close_par);
357 if (!deferred.empty()) {
358 xs << XMLStream::ESCAPE_NONE << deferred << xml::CR();
365 ParagraphList::const_iterator makeBibliography(Buffer const & buf,
367 OutputParams const & runparams,
369 ParagraphList::const_iterator const & pbegin,
370 ParagraphList::const_iterator const & pend)
373 // Use TextClass::htmlTOCLayout() to figure out how we should look.
374 xs << xml::StartTag("h2", "class='bibliography'")
375 << pbegin->layout().labelstring(false)
378 << xml::StartTag("div", "class='bibliography'")
380 makeParagraphs(buf, xs, runparams, text, pbegin, pend);
381 xs << xml::EndTag("div");
386 bool isNormalEnv(Layout const & lay)
388 return lay.latextype == LATEX_ENVIRONMENT
389 || lay.latextype == LATEX_BIB_ENVIRONMENT;
393 ParagraphList::const_iterator makeEnvironment(Buffer const & buf,
395 OutputParams const & runparams,
397 ParagraphList::const_iterator const & pbegin,
398 ParagraphList::const_iterator const & pend)
400 ParagraphList::const_iterator const begin = text.paragraphs().begin();
401 ParagraphList::const_iterator par = pbegin;
402 Layout const & bstyle = par->layout();
403 depth_type const origdepth = pbegin->params().depth();
405 // open tag for this environment
406 openParTag(xs, bstyle, pbegin->magicLabel());
409 // we will on occasion need to remember a layout from before.
410 Layout const * lastlay = nullptr;
412 while (par != pend) {
413 Layout const & style = par->layout();
414 // the counter only gets stepped if we're in some kind of list,
415 // or if it's the first time through.
416 // note that enum, etc, are handled automatically.
417 // FIXME There may be a bug here about user defined enumeration
418 // types. If so, then we'll need to take the counter and add "i",
419 // "ii", etc, as with enum.
420 Counters & cnts = buf.masterBuffer()->params().documentClass().counters();
421 docstring const & cntr = style.counter;
422 if (!style.counter.empty()
423 && (par == pbegin || !isNormalEnv(style))
424 && cnts.hasCounter(cntr)
426 cnts.step(cntr, OutputUpdate);
427 ParagraphList::const_iterator send;
429 switch (style.latextype) {
430 case LATEX_ENVIRONMENT:
431 case LATEX_LIST_ENVIRONMENT:
432 case LATEX_ITEM_ENVIRONMENT: {
433 // There are two possibilities in this case.
434 // One is that we are still in the environment in which we
435 // started---which we will be if the depth is the same.
436 if (par->params().depth() == origdepth) {
437 LATTEST(bstyle == style);
438 if (lastlay != nullptr) {
439 closeItemTag(xs, *lastlay);
443 // this will be positive, if we want to skip the
444 // initial word (if it's been taken for the label).
446 bool const labelfirst = style.htmllabelfirst();
448 openItemTag(xs, style, par->params());
451 if (style.labeltype != LABEL_NO_LABEL &&
452 style.htmllabeltag() != "NONE") {
453 if (isNormalEnv(style)) {
454 // in this case, we print the label only for the first
455 // paragraph (as in a theorem).
457 docstring const lbl =
458 pbegin->params().labelString();
460 openLabelTag(xs, style);
462 closeLabelTag(xs, style);
466 } else { // some kind of list
467 if (style.labeltype == LABEL_MANUAL) {
468 openLabelTag(xs, style);
469 sep = par->firstWordLyXHTML(xs, runparams);
470 closeLabelTag(xs, style);
474 openLabelTag(xs, style);
475 xs << par->params().labelString();
476 closeLabelTag(xs, style);
480 } // end label output
483 openItemTag(xs, style, par->params());
485 docstring deferred = par->simpleLyXHTMLOnePar(buf, xs, runparams,
486 text.outerFont(distance(begin, par)), true, true, sep);
487 xs << XMLStream::ESCAPE_NONE << deferred;
490 // We may not want to close the tag yet, in particular:
491 // If we're not at the end...
493 // and are doing items...
494 && !isNormalEnv(style)
495 // and if the depth has changed...
496 && par->params().depth() != origdepth) {
497 // then we'll save this layout for later, and close it when
498 // we get another item.
501 closeItemTag(xs, style);
504 // The other possibility is that the depth has increased, in which
505 // case we need to recurse.
507 send = findEndOfEnvironment(par, pend);
508 par = makeEnvironment(buf, xs, runparams, text, par, send);
512 case LATEX_PARAGRAPH:
513 send = findLastParagraph(par, pend);
514 par = makeParagraphs(buf, xs, runparams, text, par, send);
517 case LATEX_BIB_ENVIRONMENT:
520 par = makeParagraphs(buf, xs, runparams, text, par, send);
529 if (lastlay != nullptr)
530 closeItemTag(xs, *lastlay);
531 closeTag(xs, bstyle);
537 void makeCommand(Buffer const & buf,
539 OutputParams const & runparams,
541 ParagraphList::const_iterator const & pbegin)
543 Layout const & style = pbegin->layout();
544 if (!style.counter.empty())
545 buf.masterBuffer()->params().
546 documentClass().counters().step(style.counter, OutputUpdate);
548 bool const make_parid = !runparams.for_toc && runparams.html_make_pars;
550 openParTag(xs, style, pbegin->params(),
551 make_parid ? pbegin->magicLabel() : "");
553 // Label around sectioning number:
554 // FIXME Probably need to account for LABEL_MANUAL
555 // FIXME Probably also need now to account for labels ABOVE and CENTERED.
556 if (style.labeltype != LABEL_NO_LABEL) {
557 openLabelTag(xs, style);
558 xs << pbegin->params().labelString();
559 closeLabelTag(xs, style);
560 // Otherwise the label might run together with the text
561 xs << from_ascii(" ");
564 ParagraphList::const_iterator const begin = text.paragraphs().begin();
565 pbegin->simpleLyXHTMLOnePar(buf, xs, runparams,
566 text.outerFont(distance(begin, pbegin)));
571 } // end anonymous namespace
574 void xhtmlParagraphs(Text const & text,
577 OutputParams const & runparams)
579 ParagraphList const & paragraphs = text.paragraphs();
580 if (runparams.par_begin == runparams.par_end) {
581 runparams.par_begin = 0;
582 runparams.par_end = paragraphs.size();
584 pit_type bpit = runparams.par_begin;
585 pit_type const epit = runparams.par_end;
587 { xs << XMLStream::ESCAPE_NONE << "<!-- XHTML output error! -->\n"; return; });
589 OutputParams ourparams = runparams;
590 ParagraphList::const_iterator const pend =
591 (epit == (int) paragraphs.size()) ?
592 paragraphs.end() : paragraphs.iterator_at(epit);
593 std::stack<int> headerLevels;
595 while (bpit < epit) {
596 ParagraphList::const_iterator par = paragraphs.iterator_at(bpit);
597 if (par->params().startOfAppendix()) {
598 // We want to reset the counter corresponding to toplevel sectioning
600 buf.masterBuffer()->params().documentClass().getTOCLayout();
601 docstring const cnt = lay.counter;
604 buf.masterBuffer()->params().documentClass().counters();
608 Layout const & style = par->layout();
609 ParagraphList::const_iterator const lastpar = par;
610 ParagraphList::const_iterator send;
612 // Think about adding <section> and/or </section>s.
613 if (style.category() == from_utf8("Sectioning")) {
614 // Need to close a previous section if it has the same level or a higher one (close <section> if opening a
615 // <h2> after a <h2>, <h3>, <h4>, <h5>, or <h6>). More examples:
616 // - current: h2; back: h1; do not close any <section>
617 // - current: h1; back: h2; close two <section> (first the <h2>, then the <h1>, so a new <h1> can come)
618 // The level (h1, h2, etc.) corresponds to style.toclevel.
619 while (! headerLevels.empty() && style.toclevel <= headerLevels.top()) {
621 xs << xml::EndTag("section");
626 headerLevels.push(style.toclevel);
627 if (style.toclevel > 1) { // <h1> is the document title.
628 xs << xml::StartTag("section");
633 switch (style.latextype) {
634 case LATEX_COMMAND: {
635 // The files with which we are working never have more than
636 // one paragraph in a command structure.
638 // if (ourparams.html_in_par)
639 // fix it so we don't get sections inside standard, e.g.
640 // note that we may then need to make runparams not const, so we
641 // can communicate that back.
642 // FIXME Maybe this fix should be in the routines themselves, in case
643 // they are called from elsewhere.
644 makeCommand(buf, xs, ourparams, text, par);
648 case LATEX_ENVIRONMENT:
649 case LATEX_LIST_ENVIRONMENT:
650 case LATEX_ITEM_ENVIRONMENT: {
651 // FIXME Same fix here.
652 send = findEndOfEnvironment(par, pend);
653 par = makeEnvironment(buf, xs, ourparams, text, par, send);
656 case LATEX_BIB_ENVIRONMENT: {
657 // FIXME Same fix here.
658 send = findEndOfEnvironment(par, pend);
659 par = makeBibliography(buf, xs, ourparams, text, par, send);
662 case LATEX_PARAGRAPH:
663 send = findLastParagraph(par, pend);
664 par = makeParagraphs(buf, xs, ourparams, text, par, send);
667 bpit += distance(lastpar, par);
670 // If need be, close <section>s, but only at the end of the document (otherwise, dealt with at the beginning
672 while (! headerLevels.empty() && headerLevels.top() > 1) {
674 xs << xml::EndTag("section") << xml::CR();
679 string alignmentToCSS(LyXAlignment align)
682 case LYX_ALIGN_BLOCK:
683 // we are NOT going to use text-align: justify!!
686 case LYX_ALIGN_RIGHT:
688 case LYX_ALIGN_CENTER: