andre@3: /* Copyright (C) 2016 by ETH Zürich andre@3: * Software engineering by Intevation GmbH andre@3: * andre@3: * This file is Free Software under the GNU GPL (v>=2) andre@3: * and comes with ABSOLUTELY NO WARRANTY! andre@3: * See LICENSE.txt for details. andre@3: */ andre@3: andre@3: #include "converter.h" andre@3: #include andre@3: #include andre@3: #include andre@3: andre@3: #include "xlsxdocument.h" andre@3: #include "xlsxconditionalformatting.h" andre@3: andre@3: #include "constants.h" andre@3: andre@3: QTXLSX_USE_NAMESPACE andre@3: andre@3: Converter::Converter(const QString &input, const QString &output, andre@3: ConvertFormat fmt, const QString &title): andre@3: QThread(Q_NULLPTR), andre@3: mInput(input), andre@3: mOutput(output), andre@3: mFmt(fmt), andre@3: mTitle(title) andre@3: { andre@3: mTitleFmt.setFontUnderline(Format::FontUnderlineSingle); andre@3: mTitleFmt.setFontSize(18); andre@3: mTitleFmt.setFontName("Calibri"); andre@3: mTitleFmt.setFontBold(true); andre@3: mTitleFmt.setVerticalAlignment(Format::AlignTop); andre@3: andre@3: mQuestionFmt.setFontSize(11); andre@3: mQuestionFmt.setFontName("Calibri"); andre@3: mQuestionFmt.setFontBold(true); andre@3: mQuestionFmt.setTopBorderStyle(Format::BorderThin); andre@6: mQuestionFmt.setBottomBorderStyle(Format::BorderThin); andre@3: mQuestionFmt.setTextWarp(true); andre@3: andre@3: mAnswerChoiceFmt.setFontSize(11); andre@3: mAnswerChoiceFmt.setFontName("Calibri"); andre@3: mAnswerChoiceFmt.setHorizontalAlignment(Format::AlignRight); andre@6: mAnswerChoiceFmt.setTextWarp(true); andre@3: andre@3: mChoiceTextFmt = mAnswerChoiceFmt; andre@3: mChoiceTextFmt.setVerticalAlignment(Format::AlignVCenter); andre@3: andre@6: mFreeTextFmt = mQuestionFmt; andre@6: mFreeTextFmt.setFontBold(false); andre@3: andre@6: mAnswerTextFmt = mQuestionFmt; andre@6: mAnswerTextFmt.setVerticalAlignment(Format::AlignVCenter); andre@6: mAnswerTextFmt.setHorizontalAlignment(Format::AlignLeft); andre@3: } andre@3: andre@3: void Converter::run() andre@3: { andre@3: QFile infile; andre@3: andre@3: if (mInput.isEmpty()) { andre@3: if (!infile.open(stdin, QIODevice::ReadOnly)) { andre@3: mErrors << tr("Failed to open standard input and no input file provided."); andre@3: return; andre@3: } andre@3: } else { andre@3: infile.setFileName(mInput); andre@3: if (!infile.open(QIODevice::ReadOnly)) { andre@3: mErrors << tr("Failed to open %1 for reading.").arg(mInput); andre@3: return; andre@3: } andre@3: } andre@3: QTextStream instream(&infile); andre@3: andre@3: QFile outfile; andre@3: if (mOutput.isEmpty()) { andre@3: if (!outfile.open(stdout, QIODevice::WriteOnly)) { andre@3: mErrors << tr("Failed to open standard output and no output file provided."); andre@3: return; andre@3: } andre@3: } else { andre@3: outfile.setFileName(mOutput); andre@3: if (!outfile.open(QIODevice::WriteOnly)) { andre@3: mErrors << tr("Failed to open %1 for writing.").arg(mOutput); andre@3: return; andre@3: } andre@3: } andre@3: convertToXSLX(instream, outfile); andre@3: } andre@3: andre@3: void Converter::convertToXSLX(QTextStream& instream, QFile &output) andre@3: { andre@3: Document xlsx; andre@3: andre@3: ConditionalFormatting bars; andre@3: andre@6: bars.addDataBarRule(QColor(0xFF, 0x99, 0x33), ConditionalFormatting::VOT_Num, andre@6: "0", ConditionalFormatting::VOT_Num, "100", false); andre@3: andre@3: const double colWidth[] = COLUMN_WIDTHS; andre@6: double sum = 0; andre@3: for (int i = 1; i <= COLUMN_CNT; i++) { andre@3: xlsx.setColumnWidth(i, colWidth[i-1]); andre@6: sum += colWidth[i-1]; andre@3: } andre@3: andre@6: /* For the merged cell wordwrap trick. */ andre@6: xlsx.setColumnWidth(26, sum - 1); andre@6: xlsx.setColumnHidden(26, true); andre@6: andre@3: int row = 1; andre@3: if (!mTitle.isEmpty()) { andre@3: // Set the title of the Questionaire andre@3: xlsx.write(row++, 1, mTitle, mTitleFmt); andre@3: xlsx.mergeCells("A1:C1"); andre@3: xlsx.setRowHeight(1, TITLE_ROW_HEIGHT); andre@3: } andre@3: andre@3: const QString input = instream.readAll(); andre@3: andre@3: QRegularExpression questionEx(QUESTION_PATTERN); andre@3: QRegularExpression choiceEx(CHOICE_PATTERN); andre@3: QRegularExpression freetxtEx (FREETXT_PATTERN); andre@3: andre@3: QRegularExpressionMatch match = questionEx.match(input); andre@3: bool foundSomething = false; andre@3: int cursor = match.capturedEnd(); andre@3: while (match.hasMatch() && cursor != -1) { andre@3: /* We've matched a question pattern. With the answer andre@3: line */ andre@3: if (!match.lastCapturedIndex() == 2) { andre@3: /* Should not happen without misconfiguration. */ andre@3: mErrors << "Internal parser error."; andre@3: return; andre@3: } andre@3: foundSomething = true; andre@3: const QString question = match.captured(1).trimmed(); andre@3: const QString answerLine = match.captured(2).trimmed(); andre@6: xlsx.write(row, 2, QString(" "), mQuestionFmt); andre@6: xlsx.write(row, 3, QString(" "), mQuestionFmt); andre@3: xlsx.write(row++, 1, question, mQuestionFmt); andre@3: andre@3: if (answerLine == QStringLiteral(CHOICE_IDENTIFIER)) { andre@3: QRegularExpressionMatch choiceMatch = choiceEx.match(input, cursor); andre@3: xlsx.setRowHeight(row, CHOICE_ROW_HEIGHT); andre@3: xlsx.write(row++, 1, tr("Answer"), mAnswerChoiceFmt); andre@3: int firstChoiceRow = row; andre@3: int lastChoiceRow = row; andre@3: while (choiceMatch.hasMatch() && choiceMatch.capturedStart() == cursor + 1) { andre@3: /* We use the cursor here to keep track of the state. Only if an answer andre@3: follows immediately behind the last answer we treat it as valid as andre@3: otherwise we can't figure out when the next question begins. */ andre@3: cursor = choiceMatch.capturedEnd(); andre@3: andre@3: /* Write the values */ andre@12: QString choiceName = choiceMatch.captured(1).trimmed(); andre@12: if (choiceName.startsWith("=")) { andre@12: choiceName = " " + choiceName; andre@12: } andre@12: xlsx.write(row, 1, choiceName, mChoiceTextFmt); andre@3: bool ok; andre@3: double percent = choiceMatch.captured(3).toDouble(&ok); andre@3: if (!ok) { andre@3: mErrors << "Unparsable number in string: " + choiceMatch.captured(); andre@3: } andre@3: xlsx.write(row, 2, percent); andre@3: xlsx.write(row, 3, tr("%1% | %2 Number of votes"). andre@3: arg(choiceMatch.captured(3)).arg(choiceMatch.captured(2)), andre@3: mChoiceVotesFmt); andre@3: xlsx.setRowHeight(row, CHOICE_ROW_HEIGHT); andre@3: /* As long as we can match a choice which is either before the next question andre@3: or before the end of the document */ andre@3: choiceMatch = choiceEx.match(input, cursor); andre@3: row++; andre@3: lastChoiceRow++; andre@3: } andre@3: bars.addRange(QString("B%1:B%2").arg(firstChoiceRow).arg(lastChoiceRow)); andre@3: } else if (answerLine == QStringLiteral(TEXT_IDENTIFIER)) { andre@6: QRegularExpressionMatch textMatch = freetxtEx.match(input, cursor); andre@6: xlsx.setRowHeight(row, CHOICE_ROW_HEIGHT); andre@6: xlsx.write(row++, 1, tr("Answer"), mAnswerTextFmt); andre@6: while (textMatch.hasMatch()) { andre@6: if (textMatch.capturedStart() != cursor + 1) { andre@6: /* The format allows unescaped quotes in the text. andre@6: This makes a workaround neccessary. If we have andre@6: an Unquoted string between the next quoted string andre@6: and that Unquoted string is before the next question andre@6: we append the unquoted string and the next quoted string andre@6: with Quotes in the Row.*/ andre@6: QRegularExpressionMatch nextQuestion = questionEx.match(input, cursor); andre@6: if (nextQuestion.hasMatch() && andre@6: nextQuestion.capturedStart() < textMatch.capturedEnd()) { andre@6: /* The next question comes before the textMatch so we really have andre@6: a new question. */ andre@6: break; andre@6: } andre@6: const QString lastRow = xlsx.read(row - 1, 26).toString(); andre@6: int unquotedLen = textMatch.capturedStart() - cursor; andre@6: const QString unquoted = input.mid(cursor, unquotedLen); andre@6: qDebug() << "Found inner quoted string: " << unquoted; andre@6: /* Now combine */ andre@6: const QString combined = QString("%1\"%2\"%3").arg(lastRow). andre@6: arg(unquoted). andre@6: arg(textMatch.captured(1).trimmed()); andre@6: qDebug() << "Last row: " << lastRow; andre@6: qDebug() << "Next Question is at: " << nextQuestion.capturedStart(); andre@6: qDebug() << "Text match is: " << textMatch.captured(1).trimmed(); andre@6: qDebug() << "cursor is at: " << cursor; andre@6: qDebug() << "text match starts at: " << textMatch.capturedStart(); andre@6: xlsx.write(row - 1, 26, combined, mFreeTextFmt); andre@12: xlsx.write(row - 1, 1, combined, mFreeTextFmt); andre@6: cursor = textMatch.capturedEnd(); andre@6: textMatch = freetxtEx.match(input, cursor); andre@6: continue; andre@6: } andre@6: cursor = textMatch.capturedEnd(); andre@6: andre@6: QString text = textMatch.captured(1).trimmed(); andre@6: qDebug() << "Found free text: " << text; andre@12: if (text.startsWith("=")) { andre@12: text = " " + text; andre@12: } andre@6: andre@6: /* Merge the cells */ andre@6: xlsx.mergeCells(QString("A%1:C%1").arg(row), mFreeTextFmt); andre@6: andre@6: /* Merged cells ignore wordwrap the following trick is based on: andre@6: http://excel.tips.net/T003207_Automatic_Row_Height_For_Merged_Cells_with_Text_Wrap.html andre@6: */ andre@6: /* Write the values */ andre@6: xlsx.write(QString("Z%1").arg(row), text, mFreeTextFmt); andre@12: xlsx.write(row, 1, text, mFreeTextFmt); andre@6: row++; andre@6: textMatch = freetxtEx.match(input, cursor); andre@6: } andre@3: } andre@3: /* Insert Empty row. */ andre@3: xlsx.setRowHeight(row++, CHOICE_ROW_HEIGHT); andre@3: match = questionEx.match(input, cursor); andre@3: cursor = match.capturedEnd(); andre@3: } andre@3: xlsx.addConditionalFormatting(bars); andre@3: andre@3: if (!foundSomething) { andre@3: mErrors << tr("Failed to parse input document."); andre@3: } andre@3: andre@3: if (!xlsx.saveAs(&output)) { andre@3: mErrors << tr("Saving the XLSX document failed."); andre@3: return; andre@3: } andre@3: }