On 2025-07-27  18:51, Ihor Radchenko wrote:
> Jens Schmidt <jschmidt4...@vodafonemail.de> writes:
> 
>> It's been a while since my question on this mailing list about block
>> boundaries [1].  For my private code I came up with the function below
>> to both get and test on all sorts of block boundaries.  Like this:
>>
>>   (and-let* ((element (or element (org-element-at-point)))
>>              ;; returns nil if point is outside of markup
>>              (boundaries (org-element-boundaries element 'markup t))
>>              (start (car boundaries))
>>              (end (cdr boundaries)))
>>     ...)
>>
>> API and code are surely rather sound than ingenious, but if you
>> consider it (or some variation of it) useful for inclusion into Org
>> mode, just let me know, I'd prepare a proper patch plus tests plus ...
>>
>> The second part of the code is for interactive testing only.
>>
>> If you feel I should provide examples where this could be actually
>> used in Org's code base, just let me know - I can set up some example
>> patches as well.
> 
> Having example would help.
> [...]

Sorry for the late reply.  Have been busy and I wanted to provide
real-life examples.  See attached patch.

Some general notes:

- This is not a "real" patch I'm asking to include, just an example.
  In particular, I'm not asking to use the new function to replace
  existing code.

- The modifications are not always one-to-one, and I haven't checked
  them for bug-compatibility.

- But in general, I hope they show what `org-element-boundaries' can
  do.  At least I find the new code more readable, but that's in the
  eye of the beholder, as always.

- To find the examples, I searched (rather lazily and only in two or
  three sources) for "skip-syntax-backward".  These are usually good
  replacement candidates.

Please let me know what you think.

Thanks, as always, for your work as Org maintainer!

Jens
From 381737e75a02fafb40f4cd766ffe54a0c0e2c4f7 Mon Sep 17 00:00:00 2001
From: Jens Schmidt <farb...@vodafonemail.de>
Date: Fri, 15 Aug 2025 23:04:29 +0200
Subject: [PATCH] Provide `org-element-boundaries' and nonbinding, real-life
 examples.

From: Jens Schmidt <jschmidt4...@vodafonemail.de>

---
 lisp/ob-core.el     |  79 ++++++++++++++++---------------
 lisp/org-element.el | 113 ++++++++++++++++++++++++++++++++++++++++++++
 lisp/org.el         |  51 +++++++-------------
 3 files changed, 171 insertions(+), 72 deletions(-)

diff --git a/lisp/ob-core.el b/lisp/ob-core.el
index 326a9a857..bef28fce3 100644
--- a/lisp/ob-core.el
+++ b/lisp/ob-core.el
@@ -1984,14 +1984,13 @@ If the point is not on a source block or within blank lines after an
 src block, then return nil."
   (let ((element (or src-block (org-element-at-point))))
     (when (org-element-type-p element 'src-block)
-      (let ((end (org-element-end element)))
-	(org-with-wide-buffer
-	 ;; Ensure point is not on a blank line after the block.
-	 (forward-line 0)
-	 (skip-chars-forward " \r\t\n" end)
-	 (when (< (point) end)
-	   (prog1 (goto-char (org-element-post-affiliated element))
-	     (looking-at org-babel-src-block-regexp))))))))
+      ;; Here (and further down) I made a feeble attempt to get rid of
+      ;; references to `org-babel-src-block-regexp' and this function
+      ;; itself.  With some more effort it should be possible...
+      (let ((boundaries (org-element-boundaries element 'markup)))
+	(when (<= (point) (cdr boundaries))
+	  (prog1 (goto-char (car boundaries))
+	    (looking-at org-babel-src-block-regexp)))))))
 
 ;;;###autoload
 (defun org-babel-goto-src-block-head ()
@@ -2120,13 +2119,20 @@ With optional prefix argument ARG, jump backward ARG many source blocks."
 (defun org-babel-mark-block ()
   "Mark current source block."
   (interactive)
-  (let ((head (org-babel-where-is-src-block-head)))
-    (when head
-      (save-excursion
-        (goto-char head)
-        (looking-at org-babel-src-block-regexp))
-      (push-mark (match-end 5) nil t)
-      (goto-char (match-beginning 5)))))
+  ;; This requires two calls to `org-element-boundaries' on the
+  ;; element, one to check whether we are in the range of its markup
+  ;; and one to determine the boundaries of its contents.
+  ;;
+  ;; As a perk, one gets a user error instead of a no-op if not on a
+  ;; source block.
+  (if-let* ((element (org-element-at-point))
+            ((org-element-type-p element 'src-block))
+            ((org-element-boundaries element 'markup 'inclusive))
+            (contents (org-element-boundaries element 'contents)))
+      (progn
+        (push-mark (1+ (cdr contents)) nil t)
+        (goto-char (car contents)))
+    (user-error "Not in a source block")))
 
 (defun org-babel-demarcate-block (&optional arg)
   "Wrap or split the code in an active region or at point.
@@ -2988,28 +2994,27 @@ used as a string to be appended to #+begin_example line."
 
 (defun org-babel-update-block-body (new-body)
   "Update the body of the current code block to NEW-BODY."
-  (let ((element (org-element-at-point)))
-    (unless (org-element-type-p element 'src-block)
-      (error "Not in a source block"))
-    (goto-char (org-babel-where-is-src-block-head element))
-    (let* ((ind (org-current-text-indentation))
-	   (body-start (line-beginning-position 2))
-	   (body (org-element-normalize-string
-		  (if (org-src-preserve-indentation-p element) new-body
-		    (with-temp-buffer
-		      (insert (org-remove-indentation new-body))
-		      (indent-rigidly
-		       (point-min)
-		       (point-max)
-		       (+ ind org-edit-src-content-indentation))
-		      (buffer-string))))))
-      (delete-region body-start
-		     (org-with-wide-buffer
-		      (goto-char (org-element-end element))
-		      (skip-chars-backward " \t\n")
-		      (line-beginning-position)))
-      (goto-char body-start)
-      (insert body))))
+  ;; Again, two calls to `org-element-boundaries'.
+  (if-let* ((element (org-element-at-point))
+            ((org-element-type-p element 'src-block))
+            (markup (org-element-boundaries element 'markup 'inclusive)))
+      (let* ((ind (progn
+                    (goto-char (car markup))
+                    (org-current-text-indentation)))
+	     (body (org-element-normalize-string
+		    (if (org-src-preserve-indentation-p element) new-body
+		      (with-temp-buffer
+		        (insert (org-remove-indentation new-body))
+		        (indent-rigidly
+		         (point-min)
+		         (point-max)
+		         (+ ind org-edit-src-content-indentation))
+		        (buffer-string)))))
+             (contents (org-element-boundaries element 'contents)))
+        (delete-region (car contents) (1+ (cdr contents)))
+        (goto-char (car contents))
+        (insert body))
+    (error "Not in a source block")))
 
 (defun org-babel-merge-params (&rest alists)
   "Combine all parameter association lists in ALISTS.
diff --git a/lisp/org-element.el b/lisp/org-element.el
index 062141fce..f5a0269db 100644
--- a/lisp/org-element.el
+++ b/lisp/org-element.el
@@ -8739,6 +8739,119 @@ end of ELEM-A."
         (org-fold-core-regions (cdr folds) :relative beg-A)
         (goto-char (org-element-end elem-B))))))
 
+;;;###autoload
+(defun org-element-boundaries (element boundary-type &optional point-test)
+  "Return a cons (START . END) of the boundaries of ELEMENT.
+Determine START and END depending on BOUNDARY-TYPE, which can be any of the
+following symbols:
+
+`element', `gross-elt': Return the outer element boundaries, which include
+leading affiliated keywords and trailing blank lines [1].
+`markup', `net-elt': Return the boundaries of the element sans affiliated
+keywords and trailing blank lines.  For block-like elements this coincides
+with the markup boundaries [2].
+
+For block-like elements, that is, elements having separate markup lines
+like source blocks, the following boundary types are also available:
+
+`contents', `gross-contents': Return the boundaries of the block contents
+[3].
+`net-contents': Return the boundaries of the block contents sans leading
+and trailing whitespace (' \\t\\n\\r') [4].
+
+Here is an example block that shows the boundaries as returned by this
+function (leading and trailing blanks denoted with underscores):
+
+[1...#+name: foo
+[2...#+begin_example
+[3...__[4...foo
+bar
+__baz...4]__
+__...3]
+#+end_example__...2]
+__...1]
+Bar.
+
+BOUNDARY-TYPE can also be a cons (START-TYPE . END-TYPE) to determine START
+and END with respect to different boundary types.
+
+For certain combinations of contents-based boundary types and degenerate
+blocks, the literal interpretation of above rules could result in END being
+smaller than START.  In these cases this function returns START and END
+equal to CONTENTS-START, where CONTENTS-START is the position of the
+beginning of line after the opening markup line.
+
+Except for that special case and boundary type `net-contents' in general,
+START is always at the beginning and END is always at the end of a line.
+START is always smaller than or equal to END.
+
+If optional parameter POINT-TEST equals `exclusive', this function returns
+nil instead of the cons if point is not exclusively/strictly between START
+and END.  Any other non-nil value changes that to a non-exclusive test,
+where point is allowed to equal START or END.  POINT-TEST can also be a
+cons (START-TEST . END-TEST) to test START or END vs. point with respect to
+different values of strictness."
+  (let* ((start-type (or (car-safe boundary-type) boundary-type))
+         (end-type   (or (cdr-safe boundary-type) boundary-type))
+         (start-test (if (consp point-test) (car point-test) point-test))
+         (end-test   (if (consp point-test) (cdr point-test) point-test))
+         (point      (point))
+         start end contents-start)
+    (org-with-wide-buffer
+     (setq start
+           (pcase-exhaustive start-type
+             ((or 'element 'gross-elt)
+              (org-element-begin element))
+             ((or 'markup 'net-elt)
+              (org-element-post-affiliated element))
+             ((or 'contents 'gross-contents)
+              (goto-char (org-element-post-affiliated element))
+              (forward-line 1)
+              (setq contents-start (point))
+              (point))
+             ('net-contents
+              (goto-char (org-element-post-affiliated element))
+              (forward-line 1)
+              (setq contents-start (point))
+              (skip-chars-forward " \t\n\r")
+              (point))))
+     (setq end
+           (progn
+             (goto-char (org-element-end element))
+             (pcase-exhaustive end-type
+               ;; adjust element end depending on the
+               ;; surrounding or following element
+               ((or 'element 'gross-elt)
+                (if (looking-at "[ \t\n\r]*$")
+                    (line-end-position 1)
+                  (line-end-position 0)))
+               ((or 'markup 'net-elt)
+                (skip-chars-backward " \t\n\r")
+                (line-end-position 1))
+               ((or 'contents 'gross-contents)
+                (skip-chars-backward " \t\n\r")
+                (line-end-position 0))
+               ('net-contents
+                (skip-chars-backward " \t\n\r")
+                (forward-line 0)
+                (skip-chars-backward " \t\n\r")
+                (point)))))
+     ;; handle empty or whitespace-only blocks.  CONTENTS-START
+     ;; is nil if START-TYPE is not contents-based.  Which is OK,
+     ;; since in that case END is larger than START.
+     (when (< end start)
+       (setq start contents-start
+             end   contents-start))
+     (and (cond ((not start-test))
+                ((eq start-test 'exclusive)
+                 (< start point))
+                ((<= start point)))
+          (cond ((not end-test))
+                ((eq end-test 'exclusive)
+                 (< point end))
+                ((<= point end)))
+          (cons start end)))))
+
 (provide 'org-element)
 
 ;; Local variables:
diff --git a/lisp/org.el b/lisp/org.el
index 65abfbe1a..46cdca0bd 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -7194,12 +7194,12 @@ Assume point is at a heading or an inlinetask beginning."
 	   (indent-line-to (+ (current-indentation) diff))
 	   (forward-line 0)
 	   (or (and (looking-at-p "[ \t]*#\\+BEGIN_\\(EXAMPLE\\|SRC\\)")
-		    (let ((e (org-element-at-point)))
-		      (and (org-src-preserve-indentation-p e)
-			   (goto-char (org-element-end e))
-			   (progn (skip-chars-backward " \r\t\n")
-				  (forward-line 0)
-				  t))))
+                    ;; Here I need only one end of the boundary.  To
+                    ;; me it still seems more intelligible, anyway.
+		    (let* ((e (org-element-at-point))
+                           (c (org-element-boundaries e 'contents)))
+		      (when (org-src-preserve-indentation-p e)
+			(goto-char (1+ (cdr c))))))
 	       (forward-line)))))))))
 
 (defun org-convert-to-odd-levels ()
@@ -11119,14 +11119,12 @@ POS may also be a marker."
   (with-current-buffer (if (markerp pos) (marker-buffer pos) (current-buffer))
     (org-with-wide-buffer
      (goto-char pos)
-     (let ((drawer (org-element-at-point)))
+     (let ((drawer (org-element-at-point)) markup)
        (when (and (org-element-type-p drawer '(drawer property-drawer))
 		  (not (org-element-contents-begin drawer)))
-	 (delete-region (org-element-begin drawer)
-			(progn (goto-char (org-element-end drawer))
-			       (skip-chars-backward " \r\t\n")
-			       (forward-line)
-			       (point))))))))
+         ;; An easy one ...
+         (setq markup (org-element-boundaries drawer 'markup))
+	 (delete-region (car markup) (1+ (cdr markup))))))))
 
 (defvar org-ts-type nil)
 (defun org-sparse-tree (&optional arg type)
@@ -19039,14 +19037,9 @@ part of a source block.
 
 When ELEMENT is provided, it is considered to be element at point."
   (save-match-data (setq element (or element (org-element-at-point))))
-  (when (org-element-type-p element 'src-block)
-    (or (not inside)
-        (not (or (<= (line-beginning-position)
-                  (org-element-post-affiliated element))
-               (>= (line-end-position)
-                  (org-with-point-at (org-element-end element)
-                    (skip-chars-backward " \t\n\r")
-                    (point))))))))
+  ;; ... and an even more easy one ...
+  (and (org-element-type-p element 'src-block)
+       (org-element-boundaries element (if inside 'contents 'element) 'inclusive)))
 
 (defun org-context ()
   "Return a list of contexts of the current cursor position.
@@ -19480,10 +19473,7 @@ ELEMENT."
                     (memq type '(comment-block example-block export-block
                                                src-block verse-block)))
 		(let ((cend (or (org-element-contents-end element)
-                                (org-with-wide-buffer
-			         (goto-char (org-element-end element))
-			         (skip-chars-backward " \r\t\n")
-			         (line-beginning-position)))))
+                                (1+ (cdr (org-element-boundaries element 'contents))))))
 		  (and cend (<= cend pos))))
 	   ;; As a special case, if point is at the end of a footnote
 	   ;; definition or an item, indent like the very last element
@@ -19576,20 +19566,11 @@ Also align node properties according to `org-property-format'."
 		     (org-element-post-affiliated element)))
 	     nil)
 	    ((and (eq type 'latex-environment)
-		  (>= (point) (org-element-post-affiliated element))
-		  (< (point)
-		     (org-with-point-at (org-element-end element)
-		       (skip-chars-backward " \t\n")
-		       (line-beginning-position 2))))
+                  (org-element-boundaries element 'markup 'inclusive))
 	     nil)
 	    ((and (eq type 'src-block)
 		  org-src-tab-acts-natively
-		  (> (line-beginning-position)
-		     (org-element-post-affiliated element))
-		  (< (line-beginning-position)
-		     (org-with-point-at (org-element-end element)
-		       (skip-chars-backward " \t\n")
-		       (line-beginning-position))))
+                  (org-element-boundaries element 'contents 'inclusive))
              (let ((block-content-ind
                     (when (not (org-src-preserve-indentation-p element))
                       (org-with-point-at (org-element-property :begin element)
-- 
2.39.5

Reply via email to