// write_events.cc - Don Yang (uguu.org) // // 06/10/11 #include"write_events.h" #include #include #include #include #include #include"write_template.h" #include"unicode.h" using namespace std; namespace { static const int kReplaceEvent = AllEvents::CompiledEvent::REPLACE; static const int kAppendEvent = AllEvents::CompiledEvent::APPEND; static const int kDeleteEvent = AllEvents::CompiledEvent::DELETE; static const int kCursorEvent = AllEvents::CompiledEvent::CURSOR; static const int kSlowCursorEvent = AllEvents::CompiledEvent::SLOW_CURSOR; // Break up array initialization into smaller chunks if it contained // more than this many elements. This is needed to workaround Firefox // "array initialiser too large" error. // // This must be a multiple of 6 due to how event list is updated. static const int kMaxElementsPerArray = 0xfffc; // (slot number, list of events) typedef map > SlotMap; // Escape a single UTF-8 character static void EscapeUtf8Char(const string &input, string::const_iterator *i, string *output, int *length) { char buffer[32]; assert(*i != input.end()); const unsigned int u = ParseUtf8Char(input, i); if( u <= 0xffff ) { // 16 bits or less, use JavaScript literal snprintf(buffer, sizeof(buffer), "\\u%04X", u); output->append(buffer); ++*length; return; } // Doesn't fit in 16 bits, use HTML entity snprintf(buffer, sizeof(buffer), "&#%u;", u); output->append(buffer); *length += strlen(buffer); } // Escape a control character (e.g. tabs) static void EscapeControlChar(int c, string *output, int *length) { char buffer[8]; snprintf(buffer, sizeof(buffer), "\\x%02x", c); output->append(buffer); ++*length; } // Escape non-HTML or non-JavaScript friendly characters in string. static void EscapeString(const string &input, string *output, int *length) { output->clear(); *length = 0; for(string::const_iterator i = input.begin(); i != input.end(); ++i) { // Convert character to fit in 0..255 range, dropping the sign // bits if character is signed. const int c = static_cast(*i) & 0xff; if( c == '&' ) { output->append("&"); *length += 5; } else if( c == '<' ) { output->append("<"); *length += 4; } else if( c == '>' ) { output->append(">"); *length += 4; } else if( c == '\'' ) { output->append("\\'"); ++*length; } else if( c == '\"' ) { output->append("\\\""); ++*length; } else if( c == '\\' ) { output->append("\\\\"); ++*length; } else if( (c & 0x80) != 0 ) { EscapeUtf8Char(input, &i, output, length); } else if( c < 32 || c == 127 ) { EscapeControlChar(c, output, length); } else { output->push_back(c); ++*length; } } } } // namespace // Load events Animation::Animation(const AllEvents::CompiledEventList &compiled_events, int max_file_size, int output_slot_count) : max_event_time_(0), max_file_size_(max_file_size), output_slot_count_(output_slot_count) { if( compiled_events.empty() ) return; CopyEvents(compiled_events); SetDurations(); } Animation::~Animation() {} // Copy all input events void Animation::CopyEvents(const AllEvents::CompiledEventList &input_list) { events_.reserve(input_list.size()); string escaped_text; vector escaped_lengths; escaped_lengths.reserve(input_list.size()); for(AllEvents::CompiledEventList::const_iterator i = input_list.begin(); i != input_list.end(); ++i) { const AllEvents::CompiledEvent &input = **i; events_.push_back(Event()); Event *output = &(events_.back()); // Convert timestamp assert(input.timestamp >= 0); assert(input.frame >= 0); output->start = static_cast(input.timestamp) * 1000 + static_cast((input.frame * 1000) / input.max_frame); // Set default values output->duration = output->slot = output->row = output->column = output->offset = output->length = 0; output->text = &sentinel_string_; escaped_lengths.push_back(0); // Copy fields output->type = input.type; if( input.type == kCursorEvent ) { output->row = input.row; output->column = input.column; } else if( input.type == kSlowCursorEvent ) { output->column = input.column; output->slot = input.output->second; assert(input.column < static_cast(input.text.size())); EscapeString(input.text.substr(0, output->column), &escaped_text, &(output->offset)); EscapeString(input.text.substr(output->column), &escaped_text, &(output->length)); } else { output->slot = input.output->second; if( input.type != kDeleteEvent ) { EscapeString(input.text, &escaped_text, &(output->length)); output->text = strings_.Add(escaped_text); // output->length stores the length of string after they // are parsed by JavaScript. For canonicalization, we // need to store the length of the string as they are // represented in the string pool. Add that here now. escaped_lengths.back() = escaped_text.size(); } } } // Set max event timestamp assert(!events_.empty()); max_event_time_ = events_.back().start; // Canonicalize all strings and sort for(int i = 0; i < static_cast(events_.size()); i++) { if( escaped_lengths[i] > 0 ) { events_[i].text = strings_.Canonicalize(events_[i].text, escaped_lengths[i]); } } strings_.Sort(); } // Set duration for all events void Animation::SetDurations() { Event *last_cursor = NULL; SlotMap last_edit; for(EventList::iterator i = events_.begin(); i != events_.end(); ++i) { Event *event = &(*i); if( event->type == kCursorEvent || event->type == kSlowCursorEvent ) { // Set end time for previous cursor event if( last_cursor != NULL ) last_cursor->duration = event->start - last_cursor->start; last_cursor = event; continue; } pair p = last_edit.insert( make_pair(event->slot, vector())); vector *event_list = &(p.first->second); if( event->type == kReplaceEvent || event->type == kDeleteEvent ) { // Set end time for previous edit events for(vector::iterator j = event_list->begin(); j != event_list->end(); ++j) { (*j)->duration = event->start - (*j)->start; } event_list->clear(); } // Attach current event to slot event_list->push_back(event); } } // Write output to opened file handle void Animation::Write(FILE *output) const { assert((kMaxElementsPerArray % 6) == 0); #define ARRAY_HEADER(a) \ if( (array_size % kMaxElementsPerArray) == 0 ) \ { \ if( array_size == 0 ) a.push_back('['); \ else a.append(#a "=" #a ".concat(["); \ } \ else \ { \ a.push_back(','); \ } #define ARRAY_FOOTER(a) \ if( (array_size % kMaxElementsPerArray) == 0 ) \ a.append(array_size == kMaxElementsPerArray ? "];" : "]);") #define CLOSE_ARRAY(a) \ if( array_size > 0 ) \ { \ if( (array_size % kMaxElementsPerArray) != 0 ) \ a.append(array_size < kMaxElementsPerArray ? "];" : "]);"); \ } \ else \ { \ assert(a.empty()); \ a = "[];"; \ } // Build string table string string_table; int array_size = 0; for(StringPool::ItemList::const_iterator i = strings_.strings().begin(); i != strings_.strings().end(); ++i) { ARRAY_HEADER(string_table); string_table.push_back('"'); string_table.append((*i)->data); string_table.push_back('"'); array_size++; ARRAY_FOOTER(string_table); } CLOSE_ARRAY(string_table); // Build events #define BYTES_PER_NUMBER (3 * sizeof(int) + 1) char buffer[BYTES_PER_NUMBER * 6]; string events; Millisecond last_timestamp = 0; array_size = 0; for(EventList::const_iterator i = events_.begin(); i != events_.end(); ++i) { ARRAY_HEADER(events); // Convert start timestamps to relative timestamps (greatly // reduces output size), and encode event values using decimal. // // Here it's tempting to try to save some space using // hexadecimal, but due to the "0x" prefix, output string will // be larger with hex rather than decimal except for values >= // 1e12. We will need a timestamp delta larger than 31 years to // make this worthwhile. // // Also, using hex digits increases entropy in output file, // which means the compression ratio will be worse. const Event &e = *i; if( e.type == kCursorEvent ) { snprintf(buffer, sizeof(buffer), "%ld,%ld,%d,0,%d,%d", e.start - last_timestamp, e.duration, e.type, e.row, e.column); } else if( e.type == kSlowCursorEvent ) { snprintf(buffer, sizeof(buffer), "%ld,%ld,%d,%d,%d,%d", e.start - last_timestamp, e.duration, 3 + e.length, e.slot, e.offset, e.column); } else { snprintf(buffer, sizeof(buffer), "%ld,%ld,%d,%d,%d,%d", e.start - last_timestamp, e.duration, e.type, e.slot, ((e.length > 0) ? e.text->index : 0), e.length); } last_timestamp = e.start; events.append(buffer); // Break up large arrays array_size += 6; ARRAY_FOOTER(events); } CLOSE_ARRAY(events); #undef ARRAY_HEADER #undef ARRAY_FOOTER #undef CLOSE_ARRAY // Write output WriteHtmlOutput(output_slot_count_, max_event_time_, max_file_size_, string_table, events, output); }