#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), 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) { const pmh_element_type &eleType = styles[index].type; if (eleType == pmh_VERBATIM) { m_codeBlockFormat = styles[index].format; } else if (eleType == pmh_LINK) { m_linkFormat = styles[index].format; } else if (eleType == pmh_IMAGE) { m_imageFormat = styles[index].format; } } 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); }); static const int completeWaitTime = 500; m_completeTimer = new QTimer(this); m_completeTimer->setSingleShot(true); m_completeTimer->setInterval(completeWaitTime); connect(m_completeTimer, &QTimer::timeout, this, &HGMarkdownHighlighter::highlightCompleted); 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); // If it is a block inside HTML comment, just skip it. if (isBlockInsideCommentRegion(curBlock)) { setCurrentBlockState(HighlightBlockState::Comment); goto exit; } // PEG Markdown Highlight does not handle the ``` code block correctly. setCurrentBlockState(HighlightBlockState::Normal); highlightCodeBlock(curBlock, text); if (currentBlockState() == HighlightBlockState::Normal) { if (isVerbatimBlock(curBlock)) { setCurrentBlockState(HighlightBlockState::Verbatim); goto exit; } else if (m_enableMathjax) { highlightMathJax(curBlock, text); } } // PEG Markdown Highlight does not handle links with spaces in the URL. // Links in the URL should be encoded to %20. We just let it be here and won't // fix this. // highlightLinkWithSpacesInURL(text); highlightHeaderFast(blockNum, 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]; // pmh_H1 to pmh_H6 is continuous. bool isHeader = style.type >= pmh_H1 && style.type <= pmh_H6; 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; } // Check header. Skip those headers with no spaces after #s. if (isHeader && !isValidHeader(elem_cursor->pos, elem_cursor->end)) { 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() { // From Qt5.7, the capacity is preserved. m_commentRegions.clear(); if (!result) { return; } pmh_element *elem = result[pmh_COMMENT]; while (elem != NULL) { if (elem->end <= elem->pos) { elem = elem->next; continue; } m_commentRegions.push_back(VElementRegion(elem->pos, elem->end)); elem = elem->next; } qDebug() << "highlighter: parse" << m_commentRegions.size() << "HTML comment regions"; } void HGMarkdownHighlighter::initImageRegionsFromResult() { // From Qt5.7, the capacity is preserved. m_imageRegions.clear(); if (!result) { emit imageLinksUpdated(m_imageRegions); 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; } qDebug() << "highlighter: parse" << m_imageRegions.size() << "image regions"; emit imageLinksUpdated(m_imageRegions); } 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::initHeaderRegionsFromResult() { // From Qt5.7, the capacity is preserved. m_headerBlocks.clear(); m_headerRegions.clear(); if (!result) { emit headersUpdated(m_headerRegions); 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 || !isValidHeader(elem->pos, elem->end)) { 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()); qDebug() << "highlighter: parse" << m_headerRegions.size() << "header regions"; emit headersUpdated(m_headerRegions); } 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(const QTextBlock &p_block, const QString &p_text) { VTextBlockData *blockData = currentBlockData(); Q_ASSERT(blockData); int length = 0; int index = -1; int preState = previousBlockState(); int state = HighlightBlockState::Normal; if (preState != HighlightBlockState::CodeBlock && preState != HighlightBlockState::CodeBlockStart) { // Need to find a new code block start. index = codeBlockStartExp.indexIn(p_text); if (index >= 0 && !isVerbatimBlock(p_block)) { // Start a new code block. length = p_text.length(); state = HighlightBlockState::CodeBlockStart; // The leading spaces of code block start and end must be identical. int startLeadingSpaces = codeBlockStartExp.capturedTexts()[1].size(); blockData->setCodeBlockIndentation(startLeadingSpaces); } else { // A normal block. blockData->setCodeBlockIndentation(-1); return; } } else { // Need to find a code block end. int startLeadingSpaces = 0; VTextBlockData *preBlockData = previousBlockData(); if (preBlockData) { startLeadingSpaces = preBlockData->getCodeBlockIndentation(); } index = codeBlockEndExp.indexIn(p_text); // The closing ``` should have the same indentation as the open ```. if (index >= 0 && startLeadingSpaces == codeBlockEndExp.capturedTexts()[1].size()) { // End of code block. length = p_text.length(); state = HighlightBlockState::CodeBlockEnd; } else { // Within code block. index = 0; length = p_text.length(); state = HighlightBlockState::CodeBlock; } blockData->setCodeBlockIndentation(startLeadingSpaces); } setCurrentBlockState(state); setFormat(index, length, m_codeBlockFormat); } 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; int startIdx = 0; // Next position to search. int pos = 0; HighlightBlockState state = (HighlightBlockState)previousBlockState(); QList> blockIdices; // Mathjax block formula. if (state != HighlightBlockState::MathjaxBlock) { startIdx = m_mathjaxBlockExp.indexIn(p_text); pos = startIdx + m_mathjaxBlockExp.matchedLength(); startIdx = pos - blockMarkLength; } while (startIdx >= 0) { int endIdx = m_mathjaxBlockExp.indexIn(p_text, pos); int mathLength = 0; if (endIdx == -1) { setCurrentBlockState(HighlightBlockState::MathjaxBlock); mathLength = p_text.length() - startIdx; } else { mathLength = endIdx - startIdx + m_mathjaxBlockExp.matchedLength(); } pos = startIdx + mathLength; blockIdices.append(QPair(startIdx, pos)); setFormat(startIdx, mathLength, m_mathjaxFormat); startIdx = m_mathjaxBlockExp.indexIn(p_text, pos); pos = startIdx + m_mathjaxBlockExp.matchedLength(); startIdx = pos - blockMarkLength; } // Mathjax inline formula. startIdx = 0; pos = 0; if (state != HighlightBlockState::MathjaxInline) { startIdx = m_mathjaxInlineExp.indexIn(p_text); pos = startIdx + m_mathjaxInlineExp.matchedLength(); startIdx = pos - inlineMarkLength; } while (startIdx >= 0) { int endIdx = m_mathjaxInlineExp.indexIn(p_text, pos); int mathLength = 0; if (endIdx == -1) { setCurrentBlockState(HighlightBlockState::MathjaxBlock); mathLength = p_text.length() - startIdx; } else { mathLength = endIdx - startIdx + m_mathjaxInlineExp.matchedLength(); } pos = startIdx + mathLength; // Check if it intersect with blocks. if (!intersect(blockIdices, startIdx, pos)) { // A valid inline mathjax. if (endIdx == -1) { setCurrentBlockState(HighlightBlockState::MathjaxInline); } 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; } } } 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::highlightLinkWithSpacesInURL(const QString &p_text) { if (currentBlockState() == HighlightBlockState::CodeBlock) { return; } // TODO: should select links with spaces in URL. QRegExp regExp("[\\!]?\\[[^\\]]*\\]\\(([^\\n\\)]+)\\)"); int index = regExp.indexIn(p_text); while (index >= 0) { Q_ASSERT(regExp.captureCount() == 1); int length = regExp.matchedLength(); QString capturedText = regExp.capturedTexts()[1]; if (capturedText.contains(' ')) { if (p_text[index] == '!' && m_imageFormat.isValid()) { setFormat(index, length, m_imageFormat); } else if (m_linkFormat.isValid()) { setFormat(index, length, m_linkFormat); } } index = regExp.indexIn(p_text, index + length); } } void HGMarkdownHighlighter::parse(bool p_fast) { if (!parsing.testAndSetRelaxed(0, 1)) { return; } if (highlightingStyles.isEmpty()) { goto exit; } { m_blockHLResultReady = false; int nrBlocks = document->blockCount(); parseInternal(); initBlockHighlightFromResult(nrBlocks); m_blockHLResultReady = true; if (!p_fast) { initHtmlCommentRegionsFromResult(); initImageRegionsFromResult(); initHeaderRegionsFromResult(); initVerbatimBlocksFromResult(); } 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'; pmh_markdown_to_elements(content, pmh_EXT_NONE, &result); } void HGMarkdownHighlighter::handleContentChange(int /* position */, int charsRemoved, int charsAdded) { if (charsRemoved == 0 && charsAdded == 0) { return; } timer->stop(); timer->start(); } void HGMarkdownHighlighter::startParseAndHighlight(bool p_fast) { qDebug() << "HGMarkdownHighlighter start a new parse (fast" << p_fast << ")"; parse(p_fast); if (p_fast) { rehighlight(); } else { if (!updateCodeBlocks()) { rehighlight(); } highlightChanged(); } } 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(); } QVector codeBlocks; VCodeBlock item; bool inBlock = false; int startLeadingSpaces = -1; // Only handle complete codeblocks. QTextBlock block = document->firstBlock(); while (block.isValid()) { if (!inBlock && isVerbatimBlock(block)) { block = block.next(); continue; } QString text = block.text(); if (inBlock) { item.m_text = item.m_text + "\n" + text; int idx = codeBlockEndExp.indexIn(text); if (idx >= 0 && codeBlockEndExp.capturedTexts()[1].size() == startLeadingSpaces) { // End block. inBlock = false; item.m_endBlock = block.blockNumber(); // See if it is a code block inside HTML comment. if (!isBlockInsideCommentRegion(block)) { qDebug() << "add one code block in lang" << item.m_lang; codeBlocks.append(item); } } } else { int idx = codeBlockStartExp.indexIn(text); if (idx >= 0) { // Start block. inBlock = true; item.m_startBlock = block.blockNumber(); item.m_startPos = block.position(); item.m_text = text; if (codeBlockStartExp.captureCount() == 2) { item.m_lang = codeBlockStartExp.capturedTexts()[2]; } startLeadingSpaces = codeBlockStartExp.capturedTexts()[1].size(); } } block = block.next(); } m_numOfCodeBlockHighlightsToRecv = codeBlocks.size(); if (m_numOfCodeBlockHighlightsToRecv > 0) { emit codeBlocksUpdated(codeBlocks); return true; } else { return false; } } 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; } int start = p_block.position(); int end = start + p_block.length(); for (auto const & reg : m_commentRegions) { if (reg.contains(start) && reg.contains(end)) { return true; } } return false; } void HGMarkdownHighlighter::highlightChanged() { m_completeTimer->stop(); m_completeTimer->start(); } bool HGMarkdownHighlighter::isValidHeader(unsigned long p_pos, unsigned long p_end) { // There must exist spaces after #s. // No more than 6 #s. int nrNumberSign = 0; for (unsigned long i = p_pos; i < p_end; ++i) { QChar ch = document->characterAt(i); if (ch.isSpace()) { return nrNumberSign > 0; } else if (ch == QChar('#')) { if (++nrNumberSign > 6) { return false; } } else { return false; } } return false; } 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) { if (currentBlockState() != HighlightBlockState::Normal) { return; } 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]); } } }