diff --git a/src/ass_dialogue.cpp b/src/ass_dialogue.cpp index 38577ca75..73c272cfd 100644 --- a/src/ass_dialogue.cpp +++ b/src/ass_dialogue.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include @@ -120,7 +121,28 @@ void AssDialogue::Parse(std::string const& raw) { for (int& margin : Margin) margin = mid(0, boost::lexical_cast(tkn.next_str()), 9999); Effect = tkn.next_str_trim(); - Text = std::string(tkn.next_tok().begin(), str.end()); + + std::string text{tkn.next_tok().begin(), str.end()}; + + static const boost::regex extradata_test("^\\{(=\\d+)+\\}"); + boost::match_results rematch; + if (boost::regex_search(text.begin(), text.end(), rematch, extradata_test)) { + std::string extradata_str = rematch.str(0); + text = rematch.suffix().str(); + + static const boost::regex idmatcher("=(\\d+)"); + auto start = extradata_str.begin(); + auto end = extradata_str.end(); + std::vector ids; + while (boost::regex_search(start, end, rematch, idmatcher)) { + auto id = boost::lexical_cast(rematch.str(1)); + ids.push_back(id); + start = rematch.suffix().second; + } + ExtradataIds = ids; + } + + Text = text; } void append_int(std::string &str, int v) { @@ -156,6 +178,16 @@ std::string AssDialogue::GetData(bool ssa) const { for (auto margin : Margin) append_int(str, margin); append_unsafe_str(str, Effect); + + if (ExtradataIds.get().size() > 0) { + str += "{"; + for (auto id : ExtradataIds.get()) { + str += "="; + boost::spirit::karma::generate(back_inserter(str), boost::spirit::karma::int_, id); + } + str += "}"; + } + str += Text.get(); if (str.find('\n') != str.npos || str.find('\r') != str.npos) { diff --git a/src/ass_dialogue.h b/src/ass_dialogue.h index 2a1dd28b0..affa6809a 100644 --- a/src/ass_dialogue.h +++ b/src/ass_dialogue.h @@ -146,6 +146,8 @@ struct AssDialogueBase { boost::flyweight Actor; /// Effect name boost::flyweight Effect; + /// IDs of extradata entries for line + boost::flyweight> ExtradataIds; /// Raw text data boost::flyweight Text; }; @@ -183,3 +185,4 @@ public: AssDialogue(std::string const& data); ~AssDialogue(); }; + diff --git a/src/ass_entry.cpp b/src/ass_entry.cpp index 30ec7f8dc..bdf486b0f 100644 --- a/src/ass_entry.cpp +++ b/src/ass_entry.cpp @@ -28,6 +28,7 @@ std::string const& AssEntry::GroupHeader(bool ssa) const { "[Fonts]", "[Graphics]", "[Events]", + "[Aegisub Extradata]", "" }; @@ -37,6 +38,7 @@ std::string const& AssEntry::GroupHeader(bool ssa) const { "[Fonts]", "[Graphics]", "[Events]", + "[Aegisub Extradata]", "" }; diff --git a/src/ass_entry.h b/src/ass_entry.h index f03d10336..9556ef18a 100644 --- a/src/ass_entry.h +++ b/src/ass_entry.h @@ -43,6 +43,7 @@ enum class AssEntryGroup { FONT, GRAPHIC, DIALOGUE, + EXTRADATA, GROUP_MAX }; diff --git a/src/ass_file.cpp b/src/ass_file.cpp index 8ceafe5d1..9d07b6220 100644 --- a/src/ass_file.cpp +++ b/src/ass_file.cpp @@ -26,6 +26,7 @@ #include #include #include +#include AssFile::AssFile() { } @@ -54,6 +55,8 @@ void AssFile::LoadDefault(bool include_dialogue_line) { AssFile::AssFile(const AssFile &from) : Info(from.Info) , Attachments(from.Attachments) +, Extradata(from.Extradata) +, next_extradata_id(from.next_extradata_id) { Styles.clone_from(from.Styles, [](AssStyle const& e) { return new AssStyle(e); }, @@ -68,6 +71,8 @@ void AssFile::swap(AssFile& from) throw() { Styles.swap(from.Styles); Events.swap(from.Events); Attachments.swap(from.Attachments); + Extradata.swap(from.Extradata); + std::swap(next_extradata_id, from.next_extradata_id); } AssFile& AssFile::operator=(AssFile from) { @@ -229,3 +234,58 @@ void AssFile::Sort(EntryList &lst, CompFunc comp, std::set AssFile::GetExtradata(std::vector const& id_list) const { + // If multiple IDs have the same key name, the last ID wins + std::map result; + for (auto id : id_list) { + auto it = Extradata.find(id); + if (it != Extradata.end()) + result[it->second.first] = it->second.second; + } + return result; +} + +void AssFile::CleanExtradata() { + // Collect all IDs existing in the database + // Then remove all IDs found to be in use from this list + // Remaining is then all garbage IDs + std::vector ids; + for (auto& it : Extradata) { + ids.push_back(it.first); + } + + // For each line, find which IDs it actually uses and remove them from the unused-list + for (auto& line : Events) { + // Find the ID for each unique key in the line + std::map key_ids; + for (auto id : line.ExtradataIds.get()) { + auto ed_it = Extradata.find(id); + if (ed_it == Extradata.end()) + continue; + key_ids[ed_it->second.first] = id; + } + // Update the line's ID list to only contain the actual ID for any duplicate keys + // Also mark found IDs as used in the cleaning list + std::vector new_ids; + for (auto& keyid : key_ids) { + new_ids.push_back(keyid.second); + ids.erase(std::remove(ids.begin(), ids.end(), keyid.second)); + } + line.ExtradataIds = new_ids; + } + + // The ids list should contain only unused IDs now + for (auto id : ids) { + Extradata.erase(id); + } +} + diff --git a/src/ass_file.h b/src/ass_file.h index 7ed814e5e..9c1bd9ae8 100644 --- a/src/ass_file.h +++ b/src/ass_file.h @@ -50,6 +50,8 @@ class wxString; template using EntryList = typename boost::intrusive::make_list, boost::intrusive::base_hook>::type; +using AegisubExtradataMap = std::map>; + struct AssFileCommit { wxString const& message; int *commit_id; @@ -66,6 +68,9 @@ public: EntryList Styles; EntryList Events; std::vector Attachments; + AegisubExtradataMap Extradata; + + uint32_t next_extradata_id = 0; AssFile(); AssFile(const AssFile &from); @@ -102,6 +107,16 @@ public: int GetUIStateAsInt(std::string const& key) const; void SaveUIState(std::string const& key, std::string const& value); + /// @brief Add a new extradata entry + /// @param key Class identifier/owner for the extradata + /// @param value Data for the extradata + /// @return ID of the created entry + uint32_t AddExtradata(std::string const& key, std::string const& value); + /// Fetch all extradata entries from a list of IDs + std::map GetExtradata(std::vector const& id_list) const; + /// Remove unreferenced extradata entries + void CleanExtradata(); + /// Type of changes made in a commit enum CommitType { /// Potentially the entire file has been changed; any saved information @@ -129,7 +144,9 @@ public: COMMIT_DIAG_TIME = 0x40, /// The text of existing dialogue lines have changed COMMIT_DIAG_TEXT = 0x80, - COMMIT_DIAG_FULL = COMMIT_DIAG_META | COMMIT_DIAG_TIME | COMMIT_DIAG_TEXT + COMMIT_DIAG_FULL = COMMIT_DIAG_META | COMMIT_DIAG_TIME | COMMIT_DIAG_TEXT, + /// Extradata entries were added/modified/removed + COMMIT_EXTRADATA = 0x100, }; DEFINE_SIGNAL_ADDERS(AnnounceCommit, AddCommitListener) @@ -168,3 +185,4 @@ public: /// @param limit If non-empty, only lines in this set are sorted static void Sort(EntryList& lst, CompFunc comp = CompStart, std::set const& limit = std::set()); }; + diff --git a/src/ass_parser.cpp b/src/ass_parser.cpp index 1099cc7a7..478704eb5 100644 --- a/src/ass_parser.cpp +++ b/src/ass_parser.cpp @@ -19,12 +19,15 @@ #include "ass_file.h" #include "ass_info.h" #include "ass_style.h" +#include "string_codec.h" #include "subtitle_format.h" #include #include #include #include +#include +#include AssParser::AssParser(AssFile *target, int version) : target(target) @@ -111,6 +114,21 @@ void AssParser::ParseGraphicsLine(std::string const& data) { attach.reset(new AssAttachment(data, AssEntryGroup::GRAPHIC)); } +void AssParser::ParseExtradataLine(std::string const &data) { + static const boost::regex matcher("Data:[[:space:]]*(\\d+),([^,]+),(.*)"); + boost::match_results mr; + + if (boost::regex_match(data, mr, matcher)) { + auto id = boost::lexical_cast(mr.str(1)); + auto key = inline_string_decode(mr.str(2)); + auto value = inline_string_decode(mr.str(3)); + + // ensure next_extradata_id is always at least 1 more than the largest existing id + target->next_extradata_id = std::max(id+1, target->next_extradata_id); + target->Extradata[id] = std::make_pair(key, value); + } +} + void AssParser::AddLine(std::string const& data) { // Special-case for attachments since a line could theoretically be both a // valid attachment data line and a valid section header, and if an @@ -142,6 +160,8 @@ void AssParser::AddLine(std::string const& data) { state = &AssParser::ParseGraphicsLine; else if (low == "[fonts]") state = &AssParser::ParseFontLine; + else if (low == "[aegisub extradata]") + state = &AssParser::ParseExtradataLine; else state = &AssParser::UnknownLine; return; diff --git a/src/ass_parser.h b/src/ass_parser.h index b97bba766..c51c20204 100644 --- a/src/ass_parser.h +++ b/src/ass_parser.h @@ -31,6 +31,7 @@ class AssParser { void ParseScriptInfoLine(std::string const& data); void ParseFontLine(std::string const& data); void ParseGraphicsLine(std::string const& data); + void ParseExtradataLine(std::string const &data); void UnknownLine(std::string const&) { } public: AssParser(AssFile *target, int version); diff --git a/src/subtitle_format_ass.cpp b/src/subtitle_format_ass.cpp index 3fdc30e39..3d5972a36 100644 --- a/src/subtitle_format_ass.cpp +++ b/src/subtitle_format_ass.cpp @@ -22,6 +22,7 @@ #include "ass_file.h" #include "ass_style.h" #include "ass_parser.h" +#include "string_codec.h" #include "text_file_reader.h" #include "text_file_writer.h" #include "version.h" @@ -114,6 +115,24 @@ struct Writer { file.WriteLineToFile(ssa ? line.GetSSAText() : line.GetEntryData()); } } + + void WriteExtradata(AegisubExtradataMap const& extradata) { + if (extradata.size() == 0) + return; + + group = AssEntryGroup::EXTRADATA; + file.WriteLineToFile(""); + file.WriteLineToFile("[Aegisub Extradata]"); + for (auto const& edi : extradata) { + std::string line = "Data: "; + line += std::to_string(edi.first); + line += ","; + line += inline_string_encode(edi.second.first); + line += ","; + line += inline_string_encode(edi.second.second); + file.WriteLineToFile(line); + } + } }; } @@ -124,4 +143,5 @@ void AssSubtitleFormat::WriteFile(const AssFile *src, agi::fs::path const& filen writer.Write(src->Styles); writer.Write(src->Attachments); writer.Write(src->Events); + writer.WriteExtradata(src->Extradata); }