| | 224 | #include <ctime> |
| | 225 | |
| | 226 | #include <gtkmm/textbuffer.h> |
| | 227 | #include <libxml++/libxml++.h> |
| | 228 | |
| | 229 | #include <libinftextgtk/inf-text-gtk-buffer.h> |
| | 230 | |
| | 231 | // write the Gtk::TextBuffer from document into content, inserting <span/>s for |
| | 232 | // line breaks and authorship of chunks of text, also save all users |
| | 233 | // encountered and the total number of lines dumped |
| | 234 | void dump_buffer(DocWindow& document, |
| | 235 | xmlpp::Element* content, |
| | 236 | std::set<InfTextUser*>& users, |
| | 237 | unsigned int& line_counter) { |
| | 238 | users.clear(); |
| | 239 | line_counter = 0; |
| | 240 | xmlpp::Element* last_node = content; |
| | 241 | last_node->add_child("div")->set_attribute("class", "line_no"); |
| | 242 | |
| | 243 | //Glib::RefPtr<Gtk::TextBuffer> buffer( |
| | 244 | // Glib::wrap( |
| | 245 | GtkTextBuffer* buffer = GTK_TEXT_BUFFER(document.get_text_buffer()); |
| | 246 | InfTextGtkBuffer* inf_buffer |
| | 247 | = INF_TEXT_GTK_BUFFER( |
| | 248 | inf_session_get_buffer(INF_SESSION(document.get_session()))); |
| | 249 | |
| | 250 | guint user_id; |
| | 251 | bool user_active = false; |
| | 252 | |
| | 253 | GtkTextIter begin; |
| | 254 | gtk_text_buffer_get_start_iter(buffer, &begin); |
| | 255 | |
| | 256 | if (gtk_text_iter_is_end(&begin)) |
| | 257 | return; |
| | 258 | |
| | 259 | // TODO: I just made this use the Gtk+ API instead of Gtkmm at like 2 am, |
| | 260 | // surely there are a bunch of segfaults/memleaks lurking |
| | 261 | for (;;) { |
| | 262 | GtkTextIter next = begin; |
| | 263 | gtk_text_iter_forward_to_tag_toggle(&next, 0); |
| | 264 | |
| | 265 | // split text by newlines so we can insert line number elements |
| | 266 | gchar* text = gtk_text_iter_get_text(&begin, &next); |
| | 267 | try{ |
| | 268 | gchar* last_pos = text; |
| | 269 | for (gchar* i = text; *i; ++i) { |
| | 270 | if (*i != '\n') |
| | 271 | continue; |
| | 272 | |
| | 273 | ++line_counter; |
| | 274 | |
| | 275 | gchar* next_pos = i; |
| | 276 | ++next_pos; |
| | 277 | last_node->add_child_text(Glib::ustring(last_pos, next_pos - last_pos)); |
| | 278 | last_pos = next_pos; |
| | 279 | |
| | 280 | // drop author <span/> for a moment for the line number <span/> |
| | 281 | if (user_active) |
| | 282 | last_node = last_node->get_parent(); |
| | 283 | last_node |
| | 284 | ->add_child("div") |
| | 285 | ->set_attribute("class", "line_no"); |
| | 286 | if (user_active) { |
| | 287 | last_node = last_node->add_child("span"); |
| | 288 | last_node->set_attribute( |
| | 289 | "class", |
| | 290 | Glib::ustring::compose("user_%1", user_id)); |
| | 291 | } |
| | 292 | } |
| | 293 | last_node->add_child_text(Glib::ustring(last_pos)); |
| | 294 | } catch (...) { |
| | 295 | g_free(text); |
| | 296 | throw; |
| | 297 | } |
| | 298 | g_free(text); |
| | 299 | |
| | 300 | begin = next; |
| | 301 | |
| | 302 | if (gtk_text_iter_is_end(&begin)) |
| | 303 | break; |
| | 304 | |
| | 305 | // check author, insert new <span/> if necessary |
| | 306 | InfTextUser* user |
| | 307 | = inf_text_gtk_buffer_get_author(inf_buffer, &next); |
| | 308 | |
| | 309 | if (user) { |
| | 310 | guint new_id = inf_user_get_id(INF_USER(user)); |
| | 311 | if (!user_active || new_id != user_id) { |
| | 312 | if (user_active) |
| | 313 | last_node = last_node->get_parent(); |
| | 314 | |
| | 315 | last_node = last_node->add_child("span"); |
| | 316 | last_node->set_attribute( |
| | 317 | "class", |
| | 318 | Glib::ustring::compose("user_%1", new_id)); |
| | 319 | } |
| | 320 | |
| | 321 | user_id = new_id; |
| | 322 | user_active = true; |
| | 323 | users.insert(user); |
| | 324 | } else { |
| | 325 | if (user_active) |
| | 326 | last_node = last_node->get_parent(); |
| | 327 | user_active = false; |
| | 328 | } |
| | 329 | } |
| | 330 | } |
| | 331 | |
| | 332 | // some random interesting information/advertisement to be put at the end of |
| | 333 | // the html output |
| | 334 | void dump_info(xmlpp::Element* node, DocWindow& document) { |
| | 335 | // put current time |
| | 336 | char const* time_str; |
| | 337 | int const n = 128; |
| | 338 | char buf[n]; |
| | 339 | { |
| | 340 | std::time_t now; |
| | 341 | std::time(&now); |
| | 342 | // TODO: localtime is not threadsafe, use boost.date_time!! |
| | 343 | if (std::strftime(buf, n, "%c", localtime(&now))) |
| | 344 | time_str = buf; |
| | 345 | else |
| | 346 | time_str = _("<unable to print date>"); |
| | 347 | } |
| | 348 | |
| | 349 | // put document metadata, like path, hostname of infinoted |
| | 350 | // TODO: figure out what interesting info we can pull out of the session |
| | 351 | char session_info[] = "<placeholder for session info and path>"; |
| | 352 | |
| | 353 | // TODO: do some more magic to make localising this easier |
| | 354 | node->add_child_text(_("Document generated by ")); |
| | 355 | xmlpp::Element* link = node->add_child("a"); |
| | 356 | link->set_attribute("href", "http://gobby.0x539.de/"); |
| | 357 | link->add_child_text(PACKAGE_STRING); |
| | 358 | node->add_child_text( |
| | 359 | Glib::ustring::compose(_(" %1 from %2 at %3"), |
| | 360 | PACKAGE_STRING, session_info, time_str)); |
| | 361 | } |
| | 362 | |
| | 363 | // put author colours into the css, list each author before the actual text |
| | 364 | void add_user_colours(xmlpp::Element* css, |
| | 365 | xmlpp::Element* list, |
| | 366 | const std::set<InfTextUser*>& users) { |
| | 367 | for (std::set<InfTextUser*>::const_iterator i = users.begin(); |
| | 368 | i != users.end(); |
| | 369 | ++i) { |
| | 370 | guint id = inf_user_get_id(INF_USER(*i)); |
| | 371 | gdouble hue = inf_text_user_get_hue(*i); |
| | 372 | gdouble r, g, b; |
| | 373 | // TODO: use Gdk::Color c; c.set_hsv(...) if we are not depending on 2.14 |
| | 374 | gtk_hsv_to_rgb(hue, 0.35, 1.0, &r, &g, &b); |
| | 375 | unsigned int rgb = |
| | 376 | ((static_cast<unsigned int>(r * 0xff) << 16) |
| | 377 | | (static_cast<unsigned int>(g * 0xff) << 8) |
| | 378 | | (static_cast<unsigned int>(b * 0xff))) & 0xffffff; |
| | 379 | gchar const* name = inf_user_get_name(INF_USER(*i)); |
| | 380 | // TODO: figure out if we can get "written by <name>" tooltips easily |
| | 381 | std::ostringstream ss; |
| | 382 | ss |
| | 383 | << ".user_" << id << " {\n" |
| | 384 | << " background-color: #" << std::hex << rgb << ";\n" |
| | 385 | << "}\n"; |
| | 386 | |
| | 387 | css->add_child_text(ss.str()); |
| | 388 | xmlpp::Element* item = list->add_child("li"); |
| | 389 | item->set_attribute("class", Glib::ustring::compose("user_%1", id)); |
| | 390 | item->add_child_text(name); |
| | 391 | } |
| | 392 | } |
| | 393 | |
| | 394 | // generate xhtml representation of the document and write it to the |
| | 395 | // specified location in the filesystem |
| | 396 | void export_html(DocWindow& document, const Glib::ustring& output_path) { |
| | 397 | xmlpp::Document output; |
| | 398 | xmlpp::Element |
| | 399 | * root = output.create_root_node("html", "http://www.w3.org/1999/xhtml"), |
| | 400 | * head = root->add_child("head"), |
| | 401 | * body = root->add_child("body"), |
| | 402 | * title = head->add_child("title"), |
| | 403 | * style = head->add_child("style"), |
| | 404 | * h1 = body->add_child("h1"), |
| | 405 | * h2 = body->add_child("h2"), |
| | 406 | * user_list = body->add_child("ul"), |
| | 407 | * content = body->add_child("pre"), |
| | 408 | * info = body->add_child("p"); |
| | 409 | |
| | 410 | const Glib::ustring& document_name = document.get_title(); |
| | 411 | title->add_child_text(document_name + " - infinote document"); |
| | 412 | |
| | 413 | h1->add_child_text(document_name); |
| | 414 | |
| | 415 | content->set_attribute("class", "document"); |
| | 416 | |
| | 417 | std::set<InfTextUser*> users; |
| | 418 | unsigned int line_counter; |
| | 419 | dump_buffer(document, content, users, line_counter); |
| | 420 | |
| | 421 | h2->add_child_text(_("Participants")); |
| | 422 | |
| | 423 | info->set_attribute("class", "info"); |
| | 424 | dump_info(info, document); |
| | 425 | |
| | 426 | style->set_attribute("type", "text/css"); |
| | 427 | add_user_colours(style, user_list, users); |
| | 428 | |
| | 429 | style->add_child_text( |
| | 430 | "h1 {\n" |
| | 431 | " font-family:\n" |
| | 432 | "}\n" |
| | 433 | ".document {\n" |
| | 434 | " border-top: 1px solid gray;\n" |
| | 435 | " border-bottom: 1px solid black;\n" |
| | 436 | " padding-bottom: 1.2em;\n" |
| | 437 | " counter-reset: line;\n" |
| | 438 | "}\n" |
| | 439 | ".line_no:before {\n" |
| | 440 | " content: counter(line);\n" |
| | 441 | " counter-increment: line;\n" |
| | 442 | "}\n" |
| | 443 | ".info {\n" |
| | 444 | " font-size: small;\n" |
| | 445 | "}\n"); |
| | 446 | |
| | 447 | style->add_child_text( |
| | 448 | Glib::ustring::compose( |
| | 449 | ".line_no {\n" |
| | 450 | " position: absolute;\n" |
| | 451 | " float: left;\n" |
| | 452 | " clear: left;\n" |
| | 453 | " margin-left: -%1em;\n" |
| | 454 | " color: gray;\n" |
| | 455 | "}\n" |
| | 456 | ".document {\n" |
| | 457 | " padding-left: %1em\n" |
| | 458 | "}\n", |
| | 459 | static_cast<unsigned int>(std::log(line_counter)/std::log(10))+1)); |
| | 460 | |
| | 461 | output.write_to_file(output_path); |
| | 462 | } |
| | 463 | |
| | 464 | void Gobby::FileCommands::on_export_html() { |
| | 465 | DocWindow* document = m_folder.get_current_document(); |
| | 466 | g_assert(document != NULL); |
| | 467 | |
| | 468 | // TODO: filechooser, possibly remember choice |
| | 469 | // what is that whole operation/task thing? |
| | 470 | export_html(*document, "output.xhtml"); |
| | 471 | } |
| | 472 | |
| | 473 | |