Skip to content

Commit 88bac52

Browse files
committed
#672 note-edit: add heading depth markdown operations
1 parent 82892e2 commit 88bac52

3 files changed

Lines changed: 185 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## 26.4.21
44

5+
- Added a new **Markdown operations** submenu to the **note text edit** context
6+
menu with actions to increase or decrease the heading depth of selected ATX
7+
and setext Markdown headings (for [#672](https://github.com/pbek/QOwnNotes/issues/672))
58
- The **note link dialog** note list now also shows the `Modified` column even
69
when note sub-folder support is disabled (for [#1679](https://github.com/pbek/QOwnNotes/issues/1679))
710
- Fixed auto-detected links inside italic or bold Markdown text so trailing

src/widgets/qownnotesmarkdowntextedit.cpp

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include <QVariant>
4646
#include <QVector>
4747
#include <QWidgetAction>
48+
#include <QtGlobal>
4849
#include <algorithm>
4950

5051
#include "entities/notefolder.h"
@@ -71,6 +72,164 @@ constexpr int kMarkdownLspDiagnosticProperty = QTextFormat::UserProperty + 1;
7172
constexpr int kFoldIndicatorPadding = 4;
7273
QHash<QString, QSet<QString>> s_foldedHeadingStateByNoteReference;
7374

75+
struct SetextHeadingUnderline {
76+
int level = 0;
77+
int leadingSpaces = 0;
78+
int markerCount = 0;
79+
80+
bool isValid() const { return level > 0; }
81+
};
82+
83+
static int markdownHeadingIndent(const QString &line) {
84+
int i = 0;
85+
while (i < line.size() && i < 3 && line.at(i) == QLatin1Char(' ')) {
86+
++i;
87+
}
88+
89+
return i;
90+
}
91+
92+
static int atxHeadingLevel(const QString &line, int *headingMarkerStart = nullptr,
93+
int *headingMarkerEnd = nullptr) {
94+
int i = markdownHeadingIndent(line);
95+
const int markerStart = i;
96+
while (i < line.size() && line.at(i) == QLatin1Char('#')) {
97+
++i;
98+
}
99+
100+
const int headingLevel = i - markerStart;
101+
if (headingLevel <= 0 || headingLevel > 6) {
102+
return 0;
103+
}
104+
105+
if (i < line.size() && line.at(i) != QLatin1Char(' ') && line.at(i) != QLatin1Char('\t')) {
106+
return 0;
107+
}
108+
109+
if (headingMarkerStart) {
110+
*headingMarkerStart = markerStart;
111+
}
112+
113+
if (headingMarkerEnd) {
114+
*headingMarkerEnd = i;
115+
}
116+
117+
return headingLevel;
118+
}
119+
120+
static SetextHeadingUnderline parseSetextHeadingUnderline(const QString &line) {
121+
SetextHeadingUnderline underline;
122+
123+
int i = markdownHeadingIndent(line);
124+
underline.leadingSpaces = i;
125+
126+
if (i >= line.size()) {
127+
return underline;
128+
}
129+
130+
const QChar marker = line.at(i);
131+
if (marker != QLatin1Char('=') && marker != QLatin1Char('-')) {
132+
return underline;
133+
}
134+
135+
const int markerStart = i;
136+
while (i < line.size() && line.at(i) == marker) {
137+
++i;
138+
}
139+
140+
underline.markerCount = i - markerStart;
141+
142+
while (i < line.size() && (line.at(i) == QLatin1Char(' ') || line.at(i) == QLatin1Char('\t'))) {
143+
++i;
144+
}
145+
146+
if (i != line.size()) {
147+
return {};
148+
}
149+
150+
underline.level = (marker == QLatin1Char('=')) ? 1 : 2;
151+
return underline;
152+
}
153+
154+
static int boundedHeadingLevel(const int headingLevel, const int levelDelta) {
155+
#if __cplusplus >= 201703L
156+
return std::clamp(headingLevel + levelDelta, 1, 6);
157+
#else
158+
return qBound(1, headingLevel + levelDelta, 6);
159+
#endif
160+
}
161+
162+
QString changeHeadingDepth(const QString &text, const int levelDelta) {
163+
QString normalizedText = text;
164+
normalizedText.replace(QChar(0x2029), QLatin1Char('\n'));
165+
166+
QStringList lines = normalizedText.split(QLatin1Char('\n'),
167+
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
168+
QString::KeepEmptyParts
169+
#else
170+
Qt::KeepEmptyParts
171+
#endif
172+
);
173+
QStringList updatedLines;
174+
updatedLines.reserve(lines.size());
175+
176+
for (int lineIndex = 0; lineIndex < lines.size(); ++lineIndex) {
177+
const QString &line = lines.at(lineIndex);
178+
179+
int headingMarkerStart = 0;
180+
int headingMarkerEnd = 0;
181+
const int atxLevel = atxHeadingLevel(line, &headingMarkerStart, &headingMarkerEnd);
182+
if (atxLevel > 0) {
183+
const int newHeadingLevel = boundedHeadingLevel(atxLevel, levelDelta);
184+
if (newHeadingLevel == atxLevel) {
185+
updatedLines.append(line);
186+
continue;
187+
}
188+
189+
updatedLines.append(line.left(headingMarkerStart) +
190+
QString(newHeadingLevel, QLatin1Char('#')) +
191+
line.mid(headingMarkerEnd));
192+
continue;
193+
}
194+
195+
if (lineIndex + 1 < lines.size()) {
196+
const SetextHeadingUnderline underline =
197+
parseSetextHeadingUnderline(lines.at(lineIndex + 1));
198+
const int titleIndent = markdownHeadingIndent(line);
199+
const QString titleText = line.mid(titleIndent).trimmed();
200+
201+
if (underline.isValid() && !titleText.isEmpty()) {
202+
const int newHeadingLevel = boundedHeadingLevel(underline.level, levelDelta);
203+
if (newHeadingLevel <= 2) {
204+
updatedLines.append(line);
205+
206+
if (newHeadingLevel == underline.level) {
207+
updatedLines.append(lines.at(lineIndex + 1));
208+
} else {
209+
const int underlineWidth = std::max(
210+
underline.markerCount, std::max(static_cast<int>(titleText.size()), 3));
211+
const QChar marker =
212+
(newHeadingLevel == 1) ? QLatin1Char('=') : QLatin1Char('-');
213+
updatedLines.append(QString(underline.leadingSpaces, QLatin1Char(' ')) +
214+
QString(underlineWidth, marker));
215+
}
216+
} else {
217+
updatedLines.append(line.left(titleIndent) +
218+
QString(newHeadingLevel, QLatin1Char('#')) +
219+
QStringLiteral(" ") + titleText);
220+
}
221+
222+
++lineIndex;
223+
continue;
224+
}
225+
}
226+
227+
updatedLines.append(line);
228+
}
229+
230+
return updatedLines.join(QLatin1Char('\n'));
231+
}
232+
74233
struct InnerSelectionCandidate {
75234
int innerStart = -1;
76235
int innerEnd = -1;
@@ -1346,6 +1505,15 @@ bool QOwnNotesMarkdownTextEdit::replaceFullLineSelection(const QString &text) {
13461505
return true;
13471506
}
13481507

1508+
bool QOwnNotesMarkdownTextEdit::changeHeadingDepthOfSelection(const int levelDelta) {
1509+
QTextCursor cursor = fullLineSelectionCursor();
1510+
if (!cursor.hasSelection()) {
1511+
return false;
1512+
}
1513+
1514+
return replaceFullLineSelection(changeHeadingDepth(cursor.selectedText(), levelDelta));
1515+
}
1516+
13491517
QMargins QOwnNotesMarkdownTextEdit::viewportMargins() {
13501518
return QMarkdownTextEdit::viewportMargins();
13511519
}
@@ -2352,6 +2520,19 @@ void QOwnNotesMarkdownTextEdit::onContextMenu(QPoint pos) {
23522520
Utils::ListUtils::orderCheckboxes(fullLineSelectionCursor().selectedText()));
23532521
});
23542522

2523+
QMenu *markdownOperationsMenu = menu->addMenu(tr("Markdown operations"));
2524+
markdownOperationsMenu->setEnabled(isAllowNoteEditing);
2525+
2526+
QAction *increaseHeadingDepthAction =
2527+
markdownOperationsMenu->addAction(tr("Increase heading depth"));
2528+
connect(increaseHeadingDepthAction, &QAction::triggered, this,
2529+
[this]() { changeHeadingDepthOfSelection(1); });
2530+
2531+
QAction *decreaseHeadingDepthAction =
2532+
markdownOperationsMenu->addAction(tr("Decrease heading depth"));
2533+
connect(decreaseHeadingDepthAction, &QAction::triggered, this,
2534+
[this]() { changeHeadingDepthOfSelection(-1); });
2535+
23552536
menu->addAction(MainWindow::instance()->searchTextOnWebAction());
23562537
menu->addAction(MainWindow::instance()->findNoteAction());
23572538
}

src/widgets/qownnotesmarkdowntextedit.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ class QOwnNotesMarkdownTextEdit : public QMarkdownTextEdit {
173173
void onContextMenu(QPoint pos);
174174
QTextCursor fullLineSelectionCursor() const;
175175
bool replaceFullLineSelection(const QString &text);
176+
bool changeHeadingDepthOfSelection(int levelDelta);
176177

177178
void overrideFontSizeStyle(int fontSize);
178179

0 commit comments

Comments
 (0)