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 }