File indexing completed on 2025-02-23 04:35:29

0001 /*
0002     SPDX-FileCopyrightText: 2021-2022 Mladen Milinkovic <max@smoothware.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "richcss.h"
0008 
0009 #include <QDebug>
0010 #include <QStringBuilder>
0011 
0012 using namespace SubtitleComposer;
0013 
0014 
0015 namespace SubtitleComposer {
0016 class RichClass {
0017 public:
0018     RichClass() {}
0019 };
0020 }
0021 
0022 typedef bool (*charCompare)(QChar ch);
0023 
0024 RichCSS::RichCSS(QObject *parent)
0025     : QObject(parent)
0026 {
0027 }
0028 
0029 RichCSS::RichCSS(RichCSS &other)
0030     : QObject(),
0031       m_unformatted(other.m_unformatted),
0032       m_stylesheet(other.m_stylesheet)
0033 {
0034 }
0035 
0036 RichCSS &
0037 RichCSS::operator=(const RichCSS &rhs)
0038 {
0039     m_unformatted = rhs.m_unformatted;
0040     m_stylesheet = rhs.m_stylesheet;
0041     return *this;
0042 }
0043 
0044 static bool
0045 skipComment(const QChar **c)
0046 {
0047     // skip comment blocks
0048     if(**c == QChar('/') && !(*c)->isNull() && *((*c) + 1) == QChar('*')) {
0049         *c += 2;
0050         for(;;) {
0051             if((*c)->isNull())
0052                 break;
0053             if(**c == QChar('*') && !(*c)->isNull() && *((*c) + 1) == QChar('/')) {
0054                 *c += 2;
0055                 break;
0056             }
0057             ++(*c);
0058         }
0059         return true;
0060     }
0061     return false;
0062 }
0063 
0064 QStringList
0065 RichCSS::RuleList::toStringList()
0066 {
0067     QStringList s;
0068     for(auto it = cbegin(); it != cend(); ++it)
0069         s << QString::fromLatin1(it->name) % QChar(':') % it->value % QChar(';');
0070     return s;
0071 }
0072 
0073 void
0074 RichCSS::clear()
0075 {
0076     m_stylesheet.clear();
0077     m_unformatted.clear();
0078     emit changed();
0079 }
0080 
0081 void
0082 RichCSS::parse(const QChar *css)
0083 {
0084     if(!css || css->isNull())
0085         return;
0086 
0087     const QChar *cssStart = css;
0088 
0089     for(;;) {
0090         QString cssSel = parseCssSelector(&css);
0091 
0092         Q_ASSERT(css->isNull() || *css == QChar('{'));
0093         if(*css != QChar('{'))
0094             break;
0095         css++;
0096 
0097         RuleList cssRules = parseCssRules(&css);
0098 
0099         if(!cssSel.isEmpty() && !cssRules.isEmpty()) {
0100             auto it = m_stylesheet.begin();
0101             while(it != m_stylesheet.end()) {
0102                 if(it->selector == cssSel) {
0103                     mergeCssRules(it->rules, cssRules);
0104                     break;
0105                 }
0106                 ++it;
0107             }
0108             if(it == m_stylesheet.end())
0109                 m_stylesheet.push_back(Block{cssSel, cssRules});
0110         }
0111 
0112         Q_ASSERT(css->isNull() || *css == QChar('}'));
0113         if(*css != QChar('}'))
0114             break;
0115         css++;
0116     }
0117 
0118     m_unformatted.append(cssStart, css - cssStart);
0119 
0120     emit changed();
0121 }
0122 
0123 inline static bool
0124 nonSepSel(const QChar *c)
0125 {
0126     if(c->isSpace())
0127         return false;
0128     const char l = c->toLatin1();
0129     return l != '>' && l != '+' && l != '~';
0130 }
0131 
0132 static bool
0133 copyCssChar(QString *dst, const QChar *&c)
0134 {
0135     if(*c != QChar('\\')) {
0136         dst->push_back(*c);
0137         return false;
0138     }
0139 
0140     const QChar *e = c + 7; // max 6 hex digits
0141     uint32_t ucs32 = 0x80000000;
0142     const QChar *n = c + 1;
0143     for(;;) {
0144         if(n->isNull() || n == e)
0145             break;
0146         const char d = n->toLatin1();
0147         if(d >= '0' && d <= '9')
0148             ucs32 += d - '0';
0149         else if(d >= 'a' && d <= 'f')
0150             ucs32 += d - 'a' + 10;
0151         else if(d >= 'A' && d <= 'F')
0152             ucs32 += d - 'A' + 10;
0153         else
0154             break;
0155         ucs32 <<= 4;
0156         n++;
0157     }
0158     if(ucs32 != 0x80000000) {
0159         // parsed unicode char
0160         // NOTE: Spec at https://www.w3.org/International/questions/qa-escapes says that
0161         //       space after 6th digit is not needed, but can be included.
0162         //       Their examples ignore it - we do too.
0163         c = n != e && n->isSpace() ? n : n - 1;
0164         dst->push_back(QChar(ucs32 >> 4));
0165     } else if(!(c + 1)->isNull()) {
0166         // copy backslash
0167         dst->push_back(*c++);
0168         // and next char
0169         dst->push_back(*c);
0170     }
0171     return true;
0172 }
0173 
0174 RichCSS::Selector
0175 RichCSS::parseCssSelector(const QChar **stylesheet)
0176 {
0177     QString sel;
0178     const QChar *&c = *stylesheet;
0179 
0180     // skip starting spaces and comments
0181     while(!c->isNull()) {
0182         if(c->isSpace())
0183             c++;
0184         else if(!skipComment(&c))
0185             break;
0186     }
0187 
0188     bool skippedSpace = false;
0189     for(; !c->isNull() && *c != QChar('{'); c++) {
0190         if(skipComment(&c) && (c->isNull() || *c == QChar('{')))
0191             break;
0192         if(c->isSpace()) {
0193             skippedSpace = true;
0194             continue;
0195         }
0196         if(skippedSpace) {
0197             const QChar *p = sel.data() + sel.size() - 1;
0198             if(nonSepSel(p) && nonSepSel(c))
0199                 sel += QChar::Space;
0200             skippedSpace = false;
0201         }
0202         if(copyCssChar(&sel, c))
0203             continue;
0204         if(*c == QChar('[')) {
0205             while(!(++c)->isNull()) {
0206                 if(c->isSpace())
0207                     continue;
0208                 if(skipComment(&c) && c->isNull())
0209                     break;
0210                 if(copyCssChar(&sel, c))
0211                     continue;
0212                 if(*c == QChar(']'))
0213                     break;
0214                 if(*c == QChar('"')) {
0215                     while(!(++c)->isNull()) {
0216                         if(copyCssChar(&sel, c))
0217                             continue;
0218                         if(*c == QChar('"'))
0219                             break;
0220                     }
0221                     if(c->isNull())
0222                         break;
0223                 }
0224             }
0225             if(c->isNull())
0226                 break;
0227         }
0228     }
0229     return sel;
0230 }
0231 
0232 inline static bool
0233 nonSepVal(const QChar *c)
0234 {
0235     if(c->isSpace())
0236         return false;
0237     const char l = c->toLatin1();
0238     return l != '(' && l != ')' && l != '"' && l != '\'' && l != ',';
0239 }
0240 
0241 QString
0242 RichCSS::parseCssKey(const QChar **stylesheet)
0243 {
0244     QString cssKey;
0245     const QChar *&c = *stylesheet;
0246 
0247     // skip starting spaces and comments
0248     while(!c->isNull()) {
0249         if(c->isSpace())
0250             c++;
0251         else if(!skipComment(&c))
0252             break;
0253     }
0254 
0255     bool skippedSpace = false;
0256     for(; !c->isNull() && *c != QChar('}') && *c != QChar(':') && *c != QChar(';'); c++) {
0257         if(skipComment(&c) && (c->isNull() || *c == QChar('}') || *c == QChar(':') || *c == QChar(';')))
0258             break;
0259         if(c->isSpace()) {
0260             skippedSpace = true;
0261             continue;
0262         }
0263         if(skippedSpace) {
0264             cssKey += QChar::Space;
0265             skippedSpace = false;
0266         }
0267         copyCssChar(&cssKey, c);
0268     }
0269     return cssKey;
0270 }
0271 
0272 QString
0273 RichCSS::parseCssValue(const QChar **stylesheet)
0274 {
0275     QString cssValue;
0276     const QChar *&c = *stylesheet;
0277 
0278     // skip starting spaces and comments
0279     while(!c->isNull()) {
0280         if(c->isSpace())
0281             c++;
0282         else if(!skipComment(&c))
0283             break;
0284     }
0285 
0286     bool skippedSpace = false;
0287     for(; !c->isNull() && *c != QChar('}') && *c != QChar(';'); c++) {
0288         if(skipComment(&c) && (c->isNull() || *c == QChar('}') || *c == QChar(';')))
0289             break;
0290         if(c->isSpace()) {
0291             skippedSpace = true;
0292             continue;
0293         }
0294         if(skippedSpace) {
0295             const QChar *p = cssValue.data() + cssValue.size() - 1;
0296             if(nonSepVal(p) && nonSepVal(c))
0297                 cssValue += QChar::Space;
0298             skippedSpace = false;
0299         }
0300         if(copyCssChar(&cssValue, c))
0301             continue;
0302         if(*c == QChar('"') || *c == QChar('\'')) {
0303             const QChar ce = *c;
0304             while(!(++c)->isNull()) {
0305                 if(copyCssChar(&cssValue, c))
0306                     continue;
0307                 if(*c == ce)
0308                     break;
0309             }
0310             if(c->isNull())
0311                 break;
0312         }
0313     }
0314 
0315     return cssValue;
0316 }
0317 
0318 RichCSS::RuleList
0319 RichCSS::parseCssRules(const QChar **stylesheet)
0320 {
0321     RuleList cssRules;
0322     const QChar *&c = *stylesheet;
0323     for(;;) {
0324         QString cssKey = parseCssKey(stylesheet);
0325 
0326         Q_ASSERT(c->isNull() || *c == QChar('}') || *c == QChar(':') || *c == QChar(';'));
0327         if(*c == QChar(';')) {
0328             qWarning() << "invalid css - rule-name:" << cssKey << " wihtout value, terminated by" << *c;
0329             c++;
0330             continue;
0331         }
0332         if(*c != QChar(':'))
0333             break;
0334         c++;
0335 
0336         QString cssValue = parseCssValue(stylesheet);
0337 
0338         if(!cssKey.isEmpty() && !cssValue.isEmpty()) {
0339             RuleList::iterator it = cssRules.begin();
0340             for(;;) {
0341                 if(it == cssRules.end()) {
0342                     // new rule
0343                     cssRules.push_back(Rule{cssKey.toUtf8(), cssValue});
0344                     break;
0345                 }
0346                 if(it->name == cssKey) {
0347                     // overwrite duplicate rule
0348                     it->value = cssValue;
0349                     break;
0350                 }
0351                 ++it;
0352             }
0353         }
0354 
0355         Q_ASSERT(c->isNull() || *c == QChar('}') || *c == QChar(';'));
0356         if(*c != QChar(';'))
0357             break;
0358         c++;
0359     }
0360     return cssRules;
0361 }
0362 
0363 void
0364 RichCSS::mergeCssRules(RuleList &base, const RuleList &override) const
0365 {
0366     // we want the merged rule list to keep the original order - override is supposed to come after base in stylesheet
0367 
0368     // TODO: some rules can override multiple rules - we should expand those - e.g.
0369     //    background -> background-image background-repeat background-position ...
0370     //    font -> font-size line-height font-weight font-family ...
0371 
0372     // first remove duplicates from base
0373     for(const Rule &ro: override) {
0374         auto it = base.begin();
0375         while(it != base.end()) {
0376             if(it->name == ro.name)
0377                 it = base.erase(it); // TODO: some rules can merge with existing rules, unless we expand them above
0378             else
0379                 ++it;
0380         }
0381     }
0382 
0383     // then append new rules
0384     base.append(override);
0385 }
0386 
0387 QMap<QByteArray, QString>
0388 RichCSS::match(QSet<QString> selectors) const
0389 {
0390     QMap<QByteArray, QString> styles;
0391     for(const Block &b: qAsConst(m_stylesheet)) {
0392         const QChar *s = b.selector.constData();
0393         const QChar *ss = s;
0394         const QChar *e = s + b.selector.size();
0395         bool matched = true;
0396         for(;;) {
0397             if(*s == QChar('[')) {
0398                 while(*s != QChar(']') && s != e)
0399                     s++;
0400             }
0401             if(*s == QChar::Space || *s == QChar('>') || *s == QChar(',') || s == e) {
0402                 if(selectors.contains(QString(ss, s - ss))) {
0403                     if(*s == QChar(',') || s == e)
0404                         break; // matched full selector, we're done
0405                 } else {
0406                     while(*s != QChar(',') && s != e)
0407                         s++;
0408                     if(s == e) {
0409                         // not matched and no more selectors  - bail
0410                         matched = false;
0411                         break;
0412                     }
0413                     // not matched but have more selectors after ','
0414                 }
0415                 ss = s;
0416             }
0417             s++;
0418         }
0419         if(!matched)
0420             continue;
0421         // merge styles
0422         for(const Rule &r: qAsConst(b.rules))
0423             styles[r.name] = r.value;
0424     }
0425     return styles;
0426 }
0427 
0428 QSet<QString>
0429 RichCSS::classes() const
0430 {
0431     QSet<QString> all;
0432     for(const Block &b: qAsConst(m_stylesheet)) {
0433         const QChar *s = b.selector.constData();
0434         const QChar *ss;
0435         while(!s->isNull()) {
0436             if(*s == QChar('.')) {
0437                 ss = ++s;
0438                 while(!s->isNull() && !s->isSpace() && !s->isSymbol())
0439                     s++;
0440                 all.insert(QString(ss, s - ss));
0441             } else {
0442                 s++;
0443             }
0444         }
0445     }
0446     return all;
0447 }