#include #include #include #include #include "hgmarkdownhighlighter.h" #include "vconfigmanager.h" #include "utils/vutils.h" extern VConfigManager *g_config; const int HGMarkdownHighlighter::initCapacity = 1024; void HGMarkdownHighlighter::resizeBuffer(int newCap) { if (newCap == capacity) { return; } if (capacity > 0) { Q_ASSERT(content); delete [] content; } capacity = newCap; content = new char [capacity]; } // Will be freeed by parent automatically HGMarkdownHighlighter::HGMarkdownHighlighter(const QVector &styles, const QHash &codeBlockStyles, int waitInterval, QTextDocument *parent) : QSyntaxHighlighter(parent), highlightingStyles(styles), m_codeBlockStyles(codeBlockStyles), m_numOfCodeBlockHighlightsToRecv(0), parsing(0), m_blockHLResultReady(false), waitInterval(waitInterval), m_enableMathjax(false), m_signalOut(false), content(NULL), capacity(0), result(NULL) { codeBlockStartExp = QRegExp(VUtils::c_fencedCodeBlockStartRegExp); codeBlockEndExp = QRegExp(VUtils::c_fencedCodeBlockEndRegExp); m_mathjaxInlineExp = QRegExp(VUtils::c_mathjaxInlineRegExp); m_mathjaxBlockExp = QRegExp(VUtils::c_mathjaxBlockRegExp); m_codeBlockFormat.setForeground(QBrush(Qt::darkYellow)); for (int index = 0; index < styles.size(); ++index) { switch (styles[index].type) { case pmh_VERBATIM: m_codeBlockFormat = styles[index].format; break; case pmh_LINK: m_linkFormat = styles[index].format; break; case pmh_IMAGE: m_imageFormat = styles[index].format; break; case pmh_CODE: // Use inline code font family for MathJax format. m_mathjaxFormat.setFontFamily(styles[index].format.fontFamily()); break; default: break; } } m_colorColumnFormat = m_codeBlockFormat; m_colorColumnFormat.setForeground(QColor(g_config->getEditorColorColumnFg())); m_colorColumnFormat.setBackground(QColor(g_config->getEditorColorColumnBg())); m_mathjaxFormat.setForeground(QColor(g_config->getEditorMathjaxFg())); m_headerStyles.resize(6); for (auto const & it : highlightingStyles) { if (it.type >= pmh_H1 && it.type <= pmh_H6) { m_headerStyles[it.type - pmh_H1] = it.format; } } resizeBuffer(initCapacity); document = parent; timer = new QTimer(this); timer->setSingleShot(true); timer->setInterval(this->waitInterval); connect(timer, &QTimer::timeout, this, [this]() { startParseAndHighlight(false); }); const int completeWaitTime = 400; m_completeTimer = new QTimer(this); m_completeTimer->setSingleShot(true); m_completeTimer->setInterval(completeWaitTime); connect(m_completeTimer, &QTimer::timeout, this, &HGMarkdownHighlighter::completeHighlight); connect(document, &QTextDocument::contentsChange, this, &HGMarkdownHighlighter::handleContentChange); } HGMarkdownHighlighter::~HGMarkdownHighlighter() { if (result) { pmh_free_elements(result); result = NULL; } if (content) { delete [] content; capacity = 0; content = NULL; } } void HGMarkdownHighlighter::updateBlockUserData(int p_blockNum, const QString &p_text) { Q_UNUSED(p_text); VTextBlockData *blockData = currentBlockData(); if (!blockData) { blockData = new VTextBlockData(); setCurrentBlockUserData(blockData); } else { blockData->setCodeBlockIndentation(-1); blockData->clearMathjax(); } if (blockData->getPreviews().isEmpty()) { m_possiblePreviewBlocks.remove(p_blockNum); } else { m_possiblePreviewBlocks.insert(p_blockNum); } } void HGMarkdownHighlighter::highlightBlock(const QString &text) { QTextBlock curBlock = currentBlock(); int blockNum = curBlock.blockNumber(); if (m_blockHLResultReady && m_blockHighlights.size() > blockNum) { // units are sorted by start position and length. const QVector &units = m_blockHighlights[blockNum]; if (!units.isEmpty()) { for (int i = 0; i < units.size(); ++i) { const HLUnit &unit = units[i]; if (i == 0) { // No need to merge format. setFormat(unit.start, unit.length, highlightingStyles[unit.styleIndex].format); } else { QTextCharFormat newFormat = highlightingStyles[unit.styleIndex].format; for (int j = i - 1; j >= 0; --j) { if (units[j].start + units[j].length <= unit.start) { // It won't affect current unit. continue; } else { // Merge the format. QTextCharFormat tmpFormat(newFormat); newFormat = highlightingStyles[units[j].styleIndex].format; // tmpFormat takes precedence. newFormat.merge(tmpFormat); } } setFormat(unit.start, unit.length, newFormat); } } } } // We use PEG Markdown Highlight as the main highlighter. // We can use other highlighting methods to complement it. // Set current block's user data. updateBlockUserData(blockNum, text); int preState = previousBlockState(); bool inCodeblock = preState == HighlightBlockState::CodeBlock || preState == HighlightBlockState::CodeBlockStart; // If it is a block inside HTML comment, just skip it. // Pay attention to distinguish the HTML comments inside a fenced code block. if (!inCodeblock && isBlockInsideCommentRegion(curBlock)) { setCurrentBlockState(HighlightBlockState::Comment); goto exit; } setCurrentBlockState(HighlightBlockState::Normal); highlightCodeBlock(blockNum, text); if (currentBlockState() == HighlightBlockState::Normal) { if (isVerbatimBlock(curBlock)) { setCurrentBlockState(HighlightBlockState::Verbatim); goto exit; } highlightHeaderFast(blockNum, text); if (m_enableMathjax && currentBlockState() == HighlightBlockState::Normal) { highlightMathJax(curBlock, text); } } if (currentBlockState() != HighlightBlockState::CodeBlock) { goto exit; } // Highlight CodeBlock using VCodeBlockHighlightHelper. if (m_codeBlockHighlights.size() > blockNum) { const QVector &units = m_codeBlockHighlights[blockNum]; if (!units.isEmpty()) { QVector formats(units.size(), NULL); for (int i = 0; i < units.size(); ++i) { const HLUnitStyle &unit = units[i]; auto it = m_codeBlockStyles.find(unit.style); if (it == m_codeBlockStyles.end()) { continue; } formats[i] = &(*it); QTextCharFormat newFormat = m_codeBlockFormat; newFormat.merge(*it); for (int j = i - 1; j >= 0; --j) { if (units[j].start + units[j].length <= unit.start) { // It won't affect current unit. continue; } else { // Merge the format. if (formats[j]) { QTextCharFormat tmpFormat(newFormat); newFormat = *(formats[j]); // tmpFormat takes precedence. newFormat.merge(tmpFormat); } } } setFormat(unit.start, unit.length, newFormat); } } } highlightCodeBlockColorColumn(text); exit: highlightChanged(); } static bool compHLUnit(const HLUnit &p_a, const HLUnit &p_b) { if (p_a.start < p_b.start) { return true; } else if (p_a.start == p_b.start) { return p_a.length > p_b.length; } else { return false; } } void HGMarkdownHighlighter::initBlockHighlightFromResult(int nrBlocks) { m_blockHighlights.resize(nrBlocks); for (int i = 0; i < m_blockHighlights.size(); ++i) { m_blockHighlights[i].clear(); } if (!result) { return; } for (int i = 0; i < highlightingStyles.size(); i++) { const HighlightingStyle &style = highlightingStyles[i]; pmh_element *elem_cursor = result[style.type]; while (elem_cursor != NULL) { // elem_cursor->pos and elem_cursor->end is the start // and end position of the element in document. if (elem_cursor->end <= elem_cursor->pos) { elem_cursor = elem_cursor->next; continue; } initBlockHighlihgtOne(elem_cursor->pos, elem_cursor->end, i); elem_cursor = elem_cursor->next; } } // Sort m_blockHighlights. for (int i = 0; i < m_blockHighlights.size(); ++i) { if (m_blockHighlights[i].size() > 1) { std::sort(m_blockHighlights[i].begin(), m_blockHighlights[i].end(), compHLUnit); } } } void HGMarkdownHighlighter::initHtmlCommentRegionsFromResult() { m_commentRegions.clear(); if (!result) { return; } pmh_element *elem = result[pmh_COMMENT]; while (elem != NULL) { if (elem->end <= elem->pos) { elem = elem->next; continue; } initBlockElementRegionOne(m_commentRegions, elem->pos, elem->end); elem = elem->next; } } void HGMarkdownHighlighter::initImageRegionsFromResult() { // From Qt5.7, the capacity is preserved. m_imageRegions.clear(); if (!result) { return; } pmh_element *elem = result[pmh_IMAGE]; while (elem != NULL) { if (elem->end <= elem->pos) { elem = elem->next; continue; } m_imageRegions.push_back(VElementRegion(elem->pos, elem->end)); elem = elem->next; } } void HGMarkdownHighlighter::initVerbatimBlocksFromResult() { m_verbatimBlocks.clear(); if (!result) { return; } pmh_element *elem = result[pmh_VERBATIM]; while (elem != NULL) { if (elem->end <= elem->pos) { elem = elem->next; continue; } // [firstBlock, lastBlock]. int firstBlock = document->findBlock(elem->pos).blockNumber(); int lastBlock = document->findBlock(elem->end - 1).blockNumber(); for (int i = firstBlock; i <= lastBlock; ++i) { m_verbatimBlocks.insert(i); } elem = elem->next; } } void HGMarkdownHighlighter::initFencedCodeBlocksFromResult() { m_codeBlocks.clear(); m_codeBlocksState.clear(); if (!result) { return; } // Ordered by start position in ascending order. QMap regs; pmh_element *elem = result[pmh_FENCEDCODEBLOCK]; while (elem != NULL) { if (elem->end <= elem->pos) { elem = elem->next; continue; } if (!regs.contains(elem->pos)) { regs.insert(elem->pos, VElementRegion(elem->pos, elem->end)); } elem = elem->next; } VCodeBlock item; bool inBlock = false; for (auto it = regs.begin(); it != regs.end(); ++it) { // [firstBlock, lastBlock]. int firstBlock = document->findBlock(it.value().m_startPos).blockNumber(); int lastBlock = document->findBlock(it.value().m_endPos - 1).blockNumber(); QTextBlock block = document->findBlockByNumber(firstBlock); while (block.isValid()) { int blockNumber = block.blockNumber(); if (blockNumber > lastBlock) { break; } HighlightBlockState state = HighlightBlockState::Normal; QString text = block.text(); if (inBlock) { item.m_text = item.m_text + "\n" + text; int idx = codeBlockEndExp.indexIn(text); if (idx >= 0) { // End block. inBlock = false; state = HighlightBlockState::CodeBlockEnd; item.m_endBlock = blockNumber; m_codeBlocks.append(item); } else { // Within code block. state = HighlightBlockState::CodeBlock; } } else { int idx = codeBlockStartExp.indexIn(text); if (idx >= 0) { // Start block. inBlock = true; state = HighlightBlockState::CodeBlockStart; item.m_startBlock = blockNumber; item.m_startPos = block.position(); item.m_text = text; if (codeBlockStartExp.captureCount() == 2) { item.m_lang = codeBlockStartExp.capturedTexts()[2]; } } } if (state != HighlightBlockState::Normal) { m_codeBlocksState.insert(blockNumber, state); } block = block.next(); } } } void HGMarkdownHighlighter::initHeaderRegionsFromResult() { // From Qt5.7, the capacity is preserved. m_headerBlocks.clear(); m_headerRegions.clear(); if (!result) { return; } pmh_element_type hx[6] = {pmh_H1, pmh_H2, pmh_H3, pmh_H4, pmh_H5, pmh_H6}; for (int i = 0; i < 6; ++i) { pmh_element *elem = result[hx[i]]; while (elem != NULL) { if (elem->end <= elem->pos) { elem = elem->next; continue; } m_headerRegions.push_back(VElementRegion(elem->pos, elem->end)); QTextBlock block = document->findBlock(elem->pos); if (block.isValid()) { // Header element will contain the new line character. m_headerBlocks.insert(block.blockNumber(), HeaderBlockInfo(i, elem->end - elem->pos - 1)); } elem = elem->next; } } std::sort(m_headerRegions.begin(), m_headerRegions.end()); } void HGMarkdownHighlighter::initBlockHighlihgtOne(unsigned long pos, unsigned long end, int styleIndex) { // When the the highlight element is at the end of document, @end will equals // to the characterCount. unsigned int nrChar = (unsigned int)document->characterCount(); if (end >= nrChar && nrChar > 0) { end = nrChar - 1; } int startBlockNum = document->findBlock(pos).blockNumber(); int endBlockNum = document->findBlock(end).blockNumber(); for (int i = startBlockNum; i <= endBlockNum; ++i) { QTextBlock block = document->findBlockByNumber(i); int blockStartPos = block.position(); HLUnit unit; if (i == startBlockNum) { unit.start = pos - blockStartPos; unit.length = (startBlockNum == endBlockNum) ? (end - pos) : (block.length() - unit.start); } else if (i == endBlockNum) { unit.start = 0; unit.length = end - blockStartPos; } else { unit.start = 0; unit.length = block.length(); } unit.styleIndex = styleIndex; m_blockHighlights[i].append(unit); } } void HGMarkdownHighlighter::highlightCodeBlock(int p_blockNumber, const QString &p_text) { auto it = m_codeBlocksState.find(p_blockNumber); if (it != m_codeBlocksState.end()) { VTextBlockData *blockData = currentBlockData(); Q_ASSERT(blockData); HighlightBlockState state = it.value(); // Set code block indentation. switch (state) { case HighlightBlockState::CodeBlockStart: { int index = codeBlockStartExp.indexIn(p_text); Q_ASSERT(index >= 0); blockData->setCodeBlockIndentation(codeBlockStartExp.capturedTexts()[1].size()); break; } case HighlightBlockState::CodeBlock: V_FALLTHROUGH; case HighlightBlockState::CodeBlockEnd: { int startLeadingSpaces = 0; VTextBlockData *preBlockData = previousBlockData(); if (preBlockData) { startLeadingSpaces = preBlockData->getCodeBlockIndentation(); } blockData->setCodeBlockIndentation(startLeadingSpaces); break; } default: Q_ASSERT(false); break; } // Set code block state. setCurrentBlockState(state); } } static bool intersect(const QList> &p_indices, int &p_start, int &p_end) { for (auto const & range : p_indices) { if (p_end <= range.first) { return false; } else if (p_start < range.second) { return true; } } return false; } void HGMarkdownHighlighter::highlightMathJax(const QTextBlock &p_block, const QString &p_text) { const int blockMarkLength = 2; const int inlineMarkLength = 1; VTextBlockData *blockData = currentBlockData(); Q_ASSERT(blockData); int blockNumber = p_block.blockNumber(); int startIdx = 0; // Next position to search. int pos = 0; HighlightBlockState state = (HighlightBlockState)previousBlockState(); QList> blockIdices; bool fromPreBlock = true; // Mathjax block formula. if (state != HighlightBlockState::MathjaxBlock) { fromPreBlock = false; startIdx = findMathjaxMarker(blockNumber, p_text, pos, m_mathjaxBlockExp, blockMarkLength); pos = startIdx + blockMarkLength; } while (startIdx >= 0) { int endIdx = findMathjaxMarker(blockNumber, p_text, pos, m_mathjaxBlockExp, blockMarkLength); int mathLength = 0; MathjaxInfo info; bool valid = false; if (endIdx == -1) { mathLength = p_text.length() - startIdx; pos = p_text.length(); if (isValidMathjaxRegion(blockNumber, startIdx, pos)) { valid = true; setCurrentBlockState(HighlightBlockState::MathjaxBlock); info.m_previewedAsBlock = false; info.m_index = startIdx, info.m_length = mathLength; if (fromPreBlock) { VTextBlockData *preBlockData = previousBlockData(); Q_ASSERT(preBlockData); const MathjaxInfo &preInfo = preBlockData->getPendingMathjax(); info.m_text = preInfo.text() + "\n" + p_text.mid(startIdx, mathLength); } else { info.m_text = p_text.mid(startIdx, mathLength); } blockData->setPendingMathjax(info); } } else { // Found end marker of a formula. pos = endIdx + blockMarkLength; mathLength = pos - startIdx; if (isValidMathjaxRegion(blockNumber, startIdx, pos)) { valid = true; info.m_previewedAsBlock = false; info.m_index = startIdx; info.m_length = mathLength; if (fromPreBlock) { // A cross-block formula. if (pos >= p_text.length()) { info.m_previewedAsBlock = true; } VTextBlockData *preBlockData = previousBlockData(); Q_ASSERT(preBlockData); const MathjaxInfo &preInfo = preBlockData->getPendingMathjax(); info.m_text = preInfo.text() + "\n" + p_text.mid(startIdx, mathLength); } else { // A formula within one block. if (pos >= p_text.length() && startIdx == 0) { info.m_previewedAsBlock = true; } info.m_text = p_text.mid(startIdx, mathLength); } blockData->addMathjax(info); } } fromPreBlock = false; if (valid) { blockIdices.append(QPair(startIdx, pos)); setFormat(startIdx, mathLength, m_mathjaxFormat); startIdx = findMathjaxMarker(blockNumber, p_text, pos, m_mathjaxBlockExp, blockMarkLength); pos = startIdx + blockMarkLength; } else { // Make the second mark as the first one and try again. if (endIdx == -1) { break; } startIdx = pos - blockMarkLength; } } // Mathjax inline formula. startIdx = 0; pos = 0; fromPreBlock = true; if (state != HighlightBlockState::MathjaxInline) { fromPreBlock = false; startIdx = findMathjaxMarker(blockNumber, p_text, pos, m_mathjaxInlineExp, inlineMarkLength); pos = startIdx + inlineMarkLength; } while (startIdx >= 0) { int endIdx = findMathjaxMarker(blockNumber, p_text, pos, m_mathjaxInlineExp, inlineMarkLength); int mathLength = 0; bool valid = false; if (endIdx == -1) { mathLength = p_text.length() - startIdx; pos = p_text.length(); } else { pos = endIdx + inlineMarkLength; mathLength = pos - startIdx; } valid = isValidMathjaxRegion(blockNumber, startIdx, pos); // Check if it intersect with blocks. if (valid && !intersect(blockIdices, startIdx, pos)) { // A valid inline mathjax. MathjaxInfo info; if (endIdx == -1) { setCurrentBlockState(HighlightBlockState::MathjaxInline); info.m_previewedAsBlock = false; info.m_index = startIdx, info.m_length = mathLength; if (fromPreBlock) { VTextBlockData *preBlockData = previousBlockData(); Q_ASSERT(preBlockData); const MathjaxInfo &preInfo = preBlockData->getPendingMathjax(); info.m_text = preInfo.text() + "\n" + p_text.mid(startIdx, mathLength); } else { info.m_text = p_text.mid(startIdx, mathLength); } blockData->setPendingMathjax(info); } else { info.m_previewedAsBlock = false; info.m_index = startIdx; info.m_length = mathLength; if (fromPreBlock) { // A cross-block formula. if (pos >= p_text.length()) { info.m_previewedAsBlock = true; } VTextBlockData *preBlockData = previousBlockData(); Q_ASSERT(preBlockData); const MathjaxInfo &preInfo = preBlockData->getPendingMathjax(); info.m_text = preInfo.text() + "\n" + p_text.mid(startIdx, mathLength); } else { // A formula within one block. if (pos >= p_text.length() && startIdx == 0) { info.m_previewedAsBlock = true; } info.m_text = p_text.mid(startIdx, mathLength); } blockData->addMathjax(info); } setFormat(startIdx, mathLength, m_mathjaxFormat); startIdx = m_mathjaxInlineExp.indexIn(p_text, pos); pos = startIdx + m_mathjaxInlineExp.matchedLength(); startIdx = pos - inlineMarkLength; } else { // Make the second mark as the first one and try again. if (endIdx == -1) { break; } startIdx = pos - inlineMarkLength; } fromPreBlock = false; } } void HGMarkdownHighlighter::highlightCodeBlockColorColumn(const QString &p_text) { int cc = g_config->getColorColumn(); if (cc <= 0 || currentBlockState() != HighlightBlockState::CodeBlock) { return; } VTextBlockData *blockData = currentBlockData(); Q_ASSERT(blockData); int indent = blockData->getCodeBlockIndentation(); if (indent == -1) { return; } cc += indent; if (p_text.size() < cc) { return; } setFormat(cc - 1, 1, m_colorColumnFormat); } void HGMarkdownHighlighter::parse() { if (!parsing.testAndSetRelaxed(0, 1)) { return; } if (highlightingStyles.isEmpty()) { goto exit; } { m_blockHLResultReady = false; int nrBlocks = document->blockCount(); parseInternal(); initBlockHighlightFromResult(nrBlocks); m_blockHLResultReady = true; initHtmlCommentRegionsFromResult(); initImageRegionsFromResult(); initHeaderRegionsFromResult(); initVerbatimBlocksFromResult(); initFencedCodeBlocksFromResult(); initInlineCodeRegionsFromResult(); initBoldItalicRegionsFromResult(); initLinkRegionsFromResult(); if (result) { pmh_free_elements(result); result = NULL; } } exit: parsing.store(0); } void HGMarkdownHighlighter::parseInternal() { QString text = document->toPlainText(); QByteArray ba = text.toUtf8(); const char *data = (const char *)ba.data(); int len = ba.size(); if (result) { pmh_free_elements(result); result = NULL; } if (len == 0) { return; } else if (len >= capacity) { resizeBuffer(qMax(2 * capacity, len * 2)); } else if (len < (capacity >> 2)) { resizeBuffer(qMax(capacity >> 1, len * 2)); } memcpy(content, data, len); content[len] = '\0'; int exts = pmh_EXT_STRIKE | pmh_EXT_FRONTMATTER; pmh_markdown_to_elements(content, exts, &result); } void HGMarkdownHighlighter::handleContentChange(int /* position */, int charsRemoved, int charsAdded) { if (charsRemoved == 0 && charsAdded == 0) { return; } m_signalOut = false; timer->stop(); timer->start(); } void HGMarkdownHighlighter::startParseAndHighlight(bool p_fast) { qDebug() << "HGMarkdownHighlighter start a new parse (fast" << p_fast << ")"; parse(); m_signalOut = !p_fast; if (!p_fast) { updateCodeBlocks(); } rehighlight(); } void HGMarkdownHighlighter::updateHighlight() { timer->stop(); startParseAndHighlight(false); } void HGMarkdownHighlighter::updateHighlightFast() { timer->stop(); startParseAndHighlight(true); } bool HGMarkdownHighlighter::updateCodeBlocks() { if (!g_config->getEnableCodeBlockHighlight()) { m_codeBlockHighlights.clear(); return false; } m_codeBlockHighlights.resize(document->blockCount()); for (int i = 0; i < m_codeBlockHighlights.size(); ++i) { m_codeBlockHighlights[i].clear(); } m_numOfCodeBlockHighlightsToRecv = m_codeBlocks.size(); emit codeBlocksUpdated(m_codeBlocks); return m_numOfCodeBlockHighlightsToRecv > 0; } static bool compHLUnitStyle(const HLUnitStyle &a, const HLUnitStyle &b) { if (a.start < b.start) { return true; } else if (a.start == b.start) { return a.length > b.length; } else { return false; } } void HGMarkdownHighlighter::setCodeBlockHighlights(const QVector &p_units) { if (p_units.isEmpty()) { goto exit; } { QVector> highlights(m_codeBlockHighlights.size()); for (auto const &unit : p_units) { int pos = unit.m_position; int end = unit.m_position + unit.m_length; int startBlockNum = document->findBlock(pos).blockNumber(); int endBlockNum = document->findBlock(end).blockNumber(); // Text has been changed. Abandon the obsolete parsed result. if (startBlockNum == -1 || endBlockNum >= highlights.size()) { goto exit; } for (int i = startBlockNum; i <= endBlockNum; ++i) { QTextBlock block = document->findBlockByNumber(i); int blockStartPos = block.position(); HLUnitStyle hl; hl.style = unit.m_style; if (i == startBlockNum) { hl.start = pos - blockStartPos; hl.length = (startBlockNum == endBlockNum) ? (end - pos) : (block.length() - hl.start); } else if (i == endBlockNum) { hl.start = 0; hl.length = end - blockStartPos; } else { hl.start = 0; hl.length = block.length(); } highlights[i].append(hl); } } // Need to highlight in order. for (int i = 0; i < highlights.size(); ++i) { QVector &units = highlights[i]; if (!units.isEmpty()) { if (units.size() > 1) { std::sort(units.begin(), units.end(), compHLUnitStyle); } m_codeBlockHighlights[i].append(units); } } } exit: --m_numOfCodeBlockHighlightsToRecv; if (m_numOfCodeBlockHighlightsToRecv <= 0) { rehighlight(); } } bool HGMarkdownHighlighter::isBlockInsideCommentRegion(const QTextBlock &p_block) const { if (!p_block.isValid()) { return false; } auto it = m_commentRegions.find(p_block.blockNumber()); if (it != m_commentRegions.end()) { const QVector ®s = it.value(); if (regs.size() == 1 && regs[0].m_startPos == 0 && regs[0].m_endPos == p_block.length()) { return true; } } return false; } void HGMarkdownHighlighter::highlightChanged() { m_completeTimer->stop(); m_completeTimer->start(); } bool HGMarkdownHighlighter::isValidHeader(const QString &p_text) { // There must exist spaces after #s. // No more than 6 #s. int nrNumberSign = 0; for (int i = 0; i < p_text.size(); ++i) { QChar ch = p_text[i]; if (ch.isSpace()) { return nrNumberSign > 0; } else if (ch == QChar('#')) { if (++nrNumberSign > 6) { return false; } } else { return false; } } return false; } void HGMarkdownHighlighter::highlightHeaderFast(int p_blockNumber, const QString &p_text) { auto it = m_headerBlocks.find(p_blockNumber); if (it != m_headerBlocks.end()) { const HeaderBlockInfo &info = it.value(); if (!isValidHeader(p_text)) { // Set an empty format to clear formats. It seems to work. setFormat(0, p_text.size(), QTextCharFormat()); } else { if (info.m_length < p_text.size()) { setFormat(info.m_length, p_text.size() - info.m_length, m_headerStyles[info.m_level]); } setCurrentBlockState(HighlightBlockState::Header); } } } void HGMarkdownHighlighter::updateMathjaxBlocks() { if (!m_enableMathjax) { return; } QVector blocks; QTextBlock bl = document->firstBlock(); while (bl.isValid()) { VTextBlockData *data = static_cast(bl.userData()); if (!data) { bl = bl.next(); continue; } const QVector &info = data->getMathjax(); for (auto const & it : info) { blocks.append(VMathjaxBlock(bl.blockNumber(), it)); } bl = bl.next(); } emit mathjaxBlocksUpdated(blocks); } void HGMarkdownHighlighter::initInlineCodeRegionsFromResult() { m_inlineCodeRegions.clear(); if (!result) { return; } pmh_element *elem = result[pmh_CODE]; while (elem != NULL) { if (elem->end <= elem->pos) { elem = elem->next; continue; } initBlockElementRegionOne(m_inlineCodeRegions, elem->pos, elem->end); elem = elem->next; } } void HGMarkdownHighlighter::initBoldItalicRegionsFromResult() { m_boldItalicRegions.clear(); if (!result) { return; } pmh_element_type types[2] = {pmh_EMPH, pmh_STRONG}; for (int i = 0; i < 2; ++i) { pmh_element *elem = result[types[i]]; while (elem != NULL) { if (elem->end <= elem->pos) { elem = elem->next; continue; } initBlockElementRegionOne(m_boldItalicRegions, elem->pos, elem->end); elem = elem->next; } } } void HGMarkdownHighlighter::initLinkRegionsFromResult() { m_linkRegions.clear(); if (!result) { return; } pmh_element_type types[2] = {pmh_LINK, pmh_IMAGE}; for (int i = 0; i < 2; ++i) { pmh_element *elem = result[types[i]]; while (elem != NULL) { if (elem->end <= elem->pos) { elem = elem->next; continue; } initBlockElementRegionOne(m_linkRegions, elem->pos, elem->end); elem = elem->next; } } } void HGMarkdownHighlighter::initBlockElementRegionOne(QHash> &p_regs, unsigned long p_pos, unsigned long p_end) { // When the the highlight element is at the end of document, @p_end will equals // to the characterCount. unsigned int nrChar = (unsigned int)document->characterCount(); if (p_end >= nrChar && nrChar > 0) { p_end = nrChar - 1; } int startBlockNum = document->findBlock(p_pos).blockNumber(); int endBlockNum = document->findBlock(p_end).blockNumber(); for (int i = startBlockNum; i <= endBlockNum; ++i) { QTextBlock block = document->findBlockByNumber(i); int blockStartPos = block.position(); QVector ®s = p_regs[i]; int start, end; if (i == startBlockNum) { start = p_pos - blockStartPos; end = (startBlockNum == endBlockNum) ? (p_end - blockStartPos) : block.length(); } else if (i == endBlockNum) { start = 0; end = p_end - blockStartPos; } else { start = 0; end = block.length(); } regs.append(VElementRegion(start, end)); } } static bool indexInsideRegion(const QVector &p_regions, int p_idx) { for (auto const & reg : p_regions) { if (reg.contains(p_idx)) { return true; } } return false; } int HGMarkdownHighlighter::findMathjaxMarker(int p_blockNumber, const QString &p_text, int p_pos, QRegExp &p_reg, int p_markerLength) { if (p_pos >= p_text.size()) { return -1; } int idx = -1; auto inlineCodeIt = m_inlineCodeRegions.find(p_blockNumber); auto commentIt = m_commentRegions.find(p_blockNumber); auto boldItalicIt = m_boldItalicRegions.find(p_blockNumber); auto linkIt = m_linkRegions.find(p_blockNumber); while (p_pos < p_text.size()) { idx = p_reg.indexIn(p_text, p_pos); if (idx == -1) { return idx; } p_pos = idx + p_reg.matchedLength(); idx = p_pos - p_markerLength; // Check if this idx is legal. // Check inline code. if (inlineCodeIt != m_inlineCodeRegions.end()) { if (indexInsideRegion(inlineCodeIt.value(), idx)) { idx = -1; continue; } } if (commentIt != m_commentRegions.end()) { if (indexInsideRegion(commentIt.value(), idx)) { idx = -1; continue; } } if (boldItalicIt != m_boldItalicRegions.end()) { if (indexInsideRegion(boldItalicIt.value(), idx)) { idx = -1; continue; } } if (linkIt != m_linkRegions.end()) { if (indexInsideRegion(linkIt.value(), idx)) { idx = -1; continue; } } break; } return idx; } static bool intersectRegion(const QVector &p_regions, int p_start, int p_end) { for (auto const & reg : p_regions) { if (reg.intersect(p_start, p_end)) { return true; } } return false; } bool HGMarkdownHighlighter::isValidMathjaxRegion(int p_blockNumber, int p_start, int p_end) { auto inlineCodeIt = m_inlineCodeRegions.find(p_blockNumber); if (inlineCodeIt != m_inlineCodeRegions.end()) { if (intersectRegion(inlineCodeIt.value(), p_start, p_end)) { return false; } } auto commentIt = m_commentRegions.find(p_blockNumber); if (commentIt != m_commentRegions.end()) { if (intersectRegion(commentIt.value(), p_start, p_end)) { return false; } } auto boldItalicIt = m_boldItalicRegions.find(p_blockNumber); if (boldItalicIt != m_boldItalicRegions.end()) { if (intersectRegion(boldItalicIt.value(), p_start, p_end)) { return false; } } auto linkIt = m_linkRegions.find(p_blockNumber); if (linkIt != m_linkRegions.end()) { if (intersectRegion(linkIt.value(), p_start, p_end)) { return false; } } return true; } void HGMarkdownHighlighter::completeHighlight() { if (m_signalOut) { m_signalOut = false; updateMathjaxBlocks(); emit imageLinksUpdated(m_imageRegions); emit headersUpdated(m_headerRegions); } emit highlightCompleted(); }