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