Dekko
MarkdownDocument.cpp
1 #include "MarkdownDocument.h"
2 #include <QDebug>
3 #include <QTextFrame>
4 
5 MarkdownDocument::MarkdownDocument(QQuickItem *parent) : QQuickItem(parent),
6  m_options(Q_NULLPTR),
7  m_textDocument(Q_NULLPTR),
8  m_enabled(false),
9  m_hasSelection(false),
10  m_selectionStart(0),
11  m_selectionEnd(0),
12  m_highlighter(Q_NULLPTR),
13  m_cursorPosition(0)
14 {
15  setEnabled(true);
16  setVisible(true);
17  connect(this, &MarkdownDocument::textDocumentChanged, this, &MarkdownDocument::onDocumentChanged);
18  connect(this, &MarkdownDocument::optionsChanged, this, &MarkdownDocument::onDocumentChanged);
19  m_blockQuote.setPattern("^ {0,3}(>\\s*)+");
20  m_numList.setPattern("^\\s*([0-9]+)[.)]\\s+");
21  m_bulletList.setPattern("^\\s*[+*-]\\s+");
22  m_taskList.setPattern("^\\s*[-] \\[([x ])\\]\\s+");
23 
24  m_pairs.insert('"', '"');
25  m_pairs.insert('\'', '\'');
26  m_pairs.insert('(', ')');
27  m_pairs.insert('[', ']');
28  m_pairs.insert('{', '}');
29  m_pairs.insert('*', '*');
30  m_pairs.insert('_', '_');
31  m_pairs.insert('`', '`');
32  m_pairs.insert('<', '>');
33 
34  m_matches.insert('"', true);
35  m_matches.insert('\'', true);
36  m_matches.insert('(', true);
37  m_matches.insert('[', true);
38  m_matches.insert('{', true);
39  m_matches.insert('*', true);
40  m_matches.insert('_', true);
41  m_matches.insert('`', true);
42  m_matches.insert('<', true);
43 }
44 
45 int MarkdownDocument::cursorPosition() const
46 {
47  return m_cursor.position();
48 }
49 
50 void MarkdownDocument::indentText()
51 {
52 
53  const int tabWidth = m_options->get_tabWidth();
54  if (m_hasSelection) {
55  QTextBlock start = document()->findBlock(m_selectionStart);
56  QTextBlock end = document()->findBlock(m_selectionEnd).next();
57  m_cursor.beginEditBlock();
58 
59  while (start != end) {
60  setCursorPosition(start.position());
61  if (m_options->get_spacesForTabs()) {
62  QString indentText = "";
63  for (int i = 0; i < tabWidth; ++i) {
64  indentText += QStringLiteral(" ");
65  }
66  m_cursor.insertText(indentText);
67  } else {
68  m_cursor.insertText("\t");
69  }
70  start = start.next();
71  }
72  m_cursor.endEditBlock();
73  } else {
74  int indent = tabWidth;
75  QString indentText = "";
76  m_cursor.beginEditBlock();
77 
78  switch (m_cursor.block().userState()) {
79  // TODO:
80  default:
81  indent = tabWidth - (m_cursor.positionInBlock() % tabWidth);
82  break;
83  }
84 
85  if (m_options->get_spacesForTabs()) {
86  for (int i = 0; i < indent; ++i) {
87  indentText += QStringLiteral(" ");
88  }
89  } else {
90  indentText = QStringLiteral("\t");
91  }
92  m_cursor.insertText(indentText);
93  m_cursor.endEditBlock();
94  }
95  setCursorPosition(m_cursor.position());
96 }
97 
98 void MarkdownDocument::unindentText()
99 {
100  QTextBlock start;
101  QTextBlock end;
102 
103  if (m_hasSelection) {
104  start = document()->findBlock(m_selectionStart);
105  end = document()->findBlock(m_selectionEnd);
106  } else {
107  start = m_cursor.block();
108  end = start.next();
109  }
110 
111  m_cursor.beginEditBlock();
112 
113  while (start != end) {
114  setCursorPosition(start.position());
115  if (document()->characterAt(m_cursor.position()) == QChar('\t')) {
116  m_cursor.deleteChar();
117  } else {
118  int p = 0;
119  while (document()->characterAt(m_cursor.position()) == QChar(' ') && p < m_options->get_tabWidth()) {
120  ++p;
121  m_cursor.deleteChar();
122  }
123  }
124  start = start.next();
125  }
126  m_cursor.endEditBlock();
127  setCursorPosition(m_cursor.position());
128 }
129 
130 void MarkdownDocument::setCursorPosition(int cursorPosition)
131 {
132  if (m_cursorPosition == cursorPosition)
133  return;
134 
135  m_cursorPosition = cursorPosition;
136  m_cursor.setPosition(cursorPosition);
137  emit cursorPositionChanged(cursorPosition);
138 }
139 
140 QTextDocument *MarkdownDocument::document() const
141 {
142  if (m_textDocument != Q_NULLPTR) {
143  return m_textDocument->textDocument();
144  }
145  return Q_NULLPTR;
146 }
147 
148 void MarkdownDocument::onDocumentChanged()
149 {
150  if (document() != Q_NULLPTR && m_options != Q_NULLPTR) {
151  connect(document(), &QTextDocument::contentsChange, this, &MarkdownDocument::onContentsChange);
152  m_cursor = textCursor();
153  m_highlighter = new MarkdownHighlighter(document(), m_options);
154  }
155 }
156 
157 void MarkdownDocument::onContentsChange(const int &pos, const int &rm, const int &add)
158 {
159  Q_UNUSED(pos);
160  Q_UNUSED(rm);
161  Q_UNUSED(add);
162  // qDebug() << "CONTENTS CHANGED";
163 }
164 
165 QTextCursor MarkdownDocument::textCursor()
166 {
167  return document()->rootFrame()->firstCursorPosition();
168 }
169 
170 bool MarkdownDocument::insertPair(const QChar &c)
171 {
172  if (m_pairs.contains(c)) {
173  QChar p = m_pairs.value(c);
174  QTextBlock block;
175  QTextBlock end;
176  if (m_hasSelection) {
177  block = document()->findBlock(m_selectionStart);
178  end = document()->findBlock(m_selectionEnd);
179  if (block == end) {
180  auto s = m_selectionStart;
181  auto e = m_selectionEnd;
182  m_cursor.beginEditBlock();
183  setCursorPosition(s);
184  m_cursor.insertText(c);
185  setCursorPosition(e + 1);
186  m_cursor.insertText(p);
187  setCursorPosition(s);
188  setCursorPosition(e);
189  m_cursor.endEditBlock();
190  return true;
191  }
192  } else if (m_options->get_autoMatchEnabled() && m_matches.value(c)) {
193  m_cursor.insertText(c);
194  m_cursor.insertText(p);
195  m_cursor.movePosition(QTextCursor::PreviousCharacter);
196  setCursorPosition(m_cursor.position());
197  return true;
198  }
199  }
200  return false;
201 }
202 
203 bool MarkdownDocument::endPairHandled(const QChar &c)
204 {
205  bool lookAhead = false;
206  if (m_options->get_autoMatchEnabled() && !m_cursor.hasSelection()) {
207  if (m_pairs.values().contains(c)) {
208  auto key = m_pairs.key(c);
209  if (m_matches.value(key)) {
210  lookAhead = true;
211  }
212  }
213  }
214 
215  if (lookAhead) {
216  auto text = m_cursor.block().text();
217  auto pos = m_cursor.positionInBlock();
218  if (pos < text.length()) {
219  if (text[pos] == c) {
220  m_cursor.movePosition(QTextCursor::NextCharacter);
221  return true;
222  }
223  }
224  }
225  return false;
226 }
227 
228 void MarkdownDocument::keyPressEvent(QKeyEvent *event)
229 {
230  // qDebug() << "KEY PRESS EVENT: " << event;
231  int key = event->key();
232  bool accepted = false;
233  switch (key) {
234  case Qt::Key_Return:
235  if (!m_hasSelection) {
236  if (event->modifiers() & Qt::ShiftModifier) {
237  m_cursor.insertText(" ");
238  }
239 
240  if (event->modifiers() & Qt::ControlModifier) {
241  m_cursor.insertText("\n");
242  } else {
243  handleCRLF();
244  }
245  accepted = true;
246  }
247  break;
248  case Qt::Key_Tab:
249  indentText();
250  accepted = true;
251  break;
252  case Qt::Key_Backtab:
253  unindentText();
254  accepted = true;
255  break;
256  case Qt::Key_Backspace:
257  accepted = handleBackspace();
258  break;
259  default:
260  if (event->text().size() == 1) {
261  auto c = event->text().at(0);
262  if (!endPairHandled(c) && !insertPair(c)) {
263  accepted = false;
264  } else {
265  accepted = true;
266  }
267  }
268  break;
269  }
270  event->setAccepted(accepted);
271 }
272 
273 void MarkdownDocument::handleCRLF()
274 {
275  QString autoInsertText = "";
276  bool endList = false;
277  if (m_cursor.positionInBlock() < (m_cursor.block().length() - 1)) {
278  autoInsertText = getPreviousIndentation();
279  if (m_cursor.positionInBlock() < autoInsertText.length()) {
280  autoInsertText.truncate(m_cursor.positionInBlock());
281  }
282  } else {
283  switch (m_cursor.block().userState()) {
284  case MarkdownTokenizer::TokenState::NumList:
285  {
286  autoInsertText = getBlockStart(m_numList);
287  QStringList c = m_numList.capturedTexts();
288  if (!autoInsertText.isEmpty() && c.size() == 2) {
289  if (m_cursor.block().text().length() == autoInsertText.length()) {
290  endList = true;
291  } else {
292  QRegExp num("\\d+");
293  int liNum = c.at(1).toInt();
294  liNum++;
295  autoInsertText = autoInsertText.replace(num, QString("%1").arg(liNum));
296  }
297  } else {
298  autoInsertText = getPreviousIndentation();
299  }
300  break;
301  }
302  case MarkdownTokenizer::TokenState::BulletList:
303  {
304  autoInsertText = getBlockStart(m_taskList);
305  if (autoInsertText.isEmpty()) {
306  autoInsertText = getBlockStart(m_bulletList);
307  if (autoInsertText.isEmpty()) {
308  autoInsertText = getPreviousIndentation();
309  } else if (m_cursor.block().text().length() == autoInsertText.length()) {
310  endList = true;
311  }
312  } else {
313  if (m_cursor.block().text().length() == autoInsertText.length()) {
314  endList = true;
315  } else {
316  autoInsertText = autoInsertText.replace('x', ' ');
317  }
318  }
319  break;
320  }
321  case MarkdownTokenizer::TokenState::Blockquote:
322  {
323  autoInsertText = getBlockStart(m_blockQuote);
324  break;
325  }
326  default:
327  autoInsertText = getPreviousIndentation();
328  break;
329  }
330  }
331 
332  if (endList)
333  {
334  autoInsertText = getPreviousIndentation();
335  m_cursor.movePosition(QTextCursor::StartOfBlock);
336  m_cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
337  setCursorPosition(m_cursor.position());
338  m_cursor.insertText(autoInsertText);
339  autoInsertText = "";
340  }
341 
342  m_cursor.insertText(QStringLiteral("\n") + autoInsertText);
343 }
344 
345 bool MarkdownDocument::handleBackspace()
346 {
347  if (m_hasSelection) {
348  return false;
349  }
350 
351  int backtrackIndex = -1;
352 
353  switch (m_cursor.block().userState())
354  {
355  case MarkdownTokenizer::TokenState::NumList:
356  {
357  if (m_numList.exactMatch(m_cursor.block().text()))
358  {
359  backtrackIndex = m_cursor.block().text().indexOf(QRegExp("\\d"));
360  }
361  break;
362  }
363  case MarkdownTokenizer::TokenState::BulletList:
364  {
365  if( m_bulletList.exactMatch(m_cursor.block().text())
366  || m_taskList.exactMatch(m_cursor.block().text())) {
367 
368  backtrackIndex = m_cursor.block().text().indexOf(QRegExp("[+*-]"));
369  }
370  break;
371  }
372  case MarkdownTokenizer::TokenState::Blockquote:
373  {
374  if (m_blockQuote.exactMatch(m_cursor.block().text()))
375  {
376  backtrackIndex = m_cursor.block().text().lastIndexOf('>');
377  }
378  break;
379  }
380  default:
381  // If the first character in an automatched set is being
382  // deleted, then delete the second matching one along with it.
383  //
384  if (m_options->get_autoMatchEnabled() && (m_cursor.positionInBlock() > 0))
385  {
386  QString blockText = m_cursor.block().text();
387 
388  if (m_cursor.positionInBlock() < blockText.length())
389  {
390  QChar currentChar = blockText[m_cursor.positionInBlock()];
391  QChar previousChar = blockText[m_cursor.positionInBlock() - 1];
392 
393  if (m_pairs.value(previousChar) == currentChar)
394  {
395  m_cursor.movePosition(QTextCursor::Left);
396  m_cursor.movePosition
397  (
398  QTextCursor::Right,
399  QTextCursor::KeepAnchor,
400  2
401  );
402  setCursorPosition(m_cursor.position());
403  m_cursor.removeSelectedText();
404  return true;
405  }
406  }
407  }
408  break;
409  }
410 
411  if (backtrackIndex >= 0)
412  {
413  m_cursor.movePosition(QTextCursor::StartOfBlock);
414  m_cursor.movePosition
415  (
416  QTextCursor::Right,
417  QTextCursor::MoveAnchor,
418  backtrackIndex
419  );
420 
421  m_cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
422  setCursorPosition(m_cursor.position());
423  m_cursor.removeSelectedText();
424  return true;
425  }
426 
427  return false;
428 }
429 
430 QString MarkdownDocument::getBlockStart(QRegExp &regexp)
431 {
432  QTextBlock block = m_cursor.block();
433 
434  QString text = block.text();
435 
436  if (regexp.indexIn(text, 0) >= 0)
437  {
438  return text.left(regexp.matchedLength());
439  }
440 
441  return QString("");
442 }
443 
444 QString MarkdownDocument::getPreviousIndentation()
445 {
446  QString indent = "";
447  QTextBlock block = m_cursor.block();
448 
449  QString text = block.text();
450 
451  for (int i = 0; i < text.length(); i++)
452  {
453  if (text[i].isSpace())
454  {
455  indent += text[i];
456  }
457  else
458  {
459  return indent;
460  }
461  }
462 
463  return indent;
464 }
MarkdownHighlighter
Definition: MarkdownHighlighter.h:13