ExportCss.cpp
1/*
2 This file is part of the KDE project
3 Copyright (C) 2001, 2002, 2004 Nicolas GOUTTE <goutte@kde.org>
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Library General Public
7 License as published by the Free Software Foundation; either
8 version 2 of the License, or (at your option) any later version.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Library General Public License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to
17 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA 02110-1301, USA.
19*/
20
21#include <tqstring.h>
22#include <tqtextcodec.h>
23#include <tqfile.h>
24
25#include <tdelocale.h>
26#include <kdebug.h>
27
28#include <KWEFUtil.h>
29#include <KWEFBaseWorker.h>
30
31#include "ExportFilter.h"
32#include "ExportCss.h"
33
34TQString HtmlCssWorker::escapeCssIdentifier(const TQString& strText) const
35{
36 // Reference: section 4.1.3 of the CSS2 recommendation
37 // However most HTML user agents support this section only in a restrictive way, so we cannot use any CSS escape
38
39 // NOTE: we do not guarantee anymore that the style name is unique! (### FIXME)
40
41 TQString strReturn;
42
43 // Taken in the restrictive way, an identifier can only start with a letter.
44 const TQChar qch0(strText[0]);
45 if (((int)qch0<'a' || (int)qch0>'z') && ((int)qch0<'A' || (int)qch0>'Z'))
46 {
47 // Not a letter, so we have to add a prefix
48 strReturn+="kWoRd_"; // The curious spelling is for allowing a HTML import to identfy it and to remove it.
49 // The processing of the character itself is done below
50 }
51
52 for (uint i=0; i<strText.length(); i++)
53 {
54 const TQChar qch(strText.at(i));
55 const ushort ch=qch.unicode();
56
57 if (((ch>='a') && (ch<='z'))
58 || ((ch>='A') && (ch<='Z'))
59 || ((ch>='0') && (ch<='9'))
60 || (ch=='-') || (ch=='_')) // The underscore is allowed by the CSS2 errata
61 {
62 // Normal allowed characters (without any problem)
63 strReturn+=qch;
64 }
65 else if ((ch<=' ') || (ch>=128 && ch<=160)) // space (breaking or not) and control characters
66 {
67 // CSS2 would allow to escape it but not any HTML user agent supports this
68 strReturn+='_';
69 }
70 else if ((ch>=161) && (getCodec()->canEncode(qch)))
71 {
72 // Any Unicode character greater or egual to 161 is allowed
73 // except if it cannot be written in the encoding
74 strReturn+=qch;
75 }
76 else // if ch >= 33 && ch <=127 with holes (or not in encoding)
77 {
78 // Either CSS2 does not allow this character unescaped or it is not in the encoding
79 // but a CSS escape would break some HTML user agents (e.g. Mozilla 1.4)
80 // So we have to do our own incompatible cooking. :-(
81 strReturn+="--"; // start our private escape
82 strReturn+=TQString::number(ch,16);
83 strReturn+="--"; // end our private escape
84 }
85 }
86 return strReturn;
87}
88
89TQString HtmlCssWorker::textFormatToCss(const TextFormatting& formatOrigin,
90 const TextFormatting& formatData, const bool force) const
91{
92 // TODO: as this method comes from the AbiWord filter,
93 // TODO: verify that it is working for HTML
94
95 // TODO: rename variable formatData
96 TQString strElement; // TODO: rename this variable
97
98 // Font name
99 TQString fontName = formatData.fontName;
100 if (!fontName.isEmpty()
101 && (force || (formatOrigin.fontName!=formatData.fontName)))
102 {
103 strElement+="font-family: ";
104 if (fontName.find(' ')==-1)
105 strElement+=escapeHtmlText(fontName);
106 else
107 { // If the font name contains a space, it should be quoted.
108 strElement+='\'';
109 strElement+=escapeHtmlText(fontName);
110 strElement+='\'';
111 }
112 // ### TODO: add alternative font names
113 strElement+="; ";
114 }
115
116 if (force || (formatOrigin.italic!=formatData.italic))
117 {
118 // Font style
119 strElement+="font-style: ";
120 if ( formatData.italic )
121 {
122 strElement+="italic";
123 }
124 else
125 {
126 strElement+="normal";
127 }
128 strElement+="; ";
129 }
130
131 if (force || ((formatOrigin.weight>=75)!=(formatData.weight>=75)))
132 {
133 strElement+="font-weight: ";
134 if ( formatData.weight >= 75 )
135 {
136 strElement+="bold";
137 }
138 else
139 {
140 strElement+="normal";
141 }
142 strElement+="; ";
143 }
144
145 if (force || (formatOrigin.fontSize!=formatData.fontSize))
146 {
147 const int size=formatData.fontSize;
148 if (size>0)
149 {
150 // We use absolute font sizes.
151 strElement+="font-size: ";
152 strElement+=TQString::number(size,10);
153 strElement+="pt; ";
154 }
155 }
156
157 if (force || (formatOrigin.fgColor!=formatData.fgColor))
158 {
159 if ( formatData.fgColor.isValid() )
160 {
161 // Give colour
162 strElement+="color: ";
163 strElement+=formatData.fgColor.name();
164 strElement+="; ";
165 }
166 }
167
168 if (force || (formatOrigin.bgColor!=formatData.bgColor))
169 {
170 if ( formatData.bgColor.isValid() )
171 {
172 // Give background colour
173 strElement+="background-color: ";
174 strElement+=formatData.bgColor.name();
175 strElement+="; ";
176 }
177 }
178
179 if (force || (formatOrigin.underline!=formatData.underline)
180 || (formatOrigin.strikeout!=formatData.strikeout))
181 {
182 strElement+="text-decoration: ";
183 if ( formatData.underline )
184 {
185 strElement+="underline";
186 }
187 else if ( formatData.strikeout )
188 {
189 strElement+="line-through";
190 }
191 else
192 {
193 strElement+="none";
194 }
195 strElement+="; ";
196 }
197
198 if (force || (formatOrigin.fontAttribute!=formatData.fontAttribute))
199 {
200 bool smallcaps=false;
201 strElement+="text-transform: ";
202 if ( formatData.fontAttribute=="uppercase" )
203 {
204 strElement+="uppercase";
205 }
206 else if ( formatData.fontAttribute=="lowercase" )
207 {
208 strElement+="lowercase";
209 }
210 else if ( formatData.fontAttribute=="smallcaps" )
211 {
212 strElement+="none";
213 smallcaps=true;
214 }
215 else
216 {
217 strElement+="none";
218 }
219 strElement+="; ";
220 // ### TODO: mostly issuing font-variant is not necessary.
221 strElement+="font-variant:";
222 if (smallcaps)
223 strElement+="small-caps";
224 else
225 strElement+="normal";
226 strElement+="; ";
227 }
228
229 // TODO: As this is the last property, do not put a semi-colon
230
231 return strElement;
232}
233
234TQString HtmlCssWorker::getStartOfListOpeningTag(const CounterData::Style typeList, bool& ordered)
235{
236 TQString strResult;
237 switch (typeList)
238 {
239 case CounterData::STYLE_CUSTOMBULLET: // We cannot keep the custom type/style
240 default:
241 {
242 ordered=false;
243 strResult="<ul>\n";
244 break;
245 }
246 case CounterData::STYLE_NONE:
247 {
248 ordered=false;
249 strResult="<ul style=\"list-style-type:none\">\n";
250 break;
251 }
252 case CounterData::STYLE_CIRCLEBULLET:
253 {
254 ordered=false;
255 strResult="<ul style=\"list-style-type:circle\">\n";
256 break;
257 }
258 case CounterData::STYLE_SQUAREBULLET:
259 {
260 ordered=false;
261 strResult="<ul style=\"list-style-type:square\">\n";
262 break;
263 }
264 case CounterData::STYLE_DISCBULLET:
265 {
266 ordered=false;
267 strResult="<ul style=\"list-style-type:disc\">\n";
268 break;
269 }
270 case CounterData::STYLE_NUM:
271 {
272 ordered=true;
273 strResult="<ol style=\"list-style-type:decimal\">\n";
274 break;
275 }
276 case CounterData::STYLE_ALPHAB_L:
277 {
278 ordered=true;
279 strResult="<ol style=\"list-style-type:lower-alpha\">\n";
280 break;
281 }
282 case CounterData::STYLE_ALPHAB_U:
283 {
284 ordered=true;
285 strResult="<ol style=\"list-style-type:upper-alpha\">\n";
286 break;
287 }
288 case CounterData::STYLE_ROM_NUM_L:
289 {
290 ordered=true;
291 strResult="<ol style=\"list-style-type:lower-roman\">\n";
292 break;
293 }
294 case CounterData::STYLE_ROM_NUM_U:
295 {
296 ordered=true;
297 strResult="<ol style=\"list-style-type:upper-roman\">\n";
298 break;
299 }
300 case CounterData::STYLE_CUSTOM:
301 {
302 // We cannot keep the custom type/style
303 ordered=true;
304 strResult="<ol>\n";
305 break;
306 }
307 }
308 return strResult;
309}
310
311TQString HtmlCssWorker::layoutToCss(const LayoutData& layoutOrigin,
312 const LayoutData& layout, const bool force) const
313{
314 TQString strLayout;
315
316 if (force || (layoutOrigin.alignment!=layout.alignment))
317 {
318 if ( (layout.alignment=="left") || (layout.alignment== "right")
319 || (layout.alignment=="center") || (layout.alignment=="justify"))
320 {
321 strLayout += TQString("text-align:%1; ").arg(layout.alignment);
322 }
323 else if ( layout.alignment=="auto")
324 {
325 // Do nothing, the user-agent should be more intelligent than us.
326 }
327 else
328 {
329 kdWarning(30503) << "Unknown alignment: " << layout.alignment << endl;
330 }
331 }
332
333 if ((layout.indentLeft>=0.0)
334 && (force || (layoutOrigin.indentLeft!=layout.indentLeft)))
335 {
336 strLayout += TQString("margin-left:%1pt; ").arg(layout.indentLeft);
337 }
338
339 if ((layout.indentRight>=0.0)
340 && (force || (layoutOrigin.indentRight!=layout.indentRight)))
341 {
342 strLayout += TQString("margin-right:%1pt; ").arg(layout.indentRight);
343 }
344
345 if (force || (layoutOrigin.indentLeft!=layout.indentLeft))
346 {
347 strLayout += TQString("text-indent:%1pt; ").arg(layout.indentFirst);
348 }
349
350 if ((layout.marginBottom>=0.0)
351 && ( force || ( layoutOrigin.marginBottom != layout.marginBottom ) ) )
352 {
353 strLayout += TQString("margin-bottom:%1pt; ").arg(layout.marginBottom);
354 }
355
356 if ((layout.marginTop>=0.0)
357 && ( force || ( layoutOrigin.marginTop != layout.marginTop ) ) )
358 {
359 strLayout += TQString("margin-top:%1pt; ").arg(layout.marginTop);
360 }
361
362 if (force
363 || ( layoutOrigin.lineSpacingType != layout.lineSpacingType )
364 || ( layoutOrigin.lineSpacing != layout.lineSpacing ) )
365 {
366 switch ( layout.lineSpacingType )
367 {
368 case LayoutData::LS_CUSTOM:
369 {
370 // ### TODO: CSS 2 does not known "at-least".
371#if 0
372 // We have a custom line spacing (in points)
373 const TQString height ( TQString::number(layout.lineSpacing) ); // ### TODO: rounding?
374 strLayout += "style:line-spacing:";
375 strLayout += height;
376 strLayout += "pt; ";
377#endif
378 break;
379 }
380 case LayoutData::LS_SINGLE:
381 {
382 strLayout += "line-height:normal; "; // One
383 break;
384 }
385 case LayoutData::LS_ONEANDHALF:
386 {
387 strLayout += "line-height:150%; "; // One-and-half
388 break;
389 }
390 case LayoutData::LS_DOUBLE:
391 {
392 strLayout += "line-height:200%; "; // Two
393 break;
394 }
395 case LayoutData::LS_MULTIPLE:
396 {
397 const TQString mult ( TQString::number( tqRound( layout.lineSpacing * 100 ) ) );
398 strLayout += "line-height:";
399 strLayout += mult;
400 strLayout += "%; ";
401 break;
402 }
403 case LayoutData::LS_FIXED:
404 {
405 // We have a fixed line height (in points)
406 const TQString height ( TQString::number(layout.lineSpacing) ); // ### TODO: rounding?
407 strLayout += "line-height:";
408 strLayout += height;
409 strLayout += "pt; ";
410 break;
411 }
412 case LayoutData::LS_ATLEAST:
413 {
414 // ### TODO: CSS 2 does not known "at-least".
415 // ### TODO: however draft CCS3 (module 'line') has 'line-stacking-strategy' to tweak this behaviour
416 // We have a at-least line height (in points)
417 const TQString height ( TQString::number(layout.lineSpacing) ); // ### TODO: rounding?
418 strLayout += "line-height:";
419 strLayout += height;
420 strLayout += "pt; ";
421 break;
422 }
423 default:
424 {
425 kdWarning(30503) << "Unsupported lineSpacingType: " << layout.lineSpacingType << " (Ignoring!)" << endl;
426 break;
427 }
428 }
429 }
430
431 // TODO: Konqueror/TDEHTML does not support "text-shadow"
432 if (!force
433 && ( layoutOrigin.shadowDirection == layout.shadowDirection )
434 && ( layoutOrigin.shadowDistance == layout.shadowDistance ) )
435 {
436 // Do nothing!
437 }
438 else if ((!layout.shadowDirection) || (!layout.shadowDistance))
439 {
440 strLayout += "text-shadow:";
441 strLayout+="none; ";
442 }
443 else
444 {
445 double xDistance,yDistance;
446 const double distance=layout.shadowDistance;
447 switch (layout.shadowDirection)
448 {
449 case 1: // SD_LEFT_UP
450 {
451 xDistance= (-distance);
452 yDistance= (-distance);
453 break;
454 }
455 case 2: // SD_UP
456 {
457 xDistance= 0;
458 yDistance= (-distance);
459 break;
460 }
461 case 3: // SD_RIGHT_UP
462 {
463 xDistance= (distance);
464 yDistance= (-distance);
465 break;
466 }
467 case 4: // SD_RIGHT
468 {
469 xDistance= (distance);
470 yDistance= 0;
471 break;
472 }
473 case 5: // SD_RIGHT_BOTTOM
474 {
475 xDistance= (distance);
476 yDistance= (distance);
477 break;
478 }
479 case 6: // SD_BOTTOM
480 {
481 xDistance= 0;
482 yDistance= (distance);
483 break;
484 }
485 case 7: // SD_LEFT_BOTTOM
486 {
487 xDistance= (-distance);
488 yDistance= (distance);
489 break;
490 }
491 case 8: // SD_LEFT
492 {
493 xDistance= (distance);
494 yDistance= 0;
495 break;
496 }
497 default:
498 {
499 xDistance=0;
500 yDistance=0;
501 break;
502 }
503 }
504 if ( (!xDistance) && (!yDistance) )
505 {
506 strLayout += "text-shadow:";
507 strLayout+="none; ";
508 }
509 else
510 {
511 strLayout += "text-shadow:";
512 strLayout+=TQString("%1 %2pt %3pt; ").arg(layout.shadowColor.name())
513 .arg(xDistance,0,'f',0).arg(yDistance,0,'f',0);
514 // We do not want any scientific notation or any decimal
515 }
516 }
517
518 // TODO: borders
519
520 // This must remain last, as the last property does not have a semi-colon
521 strLayout+=textFormatToCss(layoutOrigin.formatData.text,
522 layout.formatData.text,force);
523
524 return strLayout;
525}
526
527void HtmlCssWorker::openParagraph(const TQString& strTag,
528 const LayoutData& layout, TQChar::Direction direction)
529{
530 const LayoutData& styleLayout=m_styleMap[layout.styleName];
531
532 *m_streamOut << '<' << strTag;
533
534 // Opening elements
535 *m_streamOut << " class=\"" << escapeCssIdentifier(layout.styleName);
536 *m_streamOut << "\"";
537
538 TQString strStyle=layoutToCss(styleLayout,layout,false);
539 if (!strStyle.isEmpty())
540 {
541 *m_streamOut << " style=\"" << strStyle;
542 if (direction == TQChar::DirRLE) {
543 *m_streamOut << "direction: rtl; unicode-bidi: embed; ";
544 } else if (direction == TQChar::DirRLO) {
545 *m_streamOut << "direction: rtl; unicode-bidi: override; ";
546 }
547 *m_streamOut<< "\"";
548 }
549
550 *m_streamOut << ">";
551
552 if ( 1==layout.formatData.text.verticalAlignment )
553 {
554 *m_streamOut << "<sub>"; //Subscript
555 }
556 else if ( 2==layout.formatData.text.verticalAlignment )
557 {
558 *m_streamOut << "<sup>"; //Superscript
559 }
560 if ( layout.alignment == "center" ) *m_streamOut << "<center>";
561}
562
563void HtmlCssWorker::closeParagraph(const TQString& strTag,
564 const LayoutData& layout)
565{
566 if ( 2==layout.formatData.text.verticalAlignment )
567 {
568 *m_streamOut << "</sup>"; //Superscript
569 }
570 else if ( 1==layout.formatData.text.verticalAlignment )
571 {
572 *m_streamOut << "</sub>"; //Subscript
573 }
574
575 if ( layout.alignment == "center" ) *m_streamOut << "</center>";
576 *m_streamOut << "</" << strTag << ">\n";
577}
578
579void HtmlCssWorker::openSpan(const FormatData& formatOrigin, const FormatData& format)
580{
581 *m_streamOut << "<span style=\"";
582 *m_streamOut << textFormatToCss(formatOrigin.text,format.text,false);
583 *m_streamOut << "\">"; // close span opening tag
584
585 if ( 1==format.text.verticalAlignment )
586 {
587 *m_streamOut << "<sub>"; //Subscript
588 }
589 else if ( 2==format.text.verticalAlignment )
590 {
591 *m_streamOut << "<sup>"; //Superscript
592 }
593}
594
595void HtmlCssWorker::closeSpan(const FormatData& formatOrigin, const FormatData& format)
596{
597 if ( 2==format.text.verticalAlignment )
598 {
599 *m_streamOut << "</sup>"; //Superscript
600 }
601 else if ( 1==format.text.verticalAlignment )
602 {
603 *m_streamOut << "</sub>"; //Subscript
604 }
605
606 *m_streamOut << "</span>";
607}
608
609bool HtmlCssWorker::doFullPaperFormat(const int format,
610 const double width, const double height, const int orientation)
611{
612 TQString strWidth, strHeight, strUnits;
613 KWEFUtil::GetNativePaperFormat(format, strWidth, strHeight, strUnits);
614
615 if ((strWidth.isEmpty())||(strHeight.isEmpty())||(strUnits.isEmpty()))
616 {
617 // page format is unknown, so we need the size information
618 strUnits="pt";
619 strWidth=TQString::number(width);
620 strHeight=TQString::number(height);
621 }
622 if (orientation==1)
623 {
624 // Landscape, so we must swap the sizes
625 TQString strTemp(strWidth);
626 strWidth=strHeight;
627 strHeight=strTemp;
628 }
629
630 m_strPageSize="size: ";
631 m_strPageSize+=strWidth;
632 m_strPageSize+=strUnits;
633 m_strPageSize+=" ";
634 m_strPageSize+=strHeight;
635 m_strPageSize+=strUnits;
636 m_strPageSize+=";";
637 return true;
638}
639
640bool HtmlCssWorker::doFullPaperBorders (const double top, const double left,
641 const double bottom, const double right)
642{
643 m_strPaperBorders=" margin-top: ";
644 m_strPaperBorders+=TQString::number(top);
645 m_strPaperBorders+="pt;\n";
646 m_strPaperBorders+=" margin-left: ";
647 m_strPaperBorders+=TQString::number(left);
648 m_strPaperBorders+="pt;\n";
649 m_strPaperBorders+=" margin-bottom: ";
650 m_strPaperBorders+=TQString::number(bottom);
651 m_strPaperBorders+="pt;\n";
652 m_strPaperBorders+=" margin-right: ";
653 m_strPaperBorders+=TQString::number(right);
654 m_strPaperBorders+="pt;\n";
655
656 return true;
657}
658
659bool HtmlCssWorker::doOpenStyles(void)
660{
661 *m_streamOut << "<style type=\"text/css\">\n";
662 if (!isXML())
663 {
664 // Put the style under comment to increase the compatibility with old browsers
665 // However in XHTML 1.0, you cannot put the style definition into HTML comments
666 *m_streamOut << "<!--\n";
667 }
668
669 // Say who we are (with the CVS revision number)
670 const TQString strVersion("$Revision: 483471 $");
671 // Eliminate the dollar signs
672 // (We don't want that the version number changes if the HTML file is itself put in a CVS storage.)
673 *m_streamOut << "/* KWORD_CSS_EXPORT ="
674 << strVersion.mid(10).remove('$')
675 << "*/\n";
676
677 // TODO: does KWord gives a paper colour?
678 *m_streamOut << "BODY\n{\n background-color: #FFFFFF\n}\n";
679
680 return true;
681}
682
683bool HtmlCssWorker::doFullDefineStyle(LayoutData& layout)
684{
685 //Register style in the style map
686 m_styleMap[layout.styleName]=layout;
687
688 // We do not limit (anymore) any style to <h1> ... <h6>, because
689 // the style could be forced on <p> by the layout.
690
691 *m_streamOut << "." << escapeCssIdentifier(layout.styleName);
692 *m_streamOut << "\n{\n " << layoutToCss(layout,layout,true) << "\n}\n";
693
694 return true;
695}
696
697bool HtmlCssWorker::doCloseStyles(void)
698{
699 if (!m_strPageSize.isEmpty())
700 {
701 *m_streamOut << "@page\n{\n ";
702 *m_streamOut << m_strPageSize;
703 *m_streamOut << "\n";
704 *m_streamOut << m_strPaperBorders; // ends with a LF
705 *m_streamOut << "}\n";
706 }
707
708 if (!isXML())
709 {
710 // Put the style under comment to increase the compatibility with old browsers
711 // However in XHTML 1.0, you cannot put the style definition into HTML comments
712 *m_streamOut << "-->\n";
713 }
714 *m_streamOut << "</style>\n";
715
716 return true;
717}
718
Paragraph layout.
double lineSpacing
Space between lines in pt, if lineSpacingType==0.
int shadowDirection
direction of <SHADOW>
TQColor shadowColor
red, green, blue of <SHADOW>
double indentFirst
indentation correction of first line (can be negative!)
double indentRight
right indentation (a negative value means invalid)
double shadowDistance
distance of <SHADOW>
double indentLeft
left indentation (a negative value means invalid)
TQString alignment
left, right, center, justify
int lineSpacingType
0=custom, 10=one line, 15=one and half lines, 20= 2 lines
double marginTop
space before the paragraph (a negative value means invalid)
double marginBottom
space after the paragraph (a negative value means invalid)
TextFormatting is a container for data retrieved from the <FORMAT> tag and its subtags to be used in ...
bool underline
Any underline? (If true, we do not know if simple or double or what else)
bool strikeout
true if strikeout