1 /*=============================================================================
2     Copyright (c) 2002 2004 2006 Joel de Guzman
3     Copyright (c) 2004 Eric Niebler
4     Copyright (c) 2005 Thomas Guest
5     http://spirit.sourceforge.net/
6 
7     Use, modification and distribution is subject to the Boost Software
8     License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
9     http://www.boost.org/LICENSE_1_0.txt)
10 =============================================================================*/
11 #include <sstream>
12 #include <boost/algorithm/string/join.hpp>
13 #include <boost/bind.hpp>
14 #include <boost/filesystem/operations.hpp>
15 #include "doc_info_tags.hpp"
16 #include "document_state.hpp"
17 #include "files.hpp"
18 #include "for.hpp"
19 #include "path.hpp"
20 #include "quickbook.hpp"
21 #include "state.hpp"
22 #include "stream.hpp"
23 #include "utils.hpp"
24 
25 namespace quickbook
26 {
27     struct doc_info_values
28     {
29         std::string doc_type;
30         value doc_title;
31         std::vector<value> escaped_attributes;
32         value qbk_version, compatibility_mode;
33         value id, dirname, last_revision, purpose;
34         std::vector<value> categories;
35         value lang, version;
36         std::vector<value> authors;
37         std::vector<value> copyrights;
38         value license;
39         std::vector<value> biblioids;
40         value xmlbase;
41         std::string xmlbase_value;
42 
43         std::string id_placeholder;
44         std::string include_doc_id_, id_;
45     };
46 
47     static void write_document_title(
48         collector& out, value const& title, value const& version);
49     std::string write_boostbook_header(
50         quickbook::state& state, doc_info_values const& info, bool nested_file);
51 
doc_info_output(value const & p,unsigned version)52     static std::string doc_info_output(value const& p, unsigned version)
53     {
54         if (qbk_version_n < version) {
55             std::string value = p.get_quickbook().to_s();
56             value.erase(value.find_last_not_of(" \t") + 1);
57             return value;
58         }
59         else {
60             return p.get_encoded();
61         }
62     }
63 
doc_info_attribute_name(value::tag_type tag)64     char const* doc_info_attribute_name(value::tag_type tag)
65     {
66         return doc_attributes::is_tag(tag) ? doc_attributes::name(tag)
67                                            : doc_info_attributes::name(tag);
68     }
69 
70     // Each docinfo attribute is stored in a value list, these are then stored
71     // in a sorted value list. The following convenience methods extract all the
72     // values for an attribute tag.
73 
74     // Expecting at most one attribute, with several values in the list.
consume_list(value_consumer & c,value::tag_type tag,std::vector<std::string> * duplicates)75     value consume_list(
76         value_consumer& c,
77         value::tag_type tag,
78         std::vector<std::string>* duplicates)
79     {
80         value p;
81 
82         int count = 0;
83         while (c.check(tag)) {
84             p = c.consume();
85             ++count;
86         }
87 
88         if (count > 1) duplicates->push_back(doc_info_attribute_name(tag));
89 
90         return p;
91     }
92 
93     // Expecting at most one attribute, with a single value, so extract that
94     // immediately.
consume_value_in_list(value_consumer & c,value::tag_type tag,std::vector<std::string> * duplicates)95     value consume_value_in_list(
96         value_consumer& c,
97         value::tag_type tag,
98         std::vector<std::string>* duplicates)
99     {
100         value l = consume_list(c, tag, duplicates);
101         if (l.empty()) return l;
102 
103         assert(l.is_list());
104         value_consumer c2 = l;
105         value p = c2.consume();
106         c2.finish();
107 
108         return p;
109     }
110 
111     // Any number of attributes, so stuff them into a vector.
consume_multiple_values(value_consumer & c,value::tag_type tag)112     std::vector<value> consume_multiple_values(
113         value_consumer& c, value::tag_type tag)
114     {
115         std::vector<value> values;
116 
117         while (c.check(tag)) {
118             values.push_back(c.consume());
119         }
120 
121         return values;
122     }
123 
124     enum version_state
125     {
126         version_unknown,
127         version_stable,
128         version_dev
129     };
classify_version(unsigned v)130     version_state classify_version(unsigned v)
131     {
132         return v < 100u ? version_unknown
133                         : v <= 107u ? version_stable :
134                                     // v <= 107u ? version_dev :
135                               version_unknown;
136     }
137 
get_version(quickbook::state & state,bool using_docinfo,value version)138     unsigned get_version(
139         quickbook::state& state, bool using_docinfo, value version)
140     {
141         unsigned result = 0;
142 
143         if (!version.empty()) {
144             value_consumer version_values(version);
145             bool before_docinfo =
146                 version_values.optional_consume(doc_info_tags::before_docinfo)
147                     .check();
148             int major_verison = version_values.consume().get_int();
149             int minor_verison = version_values.consume().get_int();
150             version_values.finish();
151 
152             if (before_docinfo || using_docinfo) {
153                 result =
154                     ((unsigned)major_verison * 100) + (unsigned)minor_verison;
155 
156                 if (classify_version(result) == version_unknown) {
157                     detail::outerr(state.current_file->path)
158                         << "Unknown version: " << major_verison << "."
159                         << minor_verison << std::endl;
160                     ++state.error_count;
161                 }
162             }
163         }
164 
165         return result;
166     }
167 
pre(quickbook::state & state,parse_iterator pos,value include_doc_id,bool nested_file)168     std::string pre(
169         quickbook::state& state,
170         parse_iterator pos,
171         value include_doc_id,
172         bool nested_file)
173     {
174         // The doc_info in the file has been parsed. Here's what we'll do
175         // *before* anything else.
176         //
177         // If there isn't a doc info block, then values will be empty, so most
178         // of the following code won't actually do anything.
179 
180         value_consumer values = state.values.release();
181 
182         // Skip over invalid attributes
183 
184         while (values.check(value::default_tag))
185             values.consume();
186 
187         bool use_doc_info = false;
188         doc_info_values info;
189 
190         if (values.check(doc_info_tags::type)) {
191             info.doc_type =
192                 values.consume(doc_info_tags::type).get_quickbook().to_s();
193             info.doc_title = values.consume(doc_info_tags::title);
194             use_doc_info = !nested_file || qbk_version_n >= 106u;
195         }
196         else {
197             if (!nested_file) {
198                 detail::outerr(state.current_file, pos.base())
199                     << "No doc_info block." << std::endl;
200 
201                 ++state.error_count;
202 
203                 // Create a fake document info block in order to continue.
204                 info.doc_type = "article";
205                 info.doc_title = qbk_value(
206                     state.current_file, pos.base(), pos.base(),
207                     doc_info_tags::type);
208                 use_doc_info = true;
209             }
210         }
211 
212         std::vector<std::string> duplicates;
213 
214         info.escaped_attributes =
215             consume_multiple_values(values, doc_info_tags::escaped_attribute);
216 
217         info.qbk_version =
218             consume_list(values, doc_attributes::qbk_version, &duplicates);
219         info.compatibility_mode = consume_list(
220             values, doc_attributes::compatibility_mode, &duplicates);
221         consume_multiple_values(values, doc_attributes::source_mode);
222 
223         info.id =
224             consume_value_in_list(values, doc_info_attributes::id, &duplicates);
225         info.dirname = consume_value_in_list(
226             values, doc_info_attributes::dirname, &duplicates);
227         info.last_revision = consume_value_in_list(
228             values, doc_info_attributes::last_revision, &duplicates);
229         info.purpose = consume_value_in_list(
230             values, doc_info_attributes::purpose, &duplicates);
231         info.categories =
232             consume_multiple_values(values, doc_info_attributes::category);
233         info.lang = consume_value_in_list(
234             values, doc_info_attributes::lang, &duplicates);
235         info.version = consume_value_in_list(
236             values, doc_info_attributes::version, &duplicates);
237         info.authors =
238             consume_multiple_values(values, doc_info_attributes::authors);
239         info.copyrights =
240             consume_multiple_values(values, doc_info_attributes::copyright);
241         info.license = consume_value_in_list(
242             values, doc_info_attributes::license, &duplicates);
243         info.biblioids =
244             consume_multiple_values(values, doc_info_attributes::biblioid);
245         info.xmlbase = consume_value_in_list(
246             values, doc_info_attributes::xmlbase, &duplicates);
247 
248         values.finish();
249 
250         if (!duplicates.empty()) {
251             detail::outwarn(state.current_file->path)
252                 << (duplicates.size() > 1 ? "Duplicate attributes"
253                                           : "Duplicate attribute")
254                 << ":" << boost::algorithm::join(duplicates, ", ") << "\n";
255         }
256 
257         if (!include_doc_id.empty())
258             info.include_doc_id_ = include_doc_id.get_quickbook().to_s();
259         if (!info.id.empty()) info.id_ = info.id.get_quickbook().to_s();
260 
261         // Quickbook version
262 
263         unsigned new_version =
264             get_version(state, use_doc_info, info.qbk_version);
265 
266         if (new_version != qbk_version_n) {
267             if (classify_version(new_version) == version_dev) {
268                 detail::outwarn(state.current_file->path)
269                     << "Quickbook " << (new_version / 100) << "."
270                     << (new_version % 100)
271                     << " is still under development and is "
272                        "likely to change in the future."
273                     << std::endl;
274             }
275         }
276 
277         if (new_version) {
278             qbk_version_n = new_version;
279         }
280         else if (use_doc_info) {
281             // hard code quickbook version to v1.1
282             qbk_version_n = 101;
283             detail::outwarn(state.current_file, pos.base())
284                 << "Quickbook version undefined. "
285                    "Version 1.1 is assumed"
286                 << std::endl;
287         }
288 
289         state.current_file->version(qbk_version_n);
290 
291         // Compatibility Version
292 
293         unsigned compatibility_version =
294             get_version(state, use_doc_info, info.compatibility_mode);
295 
296         if (!compatibility_version) {
297             compatibility_version =
298                 use_doc_info ? qbk_version_n
299                              : state.document.compatibility_version();
300         }
301 
302         // Start file, finish here if not generating document info.
303 
304         if (!use_doc_info) {
305             state.document.start_file(
306                 compatibility_version, info.include_doc_id_, info.id_,
307                 info.doc_title);
308             return "";
309         }
310 
311         info.id_placeholder = state.document.start_file_with_docinfo(
312             compatibility_version, info.include_doc_id_, info.id_,
313             info.doc_title);
314 
315         // Make sure we really did have a document info block.
316 
317         assert(info.doc_title.check() && !info.doc_type.empty());
318 
319         // Set xmlbase
320 
321         // std::string xmlbase_value;
322 
323         if (!info.xmlbase.empty()) {
324             path_parameter x = check_xinclude_path(info.xmlbase, state);
325 
326             if (x.type == path_parameter::path) {
327                 quickbook_path path = resolve_xinclude_path(x.value, state);
328 
329                 if (!fs::is_directory(path.file_path)) {
330                     detail::outerr(
331                         info.xmlbase.get_file(), info.xmlbase.get_position())
332                         << "xmlbase \"" << info.xmlbase.get_quickbook()
333                         << "\" isn't a directory." << std::endl;
334 
335                     ++state.error_count;
336                 }
337                 else {
338                     info.xmlbase_value =
339                         dir_path_to_url(path.abstract_file_path);
340                     state.xinclude_base = path.file_path;
341                 }
342             }
343         }
344 
345         // Warn about invalid fields
346 
347         if (info.doc_type != "library") {
348             std::vector<std::string> invalid_attributes;
349 
350             if (!info.purpose.empty()) invalid_attributes.push_back("purpose");
351 
352             if (!info.categories.empty())
353                 invalid_attributes.push_back("category");
354 
355             if (!info.dirname.empty()) invalid_attributes.push_back("dirname");
356 
357             if (!invalid_attributes.empty()) {
358                 detail::outwarn(state.current_file->path)
359                     << (invalid_attributes.size() > 1 ? "Invalid attributes"
360                                                       : "Invalid attribute")
361                     << " for '" << info.doc_type << " document info': "
362                     << boost::algorithm::join(invalid_attributes, ", ") << "\n";
363             }
364         }
365 
366         return write_boostbook_header(state, info, nested_file);
367     }
368 
write_boostbook_header(quickbook::state & state,doc_info_values const & info,bool nested_file)369     std::string write_boostbook_header(
370         quickbook::state& state, doc_info_values const& info, bool nested_file)
371     {
372         // Write out header
373 
374         if (!nested_file) {
375             state.out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
376                       << "<!DOCTYPE " << info.doc_type
377                       << " PUBLIC \"-//Boost//DTD BoostBook XML V1.0//EN\"\n"
378                       << "     "
379                          "\"http://www.boost.org/tools/boostbook/dtd/"
380                          "boostbook.dtd\">\n";
381         }
382 
383         state.out << '<' << info.doc_type << "\n"
384                   << "    id=\"" << info.id_placeholder << "\"\n";
385 
386         if (!info.lang.empty()) {
387             state.out << "    lang=\"" << doc_info_output(info.lang, 106)
388                       << "\"\n";
389         }
390 
391         if (info.doc_type == "library" && !info.doc_title.empty()) {
392             state.out << "    name=\"" << doc_info_output(info.doc_title, 106)
393                       << "\"\n";
394         }
395 
396         // Set defaults for dirname + last_revision
397 
398         if (!info.dirname.empty() || info.doc_type == "library") {
399             state.out << "    dirname=\"";
400             if (!info.dirname.empty()) {
401                 state.out << doc_info_output(info.dirname, 106);
402             }
403             else if (!info.id_.empty()) {
404                 state.out << info.id_;
405             }
406             else if (!info.include_doc_id_.empty()) {
407                 state.out << info.include_doc_id_;
408             }
409             else if (!info.doc_title.empty()) {
410                 state.out << detail::make_identifier(
411                     info.doc_title.get_quickbook());
412             }
413             else {
414                 state.out << "library";
415             }
416 
417             state.out << "\"\n";
418         }
419 
420         state.out << "    last-revision=\"";
421         if (!info.last_revision.empty()) {
422             state.out << doc_info_output(info.last_revision, 106);
423         }
424         else {
425             // default value for last-revision is now
426 
427             char strdate[64];
428             strftime(
429                 strdate, sizeof(strdate),
430                 (debug_mode ? "DEBUG MODE Date: %Y/%m/%d %H:%M:%S $"
431                             : "$" /* prevent CVS substitution */
432                               "Date: %Y/%m/%d %H:%M:%S $"),
433                 current_gm_time);
434 
435             state.out << strdate;
436         }
437 
438         state.out << "\" \n";
439 
440         if (!info.xmlbase_value.empty()) {
441             state.out << "    xml:base=\"" << info.xmlbase_value << "\"\n";
442         }
443 
444         state.out << "    xmlns:xi=\"http://www.w3.org/2001/XInclude\">\n";
445 
446         std::ostringstream tmp;
447 
448         if (!info.authors.empty()) {
449             tmp << "    <authorgroup>\n";
450             QUICKBOOK_FOR (value_consumer author_values, info.authors) {
451                 while (author_values.check()) {
452                     value surname =
453                         author_values.consume(doc_info_tags::author_surname);
454                     value first =
455                         author_values.consume(doc_info_tags::author_first);
456 
457                     tmp << "      <author>\n"
458                         << "        <firstname>" << doc_info_output(first, 106)
459                         << "</firstname>\n"
460                         << "        <surname>" << doc_info_output(surname, 106)
461                         << "</surname>\n"
462                         << "      </author>\n";
463                 }
464             }
465             tmp << "    </authorgroup>\n";
466         }
467 
468         QUICKBOOK_FOR (value_consumer copyright, info.copyrights) {
469             while (copyright.check()) {
470                 tmp << "\n"
471                     << "    <copyright>\n";
472 
473                 while (copyright.check(doc_info_tags::copyright_year)) {
474                     value year_start_value = copyright.consume();
475                     int year_start = year_start_value.get_int();
476                     int year_end =
477                         copyright.check(doc_info_tags::copyright_year_end)
478                             ? copyright.consume().get_int()
479                             : year_start;
480 
481                     if (year_end < year_start) {
482                         ++state.error_count;
483 
484                         detail::outerr(
485                             state.current_file,
486                             copyright.begin()->get_position())
487                             << "Invalid year range: " << year_start << "-"
488                             << year_end << "." << std::endl;
489                     }
490 
491                     for (; year_start <= year_end; ++year_start)
492                         tmp << "      <year>" << year_start << "</year>\n";
493                 }
494 
495                 tmp << "      <holder>"
496                     << doc_info_output(
497                            copyright.consume(doc_info_tags::copyright_name),
498                            106)
499                     << "</holder>\n"
500                     << "    </copyright>\n"
501                     << "\n";
502             }
503         }
504 
505         if (!info.license.empty()) {
506             tmp << "    <legalnotice id=\""
507                 << state.document.add_id("legal", id_category::generated)
508                 << "\">\n"
509                 << "      <para>\n"
510                 << "        " << doc_info_output(info.license, 103) << "\n"
511                 << "      </para>\n"
512                 << "    </legalnotice>\n"
513                 << "\n";
514         }
515 
516         if (!info.purpose.empty()) {
517             tmp << "    <" << info.doc_type << "purpose>\n"
518                 << "      " << doc_info_output(info.purpose, 103) << "    </"
519                 << info.doc_type << "purpose>\n"
520                 << "\n";
521         }
522 
523         QUICKBOOK_FOR (value_consumer category_values, info.categories) {
524             value category = category_values.optional_consume();
525             if (!category.empty()) {
526                 tmp << "    <" << info.doc_type << "category name=\"category:"
527                     << doc_info_output(category, 106) << "\"></"
528                     << info.doc_type << "category>\n"
529                     << "\n";
530             }
531             category_values.finish();
532         }
533 
534         QUICKBOOK_FOR (value_consumer biblioid, info.biblioids) {
535             value class_ = biblioid.consume(doc_info_tags::biblioid_class);
536             value value_ = biblioid.consume(doc_info_tags::biblioid_value);
537 
538             tmp << "    <biblioid class=\"" << class_.get_quickbook() << "\">"
539                 << doc_info_output(value_, 106) << "</biblioid>"
540                 << "\n";
541             biblioid.finish();
542         }
543 
544         QUICKBOOK_FOR (value escaped, info.escaped_attributes) {
545             tmp << "<!--quickbook-escape-prefix-->" << escaped.get_quickbook()
546                 << "<!--quickbook-escape-postfix-->";
547         }
548 
549         if (info.doc_type != "library") {
550             write_document_title(state.out, info.doc_title, info.version);
551         }
552 
553         std::string docinfo = tmp.str();
554         if (!docinfo.empty()) {
555             state.out << "  <" << info.doc_type << "info>\n"
556                       << docinfo << "  </" << info.doc_type << "info>\n"
557                       << "\n";
558         }
559 
560         if (info.doc_type == "library") {
561             write_document_title(state.out, info.doc_title, info.version);
562         }
563 
564         return info.doc_type;
565     }
566 
post(quickbook::state & state,std::string const & doc_type)567     void post(quickbook::state& state, std::string const& doc_type)
568     {
569         // We've finished generating our output. Here's what we'll do
570         // *after* everything else.
571 
572         // Close any open sections.
573         if (!doc_type.empty() && state.document.section_level() > 1) {
574             if (state.strict_mode) {
575                 detail::outerr(state.current_file->path)
576                     << "Missing [endsect] detected at end of file (strict "
577                        "mode)."
578                     << std::endl;
579                 ++state.error_count;
580             }
581             else {
582                 detail::outwarn(state.current_file->path)
583                     << "Missing [endsect] detected at end of file."
584                     << std::endl;
585             }
586 
587             while (state.document.section_level() > 1) {
588                 state.out << "</section>";
589                 state.document.end_section();
590             }
591         }
592 
593         state.document.end_file();
594         if (!doc_type.empty()) state.out << "\n</" << doc_type << ">\n\n";
595     }
596 
write_document_title(collector & out,value const & title,value const & version)597     static void write_document_title(
598         collector& out, value const& title, value const& version)
599     {
600         if (!title.empty()) {
601             out << "  <title>" << doc_info_output(title, 106);
602             if (!version.empty()) {
603                 out << ' ' << doc_info_output(version, 106);
604             }
605             out << "</title>\n\n\n";
606         }
607     }
608 }
609