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 }