4545#include < QVariant>
4646#include < QVector>
4747#include < QWidgetAction>
48+ #include < QtGlobal>
4849#include < algorithm>
4950
5051#include " entities/notefolder.h"
@@ -71,6 +72,158 @@ constexpr int kMarkdownLspDiagnosticProperty = QTextFormat::UserProperty + 1;
7172constexpr int kFoldIndicatorPadding = 4 ;
7273QHash<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 ' ), Qt::KeepEmptyParts);
167+ QStringList updatedLines;
168+ updatedLines.reserve (lines.size ());
169+
170+ for (int lineIndex = 0 ; lineIndex < lines.size (); ++lineIndex) {
171+ const QString &line = lines.at (lineIndex);
172+
173+ int headingMarkerStart = 0 ;
174+ int headingMarkerEnd = 0 ;
175+ const int atxLevel = atxHeadingLevel (line, &headingMarkerStart, &headingMarkerEnd);
176+ if (atxLevel > 0 ) {
177+ const int newHeadingLevel = boundedHeadingLevel (atxLevel, levelDelta);
178+ if (newHeadingLevel == atxLevel) {
179+ updatedLines.append (line);
180+ continue ;
181+ }
182+
183+ updatedLines.append (line.left (headingMarkerStart) +
184+ QString (newHeadingLevel, QLatin1Char (' #' )) +
185+ line.mid (headingMarkerEnd));
186+ continue ;
187+ }
188+
189+ if (lineIndex + 1 < lines.size ()) {
190+ const SetextHeadingUnderline underline =
191+ parseSetextHeadingUnderline (lines.at (lineIndex + 1 ));
192+ const int titleIndent = markdownHeadingIndent (line);
193+ const QString titleText = line.mid (titleIndent).trimmed ();
194+
195+ if (underline.isValid () && !titleText.isEmpty ()) {
196+ const int newHeadingLevel = boundedHeadingLevel (underline.level , levelDelta);
197+ if (newHeadingLevel <= 2 ) {
198+ updatedLines.append (line);
199+
200+ if (newHeadingLevel == underline.level ) {
201+ updatedLines.append (lines.at (lineIndex + 1 ));
202+ } else {
203+ const int underlineWidth = std::max (
204+ underline.markerCount , std::max (static_cast <int >(titleText.size ()), 3 ));
205+ const QChar marker =
206+ (newHeadingLevel == 1 ) ? QLatin1Char (' =' ) : QLatin1Char (' -' );
207+ updatedLines.append (QString (underline.leadingSpaces , QLatin1Char (' ' )) +
208+ QString (underlineWidth, marker));
209+ }
210+ } else {
211+ updatedLines.append (line.left (titleIndent) +
212+ QString (newHeadingLevel, QLatin1Char (' #' )) +
213+ QStringLiteral (" " ) + titleText);
214+ }
215+
216+ ++lineIndex;
217+ continue ;
218+ }
219+ }
220+
221+ updatedLines.append (line);
222+ }
223+
224+ return updatedLines.join (QLatin1Char (' \n ' ));
225+ }
226+
74227struct InnerSelectionCandidate {
75228 int innerStart = -1 ;
76229 int innerEnd = -1 ;
@@ -1346,6 +1499,15 @@ bool QOwnNotesMarkdownTextEdit::replaceFullLineSelection(const QString &text) {
13461499 return true ;
13471500}
13481501
1502+ bool QOwnNotesMarkdownTextEdit::changeHeadingDepthOfSelection (const int levelDelta) {
1503+ QTextCursor cursor = fullLineSelectionCursor ();
1504+ if (!cursor.hasSelection ()) {
1505+ return false ;
1506+ }
1507+
1508+ return replaceFullLineSelection (changeHeadingDepth (cursor.selectedText (), levelDelta));
1509+ }
1510+
13491511QMargins QOwnNotesMarkdownTextEdit::viewportMargins () {
13501512 return QMarkdownTextEdit::viewportMargins ();
13511513}
@@ -2352,6 +2514,19 @@ void QOwnNotesMarkdownTextEdit::onContextMenu(QPoint pos) {
23522514 Utils::ListUtils::orderCheckboxes (fullLineSelectionCursor ().selectedText ()));
23532515 });
23542516
2517+ QMenu *markdownOperationsMenu = menu->addMenu (tr (" Markdown operations" ));
2518+ markdownOperationsMenu->setEnabled (isAllowNoteEditing);
2519+
2520+ QAction *increaseHeadingDepthAction =
2521+ markdownOperationsMenu->addAction (tr (" Increase heading depth" ));
2522+ connect (increaseHeadingDepthAction, &QAction::triggered, this ,
2523+ [this ]() { changeHeadingDepthOfSelection (1 ); });
2524+
2525+ QAction *decreaseHeadingDepthAction =
2526+ markdownOperationsMenu->addAction (tr (" Decrease heading depth" ));
2527+ connect (decreaseHeadingDepthAction, &QAction::triggered, this ,
2528+ [this ]() { changeHeadingDepthOfSelection (-1 ); });
2529+
23552530 menu->addAction (MainWindow::instance ()->searchTextOnWebAction ());
23562531 menu->addAction (MainWindow::instance ()->findNoteAction ());
23572532 }
0 commit comments