File indexing completed on 2025-02-23 04:35:31
0001 /* 0002 SPDX-FileCopyrightText: 2023 Mladen Milinkovic <max@smoothware.net> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "richdom.h" 0008 0009 #include "core/richtext/richdocument.h" 0010 #include "helpers/common.h" 0011 0012 #include <stack> 0013 0014 #include <QStringBuilder> 0015 #include <QTextFormat> 0016 0017 0018 using namespace SubtitleComposer; 0019 0020 0021 RichDOM::RichDOM() 0022 : m_root(new Node()) 0023 { 0024 } 0025 0026 RichDOM::~RichDOM() 0027 { 0028 delete m_root; 0029 } 0030 0031 0032 RichDOM::Node::Node(NodeType type_, const QString &klass_, const QString &id_) 0033 : type(type_), 0034 id(id_), 0035 klass(klass_), 0036 next(nullptr), 0037 parent(nullptr), 0038 children(nullptr) 0039 { 0040 } 0041 0042 RichDOM::Node::~Node() 0043 { 0044 delete next; 0045 delete children; 0046 } 0047 0048 RichDOM::Node::Node(const Node &o) 0049 : type(o.type), 0050 id(o.id), 0051 klass(o.klass), 0052 nodeStart(o.nodeStart), 0053 nodeEnd(o.nodeEnd), 0054 next(nullptr), 0055 parent(nullptr), 0056 children(nullptr) 0057 { 0058 } 0059 0060 RichDOM::Node & 0061 RichDOM::Node::operator=(const Node &o) 0062 { 0063 type = o.type; 0064 id = o.id; 0065 klass = o.klass; 0066 nodeStart = o.nodeStart; 0067 nodeEnd = o.nodeEnd; 0068 return *this; 0069 } 0070 0071 QString 0072 RichDOM::Node::cssSel() 0073 { 0074 static const QString names[] = { 0075 $("body"), // Root 0076 $("b"), // Bold 0077 $("i"), // Italic 0078 $("u"), // Underline 0079 $("s"), // Strikethrough 0080 $("font"), // Font 0081 $("c"), // Class 0082 $("v"), // Voice 0083 }; 0084 0085 QString nn = names[type]; 0086 if(!klass.isEmpty()) 0087 nn += QChar('.') + klass; 0088 if(!id.isEmpty()) 0089 nn += QChar('#') + id; 0090 return nn; 0091 } 0092 0093 static RichDOM::Node * 0094 nodeOpen(RichDOM::Node *parent, quint32 pos, RichDOM::NodeType type, const QString &klass=QString(), const QString &id=QString()) 0095 { 0096 auto *node = new RichDOM::Node(type, klass, id); 0097 node->nodeStart = pos; 0098 0099 auto *p = node->parent = parent; 0100 if(!p->children) { 0101 p->children = node; 0102 } else { 0103 p = p->children; 0104 while(p->next) 0105 p = p->next; 0106 p->next = node; 0107 } 0108 0109 return node; 0110 } 0111 0112 static RichDOM::Node * 0113 nodeClose(RichDOM::Node *last, int pos, RichDOM::NodeType type, const QString &klass=QString(), const QString &id=QString()) 0114 { 0115 // find node to close up in the tree 0116 std::stack<RichDOM::Node *> tmp; 0117 while(last->parent) { 0118 // close child/wanted nodes 0119 last->nodeEnd = pos; 0120 0121 if(last->type == type && last->klass == klass && last->id == id) { 0122 // store parent of wanted node 0123 last = last->parent; 0124 break; 0125 } 0126 0127 // store closed child nodes 0128 tmp.push(last); 0129 last = last->parent; 0130 } 0131 0132 // add clones of closed child nodes 0133 while(!tmp.empty()) { 0134 auto *n = tmp.top(); 0135 tmp.pop(); 0136 last = nodeOpen(last, pos, n->type, n->klass, n->id); 0137 } 0138 0139 return last; 0140 } 0141 0142 void 0143 RichDOM::update(const RichDocument *doc) 0144 { 0145 bool fB = false; 0146 bool fI = false; 0147 bool fU = false; 0148 bool fS = false; 0149 QRgb fC = 0; 0150 QSet<QString> fClass; 0151 QString fVoice; // <v:speaker name> - can't be nested... right? No need for QSet<QString> 0152 0153 delete m_root; 0154 m_root = new Node(Root); 0155 m_root->nodeStart = 0; 0156 0157 Node *last = m_root; 0158 0159 QTextBlock bi = doc->begin(); 0160 for(;;) { 0161 for(QTextBlock::iterator it = bi.begin(); !it.atEnd(); ++it) { 0162 const QTextFragment &f = it.fragment(); 0163 if(!f.isValid()) 0164 continue; 0165 const QTextCharFormat &format = f.charFormat(); 0166 const QSet<QString> &cl = format.property(RichDocument::Class).value<QSet<QString>>(); 0167 for(auto it = fClass.begin(); it != fClass.end();) { 0168 if(cl.contains(*it)) { 0169 ++it; 0170 continue; 0171 } 0172 last = nodeClose(last, f.position(), Class, *it); 0173 it = fClass.erase(it); 0174 } 0175 for(auto it = cl.cbegin(); it != cl.cend(); ++it) { 0176 if(fClass.contains(*it)) 0177 continue; 0178 last = nodeOpen(last, f.position(), Class, *it); 0179 fClass.insert(*it); 0180 } 0181 const QString &vt = format.property(RichDocument::Voice).value<QString>(); 0182 if(fVoice != vt) { 0183 if(!fVoice.isEmpty()) 0184 last = nodeClose(last, f.position(), Voice, fVoice); 0185 fVoice = vt; 0186 last = nodeOpen(last, f.position(), Voice, fVoice); 0187 } 0188 if(fB != (format.fontWeight() == QFont::Bold)) { 0189 if((fB = !fB)) 0190 last = nodeOpen(last, f.position(), Bold); 0191 else 0192 last = nodeClose(last, f.position(), Bold); 0193 } 0194 if(fI != format.fontItalic()) { 0195 if((fI = !fI)) 0196 last = nodeOpen(last, f.position(), Italic); 0197 else 0198 last = nodeClose(last, f.position(), Italic); 0199 } 0200 if(fU != format.fontUnderline()) { 0201 if((fU = !fU)) 0202 last = nodeOpen(last, f.position(), Underline); 0203 else 0204 last = nodeClose(last, f.position(), Underline); 0205 } 0206 if(fS != format.fontStrikeOut()) { 0207 if((fS = !fS)) 0208 last = nodeOpen(last, f.position(), Strikethrough); 0209 else 0210 last = nodeClose(last, f.position(), Strikethrough); 0211 } 0212 const QRgb fg = format.foreground().style() != Qt::NoBrush ? format.foreground().color().toRgb().rgb() : 0; 0213 if(fC != fg) { 0214 if(fC) 0215 last = nodeClose(last, f.position(), Font); 0216 if((fC = fg)) 0217 last = nodeOpen(last, f.position(), Font); 0218 } 0219 } 0220 bi = bi.next(); 0221 if(bi == doc->end()) { 0222 const int pos = doc->length(); 0223 while(last) { 0224 // close remaining node 0225 last->nodeEnd = pos; 0226 last = last->parent; 0227 } 0228 return; 0229 } 0230 } 0231 // unreachable 0232 } 0233 0234 void 0235 RichDOM::Node::debugDump(QString pfx) 0236 { 0237 QString f = pfx % $(" > ") % cssSel(); 0238 qDebug() << f; 0239 if(children) 0240 children->debugDump(f); 0241 if(next) 0242 next->debugDump(pfx); 0243 }